1 module served.types;
2 
3 public import served.backend.lazy_workspaced : LazyWorkspaceD;
4 public import served.lsp.protocol;
5 public import served.lsp.protoext;
6 public import served.lsp.textdocumentmanager;
7 public import served.lsp.uri;
8 public import served.utils.events;
9 
10 static import served.extension;
11 
12 import served.serverbase;
13 mixin LanguageServerRouter!(served.extension) lspRouter;
14 
15 import core.time : MonoTime;
16 
17 import std.algorithm;
18 import std.array;
19 import std.ascii;
20 import std.conv;
21 import std.experimental.logger;
22 import std.json;
23 import std.meta;
24 import std.path;
25 import std.range;
26 import std.string;
27 
28 import fs = std.file;
29 import io = std.stdio;
30 
31 import workspaced.api;
32 
33 deprecated("import stdlib_detect directly")
34 public import served.utils.stdlib_detect : parseDmdConfImports, parseDflagsImports;
35 
36 enum IncludedFeatures = ["d", "workspaces"];
37 
38 __gshared MonoTime startupTime;
39 
40 alias documents = lspRouter.documents;
41 alias rpc = lspRouter.rpc;
42 
43 string[] compare(string prefix, T)(ref T a, ref T b)
44 {
45 	auto changed = appender!(string[]);
46 	foreach (member; __traits(allMembers, T))
47 		if (__traits(getMember, a, member) != __traits(getMember, b, member))
48 			changed ~= (prefix ~ member);
49 	return changed.data;
50 }
51 
52 alias configurationTypes = AliasSeq!(Configuration.D, Configuration.DFmt,
53 		Configuration.DScanner, Configuration.Editor, Configuration.Git);
54 static immutable string[] configurationSections = [
55 	"d", "dfmt", "dscanner", "editor", "git"
56 ];
57 
58 enum ManyProjectsAction : string
59 {
60 	ask = "ask",
61 	skip = "skip",
62 	load = "load"
63 }
64 
65 // alias to avoid name clashing
66 alias UserConfiguration = Configuration;
67 struct Configuration
68 {
69 	struct D
70 	{
71 		JSONValue stdlibPath = JSONValue("auto");
72 		string dcdClientPath = "dcd-client", dcdServerPath = "dcd-server";
73 		string dubPath = "dub";
74 		string dmdPath = "dmd";
75 		bool enableLinting = true;
76 		bool enableSDLLinting = true;
77 		bool enableStaticLinting = true;
78 		bool enableDubLinting = true;
79 		bool enableAutoComplete = true;
80 		bool enableFormatting = true;
81 		bool enableDMDImportTiming = false;
82 		bool enableCoverageDecoration = true;
83 		bool enableGCProfilerDecorations = true;
84 		bool neverUseDub = false;
85 		string[] projectImportPaths;
86 		string dubConfiguration;
87 		string dubArchType;
88 		string dubBuildType;
89 		string dubCompiler;
90 		bool overrideDfmtEditorconfig = true;
91 		bool aggressiveUpdate = false; // differs from default code-d settings on purpose!
92 		bool argumentSnippets = false;
93 		bool scanAllFolders = true;
94 		string[] disabledRootGlobs;
95 		string[] extraRoots;
96 		string manyProjectsAction = ManyProjectsAction.ask;
97 		int manyProjectsThreshold = 6;
98 		string lintOnFileOpen = "project";
99 		bool dietContextCompletion = false;
100 		bool generateModuleNames = true;
101 	}
102 
103 	struct DFmt
104 	{
105 		bool alignSwitchStatements = true;
106 		string braceStyle = "allman";
107 		bool outdentAttributes = true;
108 		bool spaceAfterCast = true;
109 		bool splitOperatorAtLineEnd = false;
110 		bool selectiveImportSpace = true;
111 		bool compactLabeledStatements = true;
112 		string templateConstraintStyle = "conditional_newline_indent";
113 		bool spaceBeforeFunctionParameters = false;
114 		bool singleTemplateConstraintIndent = false;
115 		bool spaceBeforeAAColon = false;
116 		bool keepLineBreaks = true;
117 		bool singleIndent = true;
118 	}
119 
120 	struct DScanner
121 	{
122 		string[] ignoredKeys;
123 	}
124 
125 	struct Editor
126 	{
127 		int[] rulers;
128 		int tabSize;
129 	}
130 
131 	struct Git
132 	{
133 		string path = "git";
134 	}
135 
136 	D d;
137 	DFmt dfmt;
138 	DScanner dscanner;
139 	Editor editor;
140 	Git git;
141 
142 	string[] stdlibPath(string cwd = null)
143 	{
144 		auto p = d.stdlibPath;
145 		if (p.type == JSONType.array)
146 			return p.array.map!(a => a.str.userPath).array;
147 		else
148 		{
149 			if (p.type != JSONType..string || p.str == "auto")
150 			{
151 				import served.utils.stdlib_detect;
152 
153 				return autoDetectStdlibPaths(cwd, d.dubCompiler);
154 			}
155 			else
156 				return [p.str.userPath];
157 		}
158 	}
159 
160 	string[] replace(Configuration newConfig)
161 	{
162 		string[] ret;
163 		ret ~= replaceSection!"d"(newConfig.d);
164 		ret ~= replaceSection!"dfmt"(newConfig.dfmt);
165 		ret ~= replaceSection!"dscanner"(newConfig.dscanner);
166 		ret ~= replaceSection!"editor"(newConfig.editor);
167 		ret ~= replaceSection!"git"(newConfig.git);
168 		return ret;
169 	}
170 
171 	string[] replaceSection(string section : "d")(D newD)
172 	{
173 		auto ret = compare!"d."(d, newD);
174 		d = newD;
175 		return ret;
176 	}
177 
178 	string[] replaceSection(string section : "dfmt")(DFmt newDfmt)
179 	{
180 		auto ret = compare!"dfmt."(dfmt, newDfmt);
181 		dfmt = newDfmt;
182 		return ret;
183 	}
184 
185 	string[] replaceSection(string section : "dscanner")(DScanner newDscanner)
186 	{
187 		auto ret = compare!"dscanner."(dscanner, newDscanner);
188 		dscanner = newDscanner;
189 		return ret;
190 	}
191 
192 	string[] replaceSection(string section : "editor")(Editor newEditor)
193 	{
194 		auto ret = compare!"editor."(editor, newEditor);
195 		editor = newEditor;
196 		return ret;
197 	}
198 
199 	string[] replaceSection(string section : "git")(Git newGit)
200 	{
201 		auto ret = compare!"git."(git, newGit);
202 		git = newGit;
203 		return ret;
204 	}
205 
206 	string[] replaceAllSections(JSONValue[] settings)
207 	{
208 		import painlessjson : fromJSON;
209 
210 		assert(settings.length >= configurationSections.length);
211 		auto changed = appender!(string[]);
212 		static foreach (n, section; configurationSections)
213 			changed ~= this.replaceSection!section(settings[n].fromJSON!(configurationTypes[n]));
214 		return changed.data;
215 	}
216 }
217 
218 Configuration parseConfiguration(JSONValue json)
219 {
220 	Configuration ret;
221 	if (json.type != JSONType.object)
222 	{
223 		error("Configuration is not an object!");
224 		return ret;
225 	}
226 
227 	foreach (key, value; json.object)
228 	{
229 	SectionSwitch:
230 		switch (key)
231 		{
232 			static foreach (section; configurationSections)
233 			{
234 		case section:
235 				__traits(getMember, ret, section) = value.parseConfigurationSection!(
236 						typeof(__traits(getMember, ret, section)))(key);
237 				break SectionSwitch;
238 			}
239 		default:
240 			infof("Ignoring unknown configuration section '%s'", key);
241 			break;
242 		}
243 	}
244 
245 	return ret;
246 }
247 
248 T parseConfigurationSection(T)(JSONValue json, string sectionKey)
249 {
250 	import std.traits : FieldNameTuple;
251 	import painlessjson : fromJSON;
252 
253 	T ret;
254 	if (json.type != JSONType.object)
255 	{
256 		error("Configuration is not an object!");
257 		return ret;
258 	}
259 
260 	foreach (key, value; json.object)
261 	{
262 	ConfigSwitch:
263 		switch (key)
264 		{
265 			static foreach (member; FieldNameTuple!T)
266 			{
267 		case member:
268 				{
269 					alias U = typeof(__traits(getMember, ret, member));
270 					try
271 					{
272 						static if (__traits(compiles, { T t = null; }))
273 						{
274 							if (value.type == JSONType.null_)
275 							{
276 								__traits(getMember, ret, member) = null;
277 							}
278 							else
279 							{
280 								static if (is(U : string))
281 									__traits(getMember, ret, member) = cast(U) value.str;
282 								else
283 									__traits(getMember, ret, member) = value.fromJSON!U;
284 							}
285 						}
286 						else
287 						{
288 							if (value.type == JSONType.null_)
289 							{
290 								// ignore null value on non-nullable
291 							}
292 							else
293 							{
294 								static if (is(U : string))
295 									__traits(getMember, ret, member) = cast(U) value.str;
296 								else
297 									__traits(getMember, ret, member) = value.fromJSON!U;
298 							}
299 						}
300 					}
301 					catch (Exception e)
302 					{
303 						errorf("Skipping unparsable configuration '%s.%s' which was expected to be of type %s parsed from %s: %s",
304 								sectionKey, key, U.stringof, value.type, e.msg);
305 					}
306 					break ConfigSwitch;
307 				}
308 			}
309 		default:
310 			warningf("Ignoring unknown configuration section '%s.%s'", sectionKey, key);
311 			break;
312 		}
313 	}
314 
315 	return ret;
316 }
317 
318 struct Workspace
319 {
320 	WorkspaceFolder folder;
321 	Configuration config;
322 	bool initialized, disabled;
323 	string[string] startupErrorNotifications;
324 	bool selected;
325 
326 	void startupError(string folder, string error)
327 	{
328 		if (folder !in startupErrorNotifications)
329 			startupErrorNotifications[folder] = "";
330 		string errors = startupErrorNotifications[folder];
331 		if (errors.length)
332 		{
333 			if (errors.endsWith(".", "\n\n"))
334 				startupErrorNotifications[folder] ~= " " ~ error;
335 			else if (errors.endsWith(". "))
336 				startupErrorNotifications[folder] ~= error;
337 			else
338 				startupErrorNotifications[folder] ~= "\n\n" ~ error;
339 		}
340 		else
341 			startupErrorNotifications[folder] = error;
342 	}
343 
344 	string[] stdlibPath()
345 	{
346 		return config.stdlibPath(folder.uri.uriToFile);
347 	}
348 
349 	auto describeState() const @property
350 	{
351 		static struct WorkspaceState
352 		{
353 			string uri, name;
354 			bool initialized;
355 			bool selected;
356 		}
357 
358 		WorkspaceState state;
359 		state.uri = folder.uri;
360 		state.name = folder.name;
361 		state.initialized = initialized;
362 		state.selected = selected;
363 		return state;
364 	}
365 }
366 
367 deprecated string workspaceRoot() @property
368 {
369 	return firstWorkspaceRootUri.uriToFile;
370 }
371 
372 string selectedWorkspaceUri() @property
373 {
374 	foreach (ref workspace; workspaces)
375 		if (workspace.selected)
376 			return workspace.folder.uri;
377 	return firstWorkspaceRootUri;
378 }
379 
380 string selectedWorkspaceRoot() @property
381 {
382 	return selectedWorkspaceUri.uriToFile;
383 }
384 
385 string firstWorkspaceRootUri() @property
386 {
387 	return workspaces.length ? workspaces[0].folder.uri : "";
388 }
389 
390 Workspace fallbackWorkspace;
391 Workspace[] workspaces;
392 ClientCapabilities capabilities;
393 
394 size_t workspaceIndex(string uri)
395 {
396 	if (!uri.startsWith("file://"))
397 		throw new Exception("Passed a non file:// uri to workspace(uri): '" ~ uri ~ "'");
398 	size_t best = size_t.max;
399 	size_t bestLength = 0;
400 	foreach (i, ref workspace; workspaces)
401 	{
402 		if (workspace.folder.uri.length > bestLength
403 				&& uri.startsWith(workspace.folder.uri) && !workspace.disabled)
404 		{
405 			best = i;
406 			bestLength = workspace.folder.uri.length;
407 			if (uri.length == workspace.folder.uri.length) // startsWith + same length => same string
408 				return i;
409 		}
410 	}
411 	return best;
412 }
413 
414 ref Workspace handleThings(return ref Workspace workspace, string uri, bool userExecuted,
415 		string file = __FILE__, size_t line = __LINE__)
416 {
417 	if (userExecuted)
418 	{
419 		string f = uri.uriToFile;
420 		foreach (key, error; workspace.startupErrorNotifications)
421 		{
422 			if (f.startsWith(key))
423 			{
424 				//dfmt off
425 				debug
426 					rpc.window.showErrorMessage(
427 							error ~ "\n\nFile: " ~ file ~ ":" ~ line.to!string);
428 				else
429 					rpc.window.showErrorMessage(error);
430 				//dfmt on
431 				workspace.startupErrorNotifications.remove(key);
432 			}
433 		}
434 
435 		bool notifyChange, changedOne;
436 		foreach (ref w; workspaces)
437 		{
438 			if (w.selected)
439 			{
440 				if (w.folder.uri != workspace.folder.uri)
441 					notifyChange = true;
442 				changedOne = true;
443 				w.selected = false;
444 			}
445 		}
446 		workspace.selected = true;
447 		if (notifyChange || !changedOne)
448 			rpc.notifyMethod("coded/changedSelectedWorkspace", workspace.describeState);
449 	}
450 	return workspace;
451 }
452 
453 ref Workspace workspace(string uri, bool userExecuted = true,
454 		string file = __FILE__, size_t line = __LINE__)
455 {
456 	if (!uri.length)
457 		return fallbackWorkspace;
458 
459 	auto best = workspaceIndex(uri);
460 	if (best == size_t.max)
461 		return bestWorkspaceByDependency(uri).handleThings(uri, userExecuted, file, line);
462 	return workspaces[best].handleThings(uri, userExecuted, file, line);
463 }
464 
465 ref Workspace bestWorkspaceByDependency(string uri)
466 {
467 	size_t best = size_t.max;
468 	size_t bestLength;
469 	foreach (i, ref workspace; workspaces)
470 	{
471 		auto inst = backend.getInstance(workspace.folder.uri.uriToFile);
472 		if (!inst)
473 			continue;
474 		foreach (folder; chain(inst.importPaths, inst.importFiles, inst.stringImportPaths))
475 		{
476 			string folderUri = folder.uriFromFile;
477 			if (folderUri.length > bestLength && uri.startsWith(folderUri))
478 			{
479 				best = i;
480 				bestLength = folderUri.length;
481 				if (uri.length == folderUri.length) // startsWith + same length => same string
482 					return workspace;
483 			}
484 		}
485 	}
486 	if (best == size_t.max)
487 		return fallbackWorkspace;
488 	return workspaces[best];
489 }
490 
491 ref Workspace selectedWorkspace()
492 {
493 	foreach (ref workspace; workspaces)
494 		if (workspace.selected)
495 			return workspace;
496 	return fallbackWorkspace;
497 }
498 
499 WorkspaceD.Instance _activeInstance;
500 
501 WorkspaceD.Instance activeInstance(WorkspaceD.Instance value) @property
502 {
503 	trace("Setting active instance to ", value ? value.cwd : "<null>", ".");
504 	return _activeInstance = value;
505 }
506 
507 WorkspaceD.Instance activeInstance() @property
508 {
509 	return _activeInstance;
510 }
511 
512 string workspaceRootFor(string uri)
513 {
514 	return workspace(uri).folder.uri.uriToFile;
515 }
516 
517 bool hasWorkspace(string uri)
518 {
519 	foreach (i, ref workspace; workspaces)
520 		if (uri.startsWith(workspace.folder.uri))
521 			return true;
522 	return false;
523 }
524 
525 ref Configuration config(string uri, bool userExecuted = true,
526 		string file = __FILE__, size_t line = __LINE__)
527 out (result)
528 {
529 	trace("Config for ", uri, ": ", result);
530 }
531 do
532 {
533 	return workspace(uri, userExecuted, file, line).config;
534 }
535 
536 ref Configuration firstConfig()
537 {
538 	if (!workspaces.length)
539 		throw new Exception("No config available");
540 	return workspaces[0].config;
541 }
542 
543 string userPath(string path)
544 {
545 	return expandTilde(path);
546 }
547 
548 string userPath(Configuration.Git git)
549 {
550 	// vscode may send null git path
551 	return git.path.length ? userPath(git.path) : "git";
552 }
553 
554 int toInt(JSONValue value)
555 {
556 	if (value.type == JSONType.uinteger)
557 		return cast(int) value.uinteger;
558 	else
559 		return cast(int) value.integer;
560 }
561 
562 __gshared LazyWorkspaceD backend;
563 
564 /// Quick function to check if a package.json can not not be a dub package file.
565 /// Returns: false if fields are used which aren't usually used in dub but in nodejs.
566 bool seemsLikeDubJson(JSONValue packageJson)
567 {
568 	if ("main" in packageJson || "engines" in packageJson || "publisher" in packageJson
569 			|| "private" in packageJson || "devDependencies" in packageJson)
570 		return false;
571 	if ("name" !in packageJson)
572 		return false;
573 	return true;
574 }
575 
576 /// Inserts a value into a sorted range. Inserts before equal elements.
577 /// Returns: the index where the value has been inserted.
578 size_t insertSorted(alias sort = "a<b", T)(ref T[] arr, T value)
579 {
580 	auto v = arr.binarySearch!sort(value);
581 	if (v < 0)
582 		v = ~v;
583 	arr.length++;
584 	for (ptrdiff_t i = cast(ptrdiff_t) arr.length - 1; i > v; i--)
585 		move(arr[i - 1], arr[i]);
586 	arr[v] = value;
587 	return v;
588 }
589 
590 /// Finds a value in a sorted range and returns its index.
591 /// Returns: a bitwise invert of the first element bigger than value. Use `~ret` to turn it back.
592 ptrdiff_t binarySearch(alias sort = "a<b", T)(T[] arr, T value)
593 {
594 	auto sorted = assumeSorted!sort(arr).trisect(value);
595 	if (sorted[1].length)
596 		return cast(ptrdiff_t) sorted[0].length;
597 	else
598 		return ~cast(ptrdiff_t) sorted[0].length;
599 }
600 
601 void prettyPrintStruct(alias printFunc, T, int line = __LINE__, string file = __FILE__,
602 		string funcName = __FUNCTION__, string prettyFuncName = __PRETTY_FUNCTION__,
603 		string moduleName = __MODULE__)(T value, string indent = "\t")
604 		if (is(T == struct))
605 {
606 	static foreach (i, member; T.tupleof)
607 	{
608 		{
609 			static if (is(typeof(member) == Optional!U, U))
610 			{
611 				if (value.tupleof[i].isNull)
612 				{
613 					printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent,
614 							__traits(identifier, member), "?: <null>");
615 				}
616 				else
617 				{
618 					static if (is(U == struct))
619 					{
620 						printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent,
621 								__traits(identifier, member), "?:");
622 						prettyPrintStruct!(printFunc, U, line, file, funcName, prettyFuncName, moduleName)(value.tupleof[i].get,
623 								indent ~ "\t");
624 					}
625 					else
626 					{
627 						printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent,
628 								__traits(identifier, member), "?: ", value.tupleof[i].get);
629 					}
630 				}
631 			}
632 			else static if (is(typeof(member) == JSONValue))
633 			{
634 				printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent,
635 						__traits(identifier, member), ": ", value.tupleof[i].toString());
636 			}
637 			else static if (is(typeof(member) == struct))
638 			{
639 				printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent,
640 						__traits(identifier, member), ":");
641 				prettyPrintStruct!(printFunc, typeof(member), line, file, funcName,
642 						prettyFuncName, moduleName)(value.tupleof[i], indent ~ "\t");
643 			}
644 			else
645 				printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent,
646 						__traits(identifier, member), ": ", value.tupleof[i]);
647 		}
648 	}
649 }