1 module workspaced.com.dcd;
2 
3 import std.file : tempDir;
4 
5 import core.thread;
6 import std.algorithm;
7 import std.array;
8 import std.ascii;
9 import std.conv;
10 import std.datetime;
11 import std.experimental.logger : trace;
12 import std.json;
13 import std.path;
14 import std.process;
15 import std.random;
16 import std.stdio;
17 import std.string;
18 import std.typecons;
19 
20 import workspaced.api;
21 import workspaced.helpers;
22 import workspaced.com.dcd_version;
23 
24 import served.dcd.client;
25 
26 @component("dcd")
27 class DCDComponent : ComponentWrapper
28 {
29 	mixin DefaultComponentWrapper;
30 
31 	enum WarningId
32 	{
33 		dcdServerCrash,
34 		dcdOutdated,
35 		unimplemented
36 	}
37 
38 	enum latestKnownVersion = latestKnownDCDVersion;
39 	void load()
40 	{
41 		installedVersion = workspaced.globalConfiguration.get("dcd", "_installedVersion", "");
42 
43 		if (installedVersion.length
44 				&& this.clientPath == workspaced.globalConfiguration.get("dcd", "_clientPath", "")
45 				&& this.serverPath == workspaced.globalConfiguration.get("dcd", "_serverPath", ""))
46 		{
47 			if (workspaced.globalConfiguration.get("dcd", "_usingInternal", false))
48 				client = new BuiltinDCDClient();
49 			else
50 				client = new ExternalDCDClient(this.clientPath);
51 			trace("Reusing previously identified DCD ", installedVersion);
52 		}
53 		else
54 		{
55 			reloadBinaries();
56 		}
57 	}
58 
59 	void reloadBinaries()
60 	{
61 		string clientPath = this.clientPath;
62 		string serverPath = this.serverPath;
63 
64 		client = null;
65 
66 		installedVersion = serverPath.getVersionAndFixPath;
67 		string serverPathInfo = serverPath != "dcd-server" ? "(" ~ serverPath ~ ") " : "";
68 		trace("Detected dcd-server ", serverPathInfo, installedVersion);
69 
70 		if (!checkVersion(installedVersion, BuiltinDCDClient.minSupportedServerInclusive)
71 				|| checkVersion(installedVersion, BuiltinDCDClient.maxSupportedServerExclusive))
72 		{
73 			trace("Using dcd-client instead of internal workspace-d client");
74 
75 			string clientInstalledVersion = clientPath.getVersionAndFixPath;
76 			string clientPathInfo = clientPath != "dcd-client" ? "(" ~ clientPath ~ ") " : "";
77 			trace("Detected dcd-client ", clientPathInfo, clientInstalledVersion);
78 
79 			if (clientInstalledVersion != installedVersion)
80 				throw new Exception("client & server version mismatch");
81 
82 			client = new ExternalDCDClient(clientPath);
83 		}
84 		else
85 		{
86 			trace("using builtin DCD client");
87 			client = new BuiltinDCDClient();
88 		}
89 
90 		config.set("dcd", "clientPath", clientPath);
91 		config.set("dcd", "serverPath", serverPath);
92 
93 		assert(this.clientPath == clientPath);
94 		assert(this.serverPath == serverPath);
95 
96 		//dfmt off
97 		if (isOutdated)
98 			workspaced.messageHandler.warn(refInstance, "dcd",
99 				WarningId.dcdOutdated, "DCD is outdated");
100 		//dfmt on
101 
102 		workspaced.globalConfiguration.set("dcd", "_usingInternal",
103 				cast(ExternalDCDClient) client ? false : true);
104 		workspaced.globalConfiguration.set("dcd", "_clientPath", clientPath);
105 		workspaced.globalConfiguration.set("dcd", "_serverPath", serverPath);
106 		workspaced.globalConfiguration.set("dcd", "_installedVersion", installedVersion);
107 	}
108 
109 	/// Returns: true if DCD version is less than latestKnownVersion or if server and client mismatch or if it doesn't exist.
110 	bool isOutdated()
111 	{
112 		if (!installedVersion)
113 		{
114 			string serverPath = this.serverPath;
115 
116 			try
117 			{
118 				installedVersion = serverPath.getVersionAndFixPath;
119 			}
120 			catch (ProcessException)
121 			{
122 				return true;
123 			}
124 		}
125 
126 		if (installedVersion.isLocallyCompiledDCD)
127 			return false;
128 
129 		return !checkVersion(installedVersion, latestKnownVersion);
130 	}
131 
132 	/// Returns: The current detected installed version of dcd-client.
133 	///          Ends with `"-workspaced-builtin"` if this is using the builtin
134 	///          client.
135 	string clientInstalledVersion() @property const
136 	{
137 		return cast(ExternalDCDClient) client ? installedVersion :
138 			BuiltinDCDClient.clientVersion ~ "-workspaced-builtin";
139 	}
140 
141 	/// Returns: The current detected installed version of dcd-server. `null` if
142 	///          none is installed.
143 	string serverInstalledVersion() const
144 	{
145 		if (!installedVersion)
146 		{
147 			string serverPath = this.serverPath;
148 
149 			try
150 			{
151 				return serverPath.getVersionAndFixPath;
152 			}
153 			catch (ProcessException)
154 			{
155 				return null;
156 			}
157 		}
158 
159 		return installedVersion;
160 	}
161 
162 	private auto serverThreads()
163 	{
164 		return threads(1, 2);
165 	}
166 
167 	/// This stops the dcd-server instance safely and waits for it to exit
168 	override void shutdown(bool dtor = false)
169 	{
170 		stopServerSync();
171 		if (!dtor && _threads)
172 			serverThreads.finish();
173 	}
174 
175 	/// This will start the dcd-server and load import paths from the current provider
176 	void setupServer(string[] additionalImports = [], bool quietServer = false)
177 	{
178 		startServer(importPaths ~ importFiles ~ additionalImports, quietServer);
179 	}
180 
181 	/// This will start the dcd-server. If DCD does not support IPC sockets on
182 	/// this platform, will use the TCP port specified with the `port` property
183 	/// or init config.
184 	///
185 	/// Throws an exception if a TCP port is used and another server is already
186 	/// running on it.
187 	///
188 	/// Params:
189 	///   additionalImports = import paths to cache on the server on startup.
190 	///   quietServer = if true: no output from DCD server is processed,
191 	///                 if false: every line will be traced to the output.
192 	///   selectPort = if true, increment port until an open one is found
193 	///                instead of throwing an exception.
194 	void startServer(string[] additionalImports = [], bool quietServer = false, bool selectPort = false)
195 	{
196 		ushort port = this.port;
197 		while (port + 1 < ushort.max && isPortRunning(port))
198 		{
199 			if (selectPort)
200 				port++;
201 			else
202 				throw new Exception("Already running dcd on port " ~ port.to!string);
203 		}
204 		string[] imports;
205 		foreach (i; additionalImports)
206 			if (i.length)
207 				imports ~= "-I" ~ i;
208 
209 		client.runningPort = port;
210 		client.socketFile = buildPath(tempDir,
211 				"workspace-d-sock" ~ thisProcessID.to!string ~ "-" ~ uniform!ulong.to!string(36));
212 
213 		string[] serverArgs;
214 		static if (platformSupportsDCDUnixSockets)
215 			serverArgs = [serverPath, "--socketFile", client.socketFile];
216 		else
217 			serverArgs = [serverPath, "--port", client.runningPort.to!string];
218 
219 		trace("Start dcd-server ", serverArgs);
220 		serverPipes = raw(serverArgs ~ imports,
221 				Redirect.stdin | Redirect.stderr | Redirect.stdoutToStderr);
222 		while (!serverPipes.stderr.eof)
223 		{
224 			string line = serverPipes.stderr.readln();
225 			if (!quietServer)
226 				trace("Server: ", line);
227 			if (line.canFind("Startup completed in "))
228 				break;
229 		}
230 		running = true;
231 		serverThreads.create({
232 			mixin(traceTask);
233 			scope (exit)
234 				running = false;
235 
236 			try
237 			{
238 				if (quietServer)
239 					foreach (block; serverPipes.stderr.byChunk(4096))
240 					{
241 					}
242 				else
243 					while (serverPipes.stderr.isOpen && !serverPipes.stderr.eof)
244 					{
245 						auto line = serverPipes.stderr.readln();
246 						trace("Server: ", line); // evaluates lazily, so read before
247 					}
248 			}
249 			catch (Exception e)
250 			{
251 				workspaced.messageHandler.error(refInstance, "dcd",
252 					WarningId.dcdServerCrash,
253 					"Reading/clearing stderr from dcd-server crashed (-> killing dcd-server): ",
254 					e.toString);
255 				serverPipes.pid.kill();
256 			}
257 
258 			auto code = serverPipes.pid.wait();
259 			trace("DCD-Server stopped with code ", code);
260 			if (code != 0)
261 			{
262 				workspaced.messageHandler.handleCrash(refInstance, "dcd", this);
263 			}
264 		});
265 	}
266 
267 	void stopServerSync()
268 	{
269 		if (!running)
270 			return;
271 		int i = 0;
272 		running = false;
273 		client.shutdown();
274 		while (serverPipes.pid && !serverPipes.pid.tryWait().terminated)
275 		{
276 			Thread.sleep(10.msecs);
277 			if (++i > 200) // Kill after 2 seconds
278 			{
279 				killServer();
280 				return;
281 			}
282 		}
283 	}
284 
285 	/// This stops the dcd-server asynchronously
286 	/// Returns: null
287 	Future!void stopServer()
288 	{
289 		auto ret = new typeof(return)();
290 		gthreads.create({
291 			mixin(traceTask);
292 			try
293 			{
294 				stopServerSync();
295 				ret.finish();
296 			}
297 			catch (Throwable t)
298 			{
299 				ret.error(t);
300 			}
301 		});
302 		return ret;
303 	}
304 
305 	/// This will kill the process associated with the dcd-server instance
306 	void killServer()
307 	{
308 		if (serverPipes.pid && !serverPipes.pid.tryWait().terminated)
309 			serverPipes.pid.kill();
310 	}
311 
312 	/// This will stop the dcd-server safely and restart it again using setup-server asynchronously
313 	/// Returns: null
314 	Future!void restartServer(bool quiet = false)
315 	{
316 		auto ret = new typeof(return);
317 		gthreads.create({
318 			mixin(traceTask);
319 			try
320 			{
321 				stopServerSync();
322 				setupServer([], quiet);
323 				ret.finish();
324 			}
325 			catch (Throwable t)
326 			{
327 				ret.error(t);
328 			}
329 		});
330 		return ret;
331 	}
332 
333 	/// This will query the current dcd-server status
334 	/// Returns: `{isRunning: bool}` If the dcd-server process is not running
335 	/// anymore it will return isRunning: false. Otherwise it will check for
336 	/// server status using `dcd-client --query` (or using builtin equivalent)
337 	auto serverStatus() @property
338 	{
339 		DCDServerStatus status;
340 		if (serverPipes.pid && serverPipes.pid.tryWait().terminated)
341 			status.isRunning = false;
342 		else if (client.usingUnixDomainSockets)
343 			status.isRunning = true;
344 		else
345 			status.isRunning = client.queryRunning();
346 		return status;
347 	}
348 
349 	/// Searches for a symbol across all files using `dcd-client --search`
350 	Future!(DCDSearchResult[]) searchSymbol(string query)
351 	{
352 		auto ret = new typeof(return);
353 		gthreads.create({
354 			mixin(traceTask);
355 			try
356 			{
357 				if (!running)
358 				{
359 					ret.finish(null);
360 					return;
361 				}
362 
363 				ret.finish(client.requestSymbolSearch(query)
364 					.map!(a => DCDSearchResult(a.symbolFilePath,
365 					cast(int)a.symbolLocation, [cast(char) a.kind].idup)).array);
366 			}
367 			catch (Throwable t)
368 			{
369 				ret.error(t);
370 			}
371 		});
372 		return ret;
373 	}
374 
375 	/// Reloads import paths from the current provider. Call reload there before calling it here.
376 	void refreshImports()
377 	{
378 		addImports(importPaths ~ importFiles);
379 	}
380 
381 	/// Manually adds import paths as string array
382 	void addImports(string[] imports)
383 	{
384 		imports.sort!"a<b";
385 		knownImports = multiwayUnion([knownImports.filterNonEmpty, imports.filterNonEmpty]).array;
386 		updateImports();
387 	}
388 
389 	/// Manually removes import paths using a string array. Note that trying to
390 	/// remove import paths from the import paths provider will result in them
391 	/// being readded as soon as refreshImports is called again.
392 	void removeImports(string[] imports)
393 	{
394 		knownImports = setDifference(knownImports, imports.filterNonEmpty).array;
395 		updateImports();
396 	}
397 
398 	string clientPath() @property @ignoredFunc const
399 	{
400 		return config.get("dcd", "clientPath", "dcd-client");
401 	}
402 
403 	string serverPath() @property @ignoredFunc const
404 	{
405 		return config.get("dcd", "serverPath", "dcd-server");
406 	}
407 
408 	ushort port() @property @ignoredFunc const
409 	{
410 		return cast(ushort) config.get!int("dcd", "port", 9166);
411 	}
412 
413 	/// Searches for an open port to spawn dcd-server in asynchronously starting with `port`, always increasing by one.
414 	/// Returns: 0 if not available, otherwise the port as number
415 	Future!ushort findAndSelectPort(ushort port = 9166)
416 	{
417 		if (client.usingUnixDomainSockets)
418 		{
419 			return typeof(return).fromResult(0);
420 		}
421 		auto ret = new typeof(return);
422 		gthreads.create({
423 			mixin(traceTask);
424 			try
425 			{
426 				auto newPort = findOpen(port);
427 				port = newPort;
428 				ret.finish(port);
429 			}
430 			catch (Throwable t)
431 			{
432 				ret.error(t);
433 			}
434 		});
435 		return ret;
436 	}
437 
438 	/// Finds the declaration of the symbol at position `pos` in the code
439 	Future!DCDDeclaration findDeclaration(scope const(char)[] code, int pos)
440 	{
441 		auto ret = new typeof(return);
442 		gthreads.create({
443 			mixin(traceTask);
444 			try
445 			{
446 				if (!running || pos >= code.length)
447 				{
448 					ret.finish(DCDDeclaration.init);
449 					return;
450 				}
451 
452 				// We need to move by one character on identifier characters to ensure the start character fits.
453 				if (!isIdentifierSeparatingChar(code[pos]))
454 					pos++;
455 
456 				auto info = client.requestSymbolInfo(CodeRequest("stdin", code, pos));
457 				ret.finish(DCDDeclaration(info.declarationFilePath,
458 					cast(int) info.declarationLocation));
459 			}
460 			catch (Throwable t)
461 			{
462 				ret.error(t);
463 			}
464 		});
465 		return ret;
466 	}
467 
468 	/// Finds the documentation of the symbol at position `pos` in the code
469 	Future!string getDocumentation(scope const(char)[] code, int pos)
470 	{
471 		auto ret = new typeof(return);
472 		gthreads.create({
473 			mixin(traceTask);
474 			try
475 			{
476 				if (!running || pos >= code.length)
477 				{
478 					ret.finish("");
479 					return;
480 				}
481 
482 				// We need to move by one character on identifier characters to ensure the start character fits.
483 				if (!isIdentifierSeparatingChar(code[pos]))
484 					pos++;
485 
486 				auto doc = client.requestDocumentation(CodeRequest("stdin", code, pos));
487 				ret.finish(doc.join("\n"));
488 			}
489 			catch (Throwable t)
490 			{
491 				ret.error(t);
492 			}
493 		});
494 		return ret;
495 	}
496 
497 	/// Finds declaration and usage of the token at position `pos` within the
498 	/// current document.
499 	Future!DCDLocalUse findLocalUse(scope const(char)[] code, int pos)
500 	{
501 		auto ret = new typeof(return);
502 		gthreads.create({
503 			mixin(traceTask);
504 			try
505 			{
506 				if (!running || pos >= code.length)
507 				{
508 					ret.finish(DCDLocalUse.init);
509 					return;
510 				}
511 
512 				// We need to move by one character on identifier characters to ensure the start character fits.
513 				if (!isIdentifierSeparatingChar(code[pos]))
514 					pos++;
515 
516 				auto localUse = client.requestLocalUse(CodeRequest("stdin", code, pos));
517 				ret.finish(DCDLocalUse(localUse));
518 			}
519 			catch (Throwable t)
520 			{
521 				ret.error(t);
522 			}
523 		});
524 		return ret;
525 	}
526 
527 	/// Returns the used socket file. Only available on OSX, linux and BSD with DCD >= 0.8.0
528 	/// Throws an error if not available.
529 	string getSocketFile()
530 	{
531 		if (!client.usingUnixDomainSockets)
532 			throw new Exception("Unix domain sockets not supported");
533 		return client.socketFile;
534 	}
535 
536 	/// Returns the used running port. Throws an error if using unix sockets instead
537 	ushort getRunningPort()
538 	{
539 		if (client.usingUnixDomainSockets)
540 			throw new Exception("Using unix domain sockets instead of a port");
541 		return client.runningPort;
542 	}
543 
544 	/// Queries for code completion at position `pos` in code
545 	/// Raw is anything else than identifiers and calltips which might not be implemented by this point.
546 	/// calltips.symbols and identifiers.definition, identifiers.file, identifiers.location and identifiers.documentation are only available with dcd ~master as of now.
547 	Future!DCDCompletions listCompletion(scope const(char)[] code, int pos)
548 	{
549 		auto ret = new typeof(return);
550 		gthreads.create({
551 			mixin(traceTask);
552 			try
553 			{
554 				DCDCompletions completions;
555 				if (!running)
556 				{
557 					trace("DCD not yet running!");
558 					ret.finish(completions);
559 					return;
560 				}
561 
562 				auto c = client.requestAutocomplete(CodeRequest("stdin", code, pos));
563 				if (c.type == DCDCompletionType.calltips)
564 				{
565 					completions.type = DCDCompletions.Type.calltips;
566 					auto calltips = appender!(string[]);
567 					auto symbols = appender!(DCDCompletions.Symbol[]);
568 					foreach (item; c.completions)
569 					{
570 						calltips ~= item.definition;
571 						symbols ~= DCDCompletions.Symbol(item.symbolFilePath,
572 							cast(int)item.symbolLocation, item.documentation);
573 					}
574 					completions._calltips = calltips.data;
575 					completions._symbols = symbols.data;
576 				}
577 				else if (c.type == DCDCompletionType.identifiers)
578 				{
579 					completions.type = DCDCompletions.Type.identifiers;
580 					auto identifiers = appender!(DCDIdentifier[]);
581 					foreach (item; c.completions)
582 					{
583 						identifiers ~= DCDIdentifier(item.identifier,
584 							item.kind == char.init ? "" : [cast(char)item.kind].idup,
585 							item.definition, item.symbolFilePath,
586 							cast(int)item.symbolLocation, item.documentation);
587 					}
588 					completions._identifiers = identifiers.data;
589 				}
590 				else
591 				{
592 					completions.type = DCDCompletions.Type.raw;
593 					workspaced.messageHandler.warn(refInstance, "dcd",
594 						WarningId.unimplemented,
595 						"Unknown DCD completion type: " ~ c.type.to!string);
596 				}
597 				ret.finish(completions);
598 			}
599 			catch (Throwable e)
600 			{
601 				ret.error(e);
602 			}
603 		});
604 		return ret;
605 	}
606 
607 	void updateImports()
608 	{
609 		if (!running)
610 			return;
611 
612 		auto existing = client.listImportPaths();
613 		existing.sort!"a<b";
614 		auto toAdd = setDifference(knownImports, existing);
615 		client.addImportPaths(toAdd.array);
616 	}
617 
618 	bool fromRunning(bool supportsFullOutput, string socketFile, ushort runningPort)
619 	{
620 		if (socketFile.length ? isSocketRunning(socketFile) : isPortRunning(runningPort))
621 		{
622 			running = true;
623 			client.socketFile = socketFile;
624 			client.runningPort = runningPort;
625 			return true;
626 		}
627 		else
628 			return false;
629 	}
630 
631 	deprecated("clients without full output support no longer supported") bool getSupportsFullOutput() @property
632 	{
633 		return true;
634 	}
635 
636 	bool isUsingUnixDomainSockets() @property
637 	{
638 		return client.usingUnixDomainSockets;
639 	}
640 
641 	bool isActive() @property
642 	{
643 		return running;
644 	}
645 
646 private:
647 	string installedVersion;
648 	bool running = false;
649 	ProcessPipes serverPipes;
650 	string[] knownImports;
651 	IDCDClient client = new NullDCDClient();
652 
653 	auto raw(string[] args, Redirect redirect = Redirect.all)
654 	{
655 		return pipeProcess(args, redirect, null, Config.none, refInstance ? instance.cwd : null);
656 	}
657 
658 	auto rawExec(string[] args)
659 	{
660 		return execute(args, null, Config.none, size_t.max, refInstance ? instance.cwd : null);
661 	}
662 
663 	bool isSocketRunning(string socket)
664 	{
665 		static if (!platformSupportsDCDUnixSockets)
666 			return false;
667 		else
668 			return isDCDServerRunning(false, socket, 0);
669 	}
670 
671 	bool isPortRunning(ushort port)
672 	{
673 		static if (platformSupportsDCDUnixSockets)
674 			return false;
675 		else
676 			return isDCDServerRunning(true, null, port);
677 	}
678 
679 	ushort findOpen(ushort port)
680 	{
681 		--port;
682 		bool isRunning;
683 		do
684 		{
685 			isRunning = isPortRunning(++port);
686 		}
687 		while (isRunning);
688 		return port;
689 	}
690 }
691 
692 class NullDCDClient : IDCDClient
693 {
694 	enum Methods = [
695 		"string socketFile() const @property",
696 		"void socketFile(string) @property",
697 		"ushort runningPort() const @property",
698 		"void runningPort(ushort) @property",
699 		"bool usingUnixDomainSockets() const @property",
700 		"bool queryRunning()",
701 		"bool shutdown()",
702 		"bool clearCache()",
703 		"bool addImportPaths(string[] importPaths)",
704 		"bool removeImportPaths(string[] importPaths)",
705 		"string[] listImportPaths()",
706 		"SymbolInformation requestSymbolInfo(CodeRequest loc)",
707 		"string[] requestDocumentation(CodeRequest loc)",
708 		"DCDResponse.Completion[] requestSymbolSearch(string query)",
709 		"LocalUse requestLocalUse(CodeRequest loc)",
710 		"Completion requestAutocomplete(CodeRequest loc)",
711 	];
712 
713 	static foreach (method; Methods)
714 	{
715 		mixin(method, " {
716 			import std.experimental.logger : warningf;
717 			warningf(\"Trying to use DCD function %s on uninitialized client!\", __FUNCTION__);
718 			static if (!is(typeof(return) == void))
719 				return typeof(return).init;
720 		}");
721 	}
722 }
723 
724 bool supportsUnixDomainSockets(string ver)
725 {
726 	return checkVersion(ver, [0, 8, 0]);
727 }
728 
729 unittest
730 {
731 	assert(supportsUnixDomainSockets("0.8.0-beta2+9ec55f40a26f6bb3ca95dc9232a239df6ed25c37"));
732 	assert(!supportsUnixDomainSockets("0.7.9-beta3"));
733 	assert(!supportsUnixDomainSockets("0.7.0"));
734 	assert(supportsUnixDomainSockets("v0.9.8 c7ea7e081ed9ad2d85e9f981fd047d7fcdb2cf51"));
735 	assert(supportsUnixDomainSockets("1.0.0"));
736 }
737 
738 /// Returned by findDeclaration
739 struct DCDDeclaration
740 {
741 	string file;
742 	int position;
743 }
744 
745 /// Returned by listCompletion
746 /// When identifiers: `{type:"identifiers", identifiers:[{identifier:string, type:string, definition:string, file:string, location:number, documentation:string}]}`
747 /// When calltips: `{type:"calltips", calltips:[string], symbols:[{file:string, location:number, documentation:string}]}`
748 /// When raw: `{type:"raw", raw:[string]}`
749 struct DCDCompletions
750 {
751 	/// Type of a completion
752 	enum Type
753 	{
754 		/// Unknown/Unimplemented output
755 		raw,
756 		/// Completion after a dot or a variable name
757 		identifiers,
758 		/// Completion for arguments in a function call
759 		calltips,
760 	}
761 
762 	struct Symbol
763 	{
764 		string file;
765 		int location;
766 		string documentation;
767 	}
768 
769 	/// Type of the completion (identifiers, calltips, raw)
770 	Type type;
771 	deprecated string[] raw;
772 	union
773 	{
774 		DCDIdentifier[] _identifiers;
775 		struct
776 		{
777 			string[] _calltips;
778 			Symbol[] _symbols;
779 		}
780 	}
781 
782 	enum DCDCompletions empty = DCDCompletions(Type.identifiers);
783 
784 	/// Only set with type==identifiers.
785 	inout(DCDIdentifier[]) identifiers() inout @property
786 	{
787 		if (type != Type.identifiers)
788 			throw new Exception("Type is not identifiers but attempted to access identifiers");
789 		return _identifiers;
790 	}
791 
792 	/// Only set with type==calltips.
793 	inout(string[]) calltips() inout @property
794 	{
795 		if (type != Type.calltips)
796 			throw new Exception("Type is not calltips but attempted to access calltips");
797 		return _calltips;
798 	}
799 
800 	/// Only set with type==calltips.
801 	inout(Symbol[]) symbols() inout @property
802 	{
803 		if (type != Type.calltips)
804 			throw new Exception("Type is not calltips but attempted to access symbols");
805 		return _symbols;
806 	}
807 }
808 
809 /// Returned by findLocalUse
810 struct DCDLocalUse
811 {
812 	/// File path of the declaration or stdin for input
813 	string declarationFilePath;
814 	/// Byte location of the declaration inside the declarationFilePath
815 	size_t declarationLocation;
816 	/// Array of uses within stdin / given document.
817 	size_t[] uses;
818 
819 	this(LocalUse localUse)
820 	{
821 		foreach (i, ref v; localUse.tupleof)
822 			this.tupleof[i] = v;
823 	}
824 }
825 
826 /// Returned by status
827 struct DCDServerStatus
828 {
829 	///
830 	bool isRunning;
831 }
832 
833 /// Type of the identifiers value in listCompletion
834 struct DCDIdentifier
835 {
836 	///
837 	string identifier;
838 	///
839 	string type;
840 	///
841 	string definition;
842 	///
843 	string file;
844 	/// byte location
845 	int location;
846 	///
847 	string documentation;
848 }
849 
850 /// Returned by search-symbol
851 struct DCDSearchResult
852 {
853 	///
854 	string file;
855 	///
856 	int position;
857 	///
858 	string type;
859 }
860 
861 private auto filterNonEmpty(T)(T range)
862 {
863 	return range.filter!(a => a.length);
864 }