Skip to content

Commit

Permalink
Try implement rest of Move Statement Up/Down action
Browse files Browse the repository at this point in the history
There are still various test failures, maybe this is not the right
approach. Just saving my work before trying something else.
  • Loading branch information
JojOatXGME committed Dec 17, 2024
1 parent f056c2c commit cf99487
Show file tree
Hide file tree
Showing 3 changed files with 1,267 additions and 45 deletions.
287 changes: 267 additions & 20 deletions src/main/java/org/nixos/idea/lang/NixStatementUpDownMover.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@

import com.intellij.codeInsight.editorActions.moveUpDown.LineMover;
import com.intellij.codeInsight.editorActions.moveUpDown.LineRange;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.tree.TokenSet;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.nixos.idea.file.NixFile;
import org.nixos.idea.psi.NixStatementLike;
import org.nixos.idea.psi.NixTypes;

import java.util.Set;

Expand All @@ -40,18 +44,28 @@ public boolean checkAvailable(@NotNull Editor editor, @NotNull PsiFile file, @No
return false;
}

PsiElement commonParent = findCommonParentStrict(range.getFirst(), range.getSecond());
PsiElement first = PsiTreeUtil.getDeepestFirst(range.getFirst());
PsiElement last = PsiTreeUtil.getDeepestLast(range.getSecond());
PsiElement commonParent = findCommonParentStrict(first, last);
if (commonParent == null) {
return false;
}

PsiElement first = PsiTreeUtil.findPrevParent(commonParent, range.getFirst());
PsiElement last = PsiTreeUtil.findPrevParent(commonParent, range.getSecond());
StatementLikeHandler statementHandler = new StatementLikeHandler();
for (PsiElement parent = first.getParent(); parent != commonParent; parent = first.getParent()) {
statementHandler.firstPart(first, parent);
first = parent;
}
for (PsiElement parent = last.getParent(); parent != commonParent; parent = last.getParent()) {
statementHandler.lastPart(last, parent);
last = parent;
}

while (commonParent != null) {
if (tryMoveSubElements(editor, commonParent, first, last, info, down)) {
return true;
}
if (tryMoveStatements(editor, commonParent, first, last, info, down)) {
if (statementHandler.tryMove(editor, commonParent, first, last, info, down)) {
return true;
}

Expand Down Expand Up @@ -122,19 +136,227 @@ private boolean tryMoveSubElements(
}

/**
* Try moving expressions up or down in the tree.
* Handler for moving expressions up or down in the tree.
* For example moving an assertion into the body of a let-expression.
*/
private boolean tryMoveStatements(
@NotNull Editor editor,
@NotNull PsiElement commonParent,
@NotNull PsiElement first,
@NotNull PsiElement last,
@NotNull MoveInfo info,
boolean down
) {
// TODO ...
return false;
private static final class StatementLikeHandler {
// I don't really like the implementation of this class.
// It is full of implicit assumptions about the syntax of the Nix expression language.
// Anyway, I did not have a good idea how to make it better and at least we have decent test coverage.

private static final TokenSet STATEMENT_END_TOKENS = TokenSet.create(NixTypes.SEMI, NixTypes.IN, NixTypes.COLON);
private static final TokenSet INSERTION_POINT_TOKENS = TokenSet.orSet(STATEMENT_END_TOKENS, TokenSet.create(NixTypes.THEN, NixTypes.ELSE));

private @Nullable PsiElement myFirst;
private @Nullable PsiElement myLast;
private @Nullable PsiElement myNextInsertionPoint;

private record JumpRange(@NotNull PsiElement lastOfRange, @Nullable PsiElement insertionPoint) {}

private void firstPart(@NotNull PsiElement first, @NotNull PsiElement parent) {
if (!(parent instanceof NixStatementLike)) {
myFirst = null;
} else if (myFirst == null) {
myFirst = findStatementStartLine(parent);
}
}

private void lastPart(@NotNull PsiElement last, @NotNull PsiElement parent) {
if (!(parent instanceof NixStatementLike)) {
myLast = null;
myNextInsertionPoint = null;
} else {
JumpRange end = findSelectionEnd(parent, last);
if (end != null) {
myLast = end.lastOfRange();
myNextInsertionPoint = end.insertionPoint();
} else {
myLast = null;
myNextInsertionPoint = null;
}
}
}

private boolean tryMove(
@NotNull Editor editor,
@NotNull PsiElement commonParent,
@NotNull PsiElement first,
@NotNull PsiElement last,
@NotNull MoveInfo info,
boolean down
) {
if (!(commonParent instanceof NixStatementLike)) {
if (myFirst != null && myLast != null) {
// We do not want to move statements much further up in the tree
// if the user has selected statements inside the current subtree.
info.prohibitMove();
return true;
} else {
myFirst = null;
myLast = null;
myNextInsertionPoint = null;
return false;
}
}

firstPart(first, commonParent);
if (first != last || myLast == null) {
lastPart(last, commonParent);
}

if (myFirst == null || myLast == null) {
return false;
}
if (tryMoveOverBlankLine(editor, info, myFirst, myLast, down)) {
return true;
}

if (down) {
PsiElement insertionPoint = myNextInsertionPoint;
if (insertionPoint == null && first == last) {
insertionPoint = findNextInsertionPoint(commonParent, last);
}
if (insertionPoint != null && insertionPoint != myLast) {
PsiElement next = nextLeaf(myLast);
if (next == null) {
assert false : "Should be unreachable";
} else {
return tryMoveOver(editor, info, myFirst, myLast, next, insertionPoint);
}
}
} else if (first == last) {
PsiElement insertionPoint = findPreviousInsertionPoint(commonParent, first);
if (insertionPoint != null) {
PsiElement previous = prevLeaf(myFirst);
if (previous == null) {
assert false : "Should be unreachable";
} else {
return tryMoveOver(editor, info, myFirst, myLast, insertionPoint, previous);
}
}
PsiElement startOfLine = findStatementStartLine(commonParent);
if (startOfLine != null && startOfLine != myFirst) {
PsiElement previous = prevLeaf(myFirst);
if (previous == null) {
assert false : "Should be unreachable";
} else {
return tryMoveOver(editor, info, myFirst, myLast, startOfLine, previous);
}
}
}
return false;
}

private @Nullable PsiElement findStatementStartLine(@NotNull PsiElement statement) {
if (!(statement instanceof NixStatementLike)) {
return null;
}

PsiElement current = statement;
while (true) {
PsiElement previous = current.getPrevSibling();
if (previous == null) {
PsiElement parent = current.getParent();
if (parent == null || parent instanceof PsiFile) {
return current;
} else {
current = parent;
}
} else if (previous instanceof PsiWhiteSpace) {
if (previous.textContains('\n')) {
return current;
} else {
current = previous;
}
} else if (previous instanceof PsiComment) {
current = previous;
} else {
return null;
}
}
}

private @Nullable JumpRange findSelectionEnd(@NotNull PsiElement parent, @Nullable PsiElement lastSelected) {
if (!(parent instanceof NixStatementLike)) {
return null;
}

ASTNode stmtEndToken = parent.getNode().findChildByType(STATEMENT_END_TOKENS);
if (stmtEndToken == null) {
return null;
}

PsiElement stmtEnd = stmtEndToken.getPsi();
PsiElement next = PsiTreeUtil.skipWhitespacesAndCommentsForward(stmtEnd);
PsiElement endOfLine = expandLine(stmtEnd, false);
if (next == null) {
return null;
}
if (lastSelected != null && endOfLine != null && lastSelected.getStartOffsetInParent() > endOfLine.getStartOffsetInParent()) {
endOfLine = expandLine(lastSelected, false);
}

if (endOfLine != null && next.getStartOffsetInParent() > endOfLine.getStartOffsetInParent()) {
return new JumpRange(endOfLine, findNextInsertionPoint(next, null));
} else if (next == lastSelected) {
// Optimization: `lastSelected` has already been processed
return myLast == null ? null : new JumpRange(myLast, myNextInsertionPoint);
} else {
return findSelectionEnd(next, null);
}
}

private @Nullable PsiElement findPreviousInsertionPoint(@NotNull PsiElement parent, @Nullable PsiElement before) {
if (!(parent instanceof NixStatementLike)) {
return null;
}

PsiElement start = before == null ? parent.getLastChild() :
PsiTreeUtil.skipWhitespacesBackward(PsiTreeUtil.skipWhitespacesAndCommentsBackward(before));
for (PsiElement cur = start; cur != null; cur = PsiTreeUtil.skipWhitespacesBackward(cur)) {
if (INSERTION_POINT_TOKENS.contains(cur.getNode().getElementType())) {
PsiElement childExpression = PsiTreeUtil.skipWhitespacesAndCommentsForward(cur);
if (childExpression == null /* || childExpression == myFirst */) {
return null;
}

PsiElement insertionPoint = findPreviousInsertionPoint(childExpression, null);
if (insertionPoint != null) {
return insertionPoint;
}

PsiElement startOfLine = expandLine(childExpression, true);
if (startOfLine != null && startOfLine.getStartOffsetInParent() > cur.getStartOffsetInParent()) {
return startOfLine;
}
}
}
return null;
}

private static @Nullable PsiElement findNextInsertionPoint(@NotNull PsiElement parent, @Nullable PsiElement after) {
if (!(parent instanceof NixStatementLike)) {
return null;
}

PsiElement first = after == null ? parent.getFirstChild() : PsiTreeUtil.skipWhitespacesForward(after);
for (PsiElement cur = first; cur != null; cur = PsiTreeUtil.skipWhitespacesForward(cur)) {
if (INSERTION_POINT_TOKENS.contains(cur.getNode().getElementType())) {
PsiElement next = PsiTreeUtil.skipWhitespacesAndCommentsForward(cur);
PsiElement endOfLine = expandLine(cur, false);
if (next == null || endOfLine == null) {
return null;
}

if (next.getStartOffsetInParent() > endOfLine.getStartOffsetInParent()) {
return endOfLine;
} else {
return findNextInsertionPoint(next, null);
}
}
}
return null;
}
}

/**
Expand All @@ -153,6 +375,9 @@ private static boolean tryMoveOverBlankLine(
LineRange range = createRange(document, info.toMove, first, last);

int target = down ? range.endLine : range.startLine - 1;
if (target < 0 || target > document.getLineCount()) {
return false;
}
int lineStartOffset = document.getLineStartOffset(target);
int lineEndOffset = document.getLineEndOffset(target);
if (!CharArrayUtil.isEmptyOrSpaces(document.getCharsSequence(), lineStartOffset, lineEndOffset)) {
Expand Down Expand Up @@ -226,16 +451,38 @@ private static boolean tryMoveOver(
}
}

private static PsiElement expandLine(@NotNull PsiElement element, boolean left) {
private static @Nullable PsiElement expandLine(@NotNull PsiElement element, boolean left) {
while (true) {
PsiElement next = left ? element.getPrevSibling() : element.getNextSibling();
if (next == null) {
// TODO Cover case when a newline is before the first element
return null;
} else if (next instanceof PsiWhiteSpace && next.textContains('\n')) {
return element;
PsiElement nextLeaf = left ? PsiTreeUtil.prevLeaf(element) : PsiTreeUtil.nextLeaf(element);
if (nextLeaf == null || nextLeaf instanceof PsiWhiteSpace && nextLeaf.textContains('\n')) {
return element;
} else {
return null;
}
} else if (next instanceof PsiWhiteSpace) {
if (next.textContains('\n')) {
return element;
}
}
element = next;
}
}

private static @Nullable PsiElement prevLeaf(@NotNull PsiElement element) {
PsiElement previous = PsiTreeUtil.prevLeaf(element, true);
while (previous instanceof PsiWhiteSpace) {
previous = PsiTreeUtil.prevLeaf(previous, true);
}
return previous;
}

private static @Nullable PsiElement nextLeaf(@NotNull PsiElement element) {
PsiElement previous = PsiTreeUtil.nextLeaf(element, true);
while (previous instanceof PsiWhiteSpace) {
previous = PsiTreeUtil.nextLeaf(previous, true);
}
return previous;
}
}
11 changes: 6 additions & 5 deletions src/main/lang/Nix.bnf
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,17 @@ private expr0 ::=
| expr_lambda
| expr_op

expr_assert ::= ASSERT expr SEMI expr { pin=1 }
expr_if ::= IF expr then else { pin=1 }
expr_assert ::= ASSERT expr SEMI expr { pin=1 implements=statement_like methods=[ assertion="/expr[0]" next="/expr[1]" ] }
expr_if ::= IF expr then else { pin=1 implements=statement_like }
private then ::= THEN expr { pin=1 }
private else ::= ELSE expr { pin=1 }
expr_let ::= LET !LCURLY recover_let (bind recover_let)* IN expr { pin=2 }
expr_with ::= WITH expr SEMI expr { pin=1 }
expr_let ::= LET !LCURLY recover_let (bind recover_let)* IN expr { pin=2 implements=statement_like methods=[ next="expr" ] }
expr_with ::= WITH expr SEMI expr { pin=1 implements=statement_like methods=[ import="/expr[0]" next="/expr[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 }
expr_lambda ::= lambda_params !missing_semi COLON expr { pin=3 implements=statement_like }
private lambda_params ::= argument [ !missing_semi AT formals ] | formals [ !missing_semi AT argument ]
argument ::= parameter_name { implements=parameter }
formals ::= LCURLY ( formal COMMA )* [ ( ELLIPSIS | formal ) ] recover_formals RCURLY { pin=1 }
Expand Down Expand Up @@ -234,6 +234,7 @@ attr_path ::= attr ( DOT attr )* { methods=[ firstAttr="/attr[0]" ] }

// Interface for identifiers.
fake identifier ::= ID | OR_KW
fake statement_like ::= { extends="expr" }

// 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
Loading

0 comments on commit cf99487

Please sign in to comment.