1 module served.extension;
2 
3 import core.exception;
4 import core.thread : Fiber;
5 import core.sync.mutex;
6 
7 import std.algorithm;
8 import std.array;
9 import std.conv;
10 import std.datetime.systime;
11 import std.datetime.stopwatch;
12 import fs = std.file;
13 import std.experimental.logger;
14 import std.functional;
15 import std.json;
16 import std.path;
17 import std.regex;
18 import io = std.stdio;
19 import std.string;
20 import rm.rf;
21 
22 import served.ddoc;
23 import served.fibermanager;
24 import served.types;
25 import served.translate;
26 
27 import workspaced.api;
28 import workspaced.com.dcd;
29 import workspaced.com.importer;
30 import workspaced.coms;
31 
32 import served.linters.dub : DubDiagnosticSource;
33 
34 /// Set to true when shutdown is called
35 __gshared bool shutdownRequested;
36 
37 bool safe(alias fn, Args...)(Args args)
38 {
39 	try
40 	{
41 		fn(args);
42 		return true;
43 	}
44 	catch (Exception e)
45 	{
46 		error(e);
47 		return false;
48 	}
49 	catch (AssertError e)
50 	{
51 		error(e);
52 		return false;
53 	}
54 }
55 
56 void changedConfig(string workspaceUri, string[] paths, served.types.Configuration config)
57 {
58 	StopWatch sw;
59 	sw.start();
60 
61 	if (!syncedConfiguration)
62 	{
63 		syncedConfiguration = true;
64 		doGlobalStartup();
65 	}
66 	Workspace* proj = &workspace(workspaceUri);
67 	if (proj is &fallbackWorkspace)
68 	{
69 		error("Did not find workspace ", workspaceUri, " when updating config?");
70 		return;
71 	}
72 	if (!proj.initialized)
73 	{
74 		doStartup(proj.folder.uri);
75 		proj.initialized = true;
76 	}
77 
78 	auto workspaceFs = workspaceUri.uriToFile;
79 
80 	foreach (path; paths)
81 	{
82 		switch (path)
83 		{
84 		case "d.stdlibPath":
85 			backend.get!DCDComponent(workspaceFs).addImports(config.stdlibPath);
86 			break;
87 		case "d.projectImportPaths":
88 			backend.get!DCDComponent(workspaceFs).addImports(config.d.projectImportPaths);
89 			break;
90 		case "d.dubConfiguration":
91 			auto configs = backend.get!DubComponent(workspaceFs).configurations;
92 			if (configs.length == 0)
93 				rpc.window.showInformationMessage(translate!"d.ext.noConfigurations.project");
94 			else
95 			{
96 				auto defaultConfig = config.d.dubConfiguration;
97 				if (defaultConfig.length)
98 				{
99 					if (!configs.canFind(defaultConfig))
100 						rpc.window.showErrorMessage(
101 								translate!"d.ext.config.invalid.configuration"(defaultConfig));
102 					else
103 						backend.get!DubComponent(workspaceFs).setConfiguration(defaultConfig);
104 				}
105 				else
106 					backend.get!DubComponent(workspaceFs).setConfiguration(configs[0]);
107 			}
108 			break;
109 		case "d.dubArchType":
110 			if (config.d.dubArchType.length && !backend.get!DubComponent(workspaceFs)
111 					.setArchType(JSONValue(["arch-type" : JSONValue(config.d.dubArchType)])))
112 				rpc.window.showErrorMessage(
113 						translate!"d.ext.config.invalid.archType"(config.d.dubArchType));
114 			break;
115 		case "d.dubBuildType":
116 			if (config.d.dubBuildType.length && !backend.get!DubComponent(workspaceFs)
117 					.setBuildType(JSONValue(["build-type" : JSONValue(config.d.dubBuildType)])))
118 				rpc.window.showErrorMessage(
119 						translate!"d.ext.config.invalid.buildType"(config.d.dubBuildType));
120 			break;
121 		case "d.dubCompiler":
122 			if (config.d.dubCompiler.length && !backend.get!DubComponent(workspaceFs)
123 					.setCompiler(config.d.dubCompiler))
124 				rpc.window.showErrorMessage(
125 						translate!"d.ext.config.invalid.compiler"(config.d.dubCompiler));
126 			break;
127 		default:
128 			break;
129 		}
130 	}
131 
132 	trace("Finished config change of ", workspaceUri, " with ", paths.length,
133 			" changes in ", sw.peek, ".");
134 }
135 
136 void processConfigChange(served.types.Configuration configuration)
137 {
138 	import painlessjson : fromJSON;
139 
140 	if (capabilities.workspace.configuration && workspaces.length >= 2)
141 	{
142 		ConfigurationItem[] items;
143 		foreach (workspace; workspaces)
144 			foreach (section; configurationSections)
145 				items ~= ConfigurationItem(opt(workspace.folder.uri), opt(section));
146 		auto res = rpc.sendRequest("workspace/configuration", ConfigurationParams(items));
147 		if (res.result.type == JSON_TYPE.ARRAY)
148 		{
149 			JSONValue[] settings = res.result.array;
150 			if (settings.length % configurationSections.length != 0)
151 			{
152 				error("Got invalid configuration response from language client.");
153 				trace("Response: ", res);
154 				return;
155 			}
156 			for (size_t i = 0; i < settings.length; i += configurationSections.length)
157 			{
158 				string[] changed;
159 				static foreach (n, section; configurationSections)
160 					changed ~= workspaces[i / configurationSections.length].config.replaceSection!section(
161 							settings[i + n].fromJSON!(configurationTypes[n]));
162 				changedConfig(workspaces[i / configurationSections.length].folder.uri,
163 						changed, workspaces[i / configurationSections.length].config);
164 			}
165 		}
166 	}
167 	else if (workspaces.length)
168 	{
169 		if (workspaces.length > 1)
170 			error(
171 					"Client does not support configuration request, only applying config for first workspace.");
172 		served.extension.changedConfig(workspaces[0].folder.uri,
173 				workspaces[0].config.replace(configuration), workspaces[0].config);
174 	}
175 }
176 
177 bool syncConfiguration(string workspaceUri)
178 {
179 	import painlessjson : fromJSON;
180 
181 	if (capabilities.workspace.configuration)
182 	{
183 		Workspace* proj = &workspace(workspaceUri);
184 		if (proj is &fallbackWorkspace)
185 		{
186 			error("Did not find workspace ", workspaceUri, " when syncing config?");
187 			return false;
188 		}
189 		ConfigurationItem[] items;
190 		foreach (section; configurationSections)
191 			items ~= ConfigurationItem(opt(proj.folder.uri), opt(section));
192 		auto res = rpc.sendRequest("workspace/configuration", ConfigurationParams(items));
193 		if (res.result.type == JSON_TYPE.ARRAY)
194 		{
195 			JSONValue[] settings = res.result.array;
196 			if (settings.length % configurationSections.length != 0)
197 			{
198 				error("Got invalid configuration response from language client.");
199 				trace("Response: ", res);
200 				return false;
201 			}
202 			string[] changed;
203 			static foreach (n, section; configurationSections)
204 				changed ~= proj.config.replaceSection!section(
205 						settings[n].fromJSON!(configurationTypes[n]));
206 			changedConfig(proj.folder.uri, changed, proj.config);
207 			return true;
208 		}
209 		else
210 			return false;
211 	}
212 	else
213 		return false;
214 }
215 
216 string[] getPossibleSourceRoots(string workspaceFolder)
217 {
218 	import std.file;
219 
220 	auto confPaths = config(workspaceFolder.uriFromFile, false).d.projectImportPaths.map!(
221 			a => a.isAbsolute ? a : buildNormalizedPath(workspaceRoot, a));
222 	if (!confPaths.empty)
223 		return confPaths.array;
224 	auto a = buildNormalizedPath(workspaceFolder, "source");
225 	auto b = buildNormalizedPath(workspaceFolder, "src");
226 	if (exists(a))
227 		return [a];
228 	if (exists(b))
229 		return [b];
230 	return [workspaceFolder];
231 }
232 
233 __gshared bool syncedConfiguration = false;
234 InitializeResult initialize(InitializeParams params)
235 {
236 	import std.file : chdir;
237 
238 	capabilities = params.capabilities;
239 	trace("Set capabilities to ", params);
240 
241 	if (params.workspaceFolders.length)
242 		workspaces = params.workspaceFolders.map!(a => Workspace(a,
243 				served.types.Configuration.init)).array;
244 	else if (params.rootPath.length)
245 		workspaces = [Workspace(WorkspaceFolder(params.rootPath.uriFromFile,
246 				"Root"), served.types.Configuration.init)];
247 	if (workspaces.length)
248 	{
249 		fallbackWorkspace.folder = workspaces[0].folder;
250 		fallbackWorkspace.initialized = true;
251 	}
252 
253 	InitializeResult result;
254 	result.capabilities.textDocumentSync = documents.syncKind;
255 	result.capabilities.completionProvider = CompletionOptions(false, [".", "(", "[", "="]);
256 	result.capabilities.signatureHelpProvider = SignatureHelpOptions(["(", "[", ","]);
257 	result.capabilities.workspaceSymbolProvider = true;
258 	result.capabilities.definitionProvider = true;
259 	result.capabilities.hoverProvider = true;
260 	result.capabilities.codeActionProvider = true;
261 	result.capabilities.codeLensProvider = CodeLensOptions(true);
262 	result.capabilities.documentSymbolProvider = true;
263 	result.capabilities.documentFormattingProvider = true;
264 	result.capabilities.codeActionProvider = true;
265 	result.capabilities.workspace = opt(ServerWorkspaceCapabilities(
266 			opt(ServerWorkspaceCapabilities.WorkspaceFolders(opt(true), opt(true)))));
267 
268 	setTimeout({
269 		if (!syncedConfiguration && capabilities.workspace.configuration)
270 			foreach (ref workspace; workspaces)
271 				syncConfiguration(workspace.folder.uri);
272 	}, 1000);
273 
274 	return result;
275 }
276 
277 void doGlobalStartup()
278 {
279 	try
280 	{
281 		trace("Initializing serve-d for global access");
282 
283 		backend.globalConfiguration.base = JSONValue(["dcd" : JSONValue(["clientPath"
284 				: JSONValue(firstConfig.d.dcdClientPath), "serverPath"
285 				: JSONValue(firstConfig.d.dcdServerPath), "port" : JSONValue(9166)]),
286 				"dmd" : JSONValue(["path" : JSONValue(firstConfig.d.dmdPath)])]);
287 
288 		trace("Setup global configuration as " ~ backend.globalConfiguration.base.toString);
289 
290 		trace("Registering dub");
291 		backend.register!DubComponent(false);
292 		trace("Registering fsworkspace");
293 		backend.register!FSWorkspaceComponent(false);
294 		trace("Registering dcd");
295 		backend.register!DCDComponent(false);
296 		trace("Registering dcdext");
297 		backend.register!DCDExtComponent(false);
298 		trace("Registering dmd");
299 		backend.register!DMDComponent(false);
300 		trace("Starting dscanner");
301 		backend.register!DscannerComponent;
302 		trace("Starting dfmt");
303 		backend.register!DfmtComponent;
304 		trace("Starting dlangui");
305 		backend.register!DlanguiComponent;
306 		trace("Starting importer");
307 		backend.register!ImporterComponent;
308 		trace("Starting moduleman");
309 		backend.register!ModulemanComponent;
310 
311 		if (backend.get!DCDComponent.isOutdated)
312 		{
313 			if (firstConfig.d.aggressiveUpdate)
314 				spawnFiber((&updateDCD).toDelegate);
315 			else
316 			{
317 				spawnFiber({
318 					auto action = translate!"d.ext.compileProgram"("DCD");
319 					auto res = rpc.window.requestMessage(MessageType.error, translate!"d.served.failDCD"(firstWorkspaceRootUri,
320 						firstConfig.d.dcdClientPath, firstConfig.d.dcdServerPath), [action]);
321 					if (res == action)
322 						spawnFiber((&updateDCD).toDelegate);
323 				});
324 			}
325 		}
326 	}
327 	catch (Exception e)
328 	{
329 		error("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
330 		error("Failed to fully globally initialize:");
331 		error(e);
332 		error("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
333 	}
334 }
335 
336 struct RootSuggestion
337 {
338 	string dir;
339 	bool useDub;
340 }
341 
342 RootSuggestion[] rootsForProject(string root, bool recursive, string[] blocked, string[] extra)
343 {
344 	RootSuggestion[] ret;
345 	bool rootDub = fs.exists(chainPath(root, "dub.json")) || fs.exists(chainPath(root, "dub.sdl"));
346 	if (!rootDub && fs.exists(chainPath(root, "package.json")))
347 	{
348 		auto packageJson = fs.readText(chainPath(root, "package.json"));
349 		try
350 		{
351 			auto json = parseJSON(packageJson);
352 			if (seemsLikeDubJson(json))
353 				rootDub = true;
354 		}
355 		catch (Exception)
356 		{
357 		}
358 	}
359 	ret ~= RootSuggestion(root, rootDub);
360 	if (recursive)
361 		foreach (pkg; fs.dirEntries(root, "dub.{json,sdl}", fs.SpanMode.depth))
362 		{
363 			auto dir = dirName(pkg);
364 			if (dir.canFind(".dub"))
365 				continue;
366 			if (dir == root)
367 				continue;
368 			if (blocked.any!(a => globMatch(dir.relativePath(root), a)
369 					|| globMatch(pkg.relativePath(root), a) || globMatch((dir ~ "/").relativePath, a)))
370 				continue;
371 			ret ~= RootSuggestion(dir, true);
372 		}
373 	foreach (dir; extra)
374 	{
375 		string p = buildNormalizedPath(root, dir);
376 		if (!ret.canFind!(a => a.dir == p))
377 			ret ~= RootSuggestion(p, fs.exists(chainPath(p, "dub.json"))
378 					|| fs.exists(chainPath(p, "dub.sdl")));
379 	}
380 	info("Root Suggestions: ", ret);
381 	return ret;
382 }
383 
384 void doStartup(string workspaceUri)
385 {
386 	Workspace* proj = &workspace(workspaceUri);
387 	if (proj is &fallbackWorkspace)
388 	{
389 		error("Trying to do startup on unknown workspace ", workspaceUri, "?");
390 		return;
391 	}
392 	trace("Initializing serve-d for " ~ workspaceUri);
393 
394 	foreach (root; rootsForProject(workspaceUri.uriToFile, proj.config.d.scanAllFolders,
395 			proj.config.d.disabledRootGlobs, proj.config.d.extraRoots))
396 	{
397 		auto workspaceRoot = root.dir;
398 		workspaced.api.Configuration config;
399 		config.base = JSONValue(["dcd" : JSONValue(["clientPath"
400 				: JSONValue(proj.config.d.dcdClientPath), "serverPath"
401 				: JSONValue(proj.config.d.dcdServerPath), "port" : JSONValue(9166)]),
402 				"dmd" : JSONValue(["path" : JSONValue(proj.config.d.dmdPath)])]);
403 		auto instance = backend.addInstance(workspaceRoot, config);
404 
405 		bool disableDub = proj.config.d.neverUseDub || !root.useDub;
406 		bool loadedDub;
407 		if (!disableDub)
408 		{
409 			trace("Starting dub...");
410 			try
411 			{
412 				if (backend.attach(instance, "dub"))
413 					loadedDub = true;
414 			}
415 			catch (Exception e)
416 			{
417 				error("Exception starting dub: ", e);
418 			}
419 		}
420 		if (!loadedDub)
421 		{
422 			if (!disableDub)
423 			{
424 				error("Failed starting dub in ", root, " - falling back to fsworkspace");
425 				proj.startupError(workspaceRoot, translate!"d.ext.dubFail"(instance.cwd));
426 			}
427 			try
428 			{
429 				instance.config.set("fsworkspace", "additionalPaths",
430 						getPossibleSourceRoots(workspaceRoot));
431 				if (!backend.attach(instance, "fsworkspace"))
432 					throw new Exception("Attach returned failure");
433 			}
434 			catch (Exception e)
435 			{
436 				error(e);
437 				proj.startupError(workspaceRoot, translate!"d.ext.fsworkspaceFail"(instance.cwd));
438 			}
439 		}
440 		else
441 			setTimeout({ rpc.notifyMethod("coded/initDubTree"); }, 50);
442 
443 		if (!backend.attach(instance, "dmd"))
444 			error("Failed to attach DMD component to ", workspaceUri);
445 		startDCD(instance, workspaceUri);
446 
447 		trace("Loaded Components for ", instance.cwd, ": ",
448 				instance.instanceComponents.map!"a.info.name");
449 	}
450 }
451 
452 void removeWorkspace(string workspaceUri)
453 {
454 	auto workspaceRoot = workspaceRootFor(workspaceUri);
455 	if (!workspaceRoot.length)
456 		return;
457 	backend.removeInstance(workspaceRoot);
458 	workspace(workspaceUri).disabled = true;
459 }
460 
461 void handleBroadcast(WorkspaceD workspaced, WorkspaceD.Instance instance, JSONValue data)
462 {
463 	if (!instance)
464 		return;
465 	auto type = "type" in data;
466 	if (type && type.type == JSON_TYPE.STRING && type.str == "crash")
467 	{
468 		if (data["component"].str == "dcd")
469 			spawnFiber(() => startDCD(instance, instance.cwd.uriFromFile));
470 	}
471 }
472 
473 void startDCD(WorkspaceD.Instance instance, string workspaceUri)
474 {
475 	if (shutdownRequested)
476 		return;
477 	Workspace* proj = &workspace(workspaceUri, false);
478 	if (proj is &fallbackWorkspace)
479 	{
480 		error("Trying to start DCD on unknown workspace ", workspaceUri, "?");
481 		return;
482 	}
483 	trace("Starting dcd");
484 	if (!backend.attach(instance, "dcd"))
485 		error("Failed to attach DCD component to ", instance.cwd);
486 	trace("Starting dcdext");
487 	if (!backend.attach(instance, "dcdext"))
488 		error("Failed to attach DCD component to ", instance.cwd);
489 	trace("Running DCD setup");
490 	try
491 	{
492 		trace("findAndSelectPort 9166");
493 		auto port = backend.get!DCDComponent(instance.cwd)
494 			.findAndSelectPort(cast(ushort) 9166).getYield;
495 		trace("Setting port to ", port);
496 		instance.config.set("dcd", "port", cast(int) port);
497 		trace("startServer ", proj.config.stdlibPath);
498 		backend.get!DCDComponent(instance.cwd).startServer(proj.config.stdlibPath);
499 		trace("refreshImports");
500 		backend.get!DCDComponent(instance.cwd).refreshImports();
501 	}
502 	catch (Exception e)
503 	{
504 		rpc.window.showErrorMessage(translate!"d.ext.dcdFail"(instance.cwd));
505 		error(e);
506 		trace("Instance Config: ", instance.config);
507 		return;
508 	}
509 	info("Imports for ", instance.cwd, ": ", backend.getInstance(instance.cwd).importPaths);
510 
511 	auto globalDCD = backend.get!DCDComponent;
512 	if (!globalDCD.isActive)
513 	{
514 		globalDCD.fromRunning(globalDCD.getSupportsFullOutput, globalDCD.isUsingUnixDomainSockets
515 				? globalDCD.getSocketFile : "", globalDCD.isUsingUnixDomainSockets ? 0
516 				: globalDCD.getRunningPort);
517 	}
518 }
519 
520 string determineOutputFolder()
521 {
522 	import std.process : environment;
523 
524 	version (linux)
525 	{
526 		if (fs.exists(buildPath(environment["HOME"], ".local", "share")))
527 			return buildPath(environment["HOME"], ".local", "share", "code-d", "bin");
528 		else
529 			return buildPath(environment["HOME"], ".code-d", "bin");
530 	}
531 	else version (Windows)
532 	{
533 		return buildPath(environment["APPDATA"], "code-d", "bin");
534 	}
535 	else
536 	{
537 		return buildPath(environment["HOME"], ".code-d", "bin");
538 	}
539 }
540 
541 @protocolNotification("served/updateDCD")
542 void updateDCD()
543 {
544 	rpc.notifyMethod("coded/logInstall", "Installing DCD");
545 	string outputFolder = determineOutputFolder;
546 	if (fs.exists(outputFolder))
547 		rmdirRecurseForce(outputFolder);
548 	if (!fs.exists(outputFolder))
549 		fs.mkdirRecurse(outputFolder);
550 	string[] platformOptions;
551 	version (Windows)
552 		platformOptions = ["--arch=x86_mscoff"];
553 	bool success = compileDependency(outputFolder, "DCD",
554 			"https://github.com/Hackerpilot/DCD.git", [[firstConfig.git.path,
555 			"submodule", "update", "--init", "--recursive"], ["dub", "build",
556 			"--config=client"] ~ platformOptions, ["dub", "build", "--config=server"] ~ platformOptions]);
557 	if (success)
558 	{
559 		string ext = "";
560 		version (Windows)
561 			ext = ".exe";
562 		string finalDestinationClient = buildPath(outputFolder, "DCD", "dcd-client" ~ ext);
563 		if (!fs.exists(finalDestinationClient))
564 			finalDestinationClient = buildPath(outputFolder, "DCD", "bin", "dcd-client" ~ ext);
565 		string finalDestinationServer = buildPath(outputFolder, "DCD", "dcd-server" ~ ext);
566 		if (!fs.exists(finalDestinationServer))
567 			finalDestinationServer = buildPath(outputFolder, "DCD", "bin", "dcd-server" ~ ext);
568 		foreach (ref workspace; workspaces)
569 		{
570 			workspace.config.d.dcdClientPath = finalDestinationClient;
571 			workspace.config.d.dcdServerPath = finalDestinationServer;
572 		}
573 		rpc.notifyMethod("coded/updateSetting", UpdateSettingParams("dcdClientPath",
574 				JSONValue(finalDestinationClient), true));
575 		rpc.notifyMethod("coded/updateSetting", UpdateSettingParams("dcdServerPath",
576 				JSONValue(finalDestinationServer), true));
577 		rpc.notifyMethod("coded/logInstall", "Successfully installed DCD");
578 		foreach (ref workspace; workspaces)
579 		{
580 			auto instance = backend.getInstance(workspace.folder.uri.uriToFile);
581 			if (instance is null)
582 				rpc.notifyMethod("coded/logInstall",
583 						"Failed to find workspace to start DCD for " ~ workspace.folder.uri);
584 			else
585 				startDCD(instance, workspace.folder.uri);
586 		}
587 	}
588 }
589 
590 bool compileDependency(string cwd, string name, string gitURI, string[][] commands)
591 {
592 	import std.process;
593 
594 	int run(string[] cmd, string cwd)
595 	{
596 		import core.thread;
597 
598 		rpc.notifyMethod("coded/logInstall", "> " ~ cmd.join(" "));
599 		auto stdin = pipe();
600 		auto stdout = pipe();
601 		auto pid = spawnProcess(cmd, stdin.readEnd, stdout.writeEnd,
602 				stdout.writeEnd, null, Config.none, cwd);
603 		stdin.writeEnd.close();
604 		size_t i;
605 		string[] lines;
606 		bool done;
607 		new Thread({
608 			scope (exit)
609 				done = true;
610 			foreach (line; stdout.readEnd.byLine)
611 				lines ~= line.idup;
612 		}).start();
613 		while (!pid.tryWait().terminated || !done || i < lines.length)
614 		{
615 			if (i < lines.length)
616 			{
617 				rpc.notifyMethod("coded/logInstall", lines[i++]);
618 			}
619 			Fiber.yield();
620 		}
621 		return pid.wait;
622 	}
623 
624 	rpc.notifyMethod("coded/logInstall", "Installing into " ~ cwd);
625 	try
626 	{
627 		auto newCwd = buildPath(cwd, name);
628 		if (fs.exists(newCwd))
629 		{
630 			rpc.notifyMethod("coded/logInstall", "Deleting old installation from " ~ newCwd);
631 			try
632 			{
633 				rmdirRecurseForce(newCwd);
634 			}
635 			catch (Exception)
636 			{
637 				rpc.notifyMethod("coded/logInstall", "WARNING: Failed to delete " ~ newCwd);
638 			}
639 		}
640 		auto ret = run([firstConfig.git.path, "clone", "--recursive", "--depth=1", gitURI, name], cwd);
641 		if (ret != 0)
642 			throw new Exception("git ended with error code " ~ ret.to!string);
643 		foreach (command; commands)
644 			run(command, newCwd);
645 		return true;
646 	}
647 	catch (Exception e)
648 	{
649 		rpc.notifyMethod("coded/logInstall", "Failed to install " ~ name);
650 		rpc.notifyMethod("coded/logInstall", e.toString);
651 		return false;
652 	}
653 }
654 
655 @protocolMethod("shutdown")
656 JSONValue shutdown()
657 {
658 	shutdownRequested = true;
659 	backend.shutdown();
660 	backend.destroy();
661 	served.extension.setTimeout({
662 		throw new Error("RPC still running 1s after shutdown");
663 	}, 1.seconds);
664 	return JSONValue(null);
665 }
666 
667 CompletionItemKind convertFromDCDType(string type)
668 {
669 	switch (type)
670 	{
671 	case "c":
672 		return CompletionItemKind.class_;
673 	case "i":
674 		return CompletionItemKind.interface_;
675 	case "s":
676 	case "u":
677 		return CompletionItemKind.unit;
678 	case "a":
679 	case "A":
680 	case "v":
681 		return CompletionItemKind.variable;
682 	case "m":
683 	case "e":
684 		return CompletionItemKind.field;
685 	case "k":
686 		return CompletionItemKind.keyword;
687 	case "f":
688 		return CompletionItemKind.function_;
689 	case "g":
690 		return CompletionItemKind.enum_;
691 	case "P":
692 	case "M":
693 		return CompletionItemKind.module_;
694 	case "l":
695 		return CompletionItemKind.reference;
696 	case "t":
697 	case "T":
698 		return CompletionItemKind.property;
699 	default:
700 		return CompletionItemKind.text;
701 	}
702 }
703 
704 SymbolKind convertFromDCDSearchType(string type)
705 {
706 	switch (type)
707 	{
708 	case "c":
709 		return SymbolKind.class_;
710 	case "i":
711 		return SymbolKind.interface_;
712 	case "s":
713 	case "u":
714 		return SymbolKind.package_;
715 	case "a":
716 	case "A":
717 	case "v":
718 		return SymbolKind.variable;
719 	case "m":
720 	case "e":
721 		return SymbolKind.field;
722 	case "f":
723 	case "l":
724 		return SymbolKind.function_;
725 	case "g":
726 		return SymbolKind.enum_;
727 	case "P":
728 	case "M":
729 		return SymbolKind.namespace;
730 	case "t":
731 	case "T":
732 		return SymbolKind.property;
733 	case "k":
734 	default:
735 		return cast(SymbolKind) 0;
736 	}
737 }
738 
739 SymbolKind convertFromDscannerType(string type)
740 {
741 	switch (type)
742 	{
743 	case "g":
744 		return SymbolKind.enum_;
745 	case "e":
746 		return SymbolKind.field;
747 	case "v":
748 		return SymbolKind.variable;
749 	case "i":
750 		return SymbolKind.interface_;
751 	case "c":
752 		return SymbolKind.class_;
753 	case "s":
754 		return SymbolKind.class_;
755 	case "f":
756 		return SymbolKind.function_;
757 	case "u":
758 		return SymbolKind.class_;
759 	case "T":
760 		return SymbolKind.property;
761 	case "a":
762 		return SymbolKind.field;
763 	default:
764 		return cast(SymbolKind) 0;
765 	}
766 }
767 
768 string substr(T)(string s, T start, T end)
769 {
770 	if (!s.length)
771 		return "";
772 	if (start < 0)
773 		start = 0;
774 	if (start >= s.length)
775 		start = s.length - 1;
776 	if (end > s.length)
777 		end = s.length;
778 	if (end < start)
779 		return s[start .. start];
780 	return s[start .. end];
781 }
782 
783 string[] extractFunctionParameters(string sig, bool exact = false)
784 {
785 	if (!sig.length)
786 		return [];
787 	string[] params;
788 	ptrdiff_t i = sig.length - 1;
789 
790 	if (sig[i] == ')' && !exact)
791 		i--;
792 
793 	ptrdiff_t paramEnd = i + 1;
794 
795 	void skipStr()
796 	{
797 		i--;
798 		if (sig[i + 1] == '\'')
799 			for (; i >= 0; i--)
800 				if (sig[i] == '\'')
801 					return;
802 		bool escapeNext = false;
803 		while (i >= 0)
804 		{
805 			if (sig[i] == '\\')
806 				escapeNext = false;
807 			if (escapeNext)
808 				break;
809 			if (sig[i] == '"')
810 				escapeNext = true;
811 			i--;
812 		}
813 	}
814 
815 	void skip(char open, char close)
816 	{
817 		i--;
818 		int depth = 1;
819 		while (i >= 0 && depth > 0)
820 		{
821 			if (sig[i] == '"' || sig[i] == '\'')
822 				skipStr();
823 			else
824 			{
825 				if (sig[i] == close)
826 					depth++;
827 				else if (sig[i] == open)
828 					depth--;
829 				i--;
830 			}
831 		}
832 	}
833 
834 	while (i >= 0)
835 	{
836 		switch (sig[i])
837 		{
838 		case ',':
839 			params ~= sig.substr(i + 1, paramEnd).strip;
840 			paramEnd = i;
841 			i--;
842 			break;
843 		case ';':
844 		case '(':
845 			auto param = sig.substr(i + 1, paramEnd).strip;
846 			if (param.length)
847 				params ~= param;
848 			reverse(params);
849 			return params;
850 		case ')':
851 			skip('(', ')');
852 			break;
853 		case '}':
854 			skip('{', '}');
855 			break;
856 		case ']':
857 			skip('[', ']');
858 			break;
859 		case '"':
860 		case '\'':
861 			skipStr();
862 			break;
863 		default:
864 			i--;
865 			break;
866 		}
867 	}
868 	reverse(params);
869 	return params;
870 }
871 
872 unittest
873 {
874 	void assertEqual(A, B)(A a, B b)
875 	{
876 		import std.conv : to;
877 
878 		assert(a == b, a.to!string ~ " is not equal to " ~ b.to!string);
879 	}
880 
881 	assertEqual(extractFunctionParameters("void foo()"), cast(string[])[]);
882 	assertEqual(extractFunctionParameters(`auto bar(int foo, Button, my.Callback cb)`),
883 			["int foo", "Button", "my.Callback cb"]);
884 	assertEqual(extractFunctionParameters(`SomeType!(int, "int_") foo(T, Args...)(T a, T b, string[string] map, Other!"(" stuff1, SomeType!(double, ")double") myType, Other!"(" stuff, Other!")")`),
885 			["T a", "T b", "string[string] map", `Other!"(" stuff1`,
886 			`SomeType!(double, ")double") myType`, `Other!"(" stuff`, `Other!")"`]);
887 	assertEqual(extractFunctionParameters(`SomeType!(int,"int_")foo(T,Args...)(T a,T b,string[string] map,Other!"(" stuff1,SomeType!(double,")double")myType,Other!"(" stuff,Other!")")`),
888 			["T a", "T b", "string[string] map", `Other!"(" stuff1`,
889 			`SomeType!(double,")double")myType`, `Other!"(" stuff`, `Other!")"`]);
890 	assertEqual(extractFunctionParameters(`some_garbage(code); before(this); funcCall(4`,
891 			true), [`4`]);
892 	assertEqual(extractFunctionParameters(`some_garbage(code); before(this); funcCall(4, f(4)`,
893 			true), [`4`, `f(4)`]);
894 	assertEqual(extractFunctionParameters(`some_garbage(code); before(this); funcCall(4, ["a"], JSONValue(["b": JSONValue("c")]), recursive(func, call!s()), "texts )\"(too"`,
895 			true), [`4`, `["a"]`, `JSONValue(["b": JSONValue("c")])`,
896 			`recursive(func, call!s())`, `"texts )\"(too"`]);
897 }
898 
899 // === Protocol Methods starting here ===
900 
901 @protocolMethod("textDocument/completion")
902 CompletionList provideComplete(TextDocumentPositionParams params)
903 {
904 	import painlessjson : fromJSON;
905 
906 	auto workspaceRoot = workspaceRootFor(params.textDocument.uri);
907 	Document document = documents[params.textDocument.uri];
908 	if (document.uri.toLower.endsWith("dscanner.ini"))
909 	{
910 		auto possibleFields = backend.get!DscannerComponent.listAllIniFields;
911 		auto line = document.lineAt(params.position).strip;
912 		auto defaultList = CompletionList(false, possibleFields.map!(a => CompletionItem(a.name,
913 				CompletionItemKind.field.opt, Optional!string.init, MarkupContent(a.documentation)
914 				.opt, Optional!string.init, Optional!string.init, (a.name ~ '=').opt)).array);
915 		if (!line.length)
916 			return defaultList;
917 		//dfmt off
918 		if (line[0] == '[')
919 			return CompletionList(false, [
920 				CompletionItem("analysis.config.StaticAnalysisConfig", CompletionItemKind.keyword.opt),
921 				CompletionItem("analysis.config.ModuleFilters", CompletionItemKind.keyword.opt, Optional!string.init,
922 					MarkupContent("In this optional section a comma-separated list of inclusion and exclusion"
923 					~ " selectors can be specified for every check on which selective filtering"
924 					~ " should be applied. These given selectors match on the module name and"
925 					~ " partial matches (std. or .foo.) are possible. Moreover, every selectors"
926 					~ " must begin with either + (inclusion) or - (exclusion). Exclusion selectors"
927 					~ " take precedence over all inclusion operators.").opt)
928 			]);
929 		//dfmt on
930 		auto eqIndex = line.indexOf('=');
931 		auto quotIndex = line.lastIndexOf('"');
932 		if (quotIndex != -1 && params.position.character >= quotIndex)
933 			return CompletionList.init;
934 		if (params.position.character < eqIndex)
935 			return defaultList;
936 		else//dfmt off
937 			return CompletionList(false, [
938 				CompletionItem(`"disabled"`, CompletionItemKind.value.opt, "Check is disabled".opt),
939 				CompletionItem(`"enabled"`, CompletionItemKind.value.opt, "Check is enabled".opt),
940 				CompletionItem(`"skip-unittest"`, CompletionItemKind.value.opt,
941 					"Check is enabled but not operated in the unittests".opt)
942 			]);
943 		//dfmt on
944 	}
945 	else
946 	{
947 		if (document.languageId != "d")
948 			return CompletionList.init;
949 		string line = document.lineAt(params.position);
950 		string prefix = line[0 .. min($, params.position.character)];
951 		CompletionItem[] completion;
952 		if (prefix.strip == "///" || prefix.strip == "*")
953 		{
954 			foreach (compl; import("ddocs.txt").lineSplitter)
955 			{
956 				auto item = CompletionItem(compl, CompletionItemKind.snippet.opt);
957 				item.insertText = compl ~ ": ";
958 				completion ~= item;
959 			}
960 			return CompletionList(false, completion);
961 		}
962 		auto byteOff = cast(int) document.positionToBytes(params.position);
963 		DCDCompletions result = DCDCompletions.empty;
964 		joinAll({
965 			if (backend.has!DCDComponent(workspaceRoot))
966 				result = backend.get!DCDComponent(workspaceRoot)
967 					.listCompletion(document.text, byteOff).getYield;
968 		}, {
969 			if (!line.strip.length)
970 			{
971 				auto defs = backend.get!DscannerComponent(workspaceRoot)
972 					.listDefinitions(uriToFile(params.textDocument.uri), document.text).getYield;
973 				ptrdiff_t di = -1;
974 				FuncFinder: foreach (i, def; defs)
975 				{
976 					for (int n = 1; n < 5; n++)
977 						if (def.line == params.position.line + n)
978 						{
979 							di = i;
980 							break FuncFinder;
981 						}
982 				}
983 				if (di == -1)
984 					return;
985 				auto def = defs[di];
986 				auto sig = "signature" in def.attributes;
987 				if (!sig)
988 				{
989 					CompletionItem doc = CompletionItem("///");
990 					doc.kind = CompletionItemKind.snippet;
991 					doc.insertTextFormat = InsertTextFormat.snippet;
992 					auto eol = document.eolAt(params.position.line).toString;
993 					doc.insertText = "/// ";
994 					CompletionItem doc2 = doc;
995 					doc2.label = "/**";
996 					doc2.insertText = "/** " ~ eol ~ " * $0" ~ eol ~ " */";
997 					completion ~= doc;
998 					completion ~= doc2;
999 					return;
1000 				}
1001 				auto funcArgs = extractFunctionParameters(*sig);
1002 				string[] docs;
1003 				if (def.name.matchFirst(ctRegex!`^[Gg]et([^a-z]|$)`))
1004 					docs ~= "Gets $0";
1005 				else if (def.name.matchFirst(ctRegex!`^[Ss]et([^a-z]|$)`))
1006 					docs ~= "Sets $0";
1007 				else if (def.name.matchFirst(ctRegex!`^[Ii]s([^a-z]|$)`))
1008 					docs ~= "Checks if $0";
1009 				else
1010 					docs ~= "$0";
1011 				int argNo = 1;
1012 				foreach (arg; funcArgs)
1013 				{
1014 					auto space = arg.lastIndexOf(' ');
1015 					if (space == -1)
1016 						continue;
1017 					string identifier = arg[space + 1 .. $];
1018 					if (!identifier.matchFirst(ctRegex!`[a-zA-Z_][a-zA-Z0-9_]*`))
1019 						continue;
1020 					if (argNo == 1)
1021 						docs ~= "Params:";
1022 					docs ~= "  " ~ identifier ~ " = $" ~ argNo.to!string;
1023 					argNo++;
1024 				}
1025 				auto retAttr = "return" in def.attributes;
1026 				if (retAttr && *retAttr != "void")
1027 				{
1028 					docs ~= "Returns: $" ~ argNo.to!string;
1029 					argNo++;
1030 				}
1031 				auto depr = "deprecation" in def.attributes;
1032 				if (depr)
1033 				{
1034 					docs ~= "Deprecated: $" ~ argNo.to!string ~ *depr;
1035 					argNo++;
1036 				}
1037 				CompletionItem doc = CompletionItem("///");
1038 				doc.kind = CompletionItemKind.snippet;
1039 				doc.insertTextFormat = InsertTextFormat.snippet;
1040 				auto eol = document.eolAt(params.position.line).toString;
1041 				doc.insertText = docs.map!(a => "/// " ~ a).join(eol);
1042 				CompletionItem doc2 = doc;
1043 				doc2.label = "/**";
1044 				doc2.insertText = "/** " ~ eol ~ docs.map!(a => " * " ~ a ~ eol).join() ~ " */";
1045 				completion ~= doc;
1046 				completion ~= doc2;
1047 			}
1048 		});
1049 		switch (result.type)
1050 		{
1051 		case DCDCompletions.Type.identifiers:
1052 			foreach (identifier; result.identifiers)
1053 			{
1054 				CompletionItem item;
1055 				item.label = identifier.identifier;
1056 				item.kind = identifier.type.convertFromDCDType;
1057 				if (identifier.documentation.length)
1058 					item.documentation = MarkupContent(identifier.documentation.ddocToMarked);
1059 				if (identifier.definition.length)
1060 				{
1061 					item.detail = identifier.definition;
1062 					item.sortText = identifier.definition;
1063 					// TODO: only add arguments when this is a function call, eg not on template arguments
1064 					if (identifier.type == "f" && workspace(params.textDocument.uri)
1065 							.config.d.argumentSnippets)
1066 					{
1067 						item.insertTextFormat = InsertTextFormat.snippet;
1068 						string args;
1069 						auto parts = identifier.definition.extractFunctionParameters;
1070 						if (parts.length)
1071 						{
1072 							bool isOptional;
1073 							string[] optionals;
1074 							int numRequired;
1075 							foreach (i, part; parts)
1076 							{
1077 								if (!isOptional)
1078 									isOptional = part.canFind('=');
1079 								if (isOptional)
1080 									optionals ~= part;
1081 								else
1082 								{
1083 									if (args.length)
1084 										args ~= ", ";
1085 									args ~= "${" ~ (i + 1).to!string ~ ":" ~ part ~ "}";
1086 									numRequired++;
1087 								}
1088 							}
1089 							foreach (i, part; optionals)
1090 							{
1091 								if (args.length)
1092 									part = ", " ~ part;
1093 								// Go through optionals in reverse
1094 								args ~= "${" ~ (numRequired + optionals.length - i).to!string ~ ":" ~ part ~ "}";
1095 							}
1096 							item.insertText = identifier.identifier ~ "(${0:" ~ args ~ "})";
1097 						}
1098 					}
1099 				}
1100 				completion ~= item;
1101 			}
1102 			goto case;
1103 		case DCDCompletions.Type.calltips:
1104 			return CompletionList(false, completion);
1105 		default:
1106 			throw new Exception("Unexpected result from DCD:\n\t" ~ result.raw.join("\n\t"));
1107 		}
1108 	}
1109 }
1110 
1111 @protocolMethod("textDocument/signatureHelp")
1112 SignatureHelp provideSignatureHelp(TextDocumentPositionParams params)
1113 {
1114 	auto workspaceRoot = workspaceRootFor(params.textDocument.uri);
1115 	auto document = documents[params.textDocument.uri];
1116 	if (document.languageId != "d")
1117 		return SignatureHelp.init;
1118 	auto pos = cast(int) document.positionToBytes(params.position);
1119 	DCDCompletions result = backend.get!DCDComponent(workspaceRoot)
1120 		.listCompletion(document.text, pos).getYield;
1121 	SignatureInformation[] signatures;
1122 	int[] paramsCounts;
1123 	SignatureHelp help;
1124 	switch (result.type)
1125 	{
1126 	case DCDCompletions.Type.calltips:
1127 		foreach (i, calltip; result.calltips)
1128 		{
1129 			auto sig = SignatureInformation(calltip);
1130 			immutable DCDCompletions.Symbol symbol = result.symbols[i];
1131 			if (symbol.documentation.length)
1132 				sig.documentation = MarkupContent(symbol.documentation.ddocToMarked);
1133 			auto funcParams = calltip.extractFunctionParameters;
1134 
1135 			paramsCounts ~= cast(int) funcParams.length - 1;
1136 			foreach (param; funcParams)
1137 				sig.parameters ~= ParameterInformation(param);
1138 
1139 			help.signatures ~= sig;
1140 		}
1141 		auto extractedParams = document.text[0 .. pos].extractFunctionParameters(true);
1142 		help.activeParameter = max(0, cast(int) extractedParams.length - 1);
1143 		size_t[] possibleFunctions;
1144 		foreach (i, count; paramsCounts)
1145 			if (count >= cast(int) extractedParams.length - 1)
1146 				possibleFunctions ~= i;
1147 		help.activeSignature = possibleFunctions.length ? cast(int) possibleFunctions[0] : 0;
1148 		goto case;
1149 	case DCDCompletions.Type.identifiers:
1150 		return help;
1151 	default:
1152 		throw new Exception("Unexpected result from DCD");
1153 	}
1154 }
1155 
1156 @protocolMethod("workspace/symbol")
1157 SymbolInformation[] provideWorkspaceSymbols(WorkspaceSymbolParams params)
1158 {
1159 	import std.file;
1160 
1161 	// TODO: combine all workspaces
1162 	auto result = backend.get!DCDComponent(workspaceRoot).searchSymbol(params.query).getYield;
1163 	SymbolInformation[] infos;
1164 	TextDocumentManager extraCache;
1165 	foreach (symbol; result.array)
1166 	{
1167 		auto uri = uriFromFile(symbol.file);
1168 		auto doc = documents.tryGet(uri);
1169 		Location location;
1170 		if (!doc.uri)
1171 			doc = extraCache.tryGet(uri);
1172 		if (!doc.uri)
1173 		{
1174 			doc = Document(uri);
1175 			try
1176 			{
1177 				doc.text = readText(symbol.file);
1178 			}
1179 			catch (Exception e)
1180 			{
1181 				error(e);
1182 			}
1183 		}
1184 		if (doc.text)
1185 		{
1186 			location = Location(doc.uri, TextRange(doc.bytesToPosition(cast(size_t) symbol.position)));
1187 			infos ~= SymbolInformation(params.query, convertFromDCDSearchType(symbol.type), location);
1188 		}
1189 	}
1190 	return infos;
1191 }
1192 
1193 @protocolMethod("textDocument/documentSymbol")
1194 SymbolInformation[] provideDocumentSymbols(DocumentSymbolParams params)
1195 {
1196 	auto workspaceRoot = workspaceRootFor(params.textDocument.uri);
1197 	auto document = documents[params.textDocument.uri];
1198 	auto result = backend.get!DscannerComponent(workspaceRoot)
1199 		.listDefinitions(uriToFile(params.textDocument.uri), document.text).getYield;
1200 	SymbolInformation[] ret;
1201 	foreach (def; result)
1202 	{
1203 		SymbolInformation info;
1204 		info.name = def.name;
1205 		info.location.uri = params.textDocument.uri;
1206 		info.location.range = TextRange(Position(cast(uint) def.line - 1, 0));
1207 		info.kind = convertFromDscannerType(def.type);
1208 		if (def.type == "f" && def.name == "this")
1209 			info.kind = SymbolKind.constructor;
1210 		string* ptr;
1211 		auto attribs = def.attributes;
1212 		if ((ptr = "struct" in attribs) !is null || (ptr = "class" in attribs) !is null
1213 				|| (ptr = "enum" in attribs) !is null || (ptr = "union" in attribs) !is null)
1214 			info.containerName = *ptr;
1215 		ret ~= info;
1216 	}
1217 	return ret;
1218 }
1219 
1220 @protocolMethod("textDocument/definition")
1221 ArrayOrSingle!Location provideDefinition(TextDocumentPositionParams params)
1222 {
1223 	auto workspaceRoot = workspaceRootFor(params.textDocument.uri);
1224 	auto document = documents[params.textDocument.uri];
1225 	if (document.languageId != "d")
1226 		return ArrayOrSingle!Location.init;
1227 	auto result = backend.get!DCDComponent(workspaceRoot).findDeclaration(document.text,
1228 			cast(int) document.positionToBytes(params.position)).getYield;
1229 	if (result == DCDDeclaration.init)
1230 		return ArrayOrSingle!Location.init;
1231 	auto uri = document.uri;
1232 	if (result.file != "stdin")
1233 	{
1234 		if (isAbsolute(result.file))
1235 			uri = uriFromFile(result.file);
1236 		else
1237 			uri = null;
1238 	}
1239 	size_t byteOffset = cast(size_t) result.position;
1240 	Position pos;
1241 	auto found = documents.tryGet(uri);
1242 	if (found.uri)
1243 		pos = found.bytesToPosition(byteOffset);
1244 	else
1245 	{
1246 		string abs = result.file;
1247 		if (!abs.isAbsolute)
1248 			abs = buildPath(workspaceRoot, abs);
1249 		pos = Position.init;
1250 		size_t totalLen;
1251 		foreach (line; io.File(abs).byLine(io.KeepTerminator.yes))
1252 		{
1253 			totalLen += line.length;
1254 			if (totalLen >= byteOffset)
1255 				break;
1256 			else
1257 				pos.line++;
1258 		}
1259 	}
1260 	return ArrayOrSingle!Location(Location(uri, TextRange(pos, pos)));
1261 }
1262 
1263 @protocolMethod("textDocument/formatting")
1264 TextEdit[] provideFormatting(DocumentFormattingParams params)
1265 {
1266 	auto config = workspace(params.textDocument.uri).config;
1267 	if (!config.d.enableFormatting)
1268 		return [];
1269 	auto document = documents[params.textDocument.uri];
1270 	if (document.languageId != "d")
1271 		return [];
1272 	string[] args;
1273 	if (config.d.overrideDfmtEditorconfig)
1274 	{
1275 		int maxLineLength = 120;
1276 		int softMaxLineLength = 80;
1277 		if (config.editor.rulers.length == 1)
1278 		{
1279 			maxLineLength = config.editor.rulers[0];
1280 			softMaxLineLength = maxLineLength - 40;
1281 		}
1282 		else if (config.editor.rulers.length >= 2)
1283 		{
1284 			maxLineLength = config.editor.rulers[$ - 1];
1285 			softMaxLineLength = config.editor.rulers[$ - 2];
1286 		}
1287 		//dfmt off
1288 			args = [
1289 				"--align_switch_statements", config.dfmt.alignSwitchStatements.to!string,
1290 				"--brace_style", config.dfmt.braceStyle,
1291 				"--end_of_line", document.eolAt(0).to!string,
1292 				"--indent_size", params.options.tabSize.to!string,
1293 				"--indent_style", params.options.insertSpaces ? "space" : "tab",
1294 				"--max_line_length", maxLineLength.to!string,
1295 				"--soft_max_line_length", softMaxLineLength.to!string,
1296 				"--outdent_attributes", config.dfmt.outdentAttributes.to!string,
1297 				"--space_after_cast", config.dfmt.spaceAfterCast.to!string,
1298 				"--split_operator_at_line_end", config.dfmt.splitOperatorAtLineEnd.to!string,
1299 				"--tab_width", params.options.tabSize.to!string,
1300 				"--selective_import_space", config.dfmt.selectiveImportSpace.to!string,
1301 				"--compact_labeled_statements", config.dfmt.compactLabeledStatements.to!string,
1302 				"--template_constraint_style", config.dfmt.templateConstraintStyle
1303 			];
1304 			//dfmt on
1305 	}
1306 	auto result = backend.get!DfmtComponent.format(document.text, args).getYield;
1307 	return [TextEdit(TextRange(Position(0, 0),
1308 			document.offsetToPosition(document.text.length)), result)];
1309 }
1310 
1311 @protocolMethod("textDocument/hover")
1312 Hover provideHover(TextDocumentPositionParams params)
1313 {
1314 	auto workspaceRoot = workspaceRootFor(params.textDocument.uri);
1315 	auto document = documents[params.textDocument.uri];
1316 	if (document.languageId != "d")
1317 		return Hover.init;
1318 	auto docs = backend.get!DCDComponent(workspaceRoot).getDocumentation(document.text,
1319 			cast(int) document.positionToBytes(params.position)).getYield;
1320 	Hover ret;
1321 	ret.contents = docs.ddocToMarked;
1322 	return ret;
1323 }
1324 
1325 private auto importRegex = regex(`import\s+(?:[a-zA-Z_]+\s*=\s*)?([a-zA-Z_]\w*(?:\.\w*[a-zA-Z_]\w*)*)?(\s*\:\s*(?:[a-zA-Z_,\s=]*(?://.*?[\r\n]|/\*.*?\*/|/\+.*?\+/)?)+)?;?`);
1326 private auto undefinedIdentifier = regex(
1327 		`^undefined identifier '(\w+)'(?:, did you mean .*? '(\w+)'\?)?$`);
1328 private auto undefinedTemplate = regex(`template '(\w+)' is not defined`);
1329 private auto noProperty = regex(`^no property '(\w+)'(?: for type '.*?')?$`);
1330 private auto moduleRegex = regex(`module\s+([a-zA-Z_]\w*\s*(?:\s*\.\s*[a-zA-Z_]\w*)*)\s*;`);
1331 private auto whitespace = regex(`\s*`);
1332 
1333 @protocolMethod("textDocument/codeAction")
1334 Command[] provideCodeActions(CodeActionParams params)
1335 {
1336 	auto workspaceRoot = workspaceRootFor(params.textDocument.uri);
1337 	auto document = documents[params.textDocument.uri];
1338 	if (document.languageId != "d")
1339 		return [];
1340 	Command[] ret;
1341 	if (backend.has!DCDExtComponent(workspaceRoot)) // check if extends
1342 	{
1343 		auto startIndex = document.positionToBytes(params.range.start);
1344 		ptrdiff_t idx = min(cast(ptrdiff_t) startIndex, cast(ptrdiff_t) document.text.length - 1);
1345 		while (idx > 0)
1346 		{
1347 			if (document.text[idx] == ':')
1348 			{
1349 				// probably extends
1350 				if (backend.get!DCDExtComponent(workspaceRoot)
1351 						.implement(document.text, cast(int) startIndex).getYield.strip.length > 0)
1352 					ret ~= Command("Implement base classes/interfaces", "code-d.implementMethods",
1353 							[JSONValue(document.positionToOffset(params.range.start))]);
1354 				break;
1355 			}
1356 			if (document.text[idx] == ';' || document.text[idx] == '{' || document.text[idx] == '}')
1357 				break;
1358 			idx--;
1359 		}
1360 	}
1361 	foreach (diagnostic; params.context.diagnostics)
1362 	{
1363 		if (diagnostic.source == DubDiagnosticSource)
1364 		{
1365 			auto match = diagnostic.message.matchFirst(importRegex);
1366 			if (diagnostic.message.canFind("import "))
1367 			{
1368 				if (!match)
1369 					continue;
1370 				ret ~= Command("Import " ~ match[1], "code-d.addImport",
1371 						[JSONValue(match[1]), JSONValue(document.positionToOffset(params.range[0]))]);
1372 			}
1373 			else /*if (cast(bool)(match = diagnostic.message.matchFirst(undefinedIdentifier))
1374 					|| cast(bool)(match = diagnostic.message.matchFirst(undefinedTemplate))
1375 					|| cast(bool)(match = diagnostic.message.matchFirst(noProperty)))*/
1376 			{
1377 				// temporary fix for https://issues.dlang.org/show_bug.cgi?id=18565
1378 				string[] files;
1379 				string[] modules;
1380 				int lineNo;
1381 				match = diagnostic.message.matchFirst(undefinedIdentifier);
1382 				if (match)
1383 					goto start;
1384 				match = diagnostic.message.matchFirst(undefinedTemplate);
1385 				if (match)
1386 					goto start;
1387 				match = diagnostic.message.matchFirst(noProperty);
1388 				if (match)
1389 					goto start;
1390 				goto noMatch;
1391 			start:
1392 				joinAll({
1393 					files ~= backend.get!DscannerComponent(workspaceRoot)
1394 						.findSymbol(match[1]).getYield.map!"a.file".array;
1395 				}, {
1396 					if (backend.has!DCDComponent)
1397 						files ~= backend.get!DCDComponent.searchSymbol(match[1]).getYield.map!"a.file".array;
1398 				});
1399 				foreach (file; files.sort().uniq)
1400 				{
1401 					if (!isAbsolute(file))
1402 						file = buildNormalizedPath(workspaceRoot, file);
1403 					lineNo = 0;
1404 					foreach (line; io.File(file).byLine)
1405 					{
1406 						if (++lineNo >= 100)
1407 							break;
1408 						auto match2 = line.matchFirst(moduleRegex);
1409 						if (match2)
1410 						{
1411 							modules ~= match2[1].replaceAll(whitespace, "").idup;
1412 							break;
1413 						}
1414 					}
1415 				}
1416 				foreach (mod; modules.sort().uniq)
1417 					ret ~= Command("Import " ~ mod, "code-d.addImport", [JSONValue(mod),
1418 							JSONValue(document.positionToOffset(params.range[0]))]);
1419 			noMatch:
1420 			}
1421 		}
1422 		else
1423 		{
1424 			import dscanner.analysis.imports_sortedness : ImportSortednessCheck;
1425 
1426 			if (diagnostic.message == ImportSortednessCheck.MESSAGE)
1427 			{
1428 				ret ~= Command("Sort imports", "code-d.sortImports",
1429 						[JSONValue(document.positionToOffset(params.range[0]))]);
1430 			}
1431 		}
1432 	}
1433 	return ret;
1434 }
1435 
1436 @protocolMethod("textDocument/codeLens")
1437 CodeLens[] provideCodeLens(CodeLensParams params)
1438 {
1439 	auto workspaceRoot = workspaceRootFor(params.textDocument.uri);
1440 	auto document = documents[params.textDocument.uri];
1441 	if (document.languageId != "d")
1442 		return [];
1443 	CodeLens[] ret;
1444 	if (workspace(params.textDocument.uri).config.d.enableDMDImportTiming)
1445 		foreach (match; document.text.matchAll(importRegex))
1446 		{
1447 			size_t index = match.pre.length;
1448 			auto pos = document.bytesToPosition(index);
1449 			ret ~= CodeLens(TextRange(pos), Optional!Command.init, JSONValue(["type"
1450 					: JSONValue("importcompilecheck"), "code" : JSONValue(match.hit),
1451 					"module" : JSONValue(match[1]), "workspace" : JSONValue(workspaceRoot)]));
1452 		}
1453 	return ret;
1454 }
1455 
1456 @protocolMethod("codeLens/resolve")
1457 CodeLens resolveCodeLens(CodeLens lens)
1458 {
1459 	if (lens.data.type != JSON_TYPE.OBJECT)
1460 		throw new Exception("Invalid Lens Object");
1461 	auto type = "type" in lens.data;
1462 	if (!type)
1463 		throw new Exception("No type in Lens Object");
1464 	switch (type.str)
1465 	{
1466 	case "importcompilecheck":
1467 		auto code = "code" in lens.data;
1468 		if (!code || code.type != JSON_TYPE.STRING || !code.str.length)
1469 			throw new Exception("No valid code provided");
1470 		auto module_ = "module" in lens.data;
1471 		if (!module_ || module_.type != JSON_TYPE.STRING || !module_.str.length)
1472 			throw new Exception("No valid module provided");
1473 		auto workspace = "workspace" in lens.data;
1474 		if (!workspace || workspace.type != JSON_TYPE.STRING || !workspace.str.length)
1475 			throw new Exception("No valid workspace provided");
1476 		int decMs = getImportCompilationTime(code.str, module_.str, workspace.str);
1477 		lens.command = Command((decMs < 10 ? "no noticable effect"
1478 				: "~" ~ decMs.to!string ~ "ms") ~ " for importing this");
1479 		return lens;
1480 	default:
1481 		throw new Exception("Unknown lens type");
1482 	}
1483 }
1484 
1485 bool importCompilationTimeRunning;
1486 int getImportCompilationTime(string code, string module_, string workspaceRoot)
1487 {
1488 	import std.math : round;
1489 
1490 	static struct CompileCache
1491 	{
1492 		SysTime at;
1493 		string code;
1494 		int ret;
1495 	}
1496 
1497 	static CompileCache[] cache;
1498 
1499 	auto now = Clock.currTime;
1500 
1501 	foreach_reverse (i, exist; cache)
1502 	{
1503 		if (exist.code != code)
1504 			continue;
1505 		if (now - exist.at < (exist.ret >= 500 ? 20.minutes : exist.ret >= 30 ? 5.minutes
1506 				: 2.minutes) || module_.startsWith("std."))
1507 			return exist.ret;
1508 		else
1509 		{
1510 			cache[i] = cache[$ - 1];
1511 			cache.length--;
1512 		}
1513 	}
1514 
1515 	while (importCompilationTimeRunning)
1516 		Fiber.yield();
1517 	importCompilationTimeRunning = true;
1518 	scope (exit)
1519 		importCompilationTimeRunning = false;
1520 	// run blocking so we don't compute multiple in parallel
1521 	auto ret = backend.get!DMDComponent(workspaceRoot).measureSync(code, null, 20, 500);
1522 	if (!ret.success)
1523 		throw new Exception("Compilation failed");
1524 	auto msecs = cast(int) round(ret.duration.total!"msecs" / 5.0) * 5;
1525 	cache ~= CompileCache(now, code, msecs);
1526 	StopWatch sw;
1527 	sw.start();
1528 	while (sw.peek < 100.msecs) // pass through requests for 100ms
1529 		Fiber.yield();
1530 	return msecs;
1531 }
1532 
1533 @protocolMethod("served/listConfigurations")
1534 string[] listConfigurations()
1535 {
1536 	return backend.get!DubComponent(selectedWorkspaceRoot).configurations;
1537 }
1538 
1539 @protocolMethod("served/switchConfig")
1540 bool switchConfig(string value)
1541 {
1542 	return backend.get!DubComponent(selectedWorkspaceRoot).setConfiguration(value);
1543 }
1544 
1545 @protocolMethod("served/getConfig")
1546 string getConfig(string value)
1547 {
1548 	return backend.get!DubComponent(selectedWorkspaceRoot).configuration;
1549 }
1550 
1551 @protocolMethod("served/listArchTypes")
1552 string[] listArchTypes()
1553 {
1554 	return backend.get!DubComponent(selectedWorkspaceRoot).archTypes;
1555 }
1556 
1557 @protocolMethod("served/switchArchType")
1558 bool switchArchType(string value)
1559 {
1560 	return backend.get!DubComponent(selectedWorkspaceRoot)
1561 		.setArchType(JSONValue(["arch-type" : JSONValue(value)]));
1562 }
1563 
1564 @protocolMethod("served/getArchType")
1565 string getArchType(string value)
1566 {
1567 	return backend.get!DubComponent(selectedWorkspaceRoot).archType;
1568 }
1569 
1570 @protocolMethod("served/listBuildTypes")
1571 string[] listBuildTypes()
1572 {
1573 	return backend.get!DubComponent(selectedWorkspaceRoot).buildTypes;
1574 }
1575 
1576 @protocolMethod("served/switchBuildType")
1577 bool switchBuildType(string value)
1578 {
1579 	return backend.get!DubComponent(selectedWorkspaceRoot)
1580 		.setBuildType(JSONValue(["build-type" : JSONValue(value)]));
1581 }
1582 
1583 @protocolMethod("served/getBuildType")
1584 string getBuildType()
1585 {
1586 	return backend.get!DubComponent(selectedWorkspaceRoot).buildType;
1587 }
1588 
1589 @protocolMethod("served/getCompiler")
1590 string getCompiler()
1591 {
1592 	return backend.get!DubComponent(selectedWorkspaceRoot).compiler;
1593 }
1594 
1595 @protocolMethod("served/switchCompiler")
1596 bool switchCompiler(string value)
1597 {
1598 	return backend.get!DubComponent(selectedWorkspaceRoot).setCompiler(value);
1599 }
1600 
1601 @protocolMethod("served/addImport")
1602 auto addImport(AddImportParams params)
1603 {
1604 	auto document = documents[params.textDocument.uri];
1605 	return backend.get!ImporterComponent.add(params.name.idup, document.text,
1606 			params.location, params.insertOutermost);
1607 }
1608 
1609 @protocolMethod("served/sortImports")
1610 TextEdit[] sortImports(SortImportsParams params)
1611 {
1612 	auto document = documents[params.textDocument.uri];
1613 	TextEdit[] ret;
1614 	auto sorted = backend.get!ImporterComponent.sortImports(document.text,
1615 			cast(int) document.offsetToBytes(params.location));
1616 	if (sorted == ImportBlock.init)
1617 		return ret;
1618 	auto start = document.bytesToPosition(sorted.start);
1619 	auto end = document.bytesToPosition(sorted.end);
1620 	string code = sorted.imports.to!(string[]).join(document.eolAt(0).toString);
1621 	return [TextEdit(TextRange(start, end), code)];
1622 }
1623 
1624 @protocolMethod("served/implementMethods")
1625 TextEdit[] implementMethods(ImplementMethodsParams params)
1626 {
1627 	import std.ascii : isWhite;
1628 
1629 	auto workspaceRoot = workspaceRootFor(params.textDocument.uri);
1630 	auto document = documents[params.textDocument.uri];
1631 	TextEdit[] ret;
1632 	auto location = document.offsetToBytes(params.location);
1633 	auto code = backend.get!DCDExtComponent(workspaceRoot)
1634 		.implement(document.text, cast(int) location).getYield.strip;
1635 	if (!code.length)
1636 		return ret;
1637 	auto brace = document.text.indexOf('{', location);
1638 	auto fallback = brace;
1639 	if (brace == -1)
1640 		brace = document.text.length;
1641 	else
1642 	{
1643 		fallback = document.text.indexOf('\n', location);
1644 		brace = document.text.indexOfAny("}\n", brace);
1645 		if (brace == -1)
1646 			brace = document.text.length;
1647 	}
1648 	code = "\n\t" ~ code.replace("\n", document.eolAt(0).toString ~ "\t") ~ "\n";
1649 	bool inIdentifier = true;
1650 	int depth = 0;
1651 	foreach (i; location .. brace)
1652 	{
1653 		if (document.text[i].isWhite)
1654 			inIdentifier = false;
1655 		else if (document.text[i] == '{')
1656 			break;
1657 		else if (document.text[i] == ',' || document.text[i] == '!')
1658 			inIdentifier = true;
1659 		else if (document.text[i] == '(')
1660 			depth++;
1661 		else
1662 		{
1663 			if (depth > 0)
1664 			{
1665 				inIdentifier = true;
1666 				if (document.text[i] == ')')
1667 					depth--;
1668 			}
1669 			else if (!inIdentifier)
1670 			{
1671 				if (fallback != -1)
1672 					brace = fallback;
1673 				code = "\n{" ~ code ~ "}";
1674 				break;
1675 			}
1676 		}
1677 	}
1678 	auto pos = document.bytesToPosition(brace);
1679 	return [TextEdit(TextRange(pos, pos), code)];
1680 }
1681 
1682 @protocolMethod("served/restartServer")
1683 bool restartServer()
1684 {
1685 	backend.get!DCDComponent.restartServer().getYield;
1686 	return true;
1687 }
1688 
1689 @protocolMethod("served/updateImports")
1690 bool updateImports()
1691 {
1692 	auto workspaceRoot = selectedWorkspaceRoot;
1693 	bool success;
1694 	if (backend.has!DubComponent(workspaceRoot))
1695 	{
1696 		success = backend.get!DubComponent(workspaceRoot).update.getYield;
1697 		if (success)
1698 			rpc.notifyMethod("coded/updateDubTree");
1699 	}
1700 	backend.get!DCDComponent(workspaceRoot).refreshImports();
1701 	return success;
1702 }
1703 
1704 @protocolMethod("served/listDependencies")
1705 DubDependency[] listDependencies(string packageName)
1706 {
1707 	auto workspaceRoot = selectedWorkspaceRoot;
1708 	DubDependency[] ret;
1709 	auto allDeps = backend.get!DubComponent(workspaceRoot).dependencies;
1710 	if (!packageName.length)
1711 	{
1712 		auto deps = backend.get!DubComponent(workspaceRoot).rootDependencies;
1713 		foreach (dep; deps)
1714 		{
1715 			DubDependency r;
1716 			r.name = dep;
1717 			r.root = true;
1718 			foreach (other; allDeps)
1719 				if (other.name == dep)
1720 				{
1721 					r.version_ = other.ver;
1722 					r.path = other.path;
1723 					r.description = other.description;
1724 					r.homepage = other.homepage;
1725 					r.authors = other.authors;
1726 					r.copyright = other.copyright;
1727 					r.license = other.license;
1728 					r.subPackages = other.subPackages.map!"a.name".array;
1729 					r.hasDependencies = other.dependencies.length > 0;
1730 					break;
1731 				}
1732 			ret ~= r;
1733 		}
1734 	}
1735 	else
1736 	{
1737 		string[string] aa;
1738 		foreach (other; allDeps)
1739 			if (other.name == packageName)
1740 			{
1741 				aa = other.dependencies;
1742 				break;
1743 			}
1744 		foreach (name, ver; aa)
1745 		{
1746 			DubDependency r;
1747 			r.name = name;
1748 			r.version_ = ver;
1749 			foreach (other; allDeps)
1750 				if (other.name == name)
1751 				{
1752 					r.path = other.path;
1753 					r.description = other.description;
1754 					r.homepage = other.homepage;
1755 					r.authors = other.authors;
1756 					r.copyright = other.copyright;
1757 					r.license = other.license;
1758 					r.subPackages = other.subPackages.map!"a.name".array;
1759 					r.hasDependencies = other.dependencies.length > 0;
1760 					break;
1761 				}
1762 			ret ~= r;
1763 		}
1764 	}
1765 	return ret;
1766 }
1767 
1768 // === Protocol Notifications starting here ===
1769 
1770 struct FileOpenInfo
1771 {
1772 	SysTime at;
1773 }
1774 
1775 __gshared FileOpenInfo[string] freshlyOpened;
1776 
1777 @protocolNotification("workspace/didChangeWatchedFiles")
1778 void onChangeFiles(DidChangeWatchedFilesParams params)
1779 {
1780 	foreach (change; params.changes)
1781 	{
1782 		string file = change.uri;
1783 		if (change.type == FileChangeType.created && file.endsWith(".d"))
1784 		{
1785 			auto document = documents[file];
1786 			auto isNew = file in freshlyOpened;
1787 			info(file);
1788 			if (isNew)
1789 			{
1790 				// Only edit if creation & opening is < 800msecs apart (vscode automatically opens on creation),
1791 				// we don't want to affect creation from/in other programs/editors.
1792 				if (Clock.currTime - isNew.at > 800.msecs)
1793 				{
1794 					freshlyOpened.remove(file);
1795 					continue;
1796 				}
1797 				// Sending applyEdit so it is undoable
1798 				auto patches = backend.get!ModulemanComponent.normalizeModules(file.uriToFile,
1799 						document.text);
1800 				if (patches.length)
1801 				{
1802 					WorkspaceEdit edit;
1803 					edit.changes[file] = patches.map!(a => TextEdit(TextRange(document.bytesToPosition(a.range[0]),
1804 							document.bytesToPosition(a.range[1])), a.content)).array;
1805 					rpc.sendMethod("workspace/applyEdit", ApplyWorkspaceEditParams(edit));
1806 				}
1807 			}
1808 		}
1809 	}
1810 }
1811 
1812 @protocolNotification("workspace/didChangeWorkspaceFolders")
1813 void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params)
1814 {
1815 	foreach (toRemove; params.event.removed)
1816 		removeWorkspace(toRemove.uri);
1817 	foreach (toAdd; params.event.added)
1818 	{
1819 		workspaces ~= Workspace(toAdd);
1820 		syncConfiguration(toAdd.uri);
1821 		doStartup(toAdd.uri);
1822 	}
1823 }
1824 
1825 @protocolNotification("textDocument/didOpen")
1826 void onDidOpenDocument(DidOpenTextDocumentParams params)
1827 {
1828 	freshlyOpened[params.textDocument.uri] = FileOpenInfo(Clock.currTime);
1829 }
1830 
1831 int changeTimeout;
1832 @protocolNotification("textDocument/didChange")
1833 void onDidChangeDocument(DocumentLinkParams params)
1834 {
1835 	auto document = documents[params.textDocument.uri];
1836 	if (document.languageId != "d")
1837 		return;
1838 	int delay = document.text.length > 50 * 1024 ? 1000 : 200; // be slower after 50KiB
1839 	clearTimeout(changeTimeout);
1840 	changeTimeout = setTimeout({
1841 		import served.linters.dscanner;
1842 
1843 		lint(document);
1844 		// Delay to avoid too many requests
1845 	}, delay);
1846 }
1847 
1848 @protocolNotification("textDocument/didSave")
1849 void onDidSaveDocument(DidSaveTextDocumentParams params)
1850 {
1851 	auto workspaceRoot = workspaceRootFor(params.textDocument.uri);
1852 	auto config = workspace(params.textDocument.uri).config;
1853 	auto document = documents[params.textDocument.uri];
1854 	auto fileName = params.textDocument.uri.uriToFile.baseName;
1855 
1856 	if (document.languageId == "d" || document.languageId == "diet")
1857 	{
1858 		if (!config.d.enableLinting)
1859 			return;
1860 		joinAll({
1861 			if (config.d.enableStaticLinting)
1862 			{
1863 				if (document.languageId == "diet")
1864 					return;
1865 				import served.linters.dscanner;
1866 
1867 				lint(document);
1868 			}
1869 		}, {
1870 			if (backend.has!DubComponent && config.d.enableDubLinting)
1871 			{
1872 				import served.linters.dub;
1873 
1874 				lint(document);
1875 			}
1876 		});
1877 	}
1878 	else if (fileName == "dub.json" || fileName == "dub.sdl")
1879 	{
1880 		info("Updating dependencies");
1881 		rpc.window.runOrMessage(backend.get!DubComponent(workspaceRoot).upgrade(),
1882 				MessageType.warning, translate!"d.ext.dubUpgradeFail");
1883 		rpc.window.runOrMessage(backend.get!DubComponent(workspaceRoot)
1884 				.updateImportPaths(true), MessageType.warning, translate!"d.ext.dubImportFail");
1885 		rpc.notifyMethod("coded/updateDubTree");
1886 	}
1887 }
1888 
1889 @protocolNotification("served/killServer")
1890 void killServer()
1891 {
1892 	foreach (instance; backend.instances)
1893 		if (instance.has!DCDComponent)
1894 			instance.get!DCDComponent.killServer();
1895 }
1896 
1897 @protocolNotification("served/installDependency")
1898 void installDependency(InstallRequest req)
1899 {
1900 	auto workspaceRoot = selectedWorkspaceRoot;
1901 	injectDependency(workspaceRoot, req);
1902 	if (backend.has!DubComponent)
1903 	{
1904 		backend.get!DubComponent(workspaceRoot).upgrade();
1905 		backend.get!DubComponent(workspaceRoot).updateImportPaths(true);
1906 	}
1907 	updateImports();
1908 }
1909 
1910 @protocolNotification("served/updateDependency")
1911 void updateDependency(UpdateRequest req)
1912 {
1913 	auto workspaceRoot = selectedWorkspaceRoot;
1914 	if (changeDependency(workspaceRoot, req))
1915 	{
1916 		if (backend.has!DubComponent)
1917 		{
1918 			backend.get!DubComponent(workspaceRoot).upgrade();
1919 			backend.get!DubComponent(workspaceRoot).updateImportPaths(true);
1920 		}
1921 		updateImports();
1922 	}
1923 }
1924 
1925 @protocolNotification("served/uninstallDependency")
1926 void uninstallDependency(UninstallRequest req)
1927 {
1928 	auto workspaceRoot = selectedWorkspaceRoot;
1929 	// TODO: add workspace argument
1930 	removeDependency(workspaceRoot, req.name);
1931 	if (backend.has!DubComponent)
1932 	{
1933 		backend.get!DubComponent(workspaceRoot).upgrade();
1934 		backend.get!DubComponent(workspaceRoot).updateImportPaths(true);
1935 	}
1936 	updateImports();
1937 }
1938 
1939 void injectDependency(string workspaceRoot, InstallRequest req)
1940 {
1941 	auto sdl = buildPath(workspaceRoot, "dub.sdl");
1942 	if (fs.exists(sdl))
1943 	{
1944 		int depth = 0;
1945 		auto content = fs.readText(sdl).splitLines(KeepTerminator.yes);
1946 		auto insertAt = content.length;
1947 		bool gotLineEnding = false;
1948 		string lineEnding = "\n";
1949 		foreach (i, line; content)
1950 		{
1951 			if (!gotLineEnding && line.length >= 2)
1952 			{
1953 				lineEnding = line[$ - 2 .. $];
1954 				if (lineEnding[0] != '\r')
1955 					lineEnding = line[$ - 1 .. $];
1956 				gotLineEnding = true;
1957 			}
1958 			if (depth == 0 && line.strip.startsWith("dependency "))
1959 				insertAt = i + 1;
1960 			depth += line.count('{') - line.count('}');
1961 		}
1962 		content = content[0 .. insertAt] ~ ((insertAt == content.length ? lineEnding
1963 				: "") ~ "dependency \"" ~ req.name ~ "\" version=\"~>" ~ req.version_ ~ "\"" ~ lineEnding)
1964 			~ content[insertAt .. $];
1965 		fs.write(sdl, content.join());
1966 	}
1967 	else
1968 	{
1969 		auto json = buildPath(workspaceRoot, "dub.json");
1970 		if (!fs.exists(json))
1971 			json = buildPath(workspaceRoot, "package.json");
1972 		if (!fs.exists(json))
1973 			return;
1974 		auto content = fs.readText(json).splitLines(KeepTerminator.yes);
1975 		auto insertAt = content.length ? content.length - 1 : 0;
1976 		string lineEnding = "\n";
1977 		bool gotLineEnding = false;
1978 		int depth = 0;
1979 		bool insertNext;
1980 		string indent;
1981 		bool foundBlock;
1982 		foreach (i, line; content)
1983 		{
1984 			if (!gotLineEnding && line.length >= 2)
1985 			{
1986 				lineEnding = line[$ - 2 .. $];
1987 				if (lineEnding[0] != '\r')
1988 					lineEnding = line[$ - 1 .. $];
1989 				gotLineEnding = true;
1990 			}
1991 			if (insertNext)
1992 			{
1993 				indent = line[0 .. $ - line.stripLeft.length];
1994 				insertAt = i + 1;
1995 				break;
1996 			}
1997 			if (depth == 1 && line.strip.startsWith(`"dependencies":`))
1998 			{
1999 				foundBlock = true;
2000 				if (line.strip.endsWith("{"))
2001 				{
2002 					indent = line[0 .. $ - line.stripLeft.length];
2003 					insertAt = i + 1;
2004 					break;
2005 				}
2006 				else
2007 				{
2008 					insertNext = true;
2009 				}
2010 			}
2011 			depth += line.count('{') - line.count('}') + line.count('[') - line.count(']');
2012 		}
2013 		if (foundBlock)
2014 		{
2015 			content = content[0 .. insertAt] ~ (
2016 					indent ~ indent ~ `"` ~ req.name ~ `": "~>` ~ req.version_ ~ `",` ~ lineEnding)
2017 				~ content[insertAt .. $];
2018 			fs.write(json, content.join());
2019 		}
2020 		else if (content.length)
2021 		{
2022 			if (content.length > 1)
2023 				content[$ - 2] = content[$ - 2].stripRight;
2024 			content = content[0 .. $ - 1] ~ (
2025 					"," ~ lineEnding ~ `	"dependencies": {
2026 		"` ~ req.name ~ `": "~>` ~ req.version_ ~ `"
2027 	}` ~ lineEnding)
2028 				~ content[$ - 1 .. $];
2029 			fs.write(json, content.join());
2030 		}
2031 		else
2032 		{
2033 			content ~= `{
2034 	"dependencies": {
2035 		"` ~ req.name ~ `": "~>` ~ req.version_ ~ `"
2036 	}
2037 }`;
2038 			fs.write(json, content.join());
2039 		}
2040 	}
2041 }
2042 
2043 bool changeDependency(string workspaceRoot, UpdateRequest req)
2044 {
2045 	auto sdl = buildPath(workspaceRoot, "dub.sdl");
2046 	if (fs.exists(sdl))
2047 	{
2048 		int depth = 0;
2049 		auto content = fs.readText(sdl).splitLines(KeepTerminator.yes);
2050 		size_t target = size_t.max;
2051 		foreach (i, line; content)
2052 		{
2053 			if (depth == 0 && line.strip.startsWith("dependency ")
2054 					&& line.strip["dependency".length .. $].strip.startsWith('"' ~ req.name ~ '"'))
2055 			{
2056 				target = i;
2057 				break;
2058 			}
2059 			depth += line.count('{') - line.count('}');
2060 		}
2061 		if (target == size_t.max)
2062 			return false;
2063 		auto ver = content[target].indexOf("version");
2064 		if (ver == -1)
2065 			return false;
2066 		auto quotStart = content[target].indexOf("\"", ver);
2067 		if (quotStart == -1)
2068 			return false;
2069 		auto quotEnd = content[target].indexOf("\"", quotStart + 1);
2070 		if (quotEnd == -1)
2071 			return false;
2072 		content[target] = content[target][0 .. quotStart] ~ '"' ~ req.version_ ~ '"'
2073 			~ content[target][quotEnd .. $];
2074 		fs.write(sdl, content.join());
2075 		return true;
2076 	}
2077 	else
2078 	{
2079 		auto json = buildPath(workspaceRoot, "dub.json");
2080 		if (!fs.exists(json))
2081 			json = buildPath(workspaceRoot, "package.json");
2082 		if (!fs.exists(json))
2083 			return false;
2084 		auto content = fs.readText(json);
2085 		auto replaced = content.replaceFirst(regex(`("` ~ req.name ~ `"\s*:\s*)"[^"]*"`),
2086 				`$1"` ~ req.version_ ~ `"`);
2087 		if (content == replaced)
2088 			return false;
2089 		fs.write(json, replaced);
2090 		return true;
2091 	}
2092 }
2093 
2094 bool removeDependency(string workspaceRoot, string name)
2095 {
2096 	auto sdl = buildPath(workspaceRoot, "dub.sdl");
2097 	if (fs.exists(sdl))
2098 	{
2099 		int depth = 0;
2100 		auto content = fs.readText(sdl).splitLines(KeepTerminator.yes);
2101 		size_t target = size_t.max;
2102 		foreach (i, line; content)
2103 		{
2104 			if (depth == 0 && line.strip.startsWith("dependency ")
2105 					&& line.strip["dependency".length .. $].strip.startsWith('"' ~ name ~ '"'))
2106 			{
2107 				target = i;
2108 				break;
2109 			}
2110 			depth += line.count('{') - line.count('}');
2111 		}
2112 		if (target == size_t.max)
2113 			return false;
2114 		fs.write(sdl, (content[0 .. target] ~ content[target + 1 .. $]).join());
2115 		return true;
2116 	}
2117 	else
2118 	{
2119 		auto json = buildPath(workspaceRoot, "dub.json");
2120 		if (!fs.exists(json))
2121 			json = buildPath(workspaceRoot, "package.json");
2122 		if (!fs.exists(json))
2123 			return false;
2124 		auto content = fs.readText(json);
2125 		auto replaced = content.replaceFirst(regex(`"` ~ name ~ `"\s*:\s*"[^"]*"\s*,\s*`), "");
2126 		if (content == replaced)
2127 			replaced = content.replaceFirst(regex(`\s*,\s*"` ~ name ~ `"\s*:\s*"[^"]*"`), "");
2128 		if (content == replaced)
2129 			replaced = content.replaceFirst(regex(
2130 					`"dependencies"\s*:\s*\{\s*"` ~ name ~ `"\s*:\s*"[^"]*"\s*\}\s*,\s*`), "");
2131 		if (content == replaced)
2132 			replaced = content.replaceFirst(regex(
2133 					`\s*,\s*"dependencies"\s*:\s*\{\s*"` ~ name ~ `"\s*:\s*"[^"]*"\s*\}`), "");
2134 		if (content == replaced)
2135 			return false;
2136 		fs.write(json, replaced);
2137 		return true;
2138 	}
2139 }
2140 
2141 struct Timeout
2142 {
2143 	StopWatch sw;
2144 	Duration timeout;
2145 	void delegate() callback;
2146 	int id;
2147 }
2148 
2149 int setTimeout(void delegate() callback, int ms)
2150 {
2151 	return setTimeout(callback, ms.msecs);
2152 }
2153 
2154 void setImmediate(void delegate() callback)
2155 {
2156 	setTimeout(callback, 0);
2157 }
2158 
2159 int setTimeout(void delegate() callback, Duration timeout)
2160 {
2161 	trace("Setting timeout for ", timeout);
2162 	Timeout to;
2163 	to.timeout = timeout;
2164 	to.callback = callback;
2165 	to.sw.start();
2166 	to.id = ++timeoutID;
2167 	synchronized (timeoutsMutex)
2168 		timeouts ~= to;
2169 	return to.id;
2170 }
2171 
2172 void clearTimeout(int id)
2173 {
2174 	synchronized (timeoutsMutex)
2175 		foreach_reverse (i, ref timeout; timeouts)
2176 		{
2177 			if (timeout.id == id)
2178 			{
2179 				timeout.sw.stop();
2180 				if (timeouts.length > 1)
2181 					timeouts[i] = timeouts[$ - 1];
2182 				timeouts.length--;
2183 				return;
2184 			}
2185 		}
2186 }
2187 
2188 __gshared void delegate(void delegate()) spawnFiber;
2189 
2190 shared static this()
2191 {
2192 	spawnFiber = (&setImmediate).toDelegate;
2193 	backend = new WorkspaceD();
2194 
2195 	backend.onBroadcast = (&handleBroadcast).toDelegate;
2196 	backend.onBindFail = (WorkspaceD.Instance instance, ComponentFactory factory) {
2197 		rpc.window.showErrorMessage(
2198 				"Failed to load component " ~ factory.info.name ~ " for workspace " ~ instance.cwd);
2199 	};
2200 }
2201 
2202 __gshared int timeoutID;
2203 __gshared Timeout[] timeouts;
2204 __gshared Mutex timeoutsMutex;
2205 
2206 // Called at most 100x per second
2207 void parallelMain()
2208 {
2209 	timeoutsMutex = new Mutex;
2210 	while (true)
2211 	{
2212 		synchronized (timeoutsMutex)
2213 			foreach_reverse (i, ref timeout; timeouts)
2214 			{
2215 				if (timeout.sw.peek >= timeout.timeout)
2216 				{
2217 					timeout.sw.stop();
2218 					timeout.callback();
2219 					trace("Calling timeout");
2220 					if (timeouts.length > 1)
2221 						timeouts[i] = timeouts[$ - 1];
2222 					timeouts.length--;
2223 				}
2224 			}
2225 		Fiber.yield();
2226 	}
2227 }