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 import workspaced.dparseext; 11 12 import std.algorithm; 13 import std.array; 14 import std.ascii; 15 import std.conv; 16 import std.json; 17 import std.string; 18 import std.typecons; 19 20 public import workspaced.com.snippets.control_flow; 21 public import workspaced.com.snippets.dependencies; 22 public import workspaced.com.snippets.plain; 23 public import workspaced.com.snippets.smart; 24 25 // ugly, but works for now 26 import mir.algebraic_alias.json : JsonValue = JsonAlgebraic; 27 28 /// Component for auto completing snippets with context information and formatting these snippets with dfmt. 29 @component("snippets") 30 class SnippetsComponent : ComponentWrapper 31 { 32 mixin DefaultComponentWrapper; 33 34 static PlainSnippetProvider plainSnippets; 35 static SmartSnippetProvider smartSnippets; 36 static DependencyBasedSnippetProvider dependencySnippets; 37 static ControlFlowSnippetProvider controlFlowSnippets; 38 39 protected SnippetProvider[] providers; 40 41 protected void load() 42 { 43 if (!plainSnippets) 44 plainSnippets = new PlainSnippetProvider(); 45 if (!smartSnippets) 46 smartSnippets = new SmartSnippetProvider(); 47 if (!dependencySnippets) 48 dependencySnippets = new DependencyBasedSnippetProvider(); 49 if (!controlFlowSnippets) 50 controlFlowSnippets = new ControlFlowSnippetProvider(); 51 52 config.stringBehavior = StringBehavior.source; 53 providers.reserve(16); 54 providers ~= plainSnippets; 55 providers ~= smartSnippets; 56 providers ~= dependencySnippets; 57 providers ~= controlFlowSnippets; 58 } 59 60 /** 61 * Params: 62 * file = Filename to resolve dependencies relatively from. 63 * code = Code to complete snippet in. 64 * position = Byte offset of where to find scope in. 65 * 66 * Returns: a `SnippetInfo` object for all snippet information. 67 * 68 * `.loopScope` is set if a loop can be inserted at this position, Optionally 69 * with information about close ranges. Contains `SnippetLoopScope.init` if 70 * this is not a location where a loop can be inserted. 71 */ 72 SnippetInfo determineSnippetInfo(scope const(char)[] file, scope const(char)[] code, int position) 73 { 74 // each variable is 1 75 // maybe more expensive lookups with DCD in the future 76 enum LoopVariableAnalyzeMaxCost = 90; 77 78 scope tokens = getTokensForParser(cast(const(ubyte)[]) code, config, &workspaced.stringCache); 79 auto loc = tokens.tokenIndexAtByteIndex(position); 80 81 // first check if at end of identifier, move current location to that 82 // identifier. 83 if (loc > 0 84 && loc < tokens.length 85 && tokens[loc - 1].isLikeIdentifier 86 && tokens[loc - 1].index <= position 87 && tokens[loc - 1].index + tokens[loc - 1].textLength >= position) 88 loc--; 89 // also determine info from before start of identifier (so you can start 90 // typing something and it still finds a snippet scope) 91 // > double decrement when at end of identifier, start of other token! 92 if (loc > 0 93 && loc < tokens.length 94 && tokens[loc].isLikeIdentifier 95 && tokens[loc].index <= position 96 && tokens[loc].index + tokens[loc].textLength >= position) 97 loc--; 98 99 // nudge in next token if position is after this token 100 if (loc < tokens.length && tokens[loc].isLikeIdentifier 101 && position > tokens[loc].index + tokens[loc].textLength) 102 { 103 // cursor must not be glued to the end of identifiers 104 loc++; 105 } 106 else if (loc < tokens.length && !tokens[loc].isLikeIdentifier 107 && position >= tokens[loc].index + tokens[loc].textLength) 108 { 109 // but next token if end of non-identifiers (eg `""`, `;`, `.`, `(`) 110 loc++; 111 } 112 113 int contextIndex; 114 int checkLocation = position; 115 if (loc >= 0 && loc < tokens.length) 116 { 117 contextIndex = cast(int) tokens[loc].index; 118 if (tokens[loc].index < position) 119 checkLocation = contextIndex; 120 } 121 122 if (loc == 0 || loc == tokens.length) 123 return SnippetInfo(contextIndex, [SnippetLevel.global]); 124 125 auto leading = tokens[0 .. loc]; 126 127 if (leading.length) 128 { 129 auto last = leading[$ - 1]; 130 switch (last.type) 131 { 132 case tok!".": 133 case tok!")": 134 case tok!"characterLiteral": 135 case tok!"dstringLiteral": 136 case tok!"wstringLiteral": 137 case tok!"stringLiteral": 138 // no snippets immediately after these tokens (needs some other 139 // token inbetween) 140 return SnippetInfo(contextIndex, [SnippetLevel.other]); 141 case tok!"(": 142 // current token is something like `)`, check for previous 143 // tokens like `__traits` `(` 144 if (leading.length >= 2) 145 { 146 switch (leading[$ - 2].type) 147 { 148 case tok!"__traits": 149 case tok!"version": 150 case tok!"debug": 151 return SnippetInfo(contextIndex, [SnippetLevel.other]); 152 default: break; 153 } 154 } 155 break; 156 case tok!"__traits": 157 case tok!"version": 158 case tok!"debug": 159 return SnippetInfo(contextIndex, [SnippetLevel.other]); 160 case tok!"typeof": 161 case tok!"if": 162 case tok!"while": 163 case tok!"for": 164 case tok!"foreach": 165 case tok!"foreach_reverse": 166 case tok!"switch": 167 case tok!"with": 168 case tok!"catch": 169 // immediately after these tokens, missing opening parentheses 170 if (tokens[loc].type != tok!"(") 171 return SnippetInfo(contextIndex, [SnippetLevel.other]); 172 break; 173 default: 174 break; 175 } 176 } 177 178 auto current = tokens[loc]; 179 switch (current.type) 180 { 181 case tok!"comment": 182 size_t len = max(0, cast(ptrdiff_t)position 183 - cast(ptrdiff_t)current.index); 184 // TODO: currently never called because we would either need to 185 // use the DLexer struct as parser immediately or wait until 186 // libdparse >=0.15.0 which contains trivia, where this switch 187 // needs to be modified to check the exact trivia token instead 188 // of the associated token with it. 189 if (current.text[0 .. len].startsWith("///", "/++", "/**")) 190 return SnippetInfo(contextIndex, [SnippetLevel.docComment]); 191 else if (len >= 2) 192 return SnippetInfo(contextIndex, [SnippetLevel.comment]); 193 else 194 break; 195 case tok!"characterLiteral": 196 case tok!"dstringLiteral": 197 case tok!"wstringLiteral": 198 case tok!"stringLiteral": 199 if (position <= current.index) 200 break; 201 202 auto textSoFar = current.text[1 .. position - current.index]; 203 // no string complete if we are immediately after escape or 204 // quote character 205 // TODO: properly check if this is an unescaped escape 206 if (textSoFar.endsWith('\\', current.text[0])) 207 return SnippetInfo(contextIndex, [SnippetLevel.strings, SnippetLevel.other]); 208 else 209 return SnippetInfo(contextIndex, [SnippetLevel.strings]); 210 default: 211 break; 212 } 213 214 foreach_reverse (t; leading) 215 { 216 if (t.type == tok!";") 217 break; 218 219 // test for tokens semicolon closed statements where we should abort to avoid incomplete syntax 220 if (t.type.among!(tok!"import", tok!"module")) 221 { 222 return SnippetInfo(contextIndex, [SnippetLevel.global, SnippetLevel.other]); 223 } 224 else if (t.type.among!(tok!"=", tok!"+", tok!"-", tok!"*", tok!"/", 225 tok!"%", tok!"^^", tok!"&", tok!"|", tok!"^", tok!"<<", 226 tok!">>", tok!">>>", tok!"~", tok!"in")) 227 { 228 return SnippetInfo(contextIndex, [SnippetLevel.global, SnippetLevel.value]); 229 } 230 } 231 232 RollbackAllocator rba; 233 scope parsed = parseModule(tokens, cast(string) file, &rba); 234 235 //trace("determineSnippetInfo at ", contextIndex); 236 237 scope gen = new SnippetInfoGenerator(checkLocation); 238 gen.value.contextTokenIndex = contextIndex; 239 gen.variableStack.reserve(64); 240 gen.visit(parsed); 241 242 gen.value.loopScope.supported = gen.value.level == SnippetLevel.method; 243 if (gen.value.loopScope.supported) 244 { 245 int cost = 0; 246 foreach_reverse (v; gen.variableStack) 247 { 248 if (fillLoopScopeInfo(gen.value.loopScope, v)) 249 break; 250 if (++cost > LoopVariableAnalyzeMaxCost) 251 break; 252 } 253 } 254 255 if (gen.lastStatement) 256 { 257 import dparse.ast; 258 259 LastStatementInfo info; 260 auto nodeType = gen.lastStatement.findDeepestNonBlockNode; 261 if (gen.lastStatement.tokens.length) 262 info.location = cast(int) nodeType.tokens[0].index; 263 info.type = typeid(nodeType).name; 264 auto lastDot = info.type.lastIndexOf('.'); 265 if (lastDot != -1) 266 info.type = info.type[lastDot + 1 .. $]; 267 if (auto ifStmt = cast(IfStatement)nodeType) 268 { 269 auto elseStmt = getIfElse(ifStmt); 270 if (cast(IfStatement)elseStmt) 271 info.ifHasElse = false; 272 else 273 info.ifHasElse = elseStmt !is null; 274 } 275 else if (auto ifStmt = cast(ConditionalDeclaration)nodeType) 276 info.ifHasElse = ifStmt.hasElse; 277 // if (auto ifStmt = cast(ConditionalStatement)nodeType) 278 // info.ifHasElse = !!getIfElse(ifStmt); 279 280 gen.value.lastStatement = info; 281 } 282 283 return gen.value; 284 } 285 286 Future!SnippetList getSnippets(scope const(char)[] file, scope const(char)[] code, int position) 287 { 288 mixin(gthreadsAsyncProxy!`getSnippetsBlocking(file, code, position)`); 289 } 290 291 SnippetList getSnippetsBlocking(scope const(char)[] file, scope const(char)[] code, int position) 292 { 293 auto futures = collectSnippets(file, code, position); 294 295 auto ret = appender!(Snippet[]); 296 foreach (fut; futures[1]) 297 ret.put(fut.getBlocking()); 298 return SnippetList(futures[0], ret.data); 299 } 300 301 SnippetList getSnippetsYield(scope const(char)[] file, scope const(char)[] code, int position) 302 { 303 auto futures = collectSnippets(file, code, position); 304 305 auto ret = appender!(Snippet[]); 306 foreach (fut; futures[1]) 307 ret.put(fut.getYield()); 308 return SnippetList(futures[0], ret.data); 309 } 310 311 Future!Snippet resolveSnippet(scope const(char)[] file, scope const(char)[] code, 312 int position, Snippet snippet) 313 { 314 foreach (provider; providers) 315 { 316 if (typeid(provider).name == snippet.providerId) 317 { 318 const info = determineSnippetInfo(file, code, position); 319 return provider.resolveSnippet(instance, file, code, position, info, snippet); 320 } 321 } 322 323 return typeof(return).fromResult(snippet); 324 } 325 326 Future!string format(scope const(char)[] snippet, string[] arguments = [], 327 SnippetLevel level = SnippetLevel.global) 328 { 329 mixin(gthreadsAsyncProxy!`formatSync(snippet, arguments, level)`); 330 } 331 332 /// Will format the code passed in synchronously using dfmt. Might take a short moment on larger documents. 333 /// Returns: the formatted code as string or unchanged if dfmt is not active 334 string formatSync(scope const(char)[] snippet, string[] arguments = [], 335 SnippetLevel level = SnippetLevel.global) 336 { 337 if (!has!DfmtComponent) 338 return snippet.idup; 339 340 auto dfmt = get!DfmtComponent; 341 342 auto tmp = appender!string; 343 344 final switch (level) 345 { 346 case SnippetLevel.global: 347 case SnippetLevel.other: 348 case SnippetLevel.comment: 349 case SnippetLevel.docComment: 350 case SnippetLevel.strings: 351 case SnippetLevel.mixinTemplate: 352 case SnippetLevel.newMethod: 353 case SnippetLevel.loop: 354 case SnippetLevel.switch_: 355 break; 356 case SnippetLevel.type: 357 tmp.put("struct FORMAT_HELPER {\n"); 358 break; 359 case SnippetLevel.method: 360 tmp.put("void FORMAT_HELPER() {\n"); 361 break; 362 case SnippetLevel.value: 363 tmp.put("int FORMAT_HELPER() = "); 364 break; 365 } 366 367 scope const(char)[][string] tokens; 368 369 ptrdiff_t dollar, last; 370 while (true) 371 { 372 dollar = snippet.indexOfAny(`$\`, last); 373 if (dollar == -1) 374 { 375 tmp ~= snippet[last .. $]; 376 break; 377 } 378 379 tmp ~= snippet[last .. dollar]; 380 last = dollar + 1; 381 if (last >= snippet.length) 382 break; 383 if (snippet[dollar] == '\\') 384 { 385 tmp ~= snippet[dollar + 1]; 386 last = dollar + 2; 387 } 388 else 389 { 390 string key = "__WspD_Snp_" ~ dollar.to!string ~ "_"; 391 const(char)[] str; 392 393 bool startOfBlock = snippet[0 .. dollar].stripRight.endsWith("{"); 394 bool endOfBlock; 395 396 bool makeWrappingIfMayBeDelegate() 397 { 398 endOfBlock = snippet[last .. $].stripLeft.startsWith("}"); 399 if (startOfBlock && endOfBlock) 400 { 401 // make extra long to make dfmt definitely wrap this (in case this is a delegate, otherwise this doesn't hurt either) 402 key.reserve(key.length + 200); 403 foreach (i; 0 .. 200) 404 key ~= "_"; 405 return true; 406 } 407 else 408 return false; 409 } 410 411 if (snippet[dollar + 1] == '{') 412 { 413 ptrdiff_t i = dollar + 2; 414 int depth = 1; 415 while (true) 416 { 417 auto next = snippet.indexOfAny(`\{}`, i); 418 if (next == -1) 419 { 420 i = snippet.length; 421 break; 422 } 423 424 if (snippet[next] == '\\') 425 i = next + 2; 426 else 427 { 428 if (snippet[next] == '{') 429 depth++; 430 else if (snippet[next] == '}') 431 depth--; 432 else 433 assert(false); 434 435 i = next + 1; 436 } 437 438 if (depth == 0) 439 break; 440 } 441 str = snippet[dollar .. i]; 442 last = i; 443 444 const wrapped = makeWrappingIfMayBeDelegate(); 445 446 const placeholderMightBeIdentifier = str.length > 5 447 || snippet[last .. $].stripLeft.startsWith(";", ".", "{", "(", "["); 448 449 if (wrapped || placeholderMightBeIdentifier) 450 { 451 // let's insert some token in here instead of a comment because there is probably some default content 452 // if there is a semicolon at the end we probably need to insert a semicolon here too 453 // if this is a comment placeholder let's insert a semicolon to make dfmt wrap 454 if (str[0 .. $ - 1].endsWith(';') || str[0 .. $ - 1].canFind("//")) 455 key ~= ';'; 456 } 457 else if (level != SnippetLevel.value) 458 { 459 // empty default, put in comment 460 key = "/+++" ~ key ~ "+++/"; 461 } 462 } 463 else 464 { 465 size_t end = dollar + 1; 466 467 if (snippet[dollar + 1].isDigit) 468 { 469 while (end < snippet.length && snippet[end].isDigit) 470 end++; 471 } 472 else 473 { 474 while (end < snippet.length && (snippet[end].isAlphaNum || snippet[end] == '_')) 475 end++; 476 } 477 478 str = snippet[dollar .. end]; 479 last = end; 480 481 makeWrappingIfMayBeDelegate(); 482 483 const placeholderMightBeIdentifier = snippet[last .. $].stripLeft.startsWith(";", 484 ".", "{", "(", "["); 485 486 if (placeholderMightBeIdentifier) 487 { 488 // keep value thing as simple identifier as we don't have any placeholder text 489 } 490 else if (level != SnippetLevel.value) 491 { 492 // primitive placeholder as comment 493 key = "/+++" ~ key ~ "+++/"; 494 } 495 } 496 497 tokens[key] = str; 498 tmp ~= key; 499 } 500 } 501 502 final switch (level) 503 { 504 case SnippetLevel.global: 505 case SnippetLevel.other: 506 case SnippetLevel.comment: 507 case SnippetLevel.docComment: 508 case SnippetLevel.strings: 509 case SnippetLevel.mixinTemplate: 510 case SnippetLevel.newMethod: 511 case SnippetLevel.loop: 512 case SnippetLevel.switch_: 513 break; 514 case SnippetLevel.type: 515 case SnippetLevel.method: 516 tmp.put("}"); 517 break; 518 case SnippetLevel.value: 519 tmp.put(";"); 520 break; 521 } 522 523 auto res = dfmt.formatSync(tmp.data, arguments); 524 525 string chompStr; 526 char del; 527 final switch (level) 528 { 529 case SnippetLevel.global: 530 case SnippetLevel.other: 531 case SnippetLevel.comment: 532 case SnippetLevel.docComment: 533 case SnippetLevel.strings: 534 case SnippetLevel.mixinTemplate: 535 case SnippetLevel.newMethod: 536 case SnippetLevel.loop: 537 case SnippetLevel.switch_: 538 break; 539 case SnippetLevel.type: 540 case SnippetLevel.method: 541 chompStr = "}"; 542 del = '{'; 543 break; 544 case SnippetLevel.value: 545 chompStr = ";"; 546 del = '='; 547 break; 548 } 549 550 if (chompStr.length) 551 res = res.stripRight.chomp(chompStr); 552 553 if (del != char.init) 554 { 555 auto start = res.indexOf(del); 556 if (start != -1) 557 { 558 res = res[start + 1 .. $]; 559 560 while (true) 561 { 562 // delete empty lines before first line 563 auto nl = res.indexOf('\n'); 564 if (nl != -1 && res[0 .. nl].all!isWhite) 565 res = res[nl + 1 .. $]; 566 else 567 break; 568 } 569 570 auto indent = res[0 .. res.length - res.stripLeft.length]; 571 if (indent.length) 572 { 573 // remove indentation of whole block 574 assert(indent.all!isWhite); 575 res = res.splitLines.map!(a => a.startsWith(indent) 576 ? a[indent.length .. $] : a.stripRight).join("\n"); 577 } 578 } 579 } 580 581 foreach (key, value; tokens) 582 { 583 // TODO: replacing using aho-corasick would be far more efficient but there is nothing like that in phobos 584 res = res.replace(key, value); 585 } 586 587 if (res.endsWith("\r\n") && !snippet.endsWith('\n')) 588 res.length -= 2; 589 else if (res.endsWith('\n') && !snippet.endsWith('\n')) 590 res.length--; 591 592 if (res.endsWith(";\n\n$0")) 593 res = res[0 .. $ - "\n$0".length] ~ "$0"; 594 else if (res.endsWith(";\r\n\r\n$0")) 595 res = res[0 .. $ - "\r\n$0".length] ~ "$0"; 596 597 return res; 598 } 599 600 /// Adds snippets which complete conditionally based on dub dependencies being present. 601 /// This function affects the global configuration of all instances. 602 /// Params: 603 /// requiredDependencies = The dependencies which must be present in order for this snippet to show up. 604 /// snippet = The snippet to suggest when the required dependencies are matched. 605 void addDependencySnippet(scope const string[] requiredDependencies, const PlainSnippet snippet) 606 { 607 // maybe application global change isn't such a good idea? Current config system seems too inefficient for this. 608 dependencySnippets.addSnippet(requiredDependencies, snippet); 609 } 610 611 /// Registers snippets for a variety of popular DUB dependencies. 612 void registerBuiltinDependencySnippets() 613 { 614 import workspaced.com.snippets.external_builtin; 615 616 foreach (group; builtinDependencySnippets) 617 foreach (snippet; group.snippets) 618 addDependencySnippet(group.requiredDependencies, snippet); 619 } 620 621 private: 622 Tuple!(SnippetInfo, Future!(Snippet[])[]) collectSnippets(scope const(char)[] file, 623 scope const(char)[] code, int position) 624 { 625 const inst = instance; 626 auto info = determineSnippetInfo(file, code, position); 627 auto futures = appender!(Future!(Snippet[])[]); 628 foreach (provider; providers) 629 futures.put(provider.provideSnippets(inst, file, code, position, info)); 630 return tuple(info, futures.data); 631 } 632 633 LexerConfig config; 634 } 635 636 /// 637 enum SnippetLevel 638 { 639 /// Outside of functions or types, possibly inside templates 640 global, 641 /// Inside interfaces, classes, structs or unions 642 type, 643 /// Inside method body 644 method, 645 /// inside a variable value, argument call, default value or similar 646 value, 647 /// Other scope types (for example outside of braces but after a function definition or some other invalid syntax place) 648 other, 649 /// Inside a string literal. 650 strings, 651 /// Inside a normal comment 652 comment, 653 /// Inside a documentation comment 654 docComment, 655 /// Inside explicitly declared mixin templates 656 mixinTemplate, 657 658 /// Inserted at the start of any method, meaning the scope has cleared or at least is logically separated. 659 newMethod, 660 /// a breakable loop (while, for, foreach, etc.) 661 /// This type is usually not the trailing type and will repeat method afterwards. 662 loop, 663 /// a `switch` statement 664 /// This type is usually not the trailing type and will repeat method afterwards. 665 switch_, 666 } 667 668 /// 669 struct SnippetLoopScope 670 { 671 /// true if an loop expression can be inserted at this point 672 bool supported; 673 /// true if we know we are iterating over a string (possibly needing unicode decoding) or false otherwise 674 bool stringIterator; 675 /// Explicit type to use when iterating or null if none is known 676 string type; 677 /// Best variable to iterate over or null if none was found 678 string iterator; 679 /// Number of keys to iterate over 680 int numItems = 1; 681 } 682 683 /// 684 struct SnippetInfo 685 { 686 /// Index in code which token was used to determine this snippet info. 687 int contextTokenIndex; 688 /// Levels this snippet location has gone through, latest one being the last 689 SnippetLevel[] stack = [SnippetLevel.global]; 690 /// Information about snippets using loop context 691 SnippetLoopScope loopScope; 692 /// Information about the last parsable statement before the cursor. May be 693 /// `LastStatementInfo.init` at start of function or block. 694 LastStatementInfo lastStatement; 695 696 /// Current snippet scope level of the location 697 SnippetLevel level() const @property 698 { 699 return stack.length ? stack[$ - 1] : SnippetLevel.other; 700 } 701 702 /// Checks in reverse if the given snippet level is in the stack, up until 703 /// the last newMethod level. 704 SnippetLevel findInLocalScope(SnippetLevel[] levels...) const 705 { 706 foreach_reverse (s; stack) 707 { 708 if (levels.canFind(s)) 709 return s; 710 if (s == SnippetLevel.newMethod) 711 break; 712 } 713 return SnippetLevel.init; 714 } 715 } 716 717 struct LastStatementInfo 718 { 719 /// The libdparse class name (typeid) of the last parsable statement before 720 /// the cursor, stripped of module name. 721 string type; 722 /// If type is set, this is the start location in bytes where 723 /// the first token was. 724 int location; 725 /// True if the type is (`IfStatement`, `ConditionalDeclaration` or 726 /// `ConditionalStatement`) and has a final `else` block defined. 727 bool ifHasElse; 728 } 729 730 /// A list of snippets resolved at a given position. 731 struct SnippetList 732 { 733 /// The info where this snippet is completing at. 734 SnippetInfo info; 735 /// The list of snippets that got returned. 736 Snippet[] snippets; 737 } 738 739 /// 740 interface SnippetProvider 741 { 742 Future!(Snippet[]) provideSnippets(scope const WorkspaceD.Instance instance, 743 scope const(char)[] file, scope const(char)[] code, int position, const SnippetInfo info); 744 745 Future!Snippet resolveSnippet(scope const WorkspaceD.Instance instance, 746 scope const(char)[] file, scope const(char)[] code, int position, 747 const SnippetInfo info, Snippet snippet); 748 } 749 750 /// Snippet to insert 751 struct Snippet 752 { 753 /// Internal ID for resolving this snippet 754 string id, providerId; 755 /// User-defined data for helping resolving this snippet 756 JsonValue data; 757 /// Label for this snippet 758 string title; 759 /// Shortcut to type for this snippet 760 string shortcut; 761 /// Markdown documentation for this snippet 762 string documentation; 763 /// Plain text to insert assuming global level indentation. 764 string plain; 765 /// Text with interactive snippet locations to insert assuming global indentation. 766 string snippet; 767 /// true if this snippet can be used as-is 768 bool resolved; 769 /// true if this snippet shouldn't be formatted. 770 bool unformatted; 771 /// List of imports that should be imported when using this snippet. 772 string[] imports; 773 }