Skip to content

Commit 6169ce2

Browse files
committed
feat(lsp): per-server codeIntelligence.* pref gates and showParameterHints pref
- new showParameterHints pref (default true) gating the parameter hint popup for implicit and explicit invocation, dismissing a visible popup the moment it flips off - every language server now has a codeIntelligence.<serverId> off-switch: LSPClient gates registerLanguageServer on it and auto-defines the pref for plugin-supplied servers so they get a discoverable toggle for free - runtime toggling handled centrally in LSPClient: pref off stops the server process (fallback linters take over), pref on restarts in place; restartLanguageServer/changeWorkspaceRoot/crash auto-restart guarded so a disabled server can never resurrect - new typescript/json master prefs; php/python prefs renamed to the new codeIntelligence.* family (dev-only, no aliases needed) - pref-off stop no longer logged as a crash (serverExit race with the _stopping flag) - specs: LSPClient gate + auto-define, stop/block-resurrection/restart, showParameterHints gating, json toggle round-trip
1 parent 93f1812 commit 6169ce2

9 files changed

Lines changed: 249 additions & 8 deletions

File tree

src/extensions/default/PHPSupport/ServerInstaller.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ define(function (require, exports, module) {
6767
"stale build? Run npm run build.");
6868
}
6969
const INTELEPHENSE_VERSION = _PINS.intelephense || "1.18.5";
70-
const PREF_PHP_CODE_INTELLIGENCE = "php.codeIntelligence";
70+
const PREF_PHP_CODE_INTELLIGENCE = "codeIntelligence.php";
7171

7272
let _onInstalled = null; // main.js callback: ({entryPath, upgraded}) => void
7373
let _inFlight = null; // single-flight install promise

src/extensions/default/PythonSupport/ServerInstaller.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ define(function (require, exports, module) {
7474
}
7575
const PYREFLY_VERSION = _PINS.pyrefly || "1.1.1";
7676
const RUFF_VERSION = _PINS.ruff || "0.15.20";
77-
const PREF_PYTHON_CODE_INTELLIGENCE = "python.codeIntelligence";
77+
const PREF_PYTHON_CODE_INTELLIGENCE = "codeIntelligence.python";
7878

7979
// The independently pinned/installed pieces. `pkg` is the PyPI package name; the binary of a
8080
// wheel build lives at <pkg>-<version>.data/scripts/<pkg>[.exe].

src/extensions/default/TypeScriptSupport/main.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,21 @@ define(function (require, exports, module) {
3737
FileSystem = brackets.getModule("filesystem/FileSystem"),
3838
NodeConnector = brackets.getModule("NodeConnector"),
3939
TokenUtils = brackets.getModule("utils/TokenUtils"),
40+
PreferencesManager = brackets.getModule("preferences/PreferencesManager"),
41+
Strings = brackets.getModule("strings"),
4042
CodeIntelligence = require("./CodeIntelligence"),
4143
ConfigPanel = require("./ConfigPanel");
4244

4345
const SERVER_ID = "typescript";
4446
const SUPPORTED_LANGUAGES = ["javascript", "typescript", "jsx", "tsx"];
47+
const PREF_TS_CODE_INTELLIGENCE = "codeIntelligence." + SERVER_ID;
48+
49+
// Master switch for JS/TS code intelligence - setting it back to true starts the server on
50+
// the open JS/TS file. A runtime disable of a running server is handled centrally by
51+
// LSPClient's own watcher on this pref.
52+
PreferencesManager.definePreference(PREF_TS_CODE_INTELLIGENCE, "boolean", true, {
53+
description: Strings.DESCRIPTION_TYPESCRIPT_CODE_INTELLIGENCE
54+
});
4555

4656
// Phoenix language id -> LSP languageId
4757
const LANGUAGE_ID_MAP = {
@@ -417,6 +427,9 @@ define(function (require, exports, module) {
417427
if (!canRun() || !_isServedLanguageActive()) {
418428
return;
419429
}
430+
if (PreferencesManager.get(PREF_TS_CODE_INTELLIGENCE) === false) {
431+
return;
432+
}
420433

421434
// Not running yet: lazily start it (a fresh start already points at the current project root).
422435
if (!registered) {
@@ -474,6 +487,14 @@ define(function (require, exports, module) {
474487
EditorManager.on("activeEditorChange", _ensureServerForActiveEditor);
475488
_ensureServerForActiveEditor();
476489

490+
// Re-enabling the pref starts the server on the open JS/TS file without needing a file
491+
// switch. The disable direction is handled by LSPClient stopping the running server.
492+
PreferencesManager.on("change", PREF_TS_CODE_INTELLIGENCE, function () {
493+
if (PreferencesManager.get(PREF_TS_CODE_INTELLIGENCE) !== false) {
494+
_ensureServerForActiveEditor();
495+
}
496+
});
497+
477498
// On project switch: re-evaluate checkJs and arm a one-shot repoint. The actual repoint
478499
// (workspace/didChangeWorkspaceFolders, no restart) happens the next time a served-language
479500
// file is active - here if one already is, otherwise on the activeEditorChange as the new

src/extensions/default/TypeScriptSupport/unittests.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,5 +780,89 @@ define(function (require, exports, module) {
780780
expect($("#reference-in-files-results").is(":visible")).toBe(true);
781781
}, 75000);
782782
});
783+
784+
// ----- codeIntelligence.* server gates + showParameterHints -----
785+
describe("preference gates", function () {
786+
let PreferencesManager, LSPClient;
787+
788+
beforeAll(function () {
789+
PreferencesManager = testWindow.brackets.test.PreferencesManager;
790+
LSPClient = testWindow.require("languageTools/LSPClient");
791+
});
792+
793+
afterEach(function () {
794+
// restore defaults even when a spec failed mid-way
795+
PreferencesManager.set("codeIntelligence.typescript", true);
796+
PreferencesManager.set("showParameterHints", true);
797+
});
798+
799+
it("should gate registerLanguageServer on codeIntelligence.<serverId> and auto-define the pref",
800+
async function () {
801+
PreferencesManager.set("codeIntelligence.fakeGateSpec", false);
802+
const client = await LSPClient.registerLanguageServer({
803+
serverId: "fakeGateSpec",
804+
command: "no-such-lsp-binary",
805+
languages: ["javascript"]
806+
});
807+
expect(client).toBe(null);
808+
// gated before any spawn attempt, but the pref is still auto-defined so
809+
// plugin-supplied servers get a discoverable off-switch with a description
810+
const pref = PreferencesManager.getPreference("codeIntelligence.fakeGateSpec");
811+
expect(pref).toBeTruthy();
812+
expect(pref.description).toContain("fakeGateSpec");
813+
PreferencesManager.set("codeIntelligence.fakeGateSpec", undefined);
814+
});
815+
816+
it("should stop the running server on pref off, block resurrection, and restart on pref on",
817+
async function () {
818+
await _openInProject("ts/", "type-error.ts");
819+
await awaitsFor(function () {
820+
return LSPClient.isLintingProviderActive("typescript");
821+
}, "typescript server to be active", 30000);
822+
823+
PreferencesManager.set("codeIntelligence.typescript", false);
824+
await awaitsFor(function () {
825+
return !LSPClient.isLintingProviderActive("typescript");
826+
}, "typescript server to stop on pref off", 15000);
827+
828+
// neither restart path may resurrect a pref-disabled server
829+
await LSPClient.restartLanguageServer("typescript");
830+
await LSPClient.changeWorkspaceRoot("typescript");
831+
expect(LSPClient.isLintingProviderActive("typescript")).toBe(false);
832+
833+
PreferencesManager.set("codeIntelligence.typescript", true);
834+
await awaitsFor(function () {
835+
return LSPClient.isLintingProviderActive("typescript");
836+
}, "typescript server to restart on pref on", 45000);
837+
}, 90000);
838+
839+
it("should gate the parameter hint popup on showParameterHints", async function () {
840+
await _openInProject("ts/", "type-error.ts");
841+
const editor = EditorManager.getActiveEditor();
842+
editor.document.setText("function add(a: number, b: number) { return a + b; }\nadd(1, 2);\n");
843+
editor.setCursorPos(1, 4); // inside add(|
844+
CommandManager.execute("showParameterHint");
845+
await awaitsFor(function () {
846+
return $("#function-hint-container-new").is(":visible");
847+
}, "parameter hint popup to show", 30000);
848+
849+
// flipping the pref off dismisses the visible popup
850+
PreferencesManager.set("showParameterHints", false);
851+
await awaitsFor(function () {
852+
return !$("#function-hint-container-new").is(":visible");
853+
}, "popup to dismiss on pref off", 5000);
854+
855+
// while off the gate is synchronous - no request is even issued, so the popup
856+
// must still be hidden right after an explicit invocation
857+
CommandManager.execute("showParameterHint");
858+
expect($("#function-hint-container-new").is(":visible")).toBe(false);
859+
860+
PreferencesManager.set("showParameterHints", true);
861+
CommandManager.execute("showParameterHint");
862+
await awaitsFor(function () {
863+
return $("#function-hint-container-new").is(":visible");
864+
}, "parameter hint popup to show again", 30000);
865+
}, 90000);
866+
});
783867
});
784868
});

src/extensionsIntegrated/JSONSupport/JsonLsp.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,20 @@ define(function (require, exports, module) {
3737
const EditorManager = require("editor/EditorManager"),
3838
ProjectManager = require("project/ProjectManager"),
3939
NodeConnector = require("NodeConnector"),
40+
PreferencesManager = require("preferences/PreferencesManager"),
41+
Strings = require("strings"),
4042
SchemaAssociations = require("./schemaAssociations");
4143

4244
const SERVER_ID = "json";
4345
const SUPPORTED_LANGUAGES = ["json"];
46+
const PREF_JSON_CODE_INTELLIGENCE = "codeIntelligence." + SERVER_ID;
47+
48+
// Master switch for JSON code intelligence - setting it back to true starts the server on
49+
// the open JSON file. A runtime disable of a running server is handled centrally by
50+
// LSPClient's own watcher on this pref.
51+
PreferencesManager.definePreference(PREF_JSON_CODE_INTELLIGENCE, "boolean", true, {
52+
description: Strings.DESCRIPTION_JSON_CODE_INTELLIGENCE
53+
});
4454

4555
let lspClientPromise = null;
4656
let client = null; // the LanguageClient once registered
@@ -181,6 +191,9 @@ define(function (require, exports, module) {
181191
if (!canRun() || !_isServedLanguageActive()) {
182192
return;
183193
}
194+
if (PreferencesManager.get(PREF_JSON_CODE_INTELLIGENCE) === false) {
195+
return;
196+
}
184197
if (!registered) {
185198
if (starting) {
186199
return;
@@ -219,6 +232,13 @@ define(function (require, exports, module) {
219232
pendingRepoint = true;
220233
_ensureServerForActiveEditor();
221234
});
235+
// Re-enabling the pref starts the server on the open JSON file without needing a file
236+
// switch. The disable direction is handled by LSPClient stopping the running server.
237+
PreferencesManager.on("change", PREF_JSON_CODE_INTELLIGENCE, function () {
238+
if (PreferencesManager.get(PREF_JSON_CODE_INTELLIGENCE) !== false) {
239+
_ensureServerForActiveEditor();
240+
}
241+
});
222242
}
223243

224244
/**

src/features/ParameterHintsManager.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ define(function (require, exports, module) {
3333
EditorManager = require("editor/EditorManager"),
3434
Menus = require("command/Menus"),
3535
Strings = require("strings"),
36+
PreferencesManager = require("preferences/PreferencesManager"),
3637
WorkspaceManager = require("view/WorkspaceManager"),
3738
ProviderRegistrationHandler = require("features/PriorityBasedRegistration").RegistrationHandler;
3839

@@ -89,6 +90,19 @@ define(function (require, exports, module) {
8990
registerHintProvider = _providerRegistrationHandler.registerProvider.bind(_providerRegistrationHandler),
9091
removeHintProvider = _providerRegistrationHandler.removeProvider.bind(_providerRegistrationHandler);
9192

93+
let paramHintsEnabled = true;
94+
95+
PreferencesManager.definePreference("showParameterHints", "boolean", true, {
96+
description: Strings.DESCRIPTION_SHOW_PARAMETER_HINTS
97+
});
98+
99+
PreferencesManager.on("change", "showParameterHints", function () {
100+
paramHintsEnabled = PreferencesManager.get("showParameterHints");
101+
if (!paramHintsEnabled) {
102+
dismissHint();
103+
}
104+
});
105+
92106
/**
93107
* Keep the active parameter visible inside the single-line, width-capped popup: scroll it
94108
* into view (centered) when the signature overflows, and fade whichever edge is clipped so
@@ -322,6 +336,13 @@ define(function (require, exports, module) {
322336
let $deferredPopUp = $.Deferred();
323337
let sessionProvider = null;
324338

339+
// Gates implicit ("("/"," typed, cursor tracking) and explicit (Ctrl-Shift-Space)
340+
// invocation alike - same semantics as showCodeHints gating code hint sessions.
341+
if (!paramHintsEnabled) {
342+
dismissHint(editor);
343+
return $deferredPopUp;
344+
}
345+
325346
popupShown = true;
326347
// Find a suitable provider, if any
327348
let language = editor.getLanguageForSelection(),

src/languageTools/LSPClient.js

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ define(function (require, exports, module) {
6565
FindReferencesManager = require("features/FindReferencesManager"),
6666
QuickViewManager = require("features/QuickViewManager"),
6767
CodeInspection = require("language/CodeInspection"),
68-
EventDispatcher = require("utils/EventDispatcher");
68+
EventDispatcher = require("utils/EventDispatcher"),
69+
PreferencesManager = require("preferences/PreferencesManager"),
70+
Strings = require("strings"),
71+
StringUtils = require("utils/StringUtils");
6972

7073
EventDispatcher.makeEventDispatcher(exports);
7174

@@ -229,8 +232,11 @@ define(function (require, exports, module) {
229232
}
230233
client.capabilities = null;
231234
DocumentSync.clearServer(client);
232-
if (client._stopping) {
233-
return; // Intentional stop/restart - do not auto-restart here.
235+
if (client._stopping || _isDisabledByPref(client.serverId)) {
236+
// Intentional stop/restart - do not auto-restart here. The pref check also covers
237+
// the pref-off stop: its exit event can land after stopServerProcess resolved (and
238+
// reset _stopping), which would otherwise read as a crash and bump _crashCount.
239+
return;
234240
}
235241
// Unexpected crash - log it loudly (with the server's stderr) so failures are never
236242
// silent, then self-heal with a bounded backoff to recover without a reload.
@@ -243,7 +249,8 @@ define(function (require, exports, module) {
243249
return;
244250
}
245251
setTimeout(function () {
246-
if (!clients.has(client.serverId) || client.capabilities) {
252+
if (!clients.has(client.serverId) || client.capabilities ||
253+
_isDisabledByPref(client.serverId)) {
247254
return;
248255
}
249256
_startAndInit(client).then(function () {
@@ -808,6 +815,52 @@ define(function (require, exports, module) {
808815
CodeInspection.requestRun();
809816
}
810817

818+
/**
819+
* Every registered server gets a `codeIntelligence.<serverId>` boolean preference (defined by
820+
* its extension, or auto-defined here for plugin-supplied servers) - the durable off-switch
821+
* for the whole server: hints, parameter hints, jump-to-def, references, hover, diagnostics.
822+
* @param {string} serverId
823+
* @return {string} the preference key
824+
*/
825+
function _codeIntelPrefKey(serverId) {
826+
return "codeIntelligence." + serverId;
827+
}
828+
829+
function _isDisabledByPref(serverId) {
830+
return PreferencesManager.get(_codeIntelPrefKey(serverId)) === false;
831+
}
832+
833+
// serverIds whose codeIntelligence pref already has a change listener attached - the pref
834+
// outlives the client entry (registration can be retried), so listeners attach only once.
835+
const _prefWatchedServers = new Set();
836+
837+
/**
838+
* Live-toggle a registered server from its `codeIntelligence.<serverId>` pref: off stops the
839+
* server process (providers go dormant without capabilities, and inspection re-runs so a
840+
* fallback linter can take over); on restarts it in place.
841+
*/
842+
function _watchServerPref(serverId) {
843+
if (_prefWatchedServers.has(serverId)) {
844+
return;
845+
}
846+
_prefWatchedServers.add(serverId);
847+
PreferencesManager.on("change", _codeIntelPrefKey(serverId), function () {
848+
const client = clients.get(serverId);
849+
if (!client) {
850+
return;
851+
}
852+
if (_isDisabledByPref(serverId)) {
853+
if (client.capabilities) {
854+
stopServerProcess(client).then(function () {
855+
CodeInspection.requestRun();
856+
});
857+
}
858+
} else {
859+
restartLanguageServer(serverId);
860+
}
861+
});
862+
}
863+
811864
/**
812865
* Register and start a language server, wiring all providers into the editor.
813866
*
@@ -851,6 +904,18 @@ define(function (require, exports, module) {
851904
if (clients.has(config.serverId)) {
852905
return clients.get(config.serverId);
853906
}
907+
if (!PreferencesManager.getPreference(_codeIntelPrefKey(config.serverId))) {
908+
// Auto-define so plugin-supplied servers get a discoverable pref with a description.
909+
// Built-ins (typescript/json/php/python) define theirs with richer descriptions
910+
// before registering, so this is skipped for them.
911+
PreferencesManager.definePreference(_codeIntelPrefKey(config.serverId), "boolean", true, {
912+
description: StringUtils.format(Strings.DESCRIPTION_LSP_CODE_INTELLIGENCE, config.serverId)
913+
});
914+
}
915+
_watchServerPref(config.serverId);
916+
if (_isDisabledByPref(config.serverId)) {
917+
return null;
918+
}
854919
const client = new LanguageClient(config.serverId, config.languages, config);
855920
// Register eagerly so a publishDiagnostics arriving during init is not dropped.
856921
clients.set(config.serverId, client);
@@ -910,7 +975,9 @@ define(function (require, exports, module) {
910975
*/
911976
async function changeWorkspaceRoot(serverId) {
912977
const client = clients.get(serverId);
913-
if (!client) {
978+
if (!client || _isDisabledByPref(serverId)) {
979+
// A pref-disabled server must stay down - the restart fallbacks below would
980+
// otherwise resurrect it on a project switch.
914981
return;
915982
}
916983
// Resolve the target root FIRST: a redundant call for the same root must be a cheap no-op and
@@ -952,7 +1019,7 @@ define(function (require, exports, module) {
9521019

9531020
async function restartLanguageServer(serverId) {
9541021
const client = clients.get(serverId);
955-
if (!client) {
1022+
if (!client || _isDisabledByPref(serverId)) {
9561023
return;
9571024
}
9581025
await stopServerProcess(client);

src/nls/root/strings.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1692,6 +1692,7 @@ define({
16921692
"DESCRIPTION_MOUSE_WHEEL_SCROLL_SENSITIVITY": "Multiplier for mouse wheel scroll speed (0.1 to 10, default 1). Adjust if scrolling feels too slow or fast.",
16931693
"DESCRIPTION_SHOW_CODE_HINTS": "false to disable all code hints",
16941694
"DESCRIPTION_SHOW_CODE_HINT_DOCS": "false to hide the documentation popup shown beside code hints",
1695+
"DESCRIPTION_SHOW_PARAMETER_HINTS": "false to disable the parameter hint popup shown when typing inside a function call",
16951696
"DESCRIPTION_SHOW_CURSOR_WHEN_SELECTING": "Keeps the blinking cursor visible when you have a text selection",
16961697
"DESCRIPTION_SHOW_LINE_NUMBERS": "true to show line numbers in a “gutter” to the left of the code",
16971698
"DESCRIPTION_RULERS_COLUMNS": "An array of column numbers to draw vertical rulers in the editor. Eg: [80, 100]",
@@ -1828,6 +1829,10 @@ define({
18281829
"PYTHON_INSTALL_STOP": "Stop setting up Python support ({APP_NAME} will try again on the next launch)",
18291830
"PYTHON_INSTALL_WAITING_NETWORK": "Waiting for an internet connection to set up Python support…",
18301831
"DESCRIPTION_PYTHON_CODE_INTELLIGENCE": "false to disable Python code intelligence (Pyrefly). Setting it back to true downloads the language server again automatically.",
1832+
// Language server on/off switches (LSPClient and the built-in language support extensions)
1833+
"DESCRIPTION_TYPESCRIPT_CODE_INTELLIGENCE": "false to disable JavaScript/TypeScript code intelligence (completion, hover docs, jump to definition, diagnostics)",
1834+
"DESCRIPTION_JSON_CODE_INTELLIGENCE": "false to disable JSON code intelligence (schema based completion, hover docs and validation)",
1835+
"DESCRIPTION_LSP_CODE_INTELLIGENCE": "false to disable the {0} language server",
18311836
// JSON / package.json intelligence (JSONSupport)
18321837
"NPM_VULN_PROVIDER_NAME": "npm Security Advisories",
18331838
"NPM_VULN_MESSAGE": "{0}@{1} is vulnerable: {2} ({3} severity)",

0 commit comments

Comments
 (0)