Skip to content

Commit

Permalink
#29: Add resource bundle key resolution logic to inspection EP tag fo…
Browse files Browse the repository at this point in the history
…lding
  • Loading branch information
picimako committed Sep 4, 2023
1 parent 81ec976 commit 3f0f2f4
Show file tree
Hide file tree
Showing 18 changed files with 344 additions and 24 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

## [Unreleased]

## [0.6.0]
### 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.

## [0.5.0]
### Added
- [#29](https://github.com/picimako/just-kitting/issues/29): Added code folding for `extensions.globalInspection` tags in plugin descriptor files.
Expand Down
39 changes: 25 additions & 14 deletions docs/plugin_configuration.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Plugin Configuration

<!-- TOC -->
* [Configuration file diffs with the IntelliJ Platform Plugin Template](#configuration-file-diffs-with-the-intellij-platform-plugin-template)
* [XML tag folding in plugin descriptor files](#xml-tag-folding-in-plugin-descriptor-files)
* [Supported tags](#supported-tags)
* [extensions.localInspection / extensions.globalInspection](#extensionslocalinspection--extensionsglobalinspection)
<!-- TOC -->

## Configuration file diffs with the IntelliJ Platform Plugin Template

![](https://img.shields.io/badge/diffview-orange) ![](https://img.shields.io/badge/since-0.3.0-blue) [![](https://img.shields.io/badge/implementation-CompareConfigFileWithPluginTemplateAction-blue)](../src/main/java/com/picimako/justkitting/action/diff/CompareConfigFileWithPluginTemplateAction.java)
Expand All @@ -11,7 +18,6 @@ and it opens a two-sided diff view comparing the local version of the file with
|------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
| ![compare_with_template_project_view_context_menu_action](assets/compare_with_template_project_view_context_menu_action.png) | ![compare_with_template_editor_context_menu_action](assets/compare_with_template_editor_context_menu_action.png) |


The action is available for the following configuration files:
- `build.gradle.kts`
- `gradle.properties`
Expand All @@ -32,7 +38,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/PluginDescriptorTagsFoldingBuilder.java)
![](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)

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 @@ -42,24 +48,29 @@ under <kbd>Settings</kbd> > <kbd>Editor</kbd> > <kbd>General</kbd> > <kbd>Code F

### Supported tags

#### extensions.localInspection
#### extensions.localInspection / extensions.globalInspection

The `<localInspection>` and `<globalInspection>` tag within `<extensions defaultExtensionNs="com.intellij">` folds in the form of **'for [language] at [path]'**,
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:

| Attribute | Attribute value example | Placeholder text |
|----------------|--------------------------|----------------------------|
| `language` | JAVA | for JAVA |
| `groupPath` | Group<br/>Group,Path | Group<br/>Group / Path |
| `groupPathKey` | group.path.key | {group.path.key} |
| `groupName` | Group Name | Group Name |
| `groupKey` | group.name.key | {group.name.key} |
| `displayName` | Looks for invalid things | 'Looks for invalid things' |
| `key` | display.name.key | {display.name.key} |
| Attribute | Attribute value example | Placeholder text |
|----------------|--------------------------|------------------------------------------|
| `language` | JAVA | *for JAVA* |
| `groupPath` | Group<br/>Group,Path | *Group*<br/>*Group / Path* |
| `groupPathKey` | group.path.key | *{group.path.key}* (when not resolved) |
| `groupName` | Group Name | *Group Name* |
| `groupKey` | group.name.key | *{group.name.key}* (when not resolved) |
| `displayName` | Looks for invalid things | *'Looks for invalid things'* |
| `key` | display.name.key | *{display.name.key}* (when not resolved) |

Resource bundle keys are resolved according to their resolution fallback chain:
- `key`: `bundle` attribute -> `<resource-bundle>` tag
- `groupPathKey`: `groupBundle` attribute -> `bundle` attribute -> `<resource-bundle>` tag
- `groupKey`: `groupBundle` attribute -> `bundle` attribute -> `<resource-bundle>` tag

**Notes:**
- group path comma delimiters are replaced with forward-slashes for better visuals
- the placeholder text for keys show the keys themselves enclosed in `{` and `}`. They are not resolved to the actual messages, for now.
- the placeholder text for keys show the keys themselves enclosed in `{` and `}`.

**Example:**

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ platformVersion = 2022.2

# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
platformPlugins = java,org.jetbrains.kotlin,DevKit
platformPlugins = java,org.jetbrains.kotlin,DevKit,com.intellij.properties:222.3345.108

javaVersion = 17

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,27 @@
package com.picimako.justkitting.codefolding.plugindescriptor;

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;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.devkit.util.DescriptorUtil;

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

Expand Down Expand Up @@ -120,21 +130,27 @@ private static String buildPath(XmlTag localInspection) {
* For the attribute [groupPathKey="some.bundle.key"], the placeholder text will be '{some.bundle.key}'.
* For the attributes [groupPath="Some,Path"] and [groupPathKey="some.bundle.key"], the placeholder text will be 'Some / Path'.
*/
pathElements.add(formatAttributeValue(localInspection, "groupPath", "groupPathKey", groupPath -> groupPath.replace(",", " / ")));
pathElements.add(formatAttributeValue(localInspection,
"groupPath", "groupPathKey",
Bundle.GROUP_BUNDLE, groupPath -> groupPath.replace(",", " / ")));

/*
* For the attribute [groupName="Group name"], the placeholder text will be 'Group name'.
* For the attribute [groupKey="some.bundle.key"], the placeholder text will be '{some.bundle.key}'.
* For the attributes [groupName="Group name"] and [groupPathKey="some.bundle.key"], the placeholder text will be 'Group name'.
*/
pathElements.add(formatAttributeValue(localInspection, "groupName", "groupKey", groupName -> groupName));
pathElements.add(formatAttributeValue(localInspection,
"groupName", "groupKey",
Bundle.GROUP_BUNDLE, groupName -> groupName));

/*
* For the attribute [displayName="Some inspection title"], the placeholder text will be 'Some inspection title'.
* For the attribute [groupPathKey="some.bundle.key"], the placeholder text will be '{some.bundle.key}'.
* For the attributes [displayName="Some inspection title"] and [key="some.bundle.key"], the placeholder text will be 'Some inspection title'.
*/
pathElements.add(formatAttributeValue(localInspection, "displayName", "key", displayName -> "'" + displayName + "'"));
pathElements.add(formatAttributeValue(localInspection,
"displayName", "key",
Bundle.BUNDLE, displayName -> "'" + displayName + "'"));

/*
* Path elements are joined together with a forward-slash with spaces around it.
Expand All @@ -157,19 +173,124 @@ private static String getOrEmpty(@Nullable String value) {
* @param localInspection the {@code localInspection} XML tag
* @param literalAttrName the attribute name for literal string version of the attribute
* @param keyAttrName the attribute name of the key-based counterpart of {@code literalAttrName}
* @param primaryBundle the bundle attribute name from which the fallback of message resolution starts
* @param literalValueFormatter if the value of the literal string attribute is used, it applies this formatting to it before returning
*/
private static String formatAttributeValue(XmlTag localInspection, String literalAttrName, String keyAttrName, UnaryOperator<String> literalValueFormatter) {
private static String formatAttributeValue(XmlTag localInspection, String literalAttrName, String keyAttrName,
Bundle primaryBundle,
UnaryOperator<String> literalValueFormatter) {
String displayName = localInspection.getAttributeValue(literalAttrName);
return !isEmpty(displayName)
? literalValueFormatter.apply(displayName)
: getOrEmpty(asKey(localInspection.getAttributeValue(keyAttrName)));
: resolveMessageFromBundle(localInspection, keyAttrName, primaryBundle);
}

private static String resolveMessageFromBundle(XmlTag localInspection, String keyAttrName, @NotNull InspectionFolder.Bundle primaryBundle) {
var bundle = primaryBundle;
while (bundle != Bundle.NONE) {
//GROUP_BUNDLE or BUNDLE
if (!bundle.attributeName.isEmpty()) {
var bundleAttr = localInspection.getAttribute(bundle.attributeName);
if (bundleAttr != null) {
var references = getReferences(bundleAttr.getValueElement());
if (references.isEmpty()) return asKey(localInspection.getAttributeValue(keyAttrName));

//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));
}
}
bundle = bundle.fallbackTo;
}
//PLUGIN_DESCRIPTOR: <resource-bundle>
else {
/*
* <idea-plugin>
* <resource-bundle>...</resource-bundle>
* </idea-plugin>
*/
var resourceBundleTag = DescriptorUtil.getIdeaPlugin((XmlFile) localInspection.getContainingFile()).getResourceBundle().getXmlTag();
if (resourceBundleTag == null) return asKey(localInspection.getAttributeValue(keyAttrName));

//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))
: 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 attributeValue != null && !attributeValue.isBlank() ? "{" + attributeValue + "}" : "";
return getOrEmpty(attributeValue != null && !attributeValue.isBlank() ? "{" + attributeValue + "}" : "");
}

/**
* Resource bundle locations used by inspection EPs.
*/
@RequiredArgsConstructor
private enum Bundle {
/**
* Used when none of the EP XML tag attribute, nor the {@code <resource-bundle>} tag is specified.
*/
NONE("", null),
/**
* <pre>
* &lt;idea-plugin>
* &lt;resource-bundle>...&lt;/resource-bundle>
* &lt;/idea-plugin>
* </pre>
*/
PLUGIN_DESCRIPTOR("", NONE),
/**
* <pre>
* &lt;localInspection bundle="message.JustKittingBundle" />
* </pre>
*/
BUNDLE("bundle", PLUGIN_DESCRIPTOR),
/**
* <pre>
* &lt;localInspection groupBundle="message.JustKittingBundle" />
* </pre>
*/
GROUP_BUNDLE("groupBundle", BUNDLE);

/**
* The XML tag attribute name associated with this bundle location. Empty string, if the location
* is not represented by an XML tag attribute.
*/
@NotNull
final String attributeName;
/**
* The location this bundle definition will fall back to, in case it is not specified.
*/
@Nullable
final InspectionFolder.Bundle fallbackTo;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//Copyright 2023 Tamás Balog. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.picimako.justkitting;

import com.intellij.testFramework.LightProjectDescriptor;
import org.jetbrains.annotations.NotNull;

/**
* Base class for tests that need to leverage main/ and test/ sources and resources folders in test data.
*/
public abstract class ContentRootsBasedJustKittingTestBase extends JustKittingTestBase {

@NotNull
@Override
protected LightProjectDescriptor getProjectDescriptor() {
return new ContentRootsProjectDescriptor();
}

@Override
protected void setUp() throws Exception {
super.setUp();
myFixture.copyDirectoryToProject("src", "");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//Copyright 2023 Tamás Balog. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.

package com.picimako.justkitting;

import com.intellij.openapi.module.Module;
import com.intellij.openapi.projectRoots.JavaSdk;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.roots.ContentEntry;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.jps.model.java.JavaResourceRootType;

/**
* Project descriptor that loads main/ and test/ sources and resources folders.
*/
public class ContentRootsProjectDescriptor extends DefaultLightProjectDescriptor {

@Override
public Sdk getSdk() {
return JavaSdk.getInstance().createJdk("Real JDK", System.getenv("JAVA_HOME"), false);
}

@Override
public void configureModule(@NotNull Module module, @NotNull ModifiableRootModel model, @NotNull ContentEntry contentEntry) {
super.configureModule(module, model, contentEntry);

contentEntry.clearSourceFolders();

String contentEntryUrl = contentEntry.getUrl();
// contentEntry.addSourceFolder(contentEntryUrl + "/main/java", JavaSourceRootType.SOURCE);
contentEntry.addSourceFolder(contentEntryUrl + "/main/resources", JavaResourceRootType.RESOURCE);
// contentEntry.addSourceFolder(contentEntryUrl + "/test/java", JavaSourceRootType.TEST_SOURCE);
// contentEntry.addSourceFolder(contentEntryUrl + "/test/resources", JavaResourceRootType.TEST_RESOURCE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ class GenerateStaticGetInstanceActionJavaTest : JustKittingTestBase() {
}

//Application-level light service

fun testShouldGenerateAppLevelGetterForServiceLevelApp() {
myFixture.configureByText("SomeService.java",
"""
Expand Down Expand Up @@ -149,6 +150,7 @@ class GenerateStaticGetInstanceActionJavaTest : JustKittingTestBase() {
}

//Light service without level

fun testShouldGenerateProjectLevelGetterForEmptyServiceLevelWithProperClassName() {
myFixture.configureByText("SomeProjectService.java",
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2023 Tamás Balog. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.

package com.picimako.justkitting.codefolding;

import com.picimako.justkitting.ContentRootsBasedJustKittingTestBase;

/**
* Base class for testing code folding.
*/
public abstract class ContentRootsJustKittingCodeFoldingTestBase extends ContentRootsBasedJustKittingTestBase {

protected void doXmlTestFolding(String filePath) {
myFixture.configureByFile(filePath);
myFixture.testFoldingWithCollapseStatus(getTestDataPath() + "/" + filePath);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
/**
* Integration test for {@link PluginDescriptorTagsFoldingBuilder}.
*/
@TestDataPath("$CONTENT_ROOT/testData/codefolding/plugindescriptor")
public class PluginDescriptorTagsFoldingBuilderTest extends JustKittingCodeFoldingTestBase {
@TestDataPath("$CONTENT_ROOT/testData/codefolding/plugindescriptor/noresourcebundle")
public class PluginDescriptorTagsFoldingBuilderNoResourceBundleTest extends JustKittingCodeFoldingTestBase {

@Override
protected String getTestDataPath() {
return "src/test/testData/codefolding/plugindescriptor/";
return "src/test/testData/codefolding/plugindescriptor/noresourcebundle";
}

//No folding
Expand Down
Loading

0 comments on commit 3f0f2f4

Please sign in to comment.