Skip to content

Commit

Permalink
Invert If Action (vshaxe#108)
Browse files Browse the repository at this point in the history
  • Loading branch information
RblSb authored May 14, 2023
1 parent c90645b commit 21e3ea3
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,18 @@ class ExtractVarFeature implements CodeActionContributor {
switch token.tok {
case Const(_):
final token = preDotToken(token);
if (isFunctionScope(token))
if (TokenTreeUtils.isInFunctionScope(token))
return [];
if (isTypeHint(token))
return [];
// disallow full obj extraction when cursor is in `{nam|e: value}`
if (isAnonStructureField(token))
if (TokenTreeUtils.isAnonStructureField(token))
return [];
if (isFieldAssign(token))
return [];
final action:Null<CodeAction> = makeExtractVarAction(doc, tokens, uri, token, range);
if (action == null) [] else [action];
case BrOpen, BrClose if (isAnonStructure(token)):
case BrOpen, BrClose if (TokenTreeUtils.isAnonStructure(token)):
final action:Null<CodeAction> = makeExtractVarAction(doc, tokens, uri, token, range);
if (action == null) [] else [action];
case BkOpen, BkClose:
Expand Down Expand Up @@ -84,36 +84,6 @@ class ExtractVarFeature implements CodeActionContributor {
return false;
}

function isFunctionScope(token:TokenTree):Bool {
final brOpen = token.parent ?? return false;
if (brOpen.tok != BrOpen)
return false;
final name = brOpen.parent ?? return false;
if (name.tok.match(Kwd(_) | Arrow))
return true;
final fun = name.parent ?? return false;
return fun.tok.match(Kwd(_));
}

function isAnonStructure(brToken:TokenTree):Bool {
if (brToken.tok == BrClose)
brToken = brToken.parent ?? return false;
final first = brToken!.getFirstChild() ?? return false;
final colon = first.getFirstChild() ?? return false;
if (colon.tok.match(DblDot) && !colon.nextSibling!.tok.match(Semicolon)) {
return true;
}
return false;
}

function isAnonStructureField(token:TokenTree):Bool {
final parent = token.parent ?? return false;
if (!isAnonStructure(parent))
return false;
final colon = token.getFirstChild() ?? return false;
return colon.tok.match(DblDot);
}

function makeExtractVarAction(doc:HaxeDocument, tokens:TokenTreeManager, uri:DocumentUri, token:TokenTree, range:Range):Null<CodeAction> {
// use token at the selection end for `foo = Type.foo` names
final endToken:Null<TokenTree> = tokens.getTokenAtOffset(doc.offsetAt(range.end)) ?? token;
Expand Down Expand Up @@ -194,7 +164,7 @@ class ExtractVarFeature implements CodeActionContributor {
case Kwd(KwdFunction | KwdCase | KwdFor):
return null;
case BrOpen:
if (!isAnonStructure(parent))
if (!TokenTreeUtils.isAnonStructure(parent))
return token;
default:
}
Expand Down
44 changes: 44 additions & 0 deletions src/haxeLanguageServer/features/haxe/codeAction/TokenTreeUtils.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package haxeLanguageServer.features.haxe.codeAction;

import tokentree.TokenTree;

class TokenTreeUtils {
public static function isInFunctionScope(token:TokenTree):Bool {
final brOpen = token.parent ?? return false;
if (brOpen.tok != BrOpen)
return false;
final name = brOpen.parent ?? return false;
// `function() {}` or `() -> {}`
if (name.tok.match(Kwd(KwdFunction) | Arrow))
return true;
final fun = name.parent ?? return false;
// `function name() {}`
return fun.tok.match(Kwd(KwdFunction));
}

public static function isInLoopScope(token:TokenTree):Bool {
var kwd = token.parent ?? return false;
if (kwd.tok == BrOpen)
kwd = kwd.parent ?? return false;
return kwd.tok.match(Kwd(KwdFor | KwdDo | KwdWhile));
}

public static function isAnonStructure(brToken:TokenTree):Bool {
if (brToken.tok == BrClose)
brToken = brToken.parent ?? return false;
final first = brToken!.getFirstChild() ?? return false;
final colon = first.getFirstChild() ?? return false;
if (colon.tok.match(DblDot) && !colon.nextSibling!.tok.match(Semicolon)) {
return true;
}
return false;
}

public static function isAnonStructureField(token:TokenTree):Bool {
final parent = token.parent ?? return false;
if (!isAnonStructure(parent))
return false;
final colon = token.getFirstChild() ?? return false;
return colon.tok.match(DblDot);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class UpdateSyntaxActions {
// `a.b?.c`
addSaveNavIfNotNullAction(context, params, actions, doc, ifToken, ifVarName);
}

addIfInvertAction(context, params, actions, doc, ifToken);
}

final questionToken = getNullCheckTernaryExpr(token);
Expand Down Expand Up @@ -86,16 +88,18 @@ class UpdateSyntaxActions {
});
}

static function multilineIndent(doc:HaxeDocument, context:Context, value:String, pos:Position):String {
if (!value.contains("\n"))
static function multilineIndent(doc:HaxeDocument, context:Context, value:String, pos:Position, isSameLine = true):String {
if (!value.contains("\n") && isSameLine)
return value;
value = FormatterHelper.formatText(doc, context, value, ExpressionLevel);
final line = doc.lineAt(pos.line);
final count = lineIndentationCount(line);
if (count == 0)
return value;
final prefix = "".rpad(line.charAt(0), count);
value = value.split("\n").mapi((i, s) -> i == 0 ? s : '$prefix$s').join("\n");
value = value.split("\n").mapi((i, s) -> {
(isSameLine && i == 0) ? s : '$prefix$s';
}).join("\n");
return value;
}

Expand Down Expand Up @@ -158,6 +162,191 @@ class UpdateSyntaxActions {
});
}

static function addIfInvertAction(context:Context, params:CodeActionParams, actions:Array<CodeAction>, doc:HaxeDocument, ifToken:TokenTree) {
var deadEndText = if (TokenTreeUtils.isInFunctionScope(ifToken)) {
final brOpen = ifToken.parent ?? return;
final returnToken = getBlockReturn(brOpen);
if (returnToken != null) {
doc.getText(doc.rangeAt(returnToken.getPos(), Utf8));
} else {
"return;";
}
} else if (TokenTreeUtils.isInLoopScope(ifToken)) {
"continue;";
} else return;
// only invert `if` if there is no code after it in scope
function sibFilter(sib:TokenTree):Bool {
if (sib.isComment())
return false;
if (sib.tok.match(Kwd(KwdReturn | KwdThrow)))
return false;
if (sib.tok.match(BrClose))
return false;
return true;
}
if (filterNextSibling(ifToken, sibFilter) != null)
return;

final pOpen = ifToken.getFirstChild() ?? return;
var ifBody = pOpen.nextSibling ?? return;
final replaceRange = doc.rangeAt(ifToken.getPos(), Utf8);

var elseToken = ifBody.nextSibling;
var elseBody = elseToken!.getFirstChild();
if (elseToken!.matches(Kwd(KwdElse)) && elseBody != null) {
final hasReturn = getBlockReturn(elseBody) != null;
// no need for second `return`/`continue`
if (hasReturn)
deadEndText = '';
var elseBodyText = getBlockText(doc, elseBody) + '\n$deadEndText';
elseBodyText = '{\n$elseBodyText\n}';
elseBodyText = multilineIndent(doc, context, elseBodyText.trim(), replaceRange.start);
elseBodyText = elseBodyText.rtrim();
deadEndText = elseBodyText;
}

final cond = invertCondition(pOpen);
// trace(cond);

var ifBodyText = getBlockText(doc, ifBody);
ifBodyText = multilineIndent(doc, context, ifBodyText.trim(), replaceRange.start, false);
ifBodyText = ifBodyText.rtrim();

actions.push({
title: "Invert if expression",
kind: RefactorRewrite,
edit: WorkspaceEditHelper.create(context, params, [
{
range: replaceRange,
newText: 'if ($cond) $deadEndText\n$ifBodyText'
}
]),
});
}

static function getBlockReturn(brOpen:TokenTree):Null<TokenTree> {
final maybeReturn = brOpen.getLastChild()!.previousSibling ?? cast return null;
if (!maybeReturn.tok.match(Kwd(KwdReturn | KwdThrow)))
return null;
return maybeReturn;
}

static function getBlockText(doc:HaxeDocument, block:TokenTree):String {
final range = doc.rangeAt(block.getPos(), Utf8);
var blockText = doc.getText(range);
if (block.tok == BrOpen && !TokenTreeUtils.isAnonStructure(block)) {
final reg = ~/\{((.|\n)*)\}/g;
if (reg.match(blockText)) {
blockText = reg.matched(1);
}
}
return blockText.trim();
}

static function filterNextSibling(token:Null<TokenTree>, filter:(sib:TokenTree) -> Bool):Null<TokenTree> {
var token = token ?? cast return null;
while (true) {
token = token.nextSibling ?? cast return null;
if (!filter(token))
continue;
return token;
}
}

static function invertCondition(pOpen:TokenTree):String {
var buf = "";
var current = pOpen.getFirstChild() ?? return buf;
final pClose = pOpen.getLastChild() ?? return buf;
final waitBrs = [];
var expr = "";
// do not invert rhs values if op is already inverted
var isInverted = false;
while (current != pClose) {
if (waitBrs.length > 0) {
if (current.matches(waitBrs[waitBrs.length - 1])) {
waitBrs.pop();
}
expr += switch current.tok {
case POpen, BkOpen, BrOpen:
final close = getClosingBracketTok(current.tok);
if (close != null) {
waitBrs.push(close);
}
current.toString();
case Binop(_), Arrow, Question, DblDot:
final e = current.toString();
' $e ';
case _: current.toString();
}
current = flatNextToken(current) ?? return buf + expr;
continue;
}
switch current.tok {
case Unop(OpNot):
expr += isInverted ? "!" : "";
isInverted = true;
case Kwd(KwdTrue):
expr += isInverted ? "true" : "false";
isInverted = true;
case Kwd(KwdFalse):
expr += isInverted ? "false" : "true";
isInverted = true;
case Binop(OpEq):
final op = isInverted ? "==" : "!=";
buf += '$expr $op ';
expr = "";
isInverted = true;
case Binop(OpNotEq):
final op = isInverted ? "!=" : "==";
buf += '$expr $op ';
expr = "";
isInverted = true;
case Binop(OpBoolAnd):
final e = isInverted ? expr : '!$expr';
buf += '$e || ';
expr = "";
isInverted = false;
case Binop(OpBoolOr):
final e = isInverted ? expr : '!$expr';
buf += '$e && ';
expr = "";
isInverted = false;
case POpen, BkOpen, BrOpen:
final close = getClosingBracketTok(current.tok);
if (close != null) {
waitBrs.push(close);
}
expr += current.toString();
case _:
expr += current.toString();
}
current = flatNextToken(current) ?? return buf + expr;
}
final e = isInverted ? expr : '!$expr';
return '$buf$e';
}

static function getClosingBracketTok(tok:tokentree.TokenTreeDef):Null<tokentree.TokenTreeDef> {
return switch tok {
case POpen: PClose;
case BkOpen: BkClose;
case BrOpen: BrClose;
case _: null;
}
}

static function flatNextToken(current:TokenTree):Null<TokenTree> {
final child = current.getFirstChild();
if (child != null)
return child;
while (true) {
final next = current.nextSibling;
if (next != null)
return next;
current = current.parent ?? cast return null;
}
}

static function addTernaryNullCheckAction(context:Context, params:CodeActionParams, actions:Array<CodeAction>, doc:HaxeDocument, questionToken:TokenTree) {
final binopToken = questionToken.parent!.parent ?? return;
final ifIdentEnd = binopToken.parent ?? return;
Expand Down Expand Up @@ -253,19 +442,6 @@ class UpdateSyntaxActions {
return null;
}

static function getTernaryNullCheckRanges(questionToken:TokenTree):Null<{ifVar:Range, ifNull:Range, ifNotNull:Range}> {
final t = questionToken.access()
.firstChild()
.matches(Kwd(KwdNull))
.nextSibling()
.matches(Question)
.child(1)
.matches(DblDot);
if (t.exists() == false)
return null;
return null;
}

static function getIfVarEqNullIdentRange(doc:HaxeDocument, ifToken:TokenTree):Null<Range> {
final pOpen = ifToken.getFirstChild() ?? cast return null;
final ident = pOpen.getFirstChild() ?? cast return null;
Expand Down

0 comments on commit 21e3ea3

Please sign in to comment.