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 }