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