Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minor Features #91

Merged
merged 3 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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

### Changed

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.nixos.idea.lang;

import com.intellij.codeInsight.editorActions.moveLeftRight.MoveElementLeftRightHandler;
import com.intellij.psi.PsiElement;
import org.jetbrains.annotations.NotNull;
import org.nixos.idea.psi.NixBindInherit;
import org.nixos.idea.psi.NixExprApp;
import org.nixos.idea.psi.NixExprAttrs;
import org.nixos.idea.psi.NixExprLambda;
import org.nixos.idea.psi.NixExprLet;
import org.nixos.idea.psi.NixExprList;
import org.nixos.idea.psi.NixFormals;
import org.nixos.idea.psi.NixPsiUtil;

import java.util.Collection;

public final class NixMoveElementLeftRightHandler extends MoveElementLeftRightHandler {
@Override
public PsiElement @NotNull [] getMovableSubElements(@NotNull PsiElement element) {
if (element instanceof NixExprList list) {
return asArray(list.getItems());
} else if (element instanceof NixBindInherit inherit) {
return asArray(inherit.getAttributes());
} else if (element instanceof NixExprAttrs attrs) {
return asArray(attrs.getBindList());
} else if (element instanceof NixExprLet let) {
return asArray(let.getBindList());
} else if (element instanceof NixExprLambda lambda) {
return new PsiElement[]{lambda.getArgument(), lambda.getFormals()};
} else if (element instanceof NixFormals formals) {
return asArray(formals.getFormalList());
} else if (element instanceof NixExprApp app) {
return asArray(NixPsiUtil.getArguments(app));
} else {
return PsiElement.EMPTY_ARRAY;
}
}

private PsiElement @NotNull [] asArray(@NotNull Collection<? extends PsiElement> items) {
return items.toArray(PsiElement[]::new);
}
}
116 changes: 116 additions & 0 deletions src/main/java/org/nixos/idea/lang/NixQuoteHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package org.nixos.idea.lang;

import com.intellij.codeInsight.editorActions.MultiCharQuoteHandler;
import com.intellij.codeInsight.editorActions.QuoteHandler;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.highlighter.HighlighterIterator;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.nixos.idea.psi.NixTypes;

/**
* Quote handler for the Nix Language.
* This class handles the automatic insertion of closing quotes after the user enters opening quotes.
* The methods are called in the following order whenever the user enters a quote character.
* <ol>
* <li>{@link #isClosingQuote(HighlighterIterator, int)} is only called if the typed quote character already exists just behind the caret.
* If the method returns {@code true}, the insertion of the quote and all further methods will be skipped.
* The caret will just move over the existing quote one character to the right.
* <li><em>*Insert quote character.*</em>
* <li>Handling of {@link MultiCharQuoteHandler}:<ol>
* <li>{@link #getClosingQuote(HighlighterIterator, int)} is called with the offset behind the inserted quote.
* The returned value represents the string which shall be inserted.
* Can return {@code null} to skipp further processing of {@code MultiCharQuoteHandler}.
* <li>{@link #hasNonClosedLiteral(Editor, HighlighterIterator, int)} is called with the offset before the inserted quote.
* The closing quotes returned by the previous method will only be inserted if this method returns {@code true}.
* <li><em>*Insert closing quotes as returned by {@code getClosingQuote(...)}.*</em>
* </ol>
* <li>Standard handling of {@link QuoteHandler} (skipped if closing quotes were already inserted):<ol>
* <li>{@link #isOpeningQuote(HighlighterIterator, int)} is called with the offset before the inserted quote.
* The following steps will only be executed if this method returns {@code true}.
* <li>{@link #hasNonClosedLiteral(Editor, HighlighterIterator, int)} is called with the offset before the inserted quote.
* The following steps will only be executed if this method returns {@code true}.
* <li><em>*Insert same quote character as initially typed by the user again.*</em>
* </ol>
* </ol>
*
* @see <a href="https://plugins.jetbrains.com/docs/intellij/additional-minor-features.html#quote-handling">Quote Handling Documentation</a>
*/
public final class NixQuoteHandler implements MultiCharQuoteHandler {

private static final TokenSet CLOSING_QUOTE = TokenSet.create(NixTypes.STRING_CLOSE, NixTypes.IND_STRING_CLOSE);
private static final TokenSet OPENING_QUOTE = TokenSet.create(NixTypes.STRING_OPEN, NixTypes.IND_STRING_OPEN);
private static final TokenSet STRING_CONTENT = TokenSet.create(NixTypes.STR, NixTypes.STR_ESCAPE, NixTypes.IND_STR, NixTypes.IND_STR_ESCAPE);
private static final TokenSet STRING_ANY = TokenSet.orSet(CLOSING_QUOTE, OPENING_QUOTE, STRING_CONTENT);

@Override
public boolean isClosingQuote(HighlighterIterator iterator, int offset) {
return CLOSING_QUOTE.contains(iterator.getTokenType());
}

@Override
public boolean isOpeningQuote(HighlighterIterator iterator, int offset) {
// This method comes from QuoteHandler and assumes the quote is only one char in size.
// We therefore ignore indented strings ('') in this method.
// Note that this method is not actually used for the insertion of closing quotes,
// as that is already handled by MultiCharQuoteHandler.getClosingQuote(...). See class documentation.
// However, this method is also called by BackspaceHandler to delete the closing quotes of an empty string
// when the opening quotes are removed.
return NixTypes.STRING_OPEN == iterator.getTokenType();
}

@Override
public boolean hasNonClosedLiteral(Editor editor, HighlighterIterator iterator, int offset) {
IElementType openingToken = iterator.getTokenType();
if (iterator.getEnd() != offset + 1) {
return false; // The caret isn't behind the opening quote.
} else if (openingToken == NixTypes.STRING_OPEN) {
// Insert closing quotes only if we would otherwise get a non-closed string at the end of the line.
Document doc = editor.getDocument();
int lineEnd = doc.getLineEndOffset(doc.getLineNumber(offset));
while (true) {
IElementType lastToken = iterator.getTokenType();
iterator.advance();
if (iterator.atEnd() || iterator.getStart() >= lineEnd) {
return STRING_ANY.contains(lastToken) && !CLOSING_QUOTE.contains(lastToken);
}
}
} else if (openingToken == NixTypes.IND_STRING_OPEN) {
// Insert closing quotes only if we would otherwise get a non-closed string at the end of the file.
while (true) {
IElementType lastToken = iterator.getTokenType();
iterator.advance();
if (iterator.atEnd()) {
return STRING_ANY.contains(lastToken) && !CLOSING_QUOTE.contains(lastToken);
}
}
}
return false;
}

@Override
public boolean isInsideLiteral(HighlighterIterator iterator) {
// Not sure why we need this. It seems to enable some special handling for escape sequences in IDEA.
return STRING_ANY.contains(iterator.getTokenType());
}

@Override
public @Nullable CharSequence getClosingQuote(@NotNull HighlighterIterator iterator, int offset) {
// May need to retreat iterator by one token.
// In contrast to all the other methods, this method is called with the offset behind the inserted quote.
// However, the iterator may already be at the right location if the offset is at the end of the file.
if (iterator.getEnd() != offset) {
iterator.retreat();
if (iterator.atEnd()) {
return null; // There was no previous token
}
}
IElementType tokenType = iterator.getTokenType();
return tokenType == NixTypes.STRING_OPEN ? "\"" :
tokenType == NixTypes.IND_STRING_OPEN ? "''" :
null;
}
}
37 changes: 37 additions & 0 deletions src/main/java/org/nixos/idea/lang/NixSpellcheckingStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.nixos.idea.lang;

import com.intellij.psi.PsiElement;
import com.intellij.spellchecker.inspections.IdentifierSplitter;
import com.intellij.spellchecker.tokenizer.SpellcheckingStrategy;
import com.intellij.spellchecker.tokenizer.Tokenizer;
import com.intellij.spellchecker.tokenizer.TokenizerBase;
import org.jetbrains.annotations.NotNull;
import org.nixos.idea.psi.NixIdentifier;
import org.nixos.idea.psi.NixPsiUtil;
import org.nixos.idea.psi.NixStringText;

/**
* Enables spell checking for Nix files.
*
* @see <a href="https://plugins.jetbrains.com/docs/intellij/spell-checking.html">Spell Checking Documentation</a>
* @see <a href="https://plugins.jetbrains.com/docs/intellij/spell-checking-strategy.html">Spell Checking Tutorial</a>
*/
public final class NixSpellcheckingStrategy extends SpellcheckingStrategy {

// TODO: Implement SuppressibleSpellcheckingStrategy
// https://plugins.jetbrains.com/docs/intellij/spell-checking.html#suppressing-spellchecking
// TODO: Suggest rename-refactoring for identifiers (when rename refactoring is supported)

private static final Tokenizer<NixIdentifier> IDENTIFIER_TOKENIZER = TokenizerBase.create(IdentifierSplitter.getInstance());

@Override
public @NotNull Tokenizer<?> getTokenizer(PsiElement element) {
if (element instanceof NixIdentifier identifier && NixPsiUtil.isDeclaration(identifier)) {
return IDENTIFIER_TOKENIZER;
}
if (element instanceof NixStringText) {
return TEXT_TOKENIZER;
}
return super.getTokenizer(element);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ void visit(@NotNull PsiElement element) {
}
} else if (element instanceof NixStdAttr attr &&
attr.getParent() instanceof NixBindInherit bindInherit &&
bindInherit.getExpr() == null) {
bindInherit.getSource() == null) {
String identifier = attr.getText();
PsiElement source = findSource(attr, identifier);
highlight(attr, source, identifier);
Expand Down Expand Up @@ -144,8 +144,8 @@ private static boolean iterateVariables(@NotNull List<NixBind> bindList, boolean
}
} else if (bind instanceof NixBindInherit bindInherit) {
// `let { inherit x; } in ...` does not actually introduce a new variable
if (bindInherit.getExpr() != null) {
for (NixAttr attr : bindInherit.getAttrList()) {
if (bindInherit.getSource() != null) {
for (NixAttr attr : bindInherit.getAttributes()) {
if (attr instanceof NixStdAttr && action.test(attr, fullPath ? attr.getText() : null)) {
return true;
}
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/org/nixos/idea/psi/NixPsiUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ public int size() {
};
}

public static @NotNull List<NixExpr> getArguments(@NotNull NixExprApp app) {
List<NixExpr> expressions = app.getExprList();
return expressions.subList(1, expressions.size());
}

/**
* Returns the static name of an attribute.
* Is {@code null} for dynamic attributes.
Expand All @@ -75,4 +80,14 @@ public int size() {
return null;
}
}

public static boolean isDeclaration(@NotNull NixIdentifier identifier) {
return identifier instanceof NixParameterName ||
identifier instanceof NixAttr attr && isDeclaration(attr);
}

public static boolean isDeclaration(@NotNull NixAttr attr) {
return attr.getParent() instanceof NixAttrPath path &&
path.getParent() instanceof NixBindAttr;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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.NixDeclarationHost;
import org.nixos.idea.psi.NixExprAttrs;
Expand Down Expand Up @@ -116,11 +117,11 @@ public final boolean isDeclaringVariables() {
private void collectBindDeclarations(@NotNull Symbols result, @NotNull List<NixBind> bindList, boolean isVariable) {
NixUserSymbol.Type type = isVariable ? NixUserSymbol.Type.VARIABLE : NixUserSymbol.Type.ATTRIBUTE;
for (NixBind bind : bindList) {
if (bind instanceof NixBindAttrImpl bindAttr) {
if (bind instanceof NixBindAttr bindAttr) {
result.addBindAttr(bindAttr, bindAttr.getAttrPath(), type);
} else if (bind instanceof NixBindInherit bindInherit) {
for (NixAttr inheritedAttribute : bindInherit.getAttrList()) {
result.addInherit(bindInherit, inheritedAttribute, type, bindInherit.getExpr() != null);
for (NixAttr inheritedAttribute : bindInherit.getAttributes()) {
result.addInherit(bindInherit, inheritedAttribute, type, bindInherit.getSource() != null);
}
} else {
LOG.error("Unexpected NixBind implementation: " + bind.getClass());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ abstract class AbstractNixPsiElement extends ASTWrapperPsiElement implements Nix
// TODO: Attribute reference support
return List.of();
} else if (this instanceof NixBindInherit bindInherit) {
NixExpr accessedObject = bindInherit.getExpr();
NixExpr accessedObject = bindInherit.getSource();
if (accessedObject == null) {
return bindInherit.getAttrList().stream().flatMap(attr -> {
return bindInherit.getAttributes().stream().flatMap(attr -> {
String variableName = NixPsiUtil.getAttributeName(attr);
if (variableName == null) {
return Stream.empty();
Expand Down
16 changes: 9 additions & 7 deletions src/main/lang/Nix.bnf
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,17 @@ expr_with ::= WITH expr SEMI expr { pin=1 }
private recover_let ::= { recoverWhile=let_recover }
private let_recover ::= braces_recover !(ASSERT | SEMI | IF | THEN | ELSE | LET | IN | WITH) !bind

;{ methods("argument|formal|parameter")=[ identifier="parameter_name" ] }
expr_lambda ::= lambda_params !missing_semi COLON expr { pin=3 }
private lambda_params ::= argument [ !missing_semi AT formals ] | formals [ !missing_semi AT argument ]
argument ::= <<identifier ID>> { implements=parameter }
argument ::= parameter_name { implements=parameter }
formals ::= LCURLY ( formal COMMA )* [ ( ELLIPSIS | formal ) ] recover_formals RCURLY { pin=1 }
formal ::= <<identifier ID>> [ formal_has ] { pin=2 recoverWhile=formal_recover implements=parameter }
formal ::= parameter_name [ formal_has ] { pin=2 recoverWhile=formal_recover implements=parameter }
private formal_has ::= HAS expr { pin=1 }
private formal_recover ::= curly_recover !COMMA
private recover_formals ::= { recoverWhile=curly_recover }
fake parameter ::= identifier
fake parameter ::= parameter_name
parameter_name ::= ID { implements=identifier }

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

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


// Interface for identifiers.
meta identifier ::= <<p>>
fake identifier ::= ID | OR_KW

// The lexer uses curly braces to determine its state. To avoid inconsistencies
// between the parser and lexer (i.e. the lexer sees a string where the parser
Expand Down
12 changes: 12 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,26 @@
<additionalTextAttributes scheme="Default" file="colorSchemes/NixDefault.xml"/>
<additionalTextAttributes scheme="Darcula" file="colorSchemes/NixDarcula.xml"/>

<spellchecker.support
language="Nix"
implementationClass="org.nixos.idea.lang.NixSpellcheckingStrategy"/>

<lang.braceMatcher
language="Nix"
implementationClass="org.nixos.idea.lang.NixBraceMatcher"/>

<lang.quoteHandler
language="Nix"
implementationClass="org.nixos.idea.lang.NixQuoteHandler"/>

<lang.commenter
language="Nix"
implementationClass="org.nixos.idea.lang.NixCommenter"/>

<moveLeftRightHandler
language="Nix"
implementationClass="org.nixos.idea.lang.NixMoveElementLeftRightHandler"/>

<searcher forClass="com.intellij.find.usages.api.UsageSearchParameters"
implementationClass="org.nixos.idea.lang.references.NixUsageSearcher"/>

Expand Down
Loading
Loading