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