diff --git a/source/workspaced/com/snippets/generator.d b/source/workspaced/com/snippets/generator.d index 21e87db..da45f5e 100644 --- a/source/workspaced/com/snippets/generator.d +++ b/source/workspaced/com/snippets/generator.d @@ -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; } diff --git a/source/workspaced/com/snippets/package.d b/source/workspaced/com/snippets/package.d index b4c3748..74e0808 100644 --- a/source/workspaced/com/snippets/package.d +++ b/source/workspaced/com/snippets/package.d @@ -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 { diff --git a/source/workspaced/com/snippets/smart.d b/source/workspaced/com/snippets/smart.d index ca7de02..2904165 100644 --- a/source/workspaced/com/snippets/smart.d +++ b/source/workspaced/com/snippets/smart.d @@ -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, diff --git a/source/workspaced/dparseext.d b/source/workspaced/dparseext.d index 76a0dca..2cbbbdc 100644 --- a/source/workspaced/dparseext.d +++ b/source/workspaced/dparseext.d @@ -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); +}