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.com.importer; 15 import workspaced.coms; 16 17 import std.algorithm : among, any, canFind, chunkBy, endsWith, filter, findSplit, 18 map, min, reverse, sort, startsWith, uniq; 19 import std.array : appender, array, split; 20 import std.conv : text, to; 21 import std.experimental.logger; 22 import std.format : format; 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 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) { 385 CompletionItem ret = { 386 label: a.name, 387 kind: CompletionItemKind.field, 388 documentation: MarkupContent(a.documentation), 389 insertText: a.name ~ '=' 390 }; 391 return ret; 392 }).array); 393 394 if (!line.length) 395 return defaultList; 396 397 if (line[0] == '[') 398 { 399 // ini section 400 CompletionItem staticAnalysisConfig = { 401 label: "analysis.config.StaticAnalysisConfig", 402 kind: CompletionItemKind.keyword 403 }; 404 CompletionItem moduleFilters = { 405 label: "analysis.config.ModuleFilters", 406 kind: CompletionItemKind.keyword, 407 documentation: MarkupContent( 408 "In this optional section a comma-separated list of inclusion and exclusion" 409 ~ " selectors can be specified for every check on which selective filtering" 410 ~ " should be applied. These given selectors match on the module name and" 411 ~ " partial matches (std. or .foo.) are possible. Moreover, every selectors" 412 ~ " must begin with either + (inclusion) or - (exclusion). Exclusion selectors" 413 ~ " take precedence over all inclusion operators.") 414 }; 415 return CompletionList(false, [staticAnalysisConfig, moduleFilters]); 416 } 417 418 auto eqIndex = line.indexOf('='); 419 auto quotIndex = line.lastIndexOf('"'); 420 if (quotIndex != -1 && params.position.character >= quotIndex) 421 return CompletionList.init; 422 if (params.position.character < eqIndex) 423 return defaultList; 424 else 425 { 426 CompletionItem disabled = { 427 label: `"disabled"`, 428 kind: CompletionItemKind.value, 429 detail: "Check is disabled" 430 }; 431 CompletionItem enabled = { 432 label: `"enabled"`, 433 kind: CompletionItemKind.value, 434 detail: "Check is enabled" 435 }; 436 CompletionItem skipUnittest = { 437 label: `"skip-unittest"`, 438 kind: CompletionItemKind.value, 439 detail: "Check is enabled but not operated in the unittests" 440 }; 441 return CompletionList(false, [disabled, enabled, skipUnittest]); 442 } 443 } 444 else 445 { 446 if (!instance) 447 { 448 trace("Providing no completion because no instance"); 449 return CompletionList.init; 450 } 451 452 if (document.getLanguageId == "d") 453 return provideDSourceComplete(params, instance, document); 454 else if (document.getLanguageId == "diet") 455 return provideDietSourceComplete(params, instance, document); 456 else if (document.getLanguageId == "dml") 457 return provideDMLSourceComplete(params, instance, document); 458 else 459 { 460 tracef("Providing no completion for unknown language ID %s.", document.getLanguageId); 461 return CompletionList.init; 462 } 463 } 464 } 465 466 CompletionList provideDMLSourceComplete(TextDocumentPositionParams params, 467 WorkspaceD.Instance instance, ref Document document) 468 { 469 import workspaced.com.dlangui : DlanguiComponent, CompletionType; 470 471 CompletionList ret; 472 473 auto items = backend.get!DlanguiComponent.complete(document.rawText, 474 cast(int) document.positionToBytes(params.position)).getYield(); 475 ret.items.length = items.length; 476 foreach (i, item; items) 477 { 478 CompletionItem translated; 479 480 translated.sortText = ((item.type == CompletionType.Class ? "1." : "0.") ~ item.value).opt; 481 translated.label = item.value; 482 if (item.documentation.length) 483 translated.documentation = MarkupContent(item.documentation); 484 if (item.enumName.length) 485 translated.detail = item.enumName.opt; 486 487 switch (item.type) 488 { 489 case CompletionType.Class: 490 translated.insertTextFormat = InsertTextFormat.snippet; 491 translated.insertText = item.value ~ ` {$0}`; 492 break; 493 case CompletionType.Color: 494 translated.insertTextFormat = InsertTextFormat.snippet; 495 translated.insertText = item.value ~ `: ${0:#000000}`; 496 break; 497 case CompletionType.String: 498 translated.insertTextFormat = InsertTextFormat.snippet; 499 translated.insertText = item.value ~ `: "$0"`; 500 break; 501 case CompletionType.EnumDefinition: 502 translated.insertTextFormat = InsertTextFormat.plainText; 503 translated.insertText = item.enumName ~ "." ~ item.value; 504 break; 505 case CompletionType.Rectangle: 506 case CompletionType.Number: 507 translated.insertTextFormat = InsertTextFormat.snippet; 508 translated.insertText = item.value ~ `: ${0:0}`; 509 break; 510 case CompletionType.Keyword: 511 // don't set, inherit from label 512 break; 513 default: 514 translated.insertTextFormat = InsertTextFormat.plainText; 515 translated.insertText = item.value ~ ": "; 516 break; 517 } 518 519 switch (item.type) 520 { 521 case CompletionType.Class: 522 translated.kind = CompletionItemKind.class_; 523 break; 524 case CompletionType.String: 525 translated.kind = CompletionItemKind.value; 526 break; 527 case CompletionType.Number: 528 translated.kind = CompletionItemKind.value; 529 break; 530 case CompletionType.Color: 531 translated.kind = CompletionItemKind.color; 532 break; 533 case CompletionType.EnumDefinition: 534 translated.kind = CompletionItemKind.enum_; 535 break; 536 case CompletionType.EnumValue: 537 translated.kind = CompletionItemKind.enumMember; 538 break; 539 case CompletionType.Rectangle: 540 translated.kind = CompletionItemKind.typeParameter; 541 break; 542 case CompletionType.Boolean: 543 translated.kind = CompletionItemKind.constant; 544 break; 545 case CompletionType.Keyword: 546 translated.kind = CompletionItemKind.keyword; 547 break; 548 default: 549 case CompletionType.Undefined: 550 break; 551 } 552 553 ret.items[i] = translated; 554 } 555 556 return ret; 557 } 558 559 CompletionList provideDietSourceComplete(TextDocumentPositionParams params, 560 WorkspaceD.Instance instance, ref Document document) 561 { 562 import served.utils.diet; 563 import dc = dietc.complete; 564 565 auto completion = updateDietFile(document.uri.uriToFile, document.rawText.idup); 566 567 auto dcdext = instance.has!DCDExtComponent ? instance.get!DCDExtComponent : null; 568 569 size_t offset = document.positionToBytes(params.position); 570 auto raw = completion.completeAt(offset); 571 CompletionItem[] ret; 572 573 if (raw is dc.Completion.completeD) 574 { 575 auto d = workspace(params.textDocument.uri).config.d; 576 string code; 577 contextExtractD(completion, offset, code, offset, d.dietContextCompletion); 578 if (offset <= code.length && instance.has!DCDComponent) 579 { 580 info("DCD Completing Diet for ", code, " at ", offset); 581 auto dcd = instance.get!DCDComponent.listCompletion(code, cast(int) offset).getYield; 582 if (dcd.type == DCDCompletions.Type.identifiers) 583 { 584 ret = dcd.identifiers.convertDCDIdentifiers(d.argumentSnippets, dcdext); 585 } 586 } 587 } 588 else 589 ret = raw.map!((a) { 590 CompletionItem ret; 591 ret.label = a.text; 592 if (a.text.among!(`''`, `""`, "``", `{}`, `()`, `[]`, `<>`)) 593 { 594 ret.insertTextFormat = InsertTextFormat.snippet; 595 ret.insertText = a.text[0] ~ "$0" ~ a.text[1]; 596 } 597 ret.kind = a.type.mapToCompletionItemKind.opt; 598 if (a.definition.length) 599 { 600 ret.detail = a.definition.opt; 601 if (capabilities 602 .textDocument.orDefault 603 .completion.orDefault 604 .completionItem.orDefault 605 .labelDetailsSupport.orDefault) 606 ret.labelDetails = CompletionItemLabelDetails(ret.detail); 607 } 608 if (a.documentation.length) 609 ret.documentation = MarkupContent(a.documentation); 610 if (a.preselected) 611 ret.preselect = true; 612 return ret; 613 }).array; 614 615 return CompletionList(false, ret); 616 } 617 618 CompletionList provideDSourceComplete(TextDocumentPositionParams params, 619 WorkspaceD.Instance instance, ref Document document) 620 { 621 auto lineRange = document.lineByteRangeAt(params.position.line); 622 auto byteOff = cast(int) document.positionToBytes(params.position); 623 624 auto dcdext = instance.has!DCDExtComponent ? instance.get!DCDExtComponent : null; 625 626 string line = document.rawText[lineRange[0] .. lineRange[1]].idup; 627 string prefix = line[0 .. min($, params.position.character)].strip; 628 CompletionItem[] completion; 629 Token commentToken; 630 if (document.rawText.isInComment(byteOff, backend, &commentToken)) 631 { 632 import dparse.lexer : tok; 633 if (commentToken.type == tok!"__EOF__") 634 return CompletionList.init; 635 636 if (commentToken.text.startsWith("///", "/**", "/++")) 637 { 638 trace("Providing comment completion"); 639 int prefixLen = (prefix.length > 0 && prefix[0] == '/') ? 3 : 1; 640 auto remaining = prefix[min($, prefixLen) .. $].stripLeft; 641 642 foreach (compl; import("ddocs.txt").lineSplitter) 643 { 644 if (compl.startsWith(remaining)) 645 { 646 CompletionItem item = { 647 label: compl, 648 kind: CompletionItemKind.snippet, 649 insertText: compl ~ ": " 650 }; 651 completion ~= item; 652 } 653 } 654 655 // make the comment one "line" so provide doc complete shows complete 656 // after a /** */ comment block if you are on the first line. 657 lineRange[1] = commentToken.index + commentToken.text.length; 658 provideDocComplete(params, instance, document, completion, line, lineRange); 659 660 return CompletionList(false, completion); 661 } 662 } 663 664 bool completeDCD = instance.has!DCDComponent; 665 bool completeDoc = instance.has!DscannerComponent; 666 bool completeSnippets = doCompleteSnippets && instance.has!SnippetsComponent; 667 668 tracef("Performing regular D comment completion (DCD=%s, Documentation=%s, Snippets=%s)", 669 completeDCD, completeDoc, completeSnippets); 670 const config = workspace(params.textDocument.uri).config; 671 DCDCompletions result = DCDCompletions.empty; 672 SnippetInfo snippetInfo; 673 joinAll({ 674 if (completeDCD) 675 result = instance.get!DCDComponent.listCompletion(document.rawText, byteOff).getYield; 676 }, { 677 if (completeDoc) 678 provideDocComplete(params, instance, document, completion, line, lineRange); 679 }, { 680 if (completeSnippets) 681 snippetInfo = provideSnippetComplete(params, instance, document, config, completion, byteOff); 682 else 683 snippetInfo = getSnippetInfo(instance, document, byteOff); 684 }); 685 686 if (completeDCD && result != DCDCompletions.init) 687 { 688 if (result.type == DCDCompletions.Type.identifiers) 689 { 690 auto d = config.d; 691 completion ~= convertDCDIdentifiers(result.identifiers, d.argumentSnippets, dcdext, snippetInfo); 692 } 693 else if (result.type != DCDCompletions.Type.calltips) 694 { 695 trace("Unexpected result from DCD: ", result); 696 } 697 } 698 return CompletionList(false, completion); 699 } 700 701 private void provideDocComplete(TextDocumentPositionParams params, WorkspaceD.Instance instance, 702 ref Document document, ref CompletionItem[] completion, string line, size_t[2] lineRange) 703 { 704 string lineStripped = line.strip; 705 if (lineStripped.among!("", "/", "/*", "/+", "//", "///", "/**", "/++")) 706 { 707 auto defs = instance.get!DscannerComponent.listDefinitions(uriToFile( 708 params.textDocument.uri), document.rawText[lineRange[1] .. $]).getYield; 709 ptrdiff_t di = -1; 710 FuncFinder: foreach (i, def; defs) 711 { 712 if (def.line >= 0 && def.line <= 5) 713 { 714 di = i; 715 break FuncFinder; 716 } 717 } 718 if (di == -1) 719 return; 720 auto def = defs[di]; 721 auto sig = "signature" in def.attributes; 722 if (!sig) 723 { 724 CompletionItem doc = CompletionItem("///"); 725 doc.kind = CompletionItemKind.snippet; 726 doc.insertTextFormat = InsertTextFormat.snippet; 727 auto eol = document.eolAt(params.position.line).toString; 728 doc.insertText = "/// "; 729 CompletionItem doc2 = doc; 730 CompletionItem doc3 = doc; 731 doc2.label = "/**"; 732 doc2.insertText = "/** " ~ eol ~ " * $0" ~ eol ~ " */"; 733 doc3.label = "/++"; 734 doc3.insertText = "/++ " ~ eol ~ " * $0" ~ eol ~ " +/"; 735 736 completion.addDocComplete(doc, lineStripped); 737 completion.addDocComplete(doc2, lineStripped); 738 completion.addDocComplete(doc3, lineStripped); 739 return; 740 } 741 auto funcArgs = extractFunctionParameters(*sig); 742 string[] docs = ["$0"]; 743 int argNo = 1; 744 foreach (arg; funcArgs) 745 { 746 auto space = arg.stripRight.lastIndexOf(' '); 747 if (space == -1) 748 continue; 749 auto identifier = arg[space + 1 .. $]; 750 if (!identifier.isValidDIdentifier) 751 continue; 752 if (argNo == 1) 753 docs ~= "Params:"; 754 docs ~= text(" ", identifier, " = $", argNo.to!string); 755 argNo++; 756 } 757 auto retAttr = "return" in def.attributes; 758 if (retAttr && *retAttr != "void") 759 { 760 docs ~= "Returns: $" ~ argNo.to!string; 761 argNo++; 762 } 763 auto depr = "deprecation" in def.attributes; 764 if (depr) 765 { 766 docs ~= "Deprecated: $" ~ argNo.to!string ~ *depr; 767 argNo++; 768 } 769 CompletionItem doc = CompletionItem("///"); 770 doc.kind = CompletionItemKind.snippet; 771 doc.insertTextFormat = InsertTextFormat.snippet; 772 auto eol = document.eolAt(params.position.line).toString; 773 doc.insertText = docs.map!(a => "/// " ~ a).join(eol); 774 CompletionItem doc2 = doc; 775 CompletionItem doc3 = doc; 776 doc2.label = "/**"; 777 doc2.insertText = "/** " ~ eol ~ docs.map!(a => " * " ~ a ~ eol).join() ~ " */"; 778 doc3.label = "/++"; 779 doc3.insertText = "/++ " ~ eol ~ docs.map!(a => " + " ~ a ~ eol).join() ~ " +/"; 780 781 doc.sortText = opt(sortPrefixDoc ~ "0"); 782 doc2.sortText = opt(sortPrefixDoc ~ "1"); 783 doc3.sortText = opt(sortPrefixDoc ~ "2"); 784 785 completion.addDocComplete(doc, lineStripped); 786 completion.addDocComplete(doc2, lineStripped); 787 completion.addDocComplete(doc3, lineStripped); 788 } 789 } 790 791 private SnippetInfo provideSnippetComplete(TextDocumentPositionParams params, WorkspaceD.Instance instance, 792 ref Document document, ref const UserConfiguration config, 793 ref CompletionItem[] completion, int byteOff) 794 { 795 if (byteOff > 0 && document.rawText[byteOff - 1 .. $].startsWith(".")) 796 return SnippetInfo.init; // no snippets after '.' character 797 798 auto snippets = instance.get!SnippetsComponent; 799 auto ret = snippets.getSnippetsYield(document.uri.uriToFile, document.rawText, byteOff); 800 trace("got ", ret.snippets.length, " snippets fitting in this context: ", 801 ret.snippets.map!"a.shortcut"); 802 auto eol = document.eolAt(0); 803 foreach (Snippet snippet; ret.snippets) 804 { 805 CompletionItem item = snippet.snippetToCompletionItem; 806 JsonValue[string] data; 807 data["level"] = JsonValue(ret.info.level.to!string); 808 if (!snippet.unformatted) 809 data["format"] = JsonValue(generateDfmtArgs(config, eol).join("\t")); 810 data["uri"] = JsonValue(params.textDocument.uri); 811 data["line"] = JsonValue(params.position.line); 812 data["column"] = JsonValue(params.position.character); 813 if (snippet.imports.length) 814 data["imports"] = JsonValue(snippet.imports.map!(i => JsonValue(i)).array); 815 item.data = JsonValue(data); 816 completion ~= item; 817 } 818 819 return ret.info; 820 } 821 822 private SnippetInfo getSnippetInfo(WorkspaceD.Instance instance, ref Document document, int byteOff) 823 { 824 if (byteOff > 0 && document.rawText[byteOff - 1 .. $].startsWith(".")) 825 return SnippetInfo.init; // no snippets after '.' character 826 827 auto snippets = instance.get!SnippetsComponent; 828 return snippets.determineSnippetInfo(document.uri.uriToFile, document.rawText, byteOff); 829 } 830 831 private void addDocComplete(ref CompletionItem[] completion, CompletionItem doc, string prefix) 832 in(!doc.insertText.isNone) 833 { 834 if (!doc.label.startsWith(prefix)) 835 return; 836 if (prefix.length > 0) 837 doc.insertText = doc.insertText.deref[prefix.length .. $]; 838 completion ~= doc; 839 } 840 841 private bool isInComment(scope const(char)[] code, size_t at, WorkspaceD backend, Token* outToken = null) 842 { 843 if (!backend) 844 return false; 845 846 import dparse.lexer : DLexer, LexerConfig, StringBehavior, tok; 847 848 // TODO: does this kind of token parsing belong in serve-d? 849 850 LexerConfig config; 851 config.fileName = "stdin"; 852 config.stringBehavior = StringBehavior.source; 853 auto lexer = DLexer(code, config, &backend.stringCache); 854 855 while (!lexer.empty) 856 { 857 if (lexer.front.index > at) 858 return false; 859 860 switch (lexer.front.type) 861 { 862 case tok!"comment": 863 auto t = lexer.front; 864 865 auto commentEnd = t.index + t.text.length; 866 if (t.text.startsWith("//")) 867 commentEnd++; 868 869 if (t.index <= at && at < commentEnd) 870 { 871 if (outToken !is null) 872 *outToken = t; 873 return true; 874 } 875 876 lexer.popFront(); 877 break; 878 case tok!"__EOF__": 879 if (outToken !is null) 880 *outToken = lexer.front; 881 return true; 882 default: 883 lexer.popFront(); 884 break; 885 } 886 } 887 return false; 888 } 889 890 @protocolMethod("completionItem/resolve") 891 CompletionItem resolveCompletionItem(CompletionItem item) 892 { 893 auto data = item.data; 894 895 if (!data.isNone) 896 { 897 auto object = data.deref.get!(StringMap!JsonValue); 898 const resolved = "resolved" in object; 899 const uriObj = "uri" in object; 900 const lineObj = "line" in object; 901 const columnObj = "column" in object; 902 903 if (uriObj && lineObj && columnObj) 904 { 905 auto uri = uriObj.get!string; 906 auto line = cast(uint)lineObj.get!long; 907 auto column = cast(uint)columnObj.get!long; 908 909 Document document = documents[uri]; 910 auto f = document.uri.uriToFile; 911 auto instance = backend.getBestInstance(f); 912 913 if (resolved && !resolved.get!bool) 914 { 915 if (instance.has!SnippetsComponent) 916 { 917 auto snippets = instance.get!SnippetsComponent; 918 auto snippet = snippetFromCompletionItem(item); 919 snippet = snippets.resolveSnippet(f, document.rawText, 920 cast(int) document.positionToBytes(Position(line, column)), snippet).getYield; 921 item = snippetToCompletionItem(snippet); 922 } 923 } 924 925 if (const importsJson = "imports" in object) 926 { 927 if (instance.has!ImporterComponent) 928 { 929 auto importer = instance.get!ImporterComponent; 930 TextEdit[] additionalEdits = item.additionalTextEdits.orDefault; 931 auto imports = importsJson.get!(JsonValue[]); 932 foreach (importJson; imports) 933 { 934 string importName = importJson.get!string; 935 auto importInfo = importer.add(importName, document.rawText, 936 cast(int) document.positionToBytes(Position(line, column))); 937 // TODO: use renamed imports properly 938 foreach (edit; importInfo.replacements) 939 additionalEdits ~= edit.toTextEdit(document); 940 } 941 additionalEdits.sort!"a.range.start>b.range.start"; 942 item.additionalTextEdits = additionalEdits; 943 } 944 } 945 } 946 947 if (const format = "format" in object) 948 { 949 auto args = format.get!string.split("\t"); 950 if (item.insertTextFormat.orDefault == InsertTextFormat.snippet) 951 { 952 SnippetLevel level = SnippetLevel.global; 953 if (const levelStr = "level" in object) 954 level = levelStr.get!string.to!SnippetLevel; 955 item.insertText = formatSnippet(item.insertText.deref, args, level); 956 } 957 else 958 { 959 item.insertText = formatCode(item.insertText.deref, args); 960 } 961 } 962 963 return item; 964 } 965 else 966 { 967 return item; 968 } 969 } 970 971 CompletionItem snippetToCompletionItem(Snippet snippet) 972 { 973 CompletionItem item; 974 item.label = snippet.shortcut; 975 item.sortText = opt(sortPrefixSnippets ~ snippet.shortcut); 976 item.detail = snippet.title; 977 item.kind = CompletionItemKind.snippet; 978 item.documentation = MarkupContent(MarkupKind.markdown, 979 snippet.documentation ~ "\n\n```d\n" ~ snippet.snippet ~ "\n```\n"); 980 item.filterText = snippet.shortcut; 981 if (capabilities 982 .textDocument.orDefault 983 .completion.orDefault 984 .completionItem.orDefault 985 .snippetSupport.orDefault) 986 { 987 item.insertText = snippet.snippet; 988 item.insertTextFormat = InsertTextFormat.snippet; 989 } 990 else 991 item.insertText = snippet.plain; 992 993 item.data = JsonValue([ 994 "resolved": JsonValue(snippet.resolved), 995 "id": JsonValue(snippet.id), 996 "providerId": JsonValue(snippet.providerId), 997 "data": snippet.data.toJsonValue 998 ]); 999 return item; 1000 } 1001 1002 Snippet snippetFromCompletionItem(CompletionItem item) 1003 { 1004 Snippet snippet; 1005 snippet.shortcut = item.label; 1006 snippet.title = item.detail.deref; 1007 snippet.documentation = item.documentation.match!( 1008 () => cast(string)(null), 1009 (string s) => s, 1010 (MarkupContent c) => c.value 1011 ); 1012 auto end = snippet.documentation.lastIndexOf("\n\n```d\n"); 1013 if (end != -1) 1014 snippet.documentation = snippet.documentation[0 .. end]; 1015 1016 if (capabilities 1017 .textDocument.orDefault 1018 .completion.orDefault 1019 .completionItem.orDefault 1020 .snippetSupport.orDefault) 1021 snippet.snippet = item.insertText.deref; 1022 else 1023 snippet.plain = item.insertText.deref; 1024 1025 auto itemData = item.data.deref.get!(StringMap!JsonValue); 1026 snippet.resolved = itemData["resolved"].get!bool; 1027 snippet.id = itemData["id"].get!string; 1028 snippet.providerId = itemData["providerId"].get!string; 1029 snippet.data = itemData["data"]; 1030 return snippet; 1031 } 1032 1033 unittest 1034 { 1035 auto backend = new WorkspaceD(); 1036 assert(isInComment(`hello /** world`, 10, backend)); 1037 assert(!isInComment(`hello /** world`, 3, backend)); 1038 assert(isInComment(`hello /* world */ bar`, 8, backend)); 1039 assert(isInComment(`hello /* world */ bar`, 16, backend)); 1040 assert(!isInComment(`hello /* world */ bar`, 17, backend)); 1041 assert(!isInComment("int x;\n// line comment\n", 6, backend)); 1042 assert(isInComment("int x;\n// line comment\n", 7, backend)); 1043 assert(isInComment("int x;\n// line comment\n", 9, backend)); 1044 assert(isInComment("int x;\n// line comment\n", 21, backend)); 1045 assert(isInComment("int x;\n// line comment\n", 22, backend)); 1046 assert(!isInComment("int x;\n// line comment\n", 23, backend)); 1047 } 1048 1049 auto convertDCDIdentifiers(DCDIdentifier[] identifiers, bool argumentSnippets, DCDExtComponent dcdext, 1050 SnippetInfo info = SnippetInfo.init) 1051 { 1052 CompletionItem[] completion; 1053 foreach (identifier; identifiers) 1054 { 1055 CompletionItem item; 1056 string detailDetail, detailDescription; 1057 item.label = identifier.identifier; 1058 item.kind = identifier.type.convertFromDCDType; 1059 if (identifier.documentation.length) 1060 item.documentation = MarkupContent(identifier.documentation.ddocToMarked); 1061 1062 if (identifier.definition.length == 0) 1063 { 1064 if (identifier.type.length == 1) 1065 { 1066 switch (identifier.type[0]) 1067 { 1068 case 'c': 1069 detailDescription = "Class"; 1070 break; 1071 case 'i': 1072 detailDescription = "Interface"; 1073 break; 1074 case 's': 1075 detailDescription = "Struct"; 1076 break; 1077 case 'u': 1078 detailDescription = "Union"; 1079 break; 1080 case 'a': 1081 detailDescription = "Array"; 1082 break; 1083 case 'A': 1084 detailDescription = "AA"; 1085 break; 1086 case 'v': 1087 detailDescription = "Variable"; 1088 break; 1089 case 'm': 1090 detailDescription = "Member"; 1091 break; 1092 case 'e': 1093 // lowercare to differentiate member from enum name 1094 detailDescription = "enum"; 1095 break; 1096 case 'k': 1097 detailDescription = "Keyword"; 1098 break; 1099 case 'f': 1100 detailDescription = "Function"; 1101 break; 1102 case 'g': 1103 detailDescription = "Enum"; 1104 break; 1105 case 'P': 1106 detailDescription = "Package"; 1107 break; 1108 case 'M': 1109 detailDescription = "Module"; 1110 break; 1111 case 't': 1112 case 'T': 1113 detailDescription = "Template"; 1114 break; 1115 case 'h': 1116 detailDescription = "<T>"; 1117 break; 1118 case 'p': 1119 detailDescription = "<T...>"; 1120 break; 1121 case 'l': // Alias (eventually should show what it aliases to) 1122 default: 1123 break; 1124 } 1125 } 1126 } 1127 else 1128 { 1129 item.detail = identifier.definition; 1130 1131 // check if that's actually a proper completion item to process 1132 auto definitionSpace = identifier.definition.indexOf(' '); 1133 if (definitionSpace != -1) 1134 { 1135 detailDescription = identifier.definition[0 .. definitionSpace]; 1136 1137 // if function, only show the parenthesis content 1138 if (identifier.type == "f") 1139 { 1140 auto paren = identifier.definition.indexOf('('); 1141 if (paren != -1) 1142 detailDetail = " " ~ identifier.definition[paren .. $]; 1143 } 1144 } 1145 1146 1147 // handle special cases 1148 if (identifier.type == "e") 1149 { 1150 // enum definitions are the enum identifiers (not the type) 1151 detailDescription = "enum"; 1152 } 1153 else if (identifier.type == "f" && dcdext) 1154 { 1155 CalltipsSupport funcParams = dcdext.extractCallParameters( 1156 identifier.definition, cast(int) identifier.definition.length - 1, true); 1157 1158 // if definition doesn't contains a return type, then it is a function that returns auto 1159 // it could be 'enum', but that's typically the same, and there is no way to get that info right now 1160 // need to check on DCD's part, auto/enum are removed from the definition 1161 auto nameEnd = funcParams.templateArgumentsRange[0]; 1162 if (!nameEnd) nameEnd = funcParams.functionParensRange[0]; 1163 if (!nameEnd) nameEnd = cast(int) identifier.definition.length; 1164 auto retTypeEnd = identifier.definition.lastIndexOf(' ', nameEnd); 1165 if (retTypeEnd != -1) 1166 detailDescription = identifier.definition[0 .. retTypeEnd].strip; 1167 else 1168 detailDescription = "auto"; 1169 1170 detailDetail = " " ~ identifier.definition[nameEnd .. $]; 1171 } 1172 1173 item.sortText = identifier.definition; 1174 1175 // TODO: only add arguments when this is a function call, eg not on template arguments 1176 if (identifier.type == "f" && argumentSnippets 1177 && info.level.among!(SnippetLevel.method, SnippetLevel.value)) 1178 { 1179 item.insertTextFormat = InsertTextFormat.snippet; 1180 string args; 1181 auto parts = identifier.definition.extractFunctionParameters; 1182 if (parts.length) 1183 { 1184 int numRequired; 1185 foreach (i, part; parts) 1186 { 1187 ptrdiff_t equals = part.indexOf('='); 1188 if (equals != -1) 1189 { 1190 part = part[0 .. equals].stripRight; 1191 // remove default value from autocomplete 1192 } 1193 auto space = part.lastIndexOf(' '); 1194 if (space != -1) 1195 part = part[space + 1 .. $]; 1196 1197 if (args.length) 1198 args ~= ", "; 1199 args ~= "${" ~ (i + 1).to!string ~ ":" ~ part ~ "}"; 1200 numRequired++; 1201 } 1202 item.insertText = identifier.identifier ~ "(${0:" ~ args ~ "})"; 1203 } 1204 } 1205 } 1206 1207 if (item.sortText.isNone) 1208 item.sortText = item.label; 1209 1210 item.sortText = sortPrefixDCD ~ identifier.type.sortFromDCDType ~ item.sortText.deref; 1211 1212 if (detailDescription.length || detailDetail.length) 1213 { 1214 CompletionItemLabelDetails d; 1215 if (detailDetail.length) 1216 d.detail = detailDetail; 1217 if (detailDescription.length) 1218 d.description = detailDescription; 1219 1220 item.labelDetails = d; 1221 } 1222 1223 completion ~= item; 1224 } 1225 1226 // sort only for duplicate detection (use sortText for UI sorting) 1227 completion.sort!"a.effectiveInsertText < b.effectiveInsertText"; 1228 return completion.chunkBy!( 1229 (a, b) => 1230 a.effectiveInsertText == b.effectiveInsertText 1231 && a.kind == b.kind 1232 ).map!((a) { 1233 CompletionItem ret = a.front; 1234 auto details = a.map!"a.detail" 1235 .filter!(a => !a.isNone && a.deref.length) 1236 .uniq 1237 .array; 1238 auto docs = a.map!"a.documentation" 1239 .filter!(a => a.match!( 1240 () => false, 1241 (string s) => s.length > 0, 1242 (MarkupContent s) => s.value.length > 0, 1243 )) 1244 .uniq 1245 .array; 1246 auto labelDetails = a.map!"a.labelDetails" 1247 .filter!(a => !a.isNone) 1248 .uniq 1249 .array; 1250 if (docs.length) 1251 ret.documentation = MarkupContent(MarkupKind.markdown, 1252 docs.map!(a => a.match!( 1253 () => assert(false), 1254 (string s) => s, 1255 (MarkupContent s) => s.value, 1256 )).join("\n\n")); 1257 if (details.length) 1258 ret.detail = details.map!(a => a.deref).join("\n"); 1259 1260 if (labelDetails.length == 1) 1261 { 1262 ret.labelDetails = labelDetails[0]; 1263 } 1264 else if (labelDetails.length > 1) 1265 { 1266 auto descriptions = labelDetails 1267 .filter!(a => !a.deref.description.isNone) 1268 .map!(a => a.deref.description.deref) 1269 .array 1270 .sort!"a<b" 1271 .uniq 1272 .array; 1273 auto detailDetails = labelDetails 1274 .filter!(a => !a.deref.detail.isNone) 1275 .map!(a => a.deref.detail.deref) 1276 .array 1277 .sort!"a<b" 1278 .uniq 1279 .array; 1280 1281 CompletionItemLabelDetails detail; 1282 if (descriptions.length == 1) 1283 detail.description = descriptions[0]; 1284 else if (descriptions.length) 1285 detail.description = descriptions.join(" | "); 1286 1287 if (detailDetails.length == 1) 1288 detail.detail = detailDetails[0]; 1289 else if (detailDetails.length && detailDetails[0].endsWith(")")) 1290 detail.detail = format!" (*%d overloads*)"(detailDetails.length); 1291 else if (detailDetails.length) // dunno when/if this can even happen 1292 detail.description = detailDetails.join(" |"); 1293 1294 ret.labelDetails = detail; 1295 } 1296 1297 migrateLabelDetailsSupport(ret); 1298 return ret; 1299 }) 1300 .array; 1301 } 1302 1303 private void migrateLabelDetailsSupport(ref CompletionItem item) 1304 { 1305 if (!capabilities 1306 .textDocument.orDefault 1307 .completion.orDefault 1308 .completionItem.orDefault 1309 .labelDetailsSupport.orDefault 1310 && !item.labelDetails.isNone) 1311 { 1312 // labelDetails is not supported, but let's use what we computed, it's 1313 // still very useful 1314 CompletionItemLabelDetails detail = item.labelDetails.deref; 1315 1316 // don't overwrite `detail`, it may be used to show full definition in a 1317 // documentation popup. 1318 1319 // if we got a detailed detail, use that and properly set the insertText 1320 if (!detail.detail.isNone) 1321 { 1322 if (item.insertText.isNone) 1323 item.insertText = item.label; 1324 item.label ~= detail.detail.deref; 1325 } 1326 1327 item.labelDetails = item.labelDetails._void; 1328 } 1329 } 1330 1331 // === Protocol Notifications starting here === 1332 1333 /// Restarts all DCD servers started by this serve-d instance. Returns `true` once done. 1334 @protocolMethod("served/restartServer") 1335 bool restartServer() 1336 { 1337 Future!void[] fut; 1338 foreach (instance; backend.instances) 1339 if (instance.has!DCDComponent) 1340 fut ~= instance.get!DCDComponent.restartServer(); 1341 joinAll(fut); 1342 return true; 1343 } 1344 1345 /// Kills all DCD servers started by this serve-d instance. 1346 @protocolNotification("served/killServer") 1347 void killServer() 1348 { 1349 foreach (instance; backend.instances) 1350 if (instance.has!DCDComponent) 1351 instance.get!DCDComponent.killServer(); 1352 } 1353 1354 /// Registers a snippet across the whole serve-d application which may be limited to given grammatical scopes. 1355 /// Requires `--provide context-snippets` 1356 /// Returns: `false` if SnippetsComponent hasn't been loaded yet, otherwise `true`. 1357 @protocolMethod("served/addDependencySnippet") 1358 bool addDependencySnippet(AddDependencySnippetParams params) 1359 { 1360 if (!backend.has!SnippetsComponent) 1361 return false; 1362 PlainSnippet snippet; 1363 foreach (i, ref v; snippet.tupleof) 1364 { 1365 static assert(__traits(identifier, snippet.tupleof[i]) == __traits(identifier, 1366 params.snippet.tupleof[i]), 1367 "struct definition changed without updating SerializablePlainSnippet"); 1368 // convert enums 1369 v = cast(typeof(v)) params.snippet.tupleof[i]; 1370 } 1371 backend.get!SnippetsComponent.addDependencySnippet(params.requiredDependencies, snippet); 1372 return true; 1373 }