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 workspaced.api; 21 22 @component("dfmt") 23 class DfmtComponent : ComponentWrapper 24 { 25 mixin DefaultComponentWrapper; 26 27 /// Will format the code passed in asynchronously. 28 /// Returns: the formatted code as string 29 Future!string format(scope const(char)[] code, string[] arguments = []) 30 { 31 mixin(gthreadsAsyncProxy!`formatSync(code, arguments)`); 32 } 33 34 /// Will format the code passed in synchronously. Might take a short moment on larger documents. 35 /// Returns: the formatted code as string 36 string formatSync(scope const(char)[] code, string[] arguments = []) 37 { 38 Config config; 39 config.initializeWithDefaults(); 40 string configPath; 41 if (getConfigPath("dfmt.json", configPath)) 42 { 43 stderr.writeln("Overriding dfmt arguments with workspace-d dfmt.json config file"); 44 try 45 { 46 auto json = parseJSON(fs.readText(configPath)); 47 foreach (i, ref member; config.tupleof) 48 { 49 enum name = __traits(identifier, config.tupleof[i]); 50 if (name.startsWith("dfmt_")) 51 json.tryFetchProperty(member, name["dfmt_".length .. $]); 52 else 53 json.tryFetchProperty(member, name); 54 } 55 } 56 catch (Exception e) 57 { 58 stderr.writeln("dfmt.json in workspace-d config folder is malformed"); 59 stderr.writeln(e); 60 } 61 } 62 else if (arguments.length) 63 { 64 // code for parsing args from dfmt main.d (keep up-to-date!) 65 // https://github.com/dlang-community/dfmt/blob/master/src/dfmt/main.d 66 void handleBooleans(string option, string value) 67 { 68 import dfmt.editorconfig : OptionalBoolean; 69 import std.exception : enforce; 70 71 enforce!GetOptException(value == "true" || value == "false", "Invalid argument"); 72 immutable OptionalBoolean val = value == "true" ? OptionalBoolean.t : OptionalBoolean.f; 73 switch (option) 74 { 75 case "align_switch_statements": 76 config.dfmt_align_switch_statements = val; 77 break; 78 case "outdent_attributes": 79 config.dfmt_outdent_attributes = val; 80 break; 81 case "space_after_cast": 82 config.dfmt_space_after_cast = val; 83 break; 84 case "space_before_function_parameters": 85 config.dfmt_space_before_function_parameters = val; 86 break; 87 case "split_operator_at_line_end": 88 config.dfmt_split_operator_at_line_end = val; 89 break; 90 case "selective_import_space": 91 config.dfmt_selective_import_space = val; 92 break; 93 case "compact_labeled_statements": 94 config.dfmt_compact_labeled_statements = val; 95 break; 96 case "single_template_constraint_indent": 97 config.dfmt_single_template_constraint_indent = val; 98 break; 99 case "space_before_aa_colon": 100 config.dfmt_space_before_aa_colon = val; 101 break; 102 case "keep_line_breaks": 103 config.dfmt_keep_line_breaks = val; 104 break; 105 case "single_indent": 106 config.dfmt_single_indent = val; 107 break; 108 default: 109 throw new Exception("Invalid command-line switch"); 110 } 111 } 112 113 arguments = "dfmt" ~ arguments; 114 115 // this too keep up-to-date 116 // everything except "version", "config", "help", "inplace" arguments 117 118 //dfmt off 119 getopt(arguments, 120 "align_switch_statements", &handleBooleans, 121 "brace_style", &config.dfmt_brace_style, 122 "end_of_line", &config.end_of_line, 123 "indent_size", &config.indent_size, 124 "indent_style|t", &config.indent_style, 125 "max_line_length", &config.max_line_length, 126 "soft_max_line_length", &config.dfmt_soft_max_line_length, 127 "outdent_attributes", &handleBooleans, 128 "space_after_cast", &handleBooleans, 129 "selective_import_space", &handleBooleans, 130 "space_before_function_parameters", &handleBooleans, 131 "split_operator_at_line_end", &handleBooleans, 132 "compact_labeled_statements", &handleBooleans, 133 "single_template_constraint_indent", &handleBooleans, 134 "space_before_aa_colon", &handleBooleans, 135 "tab_width", &config.tab_width, 136 "template_constraint_style", &config.dfmt_template_constraint_style, 137 "keep_line_breaks", &handleBooleans, 138 "single_indent", &handleBooleans, 139 ); 140 //dfmt on 141 } 142 auto output = appender!string; 143 fmt("stdin", cast(ubyte[]) code, output, &config); 144 if (output.data.length) 145 return output.data; 146 else 147 return code.idup; 148 } 149 150 /// Finds dfmt instruction comments (dfmt off, dfmt on) 151 /// Returns: a list of dfmt instructions, sorted in appearing (source code) 152 /// order 153 DfmtInstruction[] findDfmtInstructions(scope const(char)[] code) 154 { 155 LexerConfig config; 156 config.whitespaceBehavior = WhitespaceBehavior.skip; 157 config.commentBehavior = CommentBehavior.noIntern; 158 auto lexer = DLexer(code, config, &workspaced.stringCache); 159 auto ret = appender!(DfmtInstruction[]); 160 Search: foreach (token; lexer) 161 { 162 if (token.type == tok!"comment") 163 { 164 auto text = dfmtCommentText(token.text); 165 DfmtInstruction instruction; 166 switch (text) 167 { 168 case "dfmt on": 169 instruction.type = DfmtInstruction.Type.dfmtOn; 170 break; 171 case "dfmt off": 172 instruction.type = DfmtInstruction.Type.dfmtOff; 173 break; 174 default: 175 text = text.chompPrefix("/").strip; // make doc comments (///) appear as unknown because only first 2 // are stripped. 176 if (text.startsWith("dfmt", "dmft", "dftm")) // include some typos 177 { 178 instruction.type = DfmtInstruction.Type.unknown; 179 break; 180 } 181 continue Search; 182 } 183 instruction.index = token.index; 184 instruction.line = token.line; 185 instruction.column = token.column; 186 instruction.length = token.text.length; 187 ret.put(instruction); 188 } 189 else if (token.type == tok!"__EOF__") 190 break; 191 } 192 return ret.data; 193 } 194 } 195 196 /// 197 struct DfmtInstruction 198 { 199 /// Known instruction types 200 enum Type 201 { 202 /// Instruction to turn off formatting from here 203 dfmtOff, 204 /// Instruction to turn on formatting again from here 205 dfmtOn, 206 /// Starts with dfmt, but unknown contents 207 unknown, 208 } 209 210 /// 211 Type type; 212 /// libdparse Token location (byte based offset) 213 size_t index; 214 /// libdparse Token location (byte based, 1-based) 215 size_t line, column; 216 /// Comment length in bytes 217 size_t length; 218 } 219 220 private: 221 222 // from dfmt/formatter.d TokenFormatter!T.commentText 223 string dfmtCommentText(string commentText) 224 { 225 import std.string : strip; 226 227 if (commentText[0 .. 2] == "//") 228 commentText = commentText[2 .. $]; 229 else 230 { 231 if (commentText.length > 3) 232 commentText = commentText[2 .. $ - 2]; 233 else 234 commentText = commentText[2 .. $]; 235 } 236 return commentText.strip(); 237 } 238 239 void tryFetchProperty(T = string)(ref JSONValue json, ref T ret, string name) 240 { 241 auto ptr = name in json; 242 if (ptr) 243 { 244 auto val = *ptr; 245 static if (is(T == string) || is(T == enum)) 246 { 247 if (val.type != JSONType..string) 248 throw new Exception("dfmt config value '" ~ name ~ "' must be a string"); 249 static if (is(T == enum)) 250 ret = val.str.to!T; 251 else 252 ret = val.str; 253 } 254 else static if (is(T == uint)) 255 { 256 if (val.type != JSONType.integer) 257 throw new Exception("dfmt config value '" ~ name ~ "' must be a number"); 258 if (val.integer < 0) 259 throw new Exception("dfmt config value '" ~ name ~ "' must be a positive number"); 260 ret = cast(T) val.integer; 261 } 262 else static if (is(T == int)) 263 { 264 if (val.type != JSONType.integer) 265 throw new Exception("dfmt config value '" ~ name ~ "' must be a number"); 266 ret = cast(T) val.integer; 267 } 268 else static if (is(T == OptionalBoolean)) 269 { 270 if (val.type != JSONType.true_ && val.type != JSONType.false_) 271 throw new Exception("dfmt config value '" ~ name ~ "' must be a boolean"); 272 ret = val.type == JSONType.true_ ? OptionalBoolean.t : OptionalBoolean.f; 273 } 274 else 275 static assert(false); 276 } 277 } 278 279 unittest 280 { 281 scope backend = new WorkspaceD(); 282 auto workspace = makeTemporaryTestingWorkspace; 283 auto instance = backend.addInstance(workspace.directory); 284 backend.register!DfmtComponent; 285 DfmtComponent dfmt = instance.get!DfmtComponent; 286 287 assert(dfmt.findDfmtInstructions("void main() {}").length == 0); 288 assert(dfmt.findDfmtInstructions("void main() {\n\t// dfmt off\n}") == [ 289 DfmtInstruction(DfmtInstruction.Type.dfmtOff, 15, 2, 2, 11) 290 ]); 291 assert(dfmt.findDfmtInstructions(`import std.stdio; 292 293 // dfmt on 294 void main() 295 { 296 // dfmt off 297 writeln("hello"); 298 // dmft off 299 string[string] x = [ 300 "a": "b" 301 ]; 302 // dfmt on 303 }`.normLF) == [ 304 DfmtInstruction(DfmtInstruction.Type.dfmtOn, 19, 3, 1, 10), 305 DfmtInstruction(DfmtInstruction.Type.dfmtOff, 45, 6, 2, 11), 306 DfmtInstruction(DfmtInstruction.Type.unknown, 77, 8, 2, 11), 307 DfmtInstruction(DfmtInstruction.Type.dfmtOn, 127, 12, 2, 10), 308 ]); 309 }