1 module served.types; 2 3 public import served.backend.lazy_workspaced : LazyWorkspaceD; 4 public import served.lsp.protocol; 5 public import served.lsp.protoext; 6 public import served.lsp.textdocumentmanager; 7 public import served.lsp.uri; 8 public import served.utils.events; 9 10 static import served.extension; 11 12 import served.serverbase; 13 mixin LanguageServerRouter!(served.extension) lspRouter; 14 15 import core.time : MonoTime; 16 17 import std.algorithm; 18 import std.array; 19 import std.ascii; 20 import std.conv; 21 import std.experimental.logger; 22 import std.json; 23 import std.meta; 24 import std.path; 25 import std.range; 26 import std.string; 27 28 import fs = std.file; 29 import io = std.stdio; 30 31 import workspaced.api; 32 33 deprecated("import stdlib_detect directly") 34 public import served.utils.stdlib_detect : parseDmdConfImports, parseDflagsImports; 35 36 enum IncludedFeatures = ["d", "workspaces"]; 37 38 __gshared MonoTime startupTime; 39 40 alias documents = lspRouter.documents; 41 alias rpc = lspRouter.rpc; 42 43 string[] compare(string prefix, T)(ref T a, ref T b) 44 { 45 auto changed = appender!(string[]); 46 foreach (member; __traits(allMembers, T)) 47 if (__traits(getMember, a, member) != __traits(getMember, b, member)) 48 changed ~= (prefix ~ member); 49 return changed.data; 50 } 51 52 alias configurationTypes = AliasSeq!(Configuration.D, Configuration.DFmt, 53 Configuration.DScanner, Configuration.Editor, Configuration.Git); 54 static immutable string[] configurationSections = [ 55 "d", "dfmt", "dscanner", "editor", "git" 56 ]; 57 58 enum ManyProjectsAction : string 59 { 60 ask = "ask", 61 skip = "skip", 62 load = "load" 63 } 64 65 // alias to avoid name clashing 66 alias UserConfiguration = Configuration; 67 struct Configuration 68 { 69 struct D 70 { 71 JSONValue stdlibPath = JSONValue("auto"); 72 string dcdClientPath = "dcd-client", dcdServerPath = "dcd-server"; 73 string dubPath = "dub"; 74 string dmdPath = "dmd"; 75 bool enableLinting = true; 76 bool enableSDLLinting = true; 77 bool enableStaticLinting = true; 78 bool enableDubLinting = true; 79 bool enableAutoComplete = true; 80 bool enableFormatting = true; 81 bool enableDMDImportTiming = false; 82 bool enableCoverageDecoration = true; 83 bool enableGCProfilerDecorations = true; 84 bool neverUseDub = false; 85 string[] projectImportPaths; 86 string dubConfiguration; 87 string dubArchType; 88 string dubBuildType; 89 string dubCompiler; 90 bool overrideDfmtEditorconfig = true; 91 bool aggressiveUpdate = false; // differs from default code-d settings on purpose! 92 bool argumentSnippets = false; 93 bool scanAllFolders = true; 94 string[] disabledRootGlobs; 95 string[] extraRoots; 96 string manyProjectsAction = ManyProjectsAction.ask; 97 int manyProjectsThreshold = 6; 98 string lintOnFileOpen = "project"; 99 bool dietContextCompletion = false; 100 bool generateModuleNames = true; 101 } 102 103 struct DFmt 104 { 105 bool alignSwitchStatements = true; 106 string braceStyle = "allman"; 107 bool outdentAttributes = true; 108 bool spaceAfterCast = true; 109 bool splitOperatorAtLineEnd = false; 110 bool selectiveImportSpace = true; 111 bool compactLabeledStatements = true; 112 string templateConstraintStyle = "conditional_newline_indent"; 113 bool spaceBeforeFunctionParameters = false; 114 bool singleTemplateConstraintIndent = false; 115 bool spaceBeforeAAColon = false; 116 bool keepLineBreaks = true; 117 bool singleIndent = true; 118 } 119 120 struct DScanner 121 { 122 string[] ignoredKeys; 123 } 124 125 struct Editor 126 { 127 int[] rulers; 128 int tabSize; 129 } 130 131 struct Git 132 { 133 string path = "git"; 134 } 135 136 D d; 137 DFmt dfmt; 138 DScanner dscanner; 139 Editor editor; 140 Git git; 141 142 string[] stdlibPath(string cwd = null) 143 { 144 auto p = d.stdlibPath; 145 if (p.type == JSONType.array) 146 return p.array.map!(a => a.str.userPath).array; 147 else 148 { 149 if (p.type != JSONType..string || p.str == "auto") 150 { 151 import served.utils.stdlib_detect; 152 153 return autoDetectStdlibPaths(cwd, d.dubCompiler); 154 } 155 else 156 return [p.str.userPath]; 157 } 158 } 159 160 string[] replace(Configuration newConfig) 161 { 162 string[] ret; 163 ret ~= replaceSection!"d"(newConfig.d); 164 ret ~= replaceSection!"dfmt"(newConfig.dfmt); 165 ret ~= replaceSection!"dscanner"(newConfig.dscanner); 166 ret ~= replaceSection!"editor"(newConfig.editor); 167 ret ~= replaceSection!"git"(newConfig.git); 168 return ret; 169 } 170 171 string[] replaceSection(string section : "d")(D newD) 172 { 173 auto ret = compare!"d."(d, newD); 174 d = newD; 175 return ret; 176 } 177 178 string[] replaceSection(string section : "dfmt")(DFmt newDfmt) 179 { 180 auto ret = compare!"dfmt."(dfmt, newDfmt); 181 dfmt = newDfmt; 182 return ret; 183 } 184 185 string[] replaceSection(string section : "dscanner")(DScanner newDscanner) 186 { 187 auto ret = compare!"dscanner."(dscanner, newDscanner); 188 dscanner = newDscanner; 189 return ret; 190 } 191 192 string[] replaceSection(string section : "editor")(Editor newEditor) 193 { 194 auto ret = compare!"editor."(editor, newEditor); 195 editor = newEditor; 196 return ret; 197 } 198 199 string[] replaceSection(string section : "git")(Git newGit) 200 { 201 auto ret = compare!"git."(git, newGit); 202 git = newGit; 203 return ret; 204 } 205 206 string[] replaceAllSections(JSONValue[] settings) 207 { 208 import painlessjson : fromJSON; 209 210 assert(settings.length >= configurationSections.length); 211 auto changed = appender!(string[]); 212 static foreach (n, section; configurationSections) 213 changed ~= this.replaceSection!section(settings[n].fromJSON!(configurationTypes[n])); 214 return changed.data; 215 } 216 } 217 218 Configuration parseConfiguration(JSONValue json) 219 { 220 Configuration ret; 221 if (json.type != JSONType.object) 222 { 223 error("Configuration is not an object!"); 224 return ret; 225 } 226 227 foreach (key, value; json.object) 228 { 229 SectionSwitch: 230 switch (key) 231 { 232 static foreach (section; configurationSections) 233 { 234 case section: 235 __traits(getMember, ret, section) = value.parseConfigurationSection!( 236 typeof(__traits(getMember, ret, section)))(key); 237 break SectionSwitch; 238 } 239 default: 240 infof("Ignoring unknown configuration section '%s'", key); 241 break; 242 } 243 } 244 245 return ret; 246 } 247 248 T parseConfigurationSection(T)(JSONValue json, string sectionKey) 249 { 250 import std.traits : FieldNameTuple; 251 import painlessjson : fromJSON; 252 253 T ret; 254 if (json.type != JSONType.object) 255 { 256 error("Configuration is not an object!"); 257 return ret; 258 } 259 260 foreach (key, value; json.object) 261 { 262 ConfigSwitch: 263 switch (key) 264 { 265 static foreach (member; FieldNameTuple!T) 266 { 267 case member: 268 { 269 alias U = typeof(__traits(getMember, ret, member)); 270 try 271 { 272 static if (__traits(compiles, { T t = null; })) 273 { 274 if (value.type == JSONType.null_) 275 { 276 __traits(getMember, ret, member) = null; 277 } 278 else 279 { 280 static if (is(U : string)) 281 __traits(getMember, ret, member) = cast(U) value.str; 282 else 283 __traits(getMember, ret, member) = value.fromJSON!U; 284 } 285 } 286 else 287 { 288 if (value.type == JSONType.null_) 289 { 290 // ignore null value on non-nullable 291 } 292 else 293 { 294 static if (is(U : string)) 295 __traits(getMember, ret, member) = cast(U) value.str; 296 else 297 __traits(getMember, ret, member) = value.fromJSON!U; 298 } 299 } 300 } 301 catch (Exception e) 302 { 303 errorf("Skipping unparsable configuration '%s.%s' which was expected to be of type %s parsed from %s: %s", 304 sectionKey, key, U.stringof, value.type, e.msg); 305 } 306 break ConfigSwitch; 307 } 308 } 309 default: 310 warningf("Ignoring unknown configuration section '%s.%s'", sectionKey, key); 311 break; 312 } 313 } 314 315 return ret; 316 } 317 318 struct Workspace 319 { 320 WorkspaceFolder folder; 321 Configuration config; 322 bool initialized, disabled; 323 string[string] startupErrorNotifications; 324 bool selected; 325 326 void startupError(string folder, string error) 327 { 328 if (folder !in startupErrorNotifications) 329 startupErrorNotifications[folder] = ""; 330 string errors = startupErrorNotifications[folder]; 331 if (errors.length) 332 { 333 if (errors.endsWith(".", "\n\n")) 334 startupErrorNotifications[folder] ~= " " ~ error; 335 else if (errors.endsWith(". ")) 336 startupErrorNotifications[folder] ~= error; 337 else 338 startupErrorNotifications[folder] ~= "\n\n" ~ error; 339 } 340 else 341 startupErrorNotifications[folder] = error; 342 } 343 344 string[] stdlibPath() 345 { 346 return config.stdlibPath(folder.uri.uriToFile); 347 } 348 349 auto describeState() const @property 350 { 351 static struct WorkspaceState 352 { 353 string uri, name; 354 bool initialized; 355 bool selected; 356 } 357 358 WorkspaceState state; 359 state.uri = folder.uri; 360 state.name = folder.name; 361 state.initialized = initialized; 362 state.selected = selected; 363 return state; 364 } 365 } 366 367 deprecated string workspaceRoot() @property 368 { 369 return firstWorkspaceRootUri.uriToFile; 370 } 371 372 string selectedWorkspaceUri() @property 373 { 374 foreach (ref workspace; workspaces) 375 if (workspace.selected) 376 return workspace.folder.uri; 377 return firstWorkspaceRootUri; 378 } 379 380 string selectedWorkspaceRoot() @property 381 { 382 return selectedWorkspaceUri.uriToFile; 383 } 384 385 string firstWorkspaceRootUri() @property 386 { 387 return workspaces.length ? workspaces[0].folder.uri : ""; 388 } 389 390 Workspace fallbackWorkspace; 391 Workspace[] workspaces; 392 ClientCapabilities capabilities; 393 394 size_t workspaceIndex(string uri) 395 { 396 if (!uri.startsWith("file://")) 397 throw new Exception("Passed a non file:// uri to workspace(uri): '" ~ uri ~ "'"); 398 size_t best = size_t.max; 399 size_t bestLength = 0; 400 foreach (i, ref workspace; workspaces) 401 { 402 if (workspace.folder.uri.length > bestLength 403 && uri.startsWith(workspace.folder.uri) && !workspace.disabled) 404 { 405 best = i; 406 bestLength = workspace.folder.uri.length; 407 if (uri.length == workspace.folder.uri.length) // startsWith + same length => same string 408 return i; 409 } 410 } 411 return best; 412 } 413 414 ref Workspace handleThings(return ref Workspace workspace, string uri, bool userExecuted, 415 string file = __FILE__, size_t line = __LINE__) 416 { 417 if (userExecuted) 418 { 419 string f = uri.uriToFile; 420 foreach (key, error; workspace.startupErrorNotifications) 421 { 422 if (f.startsWith(key)) 423 { 424 //dfmt off 425 debug 426 rpc.window.showErrorMessage( 427 error ~ "\n\nFile: " ~ file ~ ":" ~ line.to!string); 428 else 429 rpc.window.showErrorMessage(error); 430 //dfmt on 431 workspace.startupErrorNotifications.remove(key); 432 } 433 } 434 435 bool notifyChange, changedOne; 436 foreach (ref w; workspaces) 437 { 438 if (w.selected) 439 { 440 if (w.folder.uri != workspace.folder.uri) 441 notifyChange = true; 442 changedOne = true; 443 w.selected = false; 444 } 445 } 446 workspace.selected = true; 447 if (notifyChange || !changedOne) 448 rpc.notifyMethod("coded/changedSelectedWorkspace", workspace.describeState); 449 } 450 return workspace; 451 } 452 453 ref Workspace workspace(string uri, bool userExecuted = true, 454 string file = __FILE__, size_t line = __LINE__) 455 { 456 if (!uri.length) 457 return fallbackWorkspace; 458 459 auto best = workspaceIndex(uri); 460 if (best == size_t.max) 461 return bestWorkspaceByDependency(uri).handleThings(uri, userExecuted, file, line); 462 return workspaces[best].handleThings(uri, userExecuted, file, line); 463 } 464 465 ref Workspace bestWorkspaceByDependency(string uri) 466 { 467 size_t best = size_t.max; 468 size_t bestLength; 469 foreach (i, ref workspace; workspaces) 470 { 471 auto inst = backend.getInstance(workspace.folder.uri.uriToFile); 472 if (!inst) 473 continue; 474 foreach (folder; chain(inst.importPaths, inst.importFiles, inst.stringImportPaths)) 475 { 476 string folderUri = folder.uriFromFile; 477 if (folderUri.length > bestLength && uri.startsWith(folderUri)) 478 { 479 best = i; 480 bestLength = folderUri.length; 481 if (uri.length == folderUri.length) // startsWith + same length => same string 482 return workspace; 483 } 484 } 485 } 486 if (best == size_t.max) 487 return fallbackWorkspace; 488 return workspaces[best]; 489 } 490 491 ref Workspace selectedWorkspace() 492 { 493 foreach (ref workspace; workspaces) 494 if (workspace.selected) 495 return workspace; 496 return fallbackWorkspace; 497 } 498 499 WorkspaceD.Instance _activeInstance; 500 501 WorkspaceD.Instance activeInstance(WorkspaceD.Instance value) @property 502 { 503 trace("Setting active instance to ", value ? value.cwd : "<null>", "."); 504 return _activeInstance = value; 505 } 506 507 WorkspaceD.Instance activeInstance() @property 508 { 509 return _activeInstance; 510 } 511 512 string workspaceRootFor(string uri) 513 { 514 return workspace(uri).folder.uri.uriToFile; 515 } 516 517 bool hasWorkspace(string uri) 518 { 519 foreach (i, ref workspace; workspaces) 520 if (uri.startsWith(workspace.folder.uri)) 521 return true; 522 return false; 523 } 524 525 ref Configuration config(string uri, bool userExecuted = true, 526 string file = __FILE__, size_t line = __LINE__) 527 out (result) 528 { 529 trace("Config for ", uri, ": ", result); 530 } 531 do 532 { 533 return workspace(uri, userExecuted, file, line).config; 534 } 535 536 ref Configuration firstConfig() 537 { 538 if (!workspaces.length) 539 throw new Exception("No config available"); 540 return workspaces[0].config; 541 } 542 543 string userPath(string path) 544 { 545 return expandTilde(path); 546 } 547 548 string userPath(Configuration.Git git) 549 { 550 // vscode may send null git path 551 return git.path.length ? userPath(git.path) : "git"; 552 } 553 554 int toInt(JSONValue value) 555 { 556 if (value.type == JSONType.uinteger) 557 return cast(int) value.uinteger; 558 else 559 return cast(int) value.integer; 560 } 561 562 __gshared LazyWorkspaceD backend; 563 564 /// Quick function to check if a package.json can not not be a dub package file. 565 /// Returns: false if fields are used which aren't usually used in dub but in nodejs. 566 bool seemsLikeDubJson(JSONValue packageJson) 567 { 568 if ("main" in packageJson || "engines" in packageJson || "publisher" in packageJson 569 || "private" in packageJson || "devDependencies" in packageJson) 570 return false; 571 if ("name" !in packageJson) 572 return false; 573 return true; 574 } 575 576 /// Inserts a value into a sorted range. Inserts before equal elements. 577 /// Returns: the index where the value has been inserted. 578 size_t insertSorted(alias sort = "a<b", T)(ref T[] arr, T value) 579 { 580 auto v = arr.binarySearch!sort(value); 581 if (v < 0) 582 v = ~v; 583 arr.length++; 584 for (ptrdiff_t i = cast(ptrdiff_t) arr.length - 1; i > v; i--) 585 move(arr[i - 1], arr[i]); 586 arr[v] = value; 587 return v; 588 } 589 590 /// Finds a value in a sorted range and returns its index. 591 /// Returns: a bitwise invert of the first element bigger than value. Use `~ret` to turn it back. 592 ptrdiff_t binarySearch(alias sort = "a<b", T)(T[] arr, T value) 593 { 594 auto sorted = assumeSorted!sort(arr).trisect(value); 595 if (sorted[1].length) 596 return cast(ptrdiff_t) sorted[0].length; 597 else 598 return ~cast(ptrdiff_t) sorted[0].length; 599 } 600 601 void prettyPrintStruct(alias printFunc, T, int line = __LINE__, string file = __FILE__, 602 string funcName = __FUNCTION__, string prettyFuncName = __PRETTY_FUNCTION__, 603 string moduleName = __MODULE__)(T value, string indent = "\t") 604 if (is(T == struct)) 605 { 606 static foreach (i, member; T.tupleof) 607 { 608 { 609 static if (is(typeof(member) == Optional!U, U)) 610 { 611 if (value.tupleof[i].isNull) 612 { 613 printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent, 614 __traits(identifier, member), "?: <null>"); 615 } 616 else 617 { 618 static if (is(U == struct)) 619 { 620 printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent, 621 __traits(identifier, member), "?:"); 622 prettyPrintStruct!(printFunc, U, line, file, funcName, prettyFuncName, moduleName)(value.tupleof[i].get, 623 indent ~ "\t"); 624 } 625 else 626 { 627 printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent, 628 __traits(identifier, member), "?: ", value.tupleof[i].get); 629 } 630 } 631 } 632 else static if (is(typeof(member) == JSONValue)) 633 { 634 printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent, 635 __traits(identifier, member), ": ", value.tupleof[i].toString()); 636 } 637 else static if (is(typeof(member) == struct)) 638 { 639 printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent, 640 __traits(identifier, member), ":"); 641 prettyPrintStruct!(printFunc, typeof(member), line, file, funcName, 642 prettyFuncName, moduleName)(value.tupleof[i], indent ~ "\t"); 643 } 644 else 645 printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent, 646 __traits(identifier, member), ": ", value.tupleof[i]); 647 } 648 } 649 }