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