Skip to content

Commit

Permalink
Implement Move Element Left/Right action
Browse files Browse the repository at this point in the history
  • Loading branch information
JojOatXGME committed Dec 8, 2024
1 parent 288e7af commit 2241f84
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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*

### 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);
}
}
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
5 changes: 5 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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ private void collectBindDeclarations(@NotNull Symbols result, @NotNull List<NixB
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
6 changes: 3 additions & 3 deletions src/main/lang/Nix.bnf
Original file line number Diff line number Diff line change
Expand Up @@ -163,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 @@ -196,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 @@ -219,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 Down
4 changes: 4 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
package org.nixos.idea.lang;

import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.testFramework.fixtures.CodeInsightTestFixture;
import org.intellij.lang.annotations.Language;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.nixos.idea._testutil.WithIdeaPlatform;
import org.nixos.idea.file.NixFileType;

@WithIdeaPlatform.OnEdt
@WithIdeaPlatform.CodeInsight
final class NixMoveElementLeftRightHandlerTest {

private final CodeInsightTestFixture myFixture;

NixMoveElementLeftRightHandlerTest(CodeInsightTestFixture fixture) {
myFixture = fixture;
}

@Nested
final class List {
@Test
void moveLeft() {
setup("[a b c d e f<caret>]");
doMoveLeft();
expect("[a b c d f<caret> e]");
doMoveLeft();
expect("[a b c f<caret> d e]");
doMoveLeft();
expect("[a b f<caret> c d e]");
doMoveLeft();
expect("[a f<caret> b c d e]");
doMoveLeft();
expect("[f<caret> a b c d e]");
doMoveLeft();
expect("[f<caret> a b c d e]");
}

@Test
void moveRight() {
setup("[ <caret>a b c d ]");
doMoveRight();
expect("[ b <caret>a c d ]");
doMoveRight();
expect("[ b c <caret>a d ]");
doMoveRight();
expect("[ b c d <caret>a ]");
doMoveRight();
expect("[ b c d <caret>a ]");
}
}

@Nested
final class BindInherit {
@Test
void moveLeft() {
setup("{ inherit (x) a b <caret>c; }");
doMoveLeft();
expect("{ inherit (x) a <caret>c b; }");
doMoveLeft();
expect("{ inherit (x) <caret>c a b; }");
doMoveLeft();
expect("{ inherit (x) <caret>c a b; }");
}

@Test
void moveRight() {
setup("{ inherit (x) a<caret> b c; }");
doMoveRight();
expect("{ inherit (x) b a<caret> c; }");
doMoveRight();
expect("{ inherit (x) b c a<caret>; }");
doMoveRight();
expect("{ inherit (x) b c a<caret>; }");
}
}

@Nested
final class Attrs {
@Test
void moveLeft() {
setup("{ a = A; b = B; c <caret>= C; }");
doMoveLeft();
expect("{ a = A; c <caret>= C; b = B; }");
doMoveLeft();
expect("{ c <caret>= C; a = A; b = B; }");
doMoveLeft();
expect("{ c <caret>= C; a = A; b = B; }");
}

@Test
void moveRight() {
setup("rec { a =<caret> A; b = B; c = C; }");
doMoveRight();
expect("rec { b = B; a =<caret> A; c = C; }");
doMoveRight();
expect("rec { b = B; c = C; a =<caret> A; }");
doMoveRight();
expect("rec { b = B; c = C; a =<caret> A; }");
}
}

@Nested
final class Let {
@Test
void moveLeft() {
setup("let a = A; b = B; c <caret>= C; in _");
doMoveLeft();
expect("let a = A; c <caret>= C; b = B; in _");
doMoveLeft();
expect("let c <caret>= C; a = A; b = B; in _");
doMoveLeft();
expect("let c <caret>= C; a = A; b = B; in _");
}

@Test
void moveRight() {
setup("let a =<caret> A; b = B; c = C; in _");
doMoveRight();
expect("let b = B; a =<caret> A; c = C; in _");
doMoveRight();
expect("let b = B; c = C; a =<caret> A; in _");
doMoveRight();
expect("let b = B; c = C; a =<caret> A; in _");
}
}

@Nested
final class Lambda {
@Test
void moveLeft() {
setup("{ x } @ a<caret>: _");
doMoveLeft();
expect("a @ { x }: _");
doMoveLeft();
expect("a @ { x }: _");
}

@Test
void moveRight() {
setup("a<caret> @ { x, y }: _");
doMoveRight();
expect("{ x, y } @ a<caret>: _");
doMoveRight();
expect("{ x, y } @ a<caret>: _");
}
}

@Nested
final class LambdaFormals {
@Test
void moveLeft() {
setup("{ a, b, c<caret> } @ x: _");
doMoveLeft();
expect("{ a, c<caret>, b } @ x: _");
doMoveLeft();
expect("{ c<caret>, a, b } @ x: _");
doMoveLeft();
expect("{ c<caret>, a, b } @ x: _");
}

@Test
void moveRight() {
setup("{ a<caret>, b, c } @ x: _");
doMoveRight();
expect("{ b, a<caret>, c } @ x: _");
doMoveRight();
expect("{ b, c, a<caret> } @ x: _");
doMoveRight();
expect("{ b, c, a<caret> } @ x: _");
}
}

@Nested
final class App {
@Test
void doNotMoveFunction() {
setup("f<caret> a b c");
doMoveLeft();
expect("f<caret> a b c");
}

@Test
void moveLeft() {
setup("f a b <caret>c");
doMoveLeft();
expect("f a <caret>c b");
doMoveLeft();
expect("f <caret>c a b");
doMoveLeft();
expect("f <caret>c a b");
}

@Test
void moveRight() {
setup("f <caret>a b c");
doMoveRight();
expect("f b <caret>a c");
doMoveRight();
expect("f b c <caret>a");
doMoveRight();
expect("f b c <caret>a");
}
}

private void setup(@Language("HTML") String code) {
myFixture.configureByText(NixFileType.INSTANCE, code);
}

private void doMoveLeft() {
myFixture.performEditorAction(IdeActions.MOVE_ELEMENT_LEFT);
}

private void doMoveRight() {
myFixture.performEditorAction(IdeActions.MOVE_ELEMENT_RIGHT);
}

private void expect(@Language("HTML") String code) {
int caretMarker = code.indexOf(CodeInsightTestFixture.CARET_MARKER);
if (caretMarker >= 0) {
code = code.substring(0, caretMarker) +
code.substring(caretMarker + CodeInsightTestFixture.CARET_MARKER.length());
}
Assertions.assertEquals(code, myFixture.getEditor().getDocument().getText());
if (caretMarker >= 0) {
Assertions.assertEquals(caretMarker, myFixture.getCaretOffset());
}
}
}

0 comments on commit 2241f84

Please sign in to comment.