Skip to content
This repository has been archived by the owner on Mar 1, 2022. It is now read-only.

Commit

Permalink
auto complete else after if blocks
Browse files Browse the repository at this point in the history
(and else if!)

woah, technology
WebFreak001 committed Nov 17, 2021
1 parent c268061 commit 6ea29b1
Showing 4 changed files with 200 additions and 13 deletions.
11 changes: 10 additions & 1 deletion source/workspaced/com/snippets/generator.d
Original file line number Diff line number Diff line change
@@ -242,7 +242,8 @@ void map()
enum StackStorageScope(string val) = "if (done) return; auto __" ~ val
~ "_scope = " ~ val ~ "; scope (exit) if (!done) " ~ val ~ " = __" ~ val ~ "_scope;";
enum SnippetLevelWrapper(SnippetLevel level) = "if (done) return; pushLevel("
~ level.stringof ~ ", dec); scope (exit) popLevel(dec);";
~ level.stringof ~ ", dec); scope (exit) popLevel(dec); "
~ "if (!dec.tokens.length || dec.tokens[0].index <= position) lastStatement = null;";
enum FullSnippetLevelWrapper(SnippetLevel level) = SnippetLevelWrapper!level ~ " super.visit(dec);";

class SnippetInfoGenerator : ASTVisitor
@@ -286,6 +287,13 @@ class SnippetInfoGenerator : ASTVisitor
super.visit(dec);
}

override void visit(const DeclarationOrStatement dec)
{
super.visit(dec);
if (!dec.tokens.length || dec.tokens[0].index <= position)
lastStatement = cast()dec;
}

static foreach (T; AliasSeq!(Arguments, ExpressionNode))
override void visit(const T dec)
{
@@ -363,6 +371,7 @@ class SnippetInfoGenerator : ASTVisitor

bool done;
VariableUsage[] variableStack;
DeclarationOrStatement lastStatement;
size_t position, current;
SnippetInfo ret;
}
70 changes: 62 additions & 8 deletions source/workspaced/com/snippets/package.d
Original file line number Diff line number Diff line change
@@ -73,9 +73,16 @@ class SnippetsComponent : ComponentWrapper
// nudge in next token if position is not exactly on the start of it
if (loc < tokens.length && tokens[loc].index < position)
loc++;
// determine info from before start of identifier (so you can start typing something and it still finds a snippet scope)
if (loc > 0 && loc < tokens.length && tokens[loc].type == tok!"identifier" && tokens[loc].index >= position)
loc--;

int contextIndex;
if (loc >= 0 && loc < tokens.length)
contextIndex = cast(int) tokens[loc].index;

if (loc == 0 || loc == tokens.length)
return SnippetInfo([SnippetLevel.global]);
return SnippetInfo(contextIndex, [SnippetLevel.global]);

auto leading = tokens[0 .. loc];

@@ -93,9 +100,9 @@ class SnippetsComponent : ComponentWrapper
// needs to be modified to check the exact trivia token instead
// of the associated token with it.
if (last.text[0 .. len].startsWith("///", "/++", "/**"))
return SnippetInfo([SnippetLevel.docComment]);
return SnippetInfo(contextIndex, [SnippetLevel.docComment]);
else if (len >= 2)
return SnippetInfo([SnippetLevel.comment]);
return SnippetInfo(contextIndex, [SnippetLevel.comment]);
else
break;
case tok!"dstringLiteral":
@@ -109,15 +116,15 @@ class SnippetsComponent : ComponentWrapper
// quote character
// TODO: properly check if this is an unescaped escape
if (textSoFar.endsWith('\\', last.text[0]))
return SnippetInfo([SnippetLevel.strings, SnippetLevel.other]);
return SnippetInfo(contextIndex, [SnippetLevel.strings, SnippetLevel.other]);
else
return SnippetInfo([SnippetLevel.strings]);
return SnippetInfo(contextIndex, [SnippetLevel.strings]);
case tok!"(":
if (leading.length >= 2)
{
auto beforeLast = leading[$ - 2];
if (beforeLast.type.among(tok!"__traits", tok!"version", tok!"debug"))
return SnippetInfo([SnippetLevel.other]);
return SnippetInfo(contextIndex, [SnippetLevel.other]);
}
break;
default:
@@ -133,13 +140,13 @@ class SnippetsComponent : ComponentWrapper
// test for tokens semicolon closed statements where we should abort to avoid incomplete syntax
if (t.type.among!(tok!"import", tok!"module"))
{
return SnippetInfo([SnippetLevel.global, SnippetLevel.other]);
return SnippetInfo(contextIndex, [SnippetLevel.global, SnippetLevel.other]);
}
else if (t.type.among!(tok!"=", tok!"+", tok!"-", tok!"*", tok!"/",
tok!"%", tok!"^^", tok!"&", tok!"|", tok!"^", tok!"<<",
tok!">>", tok!">>>", tok!"~", tok!"in"))
{
return SnippetInfo([SnippetLevel.global, SnippetLevel.value]);
return SnippetInfo(contextIndex, [SnippetLevel.global, SnippetLevel.value]);
}
}

@@ -149,6 +156,7 @@ class SnippetsComponent : ComponentWrapper
//trace("determineSnippetInfo at ", position);

scope gen = new SnippetInfoGenerator(position);
gen.value.contextTokenIndex = contextIndex;
gen.variableStack.reserve(64);
gen.visit(parsed);

@@ -165,6 +173,34 @@ class SnippetsComponent : ComponentWrapper
}
}

if (gen.lastStatement)
{
import dparse.ast;

LastStatementInfo info;
auto nodeType = gen.lastStatement.findDeepestNonBlockNode;
if (gen.lastStatement.tokens.length)
info.location = cast(int) nodeType.tokens[0].index;
info.type = typeid(nodeType).name;
auto lastDot = info.type.lastIndexOf('.');
if (lastDot != -1)
info.type = info.type[lastDot + 1 .. $];
if (auto ifStmt = cast(IfStatement)nodeType)
{
auto elseStmt = getIfElse(ifStmt);
if (cast(IfStatement)elseStmt)
info.ifHasElse = false;
else
info.ifHasElse = elseStmt !is null;
}
else if (auto ifStmt = cast(ConditionalDeclaration)nodeType)
info.ifHasElse = ifStmt.hasElse;
// if (auto ifStmt = cast(ConditionalStatement)nodeType)
// info.ifHasElse = !!getIfElse(ifStmt);

gen.value.lastStatement = info;
}

return gen.value;
}

@@ -540,10 +576,15 @@ struct SnippetLoopScope
///
struct SnippetInfo
{
/// Index in code which token was used to determine this snippet info.
int contextTokenIndex;
/// Levels this snippet location has gone through, latest one being the last
SnippetLevel[] stack = [SnippetLevel.global];
/// Information about snippets using loop context
SnippetLoopScope loopScope;
/// Information about the last parsable statement before the cursor. May be
/// `LastStatementInfo.init` at start of function or block.
LastStatementInfo lastStatement;

/// Current snippet scope level of the location
SnippetLevel level() const @property
@@ -552,6 +593,19 @@ struct SnippetInfo
}
}

struct LastStatementInfo
{
/// The libdparse class name (typeid) of the last parsable statement before
/// the cursor, stripped of module name.
string type;
/// If type is set, this is the start location in bytes where
/// the first token was.
int location;
/// True if the type is (`IfStatement`, `ConditionalDeclaration` or
/// `ConditionalStatement`) and has a final `else` block defined.
bool ifHasElse;
}

/// A list of snippets resolved at a given position.
struct SnippetList
{
53 changes: 49 additions & 4 deletions source/workspaced/com/snippets/smart.d
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
module workspaced.com.snippets.smart;

// debug = SnippetScope;

import workspaced.api;
import workspaced.com.snippets;

import std.algorithm;
import std.conv;
import std.string;

class SmartSnippetProvider : SnippetProvider
{
Future!(Snippet[]) provideSnippets(scope const WorkspaceD.Instance instance,
scope const(char)[] file, scope const(char)[] code, int position, const SnippetInfo info)
{
Snippet[] res;

if (info.loopScope.supported)
{
Snippet[] res;
if (info.loopScope.numItems > 1)
{
res ~= ndForeach(info.loopScope.numItems, info.loopScope.iterator);
@@ -29,10 +34,50 @@ class SmartSnippetProvider : SnippetProvider
res ~= simpleForeach(info.loopScope.iterator, info.loopScope.type);
res ~= stringIterators();
}
return typeof(return).fromResult(res);
}
else
return typeof(return).fromResult(null);

if (info.lastStatement.type == "IfStatement"
&& !info.lastStatement.ifHasElse)
{
int ifIndex = info.contextTokenIndex == 0 ? position : info.contextTokenIndex;
auto hasBraces = code[0 .. max(min(ifIndex, $), 0)].stripRight.endsWith("}");
Snippet snp;
snp.providerId = typeid(this).name;
snp.id = "else";
snp.title = "else";
snp.shortcut = "else";
snp.documentation = "else block";
if (hasBraces)
{
snp.plain = "else {\n\t\n}";
snp.snippet = "else {\n\t$0\n}";
}
else
{
snp.plain = "else\n\t";
snp.snippet = "else\n\t$0";
}
snp.unformatted = true;
snp.resolved = true;
res ~= snp;
}

debug (SnippetScope)
{
import painlessjson : toJSON;

Snippet ret;
ret.providerId = typeid(this).name;
ret.id = "workspaced-snippet-debug";
ret.title = "[DEBUG] Snippet";
ret.shortcut = "__debug_snippet";
ret.plain = ret.snippet = info.toJSON.toPrettyString;
ret.unformatted = true;
ret.resolved = true;
res ~= ret;
}

return typeof(return).fromResult(res.length ? res : null);
}

Future!Snippet resolveSnippet(scope const WorkspaceD.Instance instance,
79 changes: 79 additions & 0 deletions source/workspaced/dparseext.d
Original file line number Diff line number Diff line change
@@ -301,3 +301,82 @@ string evaluateExpressionString(const Token token)
return null;
}
}

/// Finds the deepest non-null node of any BaseNode. (like visiting the tree)
/// Aborts on types that contain `DeclarationOrStatement` or `Declaration[]`
/// fields.
/// Useful for getting the IfStatement out of a DeclarationOrStatement without
/// traversing its children.
BaseNode findDeepestNonBlockNode(T : BaseNode)(T ast)
{
static assert(!is(T == BaseNode), "Passed in a BaseNode, that's probably not what you wanted to do (pass in the most specific type you have)");
bool nonProcess = false;
foreach (member; ast.tupleof)
{
static if (is(typeof(member) : DeclarationOrStatement)
|| is(typeof(member) : Declaration[]))
{
nonProcess = true;
}
}

if (nonProcess)
return ast;

foreach (member; ast.tupleof)
{
static if (is(typeof(member) : BaseNode))
{
if (member !is null)
{
return findDeepestNonBlockNode(member);
}
}
}
return ast;
}

/// Gets the final `else` block of an if. Will return a node of type
/// `IfStatement` if it's an `else if` block. Returns null if there is no single
/// else statement.
BaseNode getIfElse(IfStatement ifStmt)
{
if (!ifStmt.elseStatement)
return null;

while (true)
{
auto elseStmt = ifStmt.elseStatement;
if (!elseStmt)
return ifStmt;

auto stmtInElse = elseStmt.findDeepestNonBlockNode;
assert(stmtInElse !is elseStmt);

if (cast(IfStatement)stmtInElse)
ifStmt = cast(IfStatement)stmtInElse;
else
return stmtInElse;
}
}

unittest
{
StringCache stringCache = StringCache(StringCache.defaultBucketCount);
RollbackAllocator rba;
IfStatement parseIfStmt(string code)
{
const(Token)[] tokens = getTokensForParser(cast(ubyte[])code, LexerConfig.init, &stringCache);
auto parser = new Parser();
parser.tokens = tokens;
parser.allocator = &rba;
return parser.parseIfStatement();
}

alias p = parseIfStmt;
assert(getIfElse(p("if (x) {}")) is null);
assert(getIfElse(p("if (x) {} else if (y) {}")) !is null);
assert(cast(IfStatement)getIfElse(p("if (x) {} else if (y) {}")) !is null, typeid(getIfElse(p("if (x) {} else if (y) {}"))).name);
assert(getIfElse(p("if (x) {} else if (y) {} else {}")) !is null);
assert(cast(IfStatement)getIfElse(p("if (x) {} else if (y) {} else {}")) is null);
}

0 comments on commit 6ea29b1

Please sign in to comment.