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