1 module served.commands.format;
2 
3 import served.extension;
4 import served.types;
5 
6 import workspaced.api;
7 import workspaced.com.snippets : SnippetLevel;
8 import workspaced.coms;
9 
10 import std.conv : to;
11 import std.json;
12 import std.string;
13 
14 shared string gFormattingOptionsApplyOn;
15 shared FormattingOptions gFormattingOptions;
16 
17 private static immutable string lotsOfSpaces = "                        ";
18 string indentString(const FormattingOptions options)
19 {
20 	if (options.insertSpaces)
21 	{
22 		// why would you use spaces?
23 		if (options.tabSize < lotsOfSpaces.length)
24 			return lotsOfSpaces[0 .. options.tabSize];
25 		else
26 		{
27 			// really?! you just want to see me suffer
28 			char[] ret = new char[options.tabSize];
29 			ret[] = ' ';
30 			return (() @trusted => cast(string) ret)();
31 		}
32 	}
33 	else
34 		return "\t"; // this is my favorite user
35 }
36 
37 string[] generateDfmtArgs(const ref UserConfiguration config, EolType overrideEol)
38 {
39 	string[] args;
40 	if (config.d.overrideDfmtEditorconfig)
41 	{
42 		int maxLineLength = 120;
43 		int softMaxLineLength = 80;
44 		if (config.editor.rulers.length == 1)
45 		{
46 			softMaxLineLength = maxLineLength = config.editor.rulers[0];
47 		}
48 		else if (config.editor.rulers.length >= 2)
49 		{
50 			maxLineLength = config.editor.rulers[$ - 1];
51 			softMaxLineLength = config.editor.rulers[$ - 2];
52 		}
53 		FormattingOptions options = gFormattingOptions;
54 		//dfmt off
55 		args = [
56 			"--align_switch_statements", config.dfmt.alignSwitchStatements.to!string,
57 			"--brace_style", config.dfmt.braceStyle,
58 			"--end_of_line", overrideEol.to!string,
59 			"--indent_size", options.tabSize.to!string,
60 			"--indent_style", options.insertSpaces ? "space" : "tab",
61 			"--max_line_length", maxLineLength.to!string,
62 			"--soft_max_line_length", softMaxLineLength.to!string,
63 			"--outdent_attributes", config.dfmt.outdentAttributes.to!string,
64 			"--space_after_cast", config.dfmt.spaceAfterCast.to!string,
65 			"--split_operator_at_line_end", config.dfmt.splitOperatorAtLineEnd.to!string,
66 			"--tab_width", options.tabSize.to!string,
67 			"--selective_import_space", config.dfmt.selectiveImportSpace.to!string,
68 			"--space_before_function_parameters", config.dfmt.spaceBeforeFunctionParameters.to!string,
69 			"--compact_labeled_statements", config.dfmt.compactLabeledStatements.to!string,
70 			"--template_constraint_style", config.dfmt.templateConstraintStyle,
71 			"--single_template_constraint_indent", config.dfmt.singleTemplateConstraintIndent.to!string,
72 			"--space_before_aa_colon", config.dfmt.spaceBeforeAAColon.to!string,
73 			"--keep_line_breaks", config.dfmt.keepLineBreaks.to!string,
74 			"--single_indent", config.dfmt.singleIndent.to!string,
75 		];
76 		//dfmt on
77 	}
78 	return args;
79 }
80 
81 void tryFindFormattingSettings(UserConfiguration config, Document document)
82 {
83 	gFormattingOptions.tabSize = 4;
84 	gFormattingOptions.insertSpaces = false;
85 	bool hadOneSpace;
86 	foreach (line; document.rawText.lineSplitter)
87 	{
88 		auto whitespace = line[0 .. line.length - line.stripLeft.length];
89 		if (whitespace.startsWith("\t"))
90 		{
91 			gFormattingOptions.insertSpaces = false;
92 		}
93 		else if (whitespace == " ")
94 		{
95 			hadOneSpace = true;
96 		}
97 		else if (whitespace.length >= 2)
98 		{
99 			gFormattingOptions.tabSize = hadOneSpace ? 1 : cast(int) whitespace.length;
100 			gFormattingOptions.insertSpaces = true;
101 		}
102 	}
103 
104 	if (config.editor.tabSize != 0)
105 		gFormattingOptions.tabSize = config.editor.tabSize;
106 }
107 
108 @protocolMethod("textDocument/formatting")
109 TextEdit[] provideFormatting(DocumentFormattingParams params)
110 {
111 	auto config = workspace(params.textDocument.uri).config;
112 	if (!config.d.enableFormatting)
113 		return [];
114 	auto document = documents[params.textDocument.uri];
115 	if (document.languageId != "d")
116 		return [];
117 	gFormattingOptionsApplyOn = params.textDocument.uri;
118 	gFormattingOptions = params.options;
119 	auto result = backend.get!DfmtComponent.format(document.rawText,
120 			generateDfmtArgs(config, document.eolAt(0))).getYield;
121 	return diff(document, result);
122 }
123 
124 string formatCode(string code, string[] dfmtArgs)
125 {
126 	return backend.get!DfmtComponent.format(code, dfmtArgs).getYield;
127 }
128 
129 string formatSnippet(string code, string[] dfmtArgs, SnippetLevel level = SnippetLevel.global)
130 {
131 	return backend.get!SnippetsComponent.format(code, dfmtArgs, level).getYield;
132 }
133 
134 @protocolMethod("textDocument/rangeFormatting")
135 TextEdit[] provideRangeFormatting(DocumentRangeFormattingParams params)
136 {
137 	import std.algorithm : filter;
138 	import std.array : array;
139 
140 	return provideFormatting(DocumentFormattingParams(params.textDocument, params
141 			.options))
142 		.filter!(
143 				(edit) => edit.range.intersects(params.range)
144 		).array;
145 }
146 
147 private TextEdit[] diff(Document document, const string after)
148 {
149 	import std.ascii : isWhite;
150 	import std.utf : decode;
151 
152 	auto before = document.rawText();
153 	size_t i;
154 	size_t j;
155 	TextEdit[] result;
156 
157 	size_t startIndex;
158 	size_t stopIndex;
159 	string text;
160 
161 	Position cachePosition;
162 	size_t cacheIndex;
163 
164 	bool pushTextEdit()
165 	{
166 		if (startIndex != stopIndex || text.length > 0)
167 		{
168 			auto startPosition = document.movePositionBytes(cachePosition, cacheIndex, startIndex);
169 			auto stopPosition = document.movePositionBytes(startPosition, startIndex, stopIndex);
170 			cachePosition = stopPosition;
171 			cacheIndex = stopIndex;
172 			result ~= TextEdit([startPosition, stopPosition], text);
173 			return true;
174 		}
175 
176 		return false;
177 	}
178 
179 	while (i < before.length || j < after.length)
180 	{
181 		auto newI = i;
182 		auto newJ = j;
183 		dchar beforeChar;
184 		dchar afterChar;
185 
186 		if (newI < before.length)
187 		{
188 			beforeChar = decode(before, newI);
189 		}
190 
191 		if (newJ < after.length)
192 		{
193 			afterChar = decode(after, newJ);
194 		}
195 
196 		if (i < before.length && j < after.length && beforeChar == afterChar)
197 		{
198 			i = newI;
199 			j = newJ;
200 
201 			if (pushTextEdit())
202 			{
203 				startIndex = stopIndex;
204 				text = "";
205 			}
206 		}
207 
208 		if (startIndex == stopIndex)
209 		{
210 			startIndex = i;
211 			stopIndex = i;
212 		}
213 
214 		auto addition = !isWhite(beforeChar) && isWhite(afterChar);
215 		immutable deletion = isWhite(beforeChar) && !isWhite(afterChar);
216 
217 		if (!addition && !deletion)
218 		{
219 			addition = before.length - i < after.length - j;
220 		}
221 
222 		if (addition && j < after.length)
223 		{
224 			text ~= after[j .. newJ];
225 			j = newJ;
226 		}
227 		else if (i < before.length)
228 		{
229 			stopIndex = newI;
230 			i = newI;
231 		}
232 	}
233 
234 	pushTextEdit();
235 	return result;
236 }
237 
238 unittest
239 {
240 	import std.stdio;
241 
242 	TextEdit[] test(string from, string after)
243 	{
244 		// fix assert equals tests on windows with token-strings comparing with regular strings
245 		from = from.replace("\r\n", "\n");
246 		after = after.replace("\r\n", "\n");
247 
248 		Document d = Document.nullDocument(from);
249 		auto ret = diff(d, after);
250 		foreach_reverse (patch; ret)
251 			d.applyChange(patch.range, patch.newText);
252 		assert(d.rawText == after);
253 		// writefln("diff[%d]: %s", ret.length, ret);
254 		return ret;
255 	}
256 
257 	// text replacement tests just in case some future changes are made this way
258 	test("text", "after");
259 	test("completely", "diffrn");
260 	test("complete", "completely");
261 	test("build", "built");
262 	test("test", "tetestst");
263 	test("tetestst", "test");
264 
265 	// UTF-32
266 	test("// \U0001FA00\nvoid main() {}", "// \U0001FA00\n\nvoid main()\n{\n}");
267 
268 	// otherwise dfmt only changes whitespaces
269 	assert(test("import std.stdio;\n\nvoid main()\n{\n\twriteln();\n}\n",
270 			"\timport std.stdio;\n\n\tvoid main()\n\t{\n\t\twriteln();\n\t}\n") == [
271 			TextEdit([Position(0, 0), Position(0, 0)], "\t"),
272 			TextEdit([Position(2, 0), Position(2, 0)], "\t"),
273 			TextEdit([Position(3, 0), Position(3, 0)], "\t"),
274 			TextEdit([Position(4, 1), Position(4, 1)], "\t"),
275 			TextEdit([Position(5, 0), Position(5, 0)], "\t")
276 			]);
277 	assert(test(
278 			"\timport std.stdio;\n\n\tvoid main()\n\t{\n\t\twriteln();\n\t}\n",
279 			"import std.stdio;\n\nvoid main()\n{\n\twriteln();\n}\n") == [
280 			TextEdit(
281 				[Position(0, 0), Position(0, 1)], ""),
282 			TextEdit([Position(2, 0), Position(2, 1)], ""),
283 			TextEdit([Position(3, 0), Position(3, 1)], ""),
284 			TextEdit([Position(4, 1), Position(4, 2)], ""),
285 			TextEdit([Position(5, 0), Position(5, 1)], "")
286 			]);
287 	assert(test("import std.stdio;void main(){writeln();}",
288 			"import std.stdio;\n\nvoid main()\n{\n\twriteln();\n}\n") == [
289 			TextEdit(
290 				[Position(0, 17), Position(0, 17)], "\n\n"),
291 			TextEdit([Position(0, 28), Position(0, 28)], "\n"),
292 			TextEdit([Position(0, 29), Position(0, 29)], "\n\t"),
293 			TextEdit([Position(0, 39), Position(0, 39)], "\n"),
294 			TextEdit([Position(0, 40), Position(0, 40)], "\n")
295 			]);
296 	assert(test("", "void foo()\n{\n\tcool();\n}\n") == [
297 			TextEdit([Position(0, 0), Position(0, 0)], "void foo()\n{\n\tcool();\n}\n")
298 			]);
299 	assert(test("void foo()\n{\n\tcool();\n}\n", "") == [
300 			TextEdit([Position(0, 0), Position(4, 0)], "")
301 			]);
302 
303 	assert(test(q{if (x)
304   foo();
305 else
306 {
307   bar();
308 }}, q{if (x) {
309   foo();
310 } else {
311   bar();
312 }}) == [
313 			TextEdit([Position(0, 6), Position(1, 2)], " {\n  "),
314 			TextEdit([Position(2, 0), Position(2, 0)], "} "),
315 			TextEdit([Position(2, 4), Position(3, 0)], " ")
316 			]);
317 
318 	assert(test(q{DocumentUri  uriFromFile (string file) {
319 	import std.uri :encodeComponent;
320 	if(! isAbsolute(file))  throw new Exception("Tried to pass relative path '" ~ file ~ "' to uriFromFile");
321 	file = file.buildNormalizedPath.replace("\\", "/");
322 	if (file.length == 0) return "";
323 	if (file[0] != '/') file = '/'~file; // always triple slash at start but never quad slash
324 	if (file.length >= 2 && file[0.. 2] == "//")// Shares (\\share\bob) are different somehow
325 		file = file[2 .. $];
326 	return "file://"~file.encodeComponent.replace("%2F", "/");
327 }
328 
329 string uriToFile(DocumentUri uri)
330 {
331 	import std.uri : decodeComponent;
332 	import std.string : startsWith;
333 
334 	if (uri.startsWith("file://"))
335 	{
336 		string ret = uri["file://".length .. $].decodeComponent;
337 		if (ret.length >= 3 && ret[0] == '/' && ret[2] == ':')
338 			return ret[1 .. $].replace("/", "\\");
339 		else if (ret.length >= 1 && ret[0] != '/')
340 			return "\\\\" ~ ret.replace("/", "\\");
341 		return ret;
342 	}
343 	else
344 		return null;
345 }}, q{DocumentUri uriFromFile(string file)
346 {
347 	import std.uri : encodeComponent;
348 
349 	if (!isAbsolute(file))
350 		throw new Exception("Tried to pass relative path '" ~ file ~ "' to uriFromFile");
351 	file = file.buildNormalizedPath.replace("\\", "/");
352 	if (file.length == 0)
353 		return "";
354 	if (file[0] != '/')
355 		file = '/' ~ file; // always triple slash at start but never quad slash
356 	if (file.length >= 2 && file[0 .. 2] == "//") // Shares (\\share\bob) are different somehow
357 		file = file[2 .. $];
358 	return "file://" ~ file.encodeComponent.replace("%2F", "/");
359 }
360 
361 string uriToFile(DocumentUri uri)
362 {
363 	import std.uri : decodeComponent;
364 	import std.string : startsWith;
365 
366 	if (uri.startsWith("file://"))
367 	{
368 		string ret = uri["file://".length .. $].decodeComponent;
369 		if (ret.length >= 3 && ret[0] == '/' && ret[2] == ':')
370 			return ret[1 .. $].replace("/", "\\");
371 		else if (ret.length >= 1 && ret[0] != '/')
372 			return "\\\\" ~ ret.replace("/", "\\");
373 		return ret;
374 	}
375 	else
376 		return null;
377 }}) == [
378 	TextEdit([Position(0, 12), Position(0, 13)], ""),
379 	TextEdit([Position(0, 24), Position(0, 25)], ""),
380 	TextEdit([Position(0, 38), Position(0, 39)], "\n"),
381 	TextEdit([Position(1, 17), Position(1, 17)], " "),
382 	TextEdit([Position(2, 0), Position(2, 0)], "\n"),
383 	TextEdit([Position(2, 3), Position(2, 3)], " "),
384 	TextEdit([Position(2, 5), Position(2, 6)], ""),
385 	TextEdit([Position(2, 23), Position(2, 25)], "\n\t\t"),
386 	TextEdit([Position(4, 22), Position(4, 23)], "\n\t\t"),
387 	TextEdit([Position(5, 20), Position(5, 21)], "\n\t\t"),
388 	TextEdit([Position(5, 31), Position(5, 31)], " "),
389 	TextEdit([Position(5, 32), Position(5, 32)], " "),
390 	TextEdit([Position(6, 31), Position(6, 31)], " "),
391 	TextEdit([Position(6, 45), Position(6, 45)], " "),
392 	TextEdit([Position(8, 17), Position(8, 17)], " "),
393 	TextEdit([Position(8, 18), Position(8, 18)], " ")
394 ]);
395 }