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

Commit 6ea29b1

Browse files
committed
auto complete else after if blocks
(and else if!) woah, technology
1 parent c268061 commit 6ea29b1

4 files changed

Lines changed: 200 additions & 13 deletions

File tree

source/workspaced/com/snippets/generator.d

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,8 @@ void map()
242242
enum StackStorageScope(string val) = "if (done) return; auto __" ~ val
243243
~ "_scope = " ~ val ~ "; scope (exit) if (!done) " ~ val ~ " = __" ~ val ~ "_scope;";
244244
enum SnippetLevelWrapper(SnippetLevel level) = "if (done) return; pushLevel("
245-
~ level.stringof ~ ", dec); scope (exit) popLevel(dec);";
245+
~ level.stringof ~ ", dec); scope (exit) popLevel(dec); "
246+
~ "if (!dec.tokens.length || dec.tokens[0].index <= position) lastStatement = null;";
246247
enum FullSnippetLevelWrapper(SnippetLevel level) = SnippetLevelWrapper!level ~ " super.visit(dec);";
247248

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

290+
override void visit(const DeclarationOrStatement dec)
291+
{
292+
super.visit(dec);
293+
if (!dec.tokens.length || dec.tokens[0].index <= position)
294+
lastStatement = cast()dec;
295+
}
296+
289297
static foreach (T; AliasSeq!(Arguments, ExpressionNode))
290298
override void visit(const T dec)
291299
{
@@ -363,6 +371,7 @@ class SnippetInfoGenerator : ASTVisitor
363371

364372
bool done;
365373
VariableUsage[] variableStack;
374+
DeclarationOrStatement lastStatement;
366375
size_t position, current;
367376
SnippetInfo ret;
368377
}

source/workspaced/com/snippets/package.d

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,16 @@ class SnippetsComponent : ComponentWrapper
7373
// nudge in next token if position is not exactly on the start of it
7474
if (loc < tokens.length && tokens[loc].index < position)
7575
loc++;
76+
// determine info from before start of identifier (so you can start typing something and it still finds a snippet scope)
77+
if (loc > 0 && loc < tokens.length && tokens[loc].type == tok!"identifier" && tokens[loc].index >= position)
78+
loc--;
79+
80+
int contextIndex;
81+
if (loc >= 0 && loc < tokens.length)
82+
contextIndex = cast(int) tokens[loc].index;
7683

7784
if (loc == 0 || loc == tokens.length)
78-
return SnippetInfo([SnippetLevel.global]);
85+
return SnippetInfo(contextIndex, [SnippetLevel.global]);
7986

8087
auto leading = tokens[0 .. loc];
8188

@@ -93,9 +100,9 @@ class SnippetsComponent : ComponentWrapper
93100
// needs to be modified to check the exact trivia token instead
94101
// of the associated token with it.
95102
if (last.text[0 .. len].startsWith("///", "/++", "/**"))
96-
return SnippetInfo([SnippetLevel.docComment]);
103+
return SnippetInfo(contextIndex, [SnippetLevel.docComment]);
97104
else if (len >= 2)
98-
return SnippetInfo([SnippetLevel.comment]);
105+
return SnippetInfo(contextIndex, [SnippetLevel.comment]);
99106
else
100107
break;
101108
case tok!"dstringLiteral":
@@ -109,15 +116,15 @@ class SnippetsComponent : ComponentWrapper
109116
// quote character
110117
// TODO: properly check if this is an unescaped escape
111118
if (textSoFar.endsWith('\\', last.text[0]))
112-
return SnippetInfo([SnippetLevel.strings, SnippetLevel.other]);
119+
return SnippetInfo(contextIndex, [SnippetLevel.strings, SnippetLevel.other]);
113120
else
114-
return SnippetInfo([SnippetLevel.strings]);
121+
return SnippetInfo(contextIndex, [SnippetLevel.strings]);
115122
case tok!"(":
116123
if (leading.length >= 2)
117124
{
118125
auto beforeLast = leading[$ - 2];
119126
if (beforeLast.type.among(tok!"__traits", tok!"version", tok!"debug"))
120-
return SnippetInfo([SnippetLevel.other]);
127+
return SnippetInfo(contextIndex, [SnippetLevel.other]);
121128
}
122129
break;
123130
default:
@@ -133,13 +140,13 @@ class SnippetsComponent : ComponentWrapper
133140
// test for tokens semicolon closed statements where we should abort to avoid incomplete syntax
134141
if (t.type.among!(tok!"import", tok!"module"))
135142
{
136-
return SnippetInfo([SnippetLevel.global, SnippetLevel.other]);
143+
return SnippetInfo(contextIndex, [SnippetLevel.global, SnippetLevel.other]);
137144
}
138145
else if (t.type.among!(tok!"=", tok!"+", tok!"-", tok!"*", tok!"/",
139146
tok!"%", tok!"^^", tok!"&", tok!"|", tok!"^", tok!"<<",
140147
tok!">>", tok!">>>", tok!"~", tok!"in"))
141148
{
142-
return SnippetInfo([SnippetLevel.global, SnippetLevel.value]);
149+
return SnippetInfo(contextIndex, [SnippetLevel.global, SnippetLevel.value]);
143150
}
144151
}
145152

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

151158
scope gen = new SnippetInfoGenerator(position);
159+
gen.value.contextTokenIndex = contextIndex;
152160
gen.variableStack.reserve(64);
153161
gen.visit(parsed);
154162

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

176+
if (gen.lastStatement)
177+
{
178+
import dparse.ast;
179+
180+
LastStatementInfo info;
181+
auto nodeType = gen.lastStatement.findDeepestNonBlockNode;
182+
if (gen.lastStatement.tokens.length)
183+
info.location = cast(int) nodeType.tokens[0].index;
184+
info.type = typeid(nodeType).name;
185+
auto lastDot = info.type.lastIndexOf('.');
186+
if (lastDot != -1)
187+
info.type = info.type[lastDot + 1 .. $];
188+
if (auto ifStmt = cast(IfStatement)nodeType)
189+
{
190+
auto elseStmt = getIfElse(ifStmt);
191+
if (cast(IfStatement)elseStmt)
192+
info.ifHasElse = false;
193+
else
194+
info.ifHasElse = elseStmt !is null;
195+
}
196+
else if (auto ifStmt = cast(ConditionalDeclaration)nodeType)
197+
info.ifHasElse = ifStmt.hasElse;
198+
// if (auto ifStmt = cast(ConditionalStatement)nodeType)
199+
// info.ifHasElse = !!getIfElse(ifStmt);
200+
201+
gen.value.lastStatement = info;
202+
}
203+
168204
return gen.value;
169205
}
170206

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

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

596+
struct LastStatementInfo
597+
{
598+
/// The libdparse class name (typeid) of the last parsable statement before
599+
/// the cursor, stripped of module name.
600+
string type;
601+
/// If type is set, this is the start location in bytes where
602+
/// the first token was.
603+
int location;
604+
/// True if the type is (`IfStatement`, `ConditionalDeclaration` or
605+
/// `ConditionalStatement`) and has a final `else` block defined.
606+
bool ifHasElse;
607+
}
608+
555609
/// A list of snippets resolved at a given position.
556610
struct SnippetList
557611
{

source/workspaced/com/snippets/smart.d

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
module workspaced.com.snippets.smart;
22

3+
// debug = SnippetScope;
4+
35
import workspaced.api;
46
import workspaced.com.snippets;
57

8+
import std.algorithm;
69
import std.conv;
10+
import std.string;
711

812
class SmartSnippetProvider : SnippetProvider
913
{
1014
Future!(Snippet[]) provideSnippets(scope const WorkspaceD.Instance instance,
1115
scope const(char)[] file, scope const(char)[] code, int position, const SnippetInfo info)
1216
{
17+
Snippet[] res;
18+
1319
if (info.loopScope.supported)
1420
{
15-
Snippet[] res;
1621
if (info.loopScope.numItems > 1)
1722
{
1823
res ~= ndForeach(info.loopScope.numItems, info.loopScope.iterator);
@@ -29,10 +34,50 @@ class SmartSnippetProvider : SnippetProvider
2934
res ~= simpleForeach(info.loopScope.iterator, info.loopScope.type);
3035
res ~= stringIterators();
3136
}
32-
return typeof(return).fromResult(res);
3337
}
34-
else
35-
return typeof(return).fromResult(null);
38+
39+
if (info.lastStatement.type == "IfStatement"
40+
&& !info.lastStatement.ifHasElse)
41+
{
42+
int ifIndex = info.contextTokenIndex == 0 ? position : info.contextTokenIndex;
43+
auto hasBraces = code[0 .. max(min(ifIndex, $), 0)].stripRight.endsWith("}");
44+
Snippet snp;
45+
snp.providerId = typeid(this).name;
46+
snp.id = "else";
47+
snp.title = "else";
48+
snp.shortcut = "else";
49+
snp.documentation = "else block";
50+
if (hasBraces)
51+
{
52+
snp.plain = "else {\n\t\n}";
53+
snp.snippet = "else {\n\t$0\n}";
54+
}
55+
else
56+
{
57+
snp.plain = "else\n\t";
58+
snp.snippet = "else\n\t$0";
59+
}
60+
snp.unformatted = true;
61+
snp.resolved = true;
62+
res ~= snp;
63+
}
64+
65+
debug (SnippetScope)
66+
{
67+
import painlessjson : toJSON;
68+
69+
Snippet ret;
70+
ret.providerId = typeid(this).name;
71+
ret.id = "workspaced-snippet-debug";
72+
ret.title = "[DEBUG] Snippet";
73+
ret.shortcut = "__debug_snippet";
74+
ret.plain = ret.snippet = info.toJSON.toPrettyString;
75+
ret.unformatted = true;
76+
ret.resolved = true;
77+
res ~= ret;
78+
}
79+
80+
return typeof(return).fromResult(res.length ? res : null);
3681
}
3782

3883
Future!Snippet resolveSnippet(scope const WorkspaceD.Instance instance,

source/workspaced/dparseext.d

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,3 +301,82 @@ string evaluateExpressionString(const Token token)
301301
return null;
302302
}
303303
}
304+
305+
/// Finds the deepest non-null node of any BaseNode. (like visiting the tree)
306+
/// Aborts on types that contain `DeclarationOrStatement` or `Declaration[]`
307+
/// fields.
308+
/// Useful for getting the IfStatement out of a DeclarationOrStatement without
309+
/// traversing its children.
310+
BaseNode findDeepestNonBlockNode(T : BaseNode)(T ast)
311+
{
312+
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)");
313+
bool nonProcess = false;
314+
foreach (member; ast.tupleof)
315+
{
316+
static if (is(typeof(member) : DeclarationOrStatement)
317+
|| is(typeof(member) : Declaration[]))
318+
{
319+
nonProcess = true;
320+
}
321+
}
322+
323+
if (nonProcess)
324+
return ast;
325+
326+
foreach (member; ast.tupleof)
327+
{
328+
static if (is(typeof(member) : BaseNode))
329+
{
330+
if (member !is null)
331+
{
332+
return findDeepestNonBlockNode(member);
333+
}
334+
}
335+
}
336+
return ast;
337+
}
338+
339+
/// Gets the final `else` block of an if. Will return a node of type
340+
/// `IfStatement` if it's an `else if` block. Returns null if there is no single
341+
/// else statement.
342+
BaseNode getIfElse(IfStatement ifStmt)
343+
{
344+
if (!ifStmt.elseStatement)
345+
return null;
346+
347+
while (true)
348+
{
349+
auto elseStmt = ifStmt.elseStatement;
350+
if (!elseStmt)
351+
return ifStmt;
352+
353+
auto stmtInElse = elseStmt.findDeepestNonBlockNode;
354+
assert(stmtInElse !is elseStmt);
355+
356+
if (cast(IfStatement)stmtInElse)
357+
ifStmt = cast(IfStatement)stmtInElse;
358+
else
359+
return stmtInElse;
360+
}
361+
}
362+
363+
unittest
364+
{
365+
StringCache stringCache = StringCache(StringCache.defaultBucketCount);
366+
RollbackAllocator rba;
367+
IfStatement parseIfStmt(string code)
368+
{
369+
const(Token)[] tokens = getTokensForParser(cast(ubyte[])code, LexerConfig.init, &stringCache);
370+
auto parser = new Parser();
371+
parser.tokens = tokens;
372+
parser.allocator = &rba;
373+
return parser.parseIfStatement();
374+
}
375+
376+
alias p = parseIfStmt;
377+
assert(getIfElse(p("if (x) {}")) is null);
378+
assert(getIfElse(p("if (x) {} else if (y) {}")) !is null);
379+
assert(cast(IfStatement)getIfElse(p("if (x) {} else if (y) {}")) !is null, typeid(getIfElse(p("if (x) {} else if (y) {}"))).name);
380+
assert(getIfElse(p("if (x) {} else if (y) {} else {}")) !is null);
381+
assert(cast(IfStatement)getIfElse(p("if (x) {} else if (y) {} else {}")) is null);
382+
}

0 commit comments

Comments
 (0)