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