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 14 /// These are kind-of minimum values for a bunch of "killer" tests in libdparse 15 debug 16 enum requiredLibdparsePageCount = 128; // = 1 MiB stack per fiber 17 else // release builds are more optimized with stack usage 18 enum requiredLibdparsePageCount = 32; // = 256 KiB stack per fiber 19 20 static immutable LanguageServerConfig lsConfig = { 21 defaultPages: requiredLibdparsePageCount, 22 productName: "serve-d" 23 }; 24 25 mixin LanguageServerRouter!(served.extension, lsConfig) lspRouter; 26 27 import core.time : MonoTime; 28 29 import std.algorithm; 30 import std.array; 31 import std.ascii; 32 import std.conv; 33 import std.experimental.logger; 34 import std.json; 35 import std.meta; 36 import std.path; 37 import std.range; 38 import std.string; 39 40 import fs = std.file; 41 import io = std.stdio; 42 43 import mir.serde; 44 45 import workspaced.api; 46 47 deprecated("import stdlib_detect directly") 48 public import served.utils.stdlib_detect : parseDmdConfImports, parseDflagsImports; 49 50 enum IncludedFeatures = ["d", "workspaces"]; 51 52 __gshared MonoTime startupTime; 53 54 alias documents = lspRouter.documents; 55 alias rpc = lspRouter.rpc; 56 57 enum ManyProjectsAction : string 58 { 59 ask = "ask", 60 skip = "skip", 61 load = "load" 62 } 63 64 // alias to avoid name clashing 65 alias UserConfiguration = Configuration; 66 @serdeIgnoreUnexpectedKeys 67 struct Configuration 68 { 69 @serdeIgnoreUnexpectedKeys: 70 @serdeOptional: 71 72 struct D 73 { 74 @serdeOptional: 75 Nullable!(string, string[]) stdlibPath = Nullable!(string, string[])("auto"); 76 string dcdClientPath = "dcd-client", dcdServerPath = "dcd-server"; 77 string dubPath = "dub"; 78 string dmdPath = "dmd"; 79 bool enableLinting = true; 80 bool enableSDLLinting = true; 81 bool enableStaticLinting = true; 82 bool enableDubLinting = true; 83 bool enableAutoComplete = true; 84 bool enableFormatting = true; 85 bool enableDMDImportTiming = false; 86 bool enableCoverageDecoration = true; 87 bool enableGCProfilerDecorations = true; 88 bool neverUseDub = false; 89 string[] projectImportPaths; 90 string dubConfiguration; 91 string dubArchType; 92 string dubBuildType; 93 string dubCompiler; 94 bool overrideDfmtEditorconfig = true; 95 bool aggressiveUpdate = false; // differs from default code-d settings on purpose! 96 bool argumentSnippets = false; 97 bool scanAllFolders = true; 98 string[] disabledRootGlobs; 99 string[] extraRoots; 100 string manyProjectsAction = ManyProjectsAction.ask; 101 int manyProjectsThreshold = 6; 102 string lintOnFileOpen = "project"; 103 bool dietContextCompletion = false; 104 bool generateModuleNames = true; 105 } 106 107 struct DFmt 108 { 109 @serdeOptional: 110 bool alignSwitchStatements = true; 111 string braceStyle = "allman"; 112 bool outdentAttributes = true; 113 bool spaceAfterCast = true; 114 bool splitOperatorAtLineEnd = false; 115 bool selectiveImportSpace = true; 116 bool compactLabeledStatements = true; 117 string templateConstraintStyle = "conditional_newline_indent"; 118 bool spaceBeforeFunctionParameters = false; 119 bool singleTemplateConstraintIndent = false; 120 bool spaceBeforeAAColon = false; 121 bool keepLineBreaks = true; 122 bool singleIndent = true; 123 } 124 125 struct DScanner 126 { 127 @serdeOptional: 128 string[] ignoredKeys; 129 } 130 131 struct Editor 132 { 133 @serdeOptional: 134 int[] rulers; 135 int tabSize; 136 } 137 138 struct Git 139 { 140 @serdeOptional: 141 string path = "git"; 142 } 143 144 D d; 145 DFmt dfmt; 146 DScanner dscanner; 147 Editor editor; 148 Git git; 149 150 string[] stdlibPath(string cwd = null) const 151 { 152 import served.utils.stdlib_detect; 153 154 return d.stdlibPath.match!( 155 (const typeof(null) _) => autoDetectStdlibPaths(cwd, d.dubCompiler), 156 (const string s) => s == "auto" 157 ? autoDetectStdlibPaths(cwd, d.dubCompiler) 158 : [s.userPath], 159 (const string[] a) => a.map!(s => s.userPath).array 160 ); 161 } 162 163 string dcdClientPath() const 164 { 165 return detectDcdPath(d.dcdClientPath); 166 } 167 168 string dcdServerPath() const 169 { 170 return detectDcdPath(d.dcdServerPath); 171 } 172 173 private static string detectDcdPath(string path) 174 { 175 import served.extension : determineOutputFolder; 176 import served.utils.stdlib_detect : searchPathFor; 177 178 if (path != "dcd-server" && path != "dcd-client") 179 { 180 trace("using custom DCD provided from ", path); 181 return path; 182 } 183 184 // if any such executable is found in PATH, just return path and let the 185 // OS give us what it thinks it should be. 186 if (searchPathFor(path).length) 187 return path; 188 189 version (Windows) 190 auto exePath = defaultExtension(path, ".exe"); 191 else 192 auto exePath = path; 193 194 auto outputFolder = determineOutputFolder; 195 if (fs.exists(outputFolder)) 196 { 197 version (Windows) 198 static immutable searchPrefixes = ["", "DCD", "DCD\\bin"]; 199 else 200 static immutable searchPrefixes = ["", "dcd", "DCD", "dcd/bin", "DCD/bin"]; 201 202 foreach (prefix; ["", "dcd", "DCD", "dcd/bin", "DCD/bin"]) 203 { 204 auto finalPath = buildPath(outputFolder, prefix, exePath); 205 if (fs.exists(finalPath)) 206 { 207 trace("found previously installed DCD in ", finalPath); 208 return finalPath; 209 } 210 } 211 } 212 else 213 { 214 trace("no default output folder for DCD exists yet (", outputFolder, 215 "), going to ask the user for automatic installation soon"); 216 } 217 218 return path; 219 } 220 } 221 222 struct Workspace 223 { 224 WorkspaceFolder folder; 225 bool initialized, disabled; 226 string[string] startupErrorNotifications; 227 bool selected; 228 bool useGlobalConfig; 229 230 void startupError(string folder, string error) 231 { 232 if (folder !in startupErrorNotifications) 233 startupErrorNotifications[folder] = ""; 234 string errors = startupErrorNotifications[folder]; 235 if (errors.length) 236 { 237 if (errors.endsWith(".", "\n\n")) 238 startupErrorNotifications[folder] ~= " " ~ error; 239 else if (errors.endsWith(". ")) 240 startupErrorNotifications[folder] ~= error; 241 else 242 startupErrorNotifications[folder] ~= "\n\n" ~ error; 243 } 244 else 245 startupErrorNotifications[folder] = error; 246 } 247 248 string[] stdlibPath() 249 { 250 return config.stdlibPath(folder.uri.uriToFile); 251 } 252 253 auto describeState() const @property 254 { 255 static struct WorkspaceState 256 { 257 string uri, name; 258 bool initialized; 259 bool selected; 260 const(string)[string] pendingErrors; 261 } 262 263 WorkspaceState state; 264 state.uri = folder.uri; 265 state.name = folder.name; 266 state.initialized = initialized; 267 state.selected = selected; 268 state.pendingErrors = startupErrorNotifications.dup; 269 return state; 270 } 271 272 ref inout(Configuration) config() inout 273 { 274 auto cfg = folder.uri in served.extension.perWorkspaceConfigurationStore; 275 if (!cfg || useGlobalConfig) 276 cfg = served.extension.globalConfiguration; 277 return cast(inout) cfg.config; 278 } 279 } 280 281 deprecated string workspaceRoot() @property 282 { 283 return firstWorkspaceRootUri.uriToFile; 284 } 285 286 string selectedWorkspaceUri() @property 287 { 288 foreach (ref workspace; workspaces) 289 if (workspace.selected) 290 return workspace.folder.uri; 291 return firstWorkspaceRootUri; 292 } 293 294 string selectedWorkspaceRoot() @property 295 { 296 return selectedWorkspaceUri.uriToFile; 297 } 298 299 string firstWorkspaceRootUri() @property 300 { 301 return workspaces.length ? workspaces[0].folder.uri : ""; 302 } 303 304 Workspace fallbackWorkspace; 305 Workspace[] workspaces; 306 ClientCapabilities capabilities; 307 308 size_t workspaceIndex(string uri) 309 { 310 if (!uri.startsWith("file://")) 311 throw new Exception("Passed a non file:// uri to workspace(uri): '" ~ uri ~ "'"); 312 size_t best = size_t.max; 313 size_t bestLength = 0; 314 foreach (i, ref workspace; workspaces) 315 { 316 if (workspace.folder.uri.length > bestLength 317 && uri.startsWith(workspace.folder.uri) && !workspace.disabled) 318 { 319 best = i; 320 bestLength = workspace.folder.uri.length; 321 if (uri.length == workspace.folder.uri.length) // startsWith + same length => same string 322 return i; 323 } 324 } 325 return best; 326 } 327 328 ref Workspace handleThings(return ref Workspace workspace, string uri, bool userExecuted, 329 string file = __FILE__, size_t line = __LINE__) 330 { 331 if (userExecuted) 332 { 333 string f = uri.uriToFile; 334 foreach (key, error; workspace.startupErrorNotifications) 335 { 336 if (f.startsWith(key)) 337 { 338 //dfmt off 339 debug 340 rpc.window.showErrorMessage( 341 error ~ "\n\nFile: " ~ file ~ ":" ~ line.to!string); 342 else 343 rpc.window.showErrorMessage(error); 344 //dfmt on 345 workspace.startupErrorNotifications.remove(key); 346 } 347 } 348 349 bool notifyChange, changedOne; 350 foreach (ref w; workspaces) 351 { 352 if (w.selected) 353 { 354 if (w.folder.uri != workspace.folder.uri) 355 notifyChange = true; 356 changedOne = true; 357 w.selected = false; 358 } 359 } 360 workspace.selected = true; 361 if (notifyChange || !changedOne) 362 rpc.notifyMethod("coded/changedSelectedWorkspace", workspace.describeState); 363 } 364 return workspace; 365 } 366 367 ref Workspace workspace(string uri, bool userExecuted = true, 368 string file = __FILE__, size_t line = __LINE__) 369 { 370 if (!uri.length) 371 return fallbackWorkspace; 372 373 auto best = workspaceIndex(uri); 374 if (best == size_t.max) 375 return bestWorkspaceByDependency(uri).handleThings(uri, userExecuted, file, line); 376 return workspaces[best].handleThings(uri, userExecuted, file, line); 377 } 378 379 ref Workspace bestWorkspaceByDependency(string uri) 380 { 381 size_t best = size_t.max; 382 size_t bestLength; 383 foreach (i, ref workspace; workspaces) 384 { 385 auto inst = backend.getInstance(workspace.folder.uri.uriToFile); 386 if (!inst) 387 continue; 388 foreach (folder; chain(inst.importPaths, inst.importFiles, inst.stringImportPaths)) 389 { 390 string folderUri = folder.uriFromFile; 391 if (folderUri.length > bestLength && uri.startsWith(folderUri)) 392 { 393 best = i; 394 bestLength = folderUri.length; 395 if (uri.length == folderUri.length) // startsWith + same length => same string 396 return workspace; 397 } 398 } 399 } 400 if (best == size_t.max) 401 return fallbackWorkspace; 402 return workspaces[best]; 403 } 404 405 ref Workspace selectedWorkspace() 406 { 407 foreach (ref workspace; workspaces) 408 if (workspace.selected) 409 return workspace; 410 return fallbackWorkspace; 411 } 412 413 WorkspaceD.Instance _activeInstance; 414 415 WorkspaceD.Instance activeInstance(WorkspaceD.Instance value) @property 416 { 417 trace("Setting active instance to ", value ? value.cwd : "<null>", "."); 418 return _activeInstance = value; 419 } 420 421 WorkspaceD.Instance activeInstance() @property 422 { 423 return _activeInstance; 424 } 425 426 string workspaceRootFor(string uri) 427 { 428 return workspace(uri).folder.uri.uriToFile; 429 } 430 431 bool hasWorkspace(string uri) 432 { 433 foreach (i, ref workspace; workspaces) 434 if (uri.startsWith(workspace.folder.uri)) 435 return true; 436 return false; 437 } 438 439 ref Configuration config(string uri, bool userExecuted = true, 440 string file = __FILE__, size_t line = __LINE__) 441 { 442 return workspace(uri, userExecuted, file, line).config; 443 } 444 445 ref Configuration anyConfig() 446 { 447 if (!workspaces.length) 448 return fallbackWorkspace.config; 449 return workspaces[0].config; 450 } 451 452 string userPath(string path) 453 { 454 return expandTilde(path); 455 } 456 457 string userPath(Configuration.Git git) 458 { 459 // vscode may send null git path 460 return git.path.length ? userPath(git.path) : "git"; 461 } 462 463 int toInt(JsonValue value) 464 { 465 return cast(int)value.get!long; 466 } 467 468 __gshared LazyWorkspaceD backend; 469 470 /// Quick function to check if a package.json can not not be a dub package file. 471 /// Returns: false if fields are used which aren't usually used in dub but in nodejs. 472 bool seemsLikeDubJson(string json) 473 { 474 if (!json.looksLikeJsonObject) 475 return false; 476 auto packageJson = json.parseKeySlices!("main", "engines", "publisher", 477 "private_", "devDependencies", "name"); 478 if (packageJson.main.length 479 || packageJson.engines.length 480 || packageJson.publisher.length 481 || packageJson.private_.length 482 || packageJson.devDependencies.length) 483 return false; 484 if (!packageJson.name.length) 485 return false; 486 return true; 487 } 488 489 /// Inserts a value into a sorted range. Inserts before equal elements. 490 /// Returns: the index where the value has been inserted. 491 size_t insertSorted(alias sort = "a<b", T)(ref T[] arr, T value) 492 { 493 auto v = arr.binarySearch!sort(value); 494 if (v < 0) 495 v = ~v; 496 arr.length++; 497 for (ptrdiff_t i = cast(ptrdiff_t) arr.length - 1; i > v; i--) 498 move(arr[i - 1], arr[i]); 499 arr[v] = value; 500 return v; 501 } 502 503 /// Finds a value in a sorted range and returns its index. 504 /// Returns: a bitwise invert of the first element bigger than value. Use `~ret` to turn it back. 505 ptrdiff_t binarySearch(alias sort = "a<b", T)(T[] arr, T value) 506 { 507 auto sorted = assumeSorted!sort(arr).trisect(value); 508 if (sorted[1].length) 509 return cast(ptrdiff_t) sorted[0].length; 510 else 511 return ~cast(ptrdiff_t) sorted[0].length; 512 } 513 514 void prettyPrintStruct(alias printFunc, T, int line = __LINE__, string file = __FILE__, 515 string funcName = __FUNCTION__, string prettyFuncName = __PRETTY_FUNCTION__, 516 string moduleName = __MODULE__)(T value, string indent = "\t") 517 if (is(T == struct)) 518 { 519 static foreach (i, member; T.tupleof) 520 {{ 521 static if (isVariant!(typeof(member))) 522 { 523 static if (is(typeof(member).AllowedTypes[0] == void)) 524 { 525 // is optional 526 value.tupleof[i].match!( 527 () { 528 printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent, 529 __traits(identifier, member), "?: <null>"); 530 }, 531 (val) { 532 static if (is(typeof(val) == struct)) 533 { 534 printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent, 535 __traits(identifier, member), "?:"); 536 prettyPrintStruct!(printFunc, typeof(val), line, file, funcName, prettyFuncName, moduleName)( 537 val, indent ~ "\t"); 538 } 539 else 540 { 541 printFunc!(line, file, funcName, prettyFuncName, moduleName)( 542 indent, __traits(identifier, member), "?: ", val); 543 } 544 } 545 ); 546 } 547 else 548 { 549 value.tupleof[i].match!( 550 (val) { 551 static if (is(typeof(val) == struct)) 552 { 553 printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent, 554 __traits(identifier, member), ":"); 555 prettyPrintStruct!(printFunc, typeof(val), line, file, funcName, prettyFuncName, moduleName)( 556 val, indent ~ "\t"); 557 } 558 else 559 { 560 printFunc!(line, file, funcName, prettyFuncName, moduleName)( 561 indent, __traits(identifier, member), ": ", val); 562 } 563 } 564 ); 565 } 566 } 567 else static if (is(typeof(member) == JsonValue)) 568 { 569 printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent, 570 __traits(identifier, member), ": ", value.tupleof[i].toString()); 571 } 572 else static if (is(typeof(member) == struct)) 573 { 574 printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent, 575 __traits(identifier, member), ":"); 576 prettyPrintStruct!(printFunc, typeof(member), line, file, funcName, 577 prettyFuncName, moduleName)(value.tupleof[i], indent ~ "\t"); 578 } 579 else 580 printFunc!(line, file, funcName, prettyFuncName, moduleName)(indent, 581 __traits(identifier, member), ": ", value.tupleof[i]); 582 }} 583 } 584 585 /// Event called when all components have been registered but no workspaces have 586 /// been setup yet. 587 /// Signature: `()` 588 enum onRegisteredComponents; 589 590 /// Event called when a project is available but not intended to be loaded yet. 591 /// Should not access any components, otherwise it will force a load, but only 592 /// show hints in the UI. When it's accessed and actually being loaded the 593 /// events `onAddingProject` and `onAddedProject` will be emitted. 594 /// Signature: `(WorkspaceD.Instance, string dir, string uri)` 595 enum onProjectAvailable; 596 597 /// Event called when a new workspaced instance is created. Called before dub or 598 /// fsworkspace is loaded. 599 /// Signature: `(WorkspaceD.Instance, string dir, string uri)` 600 enum onAddingProject; 601 602 /// Event called when a new project root is finished setting up. Called when all 603 /// components are loaded. DCD is loaded but not yet started at this point. 604 /// Signature: `(WorkspaceD.Instance, string dir, string rootFolderUri)` 605 enum onAddedProject;