1 /*
2  *Copyright (C) 2018 Laurent Tréguier
3  *
4  *This file is part of DLS.
5  *
6  *DLS is free software: you can redistribute it and/or modify
7  *it under the terms of the GNU General Public License as published by
8  *the Free Software Foundation, either version 3 of the License, or
9  *(at your option) any later version.
10  *
11  *DLS is distributed in the hope that it will be useful,
12  *but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *GNU General Public License for more details.
15  *
16  *You should have received a copy of the GNU General Public License
17  *along with DLS.  If not, see <http://www.gnu.org/licenses/>.
18  *
19  */
20 
21 module dls.tools.symbol_tool;
22 
23 import dls.protocol.interfaces : CompletionItemKind, SymbolKind,
24     SymbolInformation;
25 import dls.tools.tool : Tool;
26 import dls.util.uri : Uri;
27 import dparse.ast;
28 import dsymbol.symbol : CompletionKind;
29 import std.container : RedBlackTree;
30 
31 int compareLocations(inout(SymbolInformation) s1, inout(SymbolInformation) s2)
32 {
33     //dfmt off
34     return s1.location.uri < s2.location.uri ? -1
35         : s1.location.uri > s2.location.uri ? 1
36         : s1.location.range.start.line < s2.location.range.start.line ? -1
37         : s1.location.range.start.line > s2.location.range.start.line ? 1
38         : s1.location.range.start.character < s2.location.range.start.character ? -1
39         : s1.location.range.start.character > s2.location.range.start.character ? 1
40         : 0;
41     //dfmt on
42 }
43 
44 alias SymbolInformationTree = RedBlackTree!(SymbolInformation, compareLocations, true);
45 
46 private string[string] macros;
47 private CompletionItemKind[CompletionKind] completionKinds;
48 private SymbolKind[CompletionKind] symbolKinds;
49 
50 shared static this()
51 {
52     import dub.internal.vibecompat.core.log : LogLevel, setLogLevel;
53 
54     setLogLevel(LogLevel.none);
55 }
56 
57 static this()
58 {
59     //dfmt off
60     macros = [
61         "_"             : "",
62         "B"             : "**$0**",
63         "I"             : "_$0_",
64         "U"             : "$0",
65         "P"             : "\n\n$0",
66         "DL"            : "$0",
67         "DT"            : "$0",
68         "DD"            : "$0",
69         "TABLE"         : "$0",
70         "TR"            : "$0",
71         "TH"            : "$0",
72         "TD"            : "$0",
73         "OL"            : "\n\n$0",
74         "UL"            : "\n\n$0",
75         "LI"            : "- $0",
76         "BIG"           : "$0",
77         "SMALL"         : "$0",
78         "BR"            : "\n\n$0",
79         "LINK"          : "[$0]($0)",
80         "LINK2"         : "[$1]($+)",
81         "RED"           : "$0",
82         "BLUE"          : "$0",
83         "GREEN"         : "$0",
84         "YELLOW"        : "$0",
85         "BLACK"         : "$0",
86         "WHITE"         : "$0",
87         "D_CODE"        : "$0",
88         "D_INLINE_CODE" : "$0",
89         "LF"            : "\n",
90         "LPAREN"        : "(",
91         "RPAREN"        : ")",
92         "BACKTICK"      : "`",
93         "DOLLAR"        : "$",
94         "DDOC"          : "$0",
95         "BIGOH"         : "O($0)",
96         "D"             : "$0",
97         "D_COMMENT"     : "$0",
98         "D_STRING"      : "\"$0\"",
99         "D_KEYWORD"     : "$0",
100         "D_PSYMBOL"     : "$0",
101         "D_PARAM"       : "$0",
102         "LREF"          : "$0",
103         "REF"           : "$0",
104         "REF1"          : "$0",
105         "MREF"          : "$0",
106         "MREF1"         : "$0"
107     ];
108 
109     completionKinds = [
110         CompletionKind.className            : CompletionItemKind.class_,
111         CompletionKind.interfaceName        : CompletionItemKind.interface_,
112         CompletionKind.structName           : CompletionItemKind.struct_,
113         CompletionKind.unionName            : CompletionItemKind.interface_,
114         CompletionKind.variableName         : CompletionItemKind.variable,
115         CompletionKind.memberVariableName   : CompletionItemKind.field,
116         CompletionKind.keyword              : CompletionItemKind.keyword,
117         CompletionKind.functionName         : CompletionItemKind.function_,
118         CompletionKind.enumName             : CompletionItemKind.enum_,
119         CompletionKind.enumMember           : CompletionItemKind.enumMember,
120         CompletionKind.packageName          : CompletionItemKind.folder,
121         CompletionKind.moduleName           : CompletionItemKind.module_,
122         CompletionKind.aliasName            : CompletionItemKind.variable,
123         CompletionKind.templateName         : CompletionItemKind.function_,
124         CompletionKind.mixinTemplateName    : CompletionItemKind.function_
125     ];
126 
127     symbolKinds = [
128         CompletionKind.className            : SymbolKind.class_,
129         CompletionKind.interfaceName        : SymbolKind.interface_,
130         CompletionKind.structName           : SymbolKind.struct_,
131         CompletionKind.unionName            : SymbolKind.interface_,
132         CompletionKind.variableName         : SymbolKind.variable,
133         CompletionKind.memberVariableName   : SymbolKind.field,
134         CompletionKind.keyword              : SymbolKind.constant,
135         CompletionKind.functionName         : SymbolKind.function_,
136         CompletionKind.enumName             : SymbolKind.enum_,
137         CompletionKind.enumMember           : SymbolKind.enumMember,
138         CompletionKind.packageName          : SymbolKind.package_,
139         CompletionKind.moduleName           : SymbolKind.module_,
140         CompletionKind.aliasName            : SymbolKind.variable,
141         CompletionKind.templateName         : SymbolKind.function_,
142         CompletionKind.mixinTemplateName    : SymbolKind.function_
143     ];
144     //dfmt on
145 }
146 
147 void useCompatCompletionItemKinds(CompletionItemKind[] items = [])
148 {
149     import std.algorithm : canFind;
150 
151     //dfmt off
152     immutable map = [
153         CompletionKind.structName  : CompletionItemKind.class_,
154         CompletionKind.enumMember  : CompletionItemKind.field,
155         CompletionKind.packageName : CompletionItemKind.module_
156     ];
157     //dfmt on
158 
159     foreach (ck, cik; map)
160     {
161         if (!items.canFind(completionKinds[ck]))
162         {
163             completionKinds[ck] = cik;
164         }
165     }
166 }
167 
168 void useCompatSymbolKinds(SymbolKind[] symbols = [])
169 {
170     import std.algorithm : canFind;
171 
172     //dfmt off
173     immutable map = [
174         CompletionKind.structName : SymbolKind.class_,
175         CompletionKind.enumMember : SymbolKind.field
176     ];
177     //dfmt on
178 
179     foreach (ck, sk; map)
180     {
181         if (!symbols.canFind(symbolKinds[ck]))
182         {
183             symbolKinds[ck] = sk;
184         }
185     }
186 }
187 
188 class SymbolTool : Tool
189 {
190     import dcd.common.messages : AutocompleteRequest, RequestKind;
191     import dls.protocol.definitions : Location, MarkupContent, Position, Range,
192         WorkspaceEdit;
193     import dls.protocol.interfaces : CompletionItem, DocumentHighlight,
194         DocumentSymbol, Hover;
195     import dsymbol.modulecache : ASTAllocator, ModuleCache;
196     import dub.dub : Dub;
197 
198     private static SymbolTool _instance;
199 
200     static void initialize()
201     {
202         _instance = new SymbolTool();
203         _instance.importDirectories(defaultImportPaths);
204         addConfigHook(() {
205             _instance.importDirectories(Tool.configuration.symbol.importPaths);
206         });
207     }
208 
209     static void shutdown()
210     {
211         destroy(_instance);
212     }
213 
214     @property static SymbolTool instance()
215     {
216         return _instance;
217     }
218 
219     version (Windows)
220     {
221         @property private static string[] _compilerConfigPaths()
222         {
223             import std.algorithm : splitter;
224             import std.file : exists;
225             import std.path : buildNormalizedPath;
226             import std.process : environment;
227 
228             foreach (path; splitter(environment["PATH"], ';'))
229             {
230                 if (exists(buildNormalizedPath(path, "dmd.exe")))
231                 {
232                     return [buildNormalizedPath(path, "sc.ini")];
233                 }
234             }
235 
236             return [];
237         }
238     }
239     else version (Posix)
240     {
241         //dfmt off
242         private static immutable _compilerConfigPaths = [
243             "/Library/D/dmd/bin/dmd.conf",
244             "/etc/dmd.conf",
245             "/usr/local/etc/dmd.conf",
246             "/usr/local/bin/dmd.conf",
247             "/etc/ldc2.conf",
248             "/usr/local/etc/ldc2.conf",
249             "/home/linuxbrew/.linuxbrew/etc/dmd.conf"
250         ];
251         //dfmt on
252     }
253     else
254     {
255         private static immutable string[] _compilerConfigPaths;
256     }
257 
258     private string[string][string] _workspaceDependencies;
259     private ASTAllocator _allocator;
260     private ModuleCache _cache;
261 
262     @property ref ModuleCache cache()
263     {
264         return _cache;
265     }
266 
267     @property private static string[] defaultImportPaths()
268     {
269         import std.algorithm : each, filter, sort, splitter, uniq;
270         import std.array : array, replace;
271         import std.conv : to;
272         import std.file : FileException, exists, readText;
273         import std.path : asNormalizedPath, buildNormalizedPath, dirName;
274         import std.process : environment;
275         import std.regex : matchAll;
276 
277         string[] paths;
278 
279         foreach (confPath; _compilerConfigPaths)
280         {
281             if (exists(confPath))
282             {
283                 try
284                 {
285                     readText(confPath).matchAll(`-I[^\s"]+`)
286                         .each!(m => paths ~= m.hit[2 .. $].replace("%@P%",
287                                 confPath.dirName).asNormalizedPath().to!string);
288                     break;
289                 }
290                 catch (FileException e)
291                 {
292                     // File doesn't exist or could't be read
293                 }
294             }
295         }
296 
297         version (linux)
298         {
299             import std.algorithm : map;
300 
301             if (paths.length == 0)
302             {
303                 foreach (path; ["/snap", "/var/lib/snapd/snap"])
304                 {
305                     const dmdSnapPath = buildNormalizedPath(path, "dmd");
306                     const ldcSnapIncludePath = buildNormalizedPath(path,
307                             "ldc2", "current", "include", "d");
308 
309                     if (exists(dmdSnapPath))
310                     {
311                         paths = ["druntime", "phobos"].map!(end => buildNormalizedPath(dmdSnapPath,
312                                 "current", "import", end)).array;
313                         break;
314                     }
315                     else if (exists(ldcSnapIncludePath))
316                     {
317                         paths = [ldcSnapIncludePath];
318                         break;
319                     }
320                 }
321             }
322         }
323 
324         version (Windows)
325         {
326             const pathSep = ';';
327             const ldc = "ldc2.exe";
328         }
329         else
330         {
331             const pathSep = ':';
332             const ldc = "ldc2";
333         }
334 
335         if (paths.length == 0)
336         {
337             foreach (path; splitter(environment["PATH"], pathSep))
338             {
339                 if (exists(buildNormalizedPath(path, ldc)))
340                 {
341                     paths = [buildNormalizedPath(path, "..", "import")];
342                 }
343             }
344         }
345 
346         return paths.sort().uniq().filter!exists.array;
347     }
348 
349     this()
350     {
351         _allocator = new ASTAllocator();
352         _cache = ModuleCache(_allocator);
353     }
354 
355     void importPath(Uri uri)
356     {
357         import dls.protocol.messages.window : Util;
358         import dls.util.constants : Tr;
359         import dub.platform : BuildPlatform;
360         import std.algorithm : map;
361         import std.array : array;
362         import std.path : baseName, buildNormalizedPath;
363 
364         auto d = getDub(uri);
365         string[string] workspaceDeps;
366         auto buildSettingsList = d.project.rootPackage.recipe.buildSettings
367             ~ d.project.rootPackage.recipe.configurations.map!q{a.buildSettings}.array;
368 
369         foreach (buildSettings; buildSettingsList)
370         {
371             foreach (depName, depVersion; buildSettings.dependencies)
372             {
373                 workspaceDeps[depName] = depVersion.toString();
374             }
375         }
376 
377         if (uri.path in _workspaceDependencies && _workspaceDependencies[uri.path] != workspaceDeps)
378         {
379             auto id = Util.sendMessageRequest(Tr.app_upgradeSelections,
380                     [Tr.app_upgradeSelections_upgrade], [d.projectName]);
381             Util.bindMessageToRequestId(id, Tr.app_upgradeSelections, uri);
382         }
383 
384         _workspaceDependencies[uri.path] = workspaceDeps;
385         auto packages = [d.project.rootPackage];
386 
387         foreach (sub; d.project.rootPackage.subPackages)
388         {
389             auto p = d.project.packageManager.getSubPackage(d.project.rootPackage,
390                     baseName(sub.path), true);
391 
392             if (p !is null)
393             {
394                 packages ~= p;
395             }
396         }
397 
398         foreach (p; packages)
399         {
400             const desc = p.describe(BuildPlatform.any, null, null);
401             importDirectories(desc.importPaths.length > 0
402                     ? desc.importPaths.map!(path => buildNormalizedPath(p.path.toString(),
403                         path)).array : [uri.path]);
404             importSelections(Uri.fromPath(desc.path));
405         }
406     }
407 
408     void importSelections(Uri uri)
409     {
410         import std.algorithm : map, reduce;
411         import std.array : array;
412         import std.path : buildNormalizedPath;
413 
414         const d = getDub(uri);
415 
416         foreach (dep; d.project.dependencies)
417         {
418             auto paths = reduce!(q{a ~ b})(cast(string[])[],
419                     dep.recipe.buildSettings.sourcePaths.values);
420             importDirectories(paths.map!(path => buildNormalizedPath(dep.path.toString(),
421                     path)).array);
422         }
423     }
424 
425     void clearPath(Uri uri)
426     {
427         // import dls.util.logger : logger;
428 
429         // logger.infof("Clearing imports from %s", uri.path);
430         // Implement ModuleCache.clear() in DCD
431         _workspaceDependencies.remove(uri.path);
432     }
433 
434     void upgradeSelections(Uri uri)
435     {
436         import dls.util.logger : logger;
437         import std.concurrency : spawn;
438         import std.path : dirName;
439 
440         logger.infof("Upgrading dependencies from %s", dirName(uri.path));
441 
442         spawn((string uriString) {
443             import dls.protocol.interfaces.dls : TranslationParams;
444             import dls.protocol.jsonrpc : send;
445             import dls.protocol.messages.methods : Dls;
446             import dls.protocol.messages.window : Util;
447             import dls.util.constants : Tr;
448             import dub.dub : UpgradeOptions;
449 
450             send(Dls.UpgradeSelections.didStart,
451                 new TranslationParams(Tr.app_upgradeSelections_upgrading));
452 
453             try
454             {
455                 getDub(new Uri(uriString)).upgrade(UpgradeOptions.upgrade | UpgradeOptions.select);
456             }
457             catch (Exception e)
458             {
459                 Util.sendMessage(Tr.app_upgradeSelections_error, [e.msg]);
460             }
461             finally
462             {
463                 send(Dls.UpgradeSelections.didStop);
464             }
465         }, uri.toString());
466     }
467 
468     SymbolInformation[] symbol(string query)
469     {
470         import dls.util.document : Document;
471         import dls.util.logger : logger;
472         import dsymbol.string_interning : internString;
473         import dsymbol.symbol : DSymbol;
474         import std.algorithm : any, canFind, map, startsWith;
475         import std.array : appender, array;
476         import std.file : SpanMode, dirEntries;
477         import std.regex : matchFirst, regex;
478 
479         logger.infof(`Fetching symbols from workspace with query "%s"`, query);
480 
481         auto result = new SymbolInformationTree();
482 
483         void collectSymbolInformations(Uri symbolUri, const(DSymbol)* symbol,
484                 string containerName = "")
485         {
486             import std.typecons : nullable;
487 
488             if (symbol.symbolFile != symbolUri.path)
489             {
490                 return;
491             }
492 
493             auto name = symbol.name == "*constructor*" ? "this" : symbol.name;
494 
495             if (name.matchFirst(regex(query, "i")))
496             {
497                 auto location = new Location(symbolUri, Document.get(symbolUri)
498                         .wordRangeAtByte(symbol.location));
499                 result.insert(new SymbolInformation(name,
500                         symbolKinds[symbol.kind], location, containerName.nullable));
501             }
502 
503             foreach (s; symbol.getPartsByName(internString(null)))
504             {
505                 collectSymbolInformations(symbolUri, s, name);
506             }
507         }
508 
509         auto moduleUris = appender!(Uri[]);
510 
511         foreach (path; _cache.getImportPaths())
512         {
513             if (_workspaceDependencies.byKey.any!(w => path.startsWith(w)))
514             {
515                 foreach (entry; dirEntries(path, "*.{d,di}", SpanMode.breadth))
516                 {
517                     moduleUris ~= Uri.fromPath(entry.name);
518                 }
519             }
520         }
521 
522         foreach (moduleUri; moduleUris.data)
523         {
524             if (Document.uris.map!q{a.path}.canFind(moduleUri.path))
525             {
526                 result.insert(symbol!SymbolInformation(moduleUri, query));
527                 continue;
528             }
529 
530             auto moduleSymbol = _cache.cacheModule(moduleUri.path);
531 
532             if (moduleSymbol !is null)
533             {
534                 foreach (symbol; moduleSymbol.getPartsByName(internString(null)))
535                 {
536                     collectSymbolInformations(moduleUri, symbol);
537                 }
538             }
539         }
540 
541         return result.array;
542     }
543 
544     SymbolType[] symbol(SymbolType)(Uri uri, string query)
545             if (is(SymbolType == SymbolInformation) || is(SymbolType == DocumentSymbol))
546     {
547         import dls.util.document : Document;
548         import dls.util.logger : logger;
549         import dparse.lexer : LexerConfig, StringBehavior, StringCache,
550             getTokensForParser;
551         import dparse.parser : parseModule;
552         import dparse.rollback_allocator : RollbackAllocator;
553         import std.functional : toDelegate;
554 
555         logger.infof("Fetching symbols from %s", uri.path);
556 
557         static void doNothing(string, size_t, size_t, string, bool)
558         {
559         }
560 
561         auto stringCache = StringCache(StringCache.defaultBucketCount);
562         auto tokens = getTokensForParser(Document.get(uri).toString(),
563                 LexerConfig(uri.path, StringBehavior.source), &stringCache);
564         RollbackAllocator ra;
565         const mod = parseModule(tokens, uri.path, &ra, toDelegate(&doNothing));
566         auto visitor = new SymbolVisitor!SymbolType(uri, query);
567         visitor.visit(mod);
568         return visitor.result;
569     }
570 
571     CompletionItem[] completion(Uri uri, Position position)
572     {
573         import dcd.common.messages : AutocompleteResponse, CompletionType;
574         import dcd.server.autocomplete : complete;
575         import dls.util.logger : logger;
576         import std.algorithm : chunkBy, map, sort, uniq;
577         import std.array : array;
578         import std.conv : to;
579         import std.json : JSONValue;
580 
581         logger.infof("Fetching completions for %s at position %s,%s", uri.path,
582                 position.line, position.character);
583 
584         auto request = getPreparedRequest(uri, position, RequestKind.autocomplete);
585         static bool compareCompletionsLess(AutocompleteResponse.Completion a,
586                 AutocompleteResponse.Completion b)
587         {
588             //dfmt off
589             return a.identifier < b.identifier ? true
590                 : a.identifier > b.identifier ? false
591                 : a.symbolFilePath < b.symbolFilePath ? true
592                 : a.symbolFilePath > b.symbolFilePath ? false
593                 : a.symbolLocation < b.symbolLocation;
594             //dfmt on
595         }
596 
597         static bool compareCompletionsEqual(AutocompleteResponse.Completion a,
598                 AutocompleteResponse.Completion b)
599         {
600             return a.symbolFilePath == b.symbolFilePath && a.symbolLocation == b.symbolLocation;
601         }
602 
603         auto result = complete(request, _cache);
604 
605         if (result.completionType != CompletionType.identifiers)
606         {
607             return [];
608         }
609 
610         return result.completions
611             .sort!compareCompletionsLess
612             .uniq!compareCompletionsEqual
613             .chunkBy!q{a.identifier == b.identifier}
614             .map!((resultGroup) {
615                 import std.uni : toLower;
616 
617                 auto firstResult = resultGroup.front;
618                 auto item = new CompletionItem(firstResult.identifier);
619                 item.kind = completionKinds[firstResult.kind.to!CompletionKind];
620                 item.detail = firstResult.definition;
621 
622                 string[][] data;
623 
624                 foreach (res; resultGroup)
625                 {
626                     if (res.documentation.length > 0 && res.documentation.toLower() != "ditto")
627                     {
628                         data ~= [res.definition, res.documentation];
629                     }
630                 }
631 
632                 if (data.length > 0)
633                 {
634                     item.data = JSONValue(data);
635                 }
636 
637                 return item;
638             })
639             .array;
640     }
641 
642     CompletionItem completionResolve(CompletionItem item)
643     {
644         import std.algorithm : map;
645         import std.array : array;
646 
647         if (!item.data.isNull)
648         {
649             item.documentation = getDocumentation(
650                     item.data.array.map!q{ [a[0].str, a[1].str] }.array);
651             item.data.nullify();
652         }
653 
654         return item;
655     }
656 
657     Hover hover(Uri uri, Position position)
658     {
659         import dcd.server.autocomplete : getDoc;
660         import dls.util.logger : logger;
661         import std.algorithm : filter, map, sort, uniq;
662         import std.array : array;
663 
664         logger.infof("Fetching documentation for %s at position %s,%s",
665                 uri.path, position.line, position.character);
666 
667         auto request = getPreparedRequest(uri, position, RequestKind.doc);
668         auto result = getDoc(request, _cache);
669         auto completions = result.completions
670             .map!q{a.documentation}
671             .filter!q{a.length > 0}
672             .array
673             .sort().uniq();
674 
675         return completions.empty ? null
676             : new Hover(getDocumentation(completions.map!q{ ["", a] }.array));
677     }
678 
679     Location[] definition(Uri uri, Position position)
680     {
681         import dcd.common.messages : CompletionType;
682         import dcd.server.autocomplete.util : getSymbolsForCompletion;
683         import dls.util.document : Document;
684         import dls.util.logger : logger;
685         import dparse.lexer : StringCache;
686         import dparse.rollback_allocator : RollbackAllocator;
687 
688         logger.infof("Finding declarations for %s at position %s,%s", uri.path,
689                 position.line, position.character);
690 
691         auto request = getPreparedRequest(uri, position, RequestKind.symbolLocation);
692         auto stringCache = StringCache(StringCache.defaultBucketCount);
693         RollbackAllocator ra;
694         auto currentFileStuff = getSymbolsForCompletion(request,
695                 CompletionType.location, _allocator, &ra, stringCache, cache);
696 
697         scope (exit)
698         {
699             currentFileStuff.destroy();
700         }
701 
702         Location[] result;
703 
704         foreach (symbol; currentFileStuff.symbols)
705         {
706             auto symbolUri = symbol.symbolFile == "stdin" ? uri : Uri.fromPath(symbol.symbolFile);
707             auto document = Document.get(symbolUri);
708             request.fileName = symbolUri.path;
709             request.sourceCode = cast(ubyte[]) document.toString();
710             request.cursorPosition = symbol.location + 1;
711             auto stuff = getSymbolsForCompletion(request,
712                     CompletionType.location, _allocator, &ra, stringCache, cache);
713 
714             scope (exit)
715             {
716                 stuff.destroy();
717             }
718 
719             foreach (s; stuff.symbols)
720             {
721                 result ~= new Location(symbolUri, document.wordRangeAtByte(s.location));
722             }
723         }
724 
725         return result;
726     }
727 
728     Location[] typeDefinition(Uri uri, Position position)
729     {
730         import dcd.common.messages : CompletionType;
731         import dcd.server.autocomplete.util : getSymbolsForCompletion;
732         import dls.util.document : Document;
733         import dls.util.logger : logger;
734         import dparse.lexer : StringCache;
735         import dparse.rollback_allocator : RollbackAllocator;
736         import std.algorithm : filter, map, uniq;
737 
738         logger.infof("Finding type declaration for %s at position %s,%s",
739                 uri.path, position.line, position.character);
740 
741         auto request = getPreparedRequest(uri, position, RequestKind.symbolLocation);
742         auto stringCache = StringCache(StringCache.defaultBucketCount);
743         RollbackAllocator ra;
744         auto stuff = getSymbolsForCompletion(request, CompletionType.location,
745                 _allocator, &ra, stringCache, cache);
746 
747         scope (exit)
748         {
749             stuff.destroy();
750         }
751 
752         Location[] result;
753 
754         foreach (type; stuff.symbols
755                 .map!q{a.type}
756                 .filter!q{a !is null && a.symbolFile.length > 0}
757                 .uniq!q{a.symbolFile == b.symbolFile && a.location == b.location})
758         {
759             auto symbolUri = type.symbolFile == "stdin" ? uri : Uri.fromPath(type.symbolFile);
760             result ~= new Location(symbolUri, Document.get(symbolUri)
761                     .wordRangeAtByte(type.location));
762         }
763 
764         return result;
765     }
766 
767     Location[] references(Uri uri, Position position, bool includeDeclaration)
768     {
769         import dls.util.logger : logger;
770         import std.algorithm : filter, map, reduce;
771         import std.array : array;
772         import std.file : SpanMode, dirEntries;
773         import std.path : globMatch;
774 
775         logger.infof("Finding references for %s at position %s,%s", uri.path,
776                 position.line, position.character);
777 
778         auto workspaceUris = _workspaceDependencies.keys
779             .map!(w => dirEntries(w, SpanMode.depth).map!q{a.name}
780                     .filter!(path => globMatch(path, "*.{d,di}"))
781                     .map!(Uri.fromPath)
782                     .array)
783             .reduce!q{a ~ b};
784         return referencesForFiles(uri, position, workspaceUris, includeDeclaration, false);
785     }
786 
787     DocumentHighlight[] highlight(Uri uri, Position position)
788     {
789         import dls.protocol.interfaces : DocumentHighlightKind;
790         import dls.util.logger : logger;
791         import std.algorithm : any;
792         import std.path : filenameCmp;
793         import std.typecons : nullable;
794 
795         logger.infof("Highlighting usages for %s at position %s,%s", uri.path,
796                 position.line, position.character);
797 
798         auto sources = referencesForFiles(uri, position, [uri], true, true);
799         auto locations = referencesForFiles(uri, position, [uri], true, false);
800         DocumentHighlight[] result;
801 
802         foreach (location; locations)
803         {
804             auto kind = sources.any!(sourceLoc => location.range.start.line == sourceLoc.range.start.line
805                     && location.range.start.character == sourceLoc.range.start.character
806                     && filenameCmp(new Uri(location.uri).path, new Uri(sourceLoc.uri).path) == 0) ? DocumentHighlightKind
807                 .write : DocumentHighlightKind.read;
808             result ~= new DocumentHighlight(location.range, kind.nullable);
809         }
810 
811         return result;
812     }
813 
814     WorkspaceEdit rename(Uri uri, Position position, string newName)
815     {
816         import dls.protocol.definitions : TextDocumentEdit, TextEdit,
817             VersionedTextDocumentIdentifier;
818         import dls.protocol.state : initState;
819         import dls.util.document : Document;
820         import dls.util.logger : logger;
821         import std.typecons : nullable;
822 
823         if ((initState.capabilities.textDocument.isNull || initState.capabilities.textDocument.rename.isNull
824                 || initState.capabilities.textDocument.rename.prepareSupport.isNull
825                 || !initState.capabilities.textDocument.rename.prepareSupport)
826                 && prepareRename(uri, position) is null)
827         {
828             return null;
829         }
830 
831         logger.infof("Renaming symbol for %s at position %s,%s", uri.path,
832                 position.line, position.character);
833 
834         auto refs = references(uri, position, true);
835 
836         if (refs.length == 0)
837         {
838             return null;
839         }
840 
841         TextEdit[][string] changes;
842         TextDocumentEdit[] documentChanges;
843 
844         foreach (reference; refs)
845         {
846             changes[reference.uri] ~= new TextEdit(reference.range, newName);
847         }
848 
849         foreach (documentUri, textEdits; changes)
850         {
851             auto identifier = new VersionedTextDocumentIdentifier(documentUri,
852                     Document.get(new Uri(documentUri)).version_);
853             documentChanges ~= new TextDocumentEdit(identifier, textEdits);
854         }
855 
856         return new WorkspaceEdit(changes.nullable, documentChanges.nullable);
857     }
858 
859     Range prepareRename(Uri uri, Position position)
860     {
861         import dls.util.document : Document;
862         import dls.util.logger : logger;
863         import std.algorithm : any;
864 
865         logger.infof("Preparing symbol rename for %s at position %s,%s",
866                 uri.path, position.line, position.character);
867 
868         auto defs = definition(uri, position);
869         return defs.length == 0
870             || defs.any!(d => getWorkspace(new Uri(d.uri)) is null) ? null
871             : Document.get(uri).wordRangeAtPosition(position);
872     }
873 
874     Uri getWorkspace(in Uri uri)
875     {
876         import std.algorithm : startsWith;
877         import std.array : array;
878         import std.path : buildNormalizedPath, pathSplitter;
879 
880         string[] workspacePathParts;
881 
882         foreach (path; _workspaceDependencies.keys)
883         {
884             auto splitter = pathSplitter(path);
885 
886             if (pathSplitter(uri.path).startsWith(splitter))
887             {
888                 auto pathParts = splitter.array;
889 
890                 if (pathParts.length > workspacePathParts.length)
891                 {
892                     workspacePathParts = pathParts;
893                 }
894             }
895         }
896 
897         return workspacePathParts.length > 0
898             ? Uri.fromPath(buildNormalizedPath(workspacePathParts)) : null;
899     }
900 
901     private void importDirectories(string[] paths)
902     {
903         import dls.util.logger : logger;
904 
905         logger.infof("Importing directories: %s", paths);
906         _cache.addImportPaths(paths);
907     }
908 
909     private Location[] referencesForFiles(Uri uri, Position position, Uri[] files,
910             bool includeDeclaration, bool sources)
911     {
912         import dcd.common.messages : CompletionType;
913         import dcd.server.autocomplete.util : getSymbolsForCompletion;
914         import dls.util.document : Document;
915         import dparse.lexer : LexerConfig, StringBehavior, StringCache, Token,
916             WhitespaceBehavior, getTokensForParser, tok;
917         import dparse.rollback_allocator : RollbackAllocator;
918         import dsymbol.string_interning : internString;
919         import std.range : zip;
920 
921         auto request = getPreparedRequest(uri, position, RequestKind.symbolLocation);
922         auto stringCache = StringCache(StringCache.defaultBucketCount);
923         auto sourceTokens = getTokensForParser(Document.get(uri).toString(),
924                 LexerConfig(uri.path, StringBehavior.compiler, WhitespaceBehavior.skip),
925                 &stringCache);
926         RollbackAllocator ra;
927         auto stuff = getSymbolsForCompletion(request, CompletionType.location,
928                 _allocator, &ra, stringCache, cache);
929 
930         scope (exit)
931         {
932             stuff.destroy();
933         }
934 
935         const(Token)* sourceToken;
936 
937         foreach (i, token; sourceTokens)
938         {
939             if (token.type == tok!"identifier" && request.cursorPosition >= token.index
940                     && request.cursorPosition < token.index + token.text.length)
941             {
942                 sourceToken = &sourceTokens[i];
943                 break;
944             }
945         }
946 
947         if (sourceToken is null)
948         {
949             return null;
950         }
951 
952         size_t[] sourceSymbolLocations;
953         string[] sourceSymbolFiles;
954         Location[] result;
955 
956         foreach (s; stuff.symbols)
957         {
958             sourceSymbolLocations ~= s.location;
959             sourceSymbolFiles ~= s.symbolFile == "stdin" ? uri.path : s.symbolFile;
960         }
961 
962         if (sources)
963         {
964             foreach (sourceLocation, sourceFile; zip(sourceSymbolLocations, sourceSymbolFiles))
965             {
966                 auto sourceUri = Uri.fromPath(sourceFile);
967                 result ~= new Location(sourceUri.toString(),
968                         Document.get(sourceUri).wordRangeAtByte(sourceLocation));
969             }
970 
971             return result;
972         }
973 
974         bool checkFileAndLocation(string file, size_t location)
975         {
976             import std.path : filenameCmp;
977 
978             foreach (sourceLocation, sourceFile; zip(sourceSymbolLocations, sourceSymbolFiles))
979             {
980                 if (location == sourceLocation && filenameCmp(file, sourceFile) == 0)
981                 {
982                     return true;
983                 }
984             }
985 
986             return false;
987         }
988 
989         foreach (fileUri; files)
990         {
991             auto document = Document.get(fileUri);
992             request.fileName = fileUri.path;
993             request.sourceCode = cast(ubyte[]) document.toString();
994             auto tokens = getTokensForParser(request.sourceCode, LexerConfig(fileUri.path,
995                     StringBehavior.compiler, WhitespaceBehavior.skip), &stringCache);
996 
997             foreach (token; tokens)
998             {
999                 if (token.type == tok!"identifier" && token.text == sourceToken.text)
1000                 {
1001                     request.cursorPosition = token.index + 1;
1002                     auto candidateStuff = getSymbolsForCompletion(request,
1003                             CompletionType.location, _allocator, &ra, stringCache, cache);
1004 
1005                     scope (exit)
1006                     {
1007                         candidateStuff.destroy();
1008                     }
1009 
1010                     foreach (candidateSymbol; candidateStuff.symbols)
1011                     {
1012                         if (!includeDeclaration && checkFileAndLocation(fileUri.path, token.index))
1013                         {
1014                             continue;
1015                         }
1016 
1017                         const candidateSymbolFile = candidateSymbol.symbolFile == "stdin"
1018                             ? fileUri.path : candidateSymbol.symbolFile;
1019 
1020                         if (checkFileAndLocation(candidateSymbolFile, candidateSymbol.location))
1021                         {
1022                             result ~= new Location(fileUri.toString(),
1023                                     document.wordRangeAtByte(token.index));
1024                         }
1025                     }
1026                 }
1027             }
1028         }
1029 
1030         return result;
1031     }
1032 
1033     private MarkupContent getDocumentation(string[][] detailsAndDocumentations)
1034     {
1035         import ddoc : Lexer, expand;
1036         import dls.protocol.definitions : MarkupKind;
1037         import std.array : appender, replace;
1038         import std.regex : regex, split;
1039 
1040         auto result = appender!string;
1041         bool putSeparator;
1042 
1043         foreach (dad; detailsAndDocumentations)
1044         {
1045             if (putSeparator)
1046             {
1047                 result ~= "\n\n---\n\n";
1048             }
1049             else
1050             {
1051                 putSeparator = true;
1052             }
1053 
1054             auto detail = dad[0];
1055             auto documentation = dad[1];
1056             auto content = documentation.split(regex(`\n-+(\n|$)`));
1057             bool isExample;
1058 
1059             if (detail.length > 0 && detailsAndDocumentations.length > 1)
1060             {
1061                 result ~= "### ";
1062                 result ~= detail;
1063                 result ~= "\n\n";
1064             }
1065 
1066             foreach (chunk; content)
1067             {
1068                 if (isExample)
1069                 {
1070                     result ~= "```d\n";
1071                     result ~= chunk;
1072                     result ~= "\n```\n";
1073                 }
1074                 else
1075                 {
1076                     result ~= expand(Lexer(chunk.replace("\n", " ")), macros);
1077                     result ~= '\n';
1078                 }
1079 
1080                 isExample = !isExample;
1081             }
1082         }
1083 
1084         return new MarkupContent(MarkupKind.markdown, result.data);
1085     }
1086 
1087     private static AutocompleteRequest getPreparedRequest(Uri uri,
1088             Position position, RequestKind kind)
1089     {
1090         import dls.protocol.jsonrpc : InvalidParamsException;
1091         import dls.util.document : Document;
1092         import std.format : format;
1093 
1094         auto request = AutocompleteRequest();
1095         auto document = Document.get(uri);
1096 
1097         if (!document.validatePosition(position))
1098         {
1099             throw new InvalidParamsException(format!"invalid position: %s %s,%s"(uri,
1100                     position.line, position.character));
1101         }
1102 
1103         request.fileName = uri.path;
1104         request.kind = kind;
1105         request.sourceCode = cast(ubyte[]) document.toString();
1106         request.cursorPosition = document.byteAtPosition(position);
1107 
1108         return request;
1109     }
1110 
1111     private static Dub getDub(Uri uri)
1112     {
1113         import std.file : isFile;
1114         import std.path : dirName;
1115 
1116         auto d = new Dub(isFile(uri.path) ? dirName(uri.path) : uri.path);
1117         d.loadPackage();
1118         return d;
1119     }
1120 }
1121 
1122 private class SymbolVisitor(SymbolType) : ASTVisitor
1123 {
1124     import dls.protocol.definitions : Range;
1125     import dls.protocol.interfaces : DocumentSymbol;
1126     import dls.util.uri : Uri;
1127     import std.typecons : nullable;
1128 
1129     SymbolType[] result;
1130     private Uri _uri;
1131     private string _query;
1132 
1133     static if (is(SymbolType == SymbolInformation))
1134     {
1135         private string container;
1136     }
1137     else
1138     {
1139         private DocumentSymbol container;
1140     }
1141 
1142     this(Uri uri, string query)
1143     {
1144         _uri = uri;
1145         _query = query;
1146     }
1147 
1148     override void visit(const ModuleDeclaration dec)
1149     {
1150         dec.accept(this);
1151     }
1152 
1153     override void visit(const ClassDeclaration dec)
1154     {
1155         visitSymbol(dec, SymbolKind.class_, true, dec.structBody is null ? 0
1156                 : dec.structBody.endLocation);
1157     }
1158 
1159     override void visit(const StructDeclaration dec)
1160     {
1161         visitSymbol(dec, SymbolKind.struct_, true, dec.structBody is null ? 0
1162                 : dec.structBody.endLocation);
1163     }
1164 
1165     override void visit(const InterfaceDeclaration dec)
1166     {
1167         visitSymbol(dec, SymbolKind.interface_, true, dec.structBody is null ? 0
1168                 : dec.structBody.endLocation);
1169     }
1170 
1171     override void visit(const UnionDeclaration dec)
1172     {
1173         visitSymbol(dec, SymbolKind.interface_, true, dec.structBody is null ? 0
1174                 : dec.structBody.endLocation);
1175     }
1176 
1177     override void visit(const EnumDeclaration dec)
1178     {
1179         visitSymbol(dec, SymbolKind.enum_, true, dec.enumBody is null ? 0 : dec
1180                 .enumBody.endLocation);
1181     }
1182 
1183     override void visit(const EnumMember mem)
1184     {
1185         visitSymbol(mem, SymbolKind.enumMember, false);
1186     }
1187 
1188     override void visit(const AnonymousEnumMember mem)
1189     {
1190         visitSymbol(mem, SymbolKind.enumMember, false);
1191     }
1192 
1193     override void visit(const TemplateDeclaration dec)
1194     {
1195         visitSymbol(dec, SymbolKind.function_, true, dec.endLocation);
1196     }
1197 
1198     override void visit(const FunctionDeclaration dec)
1199     {
1200         visitSymbol(dec, SymbolKind.function_, false, getFunctionEndLocation(dec));
1201     }
1202 
1203     override void visit(const Constructor dec)
1204     {
1205         tryInsertFunction(dec, "this");
1206     }
1207 
1208     override void visit(const Destructor dec)
1209     {
1210         tryInsertFunction(dec, "~this");
1211     }
1212 
1213     override void visit(const StaticConstructor dec)
1214     {
1215         tryInsertFunction(dec, "static this");
1216     }
1217 
1218     override void visit(const StaticDestructor dec)
1219     {
1220         tryInsertFunction(dec, "static ~this");
1221     }
1222 
1223     override void visit(const SharedStaticConstructor dec)
1224     {
1225         tryInsertFunction(dec, "shared static this");
1226     }
1227 
1228     override void visit(const SharedStaticDestructor dec)
1229     {
1230         tryInsertFunction(dec, "shared static ~this");
1231     }
1232 
1233     override void visit(const Invariant dec)
1234     {
1235         tryInsert("invariant", SymbolKind.function_, getRange(dec),
1236                 dec.blockStatement.endLocation);
1237     }
1238 
1239     override void visit(const VariableDeclaration dec)
1240     {
1241         foreach (d; dec.declarators)
1242         {
1243             tryInsert(d.name.text, SymbolKind.variable, getRange(d.name));
1244         }
1245 
1246         dec.accept(this);
1247     }
1248 
1249     override void visit(const AutoDeclaration dec)
1250     {
1251         foreach (part; dec.parts)
1252         {
1253             tryInsert(part.identifier.text, SymbolKind.variable, getRange(part.identifier));
1254         }
1255 
1256         dec.accept(this);
1257     }
1258 
1259     override void visit(const Unittest dec)
1260     {
1261     }
1262 
1263     override void visit(const AliasDeclaration dec)
1264     {
1265         if (dec.declaratorIdentifierList !is null)
1266         {
1267             foreach (id; dec.declaratorIdentifierList.identifiers)
1268             {
1269                 tryInsert(id.text, SymbolKind.variable, getRange(id));
1270             }
1271         }
1272 
1273         dec.accept(this);
1274     }
1275 
1276     override void visit(const AliasInitializer dec)
1277     {
1278         visitSymbol(dec, SymbolKind.variable, true);
1279     }
1280 
1281     override void visit(const AliasThisDeclaration dec)
1282     {
1283         tryInsert(dec.identifier.text, SymbolKind.variable, getRange(dec.identifier));
1284         dec.accept(this);
1285     }
1286 
1287     private size_t getFunctionEndLocation(A : ASTNode)(const A dec)
1288     {
1289         size_t endLocation;
1290 
1291         if (dec.functionBody !is null)
1292         {
1293             endLocation = dec.functionBody.bodyStatement !is null
1294                 ? dec.functionBody.bodyStatement.blockStatement.endLocation
1295                 : dec.functionBody.blockStatement.endLocation;
1296         }
1297 
1298         return endLocation;
1299     }
1300 
1301     private void visitSymbol(A : ASTNode)(const A dec, SymbolKind kind,
1302             bool accept, size_t endLocation = 0)
1303     {
1304         tryInsert(dec.name.text.dup, kind, getRange(dec.name), endLocation);
1305 
1306         if (accept)
1307         {
1308             auto oldContainer = container;
1309 
1310             static if (is(SymbolType == SymbolInformation))
1311             {
1312                 container = dec.name.text.dup;
1313             }
1314             else
1315             {
1316                 container = (container is null ? result : container.children)[$ - 1];
1317             }
1318 
1319             dec.accept(this);
1320             container = oldContainer;
1321         }
1322     }
1323 
1324     private Range getRange(T)(T t)
1325     {
1326         import dls.util.document : Document;
1327 
1328         auto document = Document.get(_uri);
1329 
1330         static if (__traits(hasMember, T, "line") && __traits(hasMember, T, "column"))
1331         {
1332             return document.wordRangeAtLineAndByte(t.line - 1, t.column - 1);
1333         }
1334         else
1335         {
1336             return document.wordRangeAtByte(t.index);
1337         }
1338     }
1339 
1340     private void tryInsert(string name, SymbolKind kind, Range range, size_t endLocation = 0)
1341     {
1342         import dls.protocol.definitions : Location, Position;
1343         import dls.util.document : Document;
1344         import std.regex : matchFirst, regex;
1345         import std.typecons : Nullable, nullable;
1346 
1347         if (_query is null || name.matchFirst(regex(_query, "i")))
1348         {
1349             static if (is(SymbolType == SymbolInformation))
1350             {
1351                 result ~= new SymbolInformation(name, kind, new Location(_uri,
1352                         range), container.nullable);
1353             }
1354             else
1355             {
1356                 auto fullRange = endLocation > 0 ? new Range(range.start,
1357                         Document.get(_uri).positionAtByte(endLocation)) : range;
1358                 DocumentSymbol[] children;
1359                 (container is null ? result : container.children) ~= new DocumentSymbol(name,
1360                         Nullable!string(), kind, Nullable!bool(), fullRange,
1361                         range, children.nullable);
1362             }
1363         }
1364     }
1365 
1366     private void tryInsertFunction(A : ASTNode)(const A dec, string name)
1367     {
1368         tryInsert(name, SymbolKind.function_, getRange(dec), getFunctionEndLocation(dec));
1369     }
1370 
1371     alias visit = ASTVisitor.visit;
1372 }