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