Skip to content

Commit 2ebfbcf

Browse files
committed
feat(lsp): start the TypeScript server lazily, repoint only on project switch
The server was spawned eagerly on app boot and repointed on every project switch, regardless of language - so a Python-only project still ran vtsls and re-indexed it on switch. Mirror VS Code's onLanguage model instead: start the server only when a served-language (JS/TS/JSX/TSX) file is the active editor, and repoint (workspace/didChangeWorkspaceFolders) only when a served file is active right after a project switch - never on ordinary file switches. Once started the server stays alive idle (no proactive shutdown). - main.js: _ensureServerForActiveEditor() driven by activeEditorChange + an initial evaluation; a pendingRepoint flag (set on EVENT_PROJECT_OPEN) gates the repoint so file switches don't touch the workspace-folder/restart machinery. Report a start failure via window.logger.reportError, deduped (start is retried lazily). Drop the eager appReady start and the now-meaningless readiness flag. - LSPClient.changeWorkspaceRoot: check same-root first so a redundant call is a no-op and never restarts a server that lacks live workspace-folder support. - tests: the lazy "never started" state isn't observable in the reused integration window (keep-alive, no stop path), so don't assert it; drop the readiness-flag wait (window load already guarantees the extension's appReady ran).
1 parent 24e5a7c commit 2ebfbcf

3 files changed

Lines changed: 84 additions & 29 deletions

File tree

src/extensions/default/TypeScriptSupport/main.js

Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ define(function (require, exports, module) {
3333
const AppInit = brackets.getModule("utils/AppInit"),
3434
ProjectManager = brackets.getModule("project/ProjectManager"),
3535
DocumentManager = brackets.getModule("document/DocumentManager"),
36+
EditorManager = brackets.getModule("editor/EditorManager"),
3637
FileSystem = brackets.getModule("filesystem/FileSystem"),
3738
NodeConnector = brackets.getModule("NodeConnector"),
3839
CodeIntelligence = require("./CodeIntelligence");
@@ -203,8 +204,7 @@ define(function (require, exports, module) {
203204
* @return {boolean}
204205
*/
205206
function canRun() {
206-
return typeof Phoenix !== "undefined" && Phoenix.isNativeApp &&
207-
NodeConnector.isNodeAvailable && NodeConnector.isNodeAvailable();
207+
return Phoenix.isNativeApp && NodeConnector.isNodeAvailable();
208208
}
209209

210210
/**
@@ -252,23 +252,71 @@ define(function (require, exports, module) {
252252
}
253253
}
254254

255-
// Begin loading the LSP framework as soon as the (desktop-only) extension loads - the
256-
// reliable moment for module loading - so it is ready by the time start() runs.
255+
// Begin loading the LSP framework as soon as the (desktop-only) extension loads - the reliable
256+
// moment for module loading - so it is ready by the time we first need it. This only loads the
257+
// module; it does not spawn the server (that happens lazily, on the first served-language file).
257258
if (canRun()) {
258259
loadLSPClient();
259260
}
260261

262+
/**
263+
* True when the active editor holds a language this server handles (JS/TS/JSX/TSX).
264+
* @return {boolean}
265+
*/
266+
function _isServedLanguageActive() {
267+
const editor = EditorManager.getActiveEditor();
268+
return !!(editor && SUPPORTED_LANGUAGES.indexOf(editor.getLanguageForSelection().getId()) !== -1);
269+
}
270+
271+
let starting = false;
272+
let pendingRepoint = false; // a project switch happened; repoint once a served file is active there
273+
let initErrorReported = false; // start() is retried lazily, so report a failure to telemetry only once
274+
275+
/**
276+
* Lazily start the language server when a served-language file is active, and - only right after a
277+
* project switch - repoint the running server at the new root. Mirrors VS Code's onLanguage model:
278+
* a project with no JS/TS file opened never spawns vtsls; switching to a non-JS project leaves the
279+
* idle server where it was; and plain file switches within a project never touch the
280+
* workspace-folder / restart machinery (so they can't interfere with a crash auto-restart).
281+
*/
282+
function _ensureServerForActiveEditor() {
283+
if (!canRun() || !_isServedLanguageActive()) {
284+
return;
285+
}
286+
287+
// Not running yet: lazily start it (a fresh start already points at the current project root).
288+
if (!registered) {
289+
if (starting) {
290+
return; // a start kicked off by a previous activeEditorChange is still in flight
291+
}
292+
starting = true;
293+
pendingRepoint = false;
294+
start().catch(function (err) {
295+
if (!initErrorReported) {
296+
initErrorReported = true;
297+
window.logger && window.logger.reportError(err, "[TypeScriptSupport] LSP init failed");
298+
}
299+
}).finally(function () {
300+
starting = false;
301+
});
302+
return;
303+
}
304+
305+
// Running: repoint at the current project, but only when a project switch armed it - never on
306+
// ordinary file switches.
307+
if (pendingRepoint) {
308+
pendingRepoint = false;
309+
loadLSPClient().then(function (LSPClient) {
310+
LSPClient.changeWorkspaceRoot(SERVER_ID);
311+
});
312+
}
313+
}
314+
261315
AppInit.appReady(function () {
262316
if (!canRun()) {
263317
return;
264318
}
265319
_refreshCheckJs();
266-
start().catch(function (err) {
267-
console.error("[TypeScriptSupport] init failed", err && (err.message || err));
268-
}).finally(function () {
269-
// Signal for integration tests that the server start has been attempted/settled.
270-
window._TypeScriptSupportReadyToIntegTest = true;
271-
});
272320

273321
// Offer project-wide code intelligence (creates a default ts/jsconfig) when a JS/TS file is
274322
// opened in a project that has no config yet. Projects that already carry one are silent.
@@ -283,17 +331,20 @@ define(function (require, exports, module) {
283331
}
284332
});
285333

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.
334+
// Lazily start / repoint the server from the active editor's language (VS Code's onLanguage
335+
// model). Evaluate the editor already open at startup (session restore), then track switches.
336+
EditorManager.on("activeEditorChange", _ensureServerForActiveEditor);
337+
_ensureServerForActiveEditor();
338+
339+
// On project switch: re-evaluate checkJs and arm a one-shot repoint. The actual repoint
340+
// (workspace/didChangeWorkspaceFolders, no restart) happens the next time a served-language
341+
// file is active - here if one already is, otherwise on the activeEditorChange as the new
342+
// project's file opens. Plain file switches within a project never set this, so they don't
343+
// repoint.
290344
ProjectManager.on(ProjectManager.EVENT_PROJECT_OPEN, function () {
291345
_refreshCheckJs();
292-
if (registered) {
293-
loadLSPClient().then(function (LSPClient) {
294-
LSPClient.changeWorkspaceRoot(SERVER_ID);
295-
});
296-
}
346+
pendingRepoint = true;
347+
_ensureServerForActiveEditor();
297348
});
298349

299350
// Pick up a tsconfig/jsconfig being added, edited, or removed at the project root.

src/extensions/default/TypeScriptSupport/unittests.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ define(function (require, exports, module) {
5555
CodeInspection = testWindow.brackets.test.CodeInspection;
5656
QuickViewManager = testWindow.brackets.getModule("features/QuickViewManager");
5757
CodeInspection.toggleEnabled(true);
58-
// Wait until the extension has attempted to start the language server.
59-
await awaitsFor(function () {
60-
return testWindow._TypeScriptSupportReadyToIntegTest;
61-
}, "TypeScript LSP server to start", 30000);
58+
// createTestWindowAndRun already waited for the app (and so the extension's appReady, which
59+
// wires the lazy-start hooks) to finish loading. The server itself starts lazily on the
60+
// first served-language file - the warm-up below opens a .ts, which starts it; waiting for
61+
// its diagnostics ("not assignable") is the real readiness signal.
6262

6363
// Warm up tsserver. Its very first request pays a large one-time cost - spawning node,
6464
// launching vtsls, and loading the TypeScript library + project - which on a slow/loaded

src/languageTools/LSPClient.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,16 @@ define(function (require, exports, module) {
742742
if (!client) {
743743
return;
744744
}
745+
// Resolve the target root FIRST: a redundant call for the same root must be a cheap no-op and
746+
// must never restart (this can be called often - e.g. once per editor switch). This check has
747+
// to precede the restart fallbacks below, or a same-root call to a server that lacks live
748+
// workspace-folder support would pointlessly recycle the process.
749+
const newVfsPath = (client.config.rootUriProvider && client.config.rootUriProvider()) || _projectRootPath();
750+
const newUri = newVfsPath ? pathToServerUri(newVfsPath) : null;
751+
const oldUri = client.rootUri || null;
752+
if (newUri === oldUri) {
753+
return; // same workspace - nothing to do
754+
}
745755
// Not up yet (e.g. the project switched before init finished) - a (re)start picks up the
746756
// current root on its own.
747757
if (!client.capabilities) {
@@ -754,12 +764,6 @@ define(function (require, exports, module) {
754764
if (!supportsLiveChange) {
755765
return restartLanguageServer(serverId);
756766
}
757-
const newVfsPath = (client.config.rootUriProvider && client.config.rootUriProvider()) || _projectRootPath();
758-
const newUri = newVfsPath ? pathToServerUri(newVfsPath) : null;
759-
const oldUri = client.rootUri || null;
760-
if (newUri === oldUri) {
761-
return; // same workspace - nothing to do
762-
}
763767
const conn = await getConnector();
764768
const added = newUri ? [{ uri: newUri, name: FileUtils.getBaseName(newVfsPath) }] : [];
765769
const removed = oldUri ? [{ uri: oldUri, name: client.rootName || FileUtils.getBaseName(oldUri) }] : [];

0 commit comments

Comments
 (0)