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