Skip to content

Commit 7e362b5

Browse files
authored
Merge pull request #91 from NixOS/minorfeatures
Minor Features
2 parents 49af7b9 + f62ae07 commit 7e362b5

16 files changed

+663
-30
lines changed

.idea/codeStyles/Project.xml

+2-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
- Experimental support for resolving variables.
88
The feature is disabled by default since the functionality is rather limited for now.
99
Feel free to comment your feedback at [issue #87](https://github.com/NixOS/nix-idea/issues/87).
10+
- Support for simple spell checking
11+
- Automatic insertion of closing quotes
12+
- Support for *Code | Move Element Left/Right* (<kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>←/→</kbd>)
1013

1114
### Changed
1215

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.nixos.idea.lang;
2+
3+
import com.intellij.codeInsight.editorActions.moveLeftRight.MoveElementLeftRightHandler;
4+
import com.intellij.psi.PsiElement;
5+
import org.jetbrains.annotations.NotNull;
6+
import org.nixos.idea.psi.NixBindInherit;
7+
import org.nixos.idea.psi.NixExprApp;
8+
import org.nixos.idea.psi.NixExprAttrs;
9+
import org.nixos.idea.psi.NixExprLambda;
10+
import org.nixos.idea.psi.NixExprLet;
11+
import org.nixos.idea.psi.NixExprList;
12+
import org.nixos.idea.psi.NixFormals;
13+
import org.nixos.idea.psi.NixPsiUtil;
14+
15+
import java.util.Collection;
16+
17+
public final class NixMoveElementLeftRightHandler extends MoveElementLeftRightHandler {
18+
@Override
19+
public PsiElement @NotNull [] getMovableSubElements(@NotNull PsiElement element) {
20+
if (element instanceof NixExprList list) {
21+
return asArray(list.getItems());
22+
} else if (element instanceof NixBindInherit inherit) {
23+
return asArray(inherit.getAttributes());
24+
} else if (element instanceof NixExprAttrs attrs) {
25+
return asArray(attrs.getBindList());
26+
} else if (element instanceof NixExprLet let) {
27+
return asArray(let.getBindList());
28+
} else if (element instanceof NixExprLambda lambda) {
29+
return new PsiElement[]{lambda.getArgument(), lambda.getFormals()};
30+
} else if (element instanceof NixFormals formals) {
31+
return asArray(formals.getFormalList());
32+
} else if (element instanceof NixExprApp app) {
33+
return asArray(NixPsiUtil.getArguments(app));
34+
} else {
35+
return PsiElement.EMPTY_ARRAY;
36+
}
37+
}
38+
39+
private PsiElement @NotNull [] asArray(@NotNull Collection<? extends PsiElement> items) {
40+
return items.toArray(PsiElement[]::new);
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package org.nixos.idea.lang;
2+
3+
import com.intellij.codeInsight.editorActions.MultiCharQuoteHandler;
4+
import com.intellij.codeInsight.editorActions.QuoteHandler;
5+
import com.intellij.openapi.editor.Document;
6+
import com.intellij.openapi.editor.Editor;
7+
import com.intellij.openapi.editor.highlighter.HighlighterIterator;
8+
import com.intellij.psi.tree.IElementType;
9+
import com.intellij.psi.tree.TokenSet;
10+
import org.jetbrains.annotations.NotNull;
11+
import org.jetbrains.annotations.Nullable;
12+
import org.nixos.idea.psi.NixTypes;
13+
14+
/**
15+
* Quote handler for the Nix Language.
16+
* This class handles the automatic insertion of closing quotes after the user enters opening quotes.
17+
* The methods are called in the following order whenever the user enters a quote character.
18+
* <ol>
19+
* <li>{@link #isClosingQuote(HighlighterIterator, int)} is only called if the typed quote character already exists just behind the caret.
20+
* If the method returns {@code true}, the insertion of the quote and all further methods will be skipped.
21+
* The caret will just move over the existing quote one character to the right.
22+
* <li><em>*Insert quote character.*</em>
23+
* <li>Handling of {@link MultiCharQuoteHandler}:<ol>
24+
* <li>{@link #getClosingQuote(HighlighterIterator, int)} is called with the offset behind the inserted quote.
25+
* The returned value represents the string which shall be inserted.
26+
* Can return {@code null} to skipp further processing of {@code MultiCharQuoteHandler}.
27+
* <li>{@link #hasNonClosedLiteral(Editor, HighlighterIterator, int)} is called with the offset before the inserted quote.
28+
* The closing quotes returned by the previous method will only be inserted if this method returns {@code true}.
29+
* <li><em>*Insert closing quotes as returned by {@code getClosingQuote(...)}.*</em>
30+
* </ol>
31+
* <li>Standard handling of {@link QuoteHandler} (skipped if closing quotes were already inserted):<ol>
32+
* <li>{@link #isOpeningQuote(HighlighterIterator, int)} is called with the offset before the inserted quote.
33+
* The following steps will only be executed if this method returns {@code true}.
34+
* <li>{@link #hasNonClosedLiteral(Editor, HighlighterIterator, int)} is called with the offset before the inserted quote.
35+
* The following steps will only be executed if this method returns {@code true}.
36+
* <li><em>*Insert same quote character as initially typed by the user again.*</em>
37+
* </ol>
38+
* </ol>
39+
*
40+
* @see <a href="https://plugins.jetbrains.com/docs/intellij/additional-minor-features.html#quote-handling">Quote Handling Documentation</a>
41+
*/
42+
public final class NixQuoteHandler implements MultiCharQuoteHandler {
43+
44+
private static final TokenSet CLOSING_QUOTE = TokenSet.create(NixTypes.STRING_CLOSE, NixTypes.IND_STRING_CLOSE);
45+
private static final TokenSet OPENING_QUOTE = TokenSet.create(NixTypes.STRING_OPEN, NixTypes.IND_STRING_OPEN);
46+
private static final TokenSet STRING_CONTENT = TokenSet.create(NixTypes.STR, NixTypes.STR_ESCAPE, NixTypes.IND_STR, NixTypes.IND_STR_ESCAPE);
47+
private static final TokenSet STRING_ANY = TokenSet.orSet(CLOSING_QUOTE, OPENING_QUOTE, STRING_CONTENT);
48+
49+
@Override
50+
public boolean isClosingQuote(HighlighterIterator iterator, int offset) {
51+
return CLOSING_QUOTE.contains(iterator.getTokenType());
52+
}
53+
54+
@Override
55+
public boolean isOpeningQuote(HighlighterIterator iterator, int offset) {
56+
// This method comes from QuoteHandler and assumes the quote is only one char in size.
57+
// We therefore ignore indented strings ('') in this method.
58+
// Note that this method is not actually used for the insertion of closing quotes,
59+
// as that is already handled by MultiCharQuoteHandler.getClosingQuote(...). See class documentation.
60+
// However, this method is also called by BackspaceHandler to delete the closing quotes of an empty string
61+
// when the opening quotes are removed.
62+
return NixTypes.STRING_OPEN == iterator.getTokenType();
63+
}
64+
65+
@Override
66+
public boolean hasNonClosedLiteral(Editor editor, HighlighterIterator iterator, int offset) {
67+
IElementType openingToken = iterator.getTokenType();
68+
if (iterator.getEnd() != offset + 1) {
69+
return false; // The caret isn't behind the opening quote.
70+
} else if (openingToken == NixTypes.STRING_OPEN) {
71+
// Insert closing quotes only if we would otherwise get a non-closed string at the end of the line.
72+
Document doc = editor.getDocument();
73+
int lineEnd = doc.getLineEndOffset(doc.getLineNumber(offset));
74+
while (true) {
75+
IElementType lastToken = iterator.getTokenType();
76+
iterator.advance();
77+
if (iterator.atEnd() || iterator.getStart() >= lineEnd) {
78+
return STRING_ANY.contains(lastToken) && !CLOSING_QUOTE.contains(lastToken);
79+
}
80+
}
81+
} else if (openingToken == NixTypes.IND_STRING_OPEN) {
82+
// Insert closing quotes only if we would otherwise get a non-closed string at the end of the file.
83+
while (true) {
84+
IElementType lastToken = iterator.getTokenType();
85+
iterator.advance();
86+
if (iterator.atEnd()) {
87+
return STRING_ANY.contains(lastToken) && !CLOSING_QUOTE.contains(lastToken);
88+
}
89+
}
90+
}
91+
return false;
92+
}
93+
94+
@Override
95+
public boolean isInsideLiteral(HighlighterIterator iterator) {
96+
// Not sure why we need this. It seems to enable some special handling for escape sequences in IDEA.
97+
return STRING_ANY.contains(iterator.getTokenType());
98+
}
99+
100+
@Override
101+
public @Nullable CharSequence getClosingQuote(@NotNull HighlighterIterator iterator, int offset) {
102+
// May need to retreat iterator by one token.
103+
// In contrast to all the other methods, this method is called with the offset behind the inserted quote.
104+
// However, the iterator may already be at the right location if the offset is at the end of the file.
105+
if (iterator.getEnd() != offset) {
106+
iterator.retreat();
107+
if (iterator.atEnd()) {
108+
return null; // There was no previous token
109+
}
110+
}
111+
IElementType tokenType = iterator.getTokenType();
112+
return tokenType == NixTypes.STRING_OPEN ? "\"" :
113+
tokenType == NixTypes.IND_STRING_OPEN ? "''" :
114+
null;
115+
}
116+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package org.nixos.idea.lang;
2+
3+
import com.intellij.psi.PsiElement;
4+
import com.intellij.spellchecker.inspections.IdentifierSplitter;
5+
import com.intellij.spellchecker.tokenizer.SpellcheckingStrategy;
6+
import com.intellij.spellchecker.tokenizer.Tokenizer;
7+
import com.intellij.spellchecker.tokenizer.TokenizerBase;
8+
import org.jetbrains.annotations.NotNull;
9+
import org.nixos.idea.psi.NixIdentifier;
10+
import org.nixos.idea.psi.NixPsiUtil;
11+
import org.nixos.idea.psi.NixStringText;
12+
13+
/**
14+
* Enables spell checking for Nix files.
15+
*
16+
* @see <a href="https://plugins.jetbrains.com/docs/intellij/spell-checking.html">Spell Checking Documentation</a>
17+
* @see <a href="https://plugins.jetbrains.com/docs/intellij/spell-checking-strategy.html">Spell Checking Tutorial</a>
18+
*/
19+
public final class NixSpellcheckingStrategy extends SpellcheckingStrategy {
20+
21+
// TODO: Implement SuppressibleSpellcheckingStrategy
22+
// https://plugins.jetbrains.com/docs/intellij/spell-checking.html#suppressing-spellchecking
23+
// TODO: Suggest rename-refactoring for identifiers (when rename refactoring is supported)
24+
25+
private static final Tokenizer<NixIdentifier> IDENTIFIER_TOKENIZER = TokenizerBase.create(IdentifierSplitter.getInstance());
26+
27+
@Override
28+
public @NotNull Tokenizer<?> getTokenizer(PsiElement element) {
29+
if (element instanceof NixIdentifier identifier && NixPsiUtil.isDeclaration(identifier)) {
30+
return IDENTIFIER_TOKENIZER;
31+
}
32+
if (element instanceof NixStringText) {
33+
return TEXT_TOKENIZER;
34+
}
35+
return super.getTokenizer(element);
36+
}
37+
}

src/main/java/org/nixos/idea/lang/highlighter/NixHighlightVisitorDelegate.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ void visit(@NotNull PsiElement element) {
7575
}
7676
} else if (element instanceof NixStdAttr attr &&
7777
attr.getParent() instanceof NixBindInherit bindInherit &&
78-
bindInherit.getExpr() == null) {
78+
bindInherit.getSource() == null) {
7979
String identifier = attr.getText();
8080
PsiElement source = findSource(attr, identifier);
8181
highlight(attr, source, identifier);
@@ -144,8 +144,8 @@ private static boolean iterateVariables(@NotNull List<NixBind> bindList, boolean
144144
}
145145
} else if (bind instanceof NixBindInherit bindInherit) {
146146
// `let { inherit x; } in ...` does not actually introduce a new variable
147-
if (bindInherit.getExpr() != null) {
148-
for (NixAttr attr : bindInherit.getAttrList()) {
147+
if (bindInherit.getSource() != null) {
148+
for (NixAttr attr : bindInherit.getAttributes()) {
149149
if (attr instanceof NixStdAttr && action.test(attr, fullPath ? attr.getText() : null)) {
150150
return true;
151151
}

src/main/java/org/nixos/idea/psi/NixPsiUtil.java

+15
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ public int size() {
5353
};
5454
}
5555

56+
public static @NotNull List<NixExpr> getArguments(@NotNull NixExprApp app) {
57+
List<NixExpr> expressions = app.getExprList();
58+
return expressions.subList(1, expressions.size());
59+
}
60+
5661
/**
5762
* Returns the static name of an attribute.
5863
* Is {@code null} for dynamic attributes.
@@ -75,4 +80,14 @@ public int size() {
7580
return null;
7681
}
7782
}
83+
84+
public static boolean isDeclaration(@NotNull NixIdentifier identifier) {
85+
return identifier instanceof NixParameterName ||
86+
identifier instanceof NixAttr attr && isDeclaration(attr);
87+
}
88+
89+
public static boolean isDeclaration(@NotNull NixAttr attr) {
90+
return attr.getParent() instanceof NixAttrPath path &&
91+
path.getParent() instanceof NixBindAttr;
92+
}
7893
}

src/main/java/org/nixos/idea/psi/impl/AbstractNixDeclarationHost.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.nixos.idea.psi.NixAttr;
1010
import org.nixos.idea.psi.NixAttrPath;
1111
import org.nixos.idea.psi.NixBind;
12+
import org.nixos.idea.psi.NixBindAttr;
1213
import org.nixos.idea.psi.NixBindInherit;
1314
import org.nixos.idea.psi.NixDeclarationHost;
1415
import org.nixos.idea.psi.NixExprAttrs;
@@ -116,11 +117,11 @@ public final boolean isDeclaringVariables() {
116117
private void collectBindDeclarations(@NotNull Symbols result, @NotNull List<NixBind> bindList, boolean isVariable) {
117118
NixUserSymbol.Type type = isVariable ? NixUserSymbol.Type.VARIABLE : NixUserSymbol.Type.ATTRIBUTE;
118119
for (NixBind bind : bindList) {
119-
if (bind instanceof NixBindAttrImpl bindAttr) {
120+
if (bind instanceof NixBindAttr bindAttr) {
120121
result.addBindAttr(bindAttr, bindAttr.getAttrPath(), type);
121122
} else if (bind instanceof NixBindInherit bindInherit) {
122-
for (NixAttr inheritedAttribute : bindInherit.getAttrList()) {
123-
result.addInherit(bindInherit, inheritedAttribute, type, bindInherit.getExpr() != null);
123+
for (NixAttr inheritedAttribute : bindInherit.getAttributes()) {
124+
result.addInherit(bindInherit, inheritedAttribute, type, bindInherit.getSource() != null);
124125
}
125126
} else {
126127
LOG.error("Unexpected NixBind implementation: " + bind.getClass());

src/main/java/org/nixos/idea/psi/impl/AbstractNixPsiElement.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ abstract class AbstractNixPsiElement extends ASTWrapperPsiElement implements Nix
6868
// TODO: Attribute reference support
6969
return List.of();
7070
} else if (this instanceof NixBindInherit bindInherit) {
71-
NixExpr accessedObject = bindInherit.getExpr();
71+
NixExpr accessedObject = bindInherit.getSource();
7272
if (accessedObject == null) {
73-
return bindInherit.getAttrList().stream().flatMap(attr -> {
73+
return bindInherit.getAttributes().stream().flatMap(attr -> {
7474
String variableName = NixPsiUtil.getAttributeName(attr);
7575
if (variableName == null) {
7676
return Stream.empty();

src/main/lang/Nix.bnf

+9-7
Original file line numberDiff line numberDiff line change
@@ -104,15 +104,17 @@ expr_with ::= WITH expr SEMI expr { pin=1 }
104104
private recover_let ::= { recoverWhile=let_recover }
105105
private let_recover ::= braces_recover !(ASSERT | SEMI | IF | THEN | ELSE | LET | IN | WITH) !bind
106106

107+
;{ methods("argument|formal|parameter")=[ identifier="parameter_name" ] }
107108
expr_lambda ::= lambda_params !missing_semi COLON expr { pin=3 }
108109
private lambda_params ::= argument [ !missing_semi AT formals ] | formals [ !missing_semi AT argument ]
109-
argument ::= <<identifier ID>> { implements=parameter }
110+
argument ::= parameter_name { implements=parameter }
110111
formals ::= LCURLY ( formal COMMA )* [ ( ELLIPSIS | formal ) ] recover_formals RCURLY { pin=1 }
111-
formal ::= <<identifier ID>> [ formal_has ] { pin=2 recoverWhile=formal_recover implements=parameter }
112+
formal ::= parameter_name [ formal_has ] { pin=2 recoverWhile=formal_recover implements=parameter }
112113
private formal_has ::= HAS expr { pin=1 }
113114
private formal_recover ::= curly_recover !COMMA
114115
private recover_formals ::= { recoverWhile=curly_recover }
115-
fake parameter ::= identifier
116+
fake parameter ::= parameter_name
117+
parameter_name ::= ID { implements=identifier }
116118

117119
// Note that the rules for expr_op.* use a special processing mode of
118120
// Grammar-Kit. Left recursion would not be possible otherwise.
@@ -161,7 +163,7 @@ expr_op_base ::= expr_app
161163
// Grammar-Kit cannot handle "expr_app ::= expr_app expr_select_or_legacy" or
162164
// equivalent rules. As a workaround, we use this rule which will only create
163165
// one AST node for a series of function calls.
164-
expr_app ::= expr_select ( !missing_semi expr_select ) *
166+
expr_app ::= expr_select ( !missing_semi expr_select ) * { methods=[ lambda="/expr[0]" ] }
165167

166168
;{ methods("expr_select")=[ value="/expr[0]" default="/expr[1]" ] }
167169
expr_select ::= expr_simple [ !missing_semi ( select_attr | legacy_app_or )]
@@ -194,7 +196,7 @@ expr_lookup_path ::= SPATH
194196
expr_std_path ::= PATH_SEGMENT (PATH_SEGMENT | antiquotation)* PATH_END
195197
expr_parens ::= LPAREN expr recover_parens RPAREN { pin=1 }
196198
expr_attrs ::= [ REC | LET ] LCURLY recover_set (bind recover_set)* RCURLY { pin=2 }
197-
expr_list ::= LBRAC recover_list (expr_select recover_list)* RBRAC { pin=1 }
199+
expr_list ::= LBRAC recover_list (expr_select recover_list)* RBRAC { pin=1 methods=[ items="expr" ] }
198200
private recover_parens ::= { recoverWhile=paren_recover }
199201
private recover_set ::= { recoverWhile=set_recover }
200202
private recover_list ::= { recoverWhile=list_recover }
@@ -217,7 +219,7 @@ private string_token ::= STR | IND_STR | STR_ESCAPE | IND_STR_ESCAPE
217219
bind ::= bind_attr | bind_inherit
218220
bind_attr ::= attr_path ASSIGN bind_value SEMI { pin=2 }
219221
bind_value ::= <<parseBindValue expr0>> { elementType=expr }
220-
bind_inherit ::= INHERIT [ LPAREN expr RPAREN ] attr* SEMI { pin=1 }
222+
bind_inherit ::= INHERIT [ LPAREN expr RPAREN ] attr* SEMI { pin=1 methods=[ source="expr" attributes="attr" ] }
221223
// Is used in various rules just to provide a better error message when a
222224
// semicolon is missing. Must always be used with `!`.
223225
private missing_semi ::= <<parseIsBindValue>> ( RCURLY | IN | bind )
@@ -231,7 +233,7 @@ attr_path ::= attr ( DOT attr )* { methods=[ firstAttr="/attr[0]" ] }
231233

232234

233235
// Interface for identifiers.
234-
meta identifier ::= <<p>>
236+
fake identifier ::= ID | OR_KW
235237

236238
// The lexer uses curly braces to determine its state. To avoid inconsistencies
237239
// between the parser and lexer (i.e. the lexer sees a string where the parser

src/main/resources/META-INF/plugin.xml

+12
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,26 @@
2727
<additionalTextAttributes scheme="Default" file="colorSchemes/NixDefault.xml"/>
2828
<additionalTextAttributes scheme="Darcula" file="colorSchemes/NixDarcula.xml"/>
2929

30+
<spellchecker.support
31+
language="Nix"
32+
implementationClass="org.nixos.idea.lang.NixSpellcheckingStrategy"/>
33+
3034
<lang.braceMatcher
3135
language="Nix"
3236
implementationClass="org.nixos.idea.lang.NixBraceMatcher"/>
3337

38+
<lang.quoteHandler
39+
language="Nix"
40+
implementationClass="org.nixos.idea.lang.NixQuoteHandler"/>
41+
3442
<lang.commenter
3543
language="Nix"
3644
implementationClass="org.nixos.idea.lang.NixCommenter"/>
3745

46+
<moveLeftRightHandler
47+
language="Nix"
48+
implementationClass="org.nixos.idea.lang.NixMoveElementLeftRightHandler"/>
49+
3850
<searcher forClass="com.intellij.find.usages.api.UsageSearchParameters"
3951
implementationClass="org.nixos.idea.lang.references.NixUsageSearcher"/>
4052

0 commit comments

Comments
 (0)