1 module workspaced.com.dfmt;
2 
3 import std.json;
4 import std.conv;
5 import std.regex;
6 import fs = std.file;
7 import std.stdio : stderr;
8 import std.process;
9 import core.thread;
10 
11 import painlessjson;
12 
13 import workspaced.api;
14 
15 @component("dfmt") :
16 
17 /// Load function for dfmt. Call with `{"cmd": "load", "components": ["dfmt"]}`
18 /// This will store the working directory and executable name for future use.
19 /// Also it checks for the version. All dub methods are used with `"cmd": "dfmt"`
20 @load void start(string dir, string dfmtPath = "dfmt")
21 {
22 	cwd = dir;
23 	execPath = dfmtPath;
24 	auto features = execPath.getVersionAndFixPath;
25 	needsConfigFolder = features.hasConfigFolder;
26 	if (!checkVersion(features, [0, 5, 0]))
27 		broadcast(JSONValue([
28 			"type": JSONValue("outdated"),
29 			"component": JSONValue("dfmt")
30 		]));
31 }
32 
33 enum verRegex = ctRegex!`(\d+)\.(\d+)\.\d+`;
34 bool hasConfigFolder(string ver)
35 {
36 	auto match = ver.matchFirst(verRegex);
37 	assert(match);
38 	int major = match[1].to!int;
39 	int minor = match[2].to!int;
40 	if (major > 0)
41 		return true;
42 	if (major == 0 && minor >= 5)
43 		return true;
44 	return false;
45 }
46 
47 /// Unloads dfmt. Has no purpose right now.
48 @unload void stop()
49 {
50 }
51 
52 /// Will format the code passed in asynchronously.
53 /// Returns: the formatted code as string
54 /// Call_With: `{"cmd": "dfmt"}`
55 @any @async void format(AsyncCallback cb, string code, string[] arguments = [])
56 {
57 	new Thread({
58 		try
59 		{
60 			auto args = [execPath];
61 			string configPath;
62 			if (getConfigPath("dfmt.json", configPath))
63 			{
64 				stderr.writeln("Overriding dfmt arguments with workspace-d dfmt.json config file");
65 				try
66 				{
67 					auto json = parseJSON(fs.readText(configPath));
68 					json.tryFetchProperty!bool(args, "align_switch_statements");
69 					json.tryFetchProperty(args, "brace_style");
70 					json.tryFetchProperty(args, "end_of_line");
71 					json.tryFetchProperty!uint(args, "indent_size");
72 					json.tryFetchProperty(args, "indent_style");
73 					json.tryFetchProperty!uint(args, "max_line_length");
74 					json.tryFetchProperty!uint(args, "soft_max_line_length");
75 					json.tryFetchProperty!bool(args, "outdent_attributes");
76 					json.tryFetchProperty!bool(args, "space_after_cast");
77 					json.tryFetchProperty!bool(args, "split_operator_at_line_end");
78 					json.tryFetchProperty!uint(args, "tab_width");
79 					json.tryFetchProperty!bool(args, "selective_import_space");
80 					json.tryFetchProperty!bool(args, "compact_labeled_statements");
81 					json.tryFetchProperty(args, "template_constraint_style");
82 				}
83 				catch (Exception e)
84 				{
85 					stderr.writeln("dfmt.json in workspace-d config folder is malformed");
86 					stderr.writeln(e);
87 				}
88 			}
89 			else if (arguments.length)
90 				args ~= arguments;
91 			else if (needsConfigFolder)
92 				args ~= ["-c", cwd];
93 			auto pipes = pipeProcess(args, Redirect.all, null, Config.none, cwd);
94 			scope (exit)
95 				pipes.pid.wait();
96 			pipes.stdin.write(code);
97 			pipes.stdin.close();
98 			ubyte[4096] buffer;
99 			ubyte[] data;
100 			size_t len;
101 			do
102 			{
103 				auto appended = pipes.stdout.rawRead(buffer);
104 				len = appended.length;
105 				data ~= appended;
106 			}
107 			while (len == 4096);
108 			if (data.length)
109 				cb(null, JSONValue(cast(string) data));
110 			else
111 				cb(null, JSONValue(code));
112 		}
113 		catch (Throwable e)
114 		{
115 			cb(e, JSONValue(null));
116 		}
117 	}).start();
118 }
119 
120 private __gshared:
121 string cwd, execPath;
122 bool needsConfigFolder = false;
123 
124 void tryFetchProperty(T = string)(ref JSONValue json, ref string[] args, string name)
125 {
126 	auto ptr = name in json;
127 	if (ptr)
128 	{
129 		auto val = *ptr;
130 		static if (is(T == string))
131 		{
132 			if (val.type != JSON_TYPE.STRING)
133 				throw new Exception("dfmt config value '" ~ name ~ "' must be a string");
134 			args ~= ["--" ~ name, val.str];
135 		}
136 		else static if (is(T == uint))
137 		{
138 			if (val.type != JSON_TYPE.INTEGER)
139 				throw new Exception("dfmt config value '" ~ name ~ "' must be a number");
140 			if (val.integer < 0)
141 				throw new Exception("dfmt config value '" ~ name ~ "' must be a positive number");
142 			args ~= ["--" ~ name, val.integer.to!string];
143 		}
144 		else static if (is(T == int))
145 		{
146 			if (val.type != JSON_TYPE.INTEGER)
147 				throw new Exception("dfmt config value '" ~ name ~ "' must be a number");
148 			args ~= ["--" ~ name, val.integer.to!string];
149 		}
150 		else static if (is(T == bool))
151 		{
152 			if (val.type != JSON_TYPE.TRUE && val.type != JSON_TYPE.FALSE)
153 				throw new Exception("dfmt config value '" ~ name ~ "' must be a boolean");
154 			args ~= ["--" ~ name, val.type == JSON_TYPE.TRUE ? "true" : "false"];
155 		}
156 		else
157 			static assert(false);
158 	}
159 }