Skip to content

Commit e6a7f2b

Browse files
committed
perf(lsp): switch projects via didChangeWorkspaceFolders instead of restart
Restarting the language server on every project switch re-paid tsserver's cold start (spawning node + loading the TypeScript library) each time. When the server advertises workspace.workspaceFolders.changeNotifications, re-point it live with workspace/didChangeWorkspaceFolders (remove old root, add new) instead - no process restart, no cold start. Generic in LSPClient: servers without the capability transparently fall back to a full restart, and restart remains the recovery path for crashes / server-version changes. Verified: A->B project switch ~160ms with cross-file Find Usages working in both projects and no vtsls respawn in the logs.
1 parent bc7b318 commit e6a7f2b

2 files changed

Lines changed: 58 additions & 3 deletions

File tree

src/extensions/default/TypeScriptSupport/main.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -283,13 +283,15 @@ define(function (require, exports, module) {
283283
}
284284
});
285285

286-
// Restart the server against the new workspace root when the project changes, and
287-
// re-evaluate whether the new project type-checks its JS.
286+
// Re-point the server at the new workspace root when the project changes, and re-evaluate
287+
// whether the new project type-checks its JS. This uses workspace/didChangeWorkspaceFolders
288+
// (no process restart, so no tsserver cold start) and only falls back to a full restart for
289+
// servers that don't support live workspace-folder changes.
288290
ProjectManager.on(ProjectManager.EVENT_PROJECT_OPEN, function () {
289291
_refreshCheckJs();
290292
if (registered) {
291293
loadLSPClient().then(function (LSPClient) {
292-
LSPClient.restartLanguageServer(SERVER_ID);
294+
LSPClient.changeWorkspaceRoot(SERVER_ID);
293295
});
294296
}
295297
});

src/languageTools/LSPClient.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,11 @@ define(function (require, exports, module) {
539539
const rootVfsPath = (config.rootUriProvider && config.rootUriProvider()) || _projectRootPath();
540540
const rootUri = rootVfsPath ? pathToServerUri(rootVfsPath) : null;
541541
const rootName = rootVfsPath ? FileUtils.getBaseName(rootVfsPath) : "root";
542+
// Remember the active workspace folder so a later project switch can hand the server the
543+
// delta (removed old, added new) via workspace/didChangeWorkspaceFolders - see
544+
// changeWorkspaceRoot - instead of a full restart.
545+
client.rootUri = rootUri;
546+
client.rootName = rootName;
542547

543548
await conn.execPeer("startServer", {
544549
serverId: client.serverId,
@@ -674,6 +679,53 @@ define(function (require, exports, module) {
674679
});
675680
}
676681

682+
/**
683+
* Re-point a running server at the current project root WITHOUT restarting it, by sending
684+
* `workspace/didChangeWorkspaceFolders` (remove the old folder, add the new one). This avoids
685+
* the cold start a full restart pays on every project switch. Generic: servers that don't
686+
* advertise live workspace-folder change support transparently fall back to a full restart.
687+
* The open documents themselves are re-synced by DocumentSync's normal editor-change handling.
688+
* @param {string} serverId
689+
* @return {Promise<void>}
690+
*/
691+
async function changeWorkspaceRoot(serverId) {
692+
const client = clients.get(serverId);
693+
if (!client) {
694+
return;
695+
}
696+
// Not up yet (e.g. the project switched before init finished) - a (re)start picks up the
697+
// current root on its own.
698+
if (!client.capabilities) {
699+
return restartLanguageServer(serverId);
700+
}
701+
const wf = client.capabilities.workspace && client.capabilities.workspace.workspaceFolders;
702+
// Per the LSP spec changeNotifications is `boolean | string` (a static flag or a dynamic
703+
// registration id); either truthy form means the server accepts live folder changes.
704+
const supportsLiveChange = !!(wf && wf.supported && wf.changeNotifications);
705+
if (!supportsLiveChange) {
706+
return restartLanguageServer(serverId);
707+
}
708+
const newVfsPath = (client.config.rootUriProvider && client.config.rootUriProvider()) || _projectRootPath();
709+
const newUri = newVfsPath ? pathToServerUri(newVfsPath) : null;
710+
const oldUri = client.rootUri || null;
711+
if (newUri === oldUri) {
712+
return; // same workspace - nothing to do
713+
}
714+
const conn = await getConnector();
715+
const added = newUri ? [{ uri: newUri, name: FileUtils.getBaseName(newVfsPath) }] : [];
716+
const removed = oldUri ? [{ uri: oldUri, name: client.rootName || FileUtils.getBaseName(oldUri) }] : [];
717+
await conn.execPeer("sendNotification", {
718+
serverId: serverId,
719+
method: "workspace/didChangeWorkspaceFolders",
720+
params: { event: { added: added, removed: removed } }
721+
});
722+
client.rootUri = newUri;
723+
client.rootName = newVfsPath ? FileUtils.getBaseName(newVfsPath) : null;
724+
// Capabilities are unchanged (no restart), but the active file is now in the new project -
725+
// refresh the find-references menu state for that context.
726+
FindReferencesManager.setMenuItemStateForLanguage();
727+
}
728+
677729
async function restartLanguageServer(serverId) {
678730
const client = clients.get(serverId);
679731
if (!client) {
@@ -723,6 +775,7 @@ define(function (require, exports, module) {
723775

724776
exports.registerLanguageServer = registerLanguageServer;
725777
exports.restartLanguageServer = restartLanguageServer;
778+
exports.changeWorkspaceRoot = changeWorkspaceRoot;
726779
exports.pathToServerUri = pathToServerUri;
727780
exports.serverUriToVfsUri = serverUriToVfsUri;
728781
});

0 commit comments

Comments
 (0)