1 module served.commands.complete; 2 3 import served.commands.format : formatCode, formatSnippet; 4 import served.extension; 5 import served.types; 6 import served.utils.ddoc; 7 import served.utils.fibermanager; 8 9 import workspaced.api; 10 import workspaced.com.dfmt : DfmtComponent; 11 import workspaced.com.dcd; 12 import workspaced.com.snippets; 13 import workspaced.coms; 14 15 import std.algorithm : among, any, canFind, chunkBy, endsWith, filter, map, min, 16 reverse, sort, startsWith, uniq; 17 import std.array : appender, array; 18 import std.conv : text, to; 19 import std.experimental.logger; 20 import std.json : JSONType, JSONValue; 21 import std..string : indexOf, join, lastIndexOf, lineSplitter, strip, 22 stripLeft, stripRight, toLower; 23 import std.utf : decodeFront; 24 25 import dparse.lexer : Token; 26 27 import painlessjson : fromJSON, toJSON; 28 29 import fs = std.file; 30 import io = std.stdio; 31 32 static immutable sortPrefixDoc = "0_"; 33 static immutable sortPrefixSnippets = "2_5_"; 34 // dcd additionally sorts inside with sortFromDCDType return value (appends to this) 35 static immutable sortPrefixDCD = "2_"; 36 37 CompletionItemKind convertFromDCDType(string type) 38 { 39 if (type.length != 1) 40 return CompletionItemKind.text; 41 42 switch (type[0]) 43 { 44 case 'c': // class name 45 return CompletionItemKind.class_; 46 case 'i': // interface name 47 return CompletionItemKind.interface_; 48 case 's': // struct name 49 case 'u': // union name 50 return CompletionItemKind.struct_; 51 case 'a': // array 52 case 'A': // associative array 53 case 'v': // variable name 54 return CompletionItemKind.variable; 55 case 'm': // member variable 56 return CompletionItemKind.field; 57 case 'e': // enum member 58 return CompletionItemKind.enumMember; 59 case 'k': // keyword 60 return CompletionItemKind.keyword; 61 case 'f': // function 62 return CompletionItemKind.function_; 63 case 'g': // enum name 64 return CompletionItemKind.enum_; 65 case 'P': // package name 66 case 'M': // module name 67 return CompletionItemKind.module_; 68 case 'l': // alias name 69 return CompletionItemKind.reference; 70 case 't': // template name 71 case 'T': // mixin template name 72 return CompletionItemKind.property; 73 case 'h': // template type parameter 74 case 'p': // template variadic parameter 75 return CompletionItemKind.typeParameter; 76 default: 77 return CompletionItemKind.text; 78 } 79 } 80 81 string sortFromDCDType(string type) 82 { 83 if (type.length != 1) 84 return "9_"; 85 86 switch (type[0]) 87 { 88 case 'v': // variable name 89 return "2_"; 90 case 'm': // member variable 91 return "3_"; 92 case 'f': // function 93 return "4_"; 94 case 'k': // keyword 95 case 'e': // enum member 96 return "5_"; 97 case 'c': // class name 98 case 'i': // interface name 99 case 's': // struct name 100 case 'u': // union name 101 case 'a': // array 102 case 'A': // associative array 103 case 'g': // enum name 104 case 'P': // package name 105 case 'M': // module name 106 case 'l': // alias name 107 case 't': // template name 108 case 'T': // mixin template name 109 case 'h': // template type parameter 110 case 'p': // template variadic parameter 111 return "6_"; 112 default: 113 return "9_"; 114 } 115 } 116 117 SymbolKind convertFromDCDSearchType(string type) 118 { 119 if (type.length != 1) 120 return cast(SymbolKind) 0; 121 switch (type[0]) 122 { 123 case 'c': 124 return SymbolKind.class_; 125 case 'i': 126 return SymbolKind.interface_; 127 case 's': 128 case 'u': 129 return SymbolKind.package_; 130 case 'a': 131 case 'A': 132 case 'v': 133 return SymbolKind.variable; 134 case 'm': 135 case 'e': 136 return SymbolKind.field; 137 case 'f': 138 case 'l': 139 return SymbolKind.function_; 140 case 'g': 141 return SymbolKind.enum_; 142 case 'P': 143 case 'M': 144 return SymbolKind.namespace; 145 case 't': 146 case 'T': 147 return SymbolKind.property; 148 case 'k': 149 default: 150 return cast(SymbolKind) 0; 151 } 152 } 153 154 SymbolKind convertFromDscannerType(string type, string name = null) 155 { 156 if (type.length != 1) 157 return cast(SymbolKind) 0; 158 switch (type[0]) 159 { 160 case 'c': 161 return SymbolKind.class_; 162 case 's': 163 return SymbolKind.struct_; 164 case 'i': 165 return SymbolKind.interface_; 166 case 'T': 167 return SymbolKind.property; 168 case 'f': 169 case 'U': 170 case 'Q': 171 case 'W': 172 case 'P': 173 if (name == "this") 174 return SymbolKind.constructor; 175 else 176 return SymbolKind.function_; 177 case 'C': 178 case 'S': 179 return SymbolKind.constructor; 180 case 'g': 181 return SymbolKind.enum_; 182 case 'u': 183 return SymbolKind.struct_; 184 case 'D': 185 case 'V': 186 case 'e': 187 return SymbolKind.constant; 188 case 'v': 189 return SymbolKind.variable; 190 case 'a': 191 return SymbolKind.field; 192 default: 193 return cast(SymbolKind) 0; 194 } 195 } 196 197 SymbolKindEx convertExtendedFromDscannerType(string type) 198 { 199 if (type.length != 1) 200 return cast(SymbolKindEx) 0; 201 switch (type[0]) 202 { 203 case 'U': 204 return SymbolKindEx.test; 205 case 'D': 206 return SymbolKindEx.debugSpec; 207 case 'V': 208 return SymbolKindEx.versionSpec; 209 case 'C': 210 return SymbolKindEx.staticCtor; 211 case 'S': 212 return SymbolKindEx.sharedStaticCtor; 213 case 'Q': 214 return SymbolKindEx.staticDtor; 215 case 'W': 216 return SymbolKindEx.sharedStaticDtor; 217 case 'P': 218 return SymbolKindEx.postblit; 219 default: 220 return cast(SymbolKindEx) 0; 221 } 222 } 223 224 C[] substr(C, T)(C[] s, T start, T end) 225 { 226 if (!s.length) 227 return s; 228 if (start < 0) 229 start = 0; 230 if (start >= s.length) 231 start = s.length - 1; 232 if (end > s.length) 233 end = s.length; 234 if (end < start) 235 return s[start .. start]; 236 return s[start .. end]; 237 } 238 239 /// Extracts all function parameters for a given declaration string. 240 /// Params: 241 /// sig = the function signature such as `string[] example(string sig, bool exact = false)` 242 /// exact = set to true to make the returned values include the closing paren at the end (if exists) 243 const(char)[][] extractFunctionParameters(scope const(char)[] sig, bool exact = false) 244 { 245 if (!sig.length) 246 return []; 247 auto params = appender!(const(char)[][]); 248 ptrdiff_t i = sig.length - 1; 249 250 if (sig[i] == ')' && !exact) 251 i--; 252 253 ptrdiff_t paramEnd = i + 1; 254 255 void skipStr() 256 { 257 i--; 258 if (sig[i + 1] == '\'') 259 for (; i >= 0; i--) 260 if (sig[i] == '\'') 261 return; 262 bool escapeNext = false; 263 while (i >= 0) 264 { 265 if (sig[i] == '\\') 266 escapeNext = false; 267 if (escapeNext) 268 break; 269 if (sig[i] == '"') 270 escapeNext = true; 271 i--; 272 } 273 } 274 275 void skip(char open, char close) 276 { 277 i--; 278 int depth = 1; 279 while (i >= 0 && depth > 0) 280 { 281 if (sig[i] == '"' || sig[i] == '\'') 282 skipStr(); 283 else 284 { 285 if (sig[i] == close) 286 depth++; 287 else if (sig[i] == open) 288 depth--; 289 i--; 290 } 291 } 292 } 293 294 while (i >= 0) 295 { 296 switch (sig[i]) 297 { 298 case ',': 299 params.put(sig.substr(i + 1, paramEnd).strip); 300 paramEnd = i; 301 i--; 302 break; 303 case ';': 304 case '(': 305 auto param = sig.substr(i + 1, paramEnd).strip; 306 if (param.length) 307 params.put(param); 308 auto ret = params.data; 309 reverse(ret); 310 return ret; 311 case ')': 312 skip('(', ')'); 313 break; 314 case '}': 315 skip('{', '}'); 316 break; 317 case ']': 318 skip('[', ']'); 319 break; 320 case '"': 321 case '\'': 322 skipStr(); 323 break; 324 default: 325 i--; 326 break; 327 } 328 } 329 auto ret = params.data; 330 reverse(ret); 331 return ret; 332 } 333 334 unittest 335 { 336 void assertEqual(A, B)(A a, B b) 337 { 338 import std.conv : to; 339 340 assert(a == b, a.to!string ~ " is not equal to " ~ b.to!string); 341 } 342 343 assertEqual(extractFunctionParameters("void foo()"), cast(string[])[]); 344 assertEqual(extractFunctionParameters(`auto bar(int foo, Button, my.Callback cb)`), 345 ["int foo", "Button", "my.Callback cb"]); 346 assertEqual(extractFunctionParameters(`SomeType!(int, "int_") foo(T, Args...)(T a, T b, string[string] map, Other!"(" stuff1, SomeType!(double, ")double") myType, Other!"(" stuff, Other!")")`), 347 [ 348 "T a", "T b", "string[string] map", `Other!"(" stuff1`, 349 `SomeType!(double, ")double") myType`, `Other!"(" stuff`, `Other!")"` 350 ]); 351 assertEqual(extractFunctionParameters(`SomeType!(int,"int_")foo(T,Args...)(T a,T b,string[string] map,Other!"(" stuff1,SomeType!(double,")double")myType,Other!"(" stuff,Other!")")`), 352 [ 353 "T a", "T b", "string[string] map", `Other!"(" stuff1`, 354 `SomeType!(double,")double")myType`, `Other!"(" stuff`, `Other!")"` 355 ]); 356 assertEqual(extractFunctionParameters(`some_garbage(code); before(this); funcCall(4`, 357 true), [`4`]); 358 assertEqual(extractFunctionParameters(`some_garbage(code); before(this); funcCall(4, f(4)`, 359 true), [`4`, `f(4)`]); 360 assertEqual(extractFunctionParameters(`some_garbage(code); before(this); funcCall(4, ["a"], JSONValue(["b": JSONValue("c")]), recursive(func, call!s()), "texts )\"(too"`, 361 true), [ 362 `4`, `["a"]`, `JSONValue(["b": JSONValue("c")])`, 363 `recursive(func, call!s())`, `"texts )\"(too"` 364 ]); 365 } 366 367 /// Provide snippets in auto-completion 368 __gshared bool doCompleteSnippets = false; 369 370 // === Protocol Methods starting here === 371 372 @protocolMethod("textDocument/completion") 373 CompletionList provideComplete(TextDocumentPositionParams params) 374 { 375 Document document = documents[params.textDocument.uri]; 376 auto instance = activeInstance = backend.getBestInstance(document.uri.uriToFile); 377 trace("Completing from instance ", instance ? instance.cwd : "null"); 378 379 if (document.uri.toLower.endsWith("dscanner.ini")) 380 { 381 trace("Providing dscanner.ini completion"); 382 auto possibleFields = backend.get!DscannerComponent.listAllIniFields; 383 scope line = document.lineAtScope(params.position).strip; 384 auto defaultList = CompletionList(false, possibleFields.map!(a => CompletionItem(a.name, 385 CompletionItemKind.field.opt, Optional!string.init, 386 MarkupContent(a.documentation).opt, Optional!bool.init, Optional!bool.init, 387 Optional!string.init, Optional!string.init, (a.name ~ '=').opt)).array); 388 if (!line.length) 389 return defaultList; 390 if (line[0] == '[') 391 return CompletionList(false, [ 392 CompletionItem("analysis.config.StaticAnalysisConfig", 393 CompletionItemKind.keyword.opt), 394 CompletionItem("analysis.config.ModuleFilters", CompletionItemKind.keyword.opt, Optional!string.init, 395 MarkupContent("In this optional section a comma-separated list of inclusion and exclusion" 396 ~ " selectors can be specified for every check on which selective filtering" 397 ~ " should be applied. These given selectors match on the module name and" 398 ~ " partial matches (std. or .foo.) are possible. Moreover, every selectors" 399 ~ " must begin with either + (inclusion) or - (exclusion). Exclusion selectors" 400 ~ " take precedence over all inclusion operators.").opt) 401 ]); 402 auto eqIndex = line.indexOf('='); 403 auto quotIndex = line.lastIndexOf('"'); 404 if (quotIndex != -1 && params.position.character >= quotIndex) 405 return CompletionList.init; 406 if (params.position.character < eqIndex) 407 return defaultList; 408 else 409 return CompletionList(false, [ 410 CompletionItem(`"disabled"`, CompletionItemKind.value.opt, 411 "Check is disabled".opt), 412 CompletionItem(`"enabled"`, CompletionItemKind.value.opt, 413 "Check is enabled".opt), 414 CompletionItem(`"skip-unittest"`, CompletionItemKind.value.opt, 415 "Check is enabled but not operated in the unittests".opt) 416 ]); 417 } 418 else 419 { 420 if (!instance) 421 { 422 trace("Providing no completion because no instance"); 423 return CompletionList.init; 424 } 425 426 if (document.getLanguageId == "d") 427 return provideDSourceComplete(params, instance, document); 428 else if (document.getLanguageId == "diet") 429 return provideDietSourceComplete(params, instance, document); 430 else if (document.getLanguageId == "dml") 431 return provideDMLSourceComplete(params, instance, document); 432 else 433 { 434 tracef("Providing no completion for unknown language ID %s.", document.getLanguageId); 435 return CompletionList.init; 436 } 437 } 438 } 439 440 CompletionList provideDMLSourceComplete(TextDocumentPositionParams params, 441 WorkspaceD.Instance instance, ref Document document) 442 { 443 import workspaced.com.dlangui : DlanguiComponent, CompletionType; 444 445 CompletionList ret; 446 447 auto items = backend.get!DlanguiComponent.complete(document.rawText, 448 cast(int) document.positionToBytes(params.position)).getYield(); 449 ret.items.length = items.length; 450 foreach (i, item; items) 451 { 452 CompletionItem translated; 453 454 translated.sortText = ((item.type == CompletionType.Class ? "1." : "0.") ~ item.value).opt; 455 translated.label = item.value; 456 if (item.documentation.length) 457 translated.documentation = MarkupContent(item.documentation).opt; 458 if (item.enumName.length) 459 translated.detail = item.enumName.opt; 460 461 switch (item.type) 462 { 463 case CompletionType.Class: 464 translated.insertTextFormat = InsertTextFormat.snippet; 465 translated.insertText = item.value ~ ` {$0}`; 466 break; 467 case CompletionType.Color: 468 translated.insertTextFormat = InsertTextFormat.snippet; 469 translated.insertText = item.value ~ `: ${0:#000000}`; 470 break; 471 case CompletionType.String: 472 translated.insertTextFormat = InsertTextFormat.snippet; 473 translated.insertText = item.value ~ `: "$0"`; 474 break; 475 case CompletionType.EnumDefinition: 476 translated.insertTextFormat = InsertTextFormat.plainText; 477 translated.insertText = item.enumName ~ "." ~ item.value; 478 break; 479 case CompletionType.Rectangle: 480 case CompletionType.Number: 481 translated.insertTextFormat = InsertTextFormat.snippet; 482 translated.insertText = item.value ~ `: ${0:0}`; 483 break; 484 case CompletionType.Keyword: 485 // don't set, inherit from label 486 break; 487 default: 488 translated.insertTextFormat = InsertTextFormat.plainText; 489 translated.insertText = item.value ~ ": "; 490 break; 491 } 492 493 switch (item.type) 494 { 495 case CompletionType.Class: 496 translated.kind = CompletionItemKind.class_; 497 break; 498 case CompletionType.String: 499 translated.kind = CompletionItemKind.value; 500 break; 501 case CompletionType.Number: 502 translated.kind = CompletionItemKind.value; 503 break; 504 case CompletionType.Color: 505 translated.kind = CompletionItemKind.color; 506 break; 507 case CompletionType.EnumDefinition: 508 translated.kind = CompletionItemKind.enum_; 509 break; 510 case CompletionType.EnumValue: 511 translated.kind = CompletionItemKind.enumMember; 512 break; 513 case CompletionType.Rectangle: 514 translated.kind = CompletionItemKind.typeParameter; 515 break; 516 case CompletionType.Boolean: 517 translated.kind = CompletionItemKind.constant; 518 break; 519 case CompletionType.Keyword: 520 translated.kind = CompletionItemKind.keyword; 521 break; 522 default: 523 case CompletionType.Undefined: 524 break; 525 } 526 527 ret.items[i] = translated; 528 } 529 530 return ret; 531 } 532 533 CompletionList provideDietSourceComplete(TextDocumentPositionParams params, 534 WorkspaceD.Instance instance, ref Document document) 535 { 536 import served.utils.diet; 537 import dc = dietc.complete; 538 539 auto completion = updateDietFile(document.uri.uriToFile, document.rawText.idup); 540 541 size_t offset = document.positionToBytes(params.position); 542 auto raw = completion.completeAt(offset); 543 CompletionItem[] ret; 544 545 if (raw is dc.Completion.completeD) 546 { 547 auto d = workspace(params.textDocument.uri).config.d; 548 string code; 549 contextExtractD(completion, offset, code, offset, d.dietContextCompletion); 550 if (offset <= code.length && instance.has!DCDComponent) 551 { 552 info("DCD Completing Diet for ", code, " at ", offset); 553 auto dcd = instance.get!DCDComponent.listCompletion(code, cast(int) offset).getYield; 554 if (dcd.type == DCDCompletions.Type.identifiers) 555 { 556 ret = dcd.identifiers.convertDCDIdentifiers(d.argumentSnippets, d.completeNoDupes); 557 } 558 } 559 } 560 else 561 ret = raw.map!((a) { 562 CompletionItem ret; 563 ret.label = a.text; 564 ret.kind = a.type.mapToCompletionItemKind.opt; 565 if (a.definition.length) 566 ret.detail = a.definition.opt; 567 if (a.documentation.length) 568 ret.documentation = MarkupContent(a.documentation).opt; 569 if (a.preselected) 570 ret.preselect = true.opt; 571 return ret; 572 }).array; 573 574 return CompletionList(false, ret); 575 } 576 577 CompletionList provideDSourceComplete(TextDocumentPositionParams params, 578 WorkspaceD.Instance instance, ref Document document) 579 { 580 auto lineRange = document.lineByteRangeAt(params.position.line); 581 auto byteOff = cast(int) document.positionToBytes(params.position); 582 583 string line = document.rawText[lineRange[0] .. lineRange[1]].idup; 584 string prefix = line[0 .. min($, params.position.character)].strip; 585 CompletionItem[] completion; 586 Token commentToken; 587 if (document.rawText.isInComment(byteOff, backend, &commentToken)) 588 if (commentToken.text.startsWith("///", "/**", "/++")) 589 { 590 trace("Providing comment completion"); 591 int prefixLen = prefix[0] == '/' ? 3 : 1; 592 auto remaining = prefix[prefixLen .. $].stripLeft; 593 594 foreach (compl; import("ddocs.txt").lineSplitter) 595 { 596 if (compl.startsWith(remaining)) 597 { 598 auto item = CompletionItem(compl, CompletionItemKind.snippet.opt); 599 item.insertText = compl ~ ": "; 600 completion ~= item; 601 } 602 } 603 604 // make the comment one "line" so provide doc complete shows complete 605 // after a /** */ comment block if you are on the first line. 606 lineRange[1] = commentToken.index + commentToken.text.length; 607 provideDocComplete(params, instance, document, completion, line, lineRange); 608 609 return CompletionList(false, completion); 610 } 611 612 bool completeDCD = instance.has!DCDComponent; 613 bool completeDoc = instance.has!DscannerComponent; 614 bool completeSnippets = doCompleteSnippets && instance.has!SnippetsComponent; 615 616 tracef("Performing regular D comment completion (DCD=%s, Documentation=%s, Snippets=%s)", 617 completeDCD, completeDoc, completeSnippets); 618 const config = workspace(params.textDocument.uri).config; 619 DCDCompletions result = DCDCompletions.empty; 620 joinAll({ 621 if (completeDCD) 622 result = instance.get!DCDComponent.listCompletion(document.rawText, byteOff).getYield; 623 }, { 624 if (completeDoc) 625 provideDocComplete(params, instance, document, completion, line, lineRange); 626 }, { 627 if (completeSnippets) 628 provideSnippetComplete(params, instance, document, config, completion, byteOff); 629 }); 630 631 if (completeDCD && result != DCDCompletions.init) 632 { 633 if (result.type == DCDCompletions.Type.identifiers) 634 { 635 auto d = config.d; 636 completion ~= convertDCDIdentifiers(result.identifiers, d.argumentSnippets, d.completeNoDupes); 637 } 638 else if (result.type != DCDCompletions.Type.calltips) 639 { 640 trace("Unexpected result from DCD: ", result); 641 } 642 } 643 return CompletionList(false, completion); 644 } 645 646 private void provideDocComplete(TextDocumentPositionParams params, WorkspaceD.Instance instance, 647 ref Document document, ref CompletionItem[] completion, string line, size_t[2] lineRange) 648 { 649 string lineStripped = line.strip; 650 if (lineStripped.among!("", "/", "/*", "/+", "//", "///", "/**", "/++")) 651 { 652 auto defs = instance.get!DscannerComponent.listDefinitions(uriToFile( 653 params.textDocument.uri), document.rawText[lineRange[1] .. $]).getYield; 654 ptrdiff_t di = -1; 655 FuncFinder: foreach (i, def; defs) 656 { 657 if (def.line >= 0 && def.line <= 5) 658 { 659 di = i; 660 break FuncFinder; 661 } 662 } 663 if (di == -1) 664 return; 665 auto def = defs[di]; 666 auto sig = "signature" in def.attributes; 667 if (!sig) 668 { 669 CompletionItem doc = CompletionItem("///"); 670 doc.kind = CompletionItemKind.snippet; 671 doc.insertTextFormat = InsertTextFormat.snippet; 672 auto eol = document.eolAt(params.position.line).toString; 673 doc.insertText = "/// "; 674 CompletionItem doc2 = doc; 675 CompletionItem doc3 = doc; 676 doc2.label = "/**"; 677 doc2.insertText = "/** " ~ eol ~ " * $0" ~ eol ~ " */"; 678 doc3.label = "/++"; 679 doc3.insertText = "/++ " ~ eol ~ " * $0" ~ eol ~ " +/"; 680 681 completion.addDocComplete(doc, lineStripped); 682 completion.addDocComplete(doc2, lineStripped); 683 completion.addDocComplete(doc3, lineStripped); 684 return; 685 } 686 auto funcArgs = extractFunctionParameters(*sig); 687 string[] docs = ["$0"]; 688 int argNo = 1; 689 foreach (arg; funcArgs) 690 { 691 auto space = arg.stripRight.lastIndexOf(' '); 692 if (space == -1) 693 continue; 694 auto identifier = arg[space + 1 .. $]; 695 if (!identifier.isValidDIdentifier) 696 continue; 697 if (argNo == 1) 698 docs ~= "Params:"; 699 docs ~= text(" ", identifier, " = $", argNo.to!string); 700 argNo++; 701 } 702 auto retAttr = "return" in def.attributes; 703 if (retAttr && *retAttr != "void") 704 { 705 docs ~= "Returns: $" ~ argNo.to!string; 706 argNo++; 707 } 708 auto depr = "deprecation" in def.attributes; 709 if (depr) 710 { 711 docs ~= "Deprecated: $" ~ argNo.to!string ~ *depr; 712 argNo++; 713 } 714 CompletionItem doc = CompletionItem("///"); 715 doc.kind = CompletionItemKind.snippet; 716 doc.insertTextFormat = InsertTextFormat.snippet; 717 auto eol = document.eolAt(params.position.line).toString; 718 doc.insertText = docs.map!(a => "/// " ~ a).join(eol); 719 CompletionItem doc2 = doc; 720 CompletionItem doc3 = doc; 721 doc2.label = "/**"; 722 doc2.insertText = "/** " ~ eol ~ docs.map!(a => " * " ~ a ~ eol).join() ~ " */"; 723 doc3.label = "/++"; 724 doc3.insertText = "/++ " ~ eol ~ docs.map!(a => " + " ~ a ~ eol).join() ~ " +/"; 725 726 doc.sortText = opt(sortPrefixDoc ~ "0"); 727 doc2.sortText = opt(sortPrefixDoc ~ "1"); 728 doc3.sortText = opt(sortPrefixDoc ~ "2"); 729 730 completion.addDocComplete(doc, lineStripped); 731 completion.addDocComplete(doc2, lineStripped); 732 completion.addDocComplete(doc3, lineStripped); 733 } 734 } 735 736 private void provideSnippetComplete(TextDocumentPositionParams params, WorkspaceD.Instance instance, 737 ref Document document, ref const UserConfiguration config, 738 ref CompletionItem[] completion, int byteOff) 739 { 740 if (byteOff > 0 && document.rawText[byteOff - 1 .. $].startsWith(".")) 741 return; // no snippets after '.' character 742 743 auto snippets = instance.get!SnippetsComponent; 744 auto ret = snippets.getSnippetsYield(document.uri.uriToFile, document.rawText, byteOff); 745 trace("got ", ret.snippets.length, " snippets fitting in this context: ", 746 ret.snippets.map!"a.shortcut"); 747 auto eol = document.eolAt(0); 748 foreach (Snippet snippet; ret.snippets) 749 { 750 auto item = snippet.snippetToCompletionItem; 751 item.data["level"] = JSONValue(ret.info.level.to!string); 752 if (!snippet.unformatted) 753 item.data["format"] = toJSON(generateDfmtArgs(config, eol)); 754 item.data["params"] = toJSON(params); 755 completion ~= item; 756 } 757 } 758 759 private void addDocComplete(ref CompletionItem[] completion, CompletionItem doc, string prefix) 760 { 761 if (!doc.label.startsWith(prefix)) 762 return; 763 if (prefix.length > 0) 764 doc.insertText = doc.insertText[prefix.length .. $]; 765 completion ~= doc; 766 } 767 768 private bool isInComment(scope const(char)[] code, size_t at, WorkspaceD backend, Token* outToken = null) 769 { 770 if (!backend) 771 return false; 772 773 import dparse.lexer : DLexer, LexerConfig, StringBehavior, tok; 774 775 // TODO: does this kind of token parsing belong in serve-d? 776 777 LexerConfig config; 778 config.fileName = "stdin"; 779 config.stringBehavior = StringBehavior.source; 780 auto lexer = DLexer(code, config, &backend.stringCache); 781 782 while (!lexer.empty) switch (lexer.front.type) 783 { 784 case tok!"comment": 785 auto t = lexer.front; 786 787 auto commentEnd = t.index + t.text.length; 788 if (t.text.startsWith("//")) 789 commentEnd++; 790 791 if (t.index <= at && at < commentEnd) 792 { 793 if (outToken !is null) 794 *outToken = t; 795 return true; 796 } 797 798 lexer.popFront(); 799 break; 800 case tok!"__EOF__": 801 return false; 802 default: 803 lexer.popFront(); 804 break; 805 } 806 return false; 807 } 808 809 @protocolMethod("completionItem/resolve") 810 CompletionItem resolveCompletionItem(CompletionItem item) 811 { 812 auto data = item.data; 813 814 if (item.insertTextFormat.get == InsertTextFormat.snippet 815 && item.kind.get == CompletionItemKind.snippet && data.type == JSONType.object) 816 { 817 const resolved = "resolved" in data.object; 818 if (resolved.type != JSONType.true_) 819 { 820 TextDocumentPositionParams params = data.object["params"] 821 .fromJSON!TextDocumentPositionParams; 822 823 Document document = documents[params.textDocument.uri]; 824 auto f = document.uri.uriToFile; 825 auto instance = backend.getBestInstance(f); 826 827 if (instance.has!SnippetsComponent) 828 { 829 auto snippets = instance.get!SnippetsComponent; 830 auto snippet = snippetFromCompletionItem(item); 831 snippet = snippets.resolveSnippet(f, document.rawText, 832 cast(int) document.positionToBytes(params.position), snippet).getYield; 833 item = snippetToCompletionItem(snippet); 834 } 835 } 836 837 if (const format = "format" in data.object) 838 { 839 auto args = (*format).fromJSON!(string[]); 840 if (item.insertTextFormat.get == InsertTextFormat.snippet) 841 { 842 SnippetLevel level = SnippetLevel.global; 843 if (const levelStr = "level" in data.object) 844 level = levelStr.str.to!SnippetLevel; 845 item.insertText = formatSnippet(item.insertText.get, args, level).opt; 846 } 847 else 848 { 849 item.insertText = formatCode(item.insertText.get, args).opt; 850 } 851 } 852 853 // TODO: format code 854 return item; 855 } 856 else 857 { 858 return item; 859 } 860 } 861 862 CompletionItem snippetToCompletionItem(Snippet snippet) 863 { 864 CompletionItem item; 865 item.label = snippet.shortcut; 866 item.sortText = opt(sortPrefixSnippets ~ snippet.shortcut); 867 item.detail = snippet.title.opt; 868 item.kind = CompletionItemKind.snippet.opt; 869 item.documentation = MarkupContent(MarkupKind.markdown, 870 snippet.documentation ~ "\n\n```d\n" ~ snippet.snippet ~ "\n```\n"); 871 item.filterText = snippet.shortcut.opt; 872 if (capabilities.textDocument.completion.completionItem.snippetSupport) 873 { 874 item.insertText = snippet.snippet.opt; 875 item.insertTextFormat = InsertTextFormat.snippet.opt; 876 } 877 else 878 item.insertText = snippet.plain.opt; 879 880 item.data = JSONValue([ 881 "resolved": JSONValue(snippet.resolved), 882 "id": JSONValue(snippet.id), 883 "providerId": JSONValue(snippet.providerId), 884 "data": snippet.data 885 ]); 886 return item; 887 } 888 889 Snippet snippetFromCompletionItem(CompletionItem item) 890 { 891 Snippet snippet; 892 snippet.shortcut = item.label; 893 snippet.title = item.detail.get; 894 snippet.documentation = item.documentation.get.value; 895 auto end = snippet.documentation.lastIndexOf("\n\n```d\n"); 896 if (end != -1) 897 snippet.documentation = snippet.documentation[0 .. end]; 898 899 if (capabilities.textDocument.completion.completionItem.snippetSupport) 900 snippet.snippet = item.insertText.get; 901 else 902 snippet.plain = item.insertText.get; 903 904 snippet.resolved = item.data["resolved"].boolean; 905 snippet.id = item.data["id"].str; 906 snippet.providerId = item.data["providerId"].str; 907 snippet.data = item.data["data"]; 908 return snippet; 909 } 910 911 unittest 912 { 913 auto backend = new WorkspaceD(); 914 assert(isInComment(`hello /** world`, 10, backend)); 915 assert(!isInComment(`hello /** world`, 3, backend)); 916 assert(isInComment(`hello /* world */ bar`, 8, backend)); 917 assert(isInComment(`hello /* world */ bar`, 16, backend)); 918 assert(!isInComment(`hello /* world */ bar`, 17, backend)); 919 assert(!isInComment("int x;\n// line comment\n", 6, backend)); 920 assert(isInComment("int x;\n// line comment\n", 7, backend)); 921 assert(isInComment("int x;\n// line comment\n", 9, backend)); 922 assert(isInComment("int x;\n// line comment\n", 21, backend)); 923 assert(isInComment("int x;\n// line comment\n", 22, backend)); 924 assert(!isInComment("int x;\n// line comment\n", 23, backend)); 925 } 926 927 auto convertDCDIdentifiers(DCDIdentifier[] identifiers, bool argumentSnippets, bool completeNoDupes) 928 { 929 CompletionItem[] completion; 930 foreach (identifier; identifiers) 931 { 932 CompletionItem item; 933 item.label = identifier.identifier; 934 item.kind = identifier.type.convertFromDCDType; 935 if (identifier.documentation.length) 936 item.documentation = MarkupContent(identifier.documentation.ddocToMarked); 937 if (identifier.definition.length) 938 { 939 item.detail = identifier.definition; 940 if (!completeNoDupes) 941 item.sortText = identifier.definition; 942 // TODO: only add arguments when this is a function call, eg not on template arguments 943 if (identifier.type == "f" && argumentSnippets) 944 { 945 item.insertTextFormat = InsertTextFormat.snippet; 946 string args; 947 auto parts = identifier.definition.extractFunctionParameters; 948 if (parts.length) 949 { 950 int numRequired; 951 foreach (i, part; parts) 952 { 953 ptrdiff_t equals = part.indexOf('='); 954 if (equals != -1) 955 { 956 part = part[0 .. equals].stripRight; 957 // remove default value from autocomplete 958 } 959 auto space = part.lastIndexOf(' '); 960 if (space != -1) 961 part = part[space + 1 .. $]; 962 963 if (args.length) 964 args ~= ", "; 965 args ~= "${" ~ (i + 1).to!string ~ ":" ~ part ~ "}"; 966 numRequired++; 967 } 968 item.insertText = identifier.identifier ~ "(${0:" ~ args ~ "})"; 969 } 970 } 971 } 972 973 if (item.sortText.isNull) 974 item.sortText = item.label.opt; 975 976 item.sortText = opt(sortPrefixDCD ~ identifier.type.sortFromDCDType ~ item.sortText.get); 977 978 completion ~= item; 979 } 980 981 // sort only for duplicate detection (use sortText for UI sorting) 982 completion.sort!"a.label < b.label"; 983 if (completeNoDupes) 984 return completion.chunkBy!((a, b) => a.label == b.label && a.kind == b.kind) 985 .map!((a) { 986 CompletionItem ret = a.front; 987 auto details = a.map!"a.detail" 988 .filter!"!a.isNull && a.value.length" 989 .uniq 990 .array; 991 auto docs = a.map!"a.documentation" 992 .filter!"!a.isNull && a.value.value.length" 993 .uniq 994 .array; 995 if (docs.length) 996 ret.documentation = MarkupContent(MarkupKind.markdown, 997 docs.map!"a.value.value".join("\n\n")); 998 if (details.length) 999 ret.detail = details.map!"a.value".join("\n"); 1000 return ret; 1001 }) 1002 .array; 1003 else 1004 return completion.chunkBy!((a, b) => a.label == b.label && a.detail == b.detail 1005 && a.kind == b.kind) 1006 .map!((a) { 1007 CompletionItem ret = a.front; 1008 auto docs = a.map!"a.documentation" 1009 .filter!"!a.isNull && a.value.value.length" 1010 .uniq 1011 .array; 1012 if (docs.length) 1013 ret.documentation = MarkupContent(MarkupKind.markdown, 1014 docs.map!"a.value.value".join("\n\n")); 1015 return ret; 1016 }) 1017 .array; 1018 } 1019 1020 // === Protocol Notifications starting here === 1021 1022 /// Restarts all DCD servers started by this serve-d instance. Returns `true` once done. 1023 @protocolMethod("served/restartServer") 1024 bool restartServer() 1025 { 1026 Future!void[] fut; 1027 foreach (instance; backend.instances) 1028 if (instance.has!DCDComponent) 1029 fut ~= instance.get!DCDComponent.restartServer(); 1030 joinAll(fut); 1031 return true; 1032 } 1033 1034 /// Kills all DCD servers started by this serve-d instance. 1035 @protocolNotification("served/killServer") 1036 void killServer() 1037 { 1038 foreach (instance; backend.instances) 1039 if (instance.has!DCDComponent) 1040 instance.get!DCDComponent.killServer(); 1041 } 1042 1043 /// Registers a snippet across the whole serve-d application which may be limited to given grammatical scopes. 1044 /// Requires `--provide context-snippets` 1045 /// Returns: `false` if SnippetsComponent hasn't been loaded yet, otherwise `true`. 1046 @protocolMethod("served/addDependencySnippet") 1047 bool addDependencySnippet(AddDependencySnippetParams params) 1048 { 1049 if (!backend.has!SnippetsComponent) 1050 return false; 1051 PlainSnippet snippet; 1052 foreach (i, ref v; snippet.tupleof) 1053 { 1054 static assert(__traits(identifier, snippet.tupleof[i]) == __traits(identifier, 1055 params.snippet.tupleof[i]), 1056 "struct definition changed without updating SerializablePlainSnippet"); 1057 // convert enums 1058 v = cast(typeof(v)) params.snippet.tupleof[i]; 1059 } 1060 backend.get!SnippetsComponent.addDependencySnippet(params.requiredDependencies, snippet); 1061 return true; 1062 }