1 module served.commands.format; 2 3 import served.extension; 4 import served.types; 5 6 import workspaced.api; 7 import workspaced.com.snippets : SnippetLevel; 8 import workspaced.coms; 9 10 import std.conv : to; 11 import std.json; 12 import std.string; 13 14 struct ResolvedFormattingOptions 15 { 16 int tabSize; 17 bool insertSpaces; 18 bool trimTrailingWhitespace; 19 bool insertFinalNewline; 20 bool trimFinalNewlines; 21 22 this(FormattingOptions o) 23 { 24 tabSize = o.tabSize; 25 insertSpaces = o.insertSpaces; 26 trimTrailingWhitespace = o.trimTrailingWhitespace.orDefault; 27 insertFinalNewline = o.insertFinalNewline.orDefault; 28 trimFinalNewlines = o.trimFinalNewlines.orDefault; 29 } 30 } 31 32 shared string gFormattingOptionsApplyOn; 33 shared ResolvedFormattingOptions gFormattingOptions; 34 35 private static immutable string lotsOfSpaces = " "; 36 string indentString(const FormattingOptions options) 37 { 38 if (options.insertSpaces) 39 { 40 // why would you use spaces? 41 if (options.tabSize < lotsOfSpaces.length) 42 return lotsOfSpaces[0 .. options.tabSize]; 43 else 44 { 45 // really?! you just want to see me suffer 46 char[] ret = new char[options.tabSize]; 47 ret[] = ' '; 48 return (() @trusted => cast(string) ret)(); 49 } 50 } 51 else 52 return "\t"; // this is my favorite user 53 } 54 55 string[] generateDfmtArgs(const ref UserConfiguration config, EolType overrideEol) 56 { 57 string[] args; 58 if (config.d.overrideDfmtEditorconfig) 59 { 60 int maxLineLength = 120; 61 int softMaxLineLength = 80; 62 if (config.editor.rulers.length == 1) 63 { 64 softMaxLineLength = maxLineLength = config.editor.rulers[0]; 65 } 66 else if (config.editor.rulers.length >= 2) 67 { 68 maxLineLength = config.editor.rulers[$ - 1]; 69 softMaxLineLength = config.editor.rulers[$ - 2]; 70 } 71 ResolvedFormattingOptions options = gFormattingOptions; 72 //dfmt off 73 args = [ 74 "--align_switch_statements", config.dfmt.alignSwitchStatements.to!string, 75 "--brace_style", config.dfmt.braceStyle, 76 "--end_of_line", overrideEol.to!string, 77 "--indent_size", options.tabSize.to!string, 78 "--indent_style", options.insertSpaces ? "space" : "tab", 79 "--max_line_length", maxLineLength.to!string, 80 "--soft_max_line_length", softMaxLineLength.to!string, 81 "--outdent_attributes", config.dfmt.outdentAttributes.to!string, 82 "--space_after_cast", config.dfmt.spaceAfterCast.to!string, 83 "--split_operator_at_line_end", config.dfmt.splitOperatorAtLineEnd.to!string, 84 "--tab_width", options.tabSize.to!string, 85 "--selective_import_space", config.dfmt.selectiveImportSpace.to!string, 86 "--space_before_function_parameters", config.dfmt.spaceBeforeFunctionParameters.to!string, 87 "--compact_labeled_statements", config.dfmt.compactLabeledStatements.to!string, 88 "--template_constraint_style", config.dfmt.templateConstraintStyle, 89 "--single_template_constraint_indent", config.dfmt.singleTemplateConstraintIndent.to!string, 90 "--space_before_aa_colon", config.dfmt.spaceBeforeAAColon.to!string, 91 "--keep_line_breaks", config.dfmt.keepLineBreaks.to!string, 92 "--single_indent", config.dfmt.singleIndent.to!string, 93 ]; 94 //dfmt on 95 } 96 return args; 97 } 98 99 void tryFindFormattingSettings(UserConfiguration config, Document document) 100 { 101 ResolvedFormattingOptions options; 102 options.tabSize = 4; 103 options.insertSpaces = false; 104 bool hadOneSpace; 105 foreach (line; document.rawText.lineSplitter) 106 { 107 auto whitespace = line[0 .. line.length - line.stripLeft.length]; 108 if (whitespace.startsWith("\t")) 109 { 110 options.insertSpaces = false; 111 } 112 else if (whitespace == " ") 113 { 114 hadOneSpace = true; 115 } 116 else if (whitespace.length >= 2) 117 { 118 options.tabSize = hadOneSpace ? 1 : cast(int) whitespace.length; 119 options.insertSpaces = true; 120 } 121 } 122 123 if (config.editor.tabSize != 0) 124 options.tabSize = config.editor.tabSize; 125 gFormattingOptions = options; 126 } 127 128 @protocolMethod("textDocument/formatting") 129 TextEdit[] provideFormatting(DocumentFormattingParams params) 130 { 131 auto config = workspace(params.textDocument.uri).config; 132 if (!config.d.enableFormatting) 133 return []; 134 auto document = documents[params.textDocument.uri]; 135 if (document.languageId != "d") 136 return []; 137 gFormattingOptionsApplyOn = params.textDocument.uri; 138 gFormattingOptions = ResolvedFormattingOptions(params.options); 139 auto result = backend.get!DfmtComponent.format(document.rawText, 140 generateDfmtArgs(config, document.eolAt(0))).getYield; 141 return diff(document, result); 142 } 143 144 string formatCode(string code, string[] dfmtArgs) 145 { 146 return backend.get!DfmtComponent.format(code, dfmtArgs).getYield; 147 } 148 149 string formatSnippet(string code, string[] dfmtArgs, SnippetLevel level = SnippetLevel.global) 150 { 151 return backend.get!SnippetsComponent.format(code, dfmtArgs, level).getYield; 152 } 153 154 @protocolMethod("textDocument/rangeFormatting") 155 TextEdit[] provideRangeFormatting(DocumentRangeFormattingParams params) 156 { 157 import std.algorithm : filter; 158 import std.array : array; 159 160 return provideFormatting(DocumentFormattingParams(params.textDocument, params 161 .options)) 162 .filter!( 163 (edit) => edit.range.intersects(params.range) 164 ).array; 165 } 166 167 private TextEdit[] diff(Document document, const string after) 168 { 169 import std.ascii : isWhite; 170 import std.utf : decode; 171 172 auto before = document.rawText(); 173 size_t i; 174 size_t j; 175 TextEdit[] result; 176 177 size_t startIndex; 178 size_t stopIndex; 179 string text; 180 181 Position cachePosition; 182 size_t cacheIndex; 183 184 bool pushTextEdit() 185 { 186 if (startIndex != stopIndex || text.length > 0) 187 { 188 auto startPosition = document.movePositionBytes(cachePosition, cacheIndex, startIndex); 189 auto stopPosition = document.movePositionBytes(startPosition, startIndex, stopIndex); 190 cachePosition = stopPosition; 191 cacheIndex = stopIndex; 192 result ~= TextEdit([startPosition, stopPosition], text); 193 return true; 194 } 195 196 return false; 197 } 198 199 while (i < before.length || j < after.length) 200 { 201 auto newI = i; 202 auto newJ = j; 203 dchar beforeChar; 204 dchar afterChar; 205 206 if (newI < before.length) 207 { 208 beforeChar = decode(before, newI); 209 } 210 211 if (newJ < after.length) 212 { 213 afterChar = decode(after, newJ); 214 } 215 216 if (i < before.length && j < after.length && beforeChar == afterChar) 217 { 218 i = newI; 219 j = newJ; 220 221 if (pushTextEdit()) 222 { 223 startIndex = stopIndex; 224 text = ""; 225 } 226 } 227 228 if (startIndex == stopIndex) 229 { 230 startIndex = i; 231 stopIndex = i; 232 } 233 234 auto addition = !isWhite(beforeChar) && isWhite(afterChar); 235 immutable deletion = isWhite(beforeChar) && !isWhite(afterChar); 236 237 if (!addition && !deletion) 238 { 239 addition = before.length - i < after.length - j; 240 } 241 242 if (addition && j < after.length) 243 { 244 text ~= after[j .. newJ]; 245 j = newJ; 246 } 247 else if (i < before.length) 248 { 249 stopIndex = newI; 250 i = newI; 251 } 252 } 253 254 pushTextEdit(); 255 return result; 256 } 257 258 unittest 259 { 260 import std.stdio; 261 262 TextEdit[] test(string from, string after) 263 { 264 // fix assert equals tests on windows with token-strings comparing with regular strings 265 from = from.replace("\r\n", "\n"); 266 after = after.replace("\r\n", "\n"); 267 268 Document d = Document.nullDocument(from); 269 auto ret = diff(d, after); 270 foreach_reverse (patch; ret) 271 d.applyChange(patch.range, patch.newText); 272 assert(d.rawText == after); 273 // writefln("diff[%d]: %s", ret.length, ret); 274 return ret; 275 } 276 277 // text replacement tests just in case some future changes are made this way 278 test("text", "after"); 279 test("completely", "diffrn"); 280 test("complete", "completely"); 281 test("build", "built"); 282 test("test", "tetestst"); 283 test("tetestst", "test"); 284 285 // UTF-32 286 test("// \U0001FA00\nvoid main() {}", "// \U0001FA00\n\nvoid main()\n{\n}"); 287 288 // otherwise dfmt only changes whitespaces 289 assert(test("import std.stdio;\n\nvoid main()\n{\n\twriteln();\n}\n", 290 "\timport std.stdio;\n\n\tvoid main()\n\t{\n\t\twriteln();\n\t}\n") == [ 291 TextEdit([Position(0, 0), Position(0, 0)], "\t"), 292 TextEdit([Position(2, 0), Position(2, 0)], "\t"), 293 TextEdit([Position(3, 0), Position(3, 0)], "\t"), 294 TextEdit([Position(4, 1), Position(4, 1)], "\t"), 295 TextEdit([Position(5, 0), Position(5, 0)], "\t") 296 ]); 297 assert(test( 298 "\timport std.stdio;\n\n\tvoid main()\n\t{\n\t\twriteln();\n\t}\n", 299 "import std.stdio;\n\nvoid main()\n{\n\twriteln();\n}\n") == [ 300 TextEdit( 301 [Position(0, 0), Position(0, 1)], ""), 302 TextEdit([Position(2, 0), Position(2, 1)], ""), 303 TextEdit([Position(3, 0), Position(3, 1)], ""), 304 TextEdit([Position(4, 1), Position(4, 2)], ""), 305 TextEdit([Position(5, 0), Position(5, 1)], "") 306 ]); 307 assert(test("import std.stdio;void main(){writeln();}", 308 "import std.stdio;\n\nvoid main()\n{\n\twriteln();\n}\n") == [ 309 TextEdit( 310 [Position(0, 17), Position(0, 17)], "\n\n"), 311 TextEdit([Position(0, 28), Position(0, 28)], "\n"), 312 TextEdit([Position(0, 29), Position(0, 29)], "\n\t"), 313 TextEdit([Position(0, 39), Position(0, 39)], "\n"), 314 TextEdit([Position(0, 40), Position(0, 40)], "\n") 315 ]); 316 assert(test("", "void foo()\n{\n\tcool();\n}\n") == [ 317 TextEdit([Position(0, 0), Position(0, 0)], "void foo()\n{\n\tcool();\n}\n") 318 ]); 319 assert(test("void foo()\n{\n\tcool();\n}\n", "") == [ 320 TextEdit([Position(0, 0), Position(4, 0)], "") 321 ]); 322 323 assert(test(q{if (x) 324 foo(); 325 else 326 { 327 bar(); 328 }}, q{if (x) { 329 foo(); 330 } else { 331 bar(); 332 }}) == [ 333 TextEdit([Position(0, 6), Position(1, 2)], " {\n "), 334 TextEdit([Position(2, 0), Position(2, 0)], "} "), 335 TextEdit([Position(2, 4), Position(3, 0)], " ") 336 ]); 337 338 assert(test(q{DocumentUri uriFromFile (string file) { 339 import std.uri :encodeComponent; 340 if(! isAbsolute(file)) throw new Exception("Tried to pass relative path '" ~ file ~ "' to uriFromFile"); 341 file = file.buildNormalizedPath.replace("\\", "/"); 342 if (file.length == 0) return ""; 343 if (file[0] != '/') file = '/'~file; // always triple slash at start but never quad slash 344 if (file.length >= 2 && file[0.. 2] == "//")// Shares (\\share\bob) are different somehow 345 file = file[2 .. $]; 346 return "file://"~file.encodeComponent.replace("%2F", "/"); 347 } 348 349 string uriToFile(DocumentUri uri) 350 { 351 import std.uri : decodeComponent; 352 import std.string : startsWith; 353 354 if (uri.startsWith("file://")) 355 { 356 string ret = uri["file://".length .. $].decodeComponent; 357 if (ret.length >= 3 && ret[0] == '/' && ret[2] == ':') 358 return ret[1 .. $].replace("/", "\\"); 359 else if (ret.length >= 1 && ret[0] != '/') 360 return "\\\\" ~ ret.replace("/", "\\"); 361 return ret; 362 } 363 else 364 return null; 365 }}, q{DocumentUri uriFromFile(string file) 366 { 367 import std.uri : encodeComponent; 368 369 if (!isAbsolute(file)) 370 throw new Exception("Tried to pass relative path '" ~ file ~ "' to uriFromFile"); 371 file = file.buildNormalizedPath.replace("\\", "/"); 372 if (file.length == 0) 373 return ""; 374 if (file[0] != '/') 375 file = '/' ~ file; // always triple slash at start but never quad slash 376 if (file.length >= 2 && file[0 .. 2] == "//") // Shares (\\share\bob) are different somehow 377 file = file[2 .. $]; 378 return "file://" ~ file.encodeComponent.replace("%2F", "/"); 379 } 380 381 string uriToFile(DocumentUri uri) 382 { 383 import std.uri : decodeComponent; 384 import std.string : startsWith; 385 386 if (uri.startsWith("file://")) 387 { 388 string ret = uri["file://".length .. $].decodeComponent; 389 if (ret.length >= 3 && ret[0] == '/' && ret[2] == ':') 390 return ret[1 .. $].replace("/", "\\"); 391 else if (ret.length >= 1 && ret[0] != '/') 392 return "\\\\" ~ ret.replace("/", "\\"); 393 return ret; 394 } 395 else 396 return null; 397 }}) == [ 398 TextEdit([Position(0, 12), Position(0, 13)], ""), 399 TextEdit([Position(0, 24), Position(0, 25)], ""), 400 TextEdit([Position(0, 38), Position(0, 39)], "\n"), 401 TextEdit([Position(1, 17), Position(1, 17)], " "), 402 TextEdit([Position(2, 0), Position(2, 0)], "\n"), 403 TextEdit([Position(2, 3), Position(2, 3)], " "), 404 TextEdit([Position(2, 5), Position(2, 6)], ""), 405 TextEdit([Position(2, 23), Position(2, 25)], "\n\t\t"), 406 TextEdit([Position(4, 22), Position(4, 23)], "\n\t\t"), 407 TextEdit([Position(5, 20), Position(5, 21)], "\n\t\t"), 408 TextEdit([Position(5, 31), Position(5, 31)], " "), 409 TextEdit([Position(5, 32), Position(5, 32)], " "), 410 TextEdit([Position(6, 31), Position(6, 31)], " "), 411 TextEdit([Position(6, 45), Position(6, 45)], " "), 412 TextEdit([Position(8, 17), Position(8, 17)], " "), 413 TextEdit([Position(8, 18), Position(8, 18)], " ") 414 ]); 415 }