diff --git a/CHANGELOG.md b/CHANGELOG.md index c4df011b..3fa926d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] ### Added +- Highlighting of built-in functions and constants +- Support for [semantic highlighting](https://www.jetbrains.com/help/idea/configuring-colors-and-fonts.html#semantic-highlighting) +- Settings to change the colors used by the highlighter ### Changed diff --git a/build.gradle.kts b/build.gradle.kts index c5589741..a93eabfd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -97,6 +97,12 @@ gradle.buildFinished { } tasks { + withType { + options.encoding = "UTF-8" + } + withType { + options.encoding = "UTF-8" + } task("jbr") { description = "Create a symlink to package jetbrains.jdk" diff --git a/src/main/java/org/nixos/idea/lang/NixSyntaxHighlighter.java b/src/main/java/org/nixos/idea/lang/NixSyntaxHighlighter.java deleted file mode 100644 index 6299cc07..00000000 --- a/src/main/java/org/nixos/idea/lang/NixSyntaxHighlighter.java +++ /dev/null @@ -1,175 +0,0 @@ -package org.nixos.idea.lang; - -import com.intellij.lexer.Lexer; -import com.intellij.openapi.editor.DefaultLanguageHighlighterColors; -import com.intellij.openapi.editor.colors.TextAttributesKey; -import com.intellij.openapi.fileTypes.SyntaxHighlighterBase; -import com.intellij.psi.tree.IElementType; -import org.jetbrains.annotations.NotNull; -import org.nixos.idea.psi.NixTypes; - -import static com.intellij.openapi.editor.colors.TextAttributesKey.EMPTY_ARRAY; -import static com.intellij.openapi.editor.colors.TextAttributesKey.createTextAttributesKey; - -public class NixSyntaxHighlighter extends SyntaxHighlighterBase { - - public static final TextAttributesKey LPAREN = createTextAttributesKey("LPAREN", DefaultLanguageHighlighterColors.PARENTHESES); - public static final TextAttributesKey RPAREN = createTextAttributesKey("RPAREN", DefaultLanguageHighlighterColors.PARENTHESES); - - public static final TextAttributesKey LCURLY = createTextAttributesKey("LCURLY", DefaultLanguageHighlighterColors.BRACES); - public static final TextAttributesKey RCURLY = createTextAttributesKey("RCURLY", DefaultLanguageHighlighterColors.BRACES); - - public static final TextAttributesKey LBRAC = createTextAttributesKey("LBRAC", DefaultLanguageHighlighterColors.BRACKETS); - public static final TextAttributesKey RBRAC = createTextAttributesKey("RBRAC", DefaultLanguageHighlighterColors.BRACKETS); - - public static final TextAttributesKey COLON = createTextAttributesKey("COLON", DefaultLanguageHighlighterColors.OPERATION_SIGN); - public static final TextAttributesKey SEMI = createTextAttributesKey("SEMI", DefaultLanguageHighlighterColors.SEMICOLON); - public static final TextAttributesKey COMMA = createTextAttributesKey("COMMA", DefaultLanguageHighlighterColors.COMMA); - - public static final TextAttributesKey ID = createTextAttributesKey("ID", DefaultLanguageHighlighterColors.IDENTIFIER); - public static final TextAttributesKey INT = createTextAttributesKey("INT", DefaultLanguageHighlighterColors.NUMBER); - - public static final TextAttributesKey ASSERT = createTextAttributesKey("ASSERT", DefaultLanguageHighlighterColors.KEYWORD); - public static final TextAttributesKey IF = createTextAttributesKey("IF", DefaultLanguageHighlighterColors.KEYWORD); - public static final TextAttributesKey ELSE = createTextAttributesKey("ELSE", DefaultLanguageHighlighterColors.KEYWORD); - public static final TextAttributesKey THEN = createTextAttributesKey("THEN", DefaultLanguageHighlighterColors.KEYWORD); - public static final TextAttributesKey LET = createTextAttributesKey("LET", DefaultLanguageHighlighterColors.KEYWORD); - public static final TextAttributesKey REC = createTextAttributesKey("REC", DefaultLanguageHighlighterColors.KEYWORD); - public static final TextAttributesKey IN = createTextAttributesKey("IN", DefaultLanguageHighlighterColors.KEYWORD); - public static final TextAttributesKey WITH = createTextAttributesKey("WITH", DefaultLanguageHighlighterColors.KEYWORD); - public static final TextAttributesKey INHERIT = createTextAttributesKey("INHERIT", DefaultLanguageHighlighterColors.KEYWORD); - public static final TextAttributesKey OR_KW = createTextAttributesKey("OR_KW", DefaultLanguageHighlighterColors.KEYWORD); - - public static final TextAttributesKey GT = createTextAttributesKey("GT", DefaultLanguageHighlighterColors.OPERATION_SIGN); - public static final TextAttributesKey LT = createTextAttributesKey("LT", DefaultLanguageHighlighterColors.OPERATION_SIGN); - public static final TextAttributesKey LEQ = createTextAttributesKey("LEQ", DefaultLanguageHighlighterColors.OPERATION_SIGN); - public static final TextAttributesKey GEQ = createTextAttributesKey("GEQ", DefaultLanguageHighlighterColors.OPERATION_SIGN); - public static final TextAttributesKey NEQ = createTextAttributesKey("NEQ", DefaultLanguageHighlighterColors.OPERATION_SIGN); - public static final TextAttributesKey EQ = createTextAttributesKey("EQ", DefaultLanguageHighlighterColors.OPERATION_SIGN); - - public static final TextAttributesKey PLUS = createTextAttributesKey("PLUS", DefaultLanguageHighlighterColors.OPERATION_SIGN); - public static final TextAttributesKey MINUS = createTextAttributesKey("MINUS", DefaultLanguageHighlighterColors.OPERATION_SIGN); - public static final TextAttributesKey DIVIDE = createTextAttributesKey("DIVIDE", DefaultLanguageHighlighterColors.OPERATION_SIGN); - public static final TextAttributesKey TIMES = createTextAttributesKey("TIMES", DefaultLanguageHighlighterColors.OPERATION_SIGN); - - public static final TextAttributesKey NOT = createTextAttributesKey("NOT", DefaultLanguageHighlighterColors.OPERATION_SIGN); - public static final TextAttributesKey AND = createTextAttributesKey("AND", DefaultLanguageHighlighterColors.OPERATION_SIGN); - public static final TextAttributesKey OR = createTextAttributesKey("OR", DefaultLanguageHighlighterColors.OPERATION_SIGN); - public static final TextAttributesKey IMPL = createTextAttributesKey("IMPL", DefaultLanguageHighlighterColors.OPERATION_SIGN); - public static final TextAttributesKey UPDATE = createTextAttributesKey("UPDATE", DefaultLanguageHighlighterColors.OPERATION_SIGN); - - public static final TextAttributesKey IS = createTextAttributesKey("IS", DefaultLanguageHighlighterColors.OPERATION_SIGN); - public static final TextAttributesKey NAMED = createTextAttributesKey("NAMED", DefaultLanguageHighlighterColors.OPERATION_SIGN); - public static final TextAttributesKey CONCAT = createTextAttributesKey("CONCAT", DefaultLanguageHighlighterColors.OPERATION_SIGN); - - public static final TextAttributesKey DOT = createTextAttributesKey("DOT", DefaultLanguageHighlighterColors.OPERATION_SIGN); - public static final TextAttributesKey DOLLAR_CURLY = createTextAttributesKey("DOLLAR_CURLY", DefaultLanguageHighlighterColors.OPERATION_SIGN); - - public static final TextAttributesKey PATH = createTextAttributesKey("PATH", DefaultLanguageHighlighterColors.LABEL); - public static final TextAttributesKey SPATH = createTextAttributesKey("SPATH", DefaultLanguageHighlighterColors.LABEL); - public static final TextAttributesKey HPATH = createTextAttributesKey("HPATH", DefaultLanguageHighlighterColors.LABEL); - - public static final TextAttributesKey IND_STR = createTextAttributesKey("IND_STR", DefaultLanguageHighlighterColors.STRING); - public static final TextAttributesKey STR = createTextAttributesKey("STR", DefaultLanguageHighlighterColors.STRING); - - public static final TextAttributesKey SCOMMENT = createTextAttributesKey("SCOMMENT", DefaultLanguageHighlighterColors.LINE_COMMENT); - public static final TextAttributesKey MCOMMENT = createTextAttributesKey("MCOMMENT", DefaultLanguageHighlighterColors.BLOCK_COMMENT); - - public static final TextAttributesKey LAMBDA = createTextAttributesKey("LAMBDA", DefaultLanguageHighlighterColors.FUNCTION_DECLARATION); - public static final TextAttributesKey FORMAL = createTextAttributesKey("FORMAL", DefaultLanguageHighlighterColors.PARAMETER); - - public static final TextAttributesKey[] SEMI_KEYS = new TextAttributesKey[]{SEMI}; - public static final TextAttributesKey[] COMMA_KEYS = new TextAttributesKey[]{COMMA}; - public static final TextAttributesKey[] KEYWORD_KEYS = new TextAttributesKey[]{IN,OR_KW,REC,IF,ELSE,THEN,LET,IN,WITH,INHERIT}; - public static final TextAttributesKey[] STRING_KEYS = new TextAttributesKey[]{STR,IND_STR}; - public static final TextAttributesKey[] PAREN_KEYS = new TextAttributesKey[]{LPAREN,RPAREN,LCURLY,RCURLY,LBRAC,RBRAC}; - public static final TextAttributesKey[] COLON_KEYS = new TextAttributesKey[]{COLON}; - public static final TextAttributesKey[] ID_KEYS = new TextAttributesKey[]{ID}; - public static final TextAttributesKey[] NUMBER_KEYS = new TextAttributesKey[]{INT}; - public static final TextAttributesKey[] OPERATOR_KEYS = new TextAttributesKey[]{ - GT,LT,LEQ,GEQ,NEQ,EQ,PLUS,MINUS,DIVIDE,TIMES,NOT,AND,OR,IMPL,UPDATE,IS,NAMED,CONCAT,DOT,DOLLAR_CURLY - }; - public static final TextAttributesKey[] LINE_COMMENT_KEYS = new TextAttributesKey[]{SCOMMENT}; - public static final TextAttributesKey[] MULTI_LINE_COMMENT_KEYS = new TextAttributesKey[]{MCOMMENT}; - public static final TextAttributesKey[] FUNCTION_KEYS = new TextAttributesKey[]{LAMBDA}; - public static final TextAttributesKey[] PARAMETER_KEYS = new TextAttributesKey[]{FORMAL}; - - @Override - public @NotNull Lexer getHighlightingLexer() { - return new NixLexer(); - } - - @Override - public TextAttributesKey @NotNull [] getTokenHighlights(IElementType tokenType) { - { - if (tokenType == NixTypes.PARAM) return PARAMETER_KEYS; - if (tokenType == NixTypes.EXPR_LAMBDA) return FUNCTION_KEYS; - if (tokenType == NixTypes.COMMA) return COMMA_KEYS; - if (tokenType == NixTypes.SEMI) return SEMI_KEYS; - if (tokenType == NixTypes.COLON) { - return COLON_KEYS; - } - if (tokenType == NixTypes.SCOMMENT) { - return LINE_COMMENT_KEYS; - } - if (tokenType == NixTypes.MCOMMENT) { - return MULTI_LINE_COMMENT_KEYS; - } - if (tokenType == NixTypes.ID) { - return ID_KEYS; - } - if (tokenType == NixTypes.INT) { - return NUMBER_KEYS; - } - if( - tokenType == NixTypes.GT || - tokenType == NixTypes.LT || - tokenType == NixTypes.LEQ || - tokenType == NixTypes.GEQ || - tokenType == NixTypes.NEQ || - tokenType == NixTypes.EQ || - tokenType == NixTypes.PLUS || - tokenType == NixTypes.MINUS || - tokenType == NixTypes.DIVIDE || - tokenType == NixTypes.TIMES || - tokenType == NixTypes.NOT || - tokenType == NixTypes.AND || - tokenType == NixTypes.OR || - tokenType == NixTypes.IMPL || - tokenType == NixTypes.UPDATE || - tokenType == NixTypes.HAS || - tokenType == NixTypes.AT || - tokenType == NixTypes.CONCAT || - tokenType == NixTypes.DOT ) - return OPERATOR_KEYS; - if ( - tokenType == NixTypes.RBRAC || - tokenType == NixTypes.LBRAC || - tokenType == NixTypes.RPAREN || - tokenType == NixTypes.LPAREN || - tokenType == NixTypes.RCURLY || - tokenType == NixTypes.DOLLAR || - tokenType == NixTypes.LCURLY) { - return PAREN_KEYS; - } - if (tokenType == NixTypes.STR || tokenType == NixTypes.IND_STR) { - return STRING_KEYS; - } - if ( - tokenType == NixTypes.IF || - tokenType == NixTypes.THEN || - tokenType == NixTypes.ELSE || - tokenType == NixTypes.ASSERT || - tokenType == NixTypes.WITH || - tokenType == NixTypes.LET || - tokenType == NixTypes.IN || - tokenType == NixTypes.REC || - tokenType == NixTypes.INHERIT || - tokenType == NixTypes.OR_KW) { - return KEYWORD_KEYS; - } - return EMPTY_ARRAY; - } - } -} - diff --git a/src/main/java/org/nixos/idea/lang/builtins/NixBuiltin.java b/src/main/java/org/nixos/idea/lang/builtins/NixBuiltin.java new file mode 100644 index 00000000..521c37ba --- /dev/null +++ b/src/main/java/org/nixos/idea/lang/builtins/NixBuiltin.java @@ -0,0 +1,239 @@ +package org.nixos.idea.lang.builtins; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.nixos.idea.util.NixVersion; + +import java.util.Map; +import java.util.Set; + +public final class NixBuiltin { + //region Constants + + /** + * List of builtins which are available in the global scope (i.e. without “builtins.” prefix). + * To verify this list against your installation, start {@code nix repl} and press Tab. + */ + private static final Set GLOBAL_SCOPE = Set.of( + "false", + "null", + "true", + "abort", + "baseNameOf", + "break", + "builtins", + "derivation", + "derivationStrict", + "dirOf", + "fetchGit", + "fetchMercurial", + "fetchTarball", + "fetchTree", + "fromTOML", + "import", + "isNull", + "map", + "placeholder", + "removeAttrs", + "scopedImport", + "throw", + "toString"); + + /** + * List of all builtins. To verify this list against your installation, start {@code nix repl}, + * type {@code builtins.}, and press Tab. + */ + private static final Map BUILTINS = Map.ofEntries( + builtin("false", HighlightingType.LITERAL), + builtin("null", HighlightingType.LITERAL), + builtin("true", HighlightingType.LITERAL), + builtin("import", HighlightingType.IMPORT), + builtin("abort"), + builtin("add"), + builtin("addErrorContext"), + builtin("all"), + builtin("any"), + builtin("appendContext"), + builtin("attrNames"), + builtin("attrValues"), + builtin("baseNameOf"), + builtin("bitAnd"), + builtin("bitOr"), + builtin("bitXor"), + builtin("break", NixVersion.V2_09), + builtin("builtins"), + builtin("catAttrs"), + builtin("ceil", NixVersion.V2_04), + builtin("compareVersions"), + builtin("concatLists"), + builtin("concatMap"), + builtin("concatStringsSep"), + builtin("currentSystem"), + builtin("currentTime"), + builtin("deepSeq"), + builtin("derivation"), + builtin("derivationStrict"), + builtin("dirOf"), + builtin("div"), + builtin("elem"), + builtin("elemAt"), + builtin("fetchClosure", NixVersion.V2_08, "fetch-closure"), + builtin("fetchGit"), + builtin("fetchMercurial"), + builtin("fetchTarball"), + builtin("fetchTree", NixVersion.V2_04), + builtin("fetchurl"), + builtin("filter"), + builtin("filterSource"), + builtin("findFile"), + builtin("floor", NixVersion.V2_04), + builtin("foldl'"), + builtin("fromJSON"), + builtin("fromTOML"), + builtin("functionArgs"), + builtin("genList"), + builtin("genericClosure"), + builtin("getAttr"), + builtin("getContext"), + builtin("getEnv"), + builtin("getFlake", NixVersion.V2_04, "flakes"), + builtin("groupBy", NixVersion.V2_05), + builtin("hasAttr"), + builtin("hasContext"), + builtin("hashFile"), + builtin("hashString"), + builtin("head"), + builtin("intersectAttrs"), + builtin("isAttrs"), + builtin("isBool"), + builtin("isFloat"), + builtin("isFunction"), + builtin("isInt"), + builtin("isList"), + builtin("isNull"), + builtin("isPath"), + builtin("isString"), + builtin("langVersion"), + builtin("length"), + builtin("lessThan"), + builtin("listToAttrs"), + builtin("map"), + builtin("mapAttrs"), + builtin("match"), + builtin("mul"), + builtin("nixPath"), + builtin("nixVersion"), + builtin("parseDrvName"), + builtin("partition"), + builtin("path"), + builtin("pathExists"), + builtin("placeholder"), + builtin("readDir"), + builtin("readFile"), + builtin("removeAttrs"), + builtin("replaceStrings"), + builtin("scopedImport"), + builtin("seq"), + builtin("sort"), + builtin("split"), + builtin("splitVersion"), + builtin("storeDir"), + builtin("storePath"), + builtin("stringLength"), + builtin("sub"), + builtin("substring"), + builtin("tail"), + builtin("throw"), + builtin("toFile"), + builtin("toJSON"), + builtin("toPath"), + builtin("toString"), + builtin("toXML"), + builtin("trace"), + builtin("traceVerbose", NixVersion.V2_10), + builtin("tryEval"), + builtin("typeOf"), + builtin("unsafeDiscardOutputDependency"), + builtin("unsafeDiscardStringContext"), + builtin("unsafeGetAttrPos"), + builtin("zipAttrsWith", NixVersion.V2_06)); + + //endregion + //region Factories + + private static @NotNull Map.Entry builtin(@NotNull String name) { + return Map.entry(name, new NixBuiltin(name, null, null, null, HighlightingType.OTHER)); + } + + private static @NotNull Map.Entry builtin(@NotNull String name, @NotNull HighlightingType highlightingType) { + return Map.entry(name, new NixBuiltin(name, null, null, null, highlightingType)); + } + + private static @NotNull Map.Entry builtin(@NotNull String name, @NotNull NixVersion since) { + return Map.entry(name, new NixBuiltin(name, since, null, null, HighlightingType.OTHER)); + } + + private static @NotNull Map.Entry builtin(@NotNull String name, @NotNull NixVersion since, @NotNull String featureFlag) { + return Map.entry(name, new NixBuiltin(name, since, featureFlag, null, HighlightingType.OTHER)); + } + + //endregion + //region Instance members + + private final @NotNull String name; + private final @Nullable NixVersion since; + private final @Nullable String featureFlag; + private final @Nullable NixVersion featureFlagIntegration; + private final @NotNull HighlightingType highlightingType; + private final boolean global; + + private NixBuiltin(@NotNull String name, + @Nullable NixVersion since, + @Nullable String featureFlag, + @Nullable NixVersion featureFlagIntegration, + @NotNull HighlightingType highlightingType) { + this.name = name; + this.since = since; + this.featureFlag = featureFlag; + this.featureFlagIntegration = featureFlagIntegration; + this.highlightingType = highlightingType; + this.global = GLOBAL_SCOPE.contains(name); + } + + public HighlightingType highlightingType() { + return highlightingType; + } + + //endregion + //region Static members + + public static @Nullable NixBuiltin resolveBuiltin(@NotNull String name) { + return BUILTINS.get(name); + } + + public static @Nullable NixBuiltin resolveGlobal(@NotNull String name) { + NixBuiltin builtin = BUILTINS.get(name); + return builtin != null && builtin.global ? builtin : null; + } + + //endregion + //region Inner classes + + public enum HighlightingType { + /** + * Builtins which are casually described as literals. + * Specifically, only “{@code true}”, “{@code null}”, and “{@code false} have this type”. + */ + LITERAL, + /** + * Import function. + */ + IMPORT, + /** + * Any other builtin which does not match any of the other types. + */ + OTHER, + } + + //endregion +} diff --git a/src/main/java/org/nixos/idea/lang/highlighter/NixHighlightVisitor.java b/src/main/java/org/nixos/idea/lang/highlighter/NixHighlightVisitor.java new file mode 100644 index 00000000..553b41c0 --- /dev/null +++ b/src/main/java/org/nixos/idea/lang/highlighter/NixHighlightVisitor.java @@ -0,0 +1,56 @@ +package org.nixos.idea.lang.highlighter; + +import com.intellij.codeInsight.daemon.impl.HighlightInfo; +import com.intellij.codeInsight.daemon.impl.HighlightInfoType; +import com.intellij.codeInsight.daemon.impl.HighlightVisitor; +import com.intellij.codeInsight.daemon.impl.analysis.HighlightInfoHolder; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class NixHighlightVisitor implements HighlightVisitor { + + private HighlightInfoHolder myHolder; + private Delegate myDelegate; + + @Override + public boolean suitableForFile(@NotNull PsiFile file) { + return NixHighlightVisitorDelegate.suitableForFile(file); + } + + @Override + public void visit(@NotNull PsiElement element) { + myDelegate.visit(element); + } + + @Override + public boolean analyze(@NotNull PsiFile file, boolean updateWholeFile, @NotNull HighlightInfoHolder holder, @NotNull Runnable action) { + try { + myHolder = holder; + myDelegate = new Delegate(); + action.run(); + } finally { + myHolder = null; + myDelegate = null; + } + return true; + } + + @Override + @SuppressWarnings("MethodDoesntCallSuperMethod") + public @NotNull HighlightVisitor clone() { + return new NixHighlightVisitor(); + } + + private final class Delegate extends NixHighlightVisitorDelegate { + @Override + void highlight(@NotNull PsiElement element, @Nullable PsiElement source, @NotNull String attrPath, @Nullable HighlightInfoType type) { + if (type != null) { + myHolder.add(HighlightInfo.newHighlightInfo(type) + .range(element) + .create()); + } + } + } +} diff --git a/src/main/java/org/nixos/idea/lang/highlighter/NixHighlightVisitorDelegate.java b/src/main/java/org/nixos/idea/lang/highlighter/NixHighlightVisitorDelegate.java new file mode 100644 index 00000000..4084f680 --- /dev/null +++ b/src/main/java/org/nixos/idea/lang/highlighter/NixHighlightVisitorDelegate.java @@ -0,0 +1,212 @@ +package org.nixos.idea.lang.highlighter; + +import com.intellij.codeInsight.daemon.impl.HighlightInfoType; +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.nixos.idea.file.NixFile; +import org.nixos.idea.lang.builtins.NixBuiltin; +import org.nixos.idea.psi.NixAttr; +import org.nixos.idea.psi.NixAttrPath; +import org.nixos.idea.psi.NixBind; +import org.nixos.idea.psi.NixBindAttr; +import org.nixos.idea.psi.NixBindInherit; +import org.nixos.idea.psi.NixExpr; +import org.nixos.idea.psi.NixExprLambda; +import org.nixos.idea.psi.NixExprLet; +import org.nixos.idea.psi.NixExprSelect; +import org.nixos.idea.psi.NixIdentifier; +import org.nixos.idea.psi.NixLegacyLet; +import org.nixos.idea.psi.NixParam; +import org.nixos.idea.psi.NixParamSet; +import org.nixos.idea.psi.NixSet; +import org.nixos.idea.psi.NixStdAttr; +import org.nixos.idea.psi.NixTypes; + +import java.util.List; +import java.util.function.BiPredicate; + +/** + * Delegate for the highlighting logic used by {@link NixHighlightVisitor} and {@link NixRainbowVisitor}. + */ +abstract class NixHighlightVisitorDelegate { + + private static final String BUILTINS_PREFIX = "builtins."; + + public static final HighlightInfoType LITERAL = new HighlightInfoType.HighlightInfoTypeImpl(HighlightInfoType.SYMBOL_TYPE_SEVERITY, NixTextAttributes.LITERAL, true); + public static final HighlightInfoType IMPORT = new HighlightInfoType.HighlightInfoTypeImpl(HighlightInfoType.SYMBOL_TYPE_SEVERITY, NixTextAttributes.IMPORT, true); + public static final HighlightInfoType BUILTIN = new HighlightInfoType.HighlightInfoTypeImpl(HighlightInfoType.SYMBOL_TYPE_SEVERITY, NixTextAttributes.BUILTIN, true); + public static final HighlightInfoType LOCAL_VARIABLE = new HighlightInfoType.HighlightInfoTypeImpl(HighlightInfoType.SYMBOL_TYPE_SEVERITY, NixTextAttributes.LOCAL_VARIABLE, false); + public static final HighlightInfoType PARAMETER = new HighlightInfoType.HighlightInfoTypeImpl(HighlightInfoType.SYMBOL_TYPE_SEVERITY, NixTextAttributes.PARAMETER, false); + + static boolean suitableForFile(@NotNull PsiFile file) { + return file instanceof NixFile; + } + + /** + * Callback which gets called for usages of variables and their attributes in the given file. + * + * @param element The element which refers to a variable or attribute. + * @param source The element which defines the corresponding variable. Is {@code null} if the source cannot be determined. + * @param attrPath The used attribute path. + * @param type The type of variable, i.e. {@link #LOCAL_VARIABLE} or {@link #PARAMETER}. + */ + abstract void highlight(@NotNull PsiElement element, @Nullable PsiElement source, @NotNull String attrPath, @Nullable HighlightInfoType type); + + void visit(@NotNull PsiElement element) { + if (element instanceof NixIdentifier) { + NixExpr value = (NixIdentifier) element; + String identifier = value.getText(); + PsiElement source = findSource(element, identifier); + highlight(value, source, identifier); + } else if (element instanceof NixExprSelect) { + NixExprSelect expr = (NixExprSelect) element; + NixExpr value = expr.getValue(); + NixAttrPath attrPath = expr.getAttrPath(); + if (attrPath != null && value instanceof NixIdentifier) { + String identifier = value.getText(); + PsiElement source = findSource(element, identifier); + String pathStr = identifier; + for (NixAttr nixAttr : expr.getAttrPath().getAttrList()) { + if (!(nixAttr instanceof NixStdAttr)) { + break; + } + pathStr = pathStr + '.' + nixAttr.getText(); + highlight(nixAttr, source, pathStr); + } + } + } else { + iterateVariables(element, true, (var, path) -> { + highlight(var, element, path); + return false; + }); + } + } + + private static @Nullable PsiElement findSource(@NotNull PsiElement context, @NotNull String identifier) { + do { + if (iterateVariables(context, false, (var, __) -> var.textMatches(identifier))) { + return context; + } else { + context = context.getParent(); + } + } while (context != null); + return null; + } + + private static boolean iterateVariables(@NotNull PsiElement element, boolean fullPath, @NotNull BiPredicate action) { + if (element instanceof NixExprLet) { + NixExprLet let = (NixExprLet) element; + return iterateVariables(let.getBindList(), fullPath, action); + } else if (element instanceof NixLegacyLet) { + NixLegacyLet let = (NixLegacyLet) element; + return iterateVariables(let.getBindList(), fullPath, action); + } else if (element instanceof NixSet) { + NixSet set = (NixSet) element; + return set.getNode().findChildByType(NixTypes.REC) != null && + iterateVariables(set.getBindList(), fullPath, action); + } else if (element instanceof NixExprLambda) { + NixExprLambda lambda = (NixExprLambda) element; + ASTNode mainParam = lambda.getNode().findChildByType(NixTypes.ID); + if (mainParam != null && action.test(mainParam.getPsi(), fullPath ? mainParam.getText() : null)) { + return true; + } + NixParamSet paramSet = lambda.getParamSet(); + if (paramSet != null) { + for (NixParam param : paramSet.getParamList()) { + ASTNode paramName = param.getNode().findChildByType(NixTypes.ID); + if (paramName != null && action.test(paramName.getPsi(), fullPath ? paramName.getText() : null)) { + return true; + } + } + } + } + return false; + } + + private static boolean iterateVariables(@NotNull List bindList, boolean fullPath, @NotNull BiPredicate action) { + for (NixBind bind : bindList) { + if (bind instanceof NixBindAttr) { + NixBindAttr bindAttr = (NixBindAttr) bind; + if (fullPath) { + List attrs = bindAttr.getAttrPath().getAttrList(); + NixAttr first = attrs.get(0); + if (!(first instanceof NixStdAttr)) { + continue; + } + String pathStr = first.getText(); + if (action.test(first, pathStr)) { + return true; + } + for (NixAttr attr : attrs.subList(1, attrs.size())) { + if (!(attr instanceof NixStdAttr)) { + break; + } + pathStr = pathStr + '.' + attr.getText(); + if (action.test(attr, pathStr)) { + return true; + } + } + } else { + if (action.test(bindAttr.getAttrPath().getFirstAttr(), null)) { + return true; + } + } + } else if (bind instanceof NixBindInherit) { + for (NixAttr attr : ((NixBindInherit) bind).getAttrList()) { + if (attr instanceof NixStdAttr && action.test(attr, fullPath ? attr.getText() : null)) { + return true; + } + } + } else { + throw new IllegalStateException("Unexpected NixBind implementation: " + bind.getClass()); + } + } + return false; + } + + private static @NotNull HighlightInfoType getHighlightingBySource(@NotNull PsiElement source) { + if (source instanceof NixExprLet || + source instanceof NixLegacyLet || + source instanceof NixSet) { + return LOCAL_VARIABLE; + } else if (source instanceof NixExprLambda) { + return PARAMETER; + } else { + throw new IllegalArgumentException("Invalid source: " + source); + } + } + + private static @NotNull HighlightInfoType getHighlightingByBuiltin(@NotNull NixBuiltin builtin) { + switch (builtin.highlightingType()) { + case IMPORT: return IMPORT; + case LITERAL: return LITERAL; + case OTHER: return BUILTIN; + default: throw new IllegalStateException("unknown type: " + builtin.highlightingType()); + } + } + + private void highlight(@NotNull PsiElement element, @Nullable PsiElement source, @NotNull String attrPath) { + HighlightInfoType type = null; + if (!attrPath.contains(".")) { + if (source != null) { + type = getHighlightingBySource(source); + } + else { + NixBuiltin builtin = NixBuiltin.resolveGlobal(attrPath); + if (builtin != null) { + type = getHighlightingByBuiltin(builtin); + } + } + } + else if (attrPath.startsWith(BUILTINS_PREFIX)) { + NixBuiltin builtin = NixBuiltin.resolveBuiltin(attrPath.substring(BUILTINS_PREFIX.length())); + if (builtin != null) { + type = getHighlightingByBuiltin(builtin); + } + } + highlight(element, source, attrPath, type); + } +} diff --git a/src/main/java/org/nixos/idea/lang/highlighter/NixRainbowVisitor.java b/src/main/java/org/nixos/idea/lang/highlighter/NixRainbowVisitor.java new file mode 100644 index 00000000..0994e523 --- /dev/null +++ b/src/main/java/org/nixos/idea/lang/highlighter/NixRainbowVisitor.java @@ -0,0 +1,64 @@ +package org.nixos.idea.lang.highlighter; + +import com.intellij.codeInsight.daemon.RainbowVisitor; +import com.intellij.codeInsight.daemon.impl.HighlightInfoType; +import com.intellij.codeInsight.daemon.impl.HighlightVisitor; +import com.intellij.codeInsight.daemon.impl.analysis.HighlightInfoHolder; +import com.intellij.openapi.editor.colors.TextAttributesKey; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public final class NixRainbowVisitor extends RainbowVisitor { + + public static final List RAINBOW_ATTRIBUTES = List.of( + NixTextAttributes.LOCAL_VARIABLE, + NixTextAttributes.PARAMETER); + + private Delegate myDelegate; + + @Override + public boolean suitableForFile(@NotNull PsiFile file) { + return NixHighlightVisitorDelegate.suitableForFile(file); + } + + @Override + public void visit(@NotNull PsiElement element) { + myDelegate.visit(element); + } + + @Override + public boolean analyze(@NotNull PsiFile file, boolean updateWholeFile, @NotNull HighlightInfoHolder holder, @NotNull Runnable action) { + myDelegate = new Delegate(file); + try { + return super.analyze(file, updateWholeFile, holder, action); + } finally { + myDelegate = null; + } + } + + @Override + public @NotNull HighlightVisitor clone() { + return new NixRainbowVisitor(); + } + + private final class Delegate extends NixHighlightVisitorDelegate { + private final @NotNull PsiFile file; + + private Delegate(@NotNull PsiFile file) { + this.file = file; + } + + @Override + void highlight(@NotNull PsiElement element, @Nullable PsiElement source, @NotNull String attrPath, @Nullable HighlightInfoType type) { + if (type != LITERAL && type != IMPORT && type != BUILTIN) { + TextAttributesKey attributesKey = type == null ? NixTextAttributes.IDENTIFIER : type.getAttributesKey(); + PsiElement context = source == null ? file : source; + addInfo(getInfo(context, element, attrPath, attributesKey)); + } + } + } +} diff --git a/src/main/java/org/nixos/idea/lang/highlighter/NixSyntaxHighlighter.java b/src/main/java/org/nixos/idea/lang/highlighter/NixSyntaxHighlighter.java new file mode 100644 index 00000000..f6ab155c --- /dev/null +++ b/src/main/java/org/nixos/idea/lang/highlighter/NixSyntaxHighlighter.java @@ -0,0 +1,96 @@ +package org.nixos.idea.lang.highlighter; + +import com.intellij.lexer.Lexer; +import com.intellij.openapi.editor.HighlighterColors; +import com.intellij.openapi.editor.colors.TextAttributesKey; +import com.intellij.openapi.fileTypes.SyntaxHighlighterBase; +import com.intellij.psi.TokenType; +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.NotNull; +import org.nixos.idea.lang.NixLexer; +import org.nixos.idea.psi.NixTypes; + +import java.util.Map; + +import static java.util.Map.entry; + +public class NixSyntaxHighlighter extends SyntaxHighlighterBase { + + private static final Map TOKEN_MAP = Map.ofEntries( + // Keywords + entry(NixTypes.IF, NixTextAttributes.KEYWORD), + entry(NixTypes.THEN, NixTextAttributes.KEYWORD), + entry(NixTypes.ELSE, NixTextAttributes.KEYWORD), + entry(NixTypes.ASSERT, NixTextAttributes.KEYWORD), + entry(NixTypes.WITH, NixTextAttributes.KEYWORD), + entry(NixTypes.LET, NixTextAttributes.KEYWORD), + entry(NixTypes.IN, NixTextAttributes.KEYWORD), + entry(NixTypes.REC, NixTextAttributes.KEYWORD), + entry(NixTypes.INHERIT, NixTextAttributes.KEYWORD), + entry(NixTypes.OR_KW, NixTextAttributes.KEYWORD), + // Identifiers + entry(NixTypes.ID, NixTextAttributes.IDENTIFIER), + // Operators + entry(NixTypes.ASSIGN, NixTextAttributes.ASSIGN), + entry(NixTypes.COLON, NixTextAttributes.COLON), + entry(NixTypes.SEMI, NixTextAttributes.SEMICOLON), + entry(NixTypes.COMMA, NixTextAttributes.COMMA), + entry(NixTypes.DOT, NixTextAttributes.DOT), + entry(NixTypes.ELLIPSIS, NixTextAttributes.ELLIPSIS), + entry(NixTypes.AT, NixTextAttributes.AT), + entry(NixTypes.HAS, NixTextAttributes.OPERATION_SIGN), + entry(NixTypes.NOT, NixTextAttributes.OPERATION_SIGN), + entry(NixTypes.TIMES, NixTextAttributes.OPERATION_SIGN), + entry(NixTypes.DIVIDE, NixTextAttributes.OPERATION_SIGN), + entry(NixTypes.PLUS, NixTextAttributes.OPERATION_SIGN), + entry(NixTypes.MINUS, NixTextAttributes.OPERATION_SIGN), + entry(NixTypes.LT, NixTextAttributes.OPERATION_SIGN), + entry(NixTypes.GT, NixTextAttributes.OPERATION_SIGN), + entry(NixTypes.CONCAT, NixTextAttributes.OPERATION_SIGN), + entry(NixTypes.UPDATE, NixTextAttributes.OPERATION_SIGN), + entry(NixTypes.LEQ, NixTextAttributes.OPERATION_SIGN), + entry(NixTypes.GEQ, NixTextAttributes.OPERATION_SIGN), + entry(NixTypes.EQ, NixTextAttributes.OPERATION_SIGN), + entry(NixTypes.NEQ, NixTextAttributes.OPERATION_SIGN), + entry(NixTypes.AND, NixTextAttributes.OPERATION_SIGN), + entry(NixTypes.OR, NixTextAttributes.OPERATION_SIGN), + entry(NixTypes.IMPL, NixTextAttributes.OPERATION_SIGN), + // Parentheses + entry(NixTypes.LPAREN, NixTextAttributes.PARENTHESES), + entry(NixTypes.RPAREN, NixTextAttributes.PARENTHESES), + entry(NixTypes.LBRAC, NixTextAttributes.BRACKETS), + entry(NixTypes.RBRAC, NixTextAttributes.BRACKETS), + entry(NixTypes.LCURLY, NixTextAttributes.BRACES), + entry(NixTypes.RCURLY, NixTextAttributes.BRACES), + entry(NixTypes.DOLLAR, NixTextAttributes.BRACES), + // Literals + entry(NixTypes.INT, NixTextAttributes.NUMBER), + entry(NixTypes.FLOAT, NixTextAttributes.NUMBER), + entry(NixTypes.PATH, NixTextAttributes.PATH), + entry(NixTypes.HPATH, NixTextAttributes.PATH), + entry(NixTypes.SPATH, NixTextAttributes.PATH), + entry(NixTypes.URI, NixTextAttributes.URI), + // String literals + entry(NixTypes.STR, NixTextAttributes.STRING), + entry(NixTypes.STRING_CLOSE, NixTextAttributes.STRING), + entry(NixTypes.STRING_OPEN, NixTextAttributes.STRING), + entry(NixTypes.IND_STR, NixTextAttributes.STRING), + entry(NixTypes.IND_STRING_CLOSE, NixTextAttributes.STRING), + entry(NixTypes.IND_STRING_OPEN, NixTextAttributes.STRING), + entry(NixTypes.STR_ESCAPE, NixTextAttributes.STRING_ESCAPE), + entry(NixTypes.IND_STR_ESCAPE, NixTextAttributes.STRING_ESCAPE), + // Other + entry(NixTypes.SCOMMENT, NixTextAttributes.LINE_COMMENT), + entry(NixTypes.MCOMMENT, NixTextAttributes.BLOCK_COMMENT), + entry(TokenType.BAD_CHARACTER, HighlighterColors.BAD_CHARACTER)); + + @Override + public @NotNull Lexer getHighlightingLexer() { + return new NixLexer(); + } + + @Override + public TextAttributesKey @NotNull [] getTokenHighlights(IElementType tokenType) { + return pack(TOKEN_MAP.get(tokenType)); + } +} diff --git a/src/main/java/org/nixos/idea/lang/NixSyntaxHighlighterFactory.java b/src/main/java/org/nixos/idea/lang/highlighter/NixSyntaxHighlighterFactory.java similarity index 73% rename from src/main/java/org/nixos/idea/lang/NixSyntaxHighlighterFactory.java rename to src/main/java/org/nixos/idea/lang/highlighter/NixSyntaxHighlighterFactory.java index 4c370cd1..8723f681 100644 --- a/src/main/java/org/nixos/idea/lang/NixSyntaxHighlighterFactory.java +++ b/src/main/java/org/nixos/idea/lang/highlighter/NixSyntaxHighlighterFactory.java @@ -1,4 +1,4 @@ -package org.nixos.idea.lang; +package org.nixos.idea.lang.highlighter; import com.intellij.openapi.fileTypes.SyntaxHighlighter; import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory; @@ -7,10 +7,8 @@ import org.jetbrains.annotations.NotNull; public class NixSyntaxHighlighterFactory extends SyntaxHighlighterFactory { - @NotNull @Override - public SyntaxHighlighter getSyntaxHighlighter(Project project, VirtualFile virtualFile) { + public @NotNull SyntaxHighlighter getSyntaxHighlighter(Project project, VirtualFile virtualFile) { return new NixSyntaxHighlighter(); } } - diff --git a/src/main/java/org/nixos/idea/lang/highlighter/NixTextAttributes.java b/src/main/java/org/nixos/idea/lang/highlighter/NixTextAttributes.java new file mode 100644 index 00000000..ab28b625 --- /dev/null +++ b/src/main/java/org/nixos/idea/lang/highlighter/NixTextAttributes.java @@ -0,0 +1,69 @@ +package org.nixos.idea.lang.highlighter; + +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors; +import com.intellij.openapi.editor.colors.TextAttributesKey; + +import static com.intellij.openapi.editor.colors.TextAttributesKey.createTextAttributesKey; + +public final class NixTextAttributes { + + public static final TextAttributesKey KEYWORD = + createTextAttributesKey("NIX_KEYWORD", DefaultLanguageHighlighterColors.KEYWORD); + + public static final TextAttributesKey SEMICOLON = + createTextAttributesKey("NIX_SEMICOLON", DefaultLanguageHighlighterColors.SEMICOLON); + public static final TextAttributesKey COMMA = + createTextAttributesKey("NIX_COMMA", DefaultLanguageHighlighterColors.COMMA); + public static final TextAttributesKey DOT = + createTextAttributesKey("NIX_DOT", DefaultLanguageHighlighterColors.DOT); + public static final TextAttributesKey ASSIGN = + createTextAttributesKey("NIX_ASSIGN", DefaultLanguageHighlighterColors.OPERATION_SIGN); + public static final TextAttributesKey COLON = + createTextAttributesKey("NIX_COLON", DefaultLanguageHighlighterColors.OPERATION_SIGN); + public static final TextAttributesKey AT = + createTextAttributesKey("NIX_AT", DefaultLanguageHighlighterColors.OPERATION_SIGN); + public static final TextAttributesKey ELLIPSIS = + createTextAttributesKey("NIX_ELLIPSIS", DefaultLanguageHighlighterColors.OPERATION_SIGN); + public static final TextAttributesKey OPERATION_SIGN = + createTextAttributesKey("NIX_OPERATION_SIGN", DefaultLanguageHighlighterColors.OPERATION_SIGN); + + public static final TextAttributesKey PARENTHESES = + createTextAttributesKey("NIX_PARENTHESES", DefaultLanguageHighlighterColors.PARENTHESES); + public static final TextAttributesKey BRACES = + createTextAttributesKey("NIX_BRACES", DefaultLanguageHighlighterColors.BRACES); + public static final TextAttributesKey BRACKETS = + createTextAttributesKey("NIX_BRACKETS", DefaultLanguageHighlighterColors.BRACKETS); + + public static final TextAttributesKey IDENTIFIER = + createTextAttributesKey("NIX_IDENTIFIER", DefaultLanguageHighlighterColors.IDENTIFIER); + public static final TextAttributesKey LITERAL = + createTextAttributesKey("NIX_LITERAL", DefaultLanguageHighlighterColors.KEYWORD); + public static final TextAttributesKey IMPORT = + createTextAttributesKey("NIX_IMPORT", DefaultLanguageHighlighterColors.KEYWORD); + public static final TextAttributesKey BUILTIN = + createTextAttributesKey("NIX_BUILTIN", DefaultLanguageHighlighterColors.PREDEFINED_SYMBOL); + public static final TextAttributesKey LOCAL_VARIABLE = + createTextAttributesKey("NIX_LOCAL_VARIABLE", DefaultLanguageHighlighterColors.LOCAL_VARIABLE); + public static final TextAttributesKey PARAMETER = + createTextAttributesKey("NIX_PARAMETER", DefaultLanguageHighlighterColors.PARAMETER); + + public static final TextAttributesKey STRING = + createTextAttributesKey("NIX_STRING", DefaultLanguageHighlighterColors.STRING); + public static final TextAttributesKey STRING_ESCAPE = + createTextAttributesKey("NIX_STRING_ESCAPE", DefaultLanguageHighlighterColors.VALID_STRING_ESCAPE); + public static final TextAttributesKey URI = + createTextAttributesKey("NIX_URI", DefaultLanguageHighlighterColors.STRING); + public static final TextAttributesKey PATH = + createTextAttributesKey("NIX_PATH", DefaultLanguageHighlighterColors.STRING); + public static final TextAttributesKey NUMBER = + createTextAttributesKey("NIX_NUMBER", DefaultLanguageHighlighterColors.NUMBER); + + public static final TextAttributesKey LINE_COMMENT = + createTextAttributesKey("NIX_LINE_COMMENT", DefaultLanguageHighlighterColors.LINE_COMMENT); + public static final TextAttributesKey BLOCK_COMMENT = + createTextAttributesKey("NIX_BLOCK_COMMENT", DefaultLanguageHighlighterColors.BLOCK_COMMENT); + + private NixTextAttributes() { + // Cannot be instantiated + } +} diff --git a/src/main/java/org/nixos/idea/settings/NixColorSettingsPage.java b/src/main/java/org/nixos/idea/settings/NixColorSettingsPage.java new file mode 100644 index 00000000..a715cc86 --- /dev/null +++ b/src/main/java/org/nixos/idea/settings/NixColorSettingsPage.java @@ -0,0 +1,128 @@ +package org.nixos.idea.settings; + +import com.intellij.lang.Language; +import com.intellij.openapi.editor.colors.TextAttributesKey; +import com.intellij.openapi.fileTypes.SyntaxHighlighter; +import com.intellij.openapi.options.colors.AttributesDescriptor; +import com.intellij.openapi.options.colors.ColorDescriptor; +import com.intellij.openapi.options.colors.RainbowColorSettingsPage; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.nixos.idea.icon.NixIcons; +import org.nixos.idea.lang.NixLanguage; +import org.nixos.idea.lang.highlighter.NixRainbowVisitor; +import org.nixos.idea.lang.highlighter.NixSyntaxHighlighter; +import org.nixos.idea.lang.highlighter.NixTextAttributes; + +import javax.swing.Icon; +import java.util.Map; + +public final class NixColorSettingsPage implements RainbowColorSettingsPage { + private static final AttributesDescriptor[] DESCRIPTORS = new AttributesDescriptor[]{ + descriptor("Keyword", NixTextAttributes.KEYWORD), + descriptor("Operators//Semicolon", NixTextAttributes.SEMICOLON), + descriptor("Operators//Comma", NixTextAttributes.COMMA), + descriptor("Operators//Dot", NixTextAttributes.DOT), + descriptor("Operators//Assignment operator", NixTextAttributes.ASSIGN), + descriptor("Operators//Colon", NixTextAttributes.COLON), + descriptor("Operators//At sign (@)", NixTextAttributes.AT), + descriptor("Operators//Ellipsis", NixTextAttributes.ELLIPSIS), + descriptor("Operators//Other operators", NixTextAttributes.OPERATION_SIGN), + descriptor("Braces//Parentheses", NixTextAttributes.PARENTHESES), + descriptor("Braces//Curly braces", NixTextAttributes.BRACES), + descriptor("Braces//Brackets", NixTextAttributes.BRACKETS), + descriptor("Variables and Attributes//Other identifier", NixTextAttributes.IDENTIFIER), + descriptor("Variables and Attributes//Local variable", NixTextAttributes.LOCAL_VARIABLE), + descriptor("Variables and Attributes//Function parameter", NixTextAttributes.PARAMETER), + descriptor("Built-in constants and functions//Literals", NixTextAttributes.LITERAL), + descriptor("Built-in constants and functions//Import function", NixTextAttributes.IMPORT), + descriptor("Built-in constants and functions//Other built-ins", NixTextAttributes.BUILTIN), + descriptor("Literals and Values//Number", NixTextAttributes.NUMBER), + descriptor("Literals and Values//String", NixTextAttributes.STRING), + descriptor("Literals and Values//Escape sequence", NixTextAttributes.STRING_ESCAPE), + descriptor("Literals and Values//Path", NixTextAttributes.PATH), + descriptor("Literals and Values//URI", NixTextAttributes.URI), + descriptor("Comments//Line comment", NixTextAttributes.LINE_COMMENT), + descriptor("Comments//Block comment", NixTextAttributes.BLOCK_COMMENT), + }; + + private static final Map ADDITIONAL_HIGHLIGHTING_TAG = Map.of( + "builtin", NixTextAttributes.BUILTIN, + "import", NixTextAttributes.IMPORT, + "literal", NixTextAttributes.LITERAL, + "variable", NixTextAttributes.LOCAL_VARIABLE, + "parameter", NixTextAttributes.PARAMETER); + + @Override + public @NotNull Language getLanguage() { + return NixLanguage.INSTANCE; + } + + @Override + public boolean isRainbowType(TextAttributesKey type) { + // I think this method shall return true if the NixRainbowVisitor will always override the color of the given + // key. Unfortunately this method is not documented, but I assume the semantic highlighting will avoid using + // colors which correspond to attribute keys for which this method returns false. + return NixRainbowVisitor.RAINBOW_ATTRIBUTES.contains(type); + } + + @Override + public @NotNull Icon getIcon() { + return NixIcons.FILE; + } + + @Override + public @NotNull SyntaxHighlighter getHighlighter() { + return new NixSyntaxHighlighter(); + } + + @Override + public @NonNls @NotNull String getDemoText() { + return "/* This code demonstrates the syntax highlighting for the Nix Expression Language */\n" + + "let\n" + + " literals.null = null;\n" + + " literals.boolean = true;\n" + + " literals.number = 42;\n" + + " literals.string1 = \"This is a normal string\";\n" + + " literals.string2 = ''\n" + + " Broken escape sequence: \\${literals.number}\n" + + " Escaped interpolation: ''${literals.number}\n" + + " Generic escape sequence: $''\\{literals.number}\n" + + " '';\n" + + " literals.paths = [/etc/gitconfig ~/.gitconfig .git/config];\n" + + " # Note that unquoted URIs were deperecated by RFC 45\n" + + " literals.uri = https://github.com/NixOS/rfcs/pull/45;\n" + + "in {\n" + + " inherit (literals) number string1 string2 paths uri;\n" + + " nixpkgs = import ;\n" + + " baseNames = map baseNameOf literals.paths;\n" + + " f = { multiply ? 1, add ? 0, ... }@args:\n" + + " builtins.mapAttrs (name: value: multiply * value + add) args;\n" + + "}"; + } + + @Override + public @NotNull Map getAdditionalHighlightingTagToDescriptorMap() { + return ADDITIONAL_HIGHLIGHTING_TAG; + } + + @Override + public AttributesDescriptor @NotNull [] getAttributeDescriptors() { + return DESCRIPTORS; + } + + @Override + public ColorDescriptor @NotNull [] getColorDescriptors() { + return ColorDescriptor.EMPTY_ARRAY; + } + + @Override + public @NotNull @Nls(capitalization = Nls.Capitalization.Title) String getDisplayName() { + return "Nix"; + } + + private static @NotNull AttributesDescriptor descriptor(@NotNull @Nls(capitalization = Nls.Capitalization.Sentence) String displayName, @NotNull TextAttributesKey key) { + return new AttributesDescriptor(displayName, key); + } +} diff --git a/src/main/java/org/nixos/idea/util/NixVersion.java b/src/main/java/org/nixos/idea/util/NixVersion.java new file mode 100644 index 00000000..608c6c25 --- /dev/null +++ b/src/main/java/org/nixos/idea/util/NixVersion.java @@ -0,0 +1,33 @@ +package org.nixos.idea.util; + +/** + * Known versions of the Nix package manager. + * + * @see Release Notes + */ +public enum NixVersion { + /** + * Release 2.4 (2021-11-01). + */ + V2_04, + /** + * Release 2.5 (2021-12-13). + */ + V2_05, + /** + * Release 2.6 (2022-01-24). + */ + V2_06, + /** + * Release 2.8 (2022-04-19). + */ + V2_08, + /** + * Release 2.9 (2022-05-30). + */ + V2_09, + /** + * Release 2.10 (2022-07-11). + */ + V2_10, +} diff --git a/src/main/lang/Nix.bnf b/src/main/lang/Nix.bnf index 83e8f7f0..cb0b98c8 100644 --- a/src/main/lang/Nix.bnf +++ b/src/main/lang/Nix.bnf @@ -151,6 +151,7 @@ expr_op_base ::= expr_app // one AST node for a series of function calls. expr_app ::= expr_select ( !missing_semi expr_select ) * +;{ methods("expr_select")=[ value="/expr[0]" default="/expr[1]" ] } expr_select ::= expr_simple [ !missing_semi ( select_attr | legacy_app_or )] private select_attr ::= DOT attr_path [ select_default ] { pin=1 } private select_default ::= OR_KW expr_select { pin=1 } @@ -185,10 +186,11 @@ std_string ::= STRING_OPEN string_part* STRING_CLOSE { pin=1 } ind_string ::= IND_STRING_OPEN string_part* IND_STRING_CLOSE { pin=1 } ;{ extends("string_text|antiquotation")=string_part } string_part ::= string_text | antiquotation { recoverWhile=string_part_recover } -string_text ::= STR | IND_STR +string_text ::= string_token+ antiquotation ::= DOLLAR LCURLY expr recover_antiquotation RCURLY { pin=1 } private recover_antiquotation ::= { recoverWhile=curly_recover } -private string_part_recover ::= !(STR | IND_STR | DOLLAR | STRING_CLOSE | IND_STRING_CLOSE) +private string_part_recover ::= !(DOLLAR | STRING_CLOSE | IND_STRING_CLOSE | string_token) +private string_token ::= STR | IND_STR | STR_ESCAPE | IND_STR_ESCAPE ;{ extends("bind_attr|bind_inherit")=bind } bind ::= bind_attr | bind_inherit @@ -204,7 +206,7 @@ attr ::= std_attr | string_attr std_attr ::= ID | OR_KW string_attr ::= std_string | antiquotation -attr_path ::= attr ( DOT attr )* +attr_path ::= attr ( DOT attr )* { methods=[ firstAttr="/attr[0]" ] } // The lexer uses curly braces to determine its state. To avoid inconsistencies diff --git a/src/main/lang/Nix.flex b/src/main/lang/Nix.flex index c52ba4fb..ec9527dd 100644 --- a/src/main/lang/Nix.flex +++ b/src/main/lang/Nix.flex @@ -141,22 +141,20 @@ MCOMMENT=\/\*([^*]|\*[^\/])*\*\/ } { - ([^\$\"\\]|\$[^\{\"\\]|\\{ANY}|\$\\{ANY})*\$/\" { return STR; } - ([^\$\"\\]|\$[^\{\"\\]|\\{ANY}|\$\\{ANY})+ | - \$|\\|\$\\ { return STR; } + [^\$\"\\]+ { return STR; } + "$"|"$$"|\\ { return STR; } + \\{ANY} { return STR_ESCAPE; } "$"/"{" { pushState(ANTIQUOTATION_START); return DOLLAR; } \" { popState(); return STRING_CLOSE; } } { - ([^\$\']|\$[^\{\']|\'[^\'\$])+ | - "''$" | - \$ | - "'''" | - "''"\\{ANY} { return IND_STR; } + [^\$\']+ { return IND_STR; } + "$"|"$$"|"'" { return IND_STR; } + "''$"|"'''" { return IND_STR_ESCAPE; } + "''"\\{ANY} { return IND_STR_ESCAPE; } "$"/"{" { pushState(ANTIQUOTATION_START); return DOLLAR; } "''" { popState(); return IND_STRING_CLOSE; } - "'" { return IND_STR; } } { diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 8199fe55..1fd59e5b 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -21,7 +21,13 @@ + implementationClass="org.nixos.idea.lang.highlighter.NixSyntaxHighlighterFactory" /> + + + + + + + + diff --git a/src/main/resources/org/nixos/idea/colorSchemes/NixDarcula.xml b/src/main/resources/org/nixos/idea/colorSchemes/NixDarcula.xml new file mode 100644 index 00000000..70ae5067 --- /dev/null +++ b/src/main/resources/org/nixos/idea/colorSchemes/NixDarcula.xml @@ -0,0 +1,8 @@ + + + + diff --git a/src/main/resources/org/nixos/idea/colorSchemes/NixDefault.xml b/src/main/resources/org/nixos/idea/colorSchemes/NixDefault.xml new file mode 100644 index 00000000..64199fab --- /dev/null +++ b/src/main/resources/org/nixos/idea/colorSchemes/NixDefault.xml @@ -0,0 +1,8 @@ + + + + diff --git a/src/test/java/org/nixos/idea/_testutil/ReflectionUtils.java b/src/test/java/org/nixos/idea/_testutil/ReflectionUtils.java new file mode 100644 index 00000000..3317b377 --- /dev/null +++ b/src/test/java/org/nixos/idea/_testutil/ReflectionUtils.java @@ -0,0 +1,33 @@ +package org.nixos.idea._testutil; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Named; + +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Stream; + +public final class ReflectionUtils { + + private ReflectionUtils() { + // Cannot be instantiated. + } + + public static @NotNull Stream> getPublicStaticFieldValues(@NotNull Class owner, @NotNull Class valueType) { + return Arrays.stream(owner.getDeclaredFields()) + .filter(field -> Modifier.isPublic(field.getModifiers()) && Modifier.isStatic(field.getModifiers())) + .map(field -> { + try { + Object value = field.get(null); + if (valueType.isInstance(value)) { + String name = owner.getSimpleName() + "." + field.getName(); + return Named.of(name, valueType.cast(value)); + } + return null; + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } + }).filter(Objects::nonNull); + } +} diff --git a/src/test/java/org/nixos/idea/lang/highlighter/NixHighlightVisitorTest.java b/src/test/java/org/nixos/idea/lang/highlighter/NixHighlightVisitorTest.java new file mode 100644 index 00000000..e44eecef --- /dev/null +++ b/src/test/java/org/nixos/idea/lang/highlighter/NixHighlightVisitorTest.java @@ -0,0 +1,166 @@ +package org.nixos.idea.lang.highlighter; + +import com.intellij.codeInsight.daemon.impl.HighlightInfo; +import com.intellij.codeInsight.daemon.impl.HighlightInfoType; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.vfs.VirtualFileFilter; +import com.intellij.psi.PsiFile; +import com.intellij.psi.impl.PsiManagerEx; +import com.intellij.testFramework.ExpectedHighlightingData; +import com.intellij.testFramework.fixtures.BasePlatformTestCase; +import org.jetbrains.annotations.NotNull; +import org.nixos.idea.file.NixFileType; + +import java.lang.reflect.Field; +import java.util.Collection; + +public final class NixHighlightVisitorTest extends BasePlatformTestCase { + + public void testSelectExpression() { + // TODO: Highlight x.y as a local variable + doTest("let\n" + + " x = some_value;\n" + + " x.y = some_value;\n" + + " x.\"no-highlighting-for-string-attributes\" = some_value;\n" + + "in [\n" + + " x\n" + + " x.y\n" + + " x.y.z\n" + + " x.z\n" + + " x.\"no-highlighting-for-string-attributes\"\n" + + "]"); + } + + public void testBuiltins() { + doTest("[\n" + + " null\n" + + " true\n" + + " false\n" + + " import\n" + + " map\n" + + " builtins.null\n" + + " builtins.map\n" + + " builtins.compareVersions\n" + + " compareVersions\n" + + "]"); + } + + public void testLetExpression() { + doTest("let\n" + + " inherit (some_value) \"no-highlighting-for-string-attributes\" x;\n" + + " x = some_value;\n" + + " x.y = some_value;\n" + + " x.y.z = some_value;\n" + + " x.\"no-highlighting-for-string-attributes\" = some_value;\n" + + " copy = x;\n" + + "in [\n" + + " x\n" + + " x.y\n" + + " x.y.z\n" + + "]"); + } + + public void testLegacyLetExpression() { + doTest("let {\n" + + " inherit (some_value) \"no-highlighting-for-string-attributes\" x;\n" + + " x = some_value;\n" + + " x.y = some_value;\n" + + " x.y.z = some_value;\n" + + " x.\"no-highlighting-for-string-attributes\" = some_value;\n" + + " body = [\n" + + " x\n" + + " x.y\n" + + " x.y.z\n" + + " ];\n" + + "}"); + } + + public void testRecursiveSet() { + doTest("rec {\n" + + " inherit (some_value) \"no-highlighting-for-string-attributes\" x;\n" + + " x = some_value;\n" + + " x.y = some_value;\n" + + " x.y.z = some_value;\n" + + " x.\"no-highlighting-for-string-attributes\" = some_value;\n" + + " body = [\n" + + " x\n" + + " x.y\n" + + " x.y.z\n" + + " ];\n" + + "}"); + } + + public void testNoHighlightingForNonRecursiveSet() { + doTest("{\n" + + " inherit (some_value) \"no-highlighting-for-string-attributes\" x;\n" + + " x = some_value;\n" + + " x.y = some_value;\n" + + " x.\"no-highlighting-for-string-attributes\" = some_value;\n" + + "}"); + } + + public void testLambda() { + doTest("x:\n" + + "{y}:\n" + + "z@{za, ...}: [\n" + + " x\n" + + " y\n" + + " z\n" + + " za\n" + + "]"); + } + + public void testVariableHidesParameter() { + doTest("{f, hidden}: [\n" + + " f hidden\n" + + " (\n" + + " let hidden = some_value;\n" + + " in f hidden\n" + + " )\n" + + " (let {\n" + + " hidden = some_value;\n" + + " body = f hidden;\n" + + " })\n" + + " (rec {\n" + + " hidden = some_value;\n" + + " body = f hidden;\n" + + " })\n" + + "]"); + } + + public void testParameterHidesVariable() { + doTest("let\n" + + " f = some_value;\n" + + " hidden = some_value;\n" + + "in\n" + + " hidden:\n" + + " f hidden"); + } + + private void doTest(@NotNull String code) { + PsiFile file = myFixture.configureByText(NixFileType.INSTANCE, code); + Document document = myFixture.getEditor().getDocument(); + PsiManagerEx.getInstanceEx(getProject()).setAssertOnFileLoadingFilter(VirtualFileFilter.NONE, getTestRootDisposable()); + ExpectedHighlightingData data = new NixExpectedHighlightingData(document, false); + data.init(); + Collection infos = myFixture.doHighlighting(); + data.checkResult(file, infos, document.getText(), null); + } + + private static final class NixExpectedHighlightingData extends ExpectedHighlightingData { + public NixExpectedHighlightingData(@NotNull Document document, boolean ignoreExtraHighlighting) { + super(document, true, true, true, ignoreExtraHighlighting); + checkSymbolNames(); + } + + @Override + protected HighlightInfoType getTypeByName(String typeString) throws Exception { + try { + Field field = NixHighlightVisitorDelegate.class.getField(typeString); + return (HighlightInfoType) field.get(null); + } catch (NoSuchFieldException e) { + return super.getTypeByName(typeString); + } + } + } +} diff --git a/src/test/java/org/nixos/idea/lang/highlighter/NixRainbowVisitorTest.java b/src/test/java/org/nixos/idea/lang/highlighter/NixRainbowVisitorTest.java new file mode 100644 index 00000000..318b5e57 --- /dev/null +++ b/src/test/java/org/nixos/idea/lang/highlighter/NixRainbowVisitorTest.java @@ -0,0 +1,128 @@ +package org.nixos.idea.lang.highlighter; + +import com.intellij.testFramework.fixtures.BasePlatformTestCase; +import org.jetbrains.annotations.NotNull; + +public final class NixRainbowVisitorTest extends BasePlatformTestCase { + + public void testNoHighlightingWhenDisabled() { + myFixture.testRainbow("rainbow.nix", + "let x = y; in x", + false, false); + } + + public void testSelectExpression() { + doTest("let\n" + + " x = null;\n" + + "in [\n" + + " x\n" + + " x.y\n" + + " x.y.z\n" + + " x.z\n" + + " x.\"no-highlighting-for-string-attributes\"\n" + + "]"); + } + + public void testLetExpression() { + doTest("let\n" + + " inherit (null) \"no-highlighting-for-string-attributes\" x;\n" + + " x = null;\n" + + " x.y = null;\n" + + " x.y.z = null;\n" + + " x.\"no-highlighting-for-string-attributes\" = null;\n" + + " copy = x;\n" + + "in [\n" + + " x\n" + + " x.y\n" + + " x.y.z\n" + + "]"); + } + + public void testLegacyLetExpression() { + doTest("let {\n" + + " inherit (null) \"no-highlighting-for-string-attributes\" x;\n" + + " x = null;\n" + + " x.y = null;\n" + + " x.y.z = null;\n" + + " x.\"no-highlighting-for-string-attributes\" = null;\n" + + " body = [\n" + + " x\n" + + " x.y\n" + + " x.y.z\n" + + " ];\n" + + "}"); + } + + public void testRecursiveSet() { + doTest("rec {\n" + + " inherit (null) \"no-highlighting-for-string-attributes\" x;\n" + + " x = null;\n" + + " x.y = null;\n" + + " x.y.z = null;\n" + + " x.\"no-highlighting-for-string-attributes\" = null;\n" + + " body = [\n" + + " x\n" + + " x.y\n" + + " x.y.z\n" + + " ];\n" + + "}"); + } + + public void testNoHighlightingForNonRecursiveSet() { + doTest("{\n" + + " inherit (null) \"no-highlighting-for-string-attributes\" x;\n" + + " x = null;\n" + + " x.y = null;\n" + + " x.\"no-highlighting-for-string-attributes\" = null;\n" + + "}"); + } + + public void testLambda() { + // TODO: Ideally, za should have the same color as z.za. + doTest("x:\n" + + "{y}:\n" + + "z@{za, ...}: [\n" + + " x\n" + + " y\n" + + " z\n" + + " za\n" + + "]"); + } + + public void testUnknownSource() { + doTest("[\n" + + " x\n" + + " compareVersions\n" + + "]"); + } + + public void testNoRainbowForBuiltins() { + doTest("[\n" + + " null\n" + + " true\n" + + " false\n" + + " import\n" + + " map\n" + + " builtins.null\n" + + " builtins.map\n" + + " builtins.compareVersions\n" + + "]"); + } + + // TODO: Ideally, hidden elements should have a different color then the elements hiding them. Unfortunately, + // I haven't found a good way to implement this. + @SuppressWarnings("unused") + public void ignoreTestHidingChangesColor() { + doTest("{f, hidden}: [\n" + + " f hidden\n" + + " (let hidden = null; in f hidden)\n" + + " (let { hidden = null; body = f hidden; })\n" + + " (rec { hidden = null; body = f hidden; })\n" + + " (hidden: f hidden)\n" + + "]"); + } + + private void doTest(@NotNull String code) { + myFixture.testRainbow("rainbow.nix", code, true, true); + } +} diff --git a/src/test/java/org/nixos/idea/lang/highlighter/NixSyntaxHighlighterTest.java b/src/test/java/org/nixos/idea/lang/highlighter/NixSyntaxHighlighterTest.java new file mode 100644 index 00000000..90e9765b --- /dev/null +++ b/src/test/java/org/nixos/idea/lang/highlighter/NixSyntaxHighlighterTest.java @@ -0,0 +1,46 @@ +package org.nixos.idea.lang.highlighter; + +import com.intellij.openapi.editor.colors.TextAttributesKey; +import com.intellij.psi.TokenType; +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.nixos.idea._testutil.ReflectionUtils; +import org.nixos.idea.psi.NixTokenType; +import org.nixos.idea.psi.NixTypes; + +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +final class NixSyntaxHighlighterTest { + @Test + void testAttributesKeysForUnknownTokenType() { + TextAttributesKey[] tokenHighlights = new NixSyntaxHighlighter().getTokenHighlights(TokenType.CODE_FRAGMENT); + assertNotNull(tokenHighlights, "tokenHighlights"); + assertEquals(0, tokenHighlights.length, "tokenHighlights.length"); + } + + @ParameterizedTest + @MethodSource + void testAttributesKeysForKnownTokenTypes(@NotNull IElementType tokenType) { + TextAttributesKey[] tokenHighlights = new NixSyntaxHighlighter().getTokenHighlights(tokenType); + assertNotNull(tokenHighlights, "tokenHighlights"); + assertNotEquals(0, tokenHighlights.length, "tokenHighlights.length"); + assertAll(IntStream.range(0, tokenHighlights.length).mapToObj(index -> + () -> assertNotNull(tokenHighlights[index], String.format("tokenHighlights[%d]", index)))); + } + + static @NotNull Stream> testAttributesKeysForKnownTokenTypes() { + return Stream.concat( + Stream.of(Named.of("TokenType.BAD_CHARACTER", TokenType.BAD_CHARACTER)), + ReflectionUtils.getPublicStaticFieldValues(NixTypes.class, NixTokenType.class)); + } +} diff --git a/src/test/java/org/nixos/idea/lang/highlighter/NixTextAttributesTest.java b/src/test/java/org/nixos/idea/lang/highlighter/NixTextAttributesTest.java new file mode 100644 index 00000000..511ae155 --- /dev/null +++ b/src/test/java/org/nixos/idea/lang/highlighter/NixTextAttributesTest.java @@ -0,0 +1,43 @@ +package org.nixos.idea.lang.highlighter; + +import com.intellij.openapi.editor.colors.TextAttributesKey; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.nixos.idea._testutil.ReflectionUtils; + +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +final class NixTextAttributesTest { + @ParameterizedTest + @MethodSource + void testKeyNamesHaveNixPrefix(@NotNull TextAttributesKey key) { + assertTrue(key.getExternalName().startsWith("NIX_")); + } + + static @NotNull Stream> testKeyNamesHaveNixPrefix() { + return ReflectionUtils.getPublicStaticFieldValues(NixTextAttributes.class, TextAttributesKey.class); + } + + @ParameterizedTest + @MethodSource + void testNoDuplicateKeyNames(@NotNull TextAttributesKey key) { + Set duplicates = ReflectionUtils.getPublicStaticFieldValues(NixTextAttributes.class, TextAttributesKey.class) + .filter(other -> other.getPayload().getExternalName().equals(key.getExternalName())) + .map(Named::getName) + .collect(Collectors.toSet()); + if (duplicates.size() != 1) { + fail("Duplicates: " + duplicates); + } + } + + static @NotNull Stream> testNoDuplicateKeyNames() { + return ReflectionUtils.getPublicStaticFieldValues(NixTextAttributes.class, TextAttributesKey.class); + } +} diff --git a/src/test/java/org/nixos/idea/settings/NixColorSettingsPageTest.java b/src/test/java/org/nixos/idea/settings/NixColorSettingsPageTest.java new file mode 100644 index 00000000..5dea78dd --- /dev/null +++ b/src/test/java/org/nixos/idea/settings/NixColorSettingsPageTest.java @@ -0,0 +1,30 @@ +package org.nixos.idea.settings; + +import com.intellij.openapi.editor.colors.TextAttributesKey; +import com.intellij.openapi.options.colors.AttributesDescriptor; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.nixos.idea._testutil.ReflectionUtils; +import org.nixos.idea.lang.highlighter.NixTextAttributes; + +import java.util.Arrays; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class NixColorSettingsPageTest { + @ParameterizedTest + @MethodSource + void testNoKeyMissing(@NotNull TextAttributesKey textAttributesKey) { + AttributesDescriptor[] descriptors = new NixColorSettingsPage().getAttributeDescriptors(); + assertTrue(Arrays.stream(descriptors) + .map(AttributesDescriptor::getKey) + .anyMatch(textAttributesKey::equals)); + } + + static @NotNull Stream> testNoKeyMissing() { + return ReflectionUtils.getPublicStaticFieldValues(NixTextAttributes.class, TextAttributesKey.class); + } +} diff --git a/src/test/testData/ParsingTest/StringWithEscapeSequences.txt b/src/test/testData/ParsingTest/StringWithEscapeSequences.txt index 6aa778a0..ba58444b 100644 --- a/src/test/testData/ParsingTest/StringWithEscapeSequences.txt +++ b/src/test/testData/ParsingTest/StringWithEscapeSequences.txt @@ -5,71 +5,75 @@ Nix File(0,104) NixIndStringImpl(IND_STRING)(2,9) PsiElement(IND_STRING_OPEN)('''')(2,4) NixStringTextImpl(STRING_TEXT)(4,7) - PsiElement(IND_STR)(''''')(4,7) + PsiElement(IND_STR_ESCAPE)(''''')(4,7) PsiElement(IND_STRING_CLOSE)('''')(7,9) PsiWhiteSpace('\n')(9,10) NixStdStringImpl(STD_STRING)(10,19) PsiElement(STRING_OPEN)('"')(10,11) NixStringTextImpl(STRING_TEXT)(11,18) - PsiElement(STR)('a\${b}c')(11,18) + PsiElement(STR)('a')(11,12) + PsiElement(STR_ESCAPE)('\$')(12,14) + PsiElement(STR)('{b}c')(14,18) PsiElement(STRING_CLOSE)('"')(18,19) PsiWhiteSpace('\n')(19,20) NixIndStringImpl(IND_STRING)(20,32) PsiElement(IND_STRING_OPEN)('''')(20,22) - NixStringTextImpl(STRING_TEXT)(22,23) + NixStringTextImpl(STRING_TEXT)(22,30) PsiElement(IND_STR)('a')(22,23) - NixStringTextImpl(STRING_TEXT)(23,26) - PsiElement(IND_STR)('''$')(23,26) - NixStringTextImpl(STRING_TEXT)(26,30) + PsiElement(IND_STR_ESCAPE)('''$')(23,26) PsiElement(IND_STR)('{b}c')(26,30) PsiElement(IND_STRING_CLOSE)('''')(30,32) PsiWhiteSpace('\n')(32,33) NixStdStringImpl(STD_STRING)(33,43) PsiElement(STRING_OPEN)('"')(33,34) NixStringTextImpl(STRING_TEXT)(34,42) - PsiElement(STR)('a\n\r\tc')(34,42) + PsiElement(STR)('a')(34,35) + PsiElement(STR_ESCAPE)('\n')(35,37) + PsiElement(STR_ESCAPE)('\r')(37,39) + PsiElement(STR_ESCAPE)('\t')(39,41) + PsiElement(STR)('c')(41,42) PsiElement(STRING_CLOSE)('"')(42,43) PsiWhiteSpace('\n')(43,44) NixIndStringImpl(IND_STRING)(44,62) PsiElement(IND_STRING_OPEN)('''')(44,46) - NixStringTextImpl(STRING_TEXT)(46,47) + NixStringTextImpl(STRING_TEXT)(46,60) PsiElement(IND_STR)('a')(46,47) - NixStringTextImpl(STRING_TEXT)(47,51) - PsiElement(IND_STR)('''\n')(47,51) - NixStringTextImpl(STRING_TEXT)(51,55) - PsiElement(IND_STR)('''\r')(51,55) - NixStringTextImpl(STRING_TEXT)(55,59) - PsiElement(IND_STR)('''\t')(55,59) - NixStringTextImpl(STRING_TEXT)(59,60) + PsiElement(IND_STR_ESCAPE)('''\n')(47,51) + PsiElement(IND_STR_ESCAPE)('''\r')(51,55) + PsiElement(IND_STR_ESCAPE)('''\t')(55,59) PsiElement(IND_STR)('c')(59,60) PsiElement(IND_STRING_CLOSE)('''')(60,62) PsiWhiteSpace('\n')(62,63) NixStdStringImpl(STD_STRING)(63,69) PsiElement(STRING_OPEN)('"')(63,64) NixStringTextImpl(STRING_TEXT)(64,68) - PsiElement(STR)('a\bc')(64,68) + PsiElement(STR)('a')(64,65) + PsiElement(STR_ESCAPE)('\b')(65,67) + PsiElement(STR)('c')(67,68) PsiElement(STRING_CLOSE)('"')(68,69) PsiWhiteSpace('\n')(69,70) NixIndStringImpl(IND_STRING)(70,80) PsiElement(IND_STRING_OPEN)('''')(70,72) - NixStringTextImpl(STRING_TEXT)(72,73) + NixStringTextImpl(STRING_TEXT)(72,78) PsiElement(IND_STR)('a')(72,73) - NixStringTextImpl(STRING_TEXT)(73,77) - PsiElement(IND_STR)('''\b')(73,77) - NixStringTextImpl(STRING_TEXT)(77,78) + PsiElement(IND_STR_ESCAPE)('''\b')(73,77) PsiElement(IND_STR)('c')(77,78) PsiElement(IND_STRING_CLOSE)('''')(78,80) PsiWhiteSpace('\n')(80,81) NixStdStringImpl(STD_STRING)(81,90) PsiElement(STRING_OPEN)('"')(81,82) NixStringTextImpl(STRING_TEXT)(82,89) - PsiElement(STR)('a$${b}c')(82,89) + PsiElement(STR)('a')(82,83) + PsiElement(STR)('$$')(83,85) + PsiElement(STR)('{b}c')(85,89) PsiElement(STRING_CLOSE)('"')(89,90) PsiWhiteSpace('\n')(90,91) NixIndStringImpl(IND_STRING)(91,102) PsiElement(IND_STRING_OPEN)('''')(91,93) NixStringTextImpl(STRING_TEXT)(93,100) - PsiElement(IND_STR)('a$${b}c')(93,100) + PsiElement(IND_STR)('a')(93,94) + PsiElement(IND_STR)('$$')(94,96) + PsiElement(IND_STR)('{b}c')(96,100) PsiElement(IND_STRING_CLOSE)('''')(100,102) PsiWhiteSpace('\n')(102,103) PsiElement(])(']')(103,104)