1 module workspaced.com.dcdext;
2 
3 import dparse.ast;
4 import dparse.lexer;
5 import dparse.parser;
6 import dparse.rollback_allocator;
7 
8 import core.thread;
9 
10 import std.algorithm;
11 import std.array;
12 import std.ascii;
13 import std.conv;
14 import std.file;
15 import std.functional;
16 import std.json;
17 import std.meta;
18 import std.range;
19 import std.string;
20 
21 import workspaced.api;
22 import workspaced.com.dcd;
23 import workspaced.com.dfmt;
24 import workspaced.dparseext;
25 
26 import workspaced.visitors.classifier;
27 import workspaced.visitors.methodfinder;
28 
29 import painlessjson : SerializeIgnore;
30 
31 public import workspaced.visitors.methodfinder : InterfaceDetails, FieldDetails,
32 	MethodDetails, ArgumentInfo;
33 
34 @component("dcdext")
35 class DCDExtComponent : ComponentWrapper
36 {
37 	mixin DefaultComponentWrapper;
38 
39 	static immutable CodeRegionProtection[] mixableProtection = [
40 		CodeRegionProtection.public_ | CodeRegionProtection.default_,
41 		CodeRegionProtection.package_, CodeRegionProtection.packageIdentifier,
42 		CodeRegionProtection.protected_, CodeRegionProtection.private_
43 	];
44 
45 	/// Loads dcd extension methods. Call with `{"cmd": "load", "components": ["dcdext"]}`
46 	void load()
47 	{
48 		if (!refInstance)
49 			return;
50 
51 		config.stringBehavior = StringBehavior.source;
52 	}
53 
54 	/// Extracts calltips help information at a given position.
55 	/// The position must be within the arguments of the function and not
56 	/// outside the parentheses or inside some child call.
57 	///
58 	/// When generating the call parameters for a function definition, the position must be inside the normal parameters,
59 	/// otherwise the template arguments will be put as normal arguments.
60 	///
61 	/// Returns: the position of significant locations for parameter extraction.
62 	/// Params:
63 	///   code = code to analyze
64 	///   position = byte offset where to check for function arguments
65 	///   definition = true if this hints is a function definition (templates don't have an exclamation point '!')
66 	CalltipsSupport extractCallParameters(scope const(char)[] code, int position,
67 			bool definition = false)
68 	{
69 		auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache);
70 		if (!tokens.length)
71 			return CalltipsSupport.init;
72 		// TODO: can probably use tokenIndexAtByteIndex here
73 		auto queuedToken = tokens.countUntil!(a => a.index >= position) - 1;
74 		if (queuedToken == -2)
75 			queuedToken = cast(ptrdiff_t) tokens.length - 1;
76 		else if (queuedToken == -1)
77 			return CalltipsSupport.init;
78 
79 		// TODO: refactor code to be more readable
80 		// all this code does is:
81 		// - go back all tokens until a starting ( is found. (with nested {} scope checks for delegates and () for calls)
82 		//   - abort if not found
83 		//   - set "isTemplate" if directly before the ( is a `!` token and an identifier
84 		// - if inTemplate is true:
85 		//   - go forward to starting ( of normal arguments -- this code has checks if startParen is `!`, which currently can't be the case but might be useful
86 		// - else not in template arguments, so
87 		//   - if before ( comes a ) we are definitely in a template, so track back until starting (
88 		//   - otherwise check if it's even a template (single argument: `!`, then a token, then `(`)
89 		// - determine function name & all parents (strips out index operators)
90 		// - split template & function arguments
91 		// - return all information
92 		// it's reasonably readable with the variable names and that pseudo explanation there pretty much directly maps to the code,
93 		// so it shouldn't be too hard of a problem, it's just a lot return values per step and taking in multiple returns from previous steps.
94 
95 		/// describes if the target position is inside template arguments rather than function arguments (only works for calls and not for definition)
96 		bool inTemplate;
97 		int activeParameter; // counted commas
98 		int depth, subDepth;
99 		/// contains opening parentheses location for arguments or exclamation point for templates.
100 		auto startParen = queuedToken;
101 		while (startParen >= 0)
102 		{
103 			const c = tokens[startParen];
104 			const p = startParen > 0 ? tokens[startParen - 1] : Token.init;
105 
106 			if (c.type == tok!"{")
107 			{
108 				if (subDepth == 0)
109 				{
110 					// we went too far, probably not inside a function (or we are in a delegate, where we don't want calltips)
111 					return CalltipsSupport.init;
112 				}
113 				else
114 					subDepth--;
115 			}
116 			else if (c.type == tok!"}")
117 			{
118 				subDepth++;
119 			}
120 			else if (subDepth == 0 && c.type == tok!";")
121 			{
122 				// this doesn't look like function arguments anymore
123 				return CalltipsSupport.init;
124 			}
125 			else if (depth == 0 && !definition && c.type == tok!"!" && p.type == tok!"identifier")
126 			{
127 				inTemplate = true;
128 				break;
129 			}
130 			else if (c.type == tok!")")
131 			{
132 				depth++;
133 			}
134 			else if (c.type == tok!"(")
135 			{
136 				if (depth == 0 && subDepth == 0)
137 				{
138 					if (startParen > 1 && p.type == tok!"!" && tokens[startParen - 2].type
139 							== tok!"identifier")
140 					{
141 						startParen--;
142 						inTemplate = true;
143 					}
144 					break;
145 				}
146 				else
147 					depth--;
148 			}
149 			else if (depth == 0 && subDepth == 0 && c.type == tok!",")
150 			{
151 				activeParameter++;
152 			}
153 			startParen--;
154 		}
155 
156 		if (startParen <= 0)
157 			return CalltipsSupport.init;
158 
159 		/// Token index where the opening template parentheses or exclamation point is. At first this is only set if !definition but later on this is resolved.
160 		auto templateOpen = inTemplate ? startParen : 0;
161 		/// Token index where the normal argument parentheses start or 0 if it doesn't exist for this call/definition
162 		auto functionOpen = inTemplate ? 0 : startParen;
163 
164 		bool hasTemplateParens = false;
165 
166 		if (inTemplate)
167 		{
168 			// go forwards to function arguments
169 			if (templateOpen + 2 < tokens.length)
170 			{
171 				if (tokens[templateOpen + 1].type == tok!"(")
172 				{
173 					hasTemplateParens = true;
174 					templateOpen++;
175 					functionOpen = findClosingParenForward(tokens, templateOpen,
176 							"in template function open finder");
177 					functionOpen++;
178 
179 					if (functionOpen >= tokens.length)
180 						functionOpen = 0;
181 				}
182 				else
183 				{
184 					// single template arg (can only be one token)
185 					// https://dlang.org/spec/grammar.html#TemplateSingleArgument
186 					if (tokens[templateOpen + 2] == tok!"(")
187 						functionOpen = templateOpen + 2;
188 				}
189 			}
190 			else
191 				return CalltipsSupport.init; // syntax error
192 		}
193 		else
194 		{
195 			// go backwards to template arguments
196 			if (functionOpen > 0 && tokens[functionOpen - 1].type == tok!")")
197 			{
198 				// multi template args
199 				depth = 0;
200 				subDepth = 0;
201 				templateOpen = functionOpen - 1;
202 				const minTokenIndex = definition ? 1 : 2;
203 				while (templateOpen >= minTokenIndex)
204 				{
205 					const c = tokens[templateOpen];
206 
207 					if (c == tok!")")
208 						depth++;
209 					else
210 					{
211 						if (depth == 1 && templateOpen > minTokenIndex && c.type == tok!"(")
212 						{
213 							if (definition
214 									? tokens[templateOpen - 1].type == tok!"identifier" : (tokens[templateOpen - 1].type == tok!"!"
215 										&& tokens[templateOpen - 2].type == tok!"identifier"))
216 								break;
217 						}
218 
219 						if (depth == 0)
220 						{
221 							templateOpen = 0;
222 							break;
223 						}
224 
225 						if (c == tok!"(")
226 							depth--;
227 					}
228 
229 					templateOpen--;
230 				}
231 
232 				if (templateOpen < minTokenIndex)
233 					templateOpen = 0;
234 				else
235 					hasTemplateParens = true;
236 			}
237 			else
238 			{
239 				// single template arg (can only be one token) or no template at all here
240 				if (functionOpen >= 3 && tokens[functionOpen - 2] == tok!"!"
241 						&& tokens[functionOpen - 3] == tok!"identifier")
242 				{
243 					templateOpen = functionOpen - 2;
244 				}
245 			}
246 		}
247 
248 		depth = 0;
249 		subDepth = 0;
250 		bool inFuncName = true;
251 		auto callStart = (templateOpen ? templateOpen : functionOpen) - 1;
252 		auto funcNameStart = callStart;
253 		while (callStart >= 0)
254 		{
255 			const c = tokens[callStart];
256 			const p = callStart > 0 ? tokens[callStart - 1] : Token.init;
257 
258 			if (c.type == tok!"]")
259 				depth++;
260 			else if (c.type == tok!"[")
261 			{
262 				if (depth == 0)
263 				{
264 					// this is some sort of `foo[(4` situation
265 					return CalltipsSupport.init;
266 				}
267 				depth--;
268 			}
269 			else if (c.type == tok!")")
270 				subDepth++;
271 			else if (c.type == tok!"(")
272 			{
273 				if (subDepth == 0)
274 				{
275 					// this is some sort of `foo((4` situation
276 					return CalltipsSupport.init;
277 				}
278 				subDepth--;
279 			}
280 			else if (depth == 0)
281 			{
282 
283 				if (c.type.isCalltipable)
284 				{
285 					if (c.type == tok!"identifier" && p.type == tok!"." && (callStart < 2
286 							|| !tokens[callStart - 2].type.among!(tok!";", tok!",",
287 							tok!"{", tok!"}", tok!"(")))
288 					{
289 						// member function, traverse further...
290 						if (inFuncName)
291 						{
292 							funcNameStart = callStart;
293 							inFuncName = false;
294 						}
295 						callStart--;
296 					}
297 					else
298 					{
299 						break;
300 					}
301 				}
302 				else
303 				{
304 					// this is some sort of `4(5` or `if(4` situtation
305 					return CalltipsSupport.init;
306 				}
307 			}
308 			// we ignore stuff inside brackets and parens such as `foo[4](5).bar[6](a`
309 			callStart--;
310 		}
311 
312 		if (inFuncName)
313 			funcNameStart = callStart;
314 
315 		ptrdiff_t templateClose;
316 		if (templateOpen)
317 		{
318 			if (hasTemplateParens)
319 			{
320 				if (functionOpen)
321 					templateClose = functionOpen - 1;
322 				else
323 					templateClose = findClosingParenForward(tokens, templateOpen,
324 							"in template close finder");
325 			}
326 			else
327 				templateClose = templateOpen + 2;
328 		}
329 		//dfmt on
330 		auto functionClose = functionOpen ? findClosingParenForward(tokens,
331 				functionOpen, "in function close finder") : 0;
332 
333 		CalltipsSupport.Argument[] templateArgs;
334 		if (templateOpen)
335 			templateArgs = splitArgs(tokens[templateOpen + 1 .. templateClose]);
336 
337 		CalltipsSupport.Argument[] functionArgs;
338 		if (functionOpen)
339 			functionArgs = splitArgs(tokens[functionOpen + 1 .. functionClose]);
340 
341 		return CalltipsSupport([
342 				tokens.tokenIndex(templateOpen),
343 				templateClose ? tokens.tokenEndIndex(templateClose) : 0
344 				], hasTemplateParens, templateArgs, [
345 				tokens.tokenIndex(functionOpen),
346 				functionClose ? tokens.tokenEndIndex(functionClose) : 0
347 				], functionArgs, funcNameStart != callStart, tokens.tokenIndex(funcNameStart),
348 				tokens.tokenIndex(callStart), inTemplate, activeParameter);
349 	}
350 
351 	/// Finds the immediate surrounding code block at a position or returns CodeBlockInfo.init for none/module block.
352 	/// See_Also: CodeBlockInfo
353 	CodeBlockInfo getCodeBlockRange(scope const(char)[] code, int position)
354 	{
355 		RollbackAllocator rba;
356 		auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache);
357 		auto parsed = parseModule(tokens, "getCodeBlockRange_input.d", &rba);
358 		auto reader = new CodeBlockInfoFinder(position);
359 		reader.visit(parsed);
360 		return reader.block;
361 	}
362 
363 	/// Inserts a generic method after the corresponding block inside the scope where position is.
364 	/// If it can't find a good spot it will insert the code properly indented ata fitting location.
365 	// make public once usable
366 	private CodeReplacement[] insertCodeInContainer(string insert, scope const(char)[] code,
367 			int position, bool insertInLastBlock = true, bool insertAtEnd = true)
368 	{
369 		auto container = getCodeBlockRange(code, position);
370 
371 		scope const(char)[] codeBlock = code[container.innerRange[0] .. container.innerRange[1]];
372 
373 		RollbackAllocator rba;
374 		scope tokensInsert = getTokensForParser(cast(ubyte[]) insert, config,
375 				&workspaced.stringCache);
376 		scope parsedInsert = parseModule(tokensInsert, "insertCode_insert.d", &rba);
377 
378 		scope insertReader = new CodeDefinitionClassifier(insert);
379 		insertReader.visit(parsedInsert);
380 		scope insertRegions = insertReader.regions.sort!"a.type < b.type".uniq.array;
381 
382 		scope tokens = getTokensForParser(cast(ubyte[]) codeBlock, config, &workspaced.stringCache);
383 		scope parsed = parseModule(tokens, "insertCode_code.d", &rba);
384 
385 		scope reader = new CodeDefinitionClassifier(codeBlock);
386 		reader.visit(parsed);
387 		scope regions = reader.regions;
388 
389 		CodeReplacement[] ret;
390 
391 		foreach (CodeDefinitionClassifier.Region toInsert; insertRegions)
392 		{
393 			auto insertCode = insert[toInsert.region[0] .. toInsert.region[1]];
394 			scope existing = regions.enumerate.filter!(a => a.value.sameBlockAs(toInsert));
395 			if (existing.empty)
396 			{
397 				const checkProtection = CodeRegionProtection.init.reduce!"a | b"(
398 						mixableProtection.filter!(a => (a & toInsert.protection) != 0));
399 
400 				bool inIncompatible = false;
401 				bool lastFit = false;
402 				int fittingProtection = -1;
403 				int firstStickyProtection = -1;
404 				int regionAfterFitting = -1;
405 				foreach (i, stickyProtection; regions)
406 				{
407 					if (stickyProtection.affectsFollowing
408 							&& stickyProtection.protection != CodeRegionProtection.init)
409 					{
410 						if (firstStickyProtection == -1)
411 							firstStickyProtection = cast(int) i;
412 
413 						if ((stickyProtection.protection & checkProtection) != 0)
414 						{
415 							fittingProtection = cast(int) i;
416 							lastFit = true;
417 							if (!insertInLastBlock)
418 								break;
419 						}
420 						else
421 						{
422 							if (lastFit)
423 							{
424 								regionAfterFitting = cast(int) i;
425 								lastFit = false;
426 							}
427 							inIncompatible = true;
428 						}
429 					}
430 				}
431 				assert(firstStickyProtection != -1 || !inIncompatible);
432 				assert(regionAfterFitting != -1 || fittingProtection == -1 || !inIncompatible);
433 
434 				if (inIncompatible)
435 				{
436 					int insertRegion = fittingProtection == -1 ? firstStickyProtection : regionAfterFitting;
437 					insertCode = text(indent(insertCode, regions[insertRegion].minIndentation), "\n\n");
438 					auto len = cast(uint) insertCode.length;
439 
440 					toInsert.region[0] = regions[insertRegion].region[0];
441 					toInsert.region[1] = regions[insertRegion].region[0] + len;
442 					foreach (ref r; regions[insertRegion .. $])
443 					{
444 						r.region[0] += len;
445 						r.region[1] += len;
446 					}
447 				}
448 				else
449 				{
450 					auto lastRegion = regions.back;
451 					insertCode = indent(insertCode, lastRegion.minIndentation).idup;
452 					auto len = cast(uint) insertCode.length;
453 					toInsert.region[0] = lastRegion.region[1];
454 					toInsert.region[1] = lastRegion.region[1] + len;
455 				}
456 				regions ~= toInsert;
457 				ret ~= CodeReplacement([toInsert.region[0], toInsert.region[0]], insertCode);
458 			}
459 			else
460 			{
461 				auto target = insertInLastBlock ? existing.tail(1).front : existing.front;
462 
463 				insertCode = text("\n\n", indent(insertCode, regions[target.index].minIndentation));
464 				const codeLength = cast(int) insertCode.length;
465 
466 				if (insertAtEnd)
467 				{
468 					ret ~= CodeReplacement([
469 							target.value.region[1], target.value.region[1]
470 							], insertCode);
471 					toInsert.region[0] = target.value.region[1];
472 					toInsert.region[1] = target.value.region[1] + codeLength;
473 					regions[target.index].region[1] = toInsert.region[1];
474 					foreach (ref other; regions[target.index + 1 .. $])
475 					{
476 						other.region[0] += codeLength;
477 						other.region[1] += codeLength;
478 					}
479 				}
480 				else
481 				{
482 					ret ~= CodeReplacement([
483 							target.value.region[0], target.value.region[0]
484 							], insertCode);
485 					regions[target.index].region[1] += codeLength;
486 					foreach (ref other; regions[target.index + 1 .. $])
487 					{
488 						other.region[0] += codeLength;
489 						other.region[1] += codeLength;
490 					}
491 				}
492 			}
493 		}
494 
495 		return ret;
496 	}
497 
498 	/// Implements the interfaces or abstract classes of a specified class/interface.
499 	/// Helper function which returns all functions as one block for most primitive use.
500 	Future!string implement(scope const(char)[] code, int position,
501 			bool formatCode = true, string[] formatArgs = [])
502 	{
503 		auto ret = new typeof(return);
504 		gthreads.create({
505 			mixin(traceTask);
506 			try
507 			{
508 				auto impl = implementAllSync(code, position, formatCode, formatArgs);
509 
510 				auto buf = appender!string;
511 				string lastBaseClass;
512 				foreach (ref func; impl)
513 				{
514 					if (func.baseClass != lastBaseClass)
515 					{
516 						buf.put("// implement " ~ func.baseClass ~ "\n\n");
517 						lastBaseClass = func.baseClass;
518 					}
519 
520 					buf.put(func.code);
521 					buf.put("\n\n");
522 				}
523 				ret.finish(buf.data.length > 2 ? buf.data[0 .. $ - 2] : buf.data);
524 			}
525 			catch (Throwable t)
526 			{
527 				ret.error(t);
528 			}
529 		});
530 		return ret;
531 	}
532 
533 	/// Implements the interfaces or abstract classes of a specified class/interface.
534 	/// The async implementation is preferred when used in background tasks to prevent disruption
535 	/// of other services as a lot of code is parsed and processed multiple times for this function.
536 	/// Params:
537 	/// 	code = input file to parse and edit.
538 	/// 	position = position of the superclass or interface to implement after the colon in a class definition.
539 	/// 	formatCode = automatically calls dfmt on all function bodys when true.
540 	/// 	formatArgs = sets the formatter arguments to pass to dfmt if formatCode is true.
541 	/// 	snippetExtensions = if true, snippets according to the vscode documentation will be inserted in place of method content. See https://code.visualstudio.com/docs/editor/userdefinedsnippets#_creating-your-own-snippets
542 	/// Returns: a list of newly implemented methods
543 	Future!(ImplementedMethod[]) implementAll(scope const(char)[] code, int position,
544 			bool formatCode = true, string[] formatArgs = [], bool snippetExtensions = false)
545 	{
546 		mixin(
547 				gthreadsAsyncProxy!`implementAllSync(code, position, formatCode, formatArgs, snippetExtensions)`);
548 	}
549 
550 	/// ditto
551 	ImplementedMethod[] implementAllSync(scope const(char)[] code, int position,
552 			bool formatCode = true, string[] formatArgs = [], bool snippetExtensions = false)
553 	{
554 		auto tree = describeInterfaceRecursiveSync(code, position);
555 		auto availableVariables = tree.availableVariables;
556 
557 		string[] implementedMethods = tree.details
558 			.methods
559 			.filter!"!a.needsImplementation"
560 			.map!"a.identifier"
561 			.array;
562 
563 		int snippetIndex = 0;
564 		// maintains snippet ids and their value in an AA so they can be replaced after formatting
565 		string[string] snippetReplacements;
566 
567 		auto methods = appender!(ImplementedMethod[]);
568 		void processTree(ref InterfaceTree tree)
569 		{
570 			auto details = tree.details;
571 			if (details.methods.length)
572 			{
573 				foreach (fn; details.methods)
574 				{
575 					if (implementedMethods.canFind(fn.identifier))
576 						continue;
577 					if (!fn.needsImplementation)
578 					{
579 						implementedMethods ~= fn.identifier;
580 						continue;
581 					}
582 
583 					//dfmt off
584 					ImplementedMethod method = {
585 						baseClass: details.name,
586 						name: fn.name
587 					};
588 					//dfmt on
589 					auto buf = appender!string;
590 
591 					snippetIndex++;
592 					bool writtenSnippet;
593 					string snippetId;
594 					auto snippetBuf = appender!string;
595 
596 					void startSnippet(bool withDefault = true)
597 					{
598 						if (writtenSnippet || !snippetExtensions)
599 							return;
600 						snippetId = format!`/+++__WORKSPACED_SNIPPET__%s__+++/`(snippetIndex);
601 						buf.put(snippetId);
602 						swap(buf, snippetBuf);
603 						buf.put("${");
604 						buf.put(snippetIndex.to!string);
605 						if (withDefault)
606 							buf.put(":");
607 						writtenSnippet = true;
608 					}
609 
610 					void endSnippet()
611 					{
612 						if (!writtenSnippet || !snippetExtensions)
613 							return;
614 						buf.put("}");
615 
616 						swap(buf, snippetBuf);
617 						snippetReplacements[snippetId] = snippetBuf.data;
618 					}
619 
620 					if (details.needsOverride)
621 						buf.put("override ");
622 					buf.put(fn.signature[0 .. $ - 1]);
623 					buf.put(" {");
624 					if (fn.optionalImplementation)
625 					{
626 						buf.put("\n\t");
627 						startSnippet();
628 						buf.put("// TODO: optional implementation\n");
629 					}
630 
631 					string propertySearch;
632 					if (fn.signature.canFind("@property") && fn.arguments.length <= 1)
633 						propertySearch = fn.name;
634 					else if ((fn.name.startsWith("get") && fn.arguments.length == 0)
635 							|| (fn.name.startsWith("set") && fn.arguments.length == 1))
636 						propertySearch = fn.name[3 .. $];
637 
638 					string foundProperty;
639 					if (propertySearch)
640 					{
641 						// frontOrDefault
642 						const matching = availableVariables.find!(a => fieldNameMatches(a.name,
643 								propertySearch));
644 						if (!matching.empty)
645 							foundProperty = matching.front.name;
646 					}
647 
648 					if (foundProperty.length)
649 					{
650 						method.autoProperty = true;
651 						buf.put("\n\t");
652 						startSnippet();
653 						if (fn.returnType != "void")
654 						{
655 							method.getter = true;
656 							buf.put("return ");
657 						}
658 
659 						if (fn.name.startsWith("set") || fn.arguments.length == 1)
660 						{
661 							method.setter = true;
662 							buf.put(foundProperty ~ " = " ~ fn.arguments[0].name);
663 						}
664 						else
665 						{
666 							// neither getter nor setter, but we will just put the property here anyway
667 							buf.put(foundProperty);
668 						}
669 						buf.put(";");
670 						endSnippet();
671 						buf.put("\n");
672 					}
673 					else if (fn.hasBody)
674 					{
675 						method.callsSuper = true;
676 						buf.put("\n\t");
677 						startSnippet();
678 						if (fn.returnType != "void")
679 							buf.put("return ");
680 						buf.put("super." ~ fn.name);
681 						if (fn.arguments.length)
682 							buf.put("(" ~ format("%(%s, %)", fn.arguments)
683 									.translate(['\\': `\\`, '{': `\{`, '$': `\$`, '}': `\}`]) ~ ")");
684 						else if (fn.returnType == "void")
685 							buf.put("()"); // make functions that don't return add (), otherwise they might be attributes and don't need that
686 						buf.put(";");
687 						endSnippet();
688 						buf.put("\n");
689 					}
690 					else if (fn.returnType != "void")
691 					{
692 						method.debugImpl = true;
693 						buf.put("\n\t");
694 						if (snippetExtensions)
695 						{
696 							startSnippet(false);
697 							buf.put('|');
698 							// choice snippet
699 
700 							if (fn.returnType.endsWith("[]"))
701 								buf.put("return null; // TODO: implement");
702 							else
703 								buf.put("return " ~ fn.returnType.translate([
704 											'\\': `\\`,
705 											'{': `\{`,
706 											'$': `\$`,
707 											'}': `\}`,
708 											'|': `\|`,
709 											',': `\,`
710 										]) ~ ".init; // TODO: implement");
711 
712 							buf.put(',');
713 
714 							buf.put(`assert(false\, "Method ` ~ fn.name ~ ` not implemented");`);
715 
716 							buf.put('|');
717 							endSnippet();
718 						}
719 						else
720 						{
721 							if (fn.isNothrowOrNogc)
722 							{
723 								if (fn.returnType.endsWith("[]"))
724 									buf.put("return null; // TODO: implement");
725 								else
726 									buf.put("return " ~ fn.returnType.translate([
727 												'\\': `\\`,
728 												'{': `\{`,
729 												'$': `\$`,
730 												'}': `\}`
731 											]) ~ ".init; // TODO: implement");
732 							}
733 							else
734 								buf.put(`assert(false, "Method ` ~ fn.name ~ ` not implemented");`);
735 						}
736 						buf.put("\n");
737 					}
738 					else if (snippetExtensions)
739 					{
740 						buf.put("\n\t");
741 						startSnippet(false);
742 						endSnippet();
743 						buf.put("\n");
744 					}
745 
746 					buf.put("}");
747 
748 					method.code = buf.data;
749 					methods.put(method);
750 				}
751 			}
752 
753 			foreach (parent; tree.inherits)
754 				processTree(parent);
755 		}
756 
757 		processTree(tree);
758 
759 		if (formatCode && instance.has!DfmtComponent)
760 		{
761 			foreach (ref method; methods.data)
762 				method.code = instance.get!DfmtComponent.formatSync(method.code, formatArgs).strip;
763 		}
764 
765 		foreach (ref method; methods.data)
766 		{
767 			// TODO: replacing using aho-corasick would be far more efficient but there is nothing like that in phobos
768 			foreach (key, value; snippetReplacements)
769 			{
770 				method.code = method.code.replace(key, value);
771 			}
772 		}
773 
774 		return methods.data;
775 	}
776 
777 	/// Looks up a declaration of a type and then extracts information about it as class or interface.
778 	InterfaceDetails lookupInterface(scope const(char)[] code, int position)
779 	{
780 		auto data = get!DCDComponent.findDeclaration(code, position).getBlocking;
781 		string file = data.file;
782 		int newPosition = data.position;
783 
784 		if (!file.length || !newPosition)
785 			return InterfaceDetails.init;
786 
787 		auto newCode = code;
788 		if (file != "stdin")
789 			newCode = readText(file);
790 
791 		return getInterfaceDetails(file, newCode, newPosition);
792 	}
793 
794 	/// Extracts information about a given class or interface at the given position.
795 	InterfaceDetails getInterfaceDetails(string file, scope const(char)[] code, int position)
796 	{
797 		RollbackAllocator rba;
798 		auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache);
799 		auto parsed = parseModule(tokens, file, &rba);
800 		auto reader = new InterfaceMethodFinder(code, position);
801 		reader.visit(parsed);
802 		return reader.details;
803 	}
804 
805 	Future!InterfaceTree describeInterfaceRecursive(scope const(char)[] code, int position)
806 	{
807 		mixin(gthreadsAsyncProxy!`describeInterfaceRecursiveSync(code, position)`);
808 	}
809 
810 	InterfaceTree describeInterfaceRecursiveSync(scope const(char)[] code, int position)
811 	{
812 		auto baseInterface = getInterfaceDetails("stdin", code, position);
813 
814 		InterfaceTree tree = InterfaceTree(baseInterface);
815 
816 		InterfaceTree* treeByName(InterfaceTree* tree, string name)
817 		{
818 			if (tree.details.name == name)
819 				return tree;
820 			foreach (ref parent; tree.inherits)
821 			{
822 				InterfaceTree* t = treeByName(&parent, name);
823 				if (t !is null)
824 					return t;
825 			}
826 			return null;
827 		}
828 
829 		void traverseTree(ref InterfaceTree sub)
830 		{
831 			foreach (i, parent; sub.details.parentPositions)
832 			{
833 				string parentName = sub.details.normalizedParents[i];
834 				if (treeByName(&tree, parentName) is null)
835 				{
836 					auto details = lookupInterface(sub.details.code, parent);
837 					details.name = parentName;
838 					sub.inherits ~= InterfaceTree(details);
839 				}
840 			}
841 			foreach (ref inherit; sub.inherits)
842 				traverseTree(inherit);
843 		}
844 
845 		traverseTree(tree);
846 
847 		return tree;
848 	}
849 
850 	Related[] highlightRelated(scope const(char)[] code, int position)
851 	{
852 		auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache);
853 		if (!tokens.length)
854 			return null;
855 		auto token = tokens.tokenIndexAtByteIndex(position);
856 		if (token >= tokens.length || !tokens[token].isLikeIdentifier)
857 			return null;
858 
859 		Related[] ret;
860 
861 		switch (tokens[token].type)
862 		{
863 		case tok!"static":
864 			if (token + 1 < tokens.length)
865 			{
866 				if (tokens[token + 1].type == tok!"if")
867 				{
868 					token++;
869 					goto case tok!"if";
870 				}
871 				else if (tokens[token + 1].type == tok!"foreach" || tokens[token + 1].type == tok!"foreach_reverse")
872 				{
873 					token++;
874 					goto case tok!"for";
875 				}
876 			}
877 			goto default;
878 		case tok!"if":
879 		case tok!"else":
880 			// if lister
881 			auto finder = new IfFinder();
882 			finder.target = tokens[token].index;
883 			RollbackAllocator rba;
884 			auto parsed = parseModule(tokens, "stdin", &rba);
885 			finder.visit(parsed);
886 			foreach (ifToken; finder.foundIf)
887 				ret ~= Related(Related.Type.controlFlow, [ifToken.index, ifToken.index + ifToken.tokenText.length]);
888 			break;
889 		case tok!"for":
890 		case tok!"foreach":
891 		case tok!"foreach_reverse":
892 		case tok!"while":
893 		case tok!"do":
894 		case tok!"break":
895 		case tok!"continue":
896 			// loop and switch matcher
897 			// special case for switch
898 			auto finder = new BreakFinder();
899 			finder.target = tokens[token].index;
900 			finder.isBreak = tokens[token].type == tok!"break";
901 			finder.isLoop = !(tokens[token].type == tok!"break" || tokens[token].type == tok!"continue");
902 			if (token + 1 < tokens.length && tokens[token + 1].type == tok!"identifier")
903 				finder.label = tokens[token + 1].text;
904 			RollbackAllocator rba;
905 			auto parsed = parseModule(tokens, "stdin", &rba);
906 			finder.visit(parsed);
907 
908 			if (finder.isLoop && finder.foundBlock.length)
909 			{
910 				auto retFinder = new ReverseReturnFinder();
911 				retFinder.target = finder.target;
912 				retFinder.visit(parsed);
913 				finder.foundBlock ~= retFinder.returns;
914 				finder.foundBlock.sort!"a.index < b.index";
915 			}
916 
917 			foreach (blockToken; finder.foundBlock)
918 				ret ~= Related(Related.Type.controlFlow, [blockToken.index, blockToken.index + blockToken.tokenText.length]);
919 			break;
920 		case tok!"switch":
921 		case tok!"case":
922 		case tok!"default":
923 			// switch/case lister
924 			auto finder = new SwitchFinder();
925 			finder.target = tokens[token].index;
926 			RollbackAllocator rba;
927 			auto parsed = parseModule(tokens, "stdin", &rba);
928 			finder.visit(parsed);
929 			foreach (switchToken; finder.foundSwitch)
930 				ret ~= Related(Related.Type.controlFlow, [switchToken.index, switchToken.index + switchToken.tokenText.length]);
931 			break;
932 		case tok!"return":
933 			// return effect lister
934 			auto finder = new ReturnFinder();
935 			finder.target = tokens[token].index;
936 			RollbackAllocator rba;
937 			auto parsed = parseModule(tokens, "stdin", &rba);
938 			finder.visit(parsed);
939 			foreach (switchToken; finder.related)
940 				ret ~= Related(Related.Type.controlFlow, [switchToken.index, switchToken.index + switchToken.tokenText.length]);
941 			break;
942 		default:
943 			// exact token / string matcher
944 			auto currentText = tokens[token].tokenText;
945 			foreach (i, tok; tokens)
946 			{
947 				if (tok.type == tokens[token].type && tok.text == tokens[token].text)
948 					ret ~= Related(Related.Type.exactToken, [tok.index, tok.index + currentText.length]);
949 				else if (tok.type.isSomeString && tok.evaluateExpressionString == currentText)
950 					ret ~= Related(Related.Type.exactString, [tok.index, tok.index + tok.text.length]);
951 			}
952 			break;
953 		}
954 
955 		return ret;
956 	}
957 
958 	/// Formats DCD definitions (symbol declarations) in a readable format.
959 	/// For functions this formats each argument in a separate line.
960 	/// For other symbols the definition is returned as-is.
961 	string formatDefinitionBlock(string definition)
962 	{
963 		// DCD definition help contains calltips for functions, which always end
964 		// with )
965 		if (!definition.endsWith(")"))
966 			return definition;
967 
968 		auto tokens = getTokensForParser(cast(const(ubyte)[]) definition ~ ';',
969 			config, &workspaced.stringCache);
970 		if (!tokens.length)
971 			return definition;
972 
973 		RollbackAllocator rba;
974 		auto parser = new Parser();
975 		parser.fileName = "stdin";
976 		parser.tokens = tokens;
977 		parser.messageFunction = null;
978 		parser.messageDelegate = null;
979 		parser.allocator = &rba;
980 		const Declaration decl = parser.parseDeclaration(
981 			false, // strict
982 			true // must be declaration (for constructor)
983 		);
984 		if (!decl)
985 			return definition;
986 
987 		const FunctionDeclaration funcdecl = decl.functionDeclaration;
988 		const Constructor ctor = decl.constructor;
989 		if (!funcdecl && !ctor)
990 			return definition;
991 
992 		auto ret = appender!string();
993 		ret.reserve(definition.length);
994 
995 		if (funcdecl)
996 			ret.put(definition[0 .. funcdecl.name.index + funcdecl.name.text.length]);
997 		else if (ctor)
998 			ret.put("this");
999 
1000 		const templateParameters = funcdecl ? funcdecl.templateParameters : ctor.templateParameters;
1001 		if (templateParameters && templateParameters.templateParameterList)
1002 		{
1003 			const params = templateParameters.templateParameterList.items;
1004 			ret.put("(\n");
1005 			foreach (i, param; params)
1006 			{
1007 				assert(param.tokens.length, "no tokens for template parameter?!");
1008 				const start = param.tokens[0].index;
1009 				const end = param.tokens[$ - 1].index + tokenText(param.tokens[$ - 1]).length;
1010 				const hasNext = i + 1 < params.length;
1011 				ret.put("\t");
1012 				ret.put(definition[start .. end]);
1013 				if (hasNext)
1014 					ret.put(",");
1015 				ret.put("\n");
1016 			}
1017 			ret.put(")");
1018 		}
1019 
1020 		const parameters = funcdecl ? funcdecl.parameters : ctor.parameters;
1021 		if (parameters && (parameters.parameters.length || parameters.hasVarargs))
1022 		{
1023 			const params = parameters.parameters;
1024 			ret.put("(\n");
1025 			foreach (i, param; params)
1026 			{
1027 				assert(param.tokens.length, "no tokens for parameter?!");
1028 				const start = param.tokens[0].index;
1029 				const end = param.tokens[$ - 1].index + tokenText(param.tokens[$ - 1]).length;
1030 				const hasNext = parameters.hasVarargs || i + 1 < params.length;
1031 				ret.put("\t");
1032 				ret.put(definition[start .. end]);
1033 				if (hasNext)
1034 					ret.put(",");
1035 				ret.put("\n");
1036 			}
1037 			if (parameters.hasVarargs)
1038 				ret.put("\t...\n");
1039 			ret.put(")");
1040 		}
1041 		else
1042 		{
1043 			ret.put("()");
1044 		}
1045 
1046 		return ret.data;
1047 	}
1048 
1049 private:
1050 	LexerConfig config;
1051 }
1052 
1053 ///
1054 enum CodeRegionType : int
1055 {
1056 	/// null region (unset)
1057 	init,
1058 	/// Imports inside the block
1059 	imports = 1 << 0,
1060 	/// Aliases `alias foo this;`, `alias Type = Other;`
1061 	aliases = 1 << 1,
1062 	/// Nested classes/structs/unions/etc.
1063 	types = 1 << 2,
1064 	/// Raw variables `Type name;`
1065 	fields = 1 << 3,
1066 	/// Normal constructors `this(Args args)`
1067 	ctor = 1 << 4,
1068 	/// Copy constructors `this(this)`
1069 	copyctor = 1 << 5,
1070 	/// Destructors `~this()`
1071 	dtor = 1 << 6,
1072 	/// Properties (functions annotated with `@property`)
1073 	properties = 1 << 7,
1074 	/// Regular functions
1075 	methods = 1 << 8,
1076 }
1077 
1078 ///
1079 enum CodeRegionProtection : int
1080 {
1081 	/// null protection (unset)
1082 	init,
1083 	/// default (unmarked) protection
1084 	default_ = 1 << 0,
1085 	/// public protection
1086 	public_ = 1 << 1,
1087 	/// package (automatic) protection
1088 	package_ = 1 << 2,
1089 	/// package (manual package name) protection
1090 	packageIdentifier = 1 << 3,
1091 	/// protected protection
1092 	protected_ = 1 << 4,
1093 	/// private protection
1094 	private_ = 1 << 5,
1095 }
1096 
1097 ///
1098 enum CodeRegionStatic : int
1099 {
1100 	/// null static (unset)
1101 	init,
1102 	/// non-static code
1103 	instanced = 1 << 0,
1104 	/// static code
1105 	static_ = 1 << 1,
1106 }
1107 
1108 /// Represents a class/interface/struct/union/template with body.
1109 struct CodeBlockInfo
1110 {
1111 	///
1112 	enum Type : int
1113 	{
1114 		// keep the underlines in these values for range checking properly
1115 
1116 		///
1117 		class_,
1118 		///
1119 		interface_,
1120 		///
1121 		struct_,
1122 		///
1123 		union_,
1124 		///
1125 		template_,
1126 	}
1127 
1128 	static immutable string[] typePrefixes = [
1129 		"class ", "interface ", "struct ", "union ", "template "
1130 	];
1131 
1132 	///
1133 	Type type;
1134 	///
1135 	string name;
1136 	/// Outer range inside the code spanning curly braces and name but not type keyword.
1137 	uint[2] outerRange;
1138 	/// Inner range of body of the block touching, but not spanning curly braces.
1139 	uint[2] innerRange;
1140 
1141 	string prefix() @property
1142 	{
1143 		return typePrefixes[cast(int) type];
1144 	}
1145 }
1146 
1147 ///
1148 struct CalltipsSupport
1149 {
1150 	///
1151 	struct Argument
1152 	{
1153 		/// Ranges of type, name and value not including commas or parentheses, but being right next to them. For calls this is the only important and accurate value.
1154 		int[2] contentRange;
1155 		/// Range of just the type, or for templates also `alias`
1156 		int[2] typeRange;
1157 		/// Range of just the name
1158 		int[2] nameRange;
1159 		/// Range of just the default value
1160 		int[2] valueRange;
1161 		/// True if the type declaration is variadic (using ...), or without typeRange a completely variadic argument
1162 		bool variadic;
1163 
1164 		/// Creates Argument(range, range, range, 0)
1165 		static Argument templateType(int[2] range)
1166 		{
1167 			return Argument(range, range, range);
1168 		}
1169 
1170 		/// Creates Argument(range, 0, range, range)
1171 		static Argument templateValue(int[2] range)
1172 		{
1173 			return Argument(range, typeof(range).init, range, range);
1174 		}
1175 
1176 		/// Creates Argument(range, 0, 0, 0, true)
1177 		static Argument anyVariadic(int[2] range)
1178 		{
1179 			return Argument(range, typeof(range).init, typeof(range).init, typeof(range).init, true);
1180 		}
1181 	}
1182 
1183 	bool hasTemplate() @property
1184 	{
1185 		return hasTemplateParens || templateArgumentsRange != typeof(templateArgumentsRange).init;
1186 	}
1187 
1188 	/// Range starting before exclamation point until after closing bracket or before function opening bracket.
1189 	int[2] templateArgumentsRange;
1190 	///
1191 	bool hasTemplateParens;
1192 	///
1193 	Argument[] templateArgs;
1194 	/// Range starting before opening parentheses until after closing parentheses.
1195 	int[2] functionParensRange;
1196 	///
1197 	Argument[] functionArgs;
1198 	/// True if the function is UFCS or a member function of some object or namespace.
1199 	/// False if this is a global function call.
1200 	bool hasParent;
1201 	/// Start of the function itself.
1202 	int functionStart;
1203 	/// Start of the whole call going up all call parents. (`foo.bar.function` having `foo.bar` as parents)
1204 	int parentStart;
1205 	/// True if cursor is in template parameters
1206 	bool inTemplateParameters;
1207 	/// Number of the active parameter (where the cursor is) or -1 if in none
1208 	int activeParameter = -1;
1209 }
1210 
1211 /// Represents one method automatically implemented off a base interface.
1212 struct ImplementedMethod
1213 {
1214 	/// Contains the interface or class name from where this method is implemented.
1215 	string baseClass;
1216 	/// The name of the function being implemented.
1217 	string name;
1218 	/// True if an automatic implementation calling the base class has been made.
1219 	bool callsSuper;
1220 	/// True if a default implementation that should definitely be changed (assert or for nogc/nothrow simple init return) has been implemented.
1221 	bool debugImpl;
1222 	/// True if the method has been detected as property and implemented as such.
1223 	bool autoProperty;
1224 	/// True if the method is either a getter or a setter but not both. Is none for non-autoProperty methods but also when a getter has been detected but the method returns void.
1225 	bool getter, setter;
1226 	/// Actual code to insert for this class without class indentation but optionally already formatted.
1227 	string code;
1228 }
1229 
1230 /// Contains details about an interface or class and all extended or implemented interfaces/classes recursively.
1231 struct InterfaceTree
1232 {
1233 	/// Details of the template in question.
1234 	InterfaceDetails details;
1235 	/// All inherited classes in lexical order.
1236 	InterfaceTree[] inherits;
1237 
1238 	@SerializeIgnore const(FieldDetails)[] availableVariables(bool onlyPublic = false) const
1239 	{
1240 		if (!inherits.length && !onlyPublic)
1241 			return details.fields;
1242 
1243 		// start with private, add all the public ones later in traverseTree
1244 		auto ret = appender!(typeof(return));
1245 		if (onlyPublic)
1246 			ret.put(details.fields.filter!(a => !a.isPrivate));
1247 		else
1248 			ret.put(details.fields);
1249 
1250 		foreach (sub; inherits)
1251 			ret.put(sub.availableVariables(true));
1252 
1253 		return ret.data;
1254 	}
1255 }
1256 
1257 /// Represents one selection for things related to the queried cursor position.
1258 struct Related
1259 {
1260 	///
1261 	enum Type
1262 	{
1263 		/// token is the same as the selected token (except for non-text tokens)
1264 		exactToken,
1265 		/// string content is exactly equal to identifier text
1266 		exactString,
1267 		/// token is related to control flow:
1268 		/// - all if/else keywords when checking any of them
1269 		/// - loop/switch keyword when checking a break/continue
1270 		controlFlow
1271 	}
1272 
1273 	/// The type of the related selection.
1274 	Type type;
1275 	/// Byte range [from-inclusive, to-exclusive] of the related selection.
1276 	size_t[2] range;
1277 }
1278 
1279 private:
1280 
1281 bool isCalltipable(IdType type)
1282 {
1283 	return type == tok!"identifier" || type == tok!"assert" || type == tok!"import"
1284 		|| type == tok!"mixin" || type == tok!"super" || type == tok!"this" || type == tok!"__traits";
1285 }
1286 
1287 int[2] tokenRange(const Token token)
1288 {
1289 	return [cast(int) token.index, cast(int)(token.index + token.tokenText.length)];
1290 }
1291 
1292 int tokenEnd(const Token token)
1293 {
1294 	return cast(int)(token.index + token.tokenText.length);
1295 }
1296 
1297 int tokenIndex(const(Token)[] tokens, ptrdiff_t i)
1298 {
1299 	if (i > 0 && i == tokens.length)
1300 		return cast(int)(tokens[$ - 1].index + tokens[$ - 1].tokenText.length);
1301 	return i >= 0 ? cast(int) tokens[i].index : 0;
1302 }
1303 
1304 int tokenEndIndex(const(Token)[] tokens, ptrdiff_t i)
1305 {
1306 	if (i > 0 && i == tokens.length)
1307 		return cast(int)(tokens[$ - 1].index + tokens[$ - 1].text.length);
1308 	return i >= 0 ? cast(int)(tokens[i].index + tokens[i].tokenText.length) : 0;
1309 }
1310 
1311 /// Returns the index of the closing parentheses in tokens starting at the opening parentheses which is must be at tokens[open].
1312 ptrdiff_t findClosingParenForward(const(Token)[] tokens, ptrdiff_t open, string what = null)
1313 in(tokens[open].type == tok!"(",
1314 		"Calling findClosingParenForward must be done on a ( token and not on a " ~ str(
1315 			tokens[open].type) ~ " token! " ~ what)
1316 {
1317 	if (open >= tokens.length || open < 0)
1318 		return open;
1319 
1320 	open++;
1321 
1322 	int depth = 1;
1323 	int subDepth = 0;
1324 	while (open < tokens.length)
1325 	{
1326 		const c = tokens[open];
1327 
1328 		if (c == tok!"(")
1329 			depth++;
1330 		else if (c == tok!"{")
1331 			subDepth++;
1332 		else if (c == tok!"}")
1333 		{
1334 			if (subDepth == 0)
1335 				break;
1336 			subDepth--;
1337 		}
1338 		else
1339 		{
1340 			if (c == tok!";" && subDepth == 0)
1341 				break;
1342 
1343 			if (c == tok!")")
1344 				depth--;
1345 
1346 			if (depth == 0)
1347 				break;
1348 		}
1349 
1350 		open++;
1351 	}
1352 	return open;
1353 }
1354 
1355 CalltipsSupport.Argument[] splitArgs(const(Token)[] tokens)
1356 {
1357 	auto ret = appender!(CalltipsSupport.Argument[]);
1358 	size_t start = 0;
1359 	size_t valueStart = 0;
1360 
1361 	int depth, subDepth;
1362 	const targetDepth = tokens.length > 0 && tokens[0].type == tok!"(" ? 1 : 0;
1363 	bool gotValue;
1364 
1365 	void putArg(size_t end)
1366 	{
1367 		if (start >= end || start >= tokens.length)
1368 			return;
1369 
1370 		CalltipsSupport.Argument arg;
1371 
1372 		auto typename = tokens[start .. end];
1373 		arg.contentRange = [cast(int) typename[0].index, typename[$ - 1].tokenEnd];
1374 		if (typename.length == 1)
1375 		{
1376 			auto t = typename[0];
1377 			if (t.type == tok!"identifier" || t.type.isBasicType)
1378 				arg = CalltipsSupport.Argument.templateType(t.tokenRange);
1379 			else if (t.type == tok!"...")
1380 				arg = CalltipsSupport.Argument.anyVariadic(t.tokenRange);
1381 			else
1382 				arg = CalltipsSupport.Argument.templateValue(t.tokenRange);
1383 		}
1384 		else
1385 		{
1386 			if (gotValue && valueStart > start && valueStart <= end)
1387 			{
1388 				typename = tokens[start .. valueStart];
1389 				auto val = tokens[valueStart .. end];
1390 				if (val.length)
1391 					arg.valueRange = [cast(int) val[0].index, val[$ - 1].tokenEnd];
1392 			}
1393 
1394 			else if (typename.length == 1)
1395 			{
1396 				auto t = typename[0];
1397 				if (t.type == tok!"identifier" || t.type.isBasicType)
1398 					arg.typeRange = arg.nameRange = t.tokenRange;
1399 				else
1400 					arg.typeRange = t.tokenRange;
1401 			}
1402 			else if (typename.length)
1403 			{
1404 				if (typename[$ - 1].type == tok!"identifier")
1405 				{
1406 					arg.nameRange = typename[$ - 1].tokenRange;
1407 					typename = typename[0 .. $ - 1];
1408 				}
1409 				else if (typename[$ - 1].type == tok!"...")
1410 				{
1411 					arg.variadic = true;
1412 					if (typename.length > 1 && typename[$ - 2].type == tok!"identifier")
1413 					{
1414 						arg.nameRange = typename[$ - 2].tokenRange;
1415 						typename = typename[0 .. $ - 2];
1416 					}
1417 					else
1418 						typename = typename[0 .. 0];
1419 				}
1420 
1421 				if (typename.length)
1422 					arg.typeRange = [cast(int) typename[0].index, typename[$ - 1].tokenEnd];
1423 			}
1424 		}
1425 
1426 		ret.put(arg);
1427 
1428 		gotValue = false;
1429 		start = end + 1;
1430 	}
1431 
1432 	foreach (i, token; tokens)
1433 	{
1434 		if (token.type == tok!"{")
1435 			subDepth++;
1436 		else if (token.type == tok!"}")
1437 		{
1438 			if (subDepth == 0)
1439 				break;
1440 			subDepth--;
1441 		}
1442 		else if (token.type == tok!"(" || token.type == tok!"[")
1443 			depth++;
1444 		else if (token.type == tok!")" || token.type == tok!"]")
1445 		{
1446 			if (depth <= targetDepth)
1447 				break;
1448 			depth--;
1449 		}
1450 
1451 		if (depth == targetDepth)
1452 		{
1453 			if (token.type == tok!",")
1454 				putArg(i);
1455 			else if (token.type == tok!":" || token.type == tok!"=")
1456 			{
1457 				if (!gotValue)
1458 				{
1459 					valueStart = i + 1;
1460 					gotValue = true;
1461 				}
1462 			}
1463 		}
1464 	}
1465 	putArg(tokens.length);
1466 
1467 	return ret.data;
1468 }
1469 
1470 auto indent(scope const(char)[] code, string indentation)
1471 {
1472 	return code.lineSplitter!(KeepTerminator.yes)
1473 		.map!(a => a.length ? indentation ~ a : a)
1474 		.join;
1475 }
1476 
1477 bool fieldNameMatches(string field, in char[] expected)
1478 {
1479 	import std.uni : sicmp;
1480 
1481 	if (field.startsWith("_"))
1482 		field = field[1 .. $];
1483 	else if (field.startsWith("m_"))
1484 		field = field[2 .. $];
1485 	else if (field.length >= 2 && field[0] == 'm' && field[1].isUpper)
1486 		field = field[1 .. $];
1487 
1488 	return field.sicmp(expected) == 0;
1489 }
1490 
1491 final class CodeBlockInfoFinder : ASTVisitor
1492 {
1493 	this(int targetPosition)
1494 	{
1495 		this.targetPosition = targetPosition;
1496 	}
1497 
1498 	override void visit(const ClassDeclaration dec)
1499 	{
1500 		visitContainer(dec.name, CodeBlockInfo.Type.class_, dec.structBody);
1501 	}
1502 
1503 	override void visit(const InterfaceDeclaration dec)
1504 	{
1505 		visitContainer(dec.name, CodeBlockInfo.Type.interface_, dec.structBody);
1506 	}
1507 
1508 	override void visit(const StructDeclaration dec)
1509 	{
1510 		visitContainer(dec.name, CodeBlockInfo.Type.struct_, dec.structBody);
1511 	}
1512 
1513 	override void visit(const UnionDeclaration dec)
1514 	{
1515 		visitContainer(dec.name, CodeBlockInfo.Type.union_, dec.structBody);
1516 	}
1517 
1518 	override void visit(const TemplateDeclaration dec)
1519 	{
1520 		if (cast(int) targetPosition >= cast(int) dec.name.index && targetPosition < dec.endLocation)
1521 		{
1522 			block = CodeBlockInfo.init;
1523 			block.type = CodeBlockInfo.Type.template_;
1524 			block.name = dec.name.text;
1525 			block.outerRange = [
1526 				cast(uint) dec.name.index, cast(uint) dec.endLocation + 1
1527 			];
1528 			block.innerRange = [
1529 				cast(uint) dec.startLocation + 1, cast(uint) dec.endLocation
1530 			];
1531 			dec.accept(this);
1532 		}
1533 	}
1534 
1535 	private void visitContainer(const Token name, CodeBlockInfo.Type type, const StructBody structBody)
1536 	{
1537 		if (!structBody)
1538 			return;
1539 		if (cast(int) targetPosition >= cast(int) name.index && targetPosition < structBody.endLocation)
1540 		{
1541 			block = CodeBlockInfo.init;
1542 			block.type = type;
1543 			block.name = name.text;
1544 			block.outerRange = [
1545 				cast(uint) name.index, cast(uint) structBody.endLocation + 1
1546 			];
1547 			block.innerRange = [
1548 				cast(uint) structBody.startLocation + 1, cast(uint) structBody.endLocation
1549 			];
1550 			structBody.accept(this);
1551 		}
1552 	}
1553 
1554 	alias visit = ASTVisitor.visit;
1555 
1556 	CodeBlockInfo block;
1557 	int targetPosition;
1558 }
1559 
1560 version (unittest) static immutable string SimpleClassTestCode = q{
1561 module foo;
1562 
1563 class FooBar
1564 {
1565 public:
1566 	int i; // default instanced fields
1567 	string s;
1568 	long l;
1569 
1570 	public this() // public instanced ctor
1571 	{
1572 		i = 4;
1573 	}
1574 
1575 protected:
1576 	int x; // protected instanced field
1577 
1578 private:
1579 	static const int foo() @nogc nothrow pure @system // private static methods
1580 	{
1581 		if (s == "a")
1582 		{
1583 			i = 5;
1584 		}
1585 	}
1586 
1587 	static void bar1() {}
1588 
1589 	void bar2() {} // private instanced methods
1590 	void bar3() {}
1591 
1592 	struct Something { string bar; }
1593 
1594 	FooBar.Something somefunc() { return Something.init; }
1595 	Something somefunc2() { return Something.init; }
1596 }};
1597 
1598 unittest
1599 {
1600 	scope backend = new WorkspaceD();
1601 	auto workspace = makeTemporaryTestingWorkspace;
1602 	auto instance = backend.addInstance(workspace.directory);
1603 	backend.register!DCDExtComponent;
1604 	DCDExtComponent dcdext = instance.get!DCDExtComponent;
1605 
1606 	assert(dcdext.getCodeBlockRange(SimpleClassTestCode, 123) == CodeBlockInfo(CodeBlockInfo.Type.class_,
1607 			"FooBar", [20, SimpleClassTestCode.length], [
1608 				28, SimpleClassTestCode.length - 1
1609 			]));
1610 	assert(dcdext.getCodeBlockRange(SimpleClassTestCode, 19) == CodeBlockInfo.init);
1611 	assert(dcdext.getCodeBlockRange(SimpleClassTestCode, 20) != CodeBlockInfo.init);
1612 
1613 	auto replacements = dcdext.insertCodeInContainer("void foo()\n{\n\twriteln();\n}",
1614 			SimpleClassTestCode, 123);
1615 
1616 	// TODO: make insertCodeInContainer work properly?
1617 }
1618 
1619 unittest
1620 {
1621 	import std.conv;
1622 
1623 	scope backend = new WorkspaceD();
1624 	auto workspace = makeTemporaryTestingWorkspace;
1625 	auto instance = backend.addInstance(workspace.directory);
1626 	backend.register!DCDExtComponent;
1627 	DCDExtComponent dcdext = instance.get!DCDExtComponent;
1628 
1629 	auto extract = dcdext.extractCallParameters("int x; foo.bar(4, fgerg\n\nfoo(); int y;", 23);
1630 	assert(!extract.hasTemplate);
1631 	assert(extract.parentStart == 7);
1632 	assert(extract.functionStart == 11);
1633 	assert(extract.functionParensRange[0] == 14);
1634 	assert(extract.functionParensRange[1] <= 31);
1635 	assert(extract.functionArgs.length == 2);
1636 	assert(extract.functionArgs[0].contentRange == [15, 16]);
1637 	assert(extract.functionArgs[1].contentRange[0] == 18);
1638 	assert(extract.functionArgs[1].contentRange[1] <= 31);
1639 
1640 	extract = dcdext.extractCallParameters("int x; foo.bar(4, fgerg)\n\nfoo(); int y;", 23);
1641 	assert(!extract.hasTemplate);
1642 	assert(extract.parentStart == 7);
1643 	assert(extract.functionStart == 11);
1644 	assert(extract.functionParensRange == [14, 24]);
1645 	assert(extract.functionArgs.length == 2);
1646 	assert(extract.functionArgs[0].contentRange == [15, 16]);
1647 	assert(extract.functionArgs[1].contentRange == [18, 23]);
1648 
1649 	extract = dcdext.extractCallParameters("void foo()", 9, true);
1650 	assert(extract != CalltipsSupport.init);
1651 	extract = dcdext.extractCallParameters("void foo()", 10, true);
1652 	assert(extract == CalltipsSupport.init);
1653 
1654 	// caused segfault once, doesn't return anything important
1655 	extract = dcdext.extractCallParameters(`SomeType!(int,"int_")foo(T,Args...)(T a,T b,string[string] map,Other!"(" stuff1,SomeType!(double,")double")myType,Other!"(" stuff,Other!")")`,
1656 			140, true);
1657 	assert(extract == CalltipsSupport.init);
1658 
1659 	extract = dcdext.extractCallParameters(
1660 			`auto bar(int foo, Button, my.Callback cb, ..., int[] arr ...)`, 60, true);
1661 	assert(extract != CalltipsSupport.init);
1662 	assert(!extract.hasTemplate);
1663 	assert(!extract.inTemplateParameters);
1664 	assert(extract.activeParameter == 4);
1665 	assert(extract.functionStart == 5);
1666 	assert(extract.parentStart == 5);
1667 	assert(extract.functionParensRange == [8, 61]);
1668 	assert(extract.functionArgs.length == 5);
1669 	assert(extract.functionArgs[0].contentRange == [9, 16]);
1670 	assert(extract.functionArgs[0].typeRange == [9, 12]);
1671 	assert(extract.functionArgs[0].nameRange == [13, 16]);
1672 	assert(extract.functionArgs[1].contentRange == [18, 24]);
1673 	assert(extract.functionArgs[1].typeRange == [18, 24]);
1674 	assert(extract.functionArgs[1].nameRange == [18, 24]);
1675 	assert(extract.functionArgs[2].contentRange == [26, 40]);
1676 	assert(extract.functionArgs[2].typeRange == [26, 37]);
1677 	assert(extract.functionArgs[2].nameRange == [38, 40]);
1678 	assert(extract.functionArgs[3].contentRange == [42, 45]);
1679 	assert(extract.functionArgs[3].variadic);
1680 	assert(extract.functionArgs[4].contentRange == [47, 60]);
1681 	assert(extract.functionArgs[4].typeRange == [47, 52]);
1682 	assert(extract.functionArgs[4].nameRange == [53, 56]);
1683 	assert(extract.functionArgs[4].variadic);
1684 
1685 	extract = dcdext.extractCallParameters(q{SomeType!(int, "int_") foo(T, Args...)(T a, T b, string[string] map, Other!"(" stuff1, SomeType!(double, ")double") myType, Other!"(" stuff, Other!")")},
1686 			150, true);
1687 	assert(extract != CalltipsSupport.init);
1688 	assert(extract.hasTemplate);
1689 	assert(extract.templateArgumentsRange == [26, 38]);
1690 	assert(extract.templateArgs.length == 2);
1691 	assert(extract.templateArgs[0].contentRange == [27, 28]);
1692 	assert(extract.templateArgs[0].nameRange == [27, 28]);
1693 	assert(extract.templateArgs[1].contentRange == [30, 37]);
1694 	assert(extract.templateArgs[1].nameRange == [30, 34]);
1695 	assert(extract.functionStart == 23);
1696 	assert(extract.parentStart == 23);
1697 	assert(extract.functionParensRange == [38, 151]);
1698 	assert(extract.functionArgs.length == 7);
1699 	assert(extract.functionArgs[0].contentRange == [39, 42]);
1700 	assert(extract.functionArgs[0].typeRange == [39, 40]);
1701 	assert(extract.functionArgs[0].nameRange == [41, 42]);
1702 	assert(extract.functionArgs[1].contentRange == [44, 47]);
1703 	assert(extract.functionArgs[1].typeRange == [44, 45]);
1704 	assert(extract.functionArgs[1].nameRange == [46, 47]);
1705 	assert(extract.functionArgs[2].contentRange == [49, 67]);
1706 	assert(extract.functionArgs[2].typeRange == [49, 63]);
1707 	assert(extract.functionArgs[2].nameRange == [64, 67]);
1708 	assert(extract.functionArgs[3].contentRange == [69, 85]);
1709 	assert(extract.functionArgs[3].typeRange == [69, 78]);
1710 	assert(extract.functionArgs[3].nameRange == [79, 85]);
1711 	assert(extract.functionArgs[4].contentRange == [87, 122]);
1712 	assert(extract.functionArgs[4].typeRange == [87, 115]);
1713 	assert(extract.functionArgs[4].nameRange == [116, 122]);
1714 	assert(extract.functionArgs[5].contentRange == [124, 139]);
1715 	assert(extract.functionArgs[5].typeRange == [124, 133]);
1716 	assert(extract.functionArgs[5].nameRange == [134, 139]);
1717 	assert(extract.functionArgs[6].contentRange == [141, 150]);
1718 	assert(extract.functionArgs[6].typeRange == [141, 150]);
1719 
1720 	extract = dcdext.extractCallParameters(`some_garbage(code); before(this); funcCall(4`, 44);
1721 	assert(extract != CalltipsSupport.init);
1722 	assert(!extract.hasTemplate);
1723 	assert(extract.activeParameter == 0);
1724 	assert(extract.functionStart == 34);
1725 	assert(extract.parentStart == 34);
1726 	assert(extract.functionArgs.length == 1);
1727 	assert(extract.functionArgs[0].contentRange == [43, 44]);
1728 
1729 	extract = dcdext.extractCallParameters(`some_garbage(code); before(this); funcCall(4, f(4)`, 50);
1730 	assert(extract != CalltipsSupport.init);
1731 	assert(!extract.hasTemplate);
1732 	assert(extract.activeParameter == 1);
1733 	assert(extract.functionStart == 34);
1734 	assert(extract.parentStart == 34);
1735 	assert(extract.functionArgs.length == 2);
1736 	assert(extract.functionArgs[0].contentRange == [43, 44]);
1737 	assert(extract.functionArgs[1].contentRange == [46, 50]);
1738 
1739 	extract = dcdext.extractCallParameters(q{some_garbage(code); before(this); funcCall(4, ["a"], JSONValue(["b": JSONValue("c")]), recursive(func, call!s()), "texts )\"(too"},
1740 			129);
1741 	assert(extract != CalltipsSupport.init);
1742 	assert(!extract.hasTemplate);
1743 	assert(extract.functionStart == 34);
1744 	assert(extract.parentStart == 34);
1745 	assert(extract.functionArgs.length == 5);
1746 	assert(extract.functionArgs[0].contentRange == [43, 44]);
1747 	assert(extract.functionArgs[1].contentRange == [46, 51]);
1748 	assert(extract.functionArgs[2].contentRange == [53, 85]);
1749 	assert(extract.functionArgs[3].contentRange == [87, 112]);
1750 	assert(extract.functionArgs[4].contentRange == [114, 129]);
1751 
1752 	extract = dcdext.extractCallParameters(`void log(T t = T.x, A...)(A a) { call(Foo(["bar":"hello"])); } bool x() const @property { return false; } /// This is not code, but rather documentation`,
1753 			127);
1754 	assert(extract == CalltipsSupport.init);
1755 }
1756 
1757 unittest
1758 {
1759 	scope backend = new WorkspaceD();
1760 	auto workspace = makeTemporaryTestingWorkspace;
1761 	auto instance = backend.addInstance(workspace.directory);
1762 	backend.register!DCDExtComponent;
1763 	DCDExtComponent dcdext = instance.get!DCDExtComponent;
1764 
1765 	auto info = dcdext.describeInterfaceRecursiveSync(SimpleClassTestCode, 23);
1766 	assert(info.details.name == "FooBar");
1767 	assert(info.details.blockRange == [27, 554]);
1768 	assert(info.details.referencedTypes.length == 2);
1769 	assert(info.details.referencedTypes[0].name == "Something");
1770 	assert(info.details.referencedTypes[0].location == 455);
1771 	assert(info.details.referencedTypes[1].name == "string");
1772 	assert(info.details.referencedTypes[1].location == 74);
1773 
1774 	assert(info.details.fields.length == 4);
1775 	assert(info.details.fields[0].name == "i");
1776 	assert(info.details.fields[1].name == "s");
1777 	assert(info.details.fields[2].name == "l");
1778 	assert(info.details.fields[3].name == "x");
1779 
1780 	assert(info.details.types.length == 1);
1781 	assert(info.details.types[0].type == TypeDetails.Type.struct_);
1782 	assert(info.details.types[0].name == ["FooBar", "Something"]);
1783 	assert(info.details.types[0].nameLocation == 420);
1784 
1785 	assert(info.details.methods.length == 6);
1786 	assert(info.details.methods[0].name == "foo");
1787 	assert(
1788 			info.details.methods[0].signature
1789 			== "private static const int foo() @nogc nothrow pure @system;");
1790 	assert(info.details.methods[0].returnType == "int");
1791 	assert(info.details.methods[0].isNothrowOrNogc);
1792 	assert(info.details.methods[0].hasBody);
1793 	assert(!info.details.methods[0].needsImplementation);
1794 	assert(!info.details.methods[0].optionalImplementation);
1795 	assert(info.details.methods[0].definitionRange == [222, 286]);
1796 	assert(info.details.methods[0].blockRange == [286, 324]);
1797 
1798 	assert(info.details.methods[1].name == "bar1");
1799 	assert(info.details.methods[1].signature == "private static void bar1();");
1800 	assert(info.details.methods[1].returnType == "void");
1801 	assert(!info.details.methods[1].isNothrowOrNogc);
1802 	assert(info.details.methods[1].hasBody);
1803 	assert(!info.details.methods[1].needsImplementation);
1804 	assert(!info.details.methods[1].optionalImplementation);
1805 	assert(info.details.methods[1].definitionRange == [334, 346]);
1806 	assert(info.details.methods[1].blockRange == [346, 348]);
1807 
1808 	assert(info.details.methods[2].name == "bar2");
1809 	assert(info.details.methods[2].signature == "private void bar2();");
1810 	assert(info.details.methods[2].returnType == "void");
1811 	assert(!info.details.methods[2].isNothrowOrNogc);
1812 	assert(info.details.methods[2].hasBody);
1813 	assert(!info.details.methods[2].needsImplementation);
1814 	assert(!info.details.methods[2].optionalImplementation);
1815 	assert(info.details.methods[2].definitionRange == [351, 363]);
1816 	assert(info.details.methods[2].blockRange == [363, 365]);
1817 
1818 	assert(info.details.methods[3].name == "bar3");
1819 	assert(info.details.methods[3].signature == "private void bar3();");
1820 	assert(info.details.methods[3].returnType == "void");
1821 	assert(!info.details.methods[3].isNothrowOrNogc);
1822 	assert(info.details.methods[3].hasBody);
1823 	assert(!info.details.methods[3].needsImplementation);
1824 	assert(!info.details.methods[3].optionalImplementation);
1825 	assert(info.details.methods[3].definitionRange == [396, 408]);
1826 	assert(info.details.methods[3].blockRange == [408, 410]);
1827 
1828 	assert(info.details.methods[4].name == "somefunc");
1829 	assert(info.details.methods[4].signature == "private FooBar.Something somefunc();");
1830 	assert(info.details.methods[4].returnType == "FooBar.Something");
1831 	assert(!info.details.methods[4].isNothrowOrNogc);
1832 	assert(info.details.methods[4].hasBody);
1833 	assert(!info.details.methods[4].needsImplementation);
1834 	assert(!info.details.methods[4].optionalImplementation);
1835 	assert(info.details.methods[4].definitionRange == [448, 476]);
1836 	assert(info.details.methods[4].blockRange == [476, 502]);
1837 
1838 	// test normalization of types
1839 	assert(info.details.methods[5].name == "somefunc2");
1840 	assert(info.details.methods[5].signature == "private FooBar.Something somefunc2();",
1841 			info.details.methods[5].signature);
1842 	assert(info.details.methods[5].returnType == "FooBar.Something");
1843 	assert(!info.details.methods[5].isNothrowOrNogc);
1844 	assert(info.details.methods[5].hasBody);
1845 	assert(!info.details.methods[5].needsImplementation);
1846 	assert(!info.details.methods[5].optionalImplementation);
1847 	assert(info.details.methods[5].definitionRange == [504, 526]);
1848 	assert(info.details.methods[5].blockRange == [526, 552]);
1849 }
1850 
1851 unittest
1852 {
1853 	string testCode = q{package interface Foo0
1854 {
1855 	string stringMethod();
1856 	Tuple!(int, string, Array!bool)[][] advancedMethod(int a, int b, string c);
1857 	void normalMethod();
1858 	int attributeSuffixMethod() nothrow @property @nogc;
1859 	private
1860 	{
1861 		void middleprivate1();
1862 		void middleprivate2();
1863 	}
1864 	extern(C) @property @nogc ref immutable int attributePrefixMethod() const;
1865 	final void alreadyImplementedMethod() {}
1866 	deprecated("foo") void deprecatedMethod() {}
1867 	static void staticMethod() {}
1868 	protected void protectedMethod();
1869 private:
1870 	void barfoo();
1871 }};
1872 
1873 	scope backend = new WorkspaceD();
1874 	auto workspace = makeTemporaryTestingWorkspace;
1875 	auto instance = backend.addInstance(workspace.directory);
1876 	backend.register!DCDExtComponent;
1877 	DCDExtComponent dcdext = instance.get!DCDExtComponent;
1878 
1879 	auto info = dcdext.describeInterfaceRecursiveSync(testCode, 20);
1880 	assert(info.details.name == "Foo0");
1881 	assert(info.details.blockRange == [23, 523]);
1882 	assert(info.details.referencedTypes.length == 3);
1883 	assert(info.details.referencedTypes[0].name == "Array");
1884 	assert(info.details.referencedTypes[0].location == 70);
1885 	assert(info.details.referencedTypes[1].name == "Tuple");
1886 	assert(info.details.referencedTypes[1].location == 50);
1887 	assert(info.details.referencedTypes[2].name == "string");
1888 	assert(info.details.referencedTypes[2].location == 26);
1889 
1890 	assert(info.details.fields.length == 0);
1891 
1892 	assert(info.details.methods[0 .. 4].all!"!a.hasBody");
1893 	assert(info.details.methods[0 .. 4].all!"a.needsImplementation");
1894 	assert(info.details.methods.all!"!a.optionalImplementation");
1895 
1896 	assert(info.details.methods.length == 12);
1897 	assert(info.details.methods[0].name == "stringMethod");
1898 	assert(info.details.methods[0].signature == "string stringMethod();");
1899 	assert(info.details.methods[0].returnType == "string");
1900 	assert(!info.details.methods[0].isNothrowOrNogc);
1901 
1902 	assert(info.details.methods[1].name == "advancedMethod");
1903 	assert(info.details.methods[1].signature
1904 			== "Tuple!(int, string, Array!bool)[][] advancedMethod(int a, int b, string c);");
1905 	assert(info.details.methods[1].returnType == "Tuple!(int, string, Array!bool)[][]");
1906 	assert(!info.details.methods[1].isNothrowOrNogc);
1907 
1908 	assert(info.details.methods[2].name == "normalMethod");
1909 	assert(info.details.methods[2].signature == "void normalMethod();");
1910 	assert(info.details.methods[2].returnType == "void");
1911 
1912 	assert(info.details.methods[3].name == "attributeSuffixMethod");
1913 	assert(info.details.methods[3].signature == "int attributeSuffixMethod() nothrow @property @nogc;");
1914 	assert(info.details.methods[3].returnType == "int");
1915 	assert(info.details.methods[3].isNothrowOrNogc);
1916 
1917 	assert(info.details.methods[4].name == "middleprivate1");
1918 	assert(info.details.methods[4].signature == "private void middleprivate1();");
1919 	assert(info.details.methods[4].returnType == "void");
1920 
1921 	assert(info.details.methods[5].name == "middleprivate2");
1922 
1923 	assert(info.details.methods[6].name == "attributePrefixMethod");
1924 	assert(info.details.methods[6].signature
1925 			== "extern (C) @property @nogc ref immutable int attributePrefixMethod() const;");
1926 	assert(info.details.methods[6].returnType == "int");
1927 	assert(info.details.methods[6].isNothrowOrNogc);
1928 
1929 	assert(info.details.methods[7].name == "alreadyImplementedMethod");
1930 	assert(info.details.methods[7].signature == "void alreadyImplementedMethod();");
1931 	assert(info.details.methods[7].returnType == "void");
1932 	assert(!info.details.methods[7].needsImplementation);
1933 	assert(info.details.methods[7].hasBody);
1934 
1935 	assert(info.details.methods[8].name == "deprecatedMethod");
1936 	assert(info.details.methods[8].signature == `deprecated("foo") void deprecatedMethod();`);
1937 	assert(info.details.methods[8].returnType == "void");
1938 	assert(info.details.methods[8].needsImplementation);
1939 	assert(info.details.methods[8].hasBody);
1940 
1941 	assert(info.details.methods[9].name == "staticMethod");
1942 	assert(info.details.methods[9].signature == `static void staticMethod();`);
1943 	assert(info.details.methods[9].returnType == "void");
1944 	assert(!info.details.methods[9].needsImplementation);
1945 	assert(info.details.methods[9].hasBody);
1946 
1947 	assert(info.details.methods[10].name == "protectedMethod");
1948 	assert(info.details.methods[10].signature == `protected void protectedMethod();`);
1949 	assert(info.details.methods[10].returnType == "void");
1950 	assert(info.details.methods[10].needsImplementation);
1951 	assert(!info.details.methods[10].hasBody);
1952 
1953 	assert(info.details.methods[11].name == "barfoo");
1954 	assert(info.details.methods[11].signature == `private void barfoo();`);
1955 	assert(info.details.methods[11].returnType == "void");
1956 	assert(!info.details.methods[11].needsImplementation);
1957 	assert(!info.details.methods[11].hasBody);
1958 }
1959 
1960 unittest
1961 {
1962 	string testCode = q{module hello;
1963 
1964 interface MyInterface
1965 {
1966 	void foo();
1967 }
1968 
1969 class ImplA : MyInterface
1970 {
1971 
1972 }
1973 
1974 class ImplB : MyInterface
1975 {
1976 	void foo() {}
1977 }
1978 };
1979 
1980 	scope backend = new WorkspaceD();
1981 	auto workspace = makeTemporaryTestingWorkspace;
1982 	auto instance = backend.addInstance(workspace.directory);
1983 	backend.register!DCDExtComponent;
1984 	DCDExtComponent dcdext = instance.get!DCDExtComponent;
1985 
1986 	auto info = dcdext.getInterfaceDetails("stdin", testCode, 72);
1987 
1988 	assert(info.blockRange == [81, 85]);
1989 }
1990 
1991 unittest
1992 {
1993 	scope backend = new WorkspaceD();
1994 	auto workspace = makeTemporaryTestingWorkspace;
1995 	auto instance = backend.addInstance(workspace.directory);
1996 	backend.register!DCDExtComponent;
1997 	DCDExtComponent dcdext = instance.get!DCDExtComponent;
1998 
1999 	assert(dcdext.formatDefinitionBlock("Foo!(int, string) x") == "Foo!(int, string) x");
2000 	assert(dcdext.formatDefinitionBlock("void foo()") == "void foo()");
2001 	assert(dcdext.formatDefinitionBlock("void foo(string x)") == "void foo(\n\tstring x\n)");
2002 	assert(dcdext.formatDefinitionBlock("void foo(string x,)") == "void foo(\n\tstring x\n)");
2003 	assert(dcdext.formatDefinitionBlock("void foo(string x, int y)") == "void foo(\n\tstring x,\n\tint y\n)");
2004 	assert(dcdext.formatDefinitionBlock("void foo(string, int)") == "void foo(\n\tstring,\n\tint\n)");
2005 	assert(dcdext.formatDefinitionBlock("this(string, int)") == "this(\n\tstring,\n\tint\n)");
2006 	assert(dcdext.formatDefinitionBlock("auto foo(string, int)") == "auto foo(\n\tstring,\n\tint\n)");
2007 	assert(dcdext.formatDefinitionBlock("ComplexTemplate!(int, 'a', string, Nested!(Foo)) foo(string, int)")
2008 		== "ComplexTemplate!(int, 'a', string, Nested!(Foo)) foo(\n\tstring,\n\tint\n)");
2009 	assert(dcdext.formatDefinitionBlock("auto foo(T, V)(string, int)") == "auto foo(\n\tT,\n\tV\n)(\n\tstring,\n\tint\n)");
2010 	assert(dcdext.formatDefinitionBlock("auto foo(string, int f, ...)") == "auto foo(\n\tstring,\n\tint f,\n\t...\n)");
2011 }
2012 
2013 final class IfFinder : ASTVisitor
2014 {
2015 	Token[] currentIf, foundIf;
2016 
2017 	size_t target;
2018 
2019 	alias visit = ASTVisitor.visit;
2020 
2021 	static foreach (If; AliasSeq!(IfStatement, ConditionalStatement))
2022 	override void visit(const If ifStatement)
2023 	{
2024 		if (foundIf.length)
2025 			return;
2026 
2027 		auto lastIf = currentIf;
2028 		scope (exit)
2029 			currentIf = lastIf;
2030 
2031 		currentIf = [ifStatement.tokens[0]];
2032 
2033 		static auto thenStatement(const If v)
2034 		{
2035 			static if (is(If == IfStatement))
2036 				return v.thenStatement;
2037 			else
2038 				return v.trueStatement;
2039 		}
2040 
2041 		static auto elseStatement(const If v)
2042 		{
2043 			static if (is(If == IfStatement))
2044 				return v.elseStatement;
2045 			else
2046 				return v.falseStatement;
2047 		}
2048 
2049 		if (thenStatement(ifStatement))
2050 			thenStatement(ifStatement).accept(this);
2051 
2052 		const(BaseNode) elseStmt = elseStatement(ifStatement);
2053 		while (elseStmt)
2054 		{
2055 			auto elseToken = elseStmt.tokens.ptr - 1;
2056 
2057 			// possible from if declarations
2058 			if (elseToken.type == tok!"{" || elseToken.type == tok!":")
2059 				elseToken--;
2060 
2061 			if (elseToken.type == tok!"else")
2062 			{
2063 				if (!currentIf.length || currentIf[$ - 1] != *elseToken)
2064 					currentIf ~= *elseToken;
2065 			}
2066 
2067 			if (auto elseIf = cast(IfStatement) elseStmt)
2068 			{
2069 				currentIf ~= elseIf.tokens[0];
2070 				elseIf.accept(this);
2071 				cast()elseStmt = elseIf.elseStatement;
2072 			}
2073 			else if (auto elseStaticIf = cast(ConditionalStatement) elseStmt)
2074 			{
2075 				currentIf ~= elseStaticIf.tokens[0];
2076 				currentIf ~= elseStaticIf.tokens[1];
2077 				elseStaticIf.accept(this);
2078 				cast()elseStmt = elseStaticIf.falseStatement;
2079 			}
2080 			else if (auto declOrStatement = cast(DeclarationOrStatement) elseStmt)
2081 			{
2082 				if (declOrStatement.statement && declOrStatement.statement.statementNoCaseNoDefault)
2083 				{
2084 					if (declOrStatement.statement.statementNoCaseNoDefault.conditionalStatement)
2085 					{
2086 						cast()elseStmt = declOrStatement.statement.statementNoCaseNoDefault.conditionalStatement;
2087 					}
2088 					else if (declOrStatement.statement.statementNoCaseNoDefault.ifStatement)
2089 					{
2090 						cast()elseStmt = declOrStatement.statement.statementNoCaseNoDefault.ifStatement;
2091 					}
2092 					else
2093 					{
2094 						elseStmt.accept(this);
2095 						cast()elseStmt = null;
2096 					}
2097 				}
2098 				else if (declOrStatement.declaration && declOrStatement.declaration.conditionalDeclaration)
2099 				{
2100 					auto cond = declOrStatement.declaration.conditionalDeclaration;
2101 					if (cond.trueDeclarations.length)
2102 					{
2103 						auto ifSearch = cond.trueDeclarations[0].tokens.ptr;
2104 						while (!ifSearch.type.among!(tok!"if", tok!";", tok!"}", tok!"module"))
2105 							ifSearch--;
2106 
2107 						if (ifSearch.type == tok!"if")
2108 						{
2109 							if ((ifSearch - 1).type == tok!"static")
2110 								currentIf ~= *(ifSearch - 1);
2111 							currentIf ~= *ifSearch;
2112 						}
2113 					}
2114 
2115 					if (cond.hasElse && cond.falseDeclarations.length == 1)
2116 					{
2117 						elseStmt.accept(this);
2118 						cast()elseStmt = cast()cond.falseDeclarations[0];
2119 					}
2120 					else
2121 					{
2122 						elseStmt.accept(this);
2123 						cast()elseStmt = null;
2124 					}
2125 				}
2126 				else
2127 				{
2128 					elseStmt.accept(this);
2129 					cast()elseStmt = null;
2130 				}
2131 			}
2132 			else
2133 			{
2134 				elseStmt.accept(this);
2135 				cast()elseStmt = null;
2136 			}
2137 		}
2138 
2139 		saveIfMatching();
2140 	}
2141 
2142 	void saveIfMatching()
2143 	{
2144 		if (foundIf.length)
2145 			return;
2146 
2147 		foreach (v; currentIf)
2148 			if (v.index == target)
2149 			{
2150 				foundIf = currentIf;
2151 				return;
2152 			}
2153 	}
2154 }
2155 
2156 unittest
2157 {
2158 	scope backend = new WorkspaceD();
2159 	auto workspace = makeTemporaryTestingWorkspace;
2160 	auto instance = backend.addInstance(workspace.directory);
2161 	backend.register!DCDExtComponent;
2162 	DCDExtComponent dcdext = instance.get!DCDExtComponent;
2163 
2164 	assert(dcdext.highlightRelated(`void foo()
2165 {
2166 	if (true) {}
2167 	else static if (true) {}
2168 	else if (true) {}
2169 	else {}
2170 
2171 	if (true) {}
2172 	else static if (true) {}
2173 	else {}
2174 }`, 35) == [
2175 	Related(Related.Type.controlFlow, [14, 16]),
2176 	Related(Related.Type.controlFlow, [28, 32]),
2177 	Related(Related.Type.controlFlow, [33, 39]),
2178 	Related(Related.Type.controlFlow, [40, 42]),
2179 	Related(Related.Type.controlFlow, [54, 58]),
2180 	Related(Related.Type.controlFlow, [59, 61]),
2181 	Related(Related.Type.controlFlow, [73, 77]),
2182 ]);
2183 
2184 	assert(dcdext.highlightRelated(`void foo()
2185 {
2186 	if (true) {}
2187 	else static if (true) {}
2188 	else if (true) {}
2189 	else {}
2190 
2191 	if (true) {}
2192 	else static if (true) { int a; }
2193 	else { int b;}
2194 }`, 83) == [
2195 	Related(Related.Type.controlFlow, [83, 85]),
2196 	Related(Related.Type.controlFlow, [97, 101]),
2197 	Related(Related.Type.controlFlow, [102, 108]),
2198 	Related(Related.Type.controlFlow, [109, 111]),
2199 	Related(Related.Type.controlFlow, [131, 135]),
2200 ]);
2201 }
2202 
2203 final class SwitchFinder : ASTVisitor
2204 {
2205 	Token[] currentSwitch, foundSwitch;
2206 	const(Statement) currentStatement;
2207 
2208 	size_t target;
2209 
2210 	alias visit = ASTVisitor.visit;
2211 
2212 	override void visit(const SwitchStatement stmt)
2213 	{
2214 		if (foundSwitch.length)
2215 			return;
2216 
2217 		auto lastSwitch = currentSwitch;
2218 		scope (exit)
2219 			currentSwitch = lastSwitch;
2220 
2221 		currentSwitch = [stmt.tokens[0]];
2222 		stmt.accept(this);
2223 
2224 		saveIfMatching();
2225 	}
2226 
2227 	override void visit(const CaseRangeStatement stmt)
2228 	{
2229 		if (currentStatement)
2230 		{
2231 			auto curr = currentStatement.tokens[0];
2232 			if (curr.type == tok!"case")
2233 				currentSwitch ~= curr;
2234 		}
2235 		auto last = *(stmt.high.tokens.ptr - 1);
2236 		if (last.type == tok!"case")
2237 			currentSwitch ~= last;
2238 		stmt.accept(this);
2239 	}
2240 
2241 	override void visit(const CaseStatement stmt)
2242 	{
2243 		if (currentStatement)
2244 		{
2245 			auto curr = currentStatement.tokens[0];
2246 			if (curr.type == tok!"case")
2247 				currentSwitch ~= curr;
2248 		}
2249 		stmt.accept(this);
2250 	}
2251 
2252 	override void visit(const DefaultStatement stmt)
2253 	{
2254 		currentSwitch ~= stmt.tokens[0];
2255 		stmt.accept(this);
2256 	}
2257 
2258 	override void visit(const Statement stmt)
2259 	{
2260 		auto last = currentStatement;
2261 		scope (exit)
2262 			cast()currentStatement = cast()last;
2263 		cast()currentStatement = cast()stmt;
2264 		stmt.accept(this);
2265 	}
2266 
2267 	void saveIfMatching()
2268 	{
2269 		if (foundSwitch.length)
2270 			return;
2271 
2272 		foreach (v; currentSwitch)
2273 			if (v.index == target)
2274 			{
2275 				foundSwitch = currentSwitch;
2276 				return;
2277 			}
2278 	}
2279 }
2280 
2281 unittest
2282 {
2283 	scope backend = new WorkspaceD();
2284 	auto workspace = makeTemporaryTestingWorkspace;
2285 	auto instance = backend.addInstance(workspace.directory);
2286 	backend.register!DCDExtComponent;
2287 	DCDExtComponent dcdext = instance.get!DCDExtComponent;
2288 
2289 	assert(dcdext.highlightRelated(`void foo()
2290 {
2291 	switch (foo)
2292 	{
2293 		case 1: .. case 3:
2294 			break;
2295 		case 5:
2296 			switch (bar)
2297 			{
2298 			case 6:
2299 				break;
2300 			default:
2301 				break;
2302 			}
2303 			break;
2304 		default:
2305 			break;
2306 	}
2307 }`, 35) == [
2308 	Related(Related.Type.controlFlow, [14, 20]),
2309 	Related(Related.Type.controlFlow, [32, 36]),
2310 	Related(Related.Type.controlFlow, [43, 47]),
2311 	Related(Related.Type.controlFlow, [63, 67]),
2312 	Related(Related.Type.controlFlow, [154, 161]),
2313 ]);
2314 }
2315 
2316 final class BreakFinder : ASTVisitor
2317 {
2318 	Token[] currentBlock, foundBlock;
2319 	const(Statement) currentStatement;
2320 	bool inSwitch;
2321 
2322 	size_t target;
2323 	bool isBreak; // else continue if not loop
2324 	bool isLoop; // checking loop token (instead of break/continue)
2325 	string label;
2326 
2327 	alias visit = ASTVisitor.visit;
2328 
2329 	override void visit(const LabeledStatement stmt)
2330 	{
2331 		if (foundBlock.length)
2332 			return;
2333 
2334 		if (label.length && label == stmt.identifier.text)
2335 		{
2336 			foundBlock = [stmt.identifier];
2337 			return;
2338 		}
2339 
2340 		stmt.accept(this);
2341 	}
2342 
2343 	override void visit(const SwitchStatement stmt)
2344 	{
2345 		if (foundBlock.length)
2346 			return;
2347 
2348 		bool wasSwitch = inSwitch;
2349 		scope (exit)
2350 			inSwitch = wasSwitch;
2351 		inSwitch = true;
2352 
2353 		if (isBreak)
2354 		{
2355 			auto lastSwitch = currentBlock;
2356 			scope (exit)
2357 				currentBlock = lastSwitch;
2358 
2359 			currentBlock = [stmt.tokens[0]];
2360 			stmt.accept(this);
2361 
2362 			saveIfMatching();
2363 		}
2364 		else
2365 		{
2366 			stmt.accept(this);
2367 		}
2368 	}
2369 
2370 	static foreach (LoopT; AliasSeq!(ForeachStatement, StaticForeachDeclaration,
2371 		StaticForeachStatement, ForStatement, WhileStatement))
2372 	override void visit(const LoopT stmt)
2373 	{
2374 		if (foundBlock.length)
2375 			return;
2376 
2377 		auto lastSwitch = currentBlock;
2378 		scope (exit)
2379 			currentBlock = lastSwitch;
2380 
2381 		currentBlock = [stmt.tokens[0]];
2382 		stmt.accept(this);
2383 
2384 		saveIfMatching();
2385 	}
2386 
2387 	override void visit(const DoStatement stmt)
2388 	{
2389 		if (foundBlock.length)
2390 			return;
2391 
2392 		auto lastSwitch = currentBlock;
2393 		scope (exit)
2394 			currentBlock = lastSwitch;
2395 
2396 		currentBlock = [stmt.tokens[0]];
2397 		auto whileTok = *(stmt.expression.tokens.ptr - 2);
2398 		stmt.accept(this);
2399 		if (whileTok.type == tok!"while")
2400 			currentBlock ~= whileTok;
2401 
2402 		saveIfMatching();
2403 	}
2404 
2405 	static foreach (IgnoreT; AliasSeq!(FunctionBody, FunctionDeclaration, StructBody))
2406 	override void visit(const IgnoreT stmt)
2407 	{
2408 		if (foundBlock.length)
2409 			return;
2410 
2411 		auto lastSwitch = currentBlock;
2412 		scope (exit)
2413 			currentBlock = lastSwitch;
2414 
2415 		currentBlock = null;
2416 		stmt.accept(this);
2417 	}
2418 
2419 	override void visit(const CaseRangeStatement stmt)
2420 	{
2421 		if (isBreak)
2422 		{
2423 			if (currentStatement)
2424 			{
2425 				auto curr = currentStatement.tokens[0];
2426 				if (curr.type == tok!"case")
2427 					currentBlock ~= curr;
2428 			}
2429 			auto last = *(stmt.high.tokens.ptr - 1);
2430 			if (last.type == tok!"case")
2431 				currentBlock ~= last;
2432 		}
2433 		stmt.accept(this);
2434 	}
2435 
2436 	override void visit(const CaseStatement stmt)
2437 	{
2438 		if (currentStatement && isBreak)
2439 		{
2440 			auto curr = currentStatement.tokens[0];
2441 			if (curr.type == tok!"case")
2442 				currentBlock ~= curr;
2443 		}
2444 		stmt.accept(this);
2445 	}
2446 
2447 	override void visit(const DefaultStatement stmt)
2448 	{
2449 		if (isBreak)
2450 			currentBlock ~= stmt.tokens[0];
2451 		stmt.accept(this);
2452 	}
2453 
2454 	override void visit(const Statement stmt)
2455 	{
2456 		auto last = currentStatement;
2457 		scope (exit)
2458 			cast()currentStatement = cast()last;
2459 		cast()currentStatement = cast()stmt;
2460 		stmt.accept(this);
2461 	}
2462 
2463 	override void visit(const BreakStatement stmt)
2464 	{
2465 		if (stmt.tokens[0].index == target || isLoop)
2466 			if (isBreak)
2467 				currentBlock ~= stmt.tokens[0];
2468 		stmt.accept(this);
2469 	}
2470 
2471 	override void visit(const ContinueStatement stmt)
2472 	{
2473 		// break token:
2474 		//   continue in switch: ignore
2475 		//   continue outside switch: include
2476 		// other token:
2477 		//   continue in switch: include
2478 		//   continue outside switch: include
2479 		if (stmt.tokens[0].index == target || isLoop)
2480 			if (!(isBreak && inSwitch))
2481 				currentBlock ~= stmt.tokens[0];
2482 		stmt.accept(this);
2483 	}
2484 
2485 	void saveIfMatching()
2486 	{
2487 		if (foundBlock.length || label.length)
2488 			return;
2489 
2490 		foreach (v; currentBlock)
2491 			if (v.index == target)
2492 			{
2493 				foundBlock = currentBlock;
2494 				return;
2495 			}
2496 	}
2497 }
2498 
2499 class ReverseReturnFinder : ASTVisitor
2500 {
2501 	Token[] returns;
2502 	size_t target;
2503 	bool record;
2504 
2505 	alias visit = ASTVisitor.visit;
2506 
2507 	static foreach (DeclT; AliasSeq!(Declaration, Statement))
2508 	override void visit(const DeclT stmt)
2509 	{
2510 		if (returns.length && !record)
2511 			return;
2512 
2513 		bool matches = stmt.tokens.length && stmt.tokens[0].index == target;
2514 		if (matches)
2515 			record = true;
2516 		stmt.accept(this);
2517 		if (matches)
2518 			record = false;
2519 	}
2520 
2521 	override void visit(const ReturnStatement ret)
2522 	{
2523 		if (record)
2524 			returns ~= ret.tokens[0];
2525 		ret.accept(this);
2526 	}
2527 }
2528 
2529 unittest
2530 {
2531 	scope backend = new WorkspaceD();
2532 	auto workspace = makeTemporaryTestingWorkspace;
2533 	auto instance = backend.addInstance(workspace.directory);
2534 	backend.register!DCDExtComponent;
2535 	DCDExtComponent dcdext = instance.get!DCDExtComponent;
2536 
2537 	assert(dcdext.highlightRelated(`void foo()
2538 {
2539 	while (true)
2540 	{
2541 		foreach (a; b)
2542 		{
2543 			switch (a)
2544 			{
2545 			case 1:
2546 				break;
2547 			case 2:
2548 				continue;
2549 			default:
2550 				return;
2551 			}
2552 		}
2553 	}
2554 }`, 88) == [
2555 	Related(Related.Type.controlFlow, [54, 60]),
2556 	Related(Related.Type.controlFlow, [73, 77]),
2557 	Related(Related.Type.controlFlow, [85, 90]),
2558 	Related(Related.Type.controlFlow, [95, 99]),
2559 	Related(Related.Type.controlFlow, [120, 127]),
2560 ]);
2561 
2562 	assert(dcdext.highlightRelated(`void foo()
2563 {
2564 	while (true)
2565 	{
2566 		foreach (a; b)
2567 		{
2568 			switch (a)
2569 			{
2570 			case 1:
2571 				break;
2572 			case 2:
2573 				continue;
2574 			default:
2575 				return;
2576 			}
2577 		}
2578 	}
2579 }`, 111) == [
2580 	Related(Related.Type.controlFlow, [32, 39]),
2581 	Related(Related.Type.controlFlow, [107, 115]),
2582 ]);
2583 
2584 	assert(dcdext.highlightRelated(`void foo()
2585 {
2586 	while (true)
2587 	{
2588 		foreach (a; b)
2589 		{
2590 			switch (a)
2591 			{
2592 			case 1:
2593 				break;
2594 			case 2:
2595 				continue;
2596 			default:
2597 				return;
2598 			}
2599 		}
2600 	}
2601 }`, 15) == [
2602 	Related(Related.Type.controlFlow, [14, 19]),
2603 	Related(Related.Type.controlFlow, [133, 139]),
2604 ]);
2605 }
2606 
2607 class ReturnFinder : ASTVisitor
2608 {
2609 	Token[] returns;
2610 	Token[] currentScope;
2611 	bool inTargetBlock;
2612 	Token[] related;
2613 	size_t target;
2614 
2615 	alias visit = ASTVisitor.visit;
2616 
2617 	static foreach (DeclT; AliasSeq!(FunctionBody))
2618 	override void visit(const DeclT stmt)
2619 	{
2620 		if (inTargetBlock || related.length)
2621 			return;
2622 
2623 		auto lastScope = currentScope;
2624 		scope (exit)
2625 			currentScope = lastScope;
2626 		currentScope = null;
2627 
2628 		auto lastReturns = returns;
2629 		scope (exit)
2630 			returns = lastReturns;
2631 		returns = null;
2632 
2633 		stmt.accept(this);
2634 		if (inTargetBlock)
2635 		{
2636 			related ~= returns;
2637 
2638 			related.sort!"a.index < b.index";
2639 		}
2640 	}
2641 
2642 	static foreach (ScopeT; AliasSeq!(SwitchStatement, ForeachStatement,
2643 		StaticForeachDeclaration, StaticForeachStatement, ForStatement, WhileStatement))
2644 	override void visit(const ScopeT stmt)
2645 	{
2646 		auto lastScope = currentScope;
2647 		scope (exit)
2648 			currentScope = lastScope;
2649 		currentScope ~= stmt.tokens[0];
2650 
2651 		stmt.accept(this);
2652 	}
2653 
2654 	override void visit(const DoStatement stmt)
2655 	{
2656 		auto lastScope = currentScope;
2657 		scope (exit)
2658 			currentScope = lastScope;
2659 		currentScope ~= stmt.tokens[0];
2660 
2661 		auto whileTok = *(stmt.expression.tokens.ptr - 2);
2662 		if (whileTok.type == tok!"while")
2663 			currentScope ~= whileTok;
2664 
2665 		stmt.accept(this);
2666 	}
2667 
2668 	override void visit(const ReturnStatement ret)
2669 	{
2670 		returns ~= ret.tokens[0];
2671 		if (target == ret.tokens[0].index)
2672 		{
2673 			inTargetBlock = true;
2674 			related ~= currentScope;
2675 		}
2676 		ret.accept(this);
2677 	}
2678 }
2679 
2680 unittest
2681 {
2682 	scope backend = new WorkspaceD();
2683 	auto workspace = makeTemporaryTestingWorkspace;
2684 	auto instance = backend.addInstance(workspace.directory);
2685 	backend.register!DCDExtComponent;
2686 	DCDExtComponent dcdext = instance.get!DCDExtComponent;
2687 
2688 	assert(dcdext.highlightRelated(`void foo()
2689 {
2690 	foreach (a; b)
2691 		return;
2692 
2693 	void bar()
2694 	{
2695 		return;
2696 	}
2697 
2698 	bar();
2699 
2700 	return;
2701 }`, 33) == [
2702 	Related(Related.Type.controlFlow, [14, 21]),
2703 	Related(Related.Type.controlFlow, [31, 37]),
2704 	Related(Related.Type.controlFlow, [79, 85]),
2705 ]);
2706 }