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 scope parsed = parseModule(tokens, cast(string) file, &rba); 99 100 trace("determineSnippetInfo at ", position); 101 102 scope gen = new SnippetInfoGenerator(position); 103 gen.variableStack.reserve(64); 104 gen.visit(parsed); 105 106 gen.value.loopScope.supported = gen.value.level == SnippetLevel.method; 107 if (gen.value.loopScope.supported) 108 { 109 int cost = 0; 110 foreach_reverse (v; gen.variableStack) 111 { 112 if (fillLoopScopeInfo(gen.value.loopScope, v)) 113 break; 114 if (++cost > LoopVariableAnalyzeMaxCost) 115 break; 116 } 117 } 118 119 return gen.value; 120 } 121 122 Future!SnippetList getSnippets(scope const(char)[] file, scope const(char)[] code, int position) 123 { 124 mixin(gthreadsAsyncProxy!`getSnippetsBlocking(file, code, position)`); 125 } 126 127 SnippetList getSnippetsBlocking(scope const(char)[] file, scope const(char)[] code, int position) 128 { 129 auto futures = collectSnippets(file, code, position); 130 131 auto ret = appender!(Snippet[]); 132 foreach (fut; futures[1]) 133 ret.put(fut.getBlocking()); 134 return SnippetList(futures[0], ret.data); 135 } 136 137 SnippetList getSnippetsYield(scope const(char)[] file, scope const(char)[] code, int position) 138 { 139 auto futures = collectSnippets(file, code, position); 140 141 auto ret = appender!(Snippet[]); 142 foreach (fut; futures[1]) 143 ret.put(fut.getYield()); 144 return SnippetList(futures[0], ret.data); 145 } 146 147 Future!Snippet resolveSnippet(scope const(char)[] file, scope const(char)[] code, 148 int position, Snippet snippet) 149 { 150 foreach (provider; providers) 151 { 152 if (typeid(provider).name == snippet.providerId) 153 { 154 const info = determineSnippetInfo(file, code, position); 155 return provider.resolveSnippet(instance, file, code, position, info, snippet); 156 } 157 } 158 159 return Future!Snippet.fromResult(snippet); 160 } 161 162 Future!string format(scope const(char)[] snippet, string[] arguments = [], 163 SnippetLevel level = SnippetLevel.global) 164 { 165 mixin(gthreadsAsyncProxy!`formatSync(snippet, arguments, level)`); 166 } 167 168 /// Will format the code passed in synchronously using dfmt. Might take a short moment on larger documents. 169 /// Returns: the formatted code as string or unchanged if dfmt is not active 170 string formatSync(scope const(char)[] snippet, string[] arguments = [], 171 SnippetLevel level = SnippetLevel.global) 172 { 173 if (!has!DfmtComponent) 174 return snippet.idup; 175 176 auto dfmt = get!DfmtComponent; 177 178 auto tmp = appender!string; 179 180 final switch (level) 181 { 182 case SnippetLevel.global: 183 case SnippetLevel.other: 184 break; 185 case SnippetLevel.type: 186 tmp.put("struct FORMAT_HELPER {\n"); 187 break; 188 case SnippetLevel.method: 189 tmp.put("void FORMAT_HELPER() {\n"); 190 break; 191 case SnippetLevel.value: 192 tmp.put("int FORMAT_HELPER() = "); 193 break; 194 } 195 196 scope const(char)[][string] tokens; 197 198 ptrdiff_t dollar, last; 199 while (true) 200 { 201 dollar = snippet.indexOfAny(`$\`, last); 202 if (dollar == -1) 203 { 204 tmp ~= snippet[last .. $]; 205 break; 206 } 207 208 tmp ~= snippet[last .. dollar]; 209 last = dollar + 1; 210 if (last >= snippet.length) 211 break; 212 if (snippet[dollar] == '\\') 213 { 214 tmp ~= snippet[dollar + 1]; 215 last = dollar + 2; 216 } 217 else 218 { 219 string key = "__WspD_Snp_" ~ dollar.to!string; 220 const(char)[] str; 221 222 bool startOfBlock = snippet[0 .. dollar].stripRight.endsWith("{"); 223 bool endOfBlock; 224 225 bool makeWrappingIfMayBeDelegate() 226 { 227 endOfBlock = snippet[last .. $].stripLeft.startsWith("}"); 228 if (startOfBlock && endOfBlock) 229 { 230 // make extra long to make dfmt definitely wrap this (in case this is a delegate, otherwise this doesn't hurt either) 231 key.reserve(key.length + 200); 232 foreach (i; 0 .. 200) 233 key ~= "_"; 234 return true; 235 } 236 else 237 return false; 238 } 239 240 if (snippet[dollar + 1] == '{') 241 { 242 ptrdiff_t i = dollar + 2; 243 int depth = 1; 244 while (true) 245 { 246 auto next = snippet.indexOfAny(`\{}`, i); 247 if (next == -1) 248 { 249 i = snippet.length; 250 break; 251 } 252 253 if (snippet[next] == '\\') 254 i = next + 2; 255 else 256 { 257 if (snippet[next] == '{') 258 depth++; 259 else if (snippet[next] == '}') 260 depth--; 261 else 262 assert(false); 263 264 i = next + 1; 265 } 266 267 if (depth == 0) 268 break; 269 } 270 str = snippet[dollar .. i]; 271 last = i; 272 273 const wrapped = makeWrappingIfMayBeDelegate(); 274 275 const placeholderMightBeIdentifier = str.length > 5 276 || snippet[last .. $].stripLeft.startsWith(";", ".", "{"); 277 278 if (wrapped || placeholderMightBeIdentifier) 279 { 280 // let's insert some token in here instead of a comment because there is probably some default content 281 // if there is a semicolon at the end we probably need to insert a semicolon here too 282 // if this is a comment placeholder let's insert a semicolon to make dfmt wrap 283 if (str[0 .. $ - 1].endsWith(';') || str[0 .. $ - 1].canFind("//")) 284 key ~= ';'; 285 } 286 else if (level != SnippetLevel.value) 287 { 288 // empty default, put in comment 289 key = "/+++" ~ key ~ "+++/"; 290 } 291 } 292 else 293 { 294 size_t end = dollar + 1; 295 296 if (snippet[dollar + 1].isDigit) 297 { 298 while (end < snippet.length && snippet[end].isDigit) 299 end++; 300 } 301 else 302 { 303 while (end < snippet.length && (snippet[end].isAlphaNum || snippet[end] == '_')) 304 end++; 305 } 306 307 str = snippet[dollar .. end]; 308 last = end; 309 310 makeWrappingIfMayBeDelegate(); 311 312 const placeholderMightBeIdentifier = snippet[last .. $].stripLeft.startsWith(";", 313 ".", "{"); 314 315 if (placeholderMightBeIdentifier) 316 { 317 // keep value thing as simple identifier as we don't have any placeholder text 318 } 319 else if (level != SnippetLevel.value) 320 { 321 // primitive placeholder as comment 322 key = "/+++" ~ key ~ "+++/"; 323 } 324 } 325 326 tokens[key] = str; 327 tmp ~= key; 328 } 329 } 330 331 final switch (level) 332 { 333 case SnippetLevel.global: 334 case SnippetLevel.other: 335 break; 336 case SnippetLevel.type: 337 case SnippetLevel.method: 338 tmp.put("}"); 339 break; 340 case SnippetLevel.value: 341 tmp.put(";"); 342 break; 343 } 344 345 auto res = dfmt.formatSync(tmp.data, arguments); 346 347 string chompStr; 348 char del; 349 final switch (level) 350 { 351 case SnippetLevel.global: 352 case SnippetLevel.other: 353 break; 354 case SnippetLevel.type: 355 case SnippetLevel.method: 356 chompStr = "}"; 357 del = '{'; 358 break; 359 case SnippetLevel.value: 360 chompStr = ";"; 361 del = '='; 362 break; 363 } 364 365 if (chompStr.length) 366 res = res.stripRight.chomp(chompStr); 367 368 if (del != char.init) 369 { 370 auto start = res.indexOf(del); 371 if (start != -1) 372 { 373 res = res[start + 1 .. $]; 374 375 while (true) 376 { 377 // delete empty lines before first line 378 auto nl = res.indexOf('\n'); 379 if (nl != -1 && res[0 .. nl].all!isWhite) 380 res = res[nl + 1 .. $]; 381 else 382 break; 383 } 384 385 auto indent = res[0 .. res.length - res.stripLeft.length]; 386 if (indent.length) 387 { 388 // remove indentation of whole block 389 assert(indent.all!isWhite); 390 res = res.splitLines.map!(a => a.startsWith(indent) 391 ? a[indent.length .. $] : a.stripRight).join("\n"); 392 } 393 } 394 } 395 396 foreach (key, value; tokens) 397 { 398 // TODO: replacing using aho-corasick would be far more efficient but there is nothing like that in phobos 399 res = res.replace(key, value); 400 } 401 402 if (res.endsWith("\r\n") && !snippet.endsWith('\n')) 403 res.length -= 2; 404 else if (res.endsWith('\n') && !snippet.endsWith('\n')) 405 res.length--; 406 407 if (res.endsWith(";\n\n$0")) 408 res = res[0 .. $ - "\n$0".length] ~ "$0"; 409 else if (res.endsWith(";\r\n\r\n$0")) 410 res = res[0 .. $ - "\r\n$0".length] ~ "$0"; 411 412 return res; 413 } 414 415 /// Adds snippets which complete conditionally based on dub dependencies being present. 416 /// This function affects the global configuration of all instances. 417 /// Params: 418 /// requiredDependencies = The dependencies which must be present in order for this snippet to show up. 419 /// snippet = The snippet to suggest when the required dependencies are matched. 420 void addDependencySnippet(string[] requiredDependencies, PlainSnippet snippet) 421 { 422 // maybe application global change isn't such a good idea? Current config system seems too inefficient for this. 423 dependencySnippets.addSnippet(requiredDependencies, snippet); 424 } 425 426 private: 427 Tuple!(SnippetInfo, Future!(Snippet[])[]) collectSnippets(scope const(char)[] file, 428 scope const(char)[] code, int position) 429 { 430 const inst = instance; 431 auto info = determineSnippetInfo(file, code, position); 432 auto futures = appender!(Future!(Snippet[])[]); 433 foreach (provider; providers) 434 futures.put(provider.provideSnippets(inst, file, code, position, info)); 435 return tuple(info, futures.data); 436 } 437 438 RollbackAllocator rba; 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 }