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 
14 /// These are kind-of minimum values for a bunch of "killer" tests in libdparse
15 debug
16 	enum requiredLibdparsePageCount = 128; // = 1 MiB stack per fiber
17 else // release builds are more optimized with stack usage
18 	enum requiredLibdparsePageCount = 32; // = 256 KiB stack per fiber
19 
20 static immutable LanguageServerConfig lsConfig = {
21 	defaultPages: requiredLibdparsePageCount,
22 	productName: "serve-d"
23 };
24 
25 mixin LanguageServerRouter!(served.extension, lsConfig) lspRouter;
26 
27 import core.time : MonoTime;
28 
29 import std.algorithm;
30 import std.array;
31 import std.ascii;
32 import std.conv;
33 import std.experimental.logger;
34 import std.json;
35 import std.meta;
36 import std.path;
37 import std.range;
38 import std.string;
39 
40 import fs = std.file;
41 import io = std.stdio;
42 
43 import mir.serde;
44 
45 import workspaced.api;
46 
47 deprecated("import stdlib_detect directly")
48 public import served.utils.stdlib_detect : parseDmdConfImports, parseDflagsImports;
49 
50 enum IncludedFeatures = ["d", "workspaces"];
51 
52 __gshared MonoTime startupTime;
53 
54 alias documents = lspRouter.documents;
55 alias rpc = lspRouter.rpc;
56 
57 enum ManyProjectsAction : string
58 {
59 	ask = "ask",
60 	skip = "skip",
61 	load = "load"
62 }
63 
64 // alias to avoid name clashing
65 alias UserConfiguration = Configuration;
66 @serdeIgnoreUnexpectedKeys
67 struct Configuration
68 {
69 @serdeIgnoreUnexpectedKeys:
70 @serdeOptional:
71 
72 	struct D
73 	{
74 		@serdeOptional:
75 		Nullable!(string, string[]) stdlibPath = Nullable!(string, string[])("auto");
76 		string dcdClientPath = "dcd-client", dcdServerPath = "dcd-server";
77 		string dubPath = "dub";
78 		string dmdPath = "dmd";
79 		bool enableLinting = true;
80 		bool enableSDLLinting = true;
81 		bool enableStaticLinting = true;
82 		bool enableDubLinting = true;
83 		bool enableAutoComplete = true;
84 		bool enableFormatting = true;
85 		bool enableDMDImportTiming = false;
86 		bool enableCoverageDecoration = true;
87 		bool enableGCProfilerDecorations = true;
88 		bool neverUseDub = false;
89 		string[] projectImportPaths;
90 		string dubConfiguration;
91 		string dubArchType;
92 		string dubBuildType;
93 		string dubCompiler;
94 		bool overrideDfmtEditorconfig = true;
95 		bool aggressiveUpdate = false; // differs from default code-d settings on purpose!
96 		bool argumentSnippets = false;
97 		bool scanAllFolders = true;
98 		string[] disabledRootGlobs;
99 		string[] extraRoots;
100 		string manyProjectsAction = ManyProjectsAction.ask;
101 		int manyProjectsThreshold = 6;
102 		string lintOnFileOpen = "project";
103 		bool dietContextCompletion = false;
104 		bool generateModuleNames = true;
105 	}
106 
107 	struct DFmt
108 	{
109 		@serdeOptional:
110 		bool alignSwitchStatements = true;
111 		string braceStyle = "allman";
112 		bool outdentAttributes = true;
113 		bool spaceAfterCast = true;
114 		bool splitOperatorAtLineEnd = false;
115 		bool selectiveImportSpace = true;
116 		bool compactLabeledStatements = true;
117 		string templateConstraintStyle = "conditional_newline_indent";
118 		bool spaceBeforeFunctionParameters = false;
119 		bool singleTemplateConstraintIndent = false;
120 		bool spaceBeforeAAColon = false;
121 		bool keepLineBreaks = true;
122 		bool singleIndent = true;
123 	}
124 
125 	struct DScanner
126 	{
127 		@serdeOptional:
128 		string[] ignoredKeys;
129 	}
130 
131 	struct Editor
132 	{
133 		@serdeOptional:
134 		int[] rulers;
135 		int tabSize;
136 	}
137 
138 	struct Git
139 	{
140 		@serdeOptional:
141 		string path = "git";
142 	}
143 
144 	D d;
145 	DFmt dfmt;
146 	DScanner dscanner;
147 	Editor editor;
148 	Git git;
149 
150 	string[] stdlibPath(string cwd = null) const
151 	{
152 		import served.utils.stdlib_detect;
153 
154 		return d.stdlibPath.match!(
155 			(const typeof(null) _) => autoDetectStdlibPaths(cwd, d.dubCompiler),
156 			(const string s) => s == "auto"
157 				? autoDetectStdlibPaths(cwd, d.dubCompiler)
158 				: [s.userPath],
159 			(const string[] a) => a.map!(s => s.userPath).array
160 		);
161 	}
162 
163 	string dcdClientPath() const
164 	{
165 		return detectDcdPath(d.dcdClientPath);
166 	}
167 
168 	string dcdServerPath() const
169 	{
170 		return detectDcdPath(d.dcdServerPath);
171 	}
172 
173 	private static string detectDcdPath(string path)
174 	{
175 		import served.extension : determineOutputFolder;
176 		import served.utils.stdlib_detect : searchPathFor;
177 
178 		if (path != "dcd-server" && path != "dcd-client")
179 		{
180 			trace("using custom DCD provided from ", path);
181 			return path;
182 		}
183 
184 		// if any such executable is found in PATH, just return path and let the
185 		// OS give us what it thinks it should be.
186 		if (searchPathFor(path).length)
187 			return path;
188 
189 		version (Windows)
190 			auto exePath = defaultExtension(path, ".exe");
191 		else
192 			auto exePath = path;
193 
194 		auto outputFolder = determineOutputFolder;
195 		if (fs.exists(outputFolder))
196 		{
197 			version (Windows)
198 				static immutable searchPrefixes = ["", "DCD", "DCD\\bin"];
199 			else
200 				static immutable searchPrefixes = ["", "dcd", "DCD", "dcd/bin", "DCD/bin"];
201 
202 			foreach (prefix; ["", "dcd", "DCD", "dcd/bin", "DCD/bin"])
203 			{
204 				auto finalPath = buildPath(outputFolder, prefix, exePath);
205 				if (fs.exists(finalPath))
206 				{
207 					trace("found previously installed DCD in ", finalPath);
208 					return finalPath;
209 				}
210 			}
211 		}
212 		else
213 		{
214 			trace("no default output folder for DCD exists yet (", outputFolder,
215 				"), going to ask the user for automatic installation soon");
216 		}
217 
218 		return path;
219 	}
220 }
221 
222 struct Workspace
223 {
224 	WorkspaceFolder folder;
225 	bool initialized, disabled;
226 	string[string] startupErrorNotifications;
227 	bool selected;
228 	bool useGlobalConfig;
229 
230 	void startupError(string folder, string error)
231 	{
232 		if (folder !in startupErrorNotifications)
233 			startupErrorNotifications[folder] = "";
234 		string errors = startupErrorNotifications[folder];
235 		if (errors.length)
236 		{
237 			if (errors.endsWith(".", "\n\n"))
238 				startupErrorNotifications[folder] ~= " " ~ error;
239 			else if (errors.endsWith(". "))
240 				startupErrorNotifications[folder] ~= error;
241 			else
242 				startupErrorNotifications[folder] ~= "\n\n" ~ error;
243 		}
244 		else
245 			startupErrorNotifications[folder] = error;
246 	}
247 
248 	string[] stdlibPath()
249 	{
250 		return config.stdlibPath(folder.uri.uriToFile);
251 	}
252 
253 	auto describeState() const @property
254 	{
255 		static struct WorkspaceState
256 		{
257 			string uri, name;
258 			bool initialized;
259 			bool selected;
260 			const(string)[string] pendingErrors;
261 		}
262 
263 		WorkspaceState state;
264 		state.uri = folder.uri;
265 		state.name = folder.name;
266 		state.initialized = initialized;
267 		state.selected = selected;
268 		state.pendingErrors = startupErrorNotifications.dup;
269 		return state;
270 	}
271 
272 	ref inout(Configuration) config() inout
273 	{
274 		auto cfg = folder.uri in served.extension.perWorkspaceConfigurationStore;
275 		if (!cfg || useGlobalConfig)
276 			cfg = served.extension.globalConfiguration;
277 		return cast(inout) cfg.config;
278 	}
279 }
280 
281 deprecated string workspaceRoot() @property
282 {
283 	return firstWorkspaceRootUri.uriToFile;
284 }
285 
286 string selectedWorkspaceUri() @property
287 {
288 	foreach (ref workspace; workspaces)
289 		if (workspace.selected)
290 			return workspace.folder.uri;
291 	return firstWorkspaceRootUri;
292 }
293 
294 string selectedWorkspaceRoot() @property
295 {
296 	return selectedWorkspaceUri.uriToFile;
297 }
298 
299 string firstWorkspaceRootUri() @property
300 {
301 	return workspaces.length ? workspaces[0].folder.uri : "";
302 }
303 
304 Workspace fallbackWorkspace;
305 Workspace[] workspaces;
306 ClientCapabilities capabilities;
307 
308 size_t workspaceIndex(string uri)
309 {
310 	if (!uri.startsWith("file://"))
311 		throw new Exception("Passed a non file:// uri to workspace(uri): '" ~ uri ~ "'");
312 	size_t best = size_t.max;
313 	size_t bestLength = 0;
314 	foreach (i, ref workspace; workspaces)
315 	{
316 		if (workspace.folder.uri.length > bestLength
317 				&& uri.startsWith(workspace.folder.uri) && !workspace.disabled)
318 		{
319 			best = i;
320 			bestLength = workspace.folder.uri.length;
321 			if (uri.length == workspace.folder.uri.length) // startsWith + same length => same string
322 				return i;
323 		}
324 	}
325 	return best;
326 }
327 
328 ref Workspace handleThings(return ref Workspace workspace, string uri, bool userExecuted,
329 		string file = __FILE__, size_t line = __LINE__)
330 {
331 	if (userExecuted)
332 	{
333 		string f = uri.uriToFile;
334 		foreach (key, error; workspace.startupErrorNotifications)
335 		{
336 			if (f.startsWith(key))
337 			{
338 				//dfmt off
339 				debug
340 					rpc.window.showErrorMessage(
341 							error ~ "\n\nFile: " ~ file ~ ":" ~ line.to!string);
342 				else
343 					rpc.window.showErrorMessage(error);
344 				//dfmt on
345 				workspace.startupErrorNotifications.remove(key);
346 			}
347 		}
348 
349 		bool notifyChange, changedOne;
350 		foreach (ref w; workspaces)
351 		{
352 			if (w.selected)
353 			{
354 				if (w.folder.uri != workspace.folder.uri)
355 					notifyChange = true;
356 				changedOne = true;
357 				w.selected = false;
358 			}
359 		}
360 		workspace.selected = true;
361 		if (notifyChange || !changedOne)
362 			rpc.notifyMethod("coded/changedSelectedWorkspace", workspace.describeState);
363 	}
364 	return workspace;
365 }
366 
367 ref Workspace workspace(string uri, bool userExecuted = true,
368 		string file = __FILE__, size_t line = __LINE__)
369 {
370 	if (!uri.length)
371 		return fallbackWorkspace;
372 
373 	auto best = workspaceIndex(uri);
374 	if (best == size_t.max)
375 		return bestWorkspaceByDependency(uri).handleThings(uri, userExecuted, file, line);
376 	return workspaces[best].handleThings(uri, userExecuted, file, line);
377 }
378 
379 ref Workspace bestWorkspaceByDependency(string uri)
380 {
381 	size_t best = size_t.max;
382 	size_t bestLength;
383 	foreach (i, ref workspace; workspaces)
384 	{
385 		auto inst = backend.getInstance(workspace.folder.uri.uriToFile);
386 		if (!inst)
387 			continue;
388 		foreach (folder; chain(inst.importPaths, inst.importFiles, inst.stringImportPaths))
389 		{
390 			string folderUri = folder.uriFromFile;
391 			if (folderUri.length > bestLength && uri.startsWith(folderUri))
392 			{
393 				best = i;
394 				bestLength = folderUri.length;
395 				if (uri.length == folderUri.length) // startsWith + same length => same string
396 					return workspace;
397 			}
398 		}
399 	}
400 	if (best == size_t.max)
401 		return fallbackWorkspace;
402 	return workspaces[best];
403 }
404 
405 ref Workspace selectedWorkspace()
406 {
407 	foreach (ref workspace; workspaces)
408 		if (workspace.selected)
409 			return workspace;
410 	return fallbackWorkspace;
411 }
412 
413 WorkspaceD.Instance _activeInstance;
414 
415 WorkspaceD.Instance activeInstance(WorkspaceD.Instance value) @property
416 {
417 	trace("Setting active instance to ", value ? value.cwd : "<null>", ".");
418 	return _activeInstance = value;
419 }
420 
421 WorkspaceD.Instance activeInstance() @property
422 {
423 	return _activeInstance;
424 }
425 
426 string workspaceRootFor(string uri)
427 {
428 	return workspace(uri).folder.uri.uriToFile;
429 }
430 
431 bool hasWorkspace(string uri)
432 {
433 	foreach (i, ref workspace; workspaces)
434 		if (uri.startsWith(workspace.folder.uri))
435 			return true;
436 	return false;
437 }
438 
439 ref Configuration config(string uri, bool userExecuted = true,
440 		string file = __FILE__, size_t line = __LINE__)
441 {
442 	return workspace(uri, userExecuted, file, line).config;
443 }
444 
445 ref Configuration anyConfig()
446 {
447 	if (!workspaces.length)
448 		return fallbackWorkspace.config;
449 	return workspaces[0].config;
450 }
451 
452 string userPath(string path)
453 {
454 	return expandTilde(path);
455 }
456 
457 string userPath(Configuration.Git git)
458 {
459 	// vscode may send null git path
460 	return git.path.length ? userPath(git.path) : "git";
461 }
462 
463 int toInt(JsonValue value)
464 {
465 	return cast(int)value.get!long;
466 }
467 
468 __gshared LazyWorkspaceD backend;
469 
470 /// Quick function to check if a package.json can not not be a dub package file.
471 /// Returns: false if fields are used which aren't usually used in dub but in nodejs.
472 bool seemsLikeDubJson(string json)
473 {
474 	if (!json.looksLikeJsonObject)
475 		return false;
476 	auto packageJson = json.parseKeySlices!("main", "engines", "publisher",
477 		"private_", "devDependencies", "name");
478 	if (packageJson.main.length
479 		|| packageJson.engines.length
480 		|| packageJson.publisher.length
481 		|| packageJson.private_.length
482 		|| packageJson.devDependencies.length)
483 		return false;
484 	if (!packageJson.name.length)
485 		return false;
486 	return true;
487 }
488 
489 /// Inserts a value into a sorted range. Inserts before equal elements.
490 /// Returns: the index where the value has been inserted.
491 size_t insertSorted(alias sort = "a<b", T)(ref T[] arr, T value)
492 {
493 	auto v = arr.binarySearch!sort(value);
494 	if (v < 0)
495 		v = ~v;
496 	arr.length++;
497 	for (ptrdiff_t i = cast(ptrdiff_t) arr.length - 1; i > v; i--)
498 		move(arr[i - 1], arr[i]);
499 	arr[v] = value;
500 	return v;
501 }
502 
503 /// Finds a value in a sorted range and returns its index.
504 /// Returns: a bitwise invert of the first element bigger than value. Use `~ret` to turn it back.
505 ptrdiff_t binarySearch(alias sort = "a<b", T)(T[] arr, T value)
506 {
507 	auto sorted = assumeSorted!sort(arr).trisect(value);
508 	if (sorted[1].length)
509 		return cast(ptrdiff_t) sorted[0].length;
510 	else
511 		return ~cast(ptrdiff_t) sorted[0].length;
512 }
513 
514 void prettyPrintStruct(alias printFunc, T, int line = __LINE__, string file = __FILE__,
515 		string funcName = __FUNCTION__, string prettyFuncName = __PRETTY_FUNCTION__,
516 		string moduleName = __MODULE__)(T value, string indent = "\t")
517 		if (is(T == struct))
518 {
519 	static foreach (i, member; T.tupleof)
520 	{{
521 		static if (isVariant!(typeof(member)))
522 		{
523 			static if (is(typeof(member).AllowedTypes[0] == void))
524 			{
525 				// is optional
526 				value.tupleof[i].match!(
527 					() {
528 						printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent,
529 								__traits(identifier, member), "?: <null>");
530 					},
531 					(val) {
532 						static if (is(typeof(val) == struct))
533 						{
534 							printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent,
535 									__traits(identifier, member), "?:");
536 							prettyPrintStruct!(printFunc, typeof(val), line, file, funcName, prettyFuncName, moduleName)(
537 									val, indent ~ "\t");
538 						}
539 						else
540 						{
541 							printFunc!(line, file, funcName, prettyFuncName, moduleName)(
542 									indent, __traits(identifier, member), "?: ", val);
543 						}
544 					}
545 				);
546 			}
547 			else
548 			{
549 				value.tupleof[i].match!(
550 					(val) {
551 						static if (is(typeof(val) == struct))
552 						{
553 							printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent,
554 									__traits(identifier, member), ":");
555 							prettyPrintStruct!(printFunc, typeof(val), line, file, funcName, prettyFuncName, moduleName)(
556 									val, indent ~ "\t");
557 						}
558 						else
559 						{
560 							printFunc!(line, file, funcName, prettyFuncName, moduleName)(
561 									indent, __traits(identifier, member), ": ", val);
562 						}
563 					}
564 				);
565 			}
566 		}
567 		else static if (is(typeof(member) == JsonValue))
568 		{
569 			printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent,
570 					__traits(identifier, member), ": ", value.tupleof[i].toString());
571 		}
572 		else static if (is(typeof(member) == struct))
573 		{
574 			printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent,
575 					__traits(identifier, member), ":");
576 			prettyPrintStruct!(printFunc, typeof(member), line, file, funcName,
577 					prettyFuncName, moduleName)(value.tupleof[i], indent ~ "\t");
578 		}
579 		else
580 			printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent,
581 					__traits(identifier, member), ": ", value.tupleof[i]);
582 	}}
583 }
584 
585 /// Event called when all components have been registered but no workspaces have
586 /// been setup yet.
587 /// Signature: `()`
588 enum onRegisteredComponents;
589 
590 /// Event called when a project is available but not intended to be loaded yet.
591 /// Should not access any components, otherwise it will force a load, but only
592 /// show hints in the UI. When it's accessed and actually being loaded the
593 /// events `onAddingProject` and `onAddedProject` will be emitted.
594 /// Signature: `(WorkspaceD.Instance, string dir, string uri)`
595 enum onProjectAvailable;
596 
597 /// Event called when a new workspaced instance is created. Called before dub or
598 /// fsworkspace is loaded.
599 /// Signature: `(WorkspaceD.Instance, string dir, string uri)`
600 enum onAddingProject;
601 
602 /// Event called when a new project root is finished setting up. Called when all
603 /// components are loaded. DCD is loaded but not yet started at this point.
604 /// Signature: `(WorkspaceD.Instance, string dir, string rootFolderUri)`
605 enum onAddedProject;