1 module workspaced.com.snippets; 2 3 import dparse.lexer; 4 import dparse.parser; 5 import dparse.rollback_allocator; 6 7 import workspaced.api; 8 import workspaced.com.dfmt : DfmtComponent; 9 import workspaced.com.snippets.generator; 10 11 import std.algorithm; 12 import std.array; 13 import std.ascii; 14 import std.conv; 15 import std.json; 16 import std.string; 17 18 public import workspaced.com.snippets.plain; 19 public import workspaced.com.snippets.smart; 20 21 /// Component for auto completing snippets with context information and formatting these snippets with dfmt. 22 @component("snippets") 23 class SnippetsComponent : ComponentWrapper 24 { 25 mixin DefaultComponentWrapper; 26 27 protected SnippetProvider[] providers; 28 29 protected void load() 30 { 31 config.stringBehavior = StringBehavior.source; 32 providers.reserve(16); 33 providers ~= new PlainSnippetProvider(); 34 providers ~= new SmartSnippetProvider(); 35 } 36 37 /** 38 * Params: 39 * file = Filename to resolve dependencies relatively from. 40 * code = Code to complete snippet in. 41 * position = Byte offset of where to find scope in. 42 * 43 * Returns: a `SnippetInfo` object for all snippet information. 44 * 45 * `.loopScope` is set if a loop can be inserted at this position, Optionally 46 * with information about close ranges. Contains `SnippetLoopScope.init` if 47 * this is not a location where a loop can be inserted. 48 */ 49 SnippetInfo determineSnippetInfo(scope const(char)[] file, scope const(char)[] code, int position) 50 { 51 // each variable is 1 52 // maybe more expensive lookups with DCD in the future 53 enum LoopVariableAnalyzeMaxCost = 90; 54 55 scope tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 56 // TODO: binary search 57 size_t loc; 58 foreach (i, tok; tokens) 59 if (tok.index >= position) 60 { 61 loc = i; 62 break; 63 } 64 65 auto leading = tokens[0 .. loc]; 66 foreach_reverse (t; leading) 67 { 68 if (t.type == tok!";") 69 break; 70 71 // test for tokens semicolon closed statements where we should abort to avoid incomplete syntax 72 if (t.type.among!(tok!"import", tok!"module")) 73 { 74 return SnippetInfo([SnippetLevel.global, SnippetLevel.other]); 75 } 76 else if (t.type.among!(tok!"=", tok!"+", tok!"-", tok!"*", tok!"/", 77 tok!"%", tok!"^^", tok!"&", tok!"|", tok!"^", tok!"<<", 78 tok!">>", tok!">>>", tok!"~", tok!"in")) 79 { 80 return SnippetInfo([SnippetLevel.global, SnippetLevel.value]); 81 } 82 } 83 84 scope parsed = parseModule(tokens, cast(string) file, &rba); 85 86 trace("determineSnippetInfo at ", position); 87 88 scope gen = new SnippetInfoGenerator(position); 89 gen.variableStack.reserve(64); 90 gen.visit(parsed); 91 92 gen.value.loopScope.supported = gen.value.level == SnippetLevel.method; 93 if (gen.value.loopScope.supported) 94 { 95 int cost = 0; 96 foreach_reverse (v; gen.variableStack) 97 { 98 if (fillLoopScopeInfo(gen.value.loopScope, v)) 99 break; 100 if (++cost > LoopVariableAnalyzeMaxCost) 101 break; 102 } 103 } 104 105 return gen.value; 106 } 107 108 Future!(Snippet[]) getSnippets(scope const(char)[] file, scope const(char)[] code, int position) 109 { 110 mixin(gthreadsAsyncProxy!`getSnippetsBlocking(file, code, position)`); 111 } 112 113 Snippet[] getSnippetsBlocking(scope const(char)[] file, scope const(char)[] code, int position) 114 { 115 auto futures = collectSnippets(file, code, position); 116 117 auto ret = appender!(Snippet[]); 118 foreach (fut; futures) 119 ret.put(fut.getBlocking()); 120 return ret.data; 121 } 122 123 Snippet[] getSnippetsYield(scope const(char)[] file, scope const(char)[] code, int position) 124 { 125 auto futures = collectSnippets(file, code, position); 126 127 auto ret = appender!(Snippet[]); 128 foreach (fut; futures) 129 ret.put(fut.getYield()); 130 return ret.data; 131 } 132 133 Future!Snippet resolveSnippet(scope const(char)[] file, scope const(char)[] code, 134 int position, Snippet snippet) 135 { 136 foreach (provider; providers) 137 { 138 if (typeid(provider).name == snippet.providerId) 139 { 140 const info = determineSnippetInfo(file, code, position); 141 return provider.resolveSnippet(instance, file, code, position, info, snippet); 142 } 143 } 144 145 return Future!Snippet.fromResult(snippet); 146 } 147 148 Future!string format(scope const(char)[] snippet, string[] arguments = []) 149 { 150 mixin(gthreadsAsyncProxy!`formatSync(snippet, arguments)`); 151 } 152 153 /// Will format the code passed in synchronously using dfmt. Might take a short moment on larger documents. 154 /// Returns: the formatted code as string or unchanged if dfmt is not active 155 string formatSync(scope const(char)[] snippet, string[] arguments = []) 156 { 157 if (!has!DfmtComponent) 158 return snippet.idup; 159 160 auto dfmt = get!DfmtComponent; 161 162 auto tmp = appender!string; 163 164 scope const(char)[][string] tokens; 165 166 ptrdiff_t dollar, last; 167 while (true) 168 { 169 dollar = snippet.indexOfAny(`$\`, last); 170 if (dollar == -1) 171 { 172 tmp ~= snippet[last .. $]; 173 break; 174 } 175 176 tmp ~= snippet[last .. dollar]; 177 last = dollar + 1; 178 if (last >= snippet.length) 179 break; 180 if (snippet[dollar] == '\\') 181 { 182 tmp ~= snippet[dollar + 1]; 183 last = dollar + 2; 184 } 185 else 186 { 187 string key = "__WspD_Snp_" ~ dollar.to!string; 188 const(char)[] str; 189 190 if (snippet[dollar + 1] == '{') 191 { 192 ptrdiff_t i = dollar + 2; 193 int depth = 1; 194 while (true) 195 { 196 auto next = snippet.indexOfAny(`\{}`, i); 197 if (next == -1) 198 { 199 i = snippet.length; 200 break; 201 } 202 203 if (snippet[next] == '\\') 204 i = next + 2; 205 else 206 { 207 if (snippet[next] == '{') 208 depth++; 209 else if (snippet[next] == '}') 210 depth--; 211 else 212 assert(false); 213 214 i = next + 1; 215 } 216 217 if (depth == 0) 218 break; 219 } 220 str = snippet[dollar .. i]; 221 last = i; 222 223 if (str.length > 5 || snippet[last .. $].stripLeft.startsWith(";", ".", "{")) 224 { 225 // let's insert some token in here instead of a comment because there is probably some default content 226 if (str[0 .. $ - 1].endsWith(';')) 227 key ~= ';'; 228 } 229 else 230 { 231 // empty default, put in comment 232 key = "/+++" ~ key ~ "+++/"; 233 } 234 } 235 else 236 { 237 size_t end = dollar + 1; 238 239 if (snippet[dollar + 1].isDigit) 240 { 241 while (end < snippet.length && snippet[end].isDigit) 242 end++; 243 } 244 else 245 { 246 while (end < snippet.length && (snippet[end].isAlphaNum || snippet[end] == '_')) 247 end++; 248 } 249 250 str = snippet[dollar .. end]; 251 last = end; 252 if (snippet[last .. $].stripLeft.startsWith(";", ".", "{")) 253 { 254 // keep value thing as token 255 } 256 else 257 { 258 // primitive placeholder as comment 259 key = "/+++" ~ key ~ "+++/"; 260 } 261 } 262 263 tokens[key] = str; 264 tmp ~= key; 265 } 266 } 267 268 auto res = dfmt.formatSync(tmp.data, arguments); 269 270 foreach (key, value; tokens) 271 { 272 // TODO: replacing using aho-corasick would be far more efficient but there is nothing like that in phobos 273 res = res.replace(key, value); 274 } 275 276 if (res.endsWith("\r\n") && !snippet.endsWith('\n')) 277 res.length -= 2; 278 else if (res.endsWith('\n') && !snippet.endsWith('\n')) 279 res.length--; 280 281 if (res.endsWith(";\n\n$0")) 282 res = res[0 .. $ - "\n$0".length] ~ "$0"; 283 else if (res.endsWith(";\r\n\r\n$0")) 284 res = res[0 .. $ - "\r\n$0".length] ~ "$0"; 285 286 return res; 287 } 288 289 private: 290 Future!(Snippet[])[] collectSnippets(scope const(char)[] file, 291 scope const(char)[] code, int position) 292 { 293 const inst = instance; 294 const info = determineSnippetInfo(file, code, position); 295 auto futures = appender!(Future!(Snippet[])[]); 296 foreach (provider; providers) 297 futures.put(provider.provideSnippets(inst, file, code, position, info)); 298 return futures.data; 299 } 300 301 RollbackAllocator rba; 302 LexerConfig config; 303 } 304 305 /// 306 enum SnippetLevel 307 { 308 /// Outside of functions or types, possibly inside templates 309 global, 310 /// Inside interfaces, classes, structs or unions 311 type, 312 /// Inside method body 313 method, 314 /// inside a variable value, argument call, default value or similar 315 value, 316 /// Other scope types (for example outside of braces but after a function definition or some other invalid syntax place) 317 other 318 } 319 320 /// 321 struct SnippetLoopScope 322 { 323 /// true if an loop expression can be inserted at this point 324 bool supported; 325 /// true if we know we are iterating over a string (possibly needing unicode decoding) or false otherwise 326 bool stringIterator; 327 /// Explicit type to use when iterating or null if none is known 328 string type; 329 /// Best variable to iterate over or null if none was found 330 string iterator; 331 /// Number of keys to iterate over 332 int numItems = 1; 333 } 334 335 /// 336 struct SnippetInfo 337 { 338 /// Levels this snippet location has gone through, latest one being the last 339 SnippetLevel[] stack = [SnippetLevel.global]; 340 /// Information about snippets using loop context 341 SnippetLoopScope loopScope; 342 343 /// Current snippet scope level of the location 344 SnippetLevel level() const @property 345 { 346 return stack.length ? stack[$ - 1] : SnippetLevel.other; 347 } 348 } 349 350 /// 351 interface SnippetProvider 352 { 353 Future!(Snippet[]) provideSnippets(scope const WorkspaceD.Instance instance, 354 scope const(char)[] file, scope const(char)[] code, int position, const SnippetInfo info); 355 356 Future!Snippet resolveSnippet(scope const WorkspaceD.Instance instance, 357 scope const(char)[] file, scope const(char)[] code, int position, 358 const SnippetInfo info, Snippet snippet); 359 } 360 361 /// Snippet to insert 362 struct Snippet 363 { 364 /// Internal ID for resolving this snippet 365 string id, providerId; 366 /// User-defined data for helping resolving this snippet 367 JSONValue data; 368 /// Label for this snippet 369 string title; 370 /// Shortcut to type for this snippet 371 string shortcut; 372 /// Markdown documentation for this snippet 373 string documentation; 374 /// Plain text to insert assuming global level indentation. 375 string plain; 376 /// Text with interactive snippet locations to insert assuming global indentation. 377 string snippet; 378 /// true if this snippet can be used as-is 379 bool resolved; 380 } 381 382 unittest 383 { 384 scope backend = new WorkspaceD(); 385 auto workspace = makeTemporaryTestingWorkspace; 386 auto instance = backend.addInstance(workspace.directory); 387 backend.register!SnippetsComponent; 388 backend.register!DfmtComponent; 389 SnippetsComponent snippets = backend.get!SnippetsComponent(workspace.directory); 390 391 auto args = ["--indent_style", "tab"]; 392 393 auto res = snippets.formatSync("void main(${1:string[] args}) {\n\t$0\n}", args); 394 shouldEqual(res, "void main(${1:string[] args})\n{\n\t$0\n}"); 395 396 res = snippets.formatSync("class ${1:MyClass} {\n\t$0\n}", args); 397 shouldEqual(res, "class ${1:MyClass}\n{\n\t$0\n}"); 398 399 res = snippets.formatSync("enum ${1:MyEnum} = $2;\n$0", args); 400 shouldEqual(res, "enum ${1:MyEnum} = $2;\n$0"); 401 402 res = snippets.formatSync("import ${1:std};\n$0", args); 403 shouldEqual(res, "import ${1:std};\n$0"); 404 }