1 module served.commands.calltips;
2 
3 import served.extension;
4 import served.types;
5 import served.utils.ddoc;
6 
7 import workspaced.api;
8 import workspaced.com.dcd;
9 import workspaced.com.dcdext;
10 import workspaced.coms;
11 
12 import std.algorithm : max;
13 import std.experimental.logger;
14 import std.json;
15 import std.string : strip, stripRight;
16 
17 /**
18  * Convert DCD calltips to LSP compatible `SignatureHelp` objects
19  * Params:
20  *      calltips = Ddoc strings for each available calltip
21  *      symbols = array of possible signatures as DCD symbols
22  *      textTilCursor = The entire contents of the file being edited up
23  *                      until the cursor
24  */
25 SignatureHelp convertDCDCalltips(DCDExtComponent dcdext, string[] calltips,
26 		DCDCompletions.Symbol[] symbols, CalltipsSupport extractedParams)
27 {
28 	SignatureInformation[] signatures;
29 	int[] paramsCounts; // Number of params for each calltip
30 	SignatureHelp help;
31 
32 	foreach (i, calltip; calltips)
33 	{
34 		if (!calltip.length)
35 			continue;
36 
37 		auto sig = SignatureInformation(calltip);
38 		immutable DCDCompletions.Symbol symbol = symbols[i];
39 		const docs = parseDdoc(symbol.documentation);
40 		if (docs != Comment.init)
41 			sig.documentation = MarkupContent(docs.ddocToMarked);
42 		else if (symbol.documentation.length)
43 			sig.documentation = MarkupContent(symbol.documentation);
44 
45 		CalltipsSupport funcParams = dcdext.extractCallParameters(calltip,
46 				cast(int) calltip.length - 1, true);
47 		if (funcParams != CalltipsSupport.init)
48 		{
49 			if (extractedParams.inTemplateParameters && !funcParams.hasTemplate)
50 				continue;
51 
52 			auto args = extractedParams.inTemplateParameters
53 				? funcParams.templateArgs : funcParams.functionArgs;
54 			if (args.length && args[$ - 1].variadic)
55 				paramsCounts ~= int.max;
56 			else
57 				paramsCounts ~= cast(int) args.length;
58 
59 			ParameterInformation[] retParams;
60 			foreach (param; args)
61 			{
62 				int[2] range = param.nameRange[1] == 0 ? param.contentRange : param.nameRange;
63 				string paramName = calltip[range[0] .. range[1]];
64 				Variant!(string, uint[2]) paramLabel;
65 				if (capabilities.textDocument.orDefault
66 					.signatureHelp.orDefault
67 					.signatureInformation.orDefault
68 					.parameterInformation.orDefault
69 					.labelOffsetSupport.orDefault)
70 					paramLabel = cast(uint[2])[
71 						cast(uint) calltip[0 .. param.contentRange[0]].countUTF16Length,
72 						cast(uint) calltip[0 .. param.contentRange[1]].countUTF16Length
73 					];
74 				else
75 					paramLabel = calltip[param.contentRange[0] .. param.contentRange[1]];
76 				Variant!(void, string, MarkupContent) paramDocs;
77 				if (docs != Comment.init)
78 				{
79 					auto docString = getParamDocumentation(docs, paramName);
80 					if (docString.length)
81 					{
82 						MarkupKind[] formats = capabilities.textDocument.orDefault
83 							.signatureHelp.orDefault
84 							.signatureInformation.orDefault
85 							.documentationFormat.orDefault;
86 						MarkupKind kind = formats.length == 0
87 							? MarkupKind.markdown
88 							: formats[0];
89 						string prefix = kind == MarkupKind.markdown ? "**" ~ paramName ~ "**: " : paramName
90 							~ ": ";
91 						string docRet = kind == MarkupKind.markdown
92 							? ddocToMarkdown(docString.strip) : docString.strip;
93 						paramDocs = MarkupContent(kind, (prefix ~ docRet).stripRight);
94 					}
95 				}
96 				retParams ~= ParameterInformation(paramLabel, paramDocs);
97 			}
98 
99 			sig.parameters = retParams.opt;
100 		}
101 
102 		help.signatures ~= sig;
103 	}
104 
105 	int writtenParamsCount = cast(int)(extractedParams.inTemplateParameters
106 			? extractedParams.templateArgs.length : extractedParams.functionArgs.length);
107 
108 	size_t[] possibleFunctions;
109 	foreach (i, count; paramsCounts)
110 		if (count >= writtenParamsCount)
111 			possibleFunctions ~= i;
112 
113 	if (extractedParams.activeParameter != -1)
114 		help.activeParameter = extractedParams.activeParameter;
115 
116 	help.activeSignature = possibleFunctions.length ? cast(int) possibleFunctions[0] : 0;
117 
118 	return help;
119 }
120 
121 @protocolMethod("textDocument/signatureHelp")
122 SignatureHelp provideSignatureHelp(TextDocumentPositionParams params)
123 {
124 	auto document = documents[params.textDocument.uri];
125 	string file = document.uri.uriToFile;
126 	if (document.languageId == "d")
127 		return provideDSignatureHelp(params, file, document);
128 	else if (document.languageId == "diet")
129 		return provideDietSignatureHelp(params, file, document);
130 	else
131 		return SignatureHelp.init;
132 }
133 
134 SignatureHelp provideDSignatureHelp(TextDocumentPositionParams params,
135 		string file, ref Document document)
136 {
137 	if (!backend.hasBest!DCDComponent(file) || !backend.hasBest!DCDExtComponent(file))
138 		return SignatureHelp.init;
139 
140 	auto currOffset = cast(int) document.positionToBytes(params.position);
141 
142 	scope codeText = document.rawText.idup;
143 
144 	DCDExtComponent dcdext = backend.best!DCDExtComponent(file);
145 	auto callParams = dcdext.extractCallParameters(codeText, cast(int) currOffset);
146 	if (callParams == CalltipsSupport.init)
147 		return SignatureHelp.init;
148 
149 	DCDCompletions result = backend.best!DCDComponent(file)
150 		.listCompletion(codeText, callParams.functionParensRange[0] + 1).getYield;
151 
152 	if (result == DCDCompletions.init)
153 		return SignatureHelp.init;
154 
155 	switch (result.type)
156 	{
157 	case DCDCompletions.Type.calltips:
158 		return convertDCDCalltips(dcdext,
159 				result.calltips, result.symbols, callParams);
160 	default:
161 		trace("Unexpected result from DCD: ", result);
162 		goto case;
163 	case DCDCompletions.Type.identifiers:
164 		return SignatureHelp.init;
165 	}
166 }
167 
168 SignatureHelp provideDietSignatureHelp(TextDocumentPositionParams params,
169 		string file, ref Document document)
170 {
171 	import served.utils.diet;
172 	import dc = dietc.complete;
173 
174 	auto completion = updateDietFile(file, document.rawText.idup);
175 
176 	size_t offset = document.positionToBytes(params.position);
177 	auto raw = completion.completeAt(offset);
178 	CompletionItem[] ret;
179 
180 	if (raw is dc.Completion.completeD)
181 	{
182 		string code;
183 		dc.extractD(completion, offset, code, offset);
184 		if (offset <= code.length && backend.hasBest!DCDComponent(file)
185 				&& backend.hasBest!DCDExtComponent(file))
186 		{
187 			auto dcdext = backend.best!DCDExtComponent(file);
188 
189 			auto callParams = dcdext.extractCallParameters(code, cast(int) offset);
190 			if (callParams == CalltipsSupport.init)
191 				return SignatureHelp.init;
192 
193 			auto dcd = backend.best!DCDComponent(file).listCompletion(code,
194 					callParams.functionParensRange[0] + 1).getYield;
195 			if (dcd.type == DCDCompletions.Type.calltips)
196 				return convertDCDCalltips(dcdext, dcd.calltips, dcd.symbols, callParams);
197 		}
198 	}
199 	return SignatureHelp.init;
200 }