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 import std.typecons; 18 19 public import workspaced.com.snippets.plain; 20 public import workspaced.com.snippets.smart; 21 public import workspaced.com.snippets.dependencies; 22 23 /// Component for auto completing snippets with context information and formatting these snippets with dfmt. 24 @component("snippets") 25 class SnippetsComponent : ComponentWrapper 26 { 27 mixin DefaultComponentWrapper; 28 29 static PlainSnippetProvider plainSnippets; 30 static SmartSnippetProvider smartSnippets; 31 static DependencyBasedSnippetProvider dependencySnippets; 32 33 protected SnippetProvider[] providers; 34 35 protected void load() 36 { 37 if (!plainSnippets) 38 plainSnippets = new PlainSnippetProvider(); 39 if (!smartSnippets) 40 smartSnippets = new SmartSnippetProvider(); 41 if (!dependencySnippets) 42 dependencySnippets = new DependencyBasedSnippetProvider(); 43 44 config.stringBehavior = StringBehavior.source; 45 providers.reserve(16); 46 providers ~= plainSnippets; 47 providers ~= smartSnippets; 48 providers ~= dependencySnippets; 49 } 50 51 /** 52 * Params: 53 * file = Filename to resolve dependencies relatively from. 54 * code = Code to complete snippet in. 55 * position = Byte offset of where to find scope in. 56 * 57 * Returns: a `SnippetInfo` object for all snippet information. 58 * 59 * `.loopScope` is set if a loop can be inserted at this position, Optionally 60 * with information about close ranges. Contains `SnippetLoopScope.init` if 61 * this is not a location where a loop can be inserted. 62 */ 63 SnippetInfo determineSnippetInfo(scope const(char)[] file, scope const(char)[] code, int position) 64 { 65 // each variable is 1 66 // maybe more expensive lookups with DCD in the future 67 enum LoopVariableAnalyzeMaxCost = 90; 68 69 scope tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 70 // TODO: binary search 71 size_t loc; 72 foreach (i, tok; tokens) 73 if (tok.index >= position) 74 { 75 loc = i; 76 break; 77 } 78 79 auto leading = tokens[0 .. loc]; 80 foreach_reverse (t; leading) 81 { 82 if (t.type == tok!";") 83 break; 84 85 // test for tokens semicolon closed statements where we should abort to avoid incomplete syntax 86 if (t.type.among!(tok!"import", tok!"module")) 87 { 88 return SnippetInfo([SnippetLevel.global, SnippetLevel.other]); 89 } 90 else if (t.type.among!(tok!"=", tok!"+", tok!"-", tok!"*", tok!"/", 91 tok!"%", tok!"^^", tok!"&", tok!"|", tok!"^", tok!"<<", 92 tok!">>", tok!">>>", tok!"~", tok!"in")) 93 { 94 return SnippetInfo([SnippetLevel.global, SnippetLevel.value]); 95 } 96 } 97 98 RollbackAllocator rba; 99 scope parsed = parseModule(tokens, cast(string) file, &rba); 100 101 trace("determineSnippetInfo at ", position); 102 103 scope gen = new SnippetInfoGenerator(position); 104 gen.variableStack.reserve(64); 105 gen.visit(parsed); 106 107 gen.value.loopScope.supported = gen.value.level == SnippetLevel.method; 108 if (gen.value.loopScope.supported) 109 { 110 int cost = 0; 111 foreach_reverse (v; gen.variableStack) 112 { 113 if (fillLoopScopeInfo(gen.value.loopScope, v)) 114 break; 115 if (++cost > LoopVariableAnalyzeMaxCost) 116 break; 117 } 118 } 119 120 return gen.value; 121 } 122 123 Future!SnippetList getSnippets(scope const(char)[] file, scope const(char)[] code, int position) 124 { 125 mixin(gthreadsAsyncProxy!`getSnippetsBlocking(file, code, position)`); 126 } 127 128 SnippetList getSnippetsBlocking(scope const(char)[] file, scope const(char)[] code, int position) 129 { 130 auto futures = collectSnippets(file, code, position); 131 132 auto ret = appender!(Snippet[]); 133 foreach (fut; futures[1]) 134 ret.put(fut.getBlocking()); 135 return SnippetList(futures[0], ret.data); 136 } 137 138 SnippetList getSnippetsYield(scope const(char)[] file, scope const(char)[] code, int position) 139 { 140 auto futures = collectSnippets(file, code, position); 141 142 auto ret = appender!(Snippet[]); 143 foreach (fut; futures[1]) 144 ret.put(fut.getYield()); 145 return SnippetList(futures[0], ret.data); 146 } 147 148 Future!Snippet resolveSnippet(scope const(char)[] file, scope const(char)[] code, 149 int position, Snippet snippet) 150 { 151 foreach (provider; providers) 152 { 153 if (typeid(provider).name == snippet.providerId) 154 { 155 const info = determineSnippetInfo(file, code, position); 156 return provider.resolveSnippet(instance, file, code, position, info, snippet); 157 } 158 } 159 160 return Future!Snippet.fromResult(snippet); 161 } 162 163 Future!string format(scope const(char)[] snippet, string[] arguments = [], 164 SnippetLevel level = SnippetLevel.global) 165 { 166 mixin(gthreadsAsyncProxy!`formatSync(snippet, arguments, level)`); 167 } 168 169 /// Will format the code passed in synchronously using dfmt. Might take a short moment on larger documents. 170 /// Returns: the formatted code as string or unchanged if dfmt is not active 171 string formatSync(scope const(char)[] snippet, string[] arguments = [], 172 SnippetLevel level = SnippetLevel.global) 173 { 174 if (!has!DfmtComponent) 175 return snippet.idup; 176 177 auto dfmt = get!DfmtComponent; 178 179 auto tmp = appender!string; 180 181 final switch (level) 182 { 183 case SnippetLevel.global: 184 case SnippetLevel.other: 185 break; 186 case SnippetLevel.type: 187 tmp.put("struct FORMAT_HELPER {\n"); 188 break; 189 case SnippetLevel.method: 190 tmp.put("void FORMAT_HELPER() {\n"); 191 break; 192 case SnippetLevel.value: 193 tmp.put("int FORMAT_HELPER() = "); 194 break; 195 } 196 197 scope const(char)[][string] tokens; 198 199 ptrdiff_t dollar, last; 200 while (true) 201 { 202 dollar = snippet.indexOfAny(`$\`, last); 203 if (dollar == -1) 204 { 205 tmp ~= snippet[last .. $]; 206 break; 207 } 208 209 tmp ~= snippet[last .. dollar]; 210 last = dollar + 1; 211 if (last >= snippet.length) 212 break; 213 if (snippet[dollar] == '\\') 214 { 215 tmp ~= snippet[dollar + 1]; 216 last = dollar + 2; 217 } 218 else 219 { 220 string key = "__WspD_Snp_" ~ dollar.to!string ~ "_"; 221 const(char)[] str; 222 223 bool startOfBlock = snippet[0 .. dollar].stripRight.endsWith("{"); 224 bool endOfBlock; 225 226 bool makeWrappingIfMayBeDelegate() 227 { 228 endOfBlock = snippet[last .. $].stripLeft.startsWith("}"); 229 if (startOfBlock && endOfBlock) 230 { 231 // make extra long to make dfmt definitely wrap this (in case this is a delegate, otherwise this doesn't hurt either) 232 key.reserve(key.length + 200); 233 foreach (i; 0 .. 200) 234 key ~= "_"; 235 return true; 236 } 237 else 238 return false; 239 } 240 241 if (snippet[dollar + 1] == '{') 242 { 243 ptrdiff_t i = dollar + 2; 244 int depth = 1; 245 while (true) 246 { 247 auto next = snippet.indexOfAny(`\{}`, i); 248 if (next == -1) 249 { 250 i = snippet.length; 251 break; 252 } 253 254 if (snippet[next] == '\\') 255 i = next + 2; 256 else 257 { 258 if (snippet[next] == '{') 259 depth++; 260 else if (snippet[next] == '}') 261 depth--; 262 else 263 assert(false); 264 265 i = next + 1; 266 } 267 268 if (depth == 0) 269 break; 270 } 271 str = snippet[dollar .. i]; 272 last = i; 273 274 const wrapped = makeWrappingIfMayBeDelegate(); 275 276 const placeholderMightBeIdentifier = str.length > 5 277 || snippet[last .. $].stripLeft.startsWith(";", ".", "{"); 278 279 if (wrapped || placeholderMightBeIdentifier) 280 { 281 // let's insert some token in here instead of a comment because there is probably some default content 282 // if there is a semicolon at the end we probably need to insert a semicolon here too 283 // if this is a comment placeholder let's insert a semicolon to make dfmt wrap 284 if (str[0 .. $ - 1].endsWith(';') || str[0 .. $ - 1].canFind("//")) 285 key ~= ';'; 286 } 287 else if (level != SnippetLevel.value) 288 { 289 // empty default, put in comment 290 key = "/+++" ~ key ~ "+++/"; 291 } 292 } 293 else 294 { 295 size_t end = dollar + 1; 296 297 if (snippet[dollar + 1].isDigit) 298 { 299 while (end < snippet.length && snippet[end].isDigit) 300 end++; 301 } 302 else 303 { 304 while (end < snippet.length && (snippet[end].isAlphaNum || snippet[end] == '_')) 305 end++; 306 } 307 308 str = snippet[dollar .. end]; 309 last = end; 310 311 makeWrappingIfMayBeDelegate(); 312 313 const placeholderMightBeIdentifier = snippet[last .. $].stripLeft.startsWith(";", 314 ".", "{"); 315 316 if (placeholderMightBeIdentifier) 317 { 318 // keep value thing as simple identifier as we don't have any placeholder text 319 } 320 else if (level != SnippetLevel.value) 321 { 322 // primitive placeholder as comment 323 key = "/+++" ~ key ~ "+++/"; 324 } 325 } 326 327 tokens[key] = str; 328 tmp ~= key; 329 } 330 } 331 332 final switch (level) 333 { 334 case SnippetLevel.global: 335 case SnippetLevel.other: 336 break; 337 case SnippetLevel.type: 338 case SnippetLevel.method: 339 tmp.put("}"); 340 break; 341 case SnippetLevel.value: 342 tmp.put(";"); 343 break; 344 } 345 346 auto res = dfmt.formatSync(tmp.data, arguments); 347 348 string chompStr; 349 char del; 350 final switch (level) 351 { 352 case SnippetLevel.global: 353 case SnippetLevel.other: 354 break; 355 case SnippetLevel.type: 356 case SnippetLevel.method: 357 chompStr = "}"; 358 del = '{'; 359 break; 360 case SnippetLevel.value: 361 chompStr = ";"; 362 del = '='; 363 break; 364 } 365 366 if (chompStr.length) 367 res = res.stripRight.chomp(chompStr); 368 369 if (del != char.init) 370 { 371 auto start = res.indexOf(del); 372 if (start != -1) 373 { 374 res = res[start + 1 .. $]; 375 376 while (true) 377 { 378 // delete empty lines before first line 379 auto nl = res.indexOf('\n'); 380 if (nl != -1 && res[0 .. nl].all!isWhite) 381 res = res[nl + 1 .. $]; 382 else 383 break; 384 } 385 386 auto indent = res[0 .. res.length - res.stripLeft.length]; 387 if (indent.length) 388 { 389 // remove indentation of whole block 390 assert(indent.all!isWhite); 391 res = res.splitLines.map!(a => a.startsWith(indent) 392 ? a[indent.length .. $] : a.stripRight).join("\n"); 393 } 394 } 395 } 396 397 foreach (key, value; tokens) 398 { 399 // TODO: replacing using aho-corasick would be far more efficient but there is nothing like that in phobos 400 res = res.replace(key, value); 401 } 402 403 if (res.endsWith("\r\n") && !snippet.endsWith('\n')) 404 res.length -= 2; 405 else if (res.endsWith('\n') && !snippet.endsWith('\n')) 406 res.length--; 407 408 if (res.endsWith(";\n\n$0")) 409 res = res[0 .. $ - "\n$0".length] ~ "$0"; 410 else if (res.endsWith(";\r\n\r\n$0")) 411 res = res[0 .. $ - "\r\n$0".length] ~ "$0"; 412 413 return res; 414 } 415 416 /// Adds snippets which complete conditionally based on dub dependencies being present. 417 /// This function affects the global configuration of all instances. 418 /// Params: 419 /// requiredDependencies = The dependencies which must be present in order for this snippet to show up. 420 /// snippet = The snippet to suggest when the required dependencies are matched. 421 void addDependencySnippet(string[] requiredDependencies, PlainSnippet snippet) 422 { 423 // maybe application global change isn't such a good idea? Current config system seems too inefficient for this. 424 dependencySnippets.addSnippet(requiredDependencies, snippet); 425 } 426 427 private: 428 Tuple!(SnippetInfo, Future!(Snippet[])[]) collectSnippets(scope const(char)[] file, 429 scope const(char)[] code, int position) 430 { 431 const inst = instance; 432 auto info = determineSnippetInfo(file, code, position); 433 auto futures = appender!(Future!(Snippet[])[]); 434 foreach (provider; providers) 435 futures.put(provider.provideSnippets(inst, file, code, position, info)); 436 return tuple(info, futures.data); 437 } 438 439 LexerConfig config; 440 } 441 442 /// 443 enum SnippetLevel 444 { 445 /// Outside of functions or types, possibly inside templates 446 global, 447 /// Inside interfaces, classes, structs or unions 448 type, 449 /// Inside method body 450 method, 451 /// inside a variable value, argument call, default value or similar 452 value, 453 /// Other scope types (for example outside of braces but after a function definition or some other invalid syntax place) 454 other 455 } 456 457 /// 458 struct SnippetLoopScope 459 { 460 /// true if an loop expression can be inserted at this point 461 bool supported; 462 /// true if we know we are iterating over a string (possibly needing unicode decoding) or false otherwise 463 bool stringIterator; 464 /// Explicit type to use when iterating or null if none is known 465 string type; 466 /// Best variable to iterate over or null if none was found 467 string iterator; 468 /// Number of keys to iterate over 469 int numItems = 1; 470 } 471 472 /// 473 struct SnippetInfo 474 { 475 /// Levels this snippet location has gone through, latest one being the last 476 SnippetLevel[] stack = [SnippetLevel.global]; 477 /// Information about snippets using loop context 478 SnippetLoopScope loopScope; 479 480 /// Current snippet scope level of the location 481 SnippetLevel level() const @property 482 { 483 return stack.length ? stack[$ - 1] : SnippetLevel.other; 484 } 485 } 486 487 /// A list of snippets resolved at a given position. 488 struct SnippetList 489 { 490 /// The info where this snippet is completing at. 491 SnippetInfo info; 492 /// The list of snippets that got returned. 493 Snippet[] snippets; 494 } 495 496 /// 497 interface SnippetProvider 498 { 499 Future!(Snippet[]) provideSnippets(scope const WorkspaceD.Instance instance, 500 scope const(char)[] file, scope const(char)[] code, int position, const SnippetInfo info); 501 502 Future!Snippet resolveSnippet(scope const WorkspaceD.Instance instance, 503 scope const(char)[] file, scope const(char)[] code, int position, 504 const SnippetInfo info, Snippet snippet); 505 } 506 507 /// Snippet to insert 508 struct Snippet 509 { 510 /// Internal ID for resolving this snippet 511 string id, providerId; 512 /// User-defined data for helping resolving this snippet 513 JSONValue data; 514 /// Label for this snippet 515 string title; 516 /// Shortcut to type for this snippet 517 string shortcut; 518 /// Markdown documentation for this snippet 519 string documentation; 520 /// Plain text to insert assuming global level indentation. 521 string plain; 522 /// Text with interactive snippet locations to insert assuming global indentation. 523 string snippet; 524 /// true if this snippet can be used as-is 525 bool resolved; 526 } 527 528 unittest 529 { 530 scope backend = new WorkspaceD(); 531 auto workspace = makeTemporaryTestingWorkspace; 532 auto instance = backend.addInstance(workspace.directory); 533 backend.register!SnippetsComponent; 534 backend.register!DfmtComponent; 535 SnippetsComponent snippets = backend.get!SnippetsComponent(workspace.directory); 536 537 auto args = ["--indent_style", "tab"]; 538 539 auto res = snippets.formatSync("void main(${1:string[] args}) {\n\t$0\n}", args); 540 shouldEqual(res, "void main(${1:string[] args})\n{\n\t$0\n}"); 541 542 res = snippets.formatSync("class ${1:MyClass} {\n\t$0\n}", args); 543 shouldEqual(res, "class ${1:MyClass}\n{\n\t$0\n}"); 544 545 res = snippets.formatSync("enum ${1:MyEnum} = $2;\n$0", args); 546 shouldEqual(res, "enum ${1:MyEnum} = $2;\n$0"); 547 548 res = snippets.formatSync("import ${1:std};\n$0", args); 549 shouldEqual(res, "import ${1:std};\n$0"); 550 551 res = snippets.formatSync("import ${1:std};\n$0", args, SnippetLevel.method); 552 shouldEqual(res, "import ${1:std};\n$0"); 553 554 res = snippets.formatSync("foo(delegate() {\n${1:// foo}\n});", args, SnippetLevel.method); 555 shouldEqual(res, "foo(delegate() {\n\t${1:// foo}\n});"); 556 557 res = snippets.formatSync(`auto ${1:window} = new SimpleWindow(Size(${2:800, 600}), "$3");`, args, SnippetLevel.method); 558 shouldEqual(res, `auto ${1:window} = new SimpleWindow(Size(${2:800, 600}), "$3");`); 559 }