1 module served.extension; 2 3 import served.io.nothrow_fs; 4 import served.types; 5 import served.utils.fibermanager; 6 import served.utils.progress; 7 import served.utils.translate; 8 9 public import served.utils.async; 10 11 import core.time : msecs, seconds; 12 13 import std.algorithm : any, canFind, endsWith, map; 14 import std.array : appender, array; 15 import std.conv : to; 16 import std.datetime.stopwatch : StopWatch; 17 import std.datetime.systime : Clock, SysTime; 18 import std.experimental.logger; 19 import std.format : format; 20 import std.functional : toDelegate; 21 import std.json : JSONType, JSONValue, parseJSON; 22 import std.meta : AliasSeq; 23 import std.path : baseName, buildNormalizedPath, buildPath, chainPath, dirName, 24 globMatch, relativePath; 25 import std.string : join; 26 27 import io = std.stdio; 28 29 import workspaced.api; 30 import workspaced.coms; 31 32 // list of all commands for auto dispatch 33 public import served.commands.calltips; 34 public import served.commands.code_actions; 35 public import served.commands.code_lens; 36 public import served.commands.color; 37 public import served.commands.complete; 38 public import served.commands.dcd_update; 39 public import served.commands.definition; 40 public import served.commands.dub; 41 public import served.commands.file_search; 42 public import served.commands.format; 43 public import served.commands.highlight; 44 public import served.commands.symbol_search; 45 public import served.commands.test_provider; 46 public import served.workers.profilegc; 47 public import served.workers.rename_listener; 48 49 //dfmt off 50 alias members = AliasSeq!( 51 __traits(derivedMembers, served.extension), 52 __traits(derivedMembers, served.commands.calltips), 53 __traits(derivedMembers, served.commands.code_actions), 54 __traits(derivedMembers, served.commands.code_lens), 55 __traits(derivedMembers, served.commands.color), 56 __traits(derivedMembers, served.commands.complete), 57 __traits(derivedMembers, served.commands.dcd_update), 58 __traits(derivedMembers, served.commands.definition), 59 __traits(derivedMembers, served.commands.dub), 60 __traits(derivedMembers, served.commands.file_search), 61 __traits(derivedMembers, served.commands.format), 62 __traits(derivedMembers, served.commands.highlight), 63 __traits(derivedMembers, served.commands.symbol_search), 64 __traits(derivedMembers, served.commands.test_provider), 65 __traits(derivedMembers, served.workers.profilegc), 66 __traits(derivedMembers, served.workers.rename_listener), 67 ); 68 //dfmt on 69 70 /// Set to true when shutdown is called 71 __gshared bool shutdownRequested; 72 73 void changedConfig(string workspaceUri, string[] paths, served.types.Configuration config, 74 bool allowFallback = false, size_t index = 0, size_t numConfigs = 0) 75 { 76 StopWatch sw; 77 sw.start(); 78 79 reportProgress(ProgressType.configLoad, index, numConfigs, workspaceUri); 80 81 if (!workspaceUri.length) 82 { 83 if (!allowFallback) 84 error("Passed invalid empty workspace uri to changedConfig!"); 85 trace("Updated fallback config (user settings) for sections ", paths); 86 return; 87 } 88 89 if (!syncedConfiguration && !allowFallback) 90 { 91 syncedConfiguration = true; 92 ensureStartedUp(); 93 } 94 95 Workspace* proj = &workspace(workspaceUri); 96 bool isFallback = proj is &fallbackWorkspace; 97 if (isFallback && !allowFallback) 98 { 99 error("Did not find workspace ", workspaceUri, " when updating config?"); 100 return; 101 } 102 else if (isFallback) 103 { 104 trace("Updated fallback config (user settings) for sections ", paths); 105 return; 106 } 107 108 if (!proj.initialized) 109 { 110 doStartup(proj.folder.uri); 111 proj.initialized = true; 112 } 113 114 auto workspaceFs = workspaceUri.uriToFile; 115 116 foreach (path; paths) 117 { 118 switch (path) 119 { 120 case "d.stdlibPath": 121 if (backend.has!DCDComponent(workspaceFs)) 122 backend.get!DCDComponent(workspaceFs).addImports(config.stdlibPath(workspaceFs)); 123 break; 124 case "d.projectImportPaths": 125 if (backend.has!DCDComponent(workspaceFs)) 126 backend.get!DCDComponent(workspaceFs) 127 .addImports(config.d.projectImportPaths.map!(a => a.userPath).array); 128 break; 129 case "d.dubConfiguration": 130 if (backend.has!DubComponent(workspaceFs)) 131 { 132 auto configs = backend.get!DubComponent(workspaceFs).configurations; 133 if (configs.length == 0) 134 rpc.window.showInformationMessage(translate!"d.ext.noConfigurations.project"); 135 else 136 { 137 auto defaultConfig = config.d.dubConfiguration; 138 if (defaultConfig.length) 139 { 140 if (!configs.canFind(defaultConfig)) 141 rpc.window.showErrorMessage( 142 translate!"d.ext.config.invalid.configuration"(defaultConfig)); 143 else 144 backend.get!DubComponent(workspaceFs).setConfiguration(defaultConfig); 145 } 146 else 147 backend.get!DubComponent(workspaceFs).setConfiguration(configs[0]); 148 } 149 } 150 break; 151 case "d.dubArchType": 152 if (backend.has!DubComponent(workspaceFs) && config.d.dubArchType.length 153 && !backend.get!DubComponent(workspaceFs) 154 .setArchType(JSONValue(["arch-type": JSONValue(config.d.dubArchType)]))) 155 rpc.window.showErrorMessage( 156 translate!"d.ext.config.invalid.archType"(config.d.dubArchType)); 157 break; 158 case "d.dubBuildType": 159 if (backend.has!DubComponent(workspaceFs) && config.d.dubBuildType.length 160 && !backend.get!DubComponent(workspaceFs) 161 .setBuildType(JSONValue([ 162 "build-type": JSONValue(config.d.dubBuildType) 163 ]))) 164 rpc.window.showErrorMessage( 165 translate!"d.ext.config.invalid.buildType"(config.d.dubBuildType)); 166 break; 167 case "d.dubCompiler": 168 if (backend.has!DubComponent(workspaceFs) && config.d.dubCompiler.length 169 && !backend.get!DubComponent(workspaceFs).setCompiler(config.d.dubCompiler)) 170 rpc.window.showErrorMessage( 171 translate!"d.ext.config.invalid.compiler"(config.d.dubCompiler)); 172 break; 173 case "d.enableAutoComplete": 174 if (config.d.enableAutoComplete) 175 { 176 if (!backend.has!DCDComponent(workspaceFs)) 177 { 178 auto instance = backend.getInstance(workspaceFs); 179 lazyStartDCDServer(instance, workspaceUri); 180 } 181 } 182 else if (backend.has!DCDComponent(workspaceFs)) 183 { 184 backend.get!DCDComponent(workspaceFs).stopServer(); 185 } 186 break; 187 case "d.enableLinting": 188 if (!config.d.enableLinting) 189 { 190 import served.linters.dscanner : clear1 = clear; 191 import served.linters.dub : clear2 = clear; 192 193 clear1(); 194 clear2(); 195 } 196 break; 197 case "d.enableStaticLinting": 198 if (!config.d.enableStaticLinting) 199 { 200 import served.linters.dscanner : clear; 201 202 clear(); 203 } 204 break; 205 case "d.enableDubLinting": 206 if (!config.d.enableDubLinting) 207 { 208 import served.linters.dub : clear; 209 210 clear(); 211 } 212 break; 213 default: 214 break; 215 } 216 } 217 218 trace("Finished config change of ", workspaceUri, " with ", paths.length, 219 " changes in ", sw.peek, "."); 220 } 221 222 @protocolNotification("workspace/didChangeConfiguration") 223 void didChangeConfiguration(DidChangeConfigurationParams params) 224 { 225 processConfigChange(params.settings.parseConfiguration); 226 } 227 228 void processConfigChange(served.types.Configuration configuration) 229 { 230 import painlessjson : fromJSON; 231 232 syncingConfiguration = true; 233 scope (exit) 234 syncingConfiguration = false; 235 236 if (!workspaces.length) 237 { 238 info("initializing config for temporary fallback workspace"); 239 workspaces = [fallbackWorkspace]; 240 workspaces[0].initialized = false; 241 } 242 243 if (capabilities.workspace.configuration && workspaces.length >= 2) 244 { 245 ConfigurationItem[] items; 246 items = getGlobalConfigurationItems(); // default workspace 247 const stride = configurationSections.length; 248 249 foreach (workspace; workspaces) 250 items ~= getConfigurationItems(workspace.folder.uri); 251 252 trace("Re-requesting configuration from client because there is more than 1 workspace"); 253 auto res = rpc.sendRequest("workspace/configuration", ConfigurationParams(items)); 254 255 const expected = workspaces.length + 1; 256 JSONValue[] settings = res.validateConfigurationItemsResponse(expected); 257 if (!settings.length) 258 return; 259 260 for (size_t i = 0; i < expected; i++) 261 { 262 const isDefault = i == 0; 263 auto workspace = isDefault ? &fallbackWorkspace : &.workspace(items[i * stride].scopeUri.get, 264 false); 265 string[] changed = workspace.config.replaceAllSections(settings[i * stride .. $]); 266 changedConfig(isDefault ? null : workspace.folder.uri, changed, 267 workspace.config, isDefault, i, expected); 268 } 269 } 270 else if (workspaces.length) 271 { 272 if (workspaces.length > 1) 273 error( 274 "Client does not support configuration request, only applying config for first workspace."); 275 auto changed = workspaces[0].config.replace(configuration); 276 changedConfig(workspaces[0].folder.uri, changed, workspaces[0].config, false, 0, 1); 277 fallbackWorkspace.config = workspaces[0].config; 278 } 279 else 280 error("unexpected state: got ", workspaces.length, " workspaces and ", 281 capabilities.workspace.configuration ? "" : "no ", "configuration request support"); 282 reportProgress(ProgressType.configFinish, 0, 0); 283 } 284 285 bool syncConfiguration(string workspaceUri, size_t index = 0, size_t numConfigs = 0) 286 { 287 import painlessjson : fromJSON; 288 289 if (capabilities.workspace.configuration) 290 { 291 Workspace* proj = &workspace(workspaceUri); 292 if (proj is &fallbackWorkspace && workspaceUri.length) 293 { 294 error("Did not find workspace ", workspaceUri, " when syncing config?"); 295 return false; 296 } 297 298 ConfigurationItem[] items; 299 if (workspaceUri.length) 300 items = getConfigurationItems(proj.folder.uri); 301 else 302 items = getGlobalConfigurationItems(); 303 304 trace("Sending workspace/configuration request for ", workspaceUri); 305 auto res = rpc.sendRequest("workspace/configuration", ConfigurationParams(items)); 306 307 JSONValue[] settings = res.validateConfigurationItemsResponse(); 308 if (!settings.length) 309 return false; 310 311 string[] changed = proj.config.replaceAllSections(settings); 312 string uri = workspaceUri.length ? proj.folder.uri : null; 313 changedConfig(uri, changed, proj.config, 314 workspaceUri.length == 0, index, numConfigs); 315 return true; 316 } 317 else 318 return false; 319 } 320 321 ConfigurationItem[] getGlobalConfigurationItems() 322 { 323 ConfigurationItem[] items = new ConfigurationItem[configurationSections.length]; 324 foreach (i, section; configurationSections) 325 items[i] = ConfigurationItem(Optional!string.init, opt(section)); 326 return items; 327 } 328 329 ConfigurationItem[] getConfigurationItems(DocumentUri uri) 330 { 331 ConfigurationItem[] items = new ConfigurationItem[configurationSections.length]; 332 foreach (i, section; configurationSections) 333 items[i] = ConfigurationItem(opt(uri), opt(section)); 334 return items; 335 } 336 337 JSONValue[] validateConfigurationItemsResponse(scope return ref ResponseMessage res, 338 size_t expected = size_t.max) 339 { 340 if (res.result.type != JSONType.array) 341 { 342 error("Got invalid configuration response from language client. (not an array)"); 343 trace("Response: ", res); 344 return null; 345 } 346 347 JSONValue[] settings = res.result.array; 348 if (settings.length % configurationSections.length != 0) 349 { 350 error("Got invalid configuration response from language client. (invalid length)"); 351 trace("Response: ", res); 352 return null; 353 } 354 if (expected != size_t.max) 355 { 356 auto total = settings.length / configurationSections.length; 357 if (total > expected) 358 { 359 warning("Loading different amount of workspaces than requested: requested ", 360 expected, " but loading ", total); 361 } 362 else if (total < expected) 363 { 364 error("Didn't get all configs we asked for: requested ", expected, " but loading ", total); 365 return null; 366 } 367 } 368 return settings; 369 } 370 371 string[] getPossibleSourceRoots(string workspaceFolder) 372 { 373 import std.path : isAbsolute; 374 import std.file; 375 376 auto confPaths = config(workspaceFolder.uriFromFile, false).d.projectImportPaths.map!( 377 a => a.isAbsolute ? a : buildNormalizedPath(workspaceRoot, a)); 378 if (!confPaths.empty) 379 return confPaths.array; 380 auto a = buildNormalizedPath(workspaceFolder, "source"); 381 auto b = buildNormalizedPath(workspaceFolder, "src"); 382 if (exists(a)) 383 return [a]; 384 if (exists(b)) 385 return [b]; 386 return [workspaceFolder]; 387 } 388 389 __gshared bool syncedConfiguration = false; 390 __gshared bool syncingConfiguration = false; 391 __gshared bool startedUp = false; 392 InitializeResult initialize(InitializeParams params) 393 { 394 import std.file : chdir; 395 396 if (params.trace == "verbose") 397 globalLogLevel = LogLevel.trace; 398 399 capabilities = params.capabilities; 400 trace("initialize params:"); 401 prettyPrintStruct!trace(params); 402 403 if (params.workspaceFolders.length) 404 workspaces = params.workspaceFolders.map!(a => Workspace(a, 405 served.types.Configuration.init)).array; 406 else if (params.rootUri.length) 407 workspaces = [ 408 Workspace(WorkspaceFolder(params.rootUri, "Root"), served.types.Configuration.init) 409 ]; 410 else if (params.rootPath.length) 411 workspaces = [ 412 Workspace(WorkspaceFolder(params.rootPath.uriFromFile, "Root"), 413 served.types.Configuration.init) 414 ]; 415 416 if (workspaces.length) 417 { 418 fallbackWorkspace.folder = workspaces[0].folder; 419 fallbackWorkspace.initialized = true; 420 } 421 else 422 { 423 import std.path : buildPath; 424 import std.file : tempDir, exists, mkdir; 425 426 auto tmpFolder = buildPath(tempDir, "serve-d-dummy-workspace"); 427 if (!tmpFolder.exists) 428 mkdir(tmpFolder); 429 fallbackWorkspace.folder = WorkspaceFolder(tmpFolder.uriFromFile, "serve-d dummy tmp folder"); 430 fallbackWorkspace.initialized = true; 431 } 432 433 InitializeResult result; 434 result.capabilities.textDocumentSync = documents.syncKind; 435 // only provide fixes when doCompleteSnippets is requested 436 result.capabilities.completionProvider = CompletionOptions(doCompleteSnippets, [ 437 ".", "=", "/", "*", "+", "-" 438 ], CompletionOptions.CompletionItem(true.opt).opt); 439 result.capabilities.signatureHelpProvider = SignatureHelpOptions([ 440 "(", "[", "," 441 ]); 442 result.capabilities.workspaceSymbolProvider = true; 443 result.capabilities.definitionProvider = true; 444 result.capabilities.hoverProvider = true; 445 result.capabilities.codeActionProvider = true; 446 result.capabilities.codeLensProvider = CodeLensOptions(true); 447 result.capabilities.documentSymbolProvider = true; 448 result.capabilities.documentFormattingProvider = true; 449 result.capabilities.documentRangeFormattingProvider = true; 450 result.capabilities.colorProvider = ColorProviderOptions(); 451 result.capabilities.documentHighlightProvider = true; 452 result.capabilities.workspace = opt(ServerWorkspaceCapabilities( 453 opt(ServerWorkspaceCapabilities.WorkspaceFolders(opt(true), opt(true))))); 454 455 setTimeout({ 456 if (!syncedConfiguration && !syncingConfiguration) 457 { 458 if (capabilities.workspace.configuration) 459 { 460 if (!syncConfiguration(null, 0, workspaces.length + 1)) 461 error("Syncing user configuration failed!"); 462 463 warning( 464 "Didn't receive any configuration notification, manually requesting all configurations now"); 465 466 foreach (i, ref workspace; workspaces) 467 syncConfiguration(workspace.folder.uri, i + 1, workspaces.length + 1); 468 } 469 else 470 { 471 warning("This Language Client doesn't support configuration requests and also didn't send any ", 472 "configuration to serve-d. Initializing using default configuration"); 473 474 changedConfig(workspaces[0].folder.uri, null, workspaces[0].config); 475 fallbackWorkspace.config = workspaces[0].config; 476 } 477 478 reportProgress(ProgressType.configFinish, 0, 0); 479 } 480 }, 1000); 481 482 return result; 483 } 484 485 void ensureStartedUp() 486 { 487 if (startedUp) 488 return; 489 startedUp = true; 490 doGlobalStartup(); 491 } 492 493 void doGlobalStartup() 494 { 495 try 496 { 497 trace("Initializing serve-d for global access"); 498 499 backend.globalConfiguration.base = JSONValue( 500 [ 501 "dcd": JSONValue([ 502 "clientPath": JSONValue(firstConfig.d.dcdClientPath.userPath), 503 "serverPath": JSONValue(firstConfig.d.dcdServerPath.userPath), 504 "port": JSONValue(9166) 505 ]), 506 "dmd": JSONValue(["path": JSONValue(firstConfig.d.dmdPath.userPath)]) 507 ]); 508 509 trace("Setup global configuration as " ~ backend.globalConfiguration.base.toString); 510 511 reportProgress(ProgressType.globalStartup, 0, 0, "Initializing serve-d..."); 512 513 trace("Registering dub"); 514 backend.register!DubComponent(false); 515 trace("Registering fsworkspace"); 516 backend.register!FSWorkspaceComponent(false); 517 trace("Registering dcd"); 518 backend.register!DCDComponent; 519 trace("Registering dcdext"); 520 backend.register!DCDExtComponent; 521 trace("Registering dmd"); 522 backend.register!DMDComponent; 523 trace("Starting dscanner"); 524 backend.register!DscannerComponent; 525 trace("Starting dfmt"); 526 backend.register!DfmtComponent; 527 trace("Starting dlangui"); 528 backend.register!DlanguiComponent; 529 trace("Starting importer"); 530 backend.register!ImporterComponent; 531 trace("Starting moduleman"); 532 backend.register!ModulemanComponent; 533 trace("Starting snippets"); 534 backend.register!SnippetsComponent; 535 536 if (!backend.has!DCDComponent || backend.get!DCDComponent.isOutdated) 537 { 538 auto installed = backend.has!DCDComponent 539 ? backend.get!DCDComponent.serverInstalledVersion : "none"; 540 541 string outdatedMessage = translate!"d.served.outdatedDCD"( 542 DCDComponent.latestKnownVersion.to!(string[]).join("."), installed); 543 544 dcdUpdating = true; 545 dcdUpdateReason = format!"DCD is outdated. Expected: %(%s.%), got %s"( 546 DCDComponent.latestKnownVersion, installed); 547 if (firstConfig.d.aggressiveUpdate) 548 spawnFiber((&updateDCD).toDelegate); 549 else 550 { 551 spawnFiber({ 552 static if (isDCDFromSource) 553 auto action = translate!"d.ext.compileProgram"("DCD"); 554 else 555 auto action = translate!"d.ext.downloadProgram"("DCD"); 556 557 auto res = rpc.window.requestMessage(MessageType.error, outdatedMessage, [ 558 action 559 ]); 560 561 if (res == action) 562 spawnFiber((&updateDCD).toDelegate); 563 }, 4); 564 } 565 } 566 567 cast(void)emitExtensionEvent!onRegisteredComponents; 568 } 569 catch (Exception e) 570 { 571 error("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); 572 error("Failed to fully globally initialize:"); 573 error(e); 574 error("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); 575 } 576 } 577 578 /// A root which could be started up on load 579 struct RootSuggestion 580 { 581 /// Absolute filesystem path to the project root (assuming passed in root was absolute) 582 string dir; 583 /// 584 bool useDub; 585 } 586 587 RootSuggestion[] rootsForProject(string root, bool recursive, string[] blocked, 588 string[] extra) 589 { 590 RootSuggestion[] ret; 591 void addSuggestion(string dir, bool useDub) 592 { 593 dir = buildNormalizedPath(dir); 594 595 if (dir.endsWith('/', '\\')) 596 dir = dir[0 .. $ - 1]; 597 598 if (!ret.canFind!(a => a.dir == dir)) 599 ret ~= RootSuggestion(dir, useDub); 600 } 601 602 bool rootDub = fs.exists(chainPath(root, "dub.json")) || fs.exists(chainPath(root, "dub.sdl")); 603 if (!rootDub && fs.exists(chainPath(root, "package.json"))) 604 { 605 try 606 { 607 auto packageJson = fs.readText(chainPath(root, "package.json")); 608 auto json = parseJSON(packageJson); 609 if (seemsLikeDubJson(json)) 610 rootDub = true; 611 } 612 catch (Exception) 613 { 614 } 615 } 616 addSuggestion(root, rootDub); 617 618 if (recursive) 619 { 620 PackageDescriptorLoop: foreach (pkg; tryDirEntries(root, "dub.{json,sdl}", fs.SpanMode.breadth)) 621 { 622 auto dir = dirName(pkg); 623 if (dir.canFind(".dub")) 624 continue; 625 if (dir == root) 626 continue; 627 if (blocked.any!(a => globMatch(dir.relativePath(root), a) 628 || globMatch(pkg.relativePath(root), a) || globMatch((dir ~ "/").relativePath, a))) 629 continue; 630 addSuggestion(dir, true); 631 } 632 } 633 foreach (dir; extra) 634 { 635 string p = buildNormalizedPath(root, dir); 636 addSuggestion(p, fs.exists(chainPath(p, "dub.json")) || fs.exists(chainPath(p, "dub.sdl"))); 637 } 638 info("Root Suggestions: ", ret); 639 return ret; 640 } 641 642 void doStartup(string workspaceUri) 643 { 644 ensureStartedUp(); 645 646 Workspace* proj = &workspace(workspaceUri); 647 if (proj is &fallbackWorkspace) 648 { 649 error("Trying to do startup on unknown workspace ", workspaceUri, "?"); 650 return; 651 } 652 trace("Initializing serve-d for " ~ workspaceUri); 653 654 struct Root 655 { 656 RootSuggestion root; 657 string uri; 658 WorkspaceD.Instance instance; 659 } 660 661 bool gotOneDub; 662 scope roots = appender!(Root[]); 663 664 auto rootSuggestions = rootsForProject(workspaceUri.uriToFile, proj.config.d.scanAllFolders, 665 proj.config.d.disabledRootGlobs, proj.config.d.extraRoots); 666 667 foreach (i, root; rootSuggestions) 668 { 669 reportProgress(ProgressType.workspaceStartup, i, rootSuggestions.length, root.dir.uriFromFile); 670 info("registering instance for root ", root); 671 672 auto workspaceRoot = root.dir; 673 workspaced.api.Configuration config; 674 config.base = JSONValue([ 675 "dcd": JSONValue([ 676 "clientPath": JSONValue(proj.config.d.dcdClientPath.userPath), 677 "serverPath": JSONValue(proj.config.d.dcdServerPath.userPath), 678 "port": JSONValue(9166) 679 ]), 680 "dmd": JSONValue(["path": JSONValue(proj.config.d.dmdPath.userPath)]) 681 ]); 682 auto instance = backend.addInstance(workspaceRoot, config); 683 if (!activeInstance) 684 activeInstance = instance; 685 686 roots ~= Root(root, workspaceUri, instance); 687 emitExtensionEvent!onProjectAvailable(instance, workspaceRoot, workspaceUri); 688 689 if (auto lazyInstance = cast(LazyWorkspaceD.LazyInstance)instance) 690 { 691 auto lazyLoadCallback(WorkspaceD.Instance instance, string workspaceRoot, string workspaceUri, RootSuggestion root) 692 { 693 return () => delayedProjectActivation(instance, workspaceRoot, workspaceUri, root); 694 } 695 696 lazyInstance.onLazyLoadInstance(lazyLoadCallback(instance, workspaceRoot, workspaceUri, root)); 697 } 698 else 699 { 700 delayedProjectActivation(instance, workspaceRoot, workspaceUri, root); 701 } 702 } 703 704 trace("Starting auto completion service..."); 705 StopWatch dcdTimer; 706 dcdTimer.start(); 707 foreach (i, root; roots.data) 708 { 709 reportProgress(ProgressType.completionStartup, i, roots.data.length, 710 root.instance.cwd.uriFromFile); 711 712 lazyStartDCDServer(root.instance, root.uri); 713 } 714 dcdTimer.stop(); 715 trace("Started all completion servers in ", dcdTimer.peek); 716 } 717 718 shared int totalLoadedProjects; 719 void delayedProjectActivation(WorkspaceD.Instance instance, string workspaceRoot, string workspaceUri, RootSuggestion root) 720 { 721 import core.atomic; 722 723 Workspace* proj = &workspace(workspaceUri); 724 if (proj is &fallbackWorkspace) 725 { 726 error("Trying to do startup on unknown workspace ", root.dir, "?"); 727 throw new Exception("failed project instance startup for " ~ root.dir); 728 } 729 730 auto numLoaded = atomicOp!"+="(totalLoadedProjects, 1); 731 732 auto manyProjectsAction = cast(ManyProjectsAction) proj.config.d.manyProjectsAction; 733 auto manyThreshold = proj.config.d.manyProjectsThreshold; 734 if (manyThreshold > 0 && numLoaded > manyThreshold) 735 { 736 switch (manyProjectsAction) 737 { 738 case ManyProjectsAction.ask: 739 auto loadButton = translate!"d.served.tooManySubprojects.load"; 740 auto skipButton = translate!"d.served.tooManySubprojects.skip"; 741 auto res = rpc.window.requestMessage(MessageType.warning, 742 translate!"d.served.tooManySubprojects.path"(root.dir), 743 [loadButton, skipButton]); 744 if (res != loadButton) 745 goto case ManyProjectsAction.skip; 746 break; 747 case ManyProjectsAction.load: 748 break; 749 default: 750 error("Ignoring invalid manyProjectsAction value ", manyProjectsAction, ", defaulting to skip"); 751 goto case; 752 case ManyProjectsAction.skip: 753 backend.removeInstance(workspaceRoot); 754 throw new Exception("skipping load of this instance"); 755 } 756 } 757 758 info("Initializing instance for root ", root); 759 StopWatch rootTimer; 760 rootTimer.start(); 761 762 emitExtensionEvent!onAddingProject(instance, workspaceRoot, workspaceUri); 763 764 bool disableDub = proj.config.d.neverUseDub || !root.useDub; 765 bool loadedDub; 766 Exception err; 767 if (!disableDub) 768 { 769 trace("Starting dub..."); 770 reportProgress(ProgressType.dubReload, 0, 1, workspaceUri); 771 scope (exit) 772 reportProgress(ProgressType.dubReload, 1, 1, workspaceUri); 773 774 try 775 { 776 if (backend.attachEager(instance, "dub", err)) 777 { 778 scope (failure) 779 instance.detach!DubComponent; 780 781 instance.get!DubComponent.validateConfiguration(); 782 loadedDub = true; 783 } 784 } 785 catch (Exception e) 786 { 787 err = e; 788 loadedDub = false; 789 } 790 791 if (!loadedDub) 792 error("Exception starting dub: ", err); 793 else 794 trace("Started dub with root dependencies ", instance.get!DubComponent.rootDependencies); 795 } 796 if (!loadedDub) 797 { 798 if (!disableDub) 799 { 800 error("Failed starting dub in ", root, " - falling back to fsworkspace"); 801 proj.startupError(workspaceRoot, translate!"d.ext.dubFail"(instance.cwd, err ? err.msg : "")); 802 } 803 try 804 { 805 trace("Starting fsworkspace..."); 806 807 instance.config.set("fsworkspace", "additionalPaths", 808 getPossibleSourceRoots(workspaceRoot)); 809 if (!backend.attachEager(instance, "fsworkspace", err)) 810 throw new Exception("Attach returned failure: " ~ err.msg); 811 } 812 catch (Exception e) 813 { 814 error(e); 815 proj.startupError(workspaceRoot, translate!"d.ext.fsworkspaceFail"(instance.cwd)); 816 } 817 } 818 else 819 didLoadDubProject(); 820 821 trace("Started files provider for root ", root); 822 823 trace("Loaded Components for ", instance.cwd, ": ", 824 instance.instanceComponents.map!"a.info.name"); 825 826 emitExtensionEvent!onAddedProject(instance, workspaceRoot, workspaceUri); 827 828 rootTimer.stop(); 829 info("Root ", root, " initialized in ", rootTimer.peek); 830 } 831 832 void didLoadDubProject() 833 { 834 static bool loadedDub = false; 835 if (!loadedDub) 836 { 837 loadedDub = true; 838 setTimeout({ rpc.notifyMethod("coded/initDubTree"); }, 50); 839 } 840 } 841 842 void removeWorkspace(string workspaceUri) 843 { 844 auto workspaceRoot = workspaceRootFor(workspaceUri); 845 if (!workspaceRoot.length) 846 return; 847 backend.removeInstance(workspaceRoot); 848 workspace(workspaceUri).disabled = true; 849 } 850 851 void handleBroadcast(WorkspaceD workspaced, WorkspaceD.Instance instance, JSONValue data) 852 { 853 if (!instance) 854 return; 855 auto type = "type" in data; 856 if (type && type.type == JSONType..string && type.str == "crash") 857 { 858 if (data["component"].str == "dcd") 859 spawnFiber(() { 860 startDCDServer(instance, instance.cwd.uriFromFile); 861 }); 862 } 863 } 864 865 bool wantsDCDServer(string workspaceUri) 866 { 867 if (shutdownRequested || dcdUpdating) 868 return false; 869 Workspace* proj = &workspace(workspaceUri, false); 870 if (proj is &fallbackWorkspace) 871 { 872 error("Trying to access DCD on unknown workspace ", workspaceUri, "?"); 873 return false; 874 } 875 if (!proj.config.d.enableAutoComplete) 876 { 877 return false; 878 } 879 880 return true; 881 } 882 883 void startDCDServer(WorkspaceD.Instance instance, string workspaceUri) 884 { 885 if (!wantsDCDServer(workspaceUri)) 886 return; 887 Workspace* proj = &workspace(workspaceUri, false); 888 assert(proj, "project unloaded while starting DCD?!"); 889 890 trace("Running DCD setup"); 891 try 892 { 893 auto dcd = instance.get!DCDComponent; 894 auto stdlibPath = proj.stdlibPath; 895 trace("startServer ", stdlibPath); 896 dcd.startServer(stdlibPath, false, true); 897 trace("refreshImports"); 898 dcd.refreshImports(); 899 } 900 catch (Exception e) 901 { 902 rpc.window.showErrorMessage(translate!"d.ext.dcdFail"(instance.cwd, 903 instance.config.get("dcd", "errorlog", ""))); 904 error(e); 905 trace("Instance Config: ", instance.config); 906 return; 907 } 908 info("Imports for ", instance.cwd, ": ", instance.importPaths); 909 } 910 911 void lazyStartDCDServer(WorkspaceD.Instance instance, string workspaceUri) 912 { 913 auto lazyInstance = cast(LazyWorkspaceD.LazyInstance)instance; 914 if (lazyInstance) 915 { 916 lazyInstance.onLazyLoad("dcd", delegate() nothrow { 917 try 918 { 919 reportProgress(ProgressType.importReload, 0, 1, workspaceUri); 920 scope (exit) 921 reportProgress(ProgressType.importReload, 1, 1, workspaceUri); 922 startDCDServer(instance, workspaceUri); 923 } 924 catch (Exception e) 925 { 926 try 927 { 928 error("Failed loading DCD on demand: ", e); 929 } 930 catch (Exception) 931 { 932 } 933 } 934 }); 935 } 936 else 937 startDCDServer(instance, workspaceUri); 938 } 939 940 string determineOutputFolder() 941 { 942 import std.process : environment; 943 944 version (linux) 945 { 946 if (fs.exists(buildPath(environment["HOME"], ".local", "share"))) 947 return buildPath(environment["HOME"], ".local", "share", "code-d", "bin"); 948 else 949 return buildPath(environment["HOME"], ".code-d", "bin"); 950 } 951 else version (Windows) 952 { 953 return buildPath(environment["APPDATA"], "code-d", "bin"); 954 } 955 else 956 { 957 return buildPath(environment["HOME"], ".code-d", "bin"); 958 } 959 } 960 961 @protocolMethod("shutdown") 962 JSONValue shutdown() 963 { 964 if (!backend) 965 return JSONValue(null); 966 backend.shutdown(); 967 served.extension.setTimeout({ 968 throw new Error("RPC still running 1s after shutdown"); 969 }, 1.seconds); 970 return JSONValue(null); 971 } 972 973 // === Protocol Notifications starting here === 974 975 @protocolNotification("$/setTrace") 976 void setTrace(TraceParams params) 977 { 978 if (params.value == "verbose") 979 globalLogLevel = LogLevel.trace; 980 else 981 globalLogLevel = LogLevel.info; 982 } 983 984 @protocolNotification("workspace/didChangeWorkspaceFolders") 985 void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) 986 { 987 foreach (toRemove; params.event.removed) 988 removeWorkspace(toRemove.uri); 989 foreach (i, toAdd; params.event.added) 990 { 991 workspaces ~= Workspace(toAdd); 992 syncConfiguration(toAdd.uri, i, params.event.added.length); 993 doStartup(toAdd.uri); 994 } 995 } 996 997 @protocolNotification("textDocument/didOpen") 998 void onDidOpenDocument(DidOpenTextDocumentParams params) 999 { 1000 string lintSetting = config(params.textDocument.uri).d.lintOnFileOpen; 1001 bool shouldLint; 1002 if (lintSetting == "always") 1003 shouldLint = true; 1004 else if (lintSetting == "project") 1005 shouldLint = workspaceIndex(params.textDocument.uri) != size_t.max; 1006 1007 if (shouldLint) 1008 onDidChangeDocument(DocumentLinkParams(TextDocumentIdentifier(params.textDocument.uri))); 1009 } 1010 1011 @protocolNotification("textDocument/didClose") 1012 void onDidCloseDocument(DidOpenTextDocumentParams params) 1013 { 1014 // remove lint warnings for external projects 1015 if (workspaceIndex(params.textDocument.uri) == size_t.max) 1016 { 1017 import served.linters.diagnosticmanager : diagnostics, updateDiagnostics; 1018 1019 foreach (ref coll; diagnostics) 1020 foreach (ref diag; coll) 1021 if (diag.uri == params.textDocument.uri) 1022 diag.diagnostics = null; 1023 1024 updateDiagnostics(params.textDocument.uri); 1025 } 1026 // but keep warnings in local projects 1027 } 1028 1029 int genericChangeTimeout; 1030 @protocolNotification("textDocument/didChange") 1031 void onDidChangeDocument(DocumentLinkParams params) 1032 { 1033 auto document = documents[params.textDocument.uri]; 1034 if (document.getLanguageId != "d") 1035 return; 1036 1037 doDscanner(params); 1038 1039 int delay = document.length > 50 * 1024 ? 500 : 50; // be slower after 50KiB 1040 clearTimeout(genericChangeTimeout); 1041 genericChangeTimeout = setTimeout({ 1042 import served.linters.dfmt : lint; 1043 1044 lint(document); 1045 // Delay to avoid too many requests 1046 }, delay); 1047 } 1048 1049 int dscannerChangeTimeout; 1050 @protocolNotification("coded/doDscanner") // deprecated alias 1051 @protocolNotification("served/doDscanner") 1052 void doDscanner(DocumentLinkParams params) 1053 { 1054 auto document = documents[params.textDocument.uri]; 1055 if (document.getLanguageId != "d") 1056 return; 1057 auto d = config(params.textDocument.uri).d; 1058 if (!d.enableStaticLinting || !d.enableLinting) 1059 return; 1060 1061 int delay = document.length > 50 * 1024 ? 1000 : 200; // be slower after 50KiB 1062 clearTimeout(dscannerChangeTimeout); 1063 dscannerChangeTimeout = setTimeout({ 1064 import served.linters.dscanner; 1065 1066 lint(document); 1067 // Delay to avoid too many requests 1068 }, delay); 1069 } 1070 1071 @protocolMethod("served/getDscannerConfig") 1072 DScannerIniSection[] getDscannerConfig(DocumentLinkParams params) 1073 { 1074 import served.linters.dscanner : getDscannerIniForDocument; 1075 1076 auto instance = backend.getBestInstance!DscannerComponent( 1077 params.textDocument.uri.uriToFile); 1078 1079 if (!instance) 1080 return null; 1081 1082 string ini = "dscanner.ini"; 1083 if (params.textDocument.uri.length) 1084 ini = getDscannerIniForDocument(params.textDocument.uri, instance); 1085 1086 auto config = instance.get!DscannerComponent.getConfig(ini); 1087 1088 DScannerIniSection sec; 1089 sec.description = __traits(getAttributes, typeof(config))[0].msg; 1090 sec.name = __traits(getAttributes, typeof(config))[0].name; 1091 1092 DScannerIniFeature feature; 1093 foreach (i, ref val; config.tupleof) 1094 { 1095 static if (is(typeof(val) == string)) 1096 { 1097 feature = DScannerIniFeature.init; 1098 feature.description = __traits(getAttributes, config.tupleof[i])[0].msg; 1099 feature.name = __traits(identifier, config.tupleof[i]); 1100 feature.enabled = val; 1101 sec.features ~= feature; 1102 } 1103 } 1104 1105 return [sec]; 1106 } 1107 1108 @protocolNotification("textDocument/didSave") 1109 void onDidSaveDocument(DidSaveTextDocumentParams params) 1110 { 1111 auto workspaceRoot = workspaceRootFor(params.textDocument.uri); 1112 auto config = workspace(params.textDocument.uri).config; 1113 auto document = documents[params.textDocument.uri]; 1114 auto fileName = params.textDocument.uri.uriToFile.baseName; 1115 1116 if (document.getLanguageId == "d" || document.getLanguageId == "diet") 1117 { 1118 if (!config.d.enableLinting) 1119 return; 1120 joinAll({ 1121 if (config.d.enableStaticLinting) 1122 { 1123 import served.linters.dscanner; 1124 1125 lint(document); 1126 clearTimeout(dscannerChangeTimeout); 1127 } 1128 }, { 1129 if (config.d.enableDubLinting) 1130 { 1131 import served.linters.dub; 1132 1133 lint(document); 1134 } 1135 }); 1136 } 1137 } 1138 1139 shared static this() 1140 { 1141 import core.time : MonoTime; 1142 startupTime = MonoTime.currTime(); 1143 } 1144 1145 shared static this() 1146 { 1147 backend = new LazyWorkspaceD(); 1148 1149 backend.onBroadcast = (&handleBroadcast).toDelegate; 1150 backend.onBindFail = (WorkspaceD.Instance instance, ComponentFactory factory, Exception err) { 1151 if (!instance && err.msg.canFind("requires to be instanced")) 1152 return; 1153 1154 if (factory.info.name == "dcd") 1155 { 1156 error("Failed to attach DCD component to ", instance ? instance.cwd : null, ": ", err.msg); 1157 if (instance && !dcdUpdating) 1158 instance.config.set("dcd", "errorlog", instance.config.get("dcd", 1159 "errorlog", "") ~ "\n" ~ err.msg); 1160 return; 1161 } 1162 1163 tracef("bind fail:\n\tinstance %s\n\tfactory %s\n\tstacktrace:\n%s\n------", 1164 instance, factory.info.name, err); 1165 if (instance) 1166 { 1167 rpc.window.showErrorMessage( 1168 "Failed to load component " ~ factory.info.name ~ " for workspace " 1169 ~ instance.cwd ~ "\n\nError: " ~ err.msg); 1170 } 1171 }; 1172 } 1173 1174 shared static ~this() 1175 { 1176 if (backend) 1177 backend.shutdown(); 1178 }