1 module served.extension;
2 
3 import served.io.nothrow_fs;
4 import served.types;
5 import served.utils.fibermanager;
6 import served.utils.progress;
7 import served.utils.translate;
8 
9 public import served.utils.async;
10 
11 import core.time : msecs, seconds;
12 
13 import std.algorithm : any, canFind, endsWith, map;
14 import std.array : appender, array;
15 import std.conv : to;
16 import std.datetime.stopwatch : StopWatch;
17 import std.datetime.systime : Clock, SysTime;
18 import std.experimental.logger;
19 import std.format : format;
20 import std.functional : toDelegate;
21 import std.json : JSONType, JSONValue, parseJSON;
22 import std.meta : AliasSeq;
23 import std.path : baseName, buildNormalizedPath, buildPath, chainPath, dirName,
24 	globMatch, relativePath;
25 import std.string : join;
26 
27 import io = std.stdio;
28 
29 import workspaced.api;
30 import workspaced.coms;
31 
32 // list of all commands for auto dispatch
33 public import served.commands.calltips;
34 public import served.commands.code_actions;
35 public import served.commands.code_lens;
36 public import served.commands.color;
37 public import served.commands.complete;
38 public import served.commands.dcd_update;
39 public import served.commands.definition;
40 public import served.commands.dub;
41 public import served.commands.file_search;
42 public import served.commands.format;
43 public import served.commands.highlight;
44 public import served.commands.symbol_search;
45 public import served.commands.test_provider;
46 public import served.workers.profilegc;
47 public import served.workers.rename_listener;
48 
49 //dfmt off
50 alias members = AliasSeq!(
51 	__traits(derivedMembers, served.extension),
52 	__traits(derivedMembers, served.commands.calltips),
53 	__traits(derivedMembers, served.commands.code_actions),
54 	__traits(derivedMembers, served.commands.code_lens),
55 	__traits(derivedMembers, served.commands.color),
56 	__traits(derivedMembers, served.commands.complete),
57 	__traits(derivedMembers, served.commands.dcd_update),
58 	__traits(derivedMembers, served.commands.definition),
59 	__traits(derivedMembers, served.commands.dub),
60 	__traits(derivedMembers, served.commands.file_search),
61 	__traits(derivedMembers, served.commands.format),
62 	__traits(derivedMembers, served.commands.highlight),
63 	__traits(derivedMembers, served.commands.symbol_search),
64 	__traits(derivedMembers, served.commands.test_provider),
65 	__traits(derivedMembers, served.workers.profilegc),
66 	__traits(derivedMembers, served.workers.rename_listener),
67 );
68 //dfmt on
69 
70 /// Set to true when shutdown is called
71 __gshared bool shutdownRequested;
72 
73 void changedConfig(string workspaceUri, string[] paths, served.types.Configuration config,
74 		bool allowFallback = false, size_t index = 0, size_t numConfigs = 0)
75 {
76 	StopWatch sw;
77 	sw.start();
78 
79 	reportProgress(ProgressType.configLoad, index, numConfigs, workspaceUri);
80 
81 	if (!workspaceUri.length)
82 	{
83 		if (!allowFallback)
84 			error("Passed invalid empty workspace uri to changedConfig!");
85 		trace("Updated fallback config (user settings) for sections ", paths);
86 		return;
87 	}
88 
89 	if (!syncedConfiguration && !allowFallback)
90 	{
91 		syncedConfiguration = true;
92 		ensureStartedUp();
93 	}
94 
95 	Workspace* proj = &workspace(workspaceUri);
96 	bool isFallback = proj is &fallbackWorkspace;
97 	if (isFallback && !allowFallback)
98 	{
99 		error("Did not find workspace ", workspaceUri, " when updating config?");
100 		return;
101 	}
102 	else if (isFallback)
103 	{
104 		trace("Updated fallback config (user settings) for sections ", paths);
105 		return;
106 	}
107 
108 	if (!proj.initialized)
109 	{
110 		doStartup(proj.folder.uri);
111 		proj.initialized = true;
112 	}
113 
114 	auto workspaceFs = workspaceUri.uriToFile;
115 
116 	foreach (path; paths)
117 	{
118 		switch (path)
119 		{
120 		case "d.stdlibPath":
121 			if (backend.has!DCDComponent(workspaceFs))
122 				backend.get!DCDComponent(workspaceFs).addImports(config.stdlibPath(workspaceFs));
123 			break;
124 		case "d.projectImportPaths":
125 			if (backend.has!DCDComponent(workspaceFs))
126 				backend.get!DCDComponent(workspaceFs)
127 					.addImports(config.d.projectImportPaths.map!(a => a.userPath).array);
128 			break;
129 		case "d.dubConfiguration":
130 			if (backend.has!DubComponent(workspaceFs))
131 			{
132 				auto configs = backend.get!DubComponent(workspaceFs).configurations;
133 				if (configs.length == 0)
134 					rpc.window.showInformationMessage(translate!"d.ext.noConfigurations.project");
135 				else
136 				{
137 					auto defaultConfig = config.d.dubConfiguration;
138 					if (defaultConfig.length)
139 					{
140 						if (!configs.canFind(defaultConfig))
141 							rpc.window.showErrorMessage(
142 									translate!"d.ext.config.invalid.configuration"(defaultConfig));
143 						else
144 							backend.get!DubComponent(workspaceFs).setConfiguration(defaultConfig);
145 					}
146 					else
147 						backend.get!DubComponent(workspaceFs).setConfiguration(configs[0]);
148 				}
149 			}
150 			break;
151 		case "d.dubArchType":
152 			if (backend.has!DubComponent(workspaceFs) && config.d.dubArchType.length
153 					&& !backend.get!DubComponent(workspaceFs)
154 					.setArchType(JSONValue(["arch-type": JSONValue(config.d.dubArchType)])))
155 				rpc.window.showErrorMessage(
156 						translate!"d.ext.config.invalid.archType"(config.d.dubArchType));
157 			break;
158 		case "d.dubBuildType":
159 			if (backend.has!DubComponent(workspaceFs) && config.d.dubBuildType.length
160 					&& !backend.get!DubComponent(workspaceFs)
161 					.setBuildType(JSONValue([
162 							"build-type": JSONValue(config.d.dubBuildType)
163 						])))
164 				rpc.window.showErrorMessage(
165 						translate!"d.ext.config.invalid.buildType"(config.d.dubBuildType));
166 			break;
167 		case "d.dubCompiler":
168 			if (backend.has!DubComponent(workspaceFs) && config.d.dubCompiler.length
169 					&& !backend.get!DubComponent(workspaceFs).setCompiler(config.d.dubCompiler))
170 				rpc.window.showErrorMessage(
171 						translate!"d.ext.config.invalid.compiler"(config.d.dubCompiler));
172 			break;
173 		case "d.enableAutoComplete":
174 			if (config.d.enableAutoComplete)
175 			{
176 				if (!backend.has!DCDComponent(workspaceFs))
177 				{
178 					auto instance = backend.getInstance(workspaceFs);
179 					lazyStartDCDServer(instance, workspaceUri);
180 				}
181 			}
182 			else if (backend.has!DCDComponent(workspaceFs))
183 			{
184 				backend.get!DCDComponent(workspaceFs).stopServer();
185 			}
186 			break;
187 		case "d.enableLinting":
188 			if (!config.d.enableLinting)
189 			{
190 				import served.linters.dscanner : clear1 = clear;
191 				import served.linters.dub : clear2 = clear;
192 
193 				clear1();
194 				clear2();
195 			}
196 			break;
197 		case "d.enableStaticLinting":
198 			if (!config.d.enableStaticLinting)
199 			{
200 				import served.linters.dscanner : clear;
201 
202 				clear();
203 			}
204 			break;
205 		case "d.enableDubLinting":
206 			if (!config.d.enableDubLinting)
207 			{
208 				import served.linters.dub : clear;
209 
210 				clear();
211 			}
212 			break;
213 		default:
214 			break;
215 		}
216 	}
217 
218 	trace("Finished config change of ", workspaceUri, " with ", paths.length,
219 			" changes in ", sw.peek, ".");
220 }
221 
222 @protocolNotification("workspace/didChangeConfiguration")
223 void didChangeConfiguration(DidChangeConfigurationParams params)
224 {
225 	processConfigChange(params.settings.parseConfiguration);
226 }
227 
228 void processConfigChange(served.types.Configuration configuration)
229 {
230 	import painlessjson : fromJSON;
231 
232 	syncingConfiguration = true;
233 	scope (exit)
234 		syncingConfiguration = false;
235 
236 	if (!workspaces.length)
237 	{
238 		info("initializing config for temporary fallback workspace");
239 		workspaces = [fallbackWorkspace];
240 		workspaces[0].initialized = false;
241 	}
242 
243 	if (capabilities.workspace.configuration && workspaces.length >= 2)
244 	{
245 		ConfigurationItem[] items;
246 		items = getGlobalConfigurationItems(); // default workspace
247 		const stride = configurationSections.length;
248 
249 		foreach (workspace; workspaces)
250 			items ~= getConfigurationItems(workspace.folder.uri);
251 
252 		trace("Re-requesting configuration from client because there is more than 1 workspace");
253 		auto res = rpc.sendRequest("workspace/configuration", ConfigurationParams(items));
254 
255 		const expected = workspaces.length + 1;
256 		JSONValue[] settings = res.validateConfigurationItemsResponse(expected);
257 		if (!settings.length)
258 			return;
259 
260 		for (size_t i = 0; i < expected; i++)
261 		{
262 			const isDefault = i == 0;
263 			auto workspace = isDefault ? &fallbackWorkspace : &.workspace(items[i * stride].scopeUri.get,
264 					false);
265 			string[] changed = workspace.config.replaceAllSections(settings[i * stride .. $]);
266 			changedConfig(isDefault ? null : workspace.folder.uri, changed,
267 					workspace.config, isDefault, i, expected);
268 		}
269 	}
270 	else if (workspaces.length)
271 	{
272 		if (workspaces.length > 1)
273 			error(
274 					"Client does not support configuration request, only applying config for first workspace.");
275 		auto changed = workspaces[0].config.replace(configuration);
276 		changedConfig(workspaces[0].folder.uri, changed, workspaces[0].config, false, 0, 1);
277 		fallbackWorkspace.config = workspaces[0].config;
278 	}
279 	else
280 		error("unexpected state: got ", workspaces.length, " workspaces and ",
281 				capabilities.workspace.configuration ? "" : "no ", "configuration request support");
282 	reportProgress(ProgressType.configFinish, 0, 0);
283 }
284 
285 bool syncConfiguration(string workspaceUri, size_t index = 0, size_t numConfigs = 0)
286 {
287 	import painlessjson : fromJSON;
288 
289 	if (capabilities.workspace.configuration)
290 	{
291 		Workspace* proj = &workspace(workspaceUri);
292 		if (proj is &fallbackWorkspace && workspaceUri.length)
293 		{
294 			error("Did not find workspace ", workspaceUri, " when syncing config?");
295 			return false;
296 		}
297 
298 		ConfigurationItem[] items;
299 		if (workspaceUri.length)
300 			items = getConfigurationItems(proj.folder.uri);
301 		else
302 			items = getGlobalConfigurationItems();
303 
304 		trace("Sending workspace/configuration request for ", workspaceUri);
305 		auto res = rpc.sendRequest("workspace/configuration", ConfigurationParams(items));
306 
307 		JSONValue[] settings = res.validateConfigurationItemsResponse();
308 		if (!settings.length)
309 			return false;
310 
311 		string[] changed = proj.config.replaceAllSections(settings);
312 		string uri = workspaceUri.length ? proj.folder.uri : null;
313 		changedConfig(uri, changed, proj.config,
314 				workspaceUri.length == 0, index, numConfigs);
315 		return true;
316 	}
317 	else
318 		return false;
319 }
320 
321 ConfigurationItem[] getGlobalConfigurationItems()
322 {
323 	ConfigurationItem[] items = new ConfigurationItem[configurationSections.length];
324 	foreach (i, section; configurationSections)
325 		items[i] = ConfigurationItem(Optional!string.init, opt(section));
326 	return items;
327 }
328 
329 ConfigurationItem[] getConfigurationItems(DocumentUri uri)
330 {
331 	ConfigurationItem[] items = new ConfigurationItem[configurationSections.length];
332 	foreach (i, section; configurationSections)
333 		items[i] = ConfigurationItem(opt(uri), opt(section));
334 	return items;
335 }
336 
337 JSONValue[] validateConfigurationItemsResponse(scope return ref ResponseMessage res,
338 		size_t expected = size_t.max)
339 {
340 	if (res.result.type != JSONType.array)
341 	{
342 		error("Got invalid configuration response from language client. (not an array)");
343 		trace("Response: ", res);
344 		return null;
345 	}
346 
347 	JSONValue[] settings = res.result.array;
348 	if (settings.length % configurationSections.length != 0)
349 	{
350 		error("Got invalid configuration response from language client. (invalid length)");
351 		trace("Response: ", res);
352 		return null;
353 	}
354 	if (expected != size_t.max)
355 	{
356 		auto total = settings.length / configurationSections.length;
357 		if (total > expected)
358 		{
359 			warning("Loading different amount of workspaces than requested: requested ",
360 					expected, " but loading ", total);
361 		}
362 		else if (total < expected)
363 		{
364 			error("Didn't get all configs we asked for: requested ", expected, " but loading ", total);
365 			return null;
366 		}
367 	}
368 	return settings;
369 }
370 
371 string[] getPossibleSourceRoots(string workspaceFolder)
372 {
373 	import std.path : isAbsolute;
374 	import std.file;
375 
376 	auto confPaths = config(workspaceFolder.uriFromFile, false).d.projectImportPaths.map!(
377 			a => a.isAbsolute ? a : buildNormalizedPath(workspaceRoot, a));
378 	if (!confPaths.empty)
379 		return confPaths.array;
380 	auto a = buildNormalizedPath(workspaceFolder, "source");
381 	auto b = buildNormalizedPath(workspaceFolder, "src");
382 	if (exists(a))
383 		return [a];
384 	if (exists(b))
385 		return [b];
386 	return [workspaceFolder];
387 }
388 
389 __gshared bool syncedConfiguration = false;
390 __gshared bool syncingConfiguration = false;
391 __gshared bool startedUp = false;
392 InitializeResult initialize(InitializeParams params)
393 {
394 	import std.file : chdir;
395 
396 	if (params.trace == "verbose")
397 		globalLogLevel = LogLevel.trace;
398 
399 	capabilities = params.capabilities;
400 	trace("initialize params:");
401 	prettyPrintStruct!trace(params);
402 
403 	if (params.workspaceFolders.length)
404 		workspaces = params.workspaceFolders.map!(a => Workspace(a,
405 				served.types.Configuration.init)).array;
406 	else if (params.rootUri.length)
407 		workspaces = [
408 			Workspace(WorkspaceFolder(params.rootUri, "Root"), served.types.Configuration.init)
409 		];
410 	else if (params.rootPath.length)
411 		workspaces = [
412 			Workspace(WorkspaceFolder(params.rootPath.uriFromFile, "Root"),
413 					served.types.Configuration.init)
414 		];
415 
416 	if (workspaces.length)
417 	{
418 		fallbackWorkspace.folder = workspaces[0].folder;
419 		fallbackWorkspace.initialized = true;
420 	}
421 	else
422 	{
423 		import std.path : buildPath;
424 		import std.file : tempDir, exists, mkdir;
425 
426 		auto tmpFolder = buildPath(tempDir, "serve-d-dummy-workspace");
427 		if (!tmpFolder.exists)
428 			mkdir(tmpFolder);
429 		fallbackWorkspace.folder = WorkspaceFolder(tmpFolder.uriFromFile, "serve-d dummy tmp folder");
430 		fallbackWorkspace.initialized = true;
431 	}
432 
433 	InitializeResult result;
434 	result.capabilities.textDocumentSync = documents.syncKind;
435 	// only provide fixes when doCompleteSnippets is requested
436 	result.capabilities.completionProvider = CompletionOptions(doCompleteSnippets, [
437 			".", "=", "/", "*", "+", "-"
438 			], CompletionOptions.CompletionItem(true.opt).opt);
439 	result.capabilities.signatureHelpProvider = SignatureHelpOptions([
440 			"(", "[", ","
441 			]);
442 	result.capabilities.workspaceSymbolProvider = true;
443 	result.capabilities.definitionProvider = true;
444 	result.capabilities.hoverProvider = true;
445 	result.capabilities.codeActionProvider = true;
446 	result.capabilities.codeLensProvider = CodeLensOptions(true);
447 	result.capabilities.documentSymbolProvider = true;
448 	result.capabilities.documentFormattingProvider = true;
449 	result.capabilities.documentRangeFormattingProvider = true;
450 	result.capabilities.colorProvider = ColorProviderOptions();
451 	result.capabilities.documentHighlightProvider = true;
452 	result.capabilities.workspace = opt(ServerWorkspaceCapabilities(
453 			opt(ServerWorkspaceCapabilities.WorkspaceFolders(opt(true), opt(true)))));
454 
455 	setTimeout({
456 		if (!syncedConfiguration && !syncingConfiguration)
457 		{
458 			if (capabilities.workspace.configuration)
459 			{
460 				if (!syncConfiguration(null, 0, workspaces.length + 1))
461 					error("Syncing user configuration failed!");
462 
463 				warning(
464 					"Didn't receive any configuration notification, manually requesting all configurations now");
465 
466 				foreach (i, ref workspace; workspaces)
467 					syncConfiguration(workspace.folder.uri, i + 1, workspaces.length + 1);
468 			}
469 			else
470 			{
471 				warning("This Language Client doesn't support configuration requests and also didn't send any ",
472 					"configuration to serve-d. Initializing using default configuration");
473 
474 				changedConfig(workspaces[0].folder.uri, null, workspaces[0].config);
475 				fallbackWorkspace.config = workspaces[0].config;
476 			}
477 
478 			reportProgress(ProgressType.configFinish, 0, 0);
479 		}
480 	}, 1000);
481 
482 	return result;
483 }
484 
485 void ensureStartedUp()
486 {
487 	if (startedUp)
488 		return;
489 	startedUp = true;
490 	doGlobalStartup();
491 }
492 
493 void doGlobalStartup()
494 {
495 	try
496 	{
497 		trace("Initializing serve-d for global access");
498 
499 		backend.globalConfiguration.base = JSONValue(
500 				[
501 				"dcd": JSONValue([
502 						"clientPath": JSONValue(firstConfig.d.dcdClientPath.userPath),
503 						"serverPath": JSONValue(firstConfig.d.dcdServerPath.userPath),
504 						"port": JSONValue(9166)
505 					]),
506 				"dmd": JSONValue(["path": JSONValue(firstConfig.d.dmdPath.userPath)])
507 				]);
508 
509 		trace("Setup global configuration as " ~ backend.globalConfiguration.base.toString);
510 
511 		reportProgress(ProgressType.globalStartup, 0, 0, "Initializing serve-d...");
512 
513 		trace("Registering dub");
514 		backend.register!DubComponent(false);
515 		trace("Registering fsworkspace");
516 		backend.register!FSWorkspaceComponent(false);
517 		trace("Registering dcd");
518 		backend.register!DCDComponent;
519 		trace("Registering dcdext");
520 		backend.register!DCDExtComponent;
521 		trace("Registering dmd");
522 		backend.register!DMDComponent;
523 		trace("Starting dscanner");
524 		backend.register!DscannerComponent;
525 		trace("Starting dfmt");
526 		backend.register!DfmtComponent;
527 		trace("Starting dlangui");
528 		backend.register!DlanguiComponent;
529 		trace("Starting importer");
530 		backend.register!ImporterComponent;
531 		trace("Starting moduleman");
532 		backend.register!ModulemanComponent;
533 		trace("Starting snippets");
534 		backend.register!SnippetsComponent;
535 
536 		if (!backend.has!DCDComponent || backend.get!DCDComponent.isOutdated)
537 		{
538 			auto installed = backend.has!DCDComponent
539 				? backend.get!DCDComponent.serverInstalledVersion : "none";
540 
541 			string outdatedMessage = translate!"d.served.outdatedDCD"(
542 					DCDComponent.latestKnownVersion.to!(string[]).join("."), installed);
543 
544 			dcdUpdating = true;
545 			dcdUpdateReason = format!"DCD is outdated. Expected: %(%s.%), got %s"(
546 					DCDComponent.latestKnownVersion, installed);
547 			if (firstConfig.d.aggressiveUpdate)
548 				spawnFiber((&updateDCD).toDelegate);
549 			else
550 			{
551 				spawnFiber({
552 					static if (isDCDFromSource)
553 						auto action = translate!"d.ext.compileProgram"("DCD");
554 					else
555 						auto action = translate!"d.ext.downloadProgram"("DCD");
556 
557 					auto res = rpc.window.requestMessage(MessageType.error, outdatedMessage, [
558 							action
559 						]);
560 
561 					if (res == action)
562 						spawnFiber((&updateDCD).toDelegate);
563 				}, 4);
564 			}
565 		}
566 
567 		cast(void)emitExtensionEvent!onRegisteredComponents;
568 	}
569 	catch (Exception e)
570 	{
571 		error("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
572 		error("Failed to fully globally initialize:");
573 		error(e);
574 		error("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
575 	}
576 }
577 
578 /// A root which could be started up on load
579 struct RootSuggestion
580 {
581 	/// Absolute filesystem path to the project root (assuming passed in root was absolute)
582 	string dir;
583 	///
584 	bool useDub;
585 }
586 
587 RootSuggestion[] rootsForProject(string root, bool recursive, string[] blocked,
588 		string[] extra)
589 {
590 	RootSuggestion[] ret;
591 	void addSuggestion(string dir, bool useDub)
592 	{
593 		dir = buildNormalizedPath(dir);
594 
595 		if (dir.endsWith('/', '\\'))
596 			dir = dir[0 .. $ - 1];
597 
598 		if (!ret.canFind!(a => a.dir == dir))
599 			ret ~= RootSuggestion(dir, useDub);
600 	}
601 
602 	bool rootDub = fs.exists(chainPath(root, "dub.json")) || fs.exists(chainPath(root, "dub.sdl"));
603 	if (!rootDub && fs.exists(chainPath(root, "package.json")))
604 	{
605 		try
606 		{
607 			auto packageJson = fs.readText(chainPath(root, "package.json"));
608 			auto json = parseJSON(packageJson);
609 			if (seemsLikeDubJson(json))
610 				rootDub = true;
611 		}
612 		catch (Exception)
613 		{
614 		}
615 	}
616 	addSuggestion(root, rootDub);
617 
618 	if (recursive)
619 	{
620 		PackageDescriptorLoop: foreach (pkg; tryDirEntries(root, "dub.{json,sdl}", fs.SpanMode.breadth))
621 		{
622 			auto dir = dirName(pkg);
623 			if (dir.canFind(".dub"))
624 				continue;
625 			if (dir == root)
626 				continue;
627 			if (blocked.any!(a => globMatch(dir.relativePath(root), a)
628 					|| globMatch(pkg.relativePath(root), a) || globMatch((dir ~ "/").relativePath, a)))
629 				continue;
630 			addSuggestion(dir, true);
631 		}
632 	}
633 	foreach (dir; extra)
634 	{
635 		string p = buildNormalizedPath(root, dir);
636 		addSuggestion(p, fs.exists(chainPath(p, "dub.json")) || fs.exists(chainPath(p, "dub.sdl")));
637 	}
638 	info("Root Suggestions: ", ret);
639 	return ret;
640 }
641 
642 void doStartup(string workspaceUri)
643 {
644 	ensureStartedUp();
645 
646 	Workspace* proj = &workspace(workspaceUri);
647 	if (proj is &fallbackWorkspace)
648 	{
649 		error("Trying to do startup on unknown workspace ", workspaceUri, "?");
650 		return;
651 	}
652 	trace("Initializing serve-d for " ~ workspaceUri);
653 
654 	struct Root
655 	{
656 		RootSuggestion root;
657 		string uri;
658 		WorkspaceD.Instance instance;
659 	}
660 
661 	bool gotOneDub;
662 	scope roots = appender!(Root[]);
663 
664 	auto rootSuggestions = rootsForProject(workspaceUri.uriToFile, proj.config.d.scanAllFolders,
665 			proj.config.d.disabledRootGlobs, proj.config.d.extraRoots);
666 
667 	foreach (i, root; rootSuggestions)
668 	{
669 		reportProgress(ProgressType.workspaceStartup, i, rootSuggestions.length, root.dir.uriFromFile);
670 		info("registering instance for root ", root);
671 
672 		auto workspaceRoot = root.dir;
673 		workspaced.api.Configuration config;
674 		config.base = JSONValue([
675 				"dcd": JSONValue([
676 						"clientPath": JSONValue(proj.config.d.dcdClientPath.userPath),
677 						"serverPath": JSONValue(proj.config.d.dcdServerPath.userPath),
678 						"port": JSONValue(9166)
679 					]),
680 				"dmd": JSONValue(["path": JSONValue(proj.config.d.dmdPath.userPath)])
681 				]);
682 		auto instance = backend.addInstance(workspaceRoot, config);
683 		if (!activeInstance)
684 			activeInstance = instance;
685 
686 		roots ~= Root(root, workspaceUri, instance);
687 		emitExtensionEvent!onProjectAvailable(instance, workspaceRoot, workspaceUri);
688 
689 		if (auto lazyInstance = cast(LazyWorkspaceD.LazyInstance)instance)
690 		{
691 			auto lazyLoadCallback(WorkspaceD.Instance instance, string workspaceRoot, string workspaceUri, RootSuggestion root)
692 			{
693 				return () => delayedProjectActivation(instance, workspaceRoot, workspaceUri, root);
694 			}
695 
696 			lazyInstance.onLazyLoadInstance(lazyLoadCallback(instance, workspaceRoot, workspaceUri, root));
697 		}
698 		else
699 		{
700 			delayedProjectActivation(instance, workspaceRoot, workspaceUri, root);
701 		}
702 	}
703 
704 	trace("Starting auto completion service...");
705 	StopWatch dcdTimer;
706 	dcdTimer.start();
707 	foreach (i, root; roots.data)
708 	{
709 		reportProgress(ProgressType.completionStartup, i, roots.data.length,
710 				root.instance.cwd.uriFromFile);
711 
712 		lazyStartDCDServer(root.instance, root.uri);
713 	}
714 	dcdTimer.stop();
715 	trace("Started all completion servers in ", dcdTimer.peek);
716 }
717 
718 shared int totalLoadedProjects;
719 void delayedProjectActivation(WorkspaceD.Instance instance, string workspaceRoot, string workspaceUri, RootSuggestion root)
720 {
721 	import core.atomic;
722 
723 	Workspace* proj = &workspace(workspaceUri);
724 	if (proj is &fallbackWorkspace)
725 	{
726 		error("Trying to do startup on unknown workspace ", root.dir, "?");
727 		throw new Exception("failed project instance startup for " ~ root.dir);
728 	}
729 
730 	auto numLoaded = atomicOp!"+="(totalLoadedProjects, 1);
731 
732 	auto manyProjectsAction = cast(ManyProjectsAction) proj.config.d.manyProjectsAction;
733 	auto manyThreshold = proj.config.d.manyProjectsThreshold;
734 	if (manyThreshold > 0 && numLoaded > manyThreshold)
735 	{
736 		switch (manyProjectsAction)
737 		{
738 		case ManyProjectsAction.ask:
739 			auto loadButton = translate!"d.served.tooManySubprojects.load";
740 			auto skipButton = translate!"d.served.tooManySubprojects.skip";
741 			auto res = rpc.window.requestMessage(MessageType.warning,
742 					translate!"d.served.tooManySubprojects.path"(root.dir),
743 					[loadButton, skipButton]);
744 			if (res != loadButton)
745 				goto case ManyProjectsAction.skip;
746 			break;
747 		case ManyProjectsAction.load:
748 			break;
749 		default:
750 			error("Ignoring invalid manyProjectsAction value ", manyProjectsAction, ", defaulting to skip");
751 			goto case;
752 		case ManyProjectsAction.skip:
753 			backend.removeInstance(workspaceRoot);
754 			throw new Exception("skipping load of this instance");
755 		}
756 	}
757 
758 	info("Initializing instance for root ", root);
759 	StopWatch rootTimer;
760 	rootTimer.start();
761 
762 	emitExtensionEvent!onAddingProject(instance, workspaceRoot, workspaceUri);
763 
764 	bool disableDub = proj.config.d.neverUseDub || !root.useDub;
765 	bool loadedDub;
766 	Exception err;
767 	if (!disableDub)
768 	{
769 		trace("Starting dub...");
770 		reportProgress(ProgressType.dubReload, 0, 1, workspaceUri);
771 		scope (exit)
772 			reportProgress(ProgressType.dubReload, 1, 1, workspaceUri);
773 
774 		try
775 		{
776 			if (backend.attachEager(instance, "dub", err))
777 			{
778 				scope (failure)
779 					instance.detach!DubComponent;
780 
781 				instance.get!DubComponent.validateConfiguration();
782 				loadedDub = true;
783 			}
784 		}
785 		catch (Exception e)
786 		{
787 			err = e;
788 			loadedDub = false;
789 		}
790 
791 		if (!loadedDub)
792 			error("Exception starting dub: ", err);
793 		else
794 			trace("Started dub with root dependencies ", instance.get!DubComponent.rootDependencies);
795 	}
796 	if (!loadedDub)
797 	{
798 		if (!disableDub)
799 		{
800 			error("Failed starting dub in ", root, " - falling back to fsworkspace");
801 			proj.startupError(workspaceRoot, translate!"d.ext.dubFail"(instance.cwd, err ? err.msg : ""));
802 		}
803 		try
804 		{
805 			trace("Starting fsworkspace...");
806 
807 			instance.config.set("fsworkspace", "additionalPaths",
808 					getPossibleSourceRoots(workspaceRoot));
809 			if (!backend.attachEager(instance, "fsworkspace", err))
810 				throw new Exception("Attach returned failure: " ~ err.msg);
811 		}
812 		catch (Exception e)
813 		{
814 			error(e);
815 			proj.startupError(workspaceRoot, translate!"d.ext.fsworkspaceFail"(instance.cwd));
816 		}
817 	}
818 	else
819 		didLoadDubProject();
820 
821 	trace("Started files provider for root ", root);
822 
823 	trace("Loaded Components for ", instance.cwd, ": ",
824 			instance.instanceComponents.map!"a.info.name");
825 
826 	emitExtensionEvent!onAddedProject(instance, workspaceRoot, workspaceUri);
827 
828 	rootTimer.stop();
829 	info("Root ", root, " initialized in ", rootTimer.peek);
830 }
831 
832 void didLoadDubProject()
833 {
834 	static bool loadedDub = false;
835 	if (!loadedDub)
836 	{
837 		loadedDub = true;
838 		setTimeout({ rpc.notifyMethod("coded/initDubTree"); }, 50);
839 	}
840 }
841 
842 void removeWorkspace(string workspaceUri)
843 {
844 	auto workspaceRoot = workspaceRootFor(workspaceUri);
845 	if (!workspaceRoot.length)
846 		return;
847 	backend.removeInstance(workspaceRoot);
848 	workspace(workspaceUri).disabled = true;
849 }
850 
851 void handleBroadcast(WorkspaceD workspaced, WorkspaceD.Instance instance, JSONValue data)
852 {
853 	if (!instance)
854 		return;
855 	auto type = "type" in data;
856 	if (type && type.type == JSONType..string && type.str == "crash")
857 	{
858 		if (data["component"].str == "dcd")
859 			spawnFiber(() {
860 				startDCDServer(instance, instance.cwd.uriFromFile);
861 			});
862 	}
863 }
864 
865 bool wantsDCDServer(string workspaceUri)
866 {
867 	if (shutdownRequested || dcdUpdating)
868 		return false;
869 	Workspace* proj = &workspace(workspaceUri, false);
870 	if (proj is &fallbackWorkspace)
871 	{
872 		error("Trying to access DCD on unknown workspace ", workspaceUri, "?");
873 		return false;
874 	}
875 	if (!proj.config.d.enableAutoComplete)
876 	{
877 		return false;
878 	}
879 
880 	return true;
881 }
882 
883 void startDCDServer(WorkspaceD.Instance instance, string workspaceUri)
884 {
885 	if (!wantsDCDServer(workspaceUri))
886 		return;
887 	Workspace* proj = &workspace(workspaceUri, false);
888 	assert(proj, "project unloaded while starting DCD?!");
889 
890 	trace("Running DCD setup");
891 	try
892 	{
893 		auto dcd = instance.get!DCDComponent;
894 		auto stdlibPath = proj.stdlibPath;
895 		trace("startServer ", stdlibPath);
896 		dcd.startServer(stdlibPath, false, true);
897 		trace("refreshImports");
898 		dcd.refreshImports();
899 	}
900 	catch (Exception e)
901 	{
902 		rpc.window.showErrorMessage(translate!"d.ext.dcdFail"(instance.cwd,
903 				instance.config.get("dcd", "errorlog", "")));
904 		error(e);
905 		trace("Instance Config: ", instance.config);
906 		return;
907 	}
908 	info("Imports for ", instance.cwd, ": ", instance.importPaths);
909 }
910 
911 void lazyStartDCDServer(WorkspaceD.Instance instance, string workspaceUri)
912 {
913 	auto lazyInstance = cast(LazyWorkspaceD.LazyInstance)instance;
914 	if (lazyInstance)
915 	{
916 		lazyInstance.onLazyLoad("dcd", delegate() nothrow {
917 			try
918 			{
919 				reportProgress(ProgressType.importReload, 0, 1, workspaceUri);
920 				scope (exit)
921 					reportProgress(ProgressType.importReload, 1, 1, workspaceUri);
922 				startDCDServer(instance, workspaceUri);
923 			}
924 			catch (Exception e)
925 			{
926 				try
927 				{
928 					error("Failed loading DCD on demand: ", e);
929 				}
930 				catch (Exception)
931 				{
932 				}
933 			}
934 		});
935 	}
936 	else
937 		startDCDServer(instance, workspaceUri);
938 }
939 
940 string determineOutputFolder()
941 {
942 	import std.process : environment;
943 
944 	version (linux)
945 	{
946 		if (fs.exists(buildPath(environment["HOME"], ".local", "share")))
947 			return buildPath(environment["HOME"], ".local", "share", "code-d", "bin");
948 		else
949 			return buildPath(environment["HOME"], ".code-d", "bin");
950 	}
951 	else version (Windows)
952 	{
953 		return buildPath(environment["APPDATA"], "code-d", "bin");
954 	}
955 	else
956 	{
957 		return buildPath(environment["HOME"], ".code-d", "bin");
958 	}
959 }
960 
961 @protocolMethod("shutdown")
962 JSONValue shutdown()
963 {
964 	if (!backend)
965 		return JSONValue(null);
966 	backend.shutdown();
967 	served.extension.setTimeout({
968 		throw new Error("RPC still running 1s after shutdown");
969 	}, 1.seconds);
970 	return JSONValue(null);
971 }
972 
973 // === Protocol Notifications starting here ===
974 
975 @protocolNotification("$/setTrace")
976 void setTrace(TraceParams params)
977 {
978 	if (params.value == "verbose")
979 		globalLogLevel = LogLevel.trace;
980 	else
981 		globalLogLevel = LogLevel.info;
982 }
983 
984 @protocolNotification("workspace/didChangeWorkspaceFolders")
985 void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params)
986 {
987 	foreach (toRemove; params.event.removed)
988 		removeWorkspace(toRemove.uri);
989 	foreach (i, toAdd; params.event.added)
990 	{
991 		workspaces ~= Workspace(toAdd);
992 		syncConfiguration(toAdd.uri, i, params.event.added.length);
993 		doStartup(toAdd.uri);
994 	}
995 }
996 
997 @protocolNotification("textDocument/didOpen")
998 void onDidOpenDocument(DidOpenTextDocumentParams params)
999 {
1000 	string lintSetting = config(params.textDocument.uri).d.lintOnFileOpen;
1001 	bool shouldLint;
1002 	if (lintSetting == "always")
1003 		shouldLint = true;
1004 	else if (lintSetting == "project")
1005 		shouldLint = workspaceIndex(params.textDocument.uri) != size_t.max;
1006 
1007 	if (shouldLint)
1008 		onDidChangeDocument(DocumentLinkParams(TextDocumentIdentifier(params.textDocument.uri)));
1009 }
1010 
1011 @protocolNotification("textDocument/didClose")
1012 void onDidCloseDocument(DidOpenTextDocumentParams params)
1013 {
1014 	// remove lint warnings for external projects
1015 	if (workspaceIndex(params.textDocument.uri) == size_t.max)
1016 	{
1017 		import served.linters.diagnosticmanager : diagnostics, updateDiagnostics;
1018 
1019 		foreach (ref coll; diagnostics)
1020 			foreach (ref diag; coll)
1021 				if (diag.uri == params.textDocument.uri)
1022 					diag.diagnostics = null;
1023 
1024 		updateDiagnostics(params.textDocument.uri);
1025 	}
1026 	// but keep warnings in local projects
1027 }
1028 
1029 int genericChangeTimeout;
1030 @protocolNotification("textDocument/didChange")
1031 void onDidChangeDocument(DocumentLinkParams params)
1032 {
1033 	auto document = documents[params.textDocument.uri];
1034 	if (document.getLanguageId != "d")
1035 		return;
1036 
1037 	doDscanner(params);
1038 
1039 	int delay = document.length > 50 * 1024 ? 500 : 50; // be slower after 50KiB
1040 	clearTimeout(genericChangeTimeout);
1041 	genericChangeTimeout = setTimeout({
1042 		import served.linters.dfmt : lint;
1043 
1044 		lint(document);
1045 		// Delay to avoid too many requests
1046 	}, delay);
1047 }
1048 
1049 int dscannerChangeTimeout;
1050 @protocolNotification("coded/doDscanner")  // deprecated alias
1051 @protocolNotification("served/doDscanner")
1052 void doDscanner(DocumentLinkParams params)
1053 {
1054 	auto document = documents[params.textDocument.uri];
1055 	if (document.getLanguageId != "d")
1056 		return;
1057 	auto d = config(params.textDocument.uri).d;
1058 	if (!d.enableStaticLinting || !d.enableLinting)
1059 		return;
1060 
1061 	int delay = document.length > 50 * 1024 ? 1000 : 200; // be slower after 50KiB
1062 	clearTimeout(dscannerChangeTimeout);
1063 	dscannerChangeTimeout = setTimeout({
1064 		import served.linters.dscanner;
1065 
1066 		lint(document);
1067 		// Delay to avoid too many requests
1068 	}, delay);
1069 }
1070 
1071 @protocolMethod("served/getDscannerConfig")
1072 DScannerIniSection[] getDscannerConfig(DocumentLinkParams params)
1073 {
1074 	import served.linters.dscanner : getDscannerIniForDocument;
1075 
1076 	auto instance = backend.getBestInstance!DscannerComponent(
1077 			params.textDocument.uri.uriToFile);
1078 
1079 	if (!instance)
1080 		return null;
1081 
1082 	string ini = "dscanner.ini";
1083 	if (params.textDocument.uri.length)
1084 		ini = getDscannerIniForDocument(params.textDocument.uri, instance);
1085 
1086 	auto config = instance.get!DscannerComponent.getConfig(ini);
1087 
1088 	DScannerIniSection sec;
1089 	sec.description = __traits(getAttributes, typeof(config))[0].msg;
1090 	sec.name = __traits(getAttributes, typeof(config))[0].name;
1091 
1092 	DScannerIniFeature feature;
1093 	foreach (i, ref val; config.tupleof)
1094 	{
1095 		static if (is(typeof(val) == string))
1096 		{
1097 			feature = DScannerIniFeature.init;
1098 			feature.description = __traits(getAttributes, config.tupleof[i])[0].msg;
1099 			feature.name = __traits(identifier, config.tupleof[i]);
1100 			feature.enabled = val;
1101 			sec.features ~= feature;
1102 		}
1103 	}
1104 
1105 	return [sec];
1106 }
1107 
1108 @protocolNotification("textDocument/didSave")
1109 void onDidSaveDocument(DidSaveTextDocumentParams params)
1110 {
1111 	auto workspaceRoot = workspaceRootFor(params.textDocument.uri);
1112 	auto config = workspace(params.textDocument.uri).config;
1113 	auto document = documents[params.textDocument.uri];
1114 	auto fileName = params.textDocument.uri.uriToFile.baseName;
1115 
1116 	if (document.getLanguageId == "d" || document.getLanguageId == "diet")
1117 	{
1118 		if (!config.d.enableLinting)
1119 			return;
1120 		joinAll({
1121 			if (config.d.enableStaticLinting)
1122 			{
1123 				import served.linters.dscanner;
1124 
1125 				lint(document);
1126 				clearTimeout(dscannerChangeTimeout);
1127 			}
1128 		}, {
1129 			if (config.d.enableDubLinting)
1130 			{
1131 				import served.linters.dub;
1132 
1133 				lint(document);
1134 			}
1135 		});
1136 	}
1137 }
1138 
1139 shared static this()
1140 {
1141 	import core.time : MonoTime;
1142 	startupTime = MonoTime.currTime();
1143 }
1144 
1145 shared static this()
1146 {
1147 	backend = new LazyWorkspaceD();
1148 
1149 	backend.onBroadcast = (&handleBroadcast).toDelegate;
1150 	backend.onBindFail = (WorkspaceD.Instance instance, ComponentFactory factory, Exception err) {
1151 		if (!instance && err.msg.canFind("requires to be instanced"))
1152 			return;
1153 
1154 		if (factory.info.name == "dcd")
1155 		{
1156 			error("Failed to attach DCD component to ", instance ? instance.cwd : null, ": ", err.msg);
1157 			if (instance && !dcdUpdating)
1158 				instance.config.set("dcd", "errorlog", instance.config.get("dcd",
1159 						"errorlog", "") ~ "\n" ~ err.msg);
1160 			return;
1161 		}
1162 
1163 		tracef("bind fail:\n\tinstance %s\n\tfactory %s\n\tstacktrace:\n%s\n------",
1164 				instance, factory.info.name, err);
1165 		if (instance)
1166 		{
1167 			rpc.window.showErrorMessage(
1168 					"Failed to load component " ~ factory.info.name ~ " for workspace "
1169 					~ instance.cwd ~ "\n\nError: " ~ err.msg);
1170 		}
1171 	};
1172 }
1173 
1174 shared static ~this()
1175 {
1176 	if (backend)
1177 		backend.shutdown();
1178 }