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