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 				JSONValue paramLabel;
65 				if (capabilities.textDocument.signatureHelp.supportsLabelOffset)
66 					paramLabel = JSONValue([
67 							JSONValue(cast(int) calltip[0 .. param.contentRange[0]].countUTF16Length),
68 							JSONValue(cast(int) calltip[0 .. param.contentRange[1]].countUTF16Length)
69 							]);
70 				else
71 					paramLabel = JSONValue(calltip[param.contentRange[0] .. param.contentRange[1]]);
72 				Optional!MarkupContent paramDocs;
73 				if (docs != Comment.init)
74 				{
75 					auto docString = getParamDocumentation(docs, paramName);
76 					if (docString.length)
77 					{
78 						Optional!(string[]) formats = capabilities.textDocument
79 							.signatureHelp.signatureInformation.documentationFormat;
80 						MarkupKind kind = formats.isNull || formats.length == 0
81 							? MarkupKind.markdown : cast(MarkupKind) formats[0];
82 						string prefix = kind == MarkupKind.markdown ? "**" ~ paramName ~ "**: " : paramName
83 							~ ": ";
84 						string docRet = kind == MarkupKind.markdown
85 							? ddocToMarkdown(docString.strip) : docString.strip;
86 						paramDocs = MarkupContent(kind, (prefix ~ docRet).stripRight).opt;
87 					}
88 				}
89 				retParams ~= ParameterInformation(paramLabel, paramDocs);
90 			}
91 
92 			sig.parameters = retParams.opt;
93 		}
94 
95 		help.signatures ~= sig;
96 	}
97 
98 	int writtenParamsCount = cast(int)(extractedParams.inTemplateParameters
99 			? extractedParams.templateArgs.length : extractedParams.functionArgs.length);
100 
101 	size_t[] possibleFunctions;
102 	foreach (i, count; paramsCounts)
103 		if (count >= writtenParamsCount)
104 			possibleFunctions ~= i;
105 
106 	if (extractedParams.activeParameter != -1)
107 		help.activeParameter = extractedParams.activeParameter.opt;
108 
109 	help.activeSignature = possibleFunctions.length ? cast(int) possibleFunctions[0] : 0;
110 
111 	return help;
112 }
113 
114 @protocolMethod("textDocument/signatureHelp")
115 SignatureHelp provideSignatureHelp(TextDocumentPositionParams params)
116 {
117 	auto document = documents[params.textDocument.uri];
118 	string file = document.uri.uriToFile;
119 	if (document.languageId == "d")
120 		return provideDSignatureHelp(params, file, document);
121 	else if (document.languageId == "diet")
122 		return provideDietSignatureHelp(params, file, document);
123 	else
124 		return SignatureHelp.init;
125 }
126 
127 SignatureHelp provideDSignatureHelp(TextDocumentPositionParams params,
128 		string file, ref Document document)
129 {
130 	if (!backend.hasBest!DCDComponent(file) || !backend.hasBest!DCDExtComponent(file))
131 		return SignatureHelp.init;
132 
133 	auto currOffset = cast(int) document.positionToBytes(params.position);
134 
135 	scope codeText = document.rawText.idup;
136 
137 	DCDExtComponent dcdext = backend.best!DCDExtComponent(file);
138 	auto callParams = dcdext.extractCallParameters(codeText, cast(int) currOffset);
139 	if (callParams == CalltipsSupport.init)
140 		return SignatureHelp.init;
141 
142 	DCDCompletions result = backend.best!DCDComponent(file)
143 		.listCompletion(codeText, callParams.functionParensRange[0] + 1).getYield;
144 
145 	if (result == DCDCompletions.init)
146 		return SignatureHelp.init;
147 
148 	switch (result.type)
149 	{
150 	case DCDCompletions.Type.calltips:
151 		return convertDCDCalltips(dcdext,
152 				result.calltips, result.symbols, callParams);
153 	default:
154 		trace("Unexpected result from DCD: ", result);
155 		goto case;
156 	case DCDCompletions.Type.identifiers:
157 		return SignatureHelp.init;
158 	}
159 }
160 
161 SignatureHelp provideDietSignatureHelp(TextDocumentPositionParams params,
162 		string file, ref Document document)
163 {
164 	import served.utils.diet;
165 	import dc = dietc.complete;
166 
167 	auto completion = updateDietFile(file, document.rawText.idup);
168 
169 	size_t offset = document.positionToBytes(params.position);
170 	auto raw = completion.completeAt(offset);
171 	CompletionItem[] ret;
172 
173 	if (raw is dc.Completion.completeD)
174 	{
175 		string code;
176 		dc.extractD(completion, offset, code, offset);
177 		if (offset <= code.length && backend.hasBest!DCDComponent(file)
178 				&& backend.hasBest!DCDExtComponent(file))
179 		{
180 			auto dcdext = backend.best!DCDExtComponent(file);
181 
182 			auto callParams = dcdext.extractCallParameters(code, cast(int) offset);
183 			if (callParams == CalltipsSupport.init)
184 				return SignatureHelp.init;
185 
186 			auto dcd = backend.best!DCDComponent(file).listCompletion(code,
187 					callParams.functionParensRange[0] + 1).getYield;
188 			if (dcd.type == DCDCompletions.Type.calltips)
189 				return convertDCDCalltips(dcdext, dcd.calltips, dcd.symbols, callParams);
190 		}
191 	}
192 	return SignatureHelp.init;
193 }