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 }