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