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 }