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