diff --git a/src/editor/CodeHintManager.js b/src/editor/CodeHintManager.js index c54bb07137..6135b31a23 100644 --- a/src/editor/CodeHintManager.js +++ b/src/editor/CodeHintManager.js @@ -272,6 +272,9 @@ define(function (require, exports, module) { PreferencesManager.definePreference("showCodeHints", "boolean", true, { description: Strings.DESCRIPTION_SHOW_CODE_HINTS }); + PreferencesManager.definePreference("showCodeHintDocs", "boolean", true, { + description: Strings.DESCRIPTION_SHOW_CODE_HINT_DOCS + }); PreferencesManager.definePreference("insertHintOnTab", "boolean", true, { description: Strings.DESCRIPTION_INSERT_HINT_ON_TAB }); diff --git a/src/extensions/default/PHPSupport/ServerInstaller.js b/src/extensions/default/PHPSupport/ServerInstaller.js index c495c499d4..83f20f001d 100644 --- a/src/extensions/default/PHPSupport/ServerInstaller.js +++ b/src/extensions/default/PHPSupport/ServerInstaller.js @@ -67,7 +67,7 @@ define(function (require, exports, module) { "stale build? Run npm run build."); } const INTELEPHENSE_VERSION = _PINS.intelephense || "1.18.5"; - const PREF_PHP_CODE_INTELLIGENCE = "php.codeIntelligence"; + const PREF_PHP_CODE_INTELLIGENCE = "codeIntelligence.php"; let _onInstalled = null; // main.js callback: ({entryPath, upgraded}) => void let _inFlight = null; // single-flight install promise diff --git a/src/extensions/default/PythonSupport/ServerInstaller.js b/src/extensions/default/PythonSupport/ServerInstaller.js index d0500ad117..792ec6bfec 100644 --- a/src/extensions/default/PythonSupport/ServerInstaller.js +++ b/src/extensions/default/PythonSupport/ServerInstaller.js @@ -74,7 +74,7 @@ define(function (require, exports, module) { } const PYREFLY_VERSION = _PINS.pyrefly || "1.1.1"; const RUFF_VERSION = _PINS.ruff || "0.15.20"; - const PREF_PYTHON_CODE_INTELLIGENCE = "python.codeIntelligence"; + const PREF_PYTHON_CODE_INTELLIGENCE = "codeIntelligence.python"; // The independently pinned/installed pieces. `pkg` is the PyPI package name; the binary of a // wheel build lives at -.data/scripts/[.exe]. diff --git a/src/extensions/default/TypeScriptSupport/main.js b/src/extensions/default/TypeScriptSupport/main.js index d6e59fe2d5..f7c5fd1c8c 100644 --- a/src/extensions/default/TypeScriptSupport/main.js +++ b/src/extensions/default/TypeScriptSupport/main.js @@ -37,11 +37,21 @@ define(function (require, exports, module) { FileSystem = brackets.getModule("filesystem/FileSystem"), NodeConnector = brackets.getModule("NodeConnector"), TokenUtils = brackets.getModule("utils/TokenUtils"), + PreferencesManager = brackets.getModule("preferences/PreferencesManager"), + Strings = brackets.getModule("strings"), CodeIntelligence = require("./CodeIntelligence"), ConfigPanel = require("./ConfigPanel"); const SERVER_ID = "typescript"; const SUPPORTED_LANGUAGES = ["javascript", "typescript", "jsx", "tsx"]; + const PREF_TS_CODE_INTELLIGENCE = "codeIntelligence." + SERVER_ID; + + // Master switch for JS/TS code intelligence - setting it back to true starts the server on + // the open JS/TS file. A runtime disable of a running server is handled centrally by + // LSPClient's own watcher on this pref. + PreferencesManager.definePreference(PREF_TS_CODE_INTELLIGENCE, "boolean", true, { + description: Strings.DESCRIPTION_TYPESCRIPT_CODE_INTELLIGENCE + }); // Phoenix language id -> LSP languageId const LANGUAGE_ID_MAP = { @@ -417,6 +427,9 @@ define(function (require, exports, module) { if (!canRun() || !_isServedLanguageActive()) { return; } + if (PreferencesManager.get(PREF_TS_CODE_INTELLIGENCE) === false) { + return; + } // Not running yet: lazily start it (a fresh start already points at the current project root). if (!registered) { @@ -474,6 +487,14 @@ define(function (require, exports, module) { EditorManager.on("activeEditorChange", _ensureServerForActiveEditor); _ensureServerForActiveEditor(); + // Re-enabling the pref starts the server on the open JS/TS file without needing a file + // switch. The disable direction is handled by LSPClient stopping the running server. + PreferencesManager.on("change", PREF_TS_CODE_INTELLIGENCE, function () { + if (PreferencesManager.get(PREF_TS_CODE_INTELLIGENCE) !== false) { + _ensureServerForActiveEditor(); + } + }); + // On project switch: re-evaluate checkJs and arm a one-shot repoint. The actual repoint // (workspace/didChangeWorkspaceFolders, no restart) happens the next time a served-language // file is active - here if one already is, otherwise on the activeEditorChange as the new diff --git a/src/extensions/default/TypeScriptSupport/unittests.js b/src/extensions/default/TypeScriptSupport/unittests.js index 106e82302d..21b08a2aae 100644 --- a/src/extensions/default/TypeScriptSupport/unittests.js +++ b/src/extensions/default/TypeScriptSupport/unittests.js @@ -18,7 +18,7 @@ * */ -/*global describe, it, expect, beforeAll, afterAll, afterEach, awaitsFor, awaitsForDone, path, jsPromise */ +/*global describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, awaitsFor, awaitsForDone, path, jsPromise */ define(function (require, exports, module) { @@ -780,5 +780,171 @@ define(function (require, exports, module) { expect($("#reference-in-files-results").is(":visible")).toBe(true); }, 75000); }); + + // ----- codeIntelligence.* server gates + showParameterHints ----- + describe("preference gates", function () { + let PreferencesManager, LSPClient; + + beforeAll(function () { + PreferencesManager = testWindow.brackets.test.PreferencesManager; + LSPClient = testWindow.require("languageTools/LSPClient"); + }); + + afterEach(function () { + // restore defaults even when a spec failed mid-way + PreferencesManager.set("codeIntelligence.typescript", true); + PreferencesManager.set("showParameterHints", true); + }); + + it("should gate registerLanguageServer on codeIntelligence. and auto-define the pref", + async function () { + PreferencesManager.set("codeIntelligence.fakeGateSpec", false); + const client = await LSPClient.registerLanguageServer({ + serverId: "fakeGateSpec", + command: "no-such-lsp-binary", + languages: ["javascript"] + }); + expect(client).toBe(null); + // gated before any spawn attempt, but the pref is still auto-defined so + // plugin-supplied servers get a discoverable off-switch with a description + const pref = PreferencesManager.getPreference("codeIntelligence.fakeGateSpec"); + expect(pref).toBeTruthy(); + expect(pref.description).toContain("fakeGateSpec"); + PreferencesManager.set("codeIntelligence.fakeGateSpec", undefined); + }); + + it("should stop the running server on pref off, block resurrection, and restart on pref on", + async function () { + await _openInProject("ts/", "type-error.ts"); + await awaitsFor(function () { + return LSPClient.isLintingProviderActive("typescript"); + }, "typescript server to be active", 30000); + + PreferencesManager.set("codeIntelligence.typescript", false); + await awaitsFor(function () { + return !LSPClient.isLintingProviderActive("typescript"); + }, "typescript server to stop on pref off", 15000); + + // neither restart path may resurrect a pref-disabled server + await LSPClient.restartLanguageServer("typescript"); + await LSPClient.changeWorkspaceRoot("typescript"); + expect(LSPClient.isLintingProviderActive("typescript")).toBe(false); + + PreferencesManager.set("codeIntelligence.typescript", true); + await awaitsFor(function () { + return LSPClient.isLintingProviderActive("typescript"); + }, "typescript server to restart on pref on", 45000); + }, 90000); + + it("should gate the parameter hint popup on showParameterHints", async function () { + await _openInProject("ts/", "type-error.ts"); + const editor = EditorManager.getActiveEditor(); + editor.document.setText("function add(a: number, b: number) { return a + b; }\nadd(1, 2);\n"); + editor.setCursorPos(1, 4); // inside add(| + CommandManager.execute("showParameterHint"); + await awaitsFor(function () { + return $("#function-hint-container-new").is(":visible"); + }, "parameter hint popup to show", 30000); + + // flipping the pref off dismisses the visible popup + PreferencesManager.set("showParameterHints", false); + await awaitsFor(function () { + return !$("#function-hint-container-new").is(":visible"); + }, "popup to dismiss on pref off", 5000); + + // while off the gate is synchronous - no request is even issued, so the popup + // must still be hidden right after an explicit invocation + CommandManager.execute("showParameterHint"); + expect($("#function-hint-container-new").is(":visible")).toBe(false); + + PreferencesManager.set("showParameterHints", true); + CommandManager.execute("showParameterHint"); + await awaitsFor(function () { + return $("#function-hint-container-new").is(":visible"); + }, "parameter hint popup to show again", 30000); + }, 90000); + }); + + // ----- hint-severity diagnostics: untagged suggestions dropped, tagged ones styled ----- + describe("hint diagnostics and tags", function () { + let publishedByFile; + let origSetInspectionResults; + + beforeAll(function () { + // Capture what actually reaches the linting layer (post the LSPClient hint filter) + // so the specs can await/assert on publishes deterministically instead of relying + // only on problems-panel text. + const DefaultProviders = testWindow.require("languageTools/DefaultProviders"); + const proto = DefaultProviders.LintingProvider.prototype; + publishedByFile = {}; + origSetInspectionResults = proto.setInspectionResults; + proto.setInspectionResults = function (msgObj) { + const name = msgObj.uri.substring(msgObj.uri.lastIndexOf("/") + 1); + publishedByFile[name] = publishedByFile[name] || []; + publishedByFile[name].push(msgObj.diagnostics || []); + return origSetInspectionResults.call(this, msgObj); + }; + }); + + afterAll(function () { + const DefaultProviders = testWindow.require("languageTools/DefaultProviders"); + DefaultProviders.LintingProvider.prototype.setInspectionResults = origSetInspectionResults; + }); + + beforeEach(async function () { + // Isolate from whatever earlier specs left behind (dirty documents, an open + // parameter-hint popup, stale panel state). + await awaitsForDone(CommandManager.execute(Commands.FILE_CLOSE_ALL, { _forceClose: true }), + "close all files"); + }); + + it("should keep tagged hints (unused/deprecated) in the panel and style the text", async function () { + await _openInProject("ts/", "type-error.ts"); + const editor = EditorManager.getActiveEditor(); + editor.document.setText( + "const unusedVar: number = 1;\n" + + "/** @deprecated use newFn */\n" + + "function oldFn(): number { return 1; }\n" + + "oldFn();\n" + + "export {};\n" + ); + // tagged hints (unused tag 1, deprecated tag 2) must survive the hint filter + await awaitsFor(function () { + const all = (publishedByFile["type-error.ts"] || []).flat(); + return all.some(function (d) { return d.tags && d.tags.indexOf(1) !== -1; }) && + all.some(function (d) { return d.tags && d.tags.indexOf(2) !== -1; }); + }, "tagged unused + deprecated diagnostics to be published", 30000); + await awaitsFor(function () { + return panelText().includes("never read") && panelText().includes("deprecated"); + }, "unused + deprecated hint rows in the problems panel", 30000); + // the marked text carries the tag styles on top of the info squiggle + await awaitsFor(function () { + return $(".editor-text-fragment-unnecessary").length > 0 && + $(".editor-text-fragment-deprecated").length > 0; + }, "faded + strikethrough text marks in the editor", 30000); + }, 90000); + + it("should drop untagged hint suggestions like the CommonJS to ESM nag", async function () { + await _openInProject("js-plain/", "implicit.js"); + const editor = EditorManager.getActiveEditor(); + const publishesBeforeEdit = (publishedByFile["implicit.js"] || []).length; + // "require(" is concatenated so RequireJS's static dependency scan of this module's + // source doesn't read the string literal as an AMD dependency named "http" - that + // fails the whole file's load and silently unregisters every suite in it. + const requireCall = "require"; + editor.document.setText("const http = " + requireCall + "('http');\nconsole.log(http);\n"); + // the server re-publishes (possibly empty) diagnostics after the edit - wait for + // one, then assert the untagged 80001 CommonJS suggestion never came through + await awaitsFor(function () { + return (publishedByFile["implicit.js"] || []).length > publishesBeforeEdit; + }, "a diagnostics publish for implicit.js after the edit", 30000); + const all = (publishedByFile["implicit.js"] || []).flat(); + expect(all.some(function (d) { return d.message.indexOf("CommonJS") !== -1; })).toBe(false); + expect(all.some(function (d) { + return d.severity === 4 && !(d.tags && d.tags.length); + })).toBe(false); + expect(panelText().includes("CommonJS")).toBe(false); + }, 90000); + }); }); }); diff --git a/src/extensionsIntegrated/JSONSupport/JsonLsp.js b/src/extensionsIntegrated/JSONSupport/JsonLsp.js index 46e1436a65..b2f1c84d60 100644 --- a/src/extensionsIntegrated/JSONSupport/JsonLsp.js +++ b/src/extensionsIntegrated/JSONSupport/JsonLsp.js @@ -37,10 +37,20 @@ define(function (require, exports, module) { const EditorManager = require("editor/EditorManager"), ProjectManager = require("project/ProjectManager"), NodeConnector = require("NodeConnector"), + PreferencesManager = require("preferences/PreferencesManager"), + Strings = require("strings"), SchemaAssociations = require("./schemaAssociations"); const SERVER_ID = "json"; const SUPPORTED_LANGUAGES = ["json"]; + const PREF_JSON_CODE_INTELLIGENCE = "codeIntelligence." + SERVER_ID; + + // Master switch for JSON code intelligence - setting it back to true starts the server on + // the open JSON file. A runtime disable of a running server is handled centrally by + // LSPClient's own watcher on this pref. + PreferencesManager.definePreference(PREF_JSON_CODE_INTELLIGENCE, "boolean", true, { + description: Strings.DESCRIPTION_JSON_CODE_INTELLIGENCE + }); let lspClientPromise = null; let client = null; // the LanguageClient once registered @@ -181,6 +191,9 @@ define(function (require, exports, module) { if (!canRun() || !_isServedLanguageActive()) { return; } + if (PreferencesManager.get(PREF_JSON_CODE_INTELLIGENCE) === false) { + return; + } if (!registered) { if (starting) { return; @@ -219,6 +232,13 @@ define(function (require, exports, module) { pendingRepoint = true; _ensureServerForActiveEditor(); }); + // Re-enabling the pref starts the server on the open JSON file without needing a file + // switch. The disable direction is handled by LSPClient stopping the running server. + PreferencesManager.on("change", PREF_JSON_CODE_INTELLIGENCE, function () { + if (PreferencesManager.get(PREF_JSON_CODE_INTELLIGENCE) !== false) { + _ensureServerForActiveEditor(); + } + }); } /** diff --git a/src/features/ParameterHintsManager.js b/src/features/ParameterHintsManager.js index 70c7ddb7e2..f1e28d1dd0 100644 --- a/src/features/ParameterHintsManager.js +++ b/src/features/ParameterHintsManager.js @@ -33,6 +33,7 @@ define(function (require, exports, module) { EditorManager = require("editor/EditorManager"), Menus = require("command/Menus"), Strings = require("strings"), + PreferencesManager = require("preferences/PreferencesManager"), WorkspaceManager = require("view/WorkspaceManager"), ProviderRegistrationHandler = require("features/PriorityBasedRegistration").RegistrationHandler; @@ -89,6 +90,19 @@ define(function (require, exports, module) { registerHintProvider = _providerRegistrationHandler.registerProvider.bind(_providerRegistrationHandler), removeHintProvider = _providerRegistrationHandler.removeProvider.bind(_providerRegistrationHandler); + let paramHintsEnabled = true; + + PreferencesManager.definePreference("showParameterHints", "boolean", true, { + description: Strings.DESCRIPTION_SHOW_PARAMETER_HINTS + }); + + PreferencesManager.on("change", "showParameterHints", function () { + paramHintsEnabled = PreferencesManager.get("showParameterHints"); + if (!paramHintsEnabled) { + dismissHint(); + } + }); + /** * Keep the active parameter visible inside the single-line, width-capped popup: scroll it * into view (centered) when the signature overflows, and fade whichever edge is clipped so @@ -322,6 +336,13 @@ define(function (require, exports, module) { let $deferredPopUp = $.Deferred(); let sessionProvider = null; + // Gates implicit ("("/"," typed, cursor tracking) and explicit (Ctrl-Shift-Space) + // invocation alike - same semantics as showCodeHints gating code hint sessions. + if (!paramHintsEnabled) { + dismissHint(editor); + return $deferredPopUp; + } + popupShown = true; // Find a suitable provider, if any let language = editor.getLanguageForSelection(), diff --git a/src/language/CodeInspection.js b/src/language/CodeInspection.js index c278421c7e..1074f1a9bf 100644 --- a/src/language/CodeInspection.js +++ b/src/language/CodeInspection.js @@ -423,11 +423,24 @@ define(function (require, exports, module) { } function _getMarkOptions(error){ + let options; switch (error.type) { - case Type.ERROR: return Editor.getMarkOptionUnderlineError(); - case Type.WARNING: return Editor.getMarkOptionUnderlineWarn(); - case Type.META: return Editor.getMarkOptionUnderlineInfo(); + case Type.ERROR: options = Editor.getMarkOptionUnderlineError(); break; + case Type.WARNING: options = Editor.getMarkOptionUnderlineWarn(); break; + case Type.META: options = Editor.getMarkOptionUnderlineInfo(); break; } + // LSP DiagnosticTag values on the error (1 Unnecessary - unused symbol, 2 Deprecated) + // add a text style on top of the squiggle: faded for unused, strikethrough for + // deprecated. Panel row and squiggle treatment stay unchanged. + if (options && Array.isArray(error.tags)) { + if (error.tags.indexOf(1) !== -1) { + options.className += " editor-text-fragment-unnecessary"; + } + if (error.tags.indexOf(2) !== -1) { + options.className += " editor-text-fragment-deprecated"; + } + } + return options; } function _getMarkTypePriority(type){ diff --git a/src/languageTools/DefaultProviders.js b/src/languageTools/DefaultProviders.js index 0b79be7e27..18e0601b1e 100644 --- a/src/languageTools/DefaultProviders.js +++ b/src/languageTools/DefaultProviders.js @@ -31,6 +31,7 @@ define(function (require, exports, module) { var EditorManager = require("editor/EditorManager"), DocumentManager = require("document/DocumentManager"), + PreferencesManager = require("preferences/PreferencesManager"), CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), StringMatch = require("utils/StringMatch"), @@ -257,6 +258,12 @@ define(function (require, exports, module) { } function _showDocPopup($hint, docHtml) { + // Single choke point for the docs popup beside the hint list - the preference gates + // every caller (LSP hints here, npm hints, ...). + if (PreferencesManager.get("showCodeHintDocs") === false) { + _hideDocPopup(); + return; + } var $menu = $hint.closest(".codehint-menu"); if (!docHtml || !$menu.length) { _hideDocPopup(); @@ -936,7 +943,10 @@ define(function (require, exports, module) { ch: obj.range.end.character }, message: obj.message, - type: (obj.severity === 1 ? CodeInspection.Type.ERROR : (obj.severity === 2 ? CodeInspection.Type.WARNING : CodeInspection.Type.META)) + type: (obj.severity === 1 ? CodeInspection.Type.ERROR : (obj.severity === 2 ? CodeInspection.Type.WARNING : CodeInspection.Type.META)), + // LSP DiagnosticTag values (1 Unnecessary, 2 Deprecated) - CodeInspection styles + // the marked text off these (faded / strikethrough) on top of the squiggle. + tags: obj.tags }; }); diff --git a/src/languageTools/LSPClient.js b/src/languageTools/LSPClient.js index a9861fd624..81d07b8ee9 100644 --- a/src/languageTools/LSPClient.js +++ b/src/languageTools/LSPClient.js @@ -65,7 +65,10 @@ define(function (require, exports, module) { FindReferencesManager = require("features/FindReferencesManager"), QuickViewManager = require("features/QuickViewManager"), CodeInspection = require("language/CodeInspection"), - EventDispatcher = require("utils/EventDispatcher"); + EventDispatcher = require("utils/EventDispatcher"), + PreferencesManager = require("preferences/PreferencesManager"), + Strings = require("strings"), + StringUtils = require("utils/StringUtils"); EventDispatcher.makeEventDispatcher(exports); @@ -209,6 +212,14 @@ define(function (require, exports, module) { if (filterFn && diagnostics.length) { diagnostics = filterFn(diagnostics, { languageId: langId, filePath: vfsPath }); } + // Severity 4 (Hint) without tags is a refactoring suggestion (e.g. tsserver's "File is + // a CommonJS module; it may be converted to an ES module") - noise in a problems panel, + // so drop it (this also drops its quickfix). Tagged hints stay: tag 1 Unnecessary + // (unused symbol) and tag 2 Deprecated are real signal - they keep their panel row and + // squiggle, and additionally drive the faded/strikethrough text styling. + diagnostics = diagnostics.filter(function (d) { + return d.severity !== 4 || (d.tags && d.tags.length); + }); client.lintingProvider.setInspectionResults({ uri: vfsUri, diagnostics: diagnostics @@ -229,8 +240,11 @@ define(function (require, exports, module) { } client.capabilities = null; DocumentSync.clearServer(client); - if (client._stopping) { - return; // Intentional stop/restart - do not auto-restart here. + if (client._stopping || _isDisabledByPref(client.serverId)) { + // Intentional stop/restart - do not auto-restart here. The pref check also covers + // the pref-off stop: its exit event can land after stopServerProcess resolved (and + // reset _stopping), which would otherwise read as a crash and bump _crashCount. + return; } // Unexpected crash - log it loudly (with the server's stderr) so failures are never // silent, then self-heal with a bounded backoff to recover without a reload. @@ -243,7 +257,8 @@ define(function (require, exports, module) { return; } setTimeout(function () { - if (!clients.has(client.serverId) || client.capabilities) { + if (!clients.has(client.serverId) || client.capabilities || + _isDisabledByPref(client.serverId)) { return; } _startAndInit(client).then(function () { @@ -808,6 +823,52 @@ define(function (require, exports, module) { CodeInspection.requestRun(); } + /** + * Every registered server gets a `codeIntelligence.` boolean preference (defined by + * its extension, or auto-defined here for plugin-supplied servers) - the durable off-switch + * for the whole server: hints, parameter hints, jump-to-def, references, hover, diagnostics. + * @param {string} serverId + * @return {string} the preference key + */ + function _codeIntelPrefKey(serverId) { + return "codeIntelligence." + serverId; + } + + function _isDisabledByPref(serverId) { + return PreferencesManager.get(_codeIntelPrefKey(serverId)) === false; + } + + // serverIds whose codeIntelligence pref already has a change listener attached - the pref + // outlives the client entry (registration can be retried), so listeners attach only once. + const _prefWatchedServers = new Set(); + + /** + * Live-toggle a registered server from its `codeIntelligence.` pref: off stops the + * server process (providers go dormant without capabilities, and inspection re-runs so a + * fallback linter can take over); on restarts it in place. + */ + function _watchServerPref(serverId) { + if (_prefWatchedServers.has(serverId)) { + return; + } + _prefWatchedServers.add(serverId); + PreferencesManager.on("change", _codeIntelPrefKey(serverId), function () { + const client = clients.get(serverId); + if (!client) { + return; + } + if (_isDisabledByPref(serverId)) { + if (client.capabilities) { + stopServerProcess(client).then(function () { + CodeInspection.requestRun(); + }); + } + } else { + restartLanguageServer(serverId); + } + }); + } + /** * Register and start a language server, wiring all providers into the editor. * @@ -851,6 +912,18 @@ define(function (require, exports, module) { if (clients.has(config.serverId)) { return clients.get(config.serverId); } + if (!PreferencesManager.getPreference(_codeIntelPrefKey(config.serverId))) { + // Auto-define so plugin-supplied servers get a discoverable pref with a description. + // Built-ins (typescript/json/php/python) define theirs with richer descriptions + // before registering, so this is skipped for them. + PreferencesManager.definePreference(_codeIntelPrefKey(config.serverId), "boolean", true, { + description: StringUtils.format(Strings.DESCRIPTION_LSP_CODE_INTELLIGENCE, config.serverId) + }); + } + _watchServerPref(config.serverId); + if (_isDisabledByPref(config.serverId)) { + return null; + } const client = new LanguageClient(config.serverId, config.languages, config); // Register eagerly so a publishDiagnostics arriving during init is not dropped. clients.set(config.serverId, client); @@ -910,7 +983,9 @@ define(function (require, exports, module) { */ async function changeWorkspaceRoot(serverId) { const client = clients.get(serverId); - if (!client) { + if (!client || _isDisabledByPref(serverId)) { + // A pref-disabled server must stay down - the restart fallbacks below would + // otherwise resurrect it on a project switch. return; } // Resolve the target root FIRST: a redundant call for the same root must be a cheap no-op and @@ -952,7 +1027,7 @@ define(function (require, exports, module) { async function restartLanguageServer(serverId) { const client = clients.get(serverId); - if (!client) { + if (!client || _isDisabledByPref(serverId)) { return; } await stopServerProcess(client); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 96ca000828..dcfae61d0f 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1691,6 +1691,8 @@ define({ "DESCRIPTION_SCROLL_PAST_END": "true to enable scrolling beyond the end of the document", "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.", "DESCRIPTION_SHOW_CODE_HINTS": "false to disable all code hints", + "DESCRIPTION_SHOW_CODE_HINT_DOCS": "false to hide the documentation popup shown beside code hints", + "DESCRIPTION_SHOW_PARAMETER_HINTS": "false to disable the parameter hint popup shown when typing inside a function call", "DESCRIPTION_SHOW_CURSOR_WHEN_SELECTING": "Keeps the blinking cursor visible when you have a text selection", "DESCRIPTION_SHOW_LINE_NUMBERS": "true to show line numbers in a “gutter” to the left of the code", "DESCRIPTION_RULERS_COLUMNS": "An array of column numbers to draw vertical rulers in the editor. Eg: [80, 100]", @@ -1827,6 +1829,10 @@ define({ "PYTHON_INSTALL_STOP": "Stop setting up Python support ({APP_NAME} will try again on the next launch)", "PYTHON_INSTALL_WAITING_NETWORK": "Waiting for an internet connection to set up Python support…", "DESCRIPTION_PYTHON_CODE_INTELLIGENCE": "false to disable Python code intelligence (Pyrefly). Setting it back to true downloads the language server again automatically.", + // Language server on/off switches (LSPClient and the built-in language support extensions) + "DESCRIPTION_TYPESCRIPT_CODE_INTELLIGENCE": "false to disable JavaScript/TypeScript code intelligence (completion, hover docs, jump to definition, diagnostics)", + "DESCRIPTION_JSON_CODE_INTELLIGENCE": "false to disable JSON code intelligence (schema based completion, hover docs and validation)", + "DESCRIPTION_LSP_CODE_INTELLIGENCE": "false to disable the {0} language server", // JSON / package.json intelligence (JSONSupport) "NPM_VULN_PROVIDER_NAME": "npm Security Advisories", "NPM_VULN_MESSAGE": "{0}@{1} is vulnerable: {2} ({3} severity)", diff --git a/src/styles/brackets.less b/src/styles/brackets.less index 80367c6851..2481e40371 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -217,6 +217,17 @@ html, body { background: url('images/wavy-info.svg') repeat-x bottom left; } +// LSP DiagnosticTag text styles, added on top of the squiggle classes above: +// unused symbols (tag Unnecessary) render faded, deprecated usages (tag Deprecated) +// render struck through. +.editor-text-fragment-unnecessary { + opacity: 0.6; +} + +.editor-text-fragment-deprecated { + text-decoration: line-through; +} + .hidden-element{ display: none !important; } diff --git a/test/spec/Extn-JSONSupport-integ-test.js b/test/spec/Extn-JSONSupport-integ-test.js index b831d0d62a..3a82eb48cb 100644 --- a/test/spec/Extn-JSONSupport-integ-test.js +++ b/test/spec/Extn-JSONSupport-integ-test.js @@ -177,5 +177,28 @@ define(function (require, exports, module) { }, "vulnerability advisory in the problems panel", 30000); expect(_problemsText()).toContain("lodash@4.17.21"); }, 45000); + + it("should stop the JSON server when codeIntelligence.json turns off and restart on re-enable", + async function () { + const PreferencesManager = testWindow.brackets.test.PreferencesManager; + const LSPClient = testWindow.require("languageTools/LSPClient"); + await _openFile("broken.json"); + await awaitsFor(function () { + return LSPClient.isLintingProviderActive("json"); + }, "json server to be active", 30000); + + PreferencesManager.set("codeIntelligence.json", false); + try { + await awaitsFor(function () { + return !LSPClient.isLintingProviderActive("json"); + }, "json server to stop on pref off", 15000); + } finally { + // restore even on failure so later suites see a healthy default + PreferencesManager.set("codeIntelligence.json", true); + } + await awaitsFor(function () { + return LSPClient.isLintingProviderActive("json"); + }, "json server to restart on pref on", 45000); + }, 90000); }); });