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