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