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 }