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