1 module dls.tools.symbol_tool; 2 3 import dls.protocol.interfaces : CompletionItemKind, SymbolKind; 4 import dls.tools.tool : Tool; 5 import dsymbol.symbol : CompletionKind; 6 import std.path : asNormalizedPath, buildNormalizedPath, dirName; 7 8 private immutable macroUrl = "https://raw.githubusercontent.com/dlang/dlang.org/stable/%s.ddoc"; 9 private immutable macroFiles = ["html", "macros", "std", "std_consolidated", "std-ddox"]; 10 private string[string] macros; 11 private immutable CompletionItemKind[CompletionKind] completionKinds; 12 private immutable SymbolKind[CompletionKind] symbolKinds; 13 14 shared static this() 15 { 16 import dub.internal.vibecompat.core.log : LogLevel, setLogLevel; 17 18 //dfmt off 19 completionKinds = [ 20 CompletionKind.className : CompletionItemKind.class_, 21 CompletionKind.interfaceName : CompletionItemKind.interface_, 22 CompletionKind.structName : CompletionItemKind.struct_, 23 CompletionKind.unionName : CompletionItemKind.interface_, 24 CompletionKind.variableName : CompletionItemKind.variable, 25 CompletionKind.memberVariableName : CompletionItemKind.field, 26 CompletionKind.keyword : CompletionItemKind.keyword, 27 CompletionKind.functionName : CompletionItemKind.function_, 28 CompletionKind.enumName : CompletionItemKind.enum_, 29 CompletionKind.enumMember : CompletionItemKind.enumMember, 30 CompletionKind.packageName : CompletionItemKind.folder, 31 CompletionKind.moduleName : CompletionItemKind.module_, 32 CompletionKind.aliasName : CompletionItemKind.variable, 33 CompletionKind.templateName : CompletionItemKind.function_, 34 CompletionKind.mixinTemplateName : CompletionItemKind.function_ 35 ]; 36 37 symbolKinds = [ 38 CompletionKind.className : SymbolKind.class_, 39 CompletionKind.interfaceName : SymbolKind.interface_, 40 CompletionKind.structName : SymbolKind.struct_, 41 CompletionKind.unionName : SymbolKind.interface_, 42 CompletionKind.variableName : SymbolKind.variable, 43 CompletionKind.memberVariableName : SymbolKind.field, 44 CompletionKind.keyword : SymbolKind.constant, 45 CompletionKind.functionName : SymbolKind.function_, 46 CompletionKind.enumName : SymbolKind.enum_, 47 CompletionKind.enumMember : SymbolKind.enumMember, 48 CompletionKind.packageName : SymbolKind.package_, 49 CompletionKind.moduleName : SymbolKind.module_, 50 CompletionKind.aliasName : SymbolKind.variable, 51 CompletionKind.templateName : SymbolKind.function_, 52 CompletionKind.mixinTemplateName : SymbolKind.function_ 53 ]; 54 //dfmt on 55 56 setLogLevel(LogLevel.none); 57 } 58 59 class SymbolTool : Tool 60 { 61 import dcd.common.messages : AutocompleteRequest, RequestKind; 62 import dls.protocol.definitions : Location, MarkupContent, Position, 63 TextDocumentItem; 64 import dls.protocol.interfaces : CompletionItem, DocumentHighlight, Hover, 65 SymbolInformation; 66 import dls.util.document : Document; 67 import dls.util.logger : logger; 68 import dls.util.uri : Uri; 69 import dsymbol.modulecache : ModuleCache; 70 import dub.dub : Dub; 71 import dub.platform : BuildPlatform; 72 import std.algorithm : map, reduce, sort, uniq; 73 import std.array : appender, array, replace; 74 import std.container : RedBlackTree; 75 import std.conv : to; 76 import std.file : readText; 77 import std.json : JSONValue; 78 import std.net.curl : byLine; 79 import std.parallelism : Task; 80 import std.range : chain; 81 import std.regex : matchFirst; 82 import std.typecons : nullable; 83 84 version (Windows) 85 { 86 @property private static string[] _compilerConfigPaths() 87 { 88 import std.algorithm : splitter; 89 import std.file : exists; 90 import std.process : environment; 91 92 foreach (path; splitter(environment["PATH"], ';')) 93 { 94 if (buildNormalizedPath(path, "dmd.exe").exists()) 95 { 96 return [buildNormalizedPath(path, "sc.ini")]; 97 } 98 } 99 100 return []; 101 } 102 } 103 else version (Posix) 104 { 105 private static immutable _compilerConfigPaths = [ 106 `/etc/dmd.conf`, `/usr/local/etc/dmd.conf`, `/etc/ldc2.conf`, 107 `/usr/local/etc/ldc2.conf` 108 ]; 109 } 110 else 111 { 112 private static immutable string[] _compilerConfigPaths; 113 } 114 115 private Task!(byLine, string)*[] _macroTasks; 116 private ModuleCache*[string] _workspaceCaches; 117 private ModuleCache*[string] _libraryCaches; 118 119 @property private string[] defaultImportPaths() 120 { 121 import std.algorithm : each; 122 import std.file : FileException, exists; 123 import std.regex : matchAll; 124 125 string[] paths; 126 127 foreach (confPath; _compilerConfigPaths) 128 { 129 if (confPath.exists()) 130 { 131 try 132 { 133 readText(confPath).matchAll(`-I[^\s"]+`) 134 .each!(m => paths ~= m.hit[2 .. $].replace("%@P%", 135 confPath.dirName).asNormalizedPath().to!string); 136 break; 137 } 138 catch (FileException e) 139 { 140 // File doesn't exist or could't be read 141 } 142 } 143 } 144 145 version (linux) 146 { 147 if (paths.length == 0) 148 { 149 foreach (path; ["/snap", "/var/lib/snapd/snap"]) 150 { 151 if (buildNormalizedPath(path, "dmd").exists()) 152 { 153 paths = ["druntime", "phobos"].map!(end => buildNormalizedPath(path, 154 "dmd", "current", "import", end)).array; 155 break; 156 } 157 } 158 } 159 } 160 161 return paths.sort().uniq().array; 162 } 163 164 this() 165 { 166 import std.format : format; 167 import std.parallelism : task; 168 169 foreach (macroFile; macroFiles) 170 { 171 auto t = task!byLine(format!macroUrl(macroFile)); 172 _macroTasks ~= t; 173 t.executeInNewThread(); 174 } 175 176 importDirectories!true("", defaultImportPaths); 177 } 178 179 ModuleCache*[] getRelevantCaches(Uri uri) 180 { 181 auto result = appender([getWorkspaceCache(uri)]); 182 183 foreach (path; _libraryCaches.byKey) 184 { 185 if (path.length > 0) 186 { 187 result ~= _libraryCaches[path]; 188 } 189 } 190 191 result ~= _libraryCaches[""]; 192 193 return result.data; 194 } 195 196 ModuleCache* getWorkspaceCache(Uri uri) 197 { 198 import std.algorithm : startsWith; 199 import std.path : pathSplitter; 200 201 string[] cachePathParts; 202 203 foreach (path; chain(_workspaceCaches.byKey, _libraryCaches.byKey)) 204 { 205 auto splitter = pathSplitter(path); 206 207 if (pathSplitter(uri.path).startsWith(splitter)) 208 { 209 auto pathParts = splitter.array; 210 211 if (pathParts.length > cachePathParts.length) 212 { 213 cachePathParts = pathParts; 214 } 215 } 216 } 217 218 auto cachePath = buildNormalizedPath(cachePathParts); 219 return cachePath in _workspaceCaches ? _workspaceCaches[cachePath] 220 : _libraryCaches[cachePath]; 221 } 222 223 void importPath(Uri uri) 224 { 225 const d = getDub(uri); 226 const desc = d.project.rootPackage.describe(BuildPlatform.any, null, null); 227 importDirectories!false(uri.path, 228 desc.importPaths.map!(importPath => buildNormalizedPath(uri.path, 229 importPath)).array); 230 } 231 232 void importSelections(Uri uri) 233 { 234 const d = getDub(uri); 235 const project = d.project; 236 237 foreach (dep; project.dependencies) 238 { 239 const desc = dep.describe(BuildPlatform.any, null, 240 dep.name in project.rootPackage.recipe.buildSettings.subConfigurations 241 ? project.rootPackage.recipe.buildSettings.subConfigurations[dep.name] : null); 242 importDirectories!true(dep.name, 243 desc.importPaths.map!(importPath => buildNormalizedPath(dep.path.toString(), 244 importPath)).array, true); 245 } 246 } 247 248 void clearPath(Uri uri) 249 { 250 logger.infof("Clearing imports from %s", uri.path); 251 252 if (uri.path in _workspaceCaches) 253 { 254 _workspaceCaches.remove(uri.path); 255 } 256 else 257 { 258 _libraryCaches.remove(uri.path); 259 } 260 } 261 262 void upgradeSelections(Uri uri) 263 { 264 import std.concurrency : spawn; 265 266 logger.infof("Upgrading dependencies from %s", dirName(uri.path)); 267 268 spawn((string uriString) { 269 import dls.protocol.jsonrpc : send; 270 import dub.dub : UpgradeOptions; 271 272 send("$/dls.upgradeSelections.start"); 273 getDub(new Uri(uriString)).upgrade(UpgradeOptions.upgrade | UpgradeOptions.select); 274 send("$/dls.upgradeSelections.stop"); 275 }, uri.toString()); 276 } 277 278 SymbolInformation[] symbol(string query, Uri uri = null) 279 { 280 import dsymbol.string_interning : internString; 281 import dsymbol.symbol : DSymbol; 282 283 logger.infof(`Fetching symbols from %s with query "%s"`, uri is null 284 ? "workspace" : uri.path, query); 285 286 auto result = new RedBlackTree!(SymbolInformation, q{a.name > b.name}, true); 287 288 void collectSymbolInformations(Uri symbolUri, const(DSymbol)* symbol, 289 string containerName = "") 290 { 291 if (symbol.symbolFile != symbolUri.path) 292 { 293 return; 294 } 295 296 if (symbol.name.data.matchFirst(query)) 297 { 298 auto location = new Location(symbolUri, 299 Document[symbolUri].wordRangeAtByte(symbol.location)); 300 result.insert(new SymbolInformation(symbol.name, 301 symbolKinds[symbol.kind], location, containerName.nullable)); 302 } 303 304 foreach (s; symbol.getPartsByName(internString(null))) 305 { 306 collectSymbolInformations(symbolUri, s, symbol.name); 307 } 308 } 309 310 static Uri[] getModuleUris(ModuleCache* cache) 311 { 312 import std.file : SpanMode, dirEntries; 313 314 auto result = appender!(Uri[]); 315 316 foreach (rootPath; cache.getImportPaths()) 317 { 318 foreach (entry; dirEntries(rootPath, "*.{d,di}", SpanMode.breadth)) 319 { 320 result ~= Uri.fromPath(entry.name); 321 } 322 } 323 324 return result.data; 325 } 326 327 foreach (cache; _workspaceCaches.byValue) 328 { 329 auto moduleUris = uri is null ? getModuleUris(cache) : [uri]; 330 331 foreach (moduleUri; moduleUris) 332 { 333 auto moduleSymbol = cache.cacheModule(moduleUri.path); 334 335 if (moduleSymbol !is null) 336 { 337 const closed = openDocument(moduleUri); 338 339 foreach (symbol; moduleSymbol.getPartsByName(internString(null))) 340 { 341 collectSymbolInformations(moduleUri, symbol); 342 } 343 344 closeDocument(moduleUri, closed); 345 } 346 } 347 } 348 349 return result.array; 350 } 351 352 CompletionItem[] completion(Uri uri, Position position) 353 { 354 import dcd.common.messages : AutocompleteResponse; 355 import dcd.server.autocomplete : complete; 356 import std.algorithm : chunkBy; 357 358 logger.infof("Getting completions for %s at position %s,%s", uri.path, 359 position.line, position.character); 360 361 auto request = getPreparedRequest(uri, position); 362 request.kind = RequestKind.autocomplete; 363 364 static bool compareCompletionsLess(AutocompleteResponse.Completion a, 365 AutocompleteResponse.Completion b) 366 { 367 //dfmt off 368 return a.identifier < b.identifier ? true 369 : a.identifier > b.identifier ? false 370 : a.symbolFilePath < b.symbolFilePath ? true 371 : a.symbolFilePath > b.symbolFilePath ? false 372 : a.symbolLocation < b.symbolLocation; 373 //dfmt on 374 } 375 376 static bool compareCompletionsEqual(AutocompleteResponse.Completion a, 377 AutocompleteResponse.Completion b) 378 { 379 return a.symbolFilePath == b.symbolFilePath && a.symbolLocation == b.symbolLocation; 380 } 381 382 return chain(_workspaceCaches.byValue, _libraryCaches.byValue).map!( 383 cache => complete(request, *cache).completions) 384 .reduce!q{a ~ b} 385 .sort!compareCompletionsLess 386 .uniq!compareCompletionsEqual 387 .chunkBy!q{a.identifier == b.identifier} 388 .map!((resultGroup) { 389 import std.uni : toLower; 390 391 auto firstResult = resultGroup.front; 392 auto item = new CompletionItem(firstResult.identifier); 393 item.kind = completionKinds[firstResult.kind.to!CompletionKind]; 394 item.detail = firstResult.definition; 395 396 string[][] data; 397 398 foreach (res; resultGroup) 399 { 400 if (res.documentation.length > 0 && res.documentation.toLower() != "ditto") 401 { 402 data ~= [res.definition, res.documentation]; 403 } 404 } 405 406 if (data.length > 0) 407 { 408 item.data = JSONValue(data); 409 } 410 411 return item; 412 }) 413 .array; 414 } 415 416 CompletionItem completionResolve(CompletionItem item) 417 { 418 if (!item.data.isNull) 419 { 420 item.documentation = getDocumentation( 421 item.data.array.map!q{ [a[0].str, a[1].str] }.array); 422 item.data.nullify(); 423 } 424 425 return item; 426 } 427 428 Hover hover(Uri uri, Position position) 429 { 430 import dcd.server.autocomplete : getDoc; 431 import std.algorithm : filter; 432 433 logger.infof("Getting documentation for %s at position %s,%s", 434 uri.path, position.line, position.character); 435 436 auto request = getPreparedRequest(uri, position); 437 request.kind = RequestKind.doc; 438 auto completions = getRelevantCaches(uri).map!(cache => getDoc(request, 439 *cache).completions) 440 .reduce!q{a ~ b} 441 .map!q{a.documentation} 442 .filter!q{a.length > 0} 443 .array 444 .sort().uniq(); 445 446 return completions.empty ? null 447 : new Hover(getDocumentation(completions.map!q{ ["", a] }.array)); 448 } 449 450 Location definition(Uri uri, Position position) 451 { 452 import dcd.common.messages : AutocompleteResponse; 453 import dcd.server.autocomplete : findDeclaration; 454 import std.algorithm : find; 455 456 logger.infof("Finding declaration for %s at position %s,%s", uri.path, 457 position.line, position.character); 458 459 auto request = getPreparedRequest(uri, position); 460 request.kind = RequestKind.symbolLocation; 461 AutocompleteResponse[] results; 462 463 foreach (cache; getRelevantCaches(uri)) 464 { 465 results ~= findDeclaration(request, *cache); 466 } 467 468 results = results.find!q{a.symbolFilePath.length > 0}.array; 469 470 if (results.length == 0) 471 { 472 return null; 473 } 474 475 auto resultUri = results[0].symbolFilePath == "stdin" ? uri 476 : Uri.fromPath(results[0].symbolFilePath); 477 openDocument(resultUri); 478 479 return new Location(resultUri, 480 Document[resultUri].wordRangeAtByte(results[0].symbolLocation)); 481 } 482 483 DocumentHighlight[] highlight(Uri uri, Position position) 484 { 485 import dcd.server.autocomplete.localuse : findLocalUse; 486 import dls.protocol.interfaces : DocumentHighlightKind; 487 488 logger.infof("Highlighting usages for %s at position %s,%s", uri.path, 489 position.line, position.character); 490 491 static bool highlightLess(in DocumentHighlight a, in DocumentHighlight b) 492 { 493 return a.range.start.line < b.range.start.line 494 || (a.range.start.line == b.range.start.line 495 && a.range.start.character < b.range.start.character); 496 } 497 498 auto request = getPreparedRequest(uri, position); 499 request.kind = RequestKind.localUse; 500 auto result = new RedBlackTree!(DocumentHighlight, highlightLess, false); 501 502 foreach (cache; getRelevantCaches(uri)) 503 { 504 auto localUse = findLocalUse(request, *cache); 505 result.insert(localUse.completions.map!((res) => new DocumentHighlight( 506 Document[uri].wordRangeAtByte(res.symbolLocation), (res.symbolLocation == localUse.symbolLocation 507 ? DocumentHighlightKind.write : DocumentHighlightKind.text).nullable))); 508 } 509 510 return result.array; 511 } 512 513 package void importDirectories(bool isLibrary)(string root, string[] paths, bool refresh = false) 514 { 515 import dsymbol.modulecache : ASTAllocator; 516 import std.algorithm : canFind; 517 518 logger.infof(`Importing into cache "%s": %s`, root, paths); 519 520 static if (isLibrary) 521 { 522 alias caches = _libraryCaches; 523 } 524 else 525 { 526 alias caches = _workspaceCaches; 527 } 528 529 if (refresh && (root in caches)) 530 { 531 caches.remove(root); 532 } 533 534 if (!(root in caches)) 535 { 536 caches[root] = new ModuleCache(new ASTAllocator()); 537 } 538 539 foreach (path; paths) 540 { 541 if (!caches[root].getImportPaths().canFind(path)) 542 { 543 caches[root].addImportPaths([path]); 544 } 545 } 546 } 547 548 private MarkupContent getDocumentation(string[][] detailsAndDocumentations) 549 { 550 import arsd.htmltotext : htmlToText; 551 import ddoc : Lexer, expand; 552 import dls.protocol.definitions : MarkupKind; 553 import std.algorithm : all; 554 import std.net.curl : CurlException; 555 import std.regex : regex, split; 556 557 try 558 { 559 if (macros.keys.length == 0 && _macroTasks.all!q{a.done}) 560 { 561 foreach (macroTask; _macroTasks) 562 { 563 foreach (line; macroTask.yieldForce()) 564 { 565 auto result = matchFirst(line, `(\w+)\s*=\s*(.*)`); 566 567 if (result.length > 0) 568 { 569 macros[result[1].to!string] = result[2].to!string; 570 } 571 } 572 } 573 } 574 } 575 catch (CurlException e) 576 { 577 logger.error("Could not fetch macros"); 578 macros["_"] = ""; 579 } 580 581 auto result = appender!string; 582 bool putSeparator; 583 584 foreach (dad; detailsAndDocumentations) 585 { 586 if (putSeparator) 587 { 588 result ~= "\n\n---\n\n"; 589 } 590 else 591 { 592 putSeparator = true; 593 } 594 595 auto detail = dad[0]; 596 auto documentation = dad[1]; 597 auto content = documentation.split(regex(`\n-+(\n|$)`)) 598 .map!(chunk => chunk.replace(`\n`, " ")); 599 bool isExample; 600 601 if (detail.length > 0 && detailsAndDocumentations.length > 1) 602 { 603 result ~= "### "; 604 result ~= detail; 605 result ~= "\n\n"; 606 } 607 608 foreach (chunk; content) 609 { 610 if (isExample) 611 { 612 result ~= "```d\n"; 613 result ~= chunk; 614 result ~= "\n```\n"; 615 } 616 else 617 { 618 auto html = expand(Lexer(chunk), macros).replace(`<i>`, ``) 619 .replace(`</i>`, ``).replace(`*`, `\*`).replace(`_`, `\_`); 620 result ~= htmlToText(html); 621 result ~= '\n'; 622 } 623 624 isExample = !isExample; 625 } 626 } 627 628 return new MarkupContent(MarkupKind.markdown, result.data); 629 } 630 631 private static AutocompleteRequest getPreparedRequest(Uri uri, Position position) 632 { 633 auto request = AutocompleteRequest(); 634 auto document = Document[uri]; 635 636 request.fileName = uri.path; 637 request.sourceCode = cast(ubyte[]) document.toString(); 638 request.cursorPosition = document.byteAtPosition(position); 639 640 return request; 641 } 642 643 private static Dub getDub(Uri uri) 644 { 645 import std.file : isFile; 646 647 auto d = new Dub(isFile(uri.path) ? dirName(uri.path) : uri.path); 648 d.loadPackage(); 649 return d; 650 } 651 652 private static bool openDocument(Uri docUri) 653 { 654 auto closed = Document[docUri] is null; 655 656 if (closed) 657 { 658 auto doc = new TextDocumentItem(); 659 doc.uri = docUri; 660 doc.languageId = "d"; 661 doc.text = readText(docUri.path); 662 Document.open(doc); 663 } 664 665 return closed; 666 } 667 668 private static void closeDocument(Uri docUri, bool wasClosed) 669 { 670 import dls.protocol.definitions : TextDocumentIdentifier; 671 672 if (wasClosed) 673 { 674 auto docIdentifier = new TextDocumentIdentifier(); 675 docIdentifier.uri = docUri; 676 Document.close(docIdentifier); 677 } 678 } 679 }