Skip to content

Commit

Permalink
#29: Add code folding for intentionAction XML tags in plugin descript…
Browse files Browse the repository at this point in the history
…or files
  • Loading branch information
picimako committed Sep 15, 2023
1 parent 47dc3a7 commit 7092bc3
Show file tree
Hide file tree
Showing 15 changed files with 522 additions and 81 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
## [Unreleased]

## [0.6.0]
### Added
- [#29](https://github.com/picimako/just-kitting/issues/29): Added code folding for `extensions.intentAction` tags in plugin descriptor files.

### Changed
- [#29](https://github.com/picimako/just-kitting/issues/29): Code folding of `extensions.localInspection` and `extensions.globalInspection` tags
is extended with resource bundle message resolution with fallback logic based on where bundle names are specified in the EP or in the plugin descriptor file.
Expand Down
Binary file added docs/assets/intention_action_tag_folding.PNG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 30 additions & 3 deletions docs/plugin_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* [XML tag folding in plugin descriptor files](#xml-tag-folding-in-plugin-descriptor-files)
* [Supported tags](#supported-tags)
* [extensions.localInspection / extensions.globalInspection](#extensionslocalinspection--extensionsglobalinspection)
* [extensions.intentionAction](#extensionsintentionaction)
<!-- TOC -->

## Configuration file diffs with the IntelliJ Platform Plugin Template
Expand Down Expand Up @@ -38,7 +39,7 @@ If the remote contents cannot be downloaded, a balloon is displayed, and a log e

## XML tag folding in plugin descriptor files

![](https://img.shields.io/badge/codefolding-orange) ![](https://img.shields.io/badge/since-0.4.0-blue) [![](https://img.shields.io/badge/implementation-PluginDescriptorTagsFoldingBuilder-blue)](../src/main/java/com/picimako/justkitting/codefolding/plugindescriptor/PluginDescriptorTagsFoldingBuilder.java)
![](https://img.shields.io/badge/codefolding-orange) [![](https://img.shields.io/badge/implementation-PluginDescriptorTagsFoldingBuilder-blue)](../src/main/java/com/picimako/justkitting/codefolding/plugindescriptor/PluginDescriptorTagsFoldingBuilder.java)

There are certain extensions, and XML tags in general, in `plugin.xml` and other plugin descriptor files that can hold a lot of information.
Having many such tags can make it more difficult for users to parse them, find the one they are looking for, or just scroll through them.
Expand All @@ -50,8 +51,10 @@ under <kbd>Settings</kbd> > <kbd>Editor</kbd> > <kbd>General</kbd> > <kbd>Code F

#### extensions.localInspection / extensions.globalInspection

The `<localInspection>` and `<globalInspection>` tags within `<extensions defaultExtensionNs="com.intellij">` folds in the form of **'for [language] at [path]'**,
and supports the following attributes for folding:
![](https://img.shields.io/badge/since-0.4.0-blue) [![](https://img.shields.io/badge/implementation-InspectionFolder-blue)](../src/main/java/com/picimako/justkitting/codefolding/plugindescriptor/InspectionFolder.java)

The `<localInspection>` and `<globalInspection>` tags within `<extensions defaultExtensionNs="com.intellij">` fold in the form of **'for [language] at [path]'**,
and support the following attributes for folding:

| Attribute | Attribute value example | Placeholder text |
|----------------|--------------------------|------------------------------------------|
Expand All @@ -75,3 +78,27 @@ Resource bundle keys are resolved according to their resolution fallback chain:
**Example:**

![local_inspection_tag_folding](assets/local_inspection_tag_folding.png)

#### extensions.intentionAction

![](https://img.shields.io/badge/since-0.6.0-blue) [![](https://img.shields.io/badge/implementation-IntentionActionFolder-blue)](../src/main/java/com/picimako/justkitting/codefolding/plugindescriptor/IntentionActionFolder.java)

The `<intentionAction>` tags within `<extensions defaultExtensionNs="com.intellij">` fold in the form of
** for [language] at [category] / [family name or class name] ...**.

The `<intentionAction>` tags are folded regardless of what subtags of them are present and in what order.

If the language is not configured (e.g. not available in earlier platform versions) the tag folds without the language as
** at [category] / [family name or class name] ...**

**Class names:**

The `<className>` tag is resolved to the short name of the class, or if the plugin, whose project is currently open,
is also installed in the IDE, thus it has the intention action classes registered, it evaluates their `getFamilyName()`
methods to provide a better placeholder text.

For now, the plugin cannot evaluate the family name of `IntentionAction` classes in the currently open project.

**Example:**

![intention_action_tag_folding](assets/intention_action_tag_folding.PNG)
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,13 @@

package com.picimako.justkitting.codefolding.plugindescriptor;

import static com.intellij.openapi.util.text.StringUtil.isEmpty;

import com.intellij.lang.folding.FoldingDescriptor;
import com.intellij.lang.properties.IProperty;
import com.intellij.lang.properties.ResourceBundleReference;
import com.intellij.lang.properties.psi.PropertiesFile;
import com.intellij.openapi.editor.FoldingGroup;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiReferenceService;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlElement;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.SmartList;
Expand All @@ -23,20 +19,16 @@

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.UnaryOperator;

import static com.intellij.openapi.util.text.StringUtil.defaultIfEmpty;
import static com.intellij.openapi.util.text.StringUtil.isEmpty;

/**
* Handles the folding and placeholder text creation for the {@code localInspection} and {@code globalInspection} extensions.
*
* @since 0.5.0
*/
@RequiredArgsConstructor
public final class InspectionFolder implements PluginDescriptorTagFolder {
public final class InspectionFolder extends PluginDescriptorTagFolder {
/**
* Folding happens only when at least one these attributes is specified. Otherwise, there is nothing to fold.
*/
Expand All @@ -55,8 +47,7 @@ public void createFolding(XmlTag extensions, @NotNull List<FoldingDescriptor> de
var attributes = localInspection.getAttributes();

//If no attribute or no foldable attribute, continue processing the rest of the localInspection tags
if (attributes.length == 0 || !hasAtLeastOneFoldableAttribute(attributes))
continue;
if (!isEligibleForFolding(localInspection)) continue;

descriptors.add(new FoldingDescriptor(
localInspection.getNode(),
Expand All @@ -71,8 +62,9 @@ public void createFolding(XmlTag extensions, @NotNull List<FoldingDescriptor> de
}

@Override
public boolean hasAtLeastOneFoldableAttribute(XmlAttribute[] attributes) {
return Arrays.stream(attributes).anyMatch(attribute -> FOLDABLE_LOCAL_INSPECTION_ATTRIBUTES.contains(attribute.getName()));
public boolean isEligibleForFolding(XmlTag localInspection) {
var attributes = localInspection.getAttributes();
return attributes.length != 0 && Arrays.stream(attributes).anyMatch(attribute -> FOLDABLE_LOCAL_INSPECTION_ATTRIBUTES.contains(attribute.getName()));
}

@Override
Expand Down Expand Up @@ -132,7 +124,8 @@ private static String buildPath(XmlTag localInspection) {
*/
pathElements.add(formatAttributeValue(localInspection,
"groupPath", "groupPathKey",
Bundle.GROUP_BUNDLE, groupPath -> groupPath.replace(",", " / ")));
Bundle.GROUP_BUNDLE, groupPath -> groupPath.replace(",", " / "),
false));

/*
* For the attribute [groupName="Group name"], the placeholder text will be 'Group name'.
Expand All @@ -141,7 +134,8 @@ private static String buildPath(XmlTag localInspection) {
*/
pathElements.add(formatAttributeValue(localInspection,
"groupName", "groupKey",
Bundle.GROUP_BUNDLE, groupName -> groupName));
Bundle.GROUP_BUNDLE, groupName -> groupName,
false));

/*
* For the attribute [displayName="Some inspection title"], the placeholder text will be 'Some inspection title'.
Expand All @@ -150,7 +144,8 @@ private static String buildPath(XmlTag localInspection) {
*/
pathElements.add(formatAttributeValue(localInspection,
"displayName", "key",
Bundle.BUNDLE, displayName -> "'" + displayName + "'"));
Bundle.BUNDLE, displayName -> "'" + displayName + "'",
true));

/*
* Path elements are joined together with a forward-slash with spaces around it.
Expand All @@ -160,13 +155,6 @@ private static String buildPath(XmlTag localInspection) {
return !path.isBlank() ? "at " + path : "";
}

/**
* Returns the argument value if it is non-null and non-empty, otherwise, returns empty string.
*/
private static String getOrEmpty(@Nullable String value) {
return defaultIfEmpty(value, "");
}

/**
* Returns a formatted version of the specified attributes' values.
*
Expand All @@ -178,14 +166,17 @@ private static String getOrEmpty(@Nullable String value) {
*/
private static String formatAttributeValue(XmlTag localInspection, String literalAttrName, String keyAttrName,
Bundle primaryBundle,
UnaryOperator<String> literalValueFormatter) {
UnaryOperator<String> literalValueFormatter,
boolean wrapInSingleQuotes) {
String displayName = localInspection.getAttributeValue(literalAttrName);
return !isEmpty(displayName)
? literalValueFormatter.apply(displayName)
: resolveMessageFromBundle(localInspection, keyAttrName, primaryBundle);
: resolveMessageFromBundle(localInspection, keyAttrName, primaryBundle, wrapInSingleQuotes);
}

private static String resolveMessageFromBundle(XmlTag localInspection, String keyAttrName, @NotNull InspectionFolder.Bundle primaryBundle) {
private static String resolveMessageFromBundle(XmlTag localInspection, String keyAttrName,
@NotNull InspectionFolder.Bundle primaryBundle,
boolean wrapInSingleQuotes) {
var bundle = primaryBundle;
while (bundle != Bundle.NONE) {
//GROUP_BUNDLE or BUNDLE
Expand All @@ -198,7 +189,7 @@ private static String resolveMessageFromBundle(XmlTag localInspection, String ke
//For now, it always takes the first ResourceBundleReference, regardless if there are e.g. localizations for more languages
var resolved = findFirstBundleReference(references);
if (resolved.isPresent() && resolved.get() instanceof PropertiesFile propertiesFile) {
return findMessageInPropertiesOrDefaultToKey(propertiesFile, localInspection.getAttributeValue(keyAttrName));
return findMessageInPropertiesOrDefaultToKey(propertiesFile, localInspection.getAttributeValue(keyAttrName), wrapInSingleQuotes);
}
}
bundle = bundle.fallbackTo;
Expand All @@ -216,41 +207,14 @@ private static String resolveMessageFromBundle(XmlTag localInspection, String ke
//For now, it always takes the first ResourceBundleReference, regardless if there are e.g. localizations for more languages
return findFirstBundleReference(getReferences(resourceBundleTag))
.map(resolved -> resolved instanceof PropertiesFile propertiesFile
? findMessageInPropertiesOrDefaultToKey(propertiesFile, localInspection.getAttributeValue(keyAttrName))
? findMessageInPropertiesOrDefaultToKey(propertiesFile, localInspection.getAttributeValue(keyAttrName), wrapInSingleQuotes)
: null)
.orElseGet(() -> asKey(localInspection.getAttributeValue(keyAttrName)));
}
}
return asKey(localInspection.getAttributeValue(keyAttrName));
}

@NotNull
private static Optional<PsiElement> findFirstBundleReference(List<PsiReference> references) {
return references.stream()
.filter(ResourceBundleReference.class::isInstance)
.findFirst()
.map(PsiReference::resolve);
}

@NotNull
private static List<PsiReference> getReferences(XmlElement element) {
return PsiReferenceService.getService().getReferences(element, PsiReferenceService.Hints.NO_HINTS);
}

private static String findMessageInPropertiesOrDefaultToKey(PropertiesFile propertiesFile, @Nullable String messageKey) {
return Optional.ofNullable(messageKey)
.map(propertiesFile::findPropertyByKey)
.map(IProperty::getValue)
.orElseGet(() -> asKey(messageKey));
}

/**
* Encloses the argument attribute value in curly braces. For example: {@code some.key} becomes {@code {some.key}}.
*/
private static String asKey(@Nullable String attributeValue) {
return getOrEmpty(attributeValue != null && !attributeValue.isBlank() ? "{" + attributeValue + "}" : "");
}

/**
* Resource bundle locations used by inspection EPs.
*/
Expand Down
Loading

0 comments on commit 7092bc3

Please sign in to comment.