-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Jenkins Symbols autocomplete (#195)
Co-authored-by: Denys Digtiar <[email protected]>
- Loading branch information
1 parent
0b39014
commit e935a1b
Showing
18 changed files
with
665 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
70 changes: 70 additions & 0 deletions
70
src/main/java/io/jenkins/stapler/idea/jelly/IconSrcCompletionContributor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package io.jenkins.stapler.idea.jelly; | ||
|
||
import com.intellij.codeInsight.completion.CompletionContributor; | ||
import com.intellij.codeInsight.completion.CompletionParameters; | ||
import com.intellij.codeInsight.completion.CompletionProvider; | ||
import com.intellij.codeInsight.completion.CompletionResultSet; | ||
import com.intellij.codeInsight.completion.CompletionType; | ||
import com.intellij.codeInsight.lookup.LookupElementBuilder; | ||
import com.intellij.openapi.command.WriteCommandAction; | ||
import com.intellij.openapi.project.Project; | ||
import com.intellij.patterns.PlatformPatterns; | ||
import com.intellij.psi.PsiElement; | ||
import com.intellij.psi.impl.source.xml.XmlAttributeImpl; | ||
import com.intellij.psi.util.PsiTreeUtil; | ||
import com.intellij.psi.xml.XmlAttribute; | ||
import com.intellij.psi.xml.XmlTag; | ||
import com.intellij.util.ProcessingContext; | ||
import io.jenkins.stapler.idea.jelly.symbols.SymbolFinder; | ||
import org.jetbrains.annotations.NotNull; | ||
|
||
public class IconSrcCompletionContributor extends CompletionContributor { | ||
|
||
public IconSrcCompletionContributor() { | ||
extend( | ||
CompletionType.BASIC, | ||
PlatformPatterns.psiElement().inside(XmlAttributeImpl.class), | ||
new CompletionProvider<>() { | ||
@Override | ||
protected void addCompletions( | ||
@NotNull CompletionParameters parameters, | ||
@NotNull ProcessingContext context, | ||
@NotNull CompletionResultSet result) { | ||
PsiElement position = parameters.getPosition(); | ||
PsiElement parent = position.getParent().getParent(); | ||
|
||
if (isInsideLIconSrcAttribute(parent)) { | ||
Project project = position.getProject(); | ||
SymbolFinder.getInstance(project) | ||
.getAvailableSymbols() | ||
.forEach(symbol -> result.addElement(LookupElementBuilder.create(symbol.name()) | ||
.withPresentableText(symbol.displayText()) | ||
.withTypeText(symbol.group()) | ||
.withInsertHandler((insertionContext, item) -> { | ||
XmlAttribute attribute = PsiTreeUtil.getParentOfType( | ||
insertionContext | ||
.getFile() | ||
.findElementAt(insertionContext.getStartOffset()), | ||
XmlAttribute.class); | ||
if (attribute != null) { | ||
WriteCommandAction.runWriteCommandAction( | ||
project, () -> attribute.setValue(symbol.name())); | ||
} | ||
}))); | ||
} | ||
} | ||
}); | ||
} | ||
|
||
private boolean isInsideLIconSrcAttribute(PsiElement element) { | ||
if (element instanceof XmlAttributeImpl attribute) { | ||
if ("src".equals(attribute.getName())) { | ||
PsiElement parent = attribute.getParent(); | ||
if (parent instanceof XmlTag xmlTag) { | ||
return "icon".equals(xmlTag.getLocalName()) && "/lib/layout".equals(xmlTag.getNamespace()); | ||
} | ||
} | ||
} | ||
return false; | ||
} | ||
} |
84 changes: 84 additions & 0 deletions
84
src/main/java/io/jenkins/stapler/idea/jelly/InvalidIconSrcInspection.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package io.jenkins.stapler.idea.jelly; | ||
|
||
import static io.jenkins.stapler.idea.jelly.symbols.ClosestStringFinder.findClosestString; | ||
|
||
import com.intellij.codeInspection.LocalInspectionTool; | ||
import com.intellij.codeInspection.LocalQuickFix; | ||
import com.intellij.codeInspection.ProblemDescriptor; | ||
import com.intellij.codeInspection.ProblemsHolder; | ||
import com.intellij.codeInspection.util.IntentionFamilyName; | ||
import com.intellij.openapi.project.Project; | ||
import com.intellij.psi.PsiElement; | ||
import com.intellij.psi.PsiElementVisitor; | ||
import com.intellij.psi.XmlElementVisitor; | ||
import com.intellij.psi.xml.XmlAttribute; | ||
import com.intellij.psi.xml.XmlTag; | ||
import io.jenkins.stapler.idea.jelly.symbols.Symbol; | ||
import io.jenkins.stapler.idea.jelly.symbols.SymbolFinder; | ||
import java.util.Set; | ||
import java.util.stream.Collectors; | ||
import org.jetbrains.annotations.NotNull; | ||
|
||
public class InvalidIconSrcInspection extends LocalInspectionTool { | ||
|
||
@Override | ||
public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { | ||
return new XmlElementVisitor() { | ||
@Override | ||
public void visitXmlAttribute(@NotNull XmlAttribute attribute) { | ||
if (validAttributeToScan(attribute)) { | ||
Set<String> symbols = | ||
SymbolFinder.getInstance(attribute.getProject()).getAvailableSymbols().stream() | ||
.map(Symbol::name) | ||
.collect(Collectors.toSet()); | ||
if (!symbols.contains(attribute.getValue())) { | ||
String closestSymbol = findClosestString(attribute.getValue(), symbols); | ||
|
||
if (closestSymbol == null) { | ||
holder.registerProblem( | ||
attribute.getValueElement(), | ||
String.format("'%s' isn't a valid symbol", attribute.getValue())); | ||
} else { | ||
holder.registerProblem( | ||
attribute.getValueElement(), | ||
String.format("'%s' isn't a valid symbol", attribute.getValue()), | ||
new LocalQuickFix() { | ||
@Override | ||
public @IntentionFamilyName @NotNull String getFamilyName() { | ||
return "Replace with '" + closestSymbol + "'"; | ||
} | ||
|
||
@Override | ||
public void applyFix( | ||
@NotNull Project project, @NotNull ProblemDescriptor descriptor) { | ||
PsiElement element = descriptor.getPsiElement(); | ||
if (element != null | ||
&& element.getParent() instanceof XmlAttribute attribute) { | ||
attribute.setValue(closestSymbol); | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
} | ||
|
||
private boolean validAttributeToScan(@NotNull XmlAttribute attribute) { | ||
if ("src".equals(attribute.getName())) { | ||
PsiElement parent = attribute.getParent(); | ||
if (parent instanceof XmlTag xmlTag) { | ||
String attributeValue = attribute.getValue(); | ||
if (attributeValue == null) { | ||
return false; | ||
} | ||
return "icon".equals(xmlTag.getLocalName()) | ||
&& "/lib/layout".equals(xmlTag.getNamespace()) | ||
&& attribute.getValue().startsWith("symbol-") | ||
&& !attribute.getValue().contains("${"); // In some cases symbols are dynamically generated | ||
} | ||
} | ||
return false; | ||
} | ||
} |
61 changes: 61 additions & 0 deletions
61
src/main/java/io/jenkins/stapler/idea/jelly/symbols/ClosestStringFinder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
package io.jenkins.stapler.idea.jelly.symbols; | ||
|
||
import java.util.Set; | ||
|
||
public final class ClosestStringFinder { | ||
|
||
/** | ||
* Finds the closest string in the set to the target string. | ||
* | ||
* @param target The string to compare against. | ||
* @param set The set of strings to search. | ||
* @return The closest string, or null if the set is empty. | ||
*/ | ||
public static String findClosestString(String target, Set<String> set) { | ||
if (set == null || set.isEmpty()) { | ||
return null; | ||
} | ||
|
||
// Extract the portion before the first space in the target string | ||
String targetPrefix = target.split(" ")[0]; | ||
|
||
String closestString = null; | ||
int smallestDistance = 5; | ||
|
||
for (String candidate : set) { | ||
// Extract the portion before the first space in the candidate string | ||
String candidatePrefix = candidate.split(" ")[0]; | ||
|
||
// Calculate Levenshtein distance on the prefixes | ||
int distance = levenshteinDistance(targetPrefix, candidatePrefix); | ||
|
||
// Update the closest string if a smaller distance is found | ||
if (distance < smallestDistance) { | ||
smallestDistance = distance; | ||
closestString = candidate; // Store the full candidate string | ||
} | ||
} | ||
|
||
return closestString; | ||
} | ||
|
||
private static int levenshteinDistance(String s1, String s2) { | ||
int[][] dp = new int[s1.length() + 1][s2.length() + 1]; | ||
|
||
for (int i = 0; i <= s1.length(); i++) { | ||
for (int j = 0; j <= s2.length(); j++) { | ||
if (i == 0) { | ||
dp[i][j] = j; | ||
} else if (j == 0) { | ||
dp[i][j] = i; | ||
} else { | ||
dp[i][j] = Math.min( | ||
dp[i - 1][j - 1] + (s1.charAt(i - 1) == s2.charAt(j - 1) ? 0 : 1), | ||
Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1)); | ||
} | ||
} | ||
} | ||
|
||
return dp[s1.length()][s2.length()]; | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
src/main/java/io/jenkins/stapler/idea/jelly/symbols/IoniconsApiSymbolLookup.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package io.jenkins.stapler.idea.jelly.symbols; | ||
|
||
import static org.kohsuke.stapler.idea.MavenProjectHelper.hasDependency; | ||
|
||
import com.intellij.openapi.project.Project; | ||
import io.jenkins.plugins.ionicons.Ionicons; | ||
import java.util.Set; | ||
import java.util.stream.Collectors; | ||
|
||
public class IoniconsApiSymbolLookup implements SymbolLookup { | ||
|
||
private static final String PREFIX = "symbol-"; | ||
private static final String SUFFIX = " plugin-ionicons-api"; | ||
|
||
/** Adds the Ionicons API symbols if the plugin has the dependency added */ | ||
@Override | ||
public Set<Symbol> getSymbols(Project project) { | ||
if (!hasDependency(project, "io.jenkins.plugins", "ionicons-api")) { | ||
return Set.of(); | ||
} | ||
|
||
return Ionicons.getAvailableIcons().keySet().stream() | ||
.map(e -> new Symbol(PREFIX + e + SUFFIX, PREFIX + e, "plugin-ionicons-api")) | ||
.collect(Collectors.toSet()); | ||
} | ||
} |
Oops, something went wrong.