1 module served.extension; 2 3 import core.exception; 4 import core.thread : Fiber; 5 import core.sync.mutex; 6 7 import std.algorithm; 8 import std.array; 9 import std.conv; 10 import std.datetime.systime; 11 import std.datetime.stopwatch; 12 import fs = std.file; 13 import std.experimental.logger; 14 import std.functional; 15 import std.json; 16 import std.path; 17 import std.regex; 18 import io = std.stdio; 19 import std.string; 20 import rm.rf; 21 22 import served.ddoc; 23 import served.fibermanager; 24 import served.types; 25 import served.translate; 26 27 import workspaced.api; 28 import workspaced.com.dcd; 29 import workspaced.com.importer; 30 import workspaced.coms; 31 32 import served.linters.dub : DubDiagnosticSource; 33 34 /// Set to true when shutdown is called 35 __gshared bool shutdownRequested; 36 37 bool safe(alias fn, Args...)(Args args) 38 { 39 try 40 { 41 fn(args); 42 return true; 43 } 44 catch (Exception e) 45 { 46 error(e); 47 return false; 48 } 49 catch (AssertError e) 50 { 51 error(e); 52 return false; 53 } 54 } 55 56 void changedConfig(string workspaceUri, string[] paths, served.types.Configuration config) 57 { 58 StopWatch sw; 59 sw.start(); 60 61 if (!syncedConfiguration) 62 { 63 syncedConfiguration = true; 64 doGlobalStartup(); 65 } 66 Workspace* proj = &workspace(workspaceUri); 67 if (proj is &fallbackWorkspace) 68 { 69 error("Did not find workspace ", workspaceUri, " when updating config?"); 70 return; 71 } 72 if (!proj.initialized) 73 { 74 doStartup(proj.folder.uri); 75 proj.initialized = true; 76 } 77 78 auto workspaceFs = workspaceUri.uriToFile; 79 80 foreach (path; paths) 81 { 82 switch (path) 83 { 84 case "d.stdlibPath": 85 backend.get!DCDComponent(workspaceFs).addImports(config.stdlibPath); 86 break; 87 case "d.projectImportPaths": 88 backend.get!DCDComponent(workspaceFs).addImports(config.d.projectImportPaths); 89 break; 90 case "d.dubConfiguration": 91 auto configs = backend.get!DubComponent(workspaceFs).configurations; 92 if (configs.length == 0) 93 rpc.window.showInformationMessage(translate!"d.ext.noConfigurations.project"); 94 else 95 { 96 auto defaultConfig = config.d.dubConfiguration; 97 if (defaultConfig.length) 98 { 99 if (!configs.canFind(defaultConfig)) 100 rpc.window.showErrorMessage( 101 translate!"d.ext.config.invalid.configuration"(defaultConfig)); 102 else 103 backend.get!DubComponent(workspaceFs).setConfiguration(defaultConfig); 104 } 105 else 106 backend.get!DubComponent(workspaceFs).setConfiguration(configs[0]); 107 } 108 break; 109 case "d.dubArchType": 110 if (config.d.dubArchType.length && !backend.get!DubComponent(workspaceFs) 111 .setArchType(JSONValue(["arch-type" : JSONValue(config.d.dubArchType)]))) 112 rpc.window.showErrorMessage( 113 translate!"d.ext.config.invalid.archType"(config.d.dubArchType)); 114 break; 115 case "d.dubBuildType": 116 if (config.d.dubBuildType.length && !backend.get!DubComponent(workspaceFs) 117 .setBuildType(JSONValue(["build-type" : JSONValue(config.d.dubBuildType)]))) 118 rpc.window.showErrorMessage( 119 translate!"d.ext.config.invalid.buildType"(config.d.dubBuildType)); 120 break; 121 case "d.dubCompiler": 122 if (config.d.dubCompiler.length && !backend.get!DubComponent(workspaceFs) 123 .setCompiler(config.d.dubCompiler)) 124 rpc.window.showErrorMessage( 125 translate!"d.ext.config.invalid.compiler"(config.d.dubCompiler)); 126 break; 127 default: 128 break; 129 } 130 } 131 132 trace("Finished config change of ", workspaceUri, " with ", paths.length, 133 " changes in ", sw.peek, "."); 134 } 135 136 void processConfigChange(served.types.Configuration configuration) 137 { 138 import painlessjson : fromJSON; 139 140 if (capabilities.workspace.configuration && workspaces.length >= 2) 141 { 142 ConfigurationItem[] items; 143 foreach (workspace; workspaces) 144 foreach (section; configurationSections) 145 items ~= ConfigurationItem(opt(workspace.folder.uri), opt(section)); 146 auto res = rpc.sendRequest("workspace/configuration", ConfigurationParams(items)); 147 if (res.result.type == JSON_TYPE.ARRAY) 148 { 149 JSONValue[] settings = res.result.array; 150 if (settings.length % configurationSections.length != 0) 151 { 152 error("Got invalid configuration response from language client."); 153 trace("Response: ", res); 154 return; 155 } 156 for (size_t i = 0; i < settings.length; i += configurationSections.length) 157 { 158 string[] changed; 159 static foreach (n, section; configurationSections) 160 changed ~= workspaces[i / configurationSections.length].config.replaceSection!section( 161 settings[i + n].fromJSON!(configurationTypes[n])); 162 changedConfig(workspaces[i / configurationSections.length].folder.uri, 163 changed, workspaces[i / configurationSections.length].config); 164 } 165 } 166 } 167 else if (workspaces.length) 168 { 169 if (workspaces.length > 1) 170 error( 171 "Client does not support configuration request, only applying config for first workspace."); 172 served.extension.changedConfig(workspaces[0].folder.uri, 173 workspaces[0].config.replace(configuration), workspaces[0].config); 174 } 175 } 176 177 bool syncConfiguration(string workspaceUri) 178 { 179 import painlessjson : fromJSON; 180 181 if (capabilities.workspace.configuration) 182 { 183 Workspace* proj = &workspace(workspaceUri); 184 if (proj is &fallbackWorkspace) 185 { 186 error("Did not find workspace ", workspaceUri, " when syncing config?"); 187 return false; 188 } 189 ConfigurationItem[] items; 190 foreach (section; configurationSections) 191 items ~= ConfigurationItem(opt(proj.folder.uri), opt(section)); 192 auto res = rpc.sendRequest("workspace/configuration", ConfigurationParams(items)); 193 if (res.result.type == JSON_TYPE.ARRAY) 194 { 195 JSONValue[] settings = res.result.array; 196 if (settings.length % configurationSections.length != 0) 197 { 198 error("Got invalid configuration response from language client."); 199 trace("Response: ", res); 200 return false; 201 } 202 string[] changed; 203 static foreach (n, section; configurationSections) 204 changed ~= proj.config.replaceSection!section( 205 settings[n].fromJSON!(configurationTypes[n])); 206 changedConfig(proj.folder.uri, changed, proj.config); 207 return true; 208 } 209 else 210 return false; 211 } 212 else 213 return false; 214 } 215 216 string[] getPossibleSourceRoots(string workspaceFolder) 217 { 218 import std.file; 219 220 auto confPaths = config(workspaceFolder.uriFromFile, false).d.projectImportPaths.map!( 221 a => a.isAbsolute ? a : buildNormalizedPath(workspaceRoot, a)); 222 if (!confPaths.empty) 223 return confPaths.array; 224 auto a = buildNormalizedPath(workspaceFolder, "source"); 225 auto b = buildNormalizedPath(workspaceFolder, "src"); 226 if (exists(a)) 227 return [a]; 228 if (exists(b)) 229 return [b]; 230 return [workspaceFolder]; 231 } 232 233 __gshared bool syncedConfiguration = false; 234 InitializeResult initialize(InitializeParams params) 235 { 236 import std.file : chdir; 237 238 capabilities = params.capabilities; 239 trace("Set capabilities to ", params); 240 241 if (params.workspaceFolders.length) 242 workspaces = params.workspaceFolders.map!(a => Workspace(a, 243 served.types.Configuration.init)).array; 244 else if (params.rootPath.length) 245 workspaces = [Workspace(WorkspaceFolder(params.rootPath.uriFromFile, 246 "Root"), served.types.Configuration.init)]; 247 if (workspaces.length) 248 { 249 fallbackWorkspace.folder = workspaces[0].folder; 250 fallbackWorkspace.initialized = true; 251 } 252 253 InitializeResult result; 254 result.capabilities.textDocumentSync = documents.syncKind; 255 result.capabilities.completionProvider = CompletionOptions(false, [".", "(", "[", "="]); 256 result.capabilities.signatureHelpProvider = SignatureHelpOptions(["(", "[", ","]); 257 result.capabilities.workspaceSymbolProvider = true; 258 result.capabilities.definitionProvider = true; 259 result.capabilities.hoverProvider = true; 260 result.capabilities.codeActionProvider = true; 261 result.capabilities.codeLensProvider = CodeLensOptions(true); 262 result.capabilities.documentSymbolProvider = true; 263 result.capabilities.documentFormattingProvider = true; 264 result.capabilities.codeActionProvider = true; 265 result.capabilities.workspace = opt(ServerWorkspaceCapabilities( 266 opt(ServerWorkspaceCapabilities.WorkspaceFolders(opt(true), opt(true))))); 267 268 setTimeout({ 269 if (!syncedConfiguration && capabilities.workspace.configuration) 270 foreach (ref workspace; workspaces) 271 syncConfiguration(workspace.folder.uri); 272 }, 1000); 273 274 return result; 275 } 276 277 void doGlobalStartup() 278 { 279 try 280 { 281 trace("Initializing serve-d for global access"); 282 283 backend.globalConfiguration.base = JSONValue(["dcd" : JSONValue(["clientPath" 284 : JSONValue(firstConfig.d.dcdClientPath), "serverPath" 285 : JSONValue(firstConfig.d.dcdServerPath), "port" : JSONValue(9166)]), 286 "dmd" : JSONValue(["path" : JSONValue(firstConfig.d.dmdPath)])]); 287 288 trace("Setup global configuration as " ~ backend.globalConfiguration.base.toString); 289 290 trace("Registering dub"); 291 backend.register!DubComponent(false); 292 trace("Registering fsworkspace"); 293 backend.register!FSWorkspaceComponent(false); 294 trace("Registering dcd"); 295 backend.register!DCDComponent(false); 296 trace("Registering dcdext"); 297 backend.register!DCDExtComponent(false); 298 trace("Registering dmd"); 299 backend.register!DMDComponent(false); 300 trace("Starting dscanner"); 301 backend.register!DscannerComponent; 302 trace("Starting dfmt"); 303 backend.register!DfmtComponent; 304 trace("Starting dlangui"); 305 backend.register!DlanguiComponent; 306 trace("Starting importer"); 307 backend.register!ImporterComponent; 308 trace("Starting moduleman"); 309 backend.register!ModulemanComponent; 310 311 if (backend.get!DCDComponent.isOutdated) 312 { 313 if (firstConfig.d.aggressiveUpdate) 314 spawnFiber((&updateDCD).toDelegate); 315 else 316 { 317 spawnFiber({ 318 auto action = translate!"d.ext.compileProgram"("DCD"); 319 auto res = rpc.window.requestMessage(MessageType.error, translate!"d.served.failDCD"(firstWorkspaceRootUri, 320 firstConfig.d.dcdClientPath, firstConfig.d.dcdServerPath), [action]); 321 if (res == action) 322 spawnFiber((&updateDCD).toDelegate); 323 }); 324 } 325 } 326 } 327 catch (Exception e) 328 { 329 error("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); 330 error("Failed to fully globally initialize:"); 331 error(e); 332 error("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); 333 } 334 } 335 336 struct RootSuggestion 337 { 338 string dir; 339 bool useDub; 340 } 341 342 RootSuggestion[] rootsForProject(string root, bool recursive, string[] blocked, string[] extra) 343 { 344 RootSuggestion[] ret; 345 bool rootDub = fs.exists(chainPath(root, "dub.json")) || fs.exists(chainPath(root, "dub.sdl")); 346 if (!rootDub && fs.exists(chainPath(root, "package.json"))) 347 { 348 auto packageJson = fs.readText(chainPath(root, "package.json")); 349 try 350 { 351 auto json = parseJSON(packageJson); 352 if (seemsLikeDubJson(json)) 353 rootDub = true; 354 } 355 catch (Exception) 356 { 357 } 358 } 359 ret ~= RootSuggestion(root, rootDub); 360 if (recursive) 361 foreach (pkg; fs.dirEntries(root, "dub.{json,sdl}", fs.SpanMode.depth)) 362 { 363 auto dir = dirName(pkg); 364 if (dir.canFind(".dub")) 365 continue; 366 if (dir == root) 367 continue; 368 if (blocked.any!(a => globMatch(dir.relativePath(root), a) 369 || globMatch(pkg.relativePath(root), a) || globMatch((dir ~ "/").relativePath, a))) 370 continue; 371 ret ~= RootSuggestion(dir, true); 372 } 373 foreach (dir; extra) 374 { 375 string p = buildNormalizedPath(root, dir); 376 if (!ret.canFind!(a => a.dir == p)) 377 ret ~= RootSuggestion(p, fs.exists(chainPath(p, "dub.json")) 378 || fs.exists(chainPath(p, "dub.sdl"))); 379 } 380 info("Root Suggestions: ", ret); 381 return ret; 382 } 383 384 void doStartup(string workspaceUri) 385 { 386 Workspace* proj = &workspace(workspaceUri); 387 if (proj is &fallbackWorkspace) 388 { 389 error("Trying to do startup on unknown workspace ", workspaceUri, "?"); 390 return; 391 } 392 trace("Initializing serve-d for " ~ workspaceUri); 393 394 foreach (root; rootsForProject(workspaceUri.uriToFile, proj.config.d.scanAllFolders, 395 proj.config.d.disabledRootGlobs, proj.config.d.extraRoots)) 396 { 397 auto workspaceRoot = root.dir; 398 workspaced.api.Configuration config; 399 config.base = JSONValue(["dcd" : JSONValue(["clientPath" 400 : JSONValue(proj.config.d.dcdClientPath), "serverPath" 401 : JSONValue(proj.config.d.dcdServerPath), "port" : JSONValue(9166)]), 402 "dmd" : JSONValue(["path" : JSONValue(proj.config.d.dmdPath)])]); 403 auto instance = backend.addInstance(workspaceRoot, config); 404 405 bool disableDub = proj.config.d.neverUseDub || !root.useDub; 406 bool loadedDub; 407 if (!disableDub) 408 { 409 trace("Starting dub..."); 410 try 411 { 412 if (backend.attach(instance, "dub")) 413 loadedDub = true; 414 } 415 catch (Exception e) 416 { 417 error("Exception starting dub: ", e); 418 } 419 } 420 if (!loadedDub) 421 { 422 if (!disableDub) 423 { 424 error("Failed starting dub in ", root, " - falling back to fsworkspace"); 425 proj.startupError(workspaceRoot, translate!"d.ext.dubFail"(instance.cwd)); 426 } 427 try 428 { 429 instance.config.set("fsworkspace", "additionalPaths", 430 getPossibleSourceRoots(workspaceRoot)); 431 if (!backend.attach(instance, "fsworkspace")) 432 throw new Exception("Attach returned failure"); 433 } 434 catch (Exception e) 435 { 436 error(e); 437 proj.startupError(workspaceRoot, translate!"d.ext.fsworkspaceFail"(instance.cwd)); 438 } 439 } 440 else 441 setTimeout({ rpc.notifyMethod("coded/initDubTree"); }, 50); 442 443 if (!backend.attach(instance, "dmd")) 444 error("Failed to attach DMD component to ", workspaceUri); 445 startDCD(instance, workspaceUri); 446 447 trace("Loaded Components for ", instance.cwd, ": ", 448 instance.instanceComponents.map!"a.info.name"); 449 } 450 } 451 452 void removeWorkspace(string workspaceUri) 453 { 454 auto workspaceRoot = workspaceRootFor(workspaceUri); 455 if (!workspaceRoot.length) 456 return; 457 backend.removeInstance(workspaceRoot); 458 workspace(workspaceUri).disabled = true; 459 } 460 461 void handleBroadcast(WorkspaceD workspaced, WorkspaceD.Instance instance, JSONValue data) 462 { 463 if (!instance) 464 return; 465 auto type = "type" in data; 466 if (type && type.type == JSON_TYPE.STRING && type.str == "crash") 467 { 468 if (data["component"].str == "dcd") 469 spawnFiber(() => startDCD(instance, instance.cwd.uriFromFile)); 470 } 471 } 472 473 void startDCD(WorkspaceD.Instance instance, string workspaceUri) 474 { 475 if (shutdownRequested) 476 return; 477 Workspace* proj = &workspace(workspaceUri, false); 478 if (proj is &fallbackWorkspace) 479 { 480 error("Trying to start DCD on unknown workspace ", workspaceUri, "?"); 481 return; 482 } 483 trace("Starting dcd"); 484 if (!backend.attach(instance, "dcd")) 485 error("Failed to attach DCD component to ", instance.cwd); 486 trace("Starting dcdext"); 487 if (!backend.attach(instance, "dcdext")) 488 error("Failed to attach DCD component to ", instance.cwd); 489 trace("Running DCD setup"); 490 try 491 { 492 trace("findAndSelectPort 9166"); 493 auto port = backend.get!DCDComponent(instance.cwd) 494 .findAndSelectPort(cast(ushort) 9166).getYield; 495 trace("Setting port to ", port); 496 instance.config.set("dcd", "port", cast(int) port); 497 trace("startServer ", proj.config.stdlibPath); 498 backend.get!DCDComponent(instance.cwd).startServer(proj.config.stdlibPath); 499 trace("refreshImports"); 500 backend.get!DCDComponent(instance.cwd).refreshImports(); 501 } 502 catch (Exception e) 503 { 504 rpc.window.showErrorMessage(translate!"d.ext.dcdFail"(instance.cwd)); 505 error(e); 506 trace("Instance Config: ", instance.config); 507 return; 508 } 509 info("Imports for ", instance.cwd, ": ", backend.getInstance(instance.cwd).importPaths); 510 511 auto globalDCD = backend.get!DCDComponent; 512 if (!globalDCD.isActive) 513 { 514 globalDCD.fromRunning(globalDCD.getSupportsFullOutput, globalDCD.isUsingUnixDomainSockets 515 ? globalDCD.getSocketFile : "", globalDCD.isUsingUnixDomainSockets ? 0 516 : globalDCD.getRunningPort); 517 } 518 } 519 520 string determineOutputFolder() 521 { 522 import std.process : environment; 523 524 version (linux) 525 { 526 if (fs.exists(buildPath(environment["HOME"], ".local", "share"))) 527 return buildPath(environment["HOME"], ".local", "share", "code-d", "bin"); 528 else 529 return buildPath(environment["HOME"], ".code-d", "bin"); 530 } 531 else version (Windows) 532 { 533 return buildPath(environment["APPDATA"], "code-d", "bin"); 534 } 535 else 536 { 537 return buildPath(environment["HOME"], ".code-d", "bin"); 538 } 539 } 540 541 @protocolNotification("served/updateDCD") 542 void updateDCD() 543 { 544 rpc.notifyMethod("coded/logInstall", "Installing DCD"); 545 string outputFolder = determineOutputFolder; 546 if (fs.exists(outputFolder)) 547 rmdirRecurseForce(outputFolder); 548 if (!fs.exists(outputFolder)) 549 fs.mkdirRecurse(outputFolder); 550 string[] platformOptions; 551 version (Windows) 552 platformOptions = ["--arch=x86_mscoff"]; 553 bool success = compileDependency(outputFolder, "DCD", 554 "https://github.com/Hackerpilot/DCD.git", [[firstConfig.git.path, 555 "submodule", "update", "--init", "--recursive"], ["dub", "build", 556 "--config=client"] ~ platformOptions, ["dub", "build", "--config=server"] ~ platformOptions]); 557 if (success) 558 { 559 string ext = ""; 560 version (Windows) 561 ext = ".exe"; 562 string finalDestinationClient = buildPath(outputFolder, "DCD", "dcd-client" ~ ext); 563 if (!fs.exists(finalDestinationClient)) 564 finalDestinationClient = buildPath(outputFolder, "DCD", "bin", "dcd-client" ~ ext); 565 string finalDestinationServer = buildPath(outputFolder, "DCD", "dcd-server" ~ ext); 566 if (!fs.exists(finalDestinationServer)) 567 finalDestinationServer = buildPath(outputFolder, "DCD", "bin", "dcd-server" ~ ext); 568 foreach (ref workspace; workspaces) 569 { 570 workspace.config.d.dcdClientPath = finalDestinationClient; 571 workspace.config.d.dcdServerPath = finalDestinationServer; 572 } 573 rpc.notifyMethod("coded/updateSetting", UpdateSettingParams("dcdClientPath", 574 JSONValue(finalDestinationClient), true)); 575 rpc.notifyMethod("coded/updateSetting", UpdateSettingParams("dcdServerPath", 576 JSONValue(finalDestinationServer), true)); 577 rpc.notifyMethod("coded/logInstall", "Successfully installed DCD"); 578 foreach (ref workspace; workspaces) 579 { 580 auto instance = backend.getInstance(workspace.folder.uri.uriToFile); 581 if (instance is null) 582 rpc.notifyMethod("coded/logInstall", 583 "Failed to find workspace to start DCD for " ~ workspace.folder.uri); 584 else 585 startDCD(instance, workspace.folder.uri); 586 } 587 } 588 } 589 590 bool compileDependency(string cwd, string name, string gitURI, string[][] commands) 591 { 592 import std.process; 593 594 int run(string[] cmd, string cwd) 595 { 596 import core.thread; 597 598 rpc.notifyMethod("coded/logInstall", "> " ~ cmd.join(" ")); 599 auto stdin = pipe(); 600 auto stdout = pipe(); 601 auto pid = spawnProcess(cmd, stdin.readEnd, stdout.writeEnd, 602 stdout.writeEnd, null, Config.none, cwd); 603 stdin.writeEnd.close(); 604 size_t i; 605 string[] lines; 606 bool done; 607 new Thread({ 608 scope (exit) 609 done = true; 610 foreach (line; stdout.readEnd.byLine) 611 lines ~= line.idup; 612 }).start(); 613 while (!pid.tryWait().terminated || !done || i < lines.length) 614 { 615 if (i < lines.length) 616 { 617 rpc.notifyMethod("coded/logInstall", lines[i++]); 618 } 619 Fiber.yield(); 620 } 621 return pid.wait; 622 } 623 624 rpc.notifyMethod("coded/logInstall", "Installing into " ~ cwd); 625 try 626 { 627 auto newCwd = buildPath(cwd, name); 628 if (fs.exists(newCwd)) 629 { 630 rpc.notifyMethod("coded/logInstall", "Deleting old installation from " ~ newCwd); 631 try 632 { 633 rmdirRecurseForce(newCwd); 634 } 635 catch (Exception) 636 { 637 rpc.notifyMethod("coded/logInstall", "WARNING: Failed to delete " ~ newCwd); 638 } 639 } 640 auto ret = run([firstConfig.git.path, "clone", "--recursive", "--depth=1", gitURI, name], cwd); 641 if (ret != 0) 642 throw new Exception("git ended with error code " ~ ret.to!string); 643 foreach (command; commands) 644 run(command, newCwd); 645 return true; 646 } 647 catch (Exception e) 648 { 649 rpc.notifyMethod("coded/logInstall", "Failed to install " ~ name); 650 rpc.notifyMethod("coded/logInstall", e.toString); 651 return false; 652 } 653 } 654 655 @protocolMethod("shutdown") 656 JSONValue shutdown() 657 { 658 shutdownRequested = true; 659 backend.shutdown(); 660 backend.destroy(); 661 served.extension.setTimeout({ 662 throw new Error("RPC still running 1s after shutdown"); 663 }, 1.seconds); 664 return JSONValue(null); 665 } 666 667 CompletionItemKind convertFromDCDType(string type) 668 { 669 switch (type) 670 { 671 case "c": 672 return CompletionItemKind.class_; 673 case "i": 674 return CompletionItemKind.interface_; 675 case "s": 676 case "u": 677 return CompletionItemKind.unit; 678 case "a": 679 case "A": 680 case "v": 681 return CompletionItemKind.variable; 682 case "m": 683 case "e": 684 return CompletionItemKind.field; 685 case "k": 686 return CompletionItemKind.keyword; 687 case "f": 688 return CompletionItemKind.function_; 689 case "g": 690 return CompletionItemKind.enum_; 691 case "P": 692 case "M": 693 return CompletionItemKind.module_; 694 case "l": 695 return CompletionItemKind.reference; 696 case "t": 697 case "T": 698 return CompletionItemKind.property; 699 default: 700 return CompletionItemKind.text; 701 } 702 } 703 704 SymbolKind convertFromDCDSearchType(string type) 705 { 706 switch (type) 707 { 708 case "c": 709 return SymbolKind.class_; 710 case "i": 711 return SymbolKind.interface_; 712 case "s": 713 case "u": 714 return SymbolKind.package_; 715 case "a": 716 case "A": 717 case "v": 718 return SymbolKind.variable; 719 case "m": 720 case "e": 721 return SymbolKind.field; 722 case "f": 723 case "l": 724 return SymbolKind.function_; 725 case "g": 726 return SymbolKind.enum_; 727 case "P": 728 case "M": 729 return SymbolKind.namespace; 730 case "t": 731 case "T": 732 return SymbolKind.property; 733 case "k": 734 default: 735 return cast(SymbolKind) 0; 736 } 737 } 738 739 SymbolKind convertFromDscannerType(string type) 740 { 741 switch (type) 742 { 743 case "g": 744 return SymbolKind.enum_; 745 case "e": 746 return SymbolKind.field; 747 case "v": 748 return SymbolKind.variable; 749 case "i": 750 return SymbolKind.interface_; 751 case "c": 752 return SymbolKind.class_; 753 case "s": 754 return SymbolKind.class_; 755 case "f": 756 return SymbolKind.function_; 757 case "u": 758 return SymbolKind.class_; 759 case "T": 760 return SymbolKind.property; 761 case "a": 762 return SymbolKind.field; 763 default: 764 return cast(SymbolKind) 0; 765 } 766 } 767 768 string substr(T)(string s, T start, T end) 769 { 770 if (!s.length) 771 return ""; 772 if (start < 0) 773 start = 0; 774 if (start >= s.length) 775 start = s.length - 1; 776 if (end > s.length) 777 end = s.length; 778 if (end < start) 779 return s[start .. start]; 780 return s[start .. end]; 781 } 782 783 string[] extractFunctionParameters(string sig, bool exact = false) 784 { 785 if (!sig.length) 786 return []; 787 string[] params; 788 ptrdiff_t i = sig.length - 1; 789 790 if (sig[i] == ')' && !exact) 791 i--; 792 793 ptrdiff_t paramEnd = i + 1; 794 795 void skipStr() 796 { 797 i--; 798 if (sig[i + 1] == '\'') 799 for (; i >= 0; i--) 800 if (sig[i] == '\'') 801 return; 802 bool escapeNext = false; 803 while (i >= 0) 804 { 805 if (sig[i] == '\\') 806 escapeNext = false; 807 if (escapeNext) 808 break; 809 if (sig[i] == '"') 810 escapeNext = true; 811 i--; 812 } 813 } 814 815 void skip(char open, char close) 816 { 817 i--; 818 int depth = 1; 819 while (i >= 0 && depth > 0) 820 { 821 if (sig[i] == '"' || sig[i] == '\'') 822 skipStr(); 823 else 824 { 825 if (sig[i] == close) 826 depth++; 827 else if (sig[i] == open) 828 depth--; 829 i--; 830 } 831 } 832 } 833 834 while (i >= 0) 835 { 836 switch (sig[i]) 837 { 838 case ',': 839 params ~= sig.substr(i + 1, paramEnd).strip; 840 paramEnd = i; 841 i--; 842 break; 843 case ';': 844 case '(': 845 auto param = sig.substr(i + 1, paramEnd).strip; 846 if (param.length) 847 params ~= param; 848 reverse(params); 849 return params; 850 case ')': 851 skip('(', ')'); 852 break; 853 case '}': 854 skip('{', '}'); 855 break; 856 case ']': 857 skip('[', ']'); 858 break; 859 case '"': 860 case '\'': 861 skipStr(); 862 break; 863 default: 864 i--; 865 break; 866 } 867 } 868 reverse(params); 869 return params; 870 } 871 872 unittest 873 { 874 void assertEqual(A, B)(A a, B b) 875 { 876 import std.conv : to; 877 878 assert(a == b, a.to!string ~ " is not equal to " ~ b.to!string); 879 } 880 881 assertEqual(extractFunctionParameters("void foo()"), cast(string[])[]); 882 assertEqual(extractFunctionParameters(`auto bar(int foo, Button, my.Callback cb)`), 883 ["int foo", "Button", "my.Callback cb"]); 884 assertEqual(extractFunctionParameters(`SomeType!(int, "int_") foo(T, Args...)(T a, T b, string[string] map, Other!"(" stuff1, SomeType!(double, ")double") myType, Other!"(" stuff, Other!")")`), 885 ["T a", "T b", "string[string] map", `Other!"(" stuff1`, 886 `SomeType!(double, ")double") myType`, `Other!"(" stuff`, `Other!")"`]); 887 assertEqual(extractFunctionParameters(`SomeType!(int,"int_")foo(T,Args...)(T a,T b,string[string] map,Other!"(" stuff1,SomeType!(double,")double")myType,Other!"(" stuff,Other!")")`), 888 ["T a", "T b", "string[string] map", `Other!"(" stuff1`, 889 `SomeType!(double,")double")myType`, `Other!"(" stuff`, `Other!")"`]); 890 assertEqual(extractFunctionParameters(`some_garbage(code); before(this); funcCall(4`, 891 true), [`4`]); 892 assertEqual(extractFunctionParameters(`some_garbage(code); before(this); funcCall(4, f(4)`, 893 true), [`4`, `f(4)`]); 894 assertEqual(extractFunctionParameters(`some_garbage(code); before(this); funcCall(4, ["a"], JSONValue(["b": JSONValue("c")]), recursive(func, call!s()), "texts )\"(too"`, 895 true), [`4`, `["a"]`, `JSONValue(["b": JSONValue("c")])`, 896 `recursive(func, call!s())`, `"texts )\"(too"`]); 897 } 898 899 // === Protocol Methods starting here === 900 901 @protocolMethod("textDocument/completion") 902 CompletionList provideComplete(TextDocumentPositionParams params) 903 { 904 import painlessjson : fromJSON; 905 906 auto workspaceRoot = workspaceRootFor(params.textDocument.uri); 907 Document document = documents[params.textDocument.uri]; 908 if (document.uri.toLower.endsWith("dscanner.ini")) 909 { 910 auto possibleFields = backend.get!DscannerComponent.listAllIniFields; 911 auto line = document.lineAt(params.position).strip; 912 auto defaultList = CompletionList(false, possibleFields.map!(a => CompletionItem(a.name, 913 CompletionItemKind.field.opt, Optional!string.init, MarkupContent(a.documentation) 914 .opt, Optional!string.init, Optional!string.init, (a.name ~ '=').opt)).array); 915 if (!line.length) 916 return defaultList; 917 //dfmt off 918 if (line[0] == '[') 919 return CompletionList(false, [ 920 CompletionItem("analysis.config.StaticAnalysisConfig", CompletionItemKind.keyword.opt), 921 CompletionItem("analysis.config.ModuleFilters", CompletionItemKind.keyword.opt, Optional!string.init, 922 MarkupContent("In this optional section a comma-separated list of inclusion and exclusion" 923 ~ " selectors can be specified for every check on which selective filtering" 924 ~ " should be applied. These given selectors match on the module name and" 925 ~ " partial matches (std. or .foo.) are possible. Moreover, every selectors" 926 ~ " must begin with either + (inclusion) or - (exclusion). Exclusion selectors" 927 ~ " take precedence over all inclusion operators.").opt) 928 ]); 929 //dfmt on 930 auto eqIndex = line.indexOf('='); 931 auto quotIndex = line.lastIndexOf('"'); 932 if (quotIndex != -1 && params.position.character >= quotIndex) 933 return CompletionList.init; 934 if (params.position.character < eqIndex) 935 return defaultList; 936 else//dfmt off 937 return CompletionList(false, [ 938 CompletionItem(`"disabled"`, CompletionItemKind.value.opt, "Check is disabled".opt), 939 CompletionItem(`"enabled"`, CompletionItemKind.value.opt, "Check is enabled".opt), 940 CompletionItem(`"skip-unittest"`, CompletionItemKind.value.opt, 941 "Check is enabled but not operated in the unittests".opt) 942 ]); 943 //dfmt on 944 } 945 else 946 { 947 if (document.languageId != "d") 948 return CompletionList.init; 949 string line = document.lineAt(params.position); 950 string prefix = line[0 .. min($, params.position.character)]; 951 CompletionItem[] completion; 952 if (prefix.strip == "///" || prefix.strip == "*") 953 { 954 foreach (compl; import("ddocs.txt").lineSplitter) 955 { 956 auto item = CompletionItem(compl, CompletionItemKind.snippet.opt); 957 item.insertText = compl ~ ": "; 958 completion ~= item; 959 } 960 return CompletionList(false, completion); 961 } 962 auto byteOff = cast(int) document.positionToBytes(params.position); 963 DCDCompletions result = DCDCompletions.empty; 964 joinAll({ 965 if (backend.has!DCDComponent(workspaceRoot)) 966 result = backend.get!DCDComponent(workspaceRoot) 967 .listCompletion(document.text, byteOff).getYield; 968 }, { 969 if (!line.strip.length) 970 { 971 auto defs = backend.get!DscannerComponent(workspaceRoot) 972 .listDefinitions(uriToFile(params.textDocument.uri), document.text).getYield; 973 ptrdiff_t di = -1; 974 FuncFinder: foreach (i, def; defs) 975 { 976 for (int n = 1; n < 5; n++) 977 if (def.line == params.position.line + n) 978 { 979 di = i; 980 break FuncFinder; 981 } 982 } 983 if (di == -1) 984 return; 985 auto def = defs[di]; 986 auto sig = "signature" in def.attributes; 987 if (!sig) 988 { 989 CompletionItem doc = CompletionItem("///"); 990 doc.kind = CompletionItemKind.snippet; 991 doc.insertTextFormat = InsertTextFormat.snippet; 992 auto eol = document.eolAt(params.position.line).toString; 993 doc.insertText = "/// "; 994 CompletionItem doc2 = doc; 995 doc2.label = "/**"; 996 doc2.insertText = "/** " ~ eol ~ " * $0" ~ eol ~ " */"; 997 completion ~= doc; 998 completion ~= doc2; 999 return; 1000 } 1001 auto funcArgs = extractFunctionParameters(*sig); 1002 string[] docs; 1003 if (def.name.matchFirst(ctRegex!`^[Gg]et([^a-z]|$)`)) 1004 docs ~= "Gets $0"; 1005 else if (def.name.matchFirst(ctRegex!`^[Ss]et([^a-z]|$)`)) 1006 docs ~= "Sets $0"; 1007 else if (def.name.matchFirst(ctRegex!`^[Ii]s([^a-z]|$)`)) 1008 docs ~= "Checks if $0"; 1009 else 1010 docs ~= "$0"; 1011 int argNo = 1; 1012 foreach (arg; funcArgs) 1013 { 1014 auto space = arg.lastIndexOf(' '); 1015 if (space == -1) 1016 continue; 1017 string identifier = arg[space + 1 .. $]; 1018 if (!identifier.matchFirst(ctRegex!`[a-zA-Z_][a-zA-Z0-9_]*`)) 1019 continue; 1020 if (argNo == 1) 1021 docs ~= "Params:"; 1022 docs ~= " " ~ identifier ~ " = $" ~ argNo.to!string; 1023 argNo++; 1024 } 1025 auto retAttr = "return" in def.attributes; 1026 if (retAttr && *retAttr != "void") 1027 { 1028 docs ~= "Returns: $" ~ argNo.to!string; 1029 argNo++; 1030 } 1031 auto depr = "deprecation" in def.attributes; 1032 if (depr) 1033 { 1034 docs ~= "Deprecated: $" ~ argNo.to!string ~ *depr; 1035 argNo++; 1036 } 1037 CompletionItem doc = CompletionItem("///"); 1038 doc.kind = CompletionItemKind.snippet; 1039 doc.insertTextFormat = InsertTextFormat.snippet; 1040 auto eol = document.eolAt(params.position.line).toString; 1041 doc.insertText = docs.map!(a => "/// " ~ a).join(eol); 1042 CompletionItem doc2 = doc; 1043 doc2.label = "/**"; 1044 doc2.insertText = "/** " ~ eol ~ docs.map!(a => " * " ~ a ~ eol).join() ~ " */"; 1045 completion ~= doc; 1046 completion ~= doc2; 1047 } 1048 }); 1049 switch (result.type) 1050 { 1051 case DCDCompletions.Type.identifiers: 1052 foreach (identifier; result.identifiers) 1053 { 1054 CompletionItem item; 1055 item.label = identifier.identifier; 1056 item.kind = identifier.type.convertFromDCDType; 1057 if (identifier.documentation.length) 1058 item.documentation = MarkupContent(identifier.documentation.ddocToMarked); 1059 if (identifier.definition.length) 1060 { 1061 item.detail = identifier.definition; 1062 item.sortText = identifier.definition; 1063 // TODO: only add arguments when this is a function call, eg not on template arguments 1064 if (identifier.type == "f" && workspace(params.textDocument.uri) 1065 .config.d.argumentSnippets) 1066 { 1067 item.insertTextFormat = InsertTextFormat.snippet; 1068 string args; 1069 auto parts = identifier.definition.extractFunctionParameters; 1070 if (parts.length) 1071 { 1072 bool isOptional; 1073 string[] optionals; 1074 int numRequired; 1075 foreach (i, part; parts) 1076 { 1077 if (!isOptional) 1078 isOptional = part.canFind('='); 1079 if (isOptional) 1080 optionals ~= part; 1081 else 1082 { 1083 if (args.length) 1084 args ~= ", "; 1085 args ~= "${" ~ (i + 1).to!string ~ ":" ~ part ~ "}"; 1086 numRequired++; 1087 } 1088 } 1089 foreach (i, part; optionals) 1090 { 1091 if (args.length) 1092 part = ", " ~ part; 1093 // Go through optionals in reverse 1094 args ~= "${" ~ (numRequired + optionals.length - i).to!string ~ ":" ~ part ~ "}"; 1095 } 1096 item.insertText = identifier.identifier ~ "(${0:" ~ args ~ "})"; 1097 } 1098 } 1099 } 1100 completion ~= item; 1101 } 1102 goto case; 1103 case DCDCompletions.Type.calltips: 1104 return CompletionList(false, completion); 1105 default: 1106 throw new Exception("Unexpected result from DCD:\n\t" ~ result.raw.join("\n\t")); 1107 } 1108 } 1109 } 1110 1111 @protocolMethod("textDocument/signatureHelp") 1112 SignatureHelp provideSignatureHelp(TextDocumentPositionParams params) 1113 { 1114 auto workspaceRoot = workspaceRootFor(params.textDocument.uri); 1115 auto document = documents[params.textDocument.uri]; 1116 if (document.languageId != "d") 1117 return SignatureHelp.init; 1118 auto pos = cast(int) document.positionToBytes(params.position); 1119 DCDCompletions result = backend.get!DCDComponent(workspaceRoot) 1120 .listCompletion(document.text, pos).getYield; 1121 SignatureInformation[] signatures; 1122 int[] paramsCounts; 1123 SignatureHelp help; 1124 switch (result.type) 1125 { 1126 case DCDCompletions.Type.calltips: 1127 foreach (i, calltip; result.calltips) 1128 { 1129 auto sig = SignatureInformation(calltip); 1130 immutable DCDCompletions.Symbol symbol = result.symbols[i]; 1131 if (symbol.documentation.length) 1132 sig.documentation = MarkupContent(symbol.documentation.ddocToMarked); 1133 auto funcParams = calltip.extractFunctionParameters; 1134 1135 paramsCounts ~= cast(int) funcParams.length - 1; 1136 foreach (param; funcParams) 1137 sig.parameters ~= ParameterInformation(param); 1138 1139 help.signatures ~= sig; 1140 } 1141 auto extractedParams = document.text[0 .. pos].extractFunctionParameters(true); 1142 help.activeParameter = max(0, cast(int) extractedParams.length - 1); 1143 size_t[] possibleFunctions; 1144 foreach (i, count; paramsCounts) 1145 if (count >= cast(int) extractedParams.length - 1) 1146 possibleFunctions ~= i; 1147 help.activeSignature = possibleFunctions.length ? cast(int) possibleFunctions[0] : 0; 1148 goto case; 1149 case DCDCompletions.Type.identifiers: 1150 return help; 1151 default: 1152 throw new Exception("Unexpected result from DCD"); 1153 } 1154 } 1155 1156 @protocolMethod("workspace/symbol") 1157 SymbolInformation[] provideWorkspaceSymbols(WorkspaceSymbolParams params) 1158 { 1159 import std.file; 1160 1161 // TODO: combine all workspaces 1162 auto result = backend.get!DCDComponent(workspaceRoot).searchSymbol(params.query).getYield; 1163 SymbolInformation[] infos; 1164 TextDocumentManager extraCache; 1165 foreach (symbol; result.array) 1166 { 1167 auto uri = uriFromFile(symbol.file); 1168 auto doc = documents.tryGet(uri); 1169 Location location; 1170 if (!doc.uri) 1171 doc = extraCache.tryGet(uri); 1172 if (!doc.uri) 1173 { 1174 doc = Document(uri); 1175 try 1176 { 1177 doc.text = readText(symbol.file); 1178 } 1179 catch (Exception e) 1180 { 1181 error(e); 1182 } 1183 } 1184 if (doc.text) 1185 { 1186 location = Location(doc.uri, TextRange(doc.bytesToPosition(cast(size_t) symbol.position))); 1187 infos ~= SymbolInformation(params.query, convertFromDCDSearchType(symbol.type), location); 1188 } 1189 } 1190 return infos; 1191 } 1192 1193 @protocolMethod("textDocument/documentSymbol") 1194 SymbolInformation[] provideDocumentSymbols(DocumentSymbolParams params) 1195 { 1196 auto workspaceRoot = workspaceRootFor(params.textDocument.uri); 1197 auto document = documents[params.textDocument.uri]; 1198 auto result = backend.get!DscannerComponent(workspaceRoot) 1199 .listDefinitions(uriToFile(params.textDocument.uri), document.text).getYield; 1200 SymbolInformation[] ret; 1201 foreach (def; result) 1202 { 1203 SymbolInformation info; 1204 info.name = def.name; 1205 info.location.uri = params.textDocument.uri; 1206 info.location.range = TextRange(Position(cast(uint) def.line - 1, 0)); 1207 info.kind = convertFromDscannerType(def.type); 1208 if (def.type == "f" && def.name == "this") 1209 info.kind = SymbolKind.constructor; 1210 string* ptr; 1211 auto attribs = def.attributes; 1212 if ((ptr = "struct" in attribs) !is null || (ptr = "class" in attribs) !is null 1213 || (ptr = "enum" in attribs) !is null || (ptr = "union" in attribs) !is null) 1214 info.containerName = *ptr; 1215 ret ~= info; 1216 } 1217 return ret; 1218 } 1219 1220 @protocolMethod("textDocument/definition") 1221 ArrayOrSingle!Location provideDefinition(TextDocumentPositionParams params) 1222 { 1223 auto workspaceRoot = workspaceRootFor(params.textDocument.uri); 1224 auto document = documents[params.textDocument.uri]; 1225 if (document.languageId != "d") 1226 return ArrayOrSingle!Location.init; 1227 auto result = backend.get!DCDComponent(workspaceRoot).findDeclaration(document.text, 1228 cast(int) document.positionToBytes(params.position)).getYield; 1229 if (result == DCDDeclaration.init) 1230 return ArrayOrSingle!Location.init; 1231 auto uri = document.uri; 1232 if (result.file != "stdin") 1233 { 1234 if (isAbsolute(result.file)) 1235 uri = uriFromFile(result.file); 1236 else 1237 uri = null; 1238 } 1239 size_t byteOffset = cast(size_t) result.position; 1240 Position pos; 1241 auto found = documents.tryGet(uri); 1242 if (found.uri) 1243 pos = found.bytesToPosition(byteOffset); 1244 else 1245 { 1246 string abs = result.file; 1247 if (!abs.isAbsolute) 1248 abs = buildPath(workspaceRoot, abs); 1249 pos = Position.init; 1250 size_t totalLen; 1251 foreach (line; io.File(abs).byLine(io.KeepTerminator.yes)) 1252 { 1253 totalLen += line.length; 1254 if (totalLen >= byteOffset) 1255 break; 1256 else 1257 pos.line++; 1258 } 1259 } 1260 return ArrayOrSingle!Location(Location(uri, TextRange(pos, pos))); 1261 } 1262 1263 @protocolMethod("textDocument/formatting") 1264 TextEdit[] provideFormatting(DocumentFormattingParams params) 1265 { 1266 auto config = workspace(params.textDocument.uri).config; 1267 if (!config.d.enableFormatting) 1268 return []; 1269 auto document = documents[params.textDocument.uri]; 1270 if (document.languageId != "d") 1271 return []; 1272 string[] args; 1273 if (config.d.overrideDfmtEditorconfig) 1274 { 1275 int maxLineLength = 120; 1276 int softMaxLineLength = 80; 1277 if (config.editor.rulers.length == 1) 1278 { 1279 maxLineLength = config.editor.rulers[0]; 1280 softMaxLineLength = maxLineLength - 40; 1281 } 1282 else if (config.editor.rulers.length >= 2) 1283 { 1284 maxLineLength = config.editor.rulers[$ - 1]; 1285 softMaxLineLength = config.editor.rulers[$ - 2]; 1286 } 1287 //dfmt off 1288 args = [ 1289 "--align_switch_statements", config.dfmt.alignSwitchStatements.to!string, 1290 "--brace_style", config.dfmt.braceStyle, 1291 "--end_of_line", document.eolAt(0).to!string, 1292 "--indent_size", params.options.tabSize.to!string, 1293 "--indent_style", params.options.insertSpaces ? "space" : "tab", 1294 "--max_line_length", maxLineLength.to!string, 1295 "--soft_max_line_length", softMaxLineLength.to!string, 1296 "--outdent_attributes", config.dfmt.outdentAttributes.to!string, 1297 "--space_after_cast", config.dfmt.spaceAfterCast.to!string, 1298 "--split_operator_at_line_end", config.dfmt.splitOperatorAtLineEnd.to!string, 1299 "--tab_width", params.options.tabSize.to!string, 1300 "--selective_import_space", config.dfmt.selectiveImportSpace.to!string, 1301 "--compact_labeled_statements", config.dfmt.compactLabeledStatements.to!string, 1302 "--template_constraint_style", config.dfmt.templateConstraintStyle 1303 ]; 1304 //dfmt on 1305 } 1306 auto result = backend.get!DfmtComponent.format(document.text, args).getYield; 1307 return [TextEdit(TextRange(Position(0, 0), 1308 document.offsetToPosition(document.text.length)), result)]; 1309 } 1310 1311 @protocolMethod("textDocument/hover") 1312 Hover provideHover(TextDocumentPositionParams params) 1313 { 1314 auto workspaceRoot = workspaceRootFor(params.textDocument.uri); 1315 auto document = documents[params.textDocument.uri]; 1316 if (document.languageId != "d") 1317 return Hover.init; 1318 auto docs = backend.get!DCDComponent(workspaceRoot).getDocumentation(document.text, 1319 cast(int) document.positionToBytes(params.position)).getYield; 1320 Hover ret; 1321 ret.contents = docs.ddocToMarked; 1322 return ret; 1323 } 1324 1325 private auto importRegex = regex(`import\s+(?:[a-zA-Z_]+\s*=\s*)?([a-zA-Z_]\w*(?:\.\w*[a-zA-Z_]\w*)*)?(\s*\:\s*(?:[a-zA-Z_,\s=]*(?://.*?[\r\n]|/\*.*?\*/|/\+.*?\+/)?)+)?;?`); 1326 private auto undefinedIdentifier = regex( 1327 `^undefined identifier '(\w+)'(?:, did you mean .*? '(\w+)'\?)?$`); 1328 private auto undefinedTemplate = regex(`template '(\w+)' is not defined`); 1329 private auto noProperty = regex(`^no property '(\w+)'(?: for type '.*?')?$`); 1330 private auto moduleRegex = regex(`module\s+([a-zA-Z_]\w*\s*(?:\s*\.\s*[a-zA-Z_]\w*)*)\s*;`); 1331 private auto whitespace = regex(`\s*`); 1332 1333 @protocolMethod("textDocument/codeAction") 1334 Command[] provideCodeActions(CodeActionParams params) 1335 { 1336 auto workspaceRoot = workspaceRootFor(params.textDocument.uri); 1337 auto document = documents[params.textDocument.uri]; 1338 if (document.languageId != "d") 1339 return []; 1340 Command[] ret; 1341 if (backend.has!DCDExtComponent(workspaceRoot)) // check if extends 1342 { 1343 auto startIndex = document.positionToBytes(params.range.start); 1344 ptrdiff_t idx = min(cast(ptrdiff_t) startIndex, cast(ptrdiff_t) document.text.length - 1); 1345 while (idx > 0) 1346 { 1347 if (document.text[idx] == ':') 1348 { 1349 // probably extends 1350 if (backend.get!DCDExtComponent(workspaceRoot) 1351 .implement(document.text, cast(int) startIndex).getYield.strip.length > 0) 1352 ret ~= Command("Implement base classes/interfaces", "code-d.implementMethods", 1353 [JSONValue(document.positionToOffset(params.range.start))]); 1354 break; 1355 } 1356 if (document.text[idx] == ';' || document.text[idx] == '{' || document.text[idx] == '}') 1357 break; 1358 idx--; 1359 } 1360 } 1361 foreach (diagnostic; params.context.diagnostics) 1362 { 1363 if (diagnostic.source == DubDiagnosticSource) 1364 { 1365 auto match = diagnostic.message.matchFirst(importRegex); 1366 if (diagnostic.message.canFind("import ")) 1367 { 1368 if (!match) 1369 continue; 1370 ret ~= Command("Import " ~ match[1], "code-d.addImport", 1371 [JSONValue(match[1]), JSONValue(document.positionToOffset(params.range[0]))]); 1372 } 1373 else /*if (cast(bool)(match = diagnostic.message.matchFirst(undefinedIdentifier)) 1374 || cast(bool)(match = diagnostic.message.matchFirst(undefinedTemplate)) 1375 || cast(bool)(match = diagnostic.message.matchFirst(noProperty)))*/ 1376 { 1377 // temporary fix for https://issues.dlang.org/show_bug.cgi?id=18565 1378 string[] files; 1379 string[] modules; 1380 int lineNo; 1381 match = diagnostic.message.matchFirst(undefinedIdentifier); 1382 if (match) 1383 goto start; 1384 match = diagnostic.message.matchFirst(undefinedTemplate); 1385 if (match) 1386 goto start; 1387 match = diagnostic.message.matchFirst(noProperty); 1388 if (match) 1389 goto start; 1390 goto noMatch; 1391 start: 1392 joinAll({ 1393 files ~= backend.get!DscannerComponent(workspaceRoot) 1394 .findSymbol(match[1]).getYield.map!"a.file".array; 1395 }, { 1396 if (backend.has!DCDComponent) 1397 files ~= backend.get!DCDComponent.searchSymbol(match[1]).getYield.map!"a.file".array; 1398 }); 1399 foreach (file; files.sort().uniq) 1400 { 1401 if (!isAbsolute(file)) 1402 file = buildNormalizedPath(workspaceRoot, file); 1403 lineNo = 0; 1404 foreach (line; io.File(file).byLine) 1405 { 1406 if (++lineNo >= 100) 1407 break; 1408 auto match2 = line.matchFirst(moduleRegex); 1409 if (match2) 1410 { 1411 modules ~= match2[1].replaceAll(whitespace, "").idup; 1412 break; 1413 } 1414 } 1415 } 1416 foreach (mod; modules.sort().uniq) 1417 ret ~= Command("Import " ~ mod, "code-d.addImport", [JSONValue(mod), 1418 JSONValue(document.positionToOffset(params.range[0]))]); 1419 noMatch: 1420 } 1421 } 1422 else 1423 { 1424 import dscanner.analysis.imports_sortedness : ImportSortednessCheck; 1425 1426 if (diagnostic.message == ImportSortednessCheck.MESSAGE) 1427 { 1428 ret ~= Command("Sort imports", "code-d.sortImports", 1429 [JSONValue(document.positionToOffset(params.range[0]))]); 1430 } 1431 } 1432 } 1433 return ret; 1434 } 1435 1436 @protocolMethod("textDocument/codeLens") 1437 CodeLens[] provideCodeLens(CodeLensParams params) 1438 { 1439 auto workspaceRoot = workspaceRootFor(params.textDocument.uri); 1440 auto document = documents[params.textDocument.uri]; 1441 if (document.languageId != "d") 1442 return []; 1443 CodeLens[] ret; 1444 if (workspace(params.textDocument.uri).config.d.enableDMDImportTiming) 1445 foreach (match; document.text.matchAll(importRegex)) 1446 { 1447 size_t index = match.pre.length; 1448 auto pos = document.bytesToPosition(index); 1449 ret ~= CodeLens(TextRange(pos), Optional!Command.init, JSONValue(["type" 1450 : JSONValue("importcompilecheck"), "code" : JSONValue(match.hit), 1451 "module" : JSONValue(match[1]), "workspace" : JSONValue(workspaceRoot)])); 1452 } 1453 return ret; 1454 } 1455 1456 @protocolMethod("codeLens/resolve") 1457 CodeLens resolveCodeLens(CodeLens lens) 1458 { 1459 if (lens.data.type != JSON_TYPE.OBJECT) 1460 throw new Exception("Invalid Lens Object"); 1461 auto type = "type" in lens.data; 1462 if (!type) 1463 throw new Exception("No type in Lens Object"); 1464 switch (type.str) 1465 { 1466 case "importcompilecheck": 1467 auto code = "code" in lens.data; 1468 if (!code || code.type != JSON_TYPE.STRING || !code.str.length) 1469 throw new Exception("No valid code provided"); 1470 auto module_ = "module" in lens.data; 1471 if (!module_ || module_.type != JSON_TYPE.STRING || !module_.str.length) 1472 throw new Exception("No valid module provided"); 1473 auto workspace = "workspace" in lens.data; 1474 if (!workspace || workspace.type != JSON_TYPE.STRING || !workspace.str.length) 1475 throw new Exception("No valid workspace provided"); 1476 int decMs = getImportCompilationTime(code.str, module_.str, workspace.str); 1477 lens.command = Command((decMs < 10 ? "no noticable effect" 1478 : "~" ~ decMs.to!string ~ "ms") ~ " for importing this"); 1479 return lens; 1480 default: 1481 throw new Exception("Unknown lens type"); 1482 } 1483 } 1484 1485 bool importCompilationTimeRunning; 1486 int getImportCompilationTime(string code, string module_, string workspaceRoot) 1487 { 1488 import std.math : round; 1489 1490 static struct CompileCache 1491 { 1492 SysTime at; 1493 string code; 1494 int ret; 1495 } 1496 1497 static CompileCache[] cache; 1498 1499 auto now = Clock.currTime; 1500 1501 foreach_reverse (i, exist; cache) 1502 { 1503 if (exist.code != code) 1504 continue; 1505 if (now - exist.at < (exist.ret >= 500 ? 20.minutes : exist.ret >= 30 ? 5.minutes 1506 : 2.minutes) || module_.startsWith("std.")) 1507 return exist.ret; 1508 else 1509 { 1510 cache[i] = cache[$ - 1]; 1511 cache.length--; 1512 } 1513 } 1514 1515 while (importCompilationTimeRunning) 1516 Fiber.yield(); 1517 importCompilationTimeRunning = true; 1518 scope (exit) 1519 importCompilationTimeRunning = false; 1520 // run blocking so we don't compute multiple in parallel 1521 auto ret = backend.get!DMDComponent(workspaceRoot).measureSync(code, null, 20, 500); 1522 if (!ret.success) 1523 throw new Exception("Compilation failed"); 1524 auto msecs = cast(int) round(ret.duration.total!"msecs" / 5.0) * 5; 1525 cache ~= CompileCache(now, code, msecs); 1526 StopWatch sw; 1527 sw.start(); 1528 while (sw.peek < 100.msecs) // pass through requests for 100ms 1529 Fiber.yield(); 1530 return msecs; 1531 } 1532 1533 @protocolMethod("served/listConfigurations") 1534 string[] listConfigurations() 1535 { 1536 return backend.get!DubComponent(selectedWorkspaceRoot).configurations; 1537 } 1538 1539 @protocolMethod("served/switchConfig") 1540 bool switchConfig(string value) 1541 { 1542 return backend.get!DubComponent(selectedWorkspaceRoot).setConfiguration(value); 1543 } 1544 1545 @protocolMethod("served/getConfig") 1546 string getConfig(string value) 1547 { 1548 return backend.get!DubComponent(selectedWorkspaceRoot).configuration; 1549 } 1550 1551 @protocolMethod("served/listArchTypes") 1552 string[] listArchTypes() 1553 { 1554 return backend.get!DubComponent(selectedWorkspaceRoot).archTypes; 1555 } 1556 1557 @protocolMethod("served/switchArchType") 1558 bool switchArchType(string value) 1559 { 1560 return backend.get!DubComponent(selectedWorkspaceRoot) 1561 .setArchType(JSONValue(["arch-type" : JSONValue(value)])); 1562 } 1563 1564 @protocolMethod("served/getArchType") 1565 string getArchType(string value) 1566 { 1567 return backend.get!DubComponent(selectedWorkspaceRoot).archType; 1568 } 1569 1570 @protocolMethod("served/listBuildTypes") 1571 string[] listBuildTypes() 1572 { 1573 return backend.get!DubComponent(selectedWorkspaceRoot).buildTypes; 1574 } 1575 1576 @protocolMethod("served/switchBuildType") 1577 bool switchBuildType(string value) 1578 { 1579 return backend.get!DubComponent(selectedWorkspaceRoot) 1580 .setBuildType(JSONValue(["build-type" : JSONValue(value)])); 1581 } 1582 1583 @protocolMethod("served/getBuildType") 1584 string getBuildType() 1585 { 1586 return backend.get!DubComponent(selectedWorkspaceRoot).buildType; 1587 } 1588 1589 @protocolMethod("served/getCompiler") 1590 string getCompiler() 1591 { 1592 return backend.get!DubComponent(selectedWorkspaceRoot).compiler; 1593 } 1594 1595 @protocolMethod("served/switchCompiler") 1596 bool switchCompiler(string value) 1597 { 1598 return backend.get!DubComponent(selectedWorkspaceRoot).setCompiler(value); 1599 } 1600 1601 @protocolMethod("served/addImport") 1602 auto addImport(AddImportParams params) 1603 { 1604 auto document = documents[params.textDocument.uri]; 1605 return backend.get!ImporterComponent.add(params.name.idup, document.text, 1606 params.location, params.insertOutermost); 1607 } 1608 1609 @protocolMethod("served/sortImports") 1610 TextEdit[] sortImports(SortImportsParams params) 1611 { 1612 auto document = documents[params.textDocument.uri]; 1613 TextEdit[] ret; 1614 auto sorted = backend.get!ImporterComponent.sortImports(document.text, 1615 cast(int) document.offsetToBytes(params.location)); 1616 if (sorted == ImportBlock.init) 1617 return ret; 1618 auto start = document.bytesToPosition(sorted.start); 1619 auto end = document.bytesToPosition(sorted.end); 1620 string code = sorted.imports.to!(string[]).join(document.eolAt(0).toString); 1621 return [TextEdit(TextRange(start, end), code)]; 1622 } 1623 1624 @protocolMethod("served/implementMethods") 1625 TextEdit[] implementMethods(ImplementMethodsParams params) 1626 { 1627 import std.ascii : isWhite; 1628 1629 auto workspaceRoot = workspaceRootFor(params.textDocument.uri); 1630 auto document = documents[params.textDocument.uri]; 1631 TextEdit[] ret; 1632 auto location = document.offsetToBytes(params.location); 1633 auto code = backend.get!DCDExtComponent(workspaceRoot) 1634 .implement(document.text, cast(int) location).getYield.strip; 1635 if (!code.length) 1636 return ret; 1637 auto brace = document.text.indexOf('{', location); 1638 auto fallback = brace; 1639 if (brace == -1) 1640 brace = document.text.length; 1641 else 1642 { 1643 fallback = document.text.indexOf('\n', location); 1644 brace = document.text.indexOfAny("}\n", brace); 1645 if (brace == -1) 1646 brace = document.text.length; 1647 } 1648 code = "\n\t" ~ code.replace("\n", document.eolAt(0).toString ~ "\t") ~ "\n"; 1649 bool inIdentifier = true; 1650 int depth = 0; 1651 foreach (i; location .. brace) 1652 { 1653 if (document.text[i].isWhite) 1654 inIdentifier = false; 1655 else if (document.text[i] == '{') 1656 break; 1657 else if (document.text[i] == ',' || document.text[i] == '!') 1658 inIdentifier = true; 1659 else if (document.text[i] == '(') 1660 depth++; 1661 else 1662 { 1663 if (depth > 0) 1664 { 1665 inIdentifier = true; 1666 if (document.text[i] == ')') 1667 depth--; 1668 } 1669 else if (!inIdentifier) 1670 { 1671 if (fallback != -1) 1672 brace = fallback; 1673 code = "\n{" ~ code ~ "}"; 1674 break; 1675 } 1676 } 1677 } 1678 auto pos = document.bytesToPosition(brace); 1679 return [TextEdit(TextRange(pos, pos), code)]; 1680 } 1681 1682 @protocolMethod("served/restartServer") 1683 bool restartServer() 1684 { 1685 backend.get!DCDComponent.restartServer().getYield; 1686 return true; 1687 } 1688 1689 @protocolMethod("served/updateImports") 1690 bool updateImports() 1691 { 1692 auto workspaceRoot = selectedWorkspaceRoot; 1693 bool success; 1694 if (backend.has!DubComponent(workspaceRoot)) 1695 { 1696 success = backend.get!DubComponent(workspaceRoot).update.getYield; 1697 if (success) 1698 rpc.notifyMethod("coded/updateDubTree"); 1699 } 1700 backend.get!DCDComponent(workspaceRoot).refreshImports(); 1701 return success; 1702 } 1703 1704 @protocolMethod("served/listDependencies") 1705 DubDependency[] listDependencies(string packageName) 1706 { 1707 auto workspaceRoot = selectedWorkspaceRoot; 1708 DubDependency[] ret; 1709 auto allDeps = backend.get!DubComponent(workspaceRoot).dependencies; 1710 if (!packageName.length) 1711 { 1712 auto deps = backend.get!DubComponent(workspaceRoot).rootDependencies; 1713 foreach (dep; deps) 1714 { 1715 DubDependency r; 1716 r.name = dep; 1717 r.root = true; 1718 foreach (other; allDeps) 1719 if (other.name == dep) 1720 { 1721 r.version_ = other.ver; 1722 r.path = other.path; 1723 r.description = other.description; 1724 r.homepage = other.homepage; 1725 r.authors = other.authors; 1726 r.copyright = other.copyright; 1727 r.license = other.license; 1728 r.subPackages = other.subPackages.map!"a.name".array; 1729 r.hasDependencies = other.dependencies.length > 0; 1730 break; 1731 } 1732 ret ~= r; 1733 } 1734 } 1735 else 1736 { 1737 string[string] aa; 1738 foreach (other; allDeps) 1739 if (other.name == packageName) 1740 { 1741 aa = other.dependencies; 1742 break; 1743 } 1744 foreach (name, ver; aa) 1745 { 1746 DubDependency r; 1747 r.name = name; 1748 r.version_ = ver; 1749 foreach (other; allDeps) 1750 if (other.name == name) 1751 { 1752 r.path = other.path; 1753 r.description = other.description; 1754 r.homepage = other.homepage; 1755 r.authors = other.authors; 1756 r.copyright = other.copyright; 1757 r.license = other.license; 1758 r.subPackages = other.subPackages.map!"a.name".array; 1759 r.hasDependencies = other.dependencies.length > 0; 1760 break; 1761 } 1762 ret ~= r; 1763 } 1764 } 1765 return ret; 1766 } 1767 1768 // === Protocol Notifications starting here === 1769 1770 struct FileOpenInfo 1771 { 1772 SysTime at; 1773 } 1774 1775 __gshared FileOpenInfo[string] freshlyOpened; 1776 1777 @protocolNotification("workspace/didChangeWatchedFiles") 1778 void onChangeFiles(DidChangeWatchedFilesParams params) 1779 { 1780 foreach (change; params.changes) 1781 { 1782 string file = change.uri; 1783 if (change.type == FileChangeType.created && file.endsWith(".d")) 1784 { 1785 auto document = documents[file]; 1786 auto isNew = file in freshlyOpened; 1787 info(file); 1788 if (isNew) 1789 { 1790 // Only edit if creation & opening is < 800msecs apart (vscode automatically opens on creation), 1791 // we don't want to affect creation from/in other programs/editors. 1792 if (Clock.currTime - isNew.at > 800.msecs) 1793 { 1794 freshlyOpened.remove(file); 1795 continue; 1796 } 1797 // Sending applyEdit so it is undoable 1798 auto patches = backend.get!ModulemanComponent.normalizeModules(file.uriToFile, 1799 document.text); 1800 if (patches.length) 1801 { 1802 WorkspaceEdit edit; 1803 edit.changes[file] = patches.map!(a => TextEdit(TextRange(document.bytesToPosition(a.range[0]), 1804 document.bytesToPosition(a.range[1])), a.content)).array; 1805 rpc.sendMethod("workspace/applyEdit", ApplyWorkspaceEditParams(edit)); 1806 } 1807 } 1808 } 1809 } 1810 } 1811 1812 @protocolNotification("workspace/didChangeWorkspaceFolders") 1813 void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) 1814 { 1815 foreach (toRemove; params.event.removed) 1816 removeWorkspace(toRemove.uri); 1817 foreach (toAdd; params.event.added) 1818 { 1819 workspaces ~= Workspace(toAdd); 1820 syncConfiguration(toAdd.uri); 1821 doStartup(toAdd.uri); 1822 } 1823 } 1824 1825 @protocolNotification("textDocument/didOpen") 1826 void onDidOpenDocument(DidOpenTextDocumentParams params) 1827 { 1828 freshlyOpened[params.textDocument.uri] = FileOpenInfo(Clock.currTime); 1829 } 1830 1831 int changeTimeout; 1832 @protocolNotification("textDocument/didChange") 1833 void onDidChangeDocument(DocumentLinkParams params) 1834 { 1835 auto document = documents[params.textDocument.uri]; 1836 if (document.languageId != "d") 1837 return; 1838 int delay = document.text.length > 50 * 1024 ? 1000 : 200; // be slower after 50KiB 1839 clearTimeout(changeTimeout); 1840 changeTimeout = setTimeout({ 1841 import served.linters.dscanner; 1842 1843 lint(document); 1844 // Delay to avoid too many requests 1845 }, delay); 1846 } 1847 1848 @protocolNotification("textDocument/didSave") 1849 void onDidSaveDocument(DidSaveTextDocumentParams params) 1850 { 1851 auto workspaceRoot = workspaceRootFor(params.textDocument.uri); 1852 auto config = workspace(params.textDocument.uri).config; 1853 auto document = documents[params.textDocument.uri]; 1854 auto fileName = params.textDocument.uri.uriToFile.baseName; 1855 1856 if (document.languageId == "d" || document.languageId == "diet") 1857 { 1858 if (!config.d.enableLinting) 1859 return; 1860 joinAll({ 1861 if (config.d.enableStaticLinting) 1862 { 1863 if (document.languageId == "diet") 1864 return; 1865 import served.linters.dscanner; 1866 1867 lint(document); 1868 } 1869 }, { 1870 if (backend.has!DubComponent && config.d.enableDubLinting) 1871 { 1872 import served.linters.dub; 1873 1874 lint(document); 1875 } 1876 }); 1877 } 1878 else if (fileName == "dub.json" || fileName == "dub.sdl") 1879 { 1880 info("Updating dependencies"); 1881 rpc.window.runOrMessage(backend.get!DubComponent(workspaceRoot).upgrade(), 1882 MessageType.warning, translate!"d.ext.dubUpgradeFail"); 1883 rpc.window.runOrMessage(backend.get!DubComponent(workspaceRoot) 1884 .updateImportPaths(true), MessageType.warning, translate!"d.ext.dubImportFail"); 1885 rpc.notifyMethod("coded/updateDubTree"); 1886 } 1887 } 1888 1889 @protocolNotification("served/killServer") 1890 void killServer() 1891 { 1892 foreach (instance; backend.instances) 1893 if (instance.has!DCDComponent) 1894 instance.get!DCDComponent.killServer(); 1895 } 1896 1897 @protocolNotification("served/installDependency") 1898 void installDependency(InstallRequest req) 1899 { 1900 auto workspaceRoot = selectedWorkspaceRoot; 1901 injectDependency(workspaceRoot, req); 1902 if (backend.has!DubComponent) 1903 { 1904 backend.get!DubComponent(workspaceRoot).upgrade(); 1905 backend.get!DubComponent(workspaceRoot).updateImportPaths(true); 1906 } 1907 updateImports(); 1908 } 1909 1910 @protocolNotification("served/updateDependency") 1911 void updateDependency(UpdateRequest req) 1912 { 1913 auto workspaceRoot = selectedWorkspaceRoot; 1914 if (changeDependency(workspaceRoot, req)) 1915 { 1916 if (backend.has!DubComponent) 1917 { 1918 backend.get!DubComponent(workspaceRoot).upgrade(); 1919 backend.get!DubComponent(workspaceRoot).updateImportPaths(true); 1920 } 1921 updateImports(); 1922 } 1923 } 1924 1925 @protocolNotification("served/uninstallDependency") 1926 void uninstallDependency(UninstallRequest req) 1927 { 1928 auto workspaceRoot = selectedWorkspaceRoot; 1929 // TODO: add workspace argument 1930 removeDependency(workspaceRoot, req.name); 1931 if (backend.has!DubComponent) 1932 { 1933 backend.get!DubComponent(workspaceRoot).upgrade(); 1934 backend.get!DubComponent(workspaceRoot).updateImportPaths(true); 1935 } 1936 updateImports(); 1937 } 1938 1939 void injectDependency(string workspaceRoot, InstallRequest req) 1940 { 1941 auto sdl = buildPath(workspaceRoot, "dub.sdl"); 1942 if (fs.exists(sdl)) 1943 { 1944 int depth = 0; 1945 auto content = fs.readText(sdl).splitLines(KeepTerminator.yes); 1946 auto insertAt = content.length; 1947 bool gotLineEnding = false; 1948 string lineEnding = "\n"; 1949 foreach (i, line; content) 1950 { 1951 if (!gotLineEnding && line.length >= 2) 1952 { 1953 lineEnding = line[$ - 2 .. $]; 1954 if (lineEnding[0] != '\r') 1955 lineEnding = line[$ - 1 .. $]; 1956 gotLineEnding = true; 1957 } 1958 if (depth == 0 && line.strip.startsWith("dependency ")) 1959 insertAt = i + 1; 1960 depth += line.count('{') - line.count('}'); 1961 } 1962 content = content[0 .. insertAt] ~ ((insertAt == content.length ? lineEnding 1963 : "") ~ "dependency \"" ~ req.name ~ "\" version=\"~>" ~ req.version_ ~ "\"" ~ lineEnding) 1964 ~ content[insertAt .. $]; 1965 fs.write(sdl, content.join()); 1966 } 1967 else 1968 { 1969 auto json = buildPath(workspaceRoot, "dub.json"); 1970 if (!fs.exists(json)) 1971 json = buildPath(workspaceRoot, "package.json"); 1972 if (!fs.exists(json)) 1973 return; 1974 auto content = fs.readText(json).splitLines(KeepTerminator.yes); 1975 auto insertAt = content.length ? content.length - 1 : 0; 1976 string lineEnding = "\n"; 1977 bool gotLineEnding = false; 1978 int depth = 0; 1979 bool insertNext; 1980 string indent; 1981 bool foundBlock; 1982 foreach (i, line; content) 1983 { 1984 if (!gotLineEnding && line.length >= 2) 1985 { 1986 lineEnding = line[$ - 2 .. $]; 1987 if (lineEnding[0] != '\r') 1988 lineEnding = line[$ - 1 .. $]; 1989 gotLineEnding = true; 1990 } 1991 if (insertNext) 1992 { 1993 indent = line[0 .. $ - line.stripLeft.length]; 1994 insertAt = i + 1; 1995 break; 1996 } 1997 if (depth == 1 && line.strip.startsWith(`"dependencies":`)) 1998 { 1999 foundBlock = true; 2000 if (line.strip.endsWith("{")) 2001 { 2002 indent = line[0 .. $ - line.stripLeft.length]; 2003 insertAt = i + 1; 2004 break; 2005 } 2006 else 2007 { 2008 insertNext = true; 2009 } 2010 } 2011 depth += line.count('{') - line.count('}') + line.count('[') - line.count(']'); 2012 } 2013 if (foundBlock) 2014 { 2015 content = content[0 .. insertAt] ~ ( 2016 indent ~ indent ~ `"` ~ req.name ~ `": "~>` ~ req.version_ ~ `",` ~ lineEnding) 2017 ~ content[insertAt .. $]; 2018 fs.write(json, content.join()); 2019 } 2020 else if (content.length) 2021 { 2022 if (content.length > 1) 2023 content[$ - 2] = content[$ - 2].stripRight; 2024 content = content[0 .. $ - 1] ~ ( 2025 "," ~ lineEnding ~ ` "dependencies": { 2026 "` ~ req.name ~ `": "~>` ~ req.version_ ~ `" 2027 }` ~ lineEnding) 2028 ~ content[$ - 1 .. $]; 2029 fs.write(json, content.join()); 2030 } 2031 else 2032 { 2033 content ~= `{ 2034 "dependencies": { 2035 "` ~ req.name ~ `": "~>` ~ req.version_ ~ `" 2036 } 2037 }`; 2038 fs.write(json, content.join()); 2039 } 2040 } 2041 } 2042 2043 bool changeDependency(string workspaceRoot, UpdateRequest req) 2044 { 2045 auto sdl = buildPath(workspaceRoot, "dub.sdl"); 2046 if (fs.exists(sdl)) 2047 { 2048 int depth = 0; 2049 auto content = fs.readText(sdl).splitLines(KeepTerminator.yes); 2050 size_t target = size_t.max; 2051 foreach (i, line; content) 2052 { 2053 if (depth == 0 && line.strip.startsWith("dependency ") 2054 && line.strip["dependency".length .. $].strip.startsWith('"' ~ req.name ~ '"')) 2055 { 2056 target = i; 2057 break; 2058 } 2059 depth += line.count('{') - line.count('}'); 2060 } 2061 if (target == size_t.max) 2062 return false; 2063 auto ver = content[target].indexOf("version"); 2064 if (ver == -1) 2065 return false; 2066 auto quotStart = content[target].indexOf("\"", ver); 2067 if (quotStart == -1) 2068 return false; 2069 auto quotEnd = content[target].indexOf("\"", quotStart + 1); 2070 if (quotEnd == -1) 2071 return false; 2072 content[target] = content[target][0 .. quotStart] ~ '"' ~ req.version_ ~ '"' 2073 ~ content[target][quotEnd .. $]; 2074 fs.write(sdl, content.join()); 2075 return true; 2076 } 2077 else 2078 { 2079 auto json = buildPath(workspaceRoot, "dub.json"); 2080 if (!fs.exists(json)) 2081 json = buildPath(workspaceRoot, "package.json"); 2082 if (!fs.exists(json)) 2083 return false; 2084 auto content = fs.readText(json); 2085 auto replaced = content.replaceFirst(regex(`("` ~ req.name ~ `"\s*:\s*)"[^"]*"`), 2086 `$1"` ~ req.version_ ~ `"`); 2087 if (content == replaced) 2088 return false; 2089 fs.write(json, replaced); 2090 return true; 2091 } 2092 } 2093 2094 bool removeDependency(string workspaceRoot, string name) 2095 { 2096 auto sdl = buildPath(workspaceRoot, "dub.sdl"); 2097 if (fs.exists(sdl)) 2098 { 2099 int depth = 0; 2100 auto content = fs.readText(sdl).splitLines(KeepTerminator.yes); 2101 size_t target = size_t.max; 2102 foreach (i, line; content) 2103 { 2104 if (depth == 0 && line.strip.startsWith("dependency ") 2105 && line.strip["dependency".length .. $].strip.startsWith('"' ~ name ~ '"')) 2106 { 2107 target = i; 2108 break; 2109 } 2110 depth += line.count('{') - line.count('}'); 2111 } 2112 if (target == size_t.max) 2113 return false; 2114 fs.write(sdl, (content[0 .. target] ~ content[target + 1 .. $]).join()); 2115 return true; 2116 } 2117 else 2118 { 2119 auto json = buildPath(workspaceRoot, "dub.json"); 2120 if (!fs.exists(json)) 2121 json = buildPath(workspaceRoot, "package.json"); 2122 if (!fs.exists(json)) 2123 return false; 2124 auto content = fs.readText(json); 2125 auto replaced = content.replaceFirst(regex(`"` ~ name ~ `"\s*:\s*"[^"]*"\s*,\s*`), ""); 2126 if (content == replaced) 2127 replaced = content.replaceFirst(regex(`\s*,\s*"` ~ name ~ `"\s*:\s*"[^"]*"`), ""); 2128 if (content == replaced) 2129 replaced = content.replaceFirst(regex( 2130 `"dependencies"\s*:\s*\{\s*"` ~ name ~ `"\s*:\s*"[^"]*"\s*\}\s*,\s*`), ""); 2131 if (content == replaced) 2132 replaced = content.replaceFirst(regex( 2133 `\s*,\s*"dependencies"\s*:\s*\{\s*"` ~ name ~ `"\s*:\s*"[^"]*"\s*\}`), ""); 2134 if (content == replaced) 2135 return false; 2136 fs.write(json, replaced); 2137 return true; 2138 } 2139 } 2140 2141 struct Timeout 2142 { 2143 StopWatch sw; 2144 Duration timeout; 2145 void delegate() callback; 2146 int id; 2147 } 2148 2149 int setTimeout(void delegate() callback, int ms) 2150 { 2151 return setTimeout(callback, ms.msecs); 2152 } 2153 2154 void setImmediate(void delegate() callback) 2155 { 2156 setTimeout(callback, 0); 2157 } 2158 2159 int setTimeout(void delegate() callback, Duration timeout) 2160 { 2161 trace("Setting timeout for ", timeout); 2162 Timeout to; 2163 to.timeout = timeout; 2164 to.callback = callback; 2165 to.sw.start(); 2166 to.id = ++timeoutID; 2167 synchronized (timeoutsMutex) 2168 timeouts ~= to; 2169 return to.id; 2170 } 2171 2172 void clearTimeout(int id) 2173 { 2174 synchronized (timeoutsMutex) 2175 foreach_reverse (i, ref timeout; timeouts) 2176 { 2177 if (timeout.id == id) 2178 { 2179 timeout.sw.stop(); 2180 if (timeouts.length > 1) 2181 timeouts[i] = timeouts[$ - 1]; 2182 timeouts.length--; 2183 return; 2184 } 2185 } 2186 } 2187 2188 __gshared void delegate(void delegate()) spawnFiber; 2189 2190 shared static this() 2191 { 2192 spawnFiber = (&setImmediate).toDelegate; 2193 backend = new WorkspaceD(); 2194 2195 backend.onBroadcast = (&handleBroadcast).toDelegate; 2196 backend.onBindFail = (WorkspaceD.Instance instance, ComponentFactory factory) { 2197 rpc.window.showErrorMessage( 2198 "Failed to load component " ~ factory.info.name ~ " for workspace " ~ instance.cwd); 2199 }; 2200 } 2201 2202 __gshared int timeoutID; 2203 __gshared Timeout[] timeouts; 2204 __gshared Mutex timeoutsMutex; 2205 2206 // Called at most 100x per second 2207 void parallelMain() 2208 { 2209 timeoutsMutex = new Mutex; 2210 while (true) 2211 { 2212 synchronized (timeoutsMutex) 2213 foreach_reverse (i, ref timeout; timeouts) 2214 { 2215 if (timeout.sw.peek >= timeout.timeout) 2216 { 2217 timeout.sw.stop(); 2218 timeout.callback(); 2219 trace("Calling timeout"); 2220 if (timeouts.length > 1) 2221 timeouts[i] = timeouts[$ - 1]; 2222 timeouts.length--; 2223 } 2224 } 2225 Fiber.yield(); 2226 } 2227 }