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 				default:
108 					throw new Exception("Invalid command-line switch");
109 				}
110 			}
111 
112 			arguments = "dfmt" ~ arguments;
113 
114 			// this too keep up-to-date
115 			// everything except "version", "config", "help", "inplace" arguments
116 
117 			//dfmt off
118 			getopt(arguments,
119 				"align_switch_statements", &handleBooleans,
120 				"brace_style", &config.dfmt_brace_style,
121 				"end_of_line", &config.end_of_line,
122 				"indent_size", &config.indent_size,
123 				"indent_style|t", &config.indent_style,
124 				"max_line_length", &config.max_line_length,
125 				"soft_max_line_length", &config.dfmt_soft_max_line_length,
126 				"outdent_attributes", &handleBooleans,
127 				"space_after_cast", &handleBooleans,
128 				"selective_import_space", &handleBooleans,
129 				"space_before_function_parameters", &handleBooleans,
130 				"split_operator_at_line_end", &handleBooleans,
131 				"compact_labeled_statements", &handleBooleans,
132 				"single_template_constraint_indent", &handleBooleans,
133 				"space_before_aa_colon", &handleBooleans,
134 				"tab_width", &config.tab_width,
135 				"template_constraint_style", &config.dfmt_template_constraint_style,
136 				"keep_line_breaks", &handleBooleans
137 			);
138 			//dfmt on
139 		}
140 		auto output = appender!string;
141 		fmt("stdin", cast(ubyte[]) code, output, &config);
142 		if (output.data.length)
143 			return output.data;
144 		else
145 			return code.idup;
146 	}
147 
148 	/// Finds dfmt instruction comments (dfmt off, dfmt on)
149 	/// Returns: a list of dfmt instructions, sorted in appearing (source code)
150 	/// order
151 	DfmtInstruction[] findDfmtInstructions(scope const(char)[] code)
152 	{
153 		LexerConfig config;
154 		config.whitespaceBehavior = WhitespaceBehavior.skip;
155 		config.commentBehavior = CommentBehavior.noIntern;
156 		auto lexer = DLexer(code, config, &workspaced.stringCache);
157 		auto ret = appender!(DfmtInstruction[]);
158 		Search: foreach (token; lexer)
159 		{
160 			if (token.type == tok!"comment")
161 			{
162 				auto text = dfmtCommentText(token.text);
163 				DfmtInstruction instruction;
164 				switch (text)
165 				{
166 				case "dfmt on":
167 					instruction.type = DfmtInstruction.Type.dfmtOn;
168 					break;
169 				case "dfmt off":
170 					instruction.type = DfmtInstruction.Type.dfmtOff;
171 					break;
172 				default:
173 					text = text.chompPrefix("/").strip; // make doc comments (///) appear as unknown because only first 2 // are stripped.
174 					if (text.startsWith("dfmt", "dmft", "dftm")) // include some typos
175 					{
176 						instruction.type = DfmtInstruction.Type.unknown;
177 						break;
178 					}
179 					continue Search;
180 				}
181 				instruction.index = token.index;
182 				instruction.line = token.line;
183 				instruction.column = token.column;
184 				instruction.length = token.text.length;
185 				ret.put(instruction);
186 			}
187 			else if (token.type == tok!"__EOF__")
188 				break;
189 		}
190 		return ret.data;
191 	}
192 }
193 
194 ///
195 struct DfmtInstruction
196 {
197 	/// Known instruction types
198 	enum Type
199 	{
200 		/// Instruction to turn off formatting from here
201 		dfmtOff,
202 		/// Instruction to turn on formatting again from here
203 		dfmtOn,
204 		/// Starts with dfmt, but unknown contents
205 		unknown,
206 	}
207 
208 	///
209 	Type type;
210 	/// libdparse Token location (byte based offset)
211 	size_t index;
212 	/// libdparse Token location (byte based, 1-based)
213 	size_t line, column;
214 	/// Comment length in bytes
215 	size_t length;
216 }
217 
218 private:
219 
220 // from dfmt/formatter.d TokenFormatter!T.commentText
221 string dfmtCommentText(string commentText)
222 {
223 	import std..string : strip;
224 
225 	if (commentText[0 .. 2] == "//")
226 		commentText = commentText[2 .. $];
227 	else
228 	{
229 		if (commentText.length > 3)
230 			commentText = commentText[2 .. $ - 2];
231 		else
232 			commentText = commentText[2 .. $];
233 	}
234 	return commentText.strip();
235 }
236 
237 void tryFetchProperty(T = string)(ref JSONValue json, ref T ret, string name)
238 {
239 	auto ptr = name in json;
240 	if (ptr)
241 	{
242 		auto val = *ptr;
243 		static if (is(T == string) || is(T == enum))
244 		{
245 			if (val.type != JSONType..string)
246 				throw new Exception("dfmt config value '" ~ name ~ "' must be a string");
247 			static if (is(T == enum))
248 				ret = val.str.to!T;
249 			else
250 				ret = val.str;
251 		}
252 		else static if (is(T == uint))
253 		{
254 			if (val.type != JSONType.integer)
255 				throw new Exception("dfmt config value '" ~ name ~ "' must be a number");
256 			if (val.integer < 0)
257 				throw new Exception("dfmt config value '" ~ name ~ "' must be a positive number");
258 			ret = cast(T) val.integer;
259 		}
260 		else static if (is(T == int))
261 		{
262 			if (val.type != JSONType.integer)
263 				throw new Exception("dfmt config value '" ~ name ~ "' must be a number");
264 			ret = cast(T) val.integer;
265 		}
266 		else static if (is(T == OptionalBoolean))
267 		{
268 			if (val.type != JSONType.true_ && val.type != JSONType.false_)
269 				throw new Exception("dfmt config value '" ~ name ~ "' must be a boolean");
270 			ret = val.type == JSONType.true_ ? OptionalBoolean.t : OptionalBoolean.f;
271 		}
272 		else
273 			static assert(false);
274 	}
275 }
276 
277 unittest
278 {
279 	scope backend = new WorkspaceD();
280 	auto workspace = makeTemporaryTestingWorkspace;
281 	auto instance = backend.addInstance(workspace.directory);
282 	backend.register!DfmtComponent;
283 	DfmtComponent dfmt = instance.get!DfmtComponent;
284 
285 	assert(dfmt.findDfmtInstructions("void main() {}").length == 0);
286 	assert(dfmt.findDfmtInstructions("void main() {\n\t// dfmt off\n}") == [
287 		DfmtInstruction(DfmtInstruction.Type.dfmtOff, 15, 2, 2, 11)
288 	]);
289 	assert(dfmt.findDfmtInstructions(`import std.stdio;
290 
291 // dfmt on
292 void main()
293 {
294 	// dfmt off
295 	writeln("hello");
296 	// dmft off
297 	string[string] x = [
298 		"a": "b"
299 	];
300 	// dfmt on
301 }`) == [
302 		DfmtInstruction(DfmtInstruction.Type.dfmtOn, 19, 3, 1, 10),
303 		DfmtInstruction(DfmtInstruction.Type.dfmtOff, 45, 6, 2, 11),
304 		DfmtInstruction(DfmtInstruction.Type.unknown, 77, 8, 2, 11),
305 		DfmtInstruction(DfmtInstruction.Type.dfmtOn, 127, 12, 2, 10),
306 	]);
307 }