1 module workspaced.com.dfmt;
2 
3 import fs = std.file;
4 import std.algorithm;
5 import std.array;
6 import std.conv;
7 import std.getopt;
8 import std.json;
9 import std.stdio : stderr;
10 import std.string;
11 
12 import dfmt.config;
13 import dfmt.editorconfig;
14 import dfmt.formatter : fmt = format;
15 
16 import dparse.lexer;
17 
18 import core.thread;
19 
20 import workspaced.api;
21 
22 @component("dfmt")
23 class DfmtComponent : ComponentWrapper
24 {
25 	mixin DefaultComponentWrapper;
26 
27 	/// Will format the code passed in asynchronously.
28 	/// Returns: the formatted code as string
29 	Future!string format(scope const(char)[] code, string[] arguments = [])
30 	{
31 		mixin(gthreadsAsyncProxy!`formatSync(code, arguments)`);
32 	}
33 
34 	/// Will format the code passed in synchronously. Might take a short moment on larger documents.
35 	/// Returns: the formatted code as string
36 	string formatSync(scope const(char)[] code, string[] arguments = [])
37 	{
38 		Config config;
39 		config.initializeWithDefaults();
40 		string configPath;
41 		if (getConfigPath("dfmt.json", configPath))
42 		{
43 			stderr.writeln("Overriding dfmt arguments with workspace-d dfmt.json config file");
44 			try
45 			{
46 				auto json = parseJSON(fs.readText(configPath));
47 				foreach (i, ref member; config.tupleof)
48 				{
49 					enum name = __traits(identifier, config.tupleof[i]);
50 					if (name.startsWith("dfmt_"))
51 						json.tryFetchProperty(member, name["dfmt_".length .. $]);
52 					else
53 						json.tryFetchProperty(member, name);
54 				}
55 			}
56 			catch (Exception e)
57 			{
58 				stderr.writeln("dfmt.json in workspace-d config folder is malformed");
59 				stderr.writeln(e);
60 			}
61 		}
62 		else if (arguments.length)
63 		{
64 			// code for parsing args from dfmt main.d (keep up-to-date!)
65 			// https://github.com/dlang-community/dfmt/blob/master/src/dfmt/main.d
66 			void handleBooleans(string option, string value)
67 			{
68 				import dfmt.editorconfig : OptionalBoolean;
69 				import std.exception : enforce;
70 
71 				enforce!GetOptException(value == "true" || value == "false", "Invalid argument");
72 				immutable OptionalBoolean val = value == "true" ? OptionalBoolean.t : OptionalBoolean.f;
73 				switch (option)
74 				{
75 				case "align_switch_statements":
76 					config.dfmt_align_switch_statements = val;
77 					break;
78 				case "outdent_attributes":
79 					config.dfmt_outdent_attributes = val;
80 					break;
81 				case "space_after_cast":
82 					config.dfmt_space_after_cast = val;
83 					break;
84 				case "space_before_function_parameters":
85 					config.dfmt_space_before_function_parameters = val;
86 					break;
87 				case "split_operator_at_line_end":
88 					config.dfmt_split_operator_at_line_end = val;
89 					break;
90 				case "selective_import_space":
91 					config.dfmt_selective_import_space = val;
92 					break;
93 				case "compact_labeled_statements":
94 					config.dfmt_compact_labeled_statements = val;
95 					break;
96 				case "single_template_constraint_indent":
97 					config.dfmt_single_template_constraint_indent = val;
98 					break;
99 				case "space_before_aa_colon":
100 					config.dfmt_space_before_aa_colon = val;
101 					break;
102 				case "keep_line_breaks":
103 					config.dfmt_keep_line_breaks = val;
104 					break;
105 				case "single_indent":
106 					config.dfmt_single_indent = val;
107 					break;
108 				default:
109 					throw new Exception("Invalid command-line switch");
110 				}
111 			}
112 
113 			arguments = "dfmt" ~ arguments;
114 
115 			// this too keep up-to-date
116 			// everything except "version", "config", "help", "inplace" arguments
117 
118 			//dfmt off
119 			getopt(arguments,
120 				"align_switch_statements", &handleBooleans,
121 				"brace_style", &config.dfmt_brace_style,
122 				"end_of_line", &config.end_of_line,
123 				"indent_size", &config.indent_size,
124 				"indent_style|t", &config.indent_style,
125 				"max_line_length", &config.max_line_length,
126 				"soft_max_line_length", &config.dfmt_soft_max_line_length,
127 				"outdent_attributes", &handleBooleans,
128 				"space_after_cast", &handleBooleans,
129 				"selective_import_space", &handleBooleans,
130 				"space_before_function_parameters", &handleBooleans,
131 				"split_operator_at_line_end", &handleBooleans,
132 				"compact_labeled_statements", &handleBooleans,
133 				"single_template_constraint_indent", &handleBooleans,
134 				"space_before_aa_colon", &handleBooleans,
135 				"tab_width", &config.tab_width,
136 				"template_constraint_style", &config.dfmt_template_constraint_style,
137 				"keep_line_breaks", &handleBooleans,
138 				"single_indent", &handleBooleans,
139 			);
140 			//dfmt on
141 		}
142 		auto output = appender!string;
143 		fmt("stdin", cast(ubyte[]) code, output, &config);
144 		if (output.data.length)
145 			return output.data;
146 		else
147 			return code.idup;
148 	}
149 
150 	/// Finds dfmt instruction comments (dfmt off, dfmt on)
151 	/// Returns: a list of dfmt instructions, sorted in appearing (source code)
152 	/// order
153 	DfmtInstruction[] findDfmtInstructions(scope const(char)[] code)
154 	{
155 		LexerConfig config;
156 		config.whitespaceBehavior = WhitespaceBehavior.skip;
157 		config.commentBehavior = CommentBehavior.noIntern;
158 		auto lexer = DLexer(code, config, &workspaced.stringCache);
159 		auto ret = appender!(DfmtInstruction[]);
160 		Search: foreach (token; lexer)
161 		{
162 			if (token.type == tok!"comment")
163 			{
164 				auto text = dfmtCommentText(token.text);
165 				DfmtInstruction instruction;
166 				switch (text)
167 				{
168 				case "dfmt on":
169 					instruction.type = DfmtInstruction.Type.dfmtOn;
170 					break;
171 				case "dfmt off":
172 					instruction.type = DfmtInstruction.Type.dfmtOff;
173 					break;
174 				default:
175 					text = text.chompPrefix("/").strip; // make doc comments (///) appear as unknown because only first 2 // are stripped.
176 					if (text.startsWith("dfmt", "dmft", "dftm")) // include some typos
177 					{
178 						instruction.type = DfmtInstruction.Type.unknown;
179 						break;
180 					}
181 					continue Search;
182 				}
183 				instruction.index = token.index;
184 				instruction.line = token.line;
185 				instruction.column = token.column;
186 				instruction.length = token.text.length;
187 				ret.put(instruction);
188 			}
189 			else if (token.type == tok!"__EOF__")
190 				break;
191 		}
192 		return ret.data;
193 	}
194 }
195 
196 ///
197 struct DfmtInstruction
198 {
199 	/// Known instruction types
200 	enum Type
201 	{
202 		/// Instruction to turn off formatting from here
203 		dfmtOff,
204 		/// Instruction to turn on formatting again from here
205 		dfmtOn,
206 		/// Starts with dfmt, but unknown contents
207 		unknown,
208 	}
209 
210 	///
211 	Type type;
212 	/// libdparse Token location (byte based offset)
213 	size_t index;
214 	/// libdparse Token location (byte based, 1-based)
215 	size_t line, column;
216 	/// Comment length in bytes
217 	size_t length;
218 }
219 
220 private:
221 
222 // from dfmt/formatter.d TokenFormatter!T.commentText
223 string dfmtCommentText(string commentText)
224 {
225 	import std.string : strip;
226 
227 	if (commentText[0 .. 2] == "//")
228 		commentText = commentText[2 .. $];
229 	else
230 	{
231 		if (commentText.length > 3)
232 			commentText = commentText[2 .. $ - 2];
233 		else
234 			commentText = commentText[2 .. $];
235 	}
236 	return commentText.strip();
237 }
238 
239 void tryFetchProperty(T = string)(ref JSONValue json, ref T ret, string name)
240 {
241 	auto ptr = name in json;
242 	if (ptr)
243 	{
244 		auto val = *ptr;
245 		static if (is(T == string) || is(T == enum))
246 		{
247 			if (val.type != JSONType..string)
248 				throw new Exception("dfmt config value '" ~ name ~ "' must be a string");
249 			static if (is(T == enum))
250 				ret = val.str.to!T;
251 			else
252 				ret = val.str;
253 		}
254 		else static if (is(T == uint))
255 		{
256 			if (val.type != JSONType.integer)
257 				throw new Exception("dfmt config value '" ~ name ~ "' must be a number");
258 			if (val.integer < 0)
259 				throw new Exception("dfmt config value '" ~ name ~ "' must be a positive number");
260 			ret = cast(T) val.integer;
261 		}
262 		else static if (is(T == int))
263 		{
264 			if (val.type != JSONType.integer)
265 				throw new Exception("dfmt config value '" ~ name ~ "' must be a number");
266 			ret = cast(T) val.integer;
267 		}
268 		else static if (is(T == OptionalBoolean))
269 		{
270 			if (val.type != JSONType.true_ && val.type != JSONType.false_)
271 				throw new Exception("dfmt config value '" ~ name ~ "' must be a boolean");
272 			ret = val.type == JSONType.true_ ? OptionalBoolean.t : OptionalBoolean.f;
273 		}
274 		else
275 			static assert(false);
276 	}
277 }
278 
279 unittest
280 {
281 	scope backend = new WorkspaceD();
282 	auto workspace = makeTemporaryTestingWorkspace;
283 	auto instance = backend.addInstance(workspace.directory);
284 	backend.register!DfmtComponent;
285 	DfmtComponent dfmt = instance.get!DfmtComponent;
286 
287 	assert(dfmt.findDfmtInstructions("void main() {}").length == 0);
288 	assert(dfmt.findDfmtInstructions("void main() {\n\t// dfmt off\n}") == [
289 		DfmtInstruction(DfmtInstruction.Type.dfmtOff, 15, 2, 2, 11)
290 	]);
291 	assert(dfmt.findDfmtInstructions(`import std.stdio;
292 
293 // dfmt on
294 void main()
295 {
296 	// dfmt off
297 	writeln("hello");
298 	// dmft off
299 	string[string] x = [
300 		"a": "b"
301 	];
302 	// dfmt on
303 }`.normLF) == [
304 		DfmtInstruction(DfmtInstruction.Type.dfmtOn, 19, 3, 1, 10),
305 		DfmtInstruction(DfmtInstruction.Type.dfmtOff, 45, 6, 2, 11),
306 		DfmtInstruction(DfmtInstruction.Type.unknown, 77, 8, 2, 11),
307 		DfmtInstruction(DfmtInstruction.Type.dfmtOn, 127, 12, 2, 10),
308 	]);
309 }