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