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