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