Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/API-Reference/widgets/NotificationUI.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ CSS class names for notification styles.
| SUCCESS | <code>string</code> | <code>&quot;style-success&quot;</code> |
| ERROR | <code>string</code> | <code>&quot;style-error&quot;</code> |
| DANGER | <code>string</code> | <code>&quot;style-danger&quot;</code> |
| SUBTLE | <code>string</code> | <code>&quot;style-subtle&quot;</code> |

<a name="module_widgets/NotificationUI..CLOSE_REASON"></a>

Expand Down
405 changes: 405 additions & 0 deletions src/extensions/default/TypeScriptSupport/CodeIntelligence.js

Large diffs are not rendered by default.

24 changes: 20 additions & 4 deletions src/extensions/default/TypeScriptSupport/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ define(function (require, exports, module) {
ProjectManager = brackets.getModule("project/ProjectManager"),
DocumentManager = brackets.getModule("document/DocumentManager"),
FileSystem = brackets.getModule("filesystem/FileSystem"),
NodeConnector = brackets.getModule("NodeConnector");
NodeConnector = brackets.getModule("NodeConnector"),
CodeIntelligence = require("./CodeIntelligence");

const SERVER_ID = "typescript";
const SUPPORTED_LANGUAGES = ["javascript", "typescript", "jsx", "tsx"];
Expand Down Expand Up @@ -269,13 +270,28 @@ define(function (require, exports, module) {
window._TypeScriptSupportReadyToIntegTest = true;
});

// Restart the server against the new workspace root when the project changes, and
// re-evaluate whether the new project type-checks its JS.
// Offer project-wide code intelligence (creates a default ts/jsconfig) when a JS/TS file is
// opened in a project that has no config yet. Projects that already carry one are silent.
CodeIntelligence.init({
supportedLanguages: SUPPORTED_LANGUAGES,
restartServer: function () {
if (registered) {
loadLSPClient().then(function (LSPClient) {
LSPClient.restartLanguageServer(SERVER_ID);
});
}
}
});

// Re-point the server at the new workspace root when the project changes, and re-evaluate
// whether the new project type-checks its JS. This uses workspace/didChangeWorkspaceFolders
// (no process restart, so no tsserver cold start) and only falls back to a full restart for
// servers that don't support live workspace-folder changes.
ProjectManager.on(ProjectManager.EVENT_PROJECT_OPEN, function () {
_refreshCheckJs();
if (registered) {
loadLSPClient().then(function (LSPClient) {
LSPClient.restartLanguageServer(SERVER_ID);
LSPClient.changeWorkspaceRoot(SERVER_ID);
});
}
});
Expand Down
39 changes: 25 additions & 14 deletions src/extensions/default/TypeScriptSupport/unittests.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,18 @@ define(function (require, exports, module) {
await awaitsFor(function () {
return testWindow._TypeScriptSupportReadyToIntegTest;
}, "TypeScript LSP server to start", 30000);
}, 30000);

// Warm up tsserver. Its very first request pays a large one-time cost - spawning node,
// launching vtsls, and loading the TypeScript library + project - which on a slow/loaded
// CI runner can exceed a single spec's timeout (fast dev machines never see it). Pay it
// once here with a generous budget so every spec below talks to an already-primed server;
// later project-switch restarts reuse the warm process and are fast.
await SpecRunnerUtils.loadProjectInTestWindow(testFolder + "ts/");
await awaitsForDone(SpecRunnerUtils.openProjectFiles(["type-error.ts"]), "warm-up: open type-error.ts");
await awaitsFor(function () {
return $("#problems-panel").text().includes("not assignable");
}, "tsserver to warm up on first cold start", 90000);
}, 100000);

afterAll(async function () {
testWindow = null;
Expand All @@ -85,34 +96,34 @@ define(function (require, exports, module) {
// type-error.ts assigns a string to a `number` -> TS2322 "... not assignable ...".
await awaitsFor(function () {
return panelText().includes("not assignable");
}, "TypeScript type error to be reported", 20000);
}, 30000);
}, "TypeScript type error to be reported", 30000);
}, 45000);

it("should report implicit-any in a JS project that opts into checkJs", async function () {
// js-checkjs has a jsconfig.json with checkJs + noImplicitAny, so the untyped parameter
// in implicit.js IS flagged - and our diagnostic filter keeps it (the project opted in).
await _openInProject("js-checkjs/", "implicit.js");
await awaitsFor(function () {
return panelText().includes(IMPLICIT_ANY_MESSAGE);
}, "implicit-any to be reported under checkJs", 20000);
}, 30000);
}, "implicit-any to be reported under checkJs", 30000);
}, 45000);

it("should NOT report implicit-any in a plain JS project", async function () {
// Precondition: confirm the server actually produces implicit-any for this exact code
// (under checkJs), so the plain-project assertion below reflects gating, not just timing.
await _openInProject("js-checkjs/", "implicit.js");
await awaitsFor(function () {
return panelText().includes(IMPLICIT_ANY_MESSAGE);
}, "implicit-any under checkJs (precondition)", 20000);
}, "implicit-any under checkJs (precondition)", 30000);

// Same code in a plain JS project (no jsconfig / no @ts-check): the "go add types" nag
// must not appear. Wait for inspection to settle clean, then assert it is absent.
await _openInProject("js-plain/", "implicit.js");
await awaitsFor(function () {
return $("#status-inspection").hasClass("inspection-valid");
}, "plain JS inspection to settle with no problems", 20000);
}, "plain JS inspection to settle with no problems", 30000);
expect(panelText().includes(IMPLICIT_ANY_MESSAGE)).toBe(false);
}, 30000);
}, 75000);

// ----- hover quick-actions (Go to Definition / Find Usages) -------------------------------

Expand All @@ -138,7 +149,7 @@ define(function (require, exports, module) {
await awaitsFor(async function () {
popover = await _hoverPopoverAt(editor, CALL_LINE, CALL_CH);
return !!(popover && popover.content && popover.content.find(".lsp-hover-action").length === 2);
}, "hover quick actions to appear", 20000);
}, "hover quick actions to appear", 30000);

const labels = popover.content.find(".lsp-hover-action-label").map(function () {
return $(this).text();
Expand All @@ -158,16 +169,16 @@ define(function (require, exports, module) {
$act.trigger("click");
}
return EditorManager.getCurrentFullEditor().getCursorPos().line === DECL_LINE;
}, "Go to Definition to navigate to the declaration", 25000);
}, "Go to Definition to navigate to the declaration", 30000);
expect(EditorManager.getCurrentFullEditor().getCursorPos().line).toBe(DECL_LINE);
}, 40000);
}, 75000);

it("hover Find Usages opens the references panel (" + tc.ext + ")", async function () {
await _openInProject(tc.folder, tc.file);
const editor = EditorManager.getCurrentFullEditor();
await awaitsFor(async function () {
return !!(await _hoverPopoverAt(editor, CALL_LINE, CALL_CH));
}, "hover popover to be available", 20000);
}, "hover popover to be available", 30000);

// "Find Usages" is the right-aligned action; clicking it opens the references panel.
// Retry through the hover until the panel opens (the server may still be indexing).
Expand All @@ -181,9 +192,9 @@ define(function (require, exports, module) {
$end.trigger("click");
}
return $("#reference-in-files-results").is(":visible");
}, "references panel to open", 25000);
}, "references panel to open", 30000);
expect($("#reference-in-files-results").is(":visible")).toBe(true);
}, 40000);
}, 75000);
});
});
});
26 changes: 25 additions & 1 deletion src/languageTools/DefaultProviders.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,30 @@
}
}

// Syntax-highlight fenced code blocks (the signature/examples in completion docs) with the
// globally available highlight.js, so they read like code instead of flat monospace. Theme-aware
// token colours live in src/styles/brackets.less (.lsp-hint-doc-popup, shared with the hover).
function _highlightCode(html) {

Check warning on line 63 in src/languageTools/DefaultProviders.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move function '_highlightCode' to the outer scope.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AZ7kQSPXYNnNXkAmzcJE&open=AZ7kQSPXYNnNXkAmzcJE&pullRequest=2986
var hljs = (typeof Phoenix !== "undefined") && Phoenix.libs && Phoenix.libs.hljs;

Check failure on line 64 in src/languageTools/DefaultProviders.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AZ7kQSPXYNnNXkAmzcJF&open=AZ7kQSPXYNnNXkAmzcJF&pullRequest=2986

Check warning on line 64 in src/languageTools/DefaultProviders.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AZ7kQSPXYNnNXkAmzcJG&open=AZ7kQSPXYNnNXkAmzcJG&pullRequest=2986
if (!hljs) {
return html;
}
var $wrap = $("<div>").html(html);

Check failure on line 68 in src/languageTools/DefaultProviders.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AZ7kQSPXYNnNXkAmzcJH&open=AZ7kQSPXYNnNXkAmzcJH&pullRequest=2986
$wrap.find("pre > code").each(function () {
var $code = $(this);

Check failure on line 70 in src/languageTools/DefaultProviders.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AZ7kQSPXYNnNXkAmzcJI&open=AZ7kQSPXYNnNXkAmzcJI&pullRequest=2986
var match = ($code.attr("class") || "").match(/language-([\w-]+)/);

Check failure on line 71 in src/languageTools/DefaultProviders.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AZ7kQSPXYNnNXkAmzcJJ&open=AZ7kQSPXYNnNXkAmzcJJ&pullRequest=2986
var lang = match && match[1];

Check failure on line 72 in src/languageTools/DefaultProviders.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AZ7kQSPXYNnNXkAmzcJK&open=AZ7kQSPXYNnNXkAmzcJK&pullRequest=2986

Check warning on line 72 in src/languageTools/DefaultProviders.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AZ7kQSPXYNnNXkAmzcJL&open=AZ7kQSPXYNnNXkAmzcJL&pullRequest=2986
if (lang && hljs.getLanguage(lang)) {
try {
$code.html(hljs.highlight($code.text(), { language: lang }).value).addClass("hljs");
} catch (e) {
// leave the block unhighlighted on any hljs error
}

Check warning on line 78 in src/languageTools/DefaultProviders.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Handle this exception or don't catch it at all.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AZ7kQSPXYNnNXkAmzcJM&open=AZ7kQSPXYNnNXkAmzcJM&pullRequest=2986
}
});
return $wrap.html();
}

function _docToHtml(documentation) {
if (!documentation) {
return "";
Expand All @@ -66,7 +90,7 @@
return "";
}
try {
return marked.parse(md);
return _highlightCode(marked.parse(md));
} catch (e) {
return _.escape(md);
}
Expand Down
92 changes: 90 additions & 2 deletions src/languageTools/LSPClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,12 @@
if (!client) {
return;
}
if (client._stopping) {
// The server is shutting down/restarting. Ignore any late messages it emits during the
// teardown window so stale diagnostics from the dying instance don't leak into the fresh
// one that replaces it (both share the same serverId).
return;
}
if (data.method === "textDocument/publishDiagnostics" && client.lintingProvider) {
const params = data.params || {};
// Rewrite the URI to a VFS-based URI so the linting provider keys results by the
Expand Down Expand Up @@ -533,6 +539,11 @@
const rootVfsPath = (config.rootUriProvider && config.rootUriProvider()) || _projectRootPath();
const rootUri = rootVfsPath ? pathToServerUri(rootVfsPath) : null;
const rootName = rootVfsPath ? FileUtils.getBaseName(rootVfsPath) : "root";
// Remember the active workspace folder so a later project switch can hand the server the
// delta (removed old, added new) via workspace/didChangeWorkspaceFolders - see
// changeWorkspaceRoot - instead of a full restart.
client.rootUri = rootUri;
client.rootName = rootName;

await conn.execPeer("startServer", {
serverId: client.serverId,
Expand Down Expand Up @@ -647,6 +658,74 @@
* @param {string} serverId
* @return {Promise<void>}
*/
// How long to wait for a server to acknowledge a graceful `shutdown` before we hard-kill it.
// Healthy servers reply in well under this; the cap is a failsafe so a slow/buggy/hung server
// can't stall the restart indefinitely.
const SHUTDOWN_TIMEOUT_MS = 3000;

// Resolve/reject with `promise`, but reject with a timeout error if it doesn't settle in `ms`.
function _withTimeout(promise, ms) {

Check warning on line 667 in src/languageTools/LSPClient.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move function '_withTimeout' to the outer scope.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AZ7kJp_9upWwgW3YCHsH&open=AZ7kJp_9upWwgW3YCHsH&pullRequest=2986
return new Promise(function (resolve, reject) {
const timer = setTimeout(function () {
reject(new Error("timeout"));
}, ms);
promise.then(function (value) {
clearTimeout(timer);
resolve(value);
}, function (err) {
clearTimeout(timer);
reject(err);
});
});
}

/**
* Re-point a running server at the current project root WITHOUT restarting it, by sending
* `workspace/didChangeWorkspaceFolders` (remove the old folder, add the new one). This avoids
* the cold start a full restart pays on every project switch. Generic: servers that don't
* advertise live workspace-folder change support transparently fall back to a full restart.
* The open documents themselves are re-synced by DocumentSync's normal editor-change handling.
* @param {string} serverId
* @return {Promise<void>}
*/
async function changeWorkspaceRoot(serverId) {
const client = clients.get(serverId);
if (!client) {
return;
}
// Not up yet (e.g. the project switched before init finished) - a (re)start picks up the
// current root on its own.
if (!client.capabilities) {
return restartLanguageServer(serverId);
}
const wf = client.capabilities.workspace && client.capabilities.workspace.workspaceFolders;

Check warning on line 701 in src/languageTools/LSPClient.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AZ7kS5ynupWwgW3YDOYp&open=AZ7kS5ynupWwgW3YDOYp&pullRequest=2986
// Per the LSP spec changeNotifications is `boolean | string` (a static flag or a dynamic
// registration id); either truthy form means the server accepts live folder changes.
const supportsLiveChange = !!(wf && wf.supported && wf.changeNotifications);

Check warning on line 704 in src/languageTools/LSPClient.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AZ7kS5ynupWwgW3YDOYq&open=AZ7kS5ynupWwgW3YDOYq&pullRequest=2986
if (!supportsLiveChange) {
return restartLanguageServer(serverId);
}
const newVfsPath = (client.config.rootUriProvider && client.config.rootUriProvider()) || _projectRootPath();

Check warning on line 708 in src/languageTools/LSPClient.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AZ7kS5ynupWwgW3YDOYr&open=AZ7kS5ynupWwgW3YDOYr&pullRequest=2986
const newUri = newVfsPath ? pathToServerUri(newVfsPath) : null;
const oldUri = client.rootUri || null;
if (newUri === oldUri) {
return; // same workspace - nothing to do
}
const conn = await getConnector();
const added = newUri ? [{ uri: newUri, name: FileUtils.getBaseName(newVfsPath) }] : [];
const removed = oldUri ? [{ uri: oldUri, name: client.rootName || FileUtils.getBaseName(oldUri) }] : [];
await conn.execPeer("sendNotification", {
serverId: serverId,
method: "workspace/didChangeWorkspaceFolders",
params: { event: { added: added, removed: removed } }
});
client.rootUri = newUri;
client.rootName = newVfsPath ? FileUtils.getBaseName(newVfsPath) : null;
// Capabilities are unchanged (no restart), but the active file is now in the new project -
// refresh the find-references menu state for that context.
FindReferencesManager.setMenuItemStateForLanguage();
}

async function restartLanguageServer(serverId) {
const client = clients.get(serverId);
if (!client) {
Expand Down Expand Up @@ -676,18 +755,27 @@
client.capabilities = null;
client._completionCache = null;
DocumentSync.clearServer(client);
// Attempt a graceful LSP shutdown - some servers need it to flush state or clean up child
// processes - but BOUND it. The `shutdown` request blocks until the server replies, and a
// busy or cold server can be slow (or never reply), which would stall the restart; on a
// project switch we'd end up waiting for the old server to finish booting just to tell it to
// die, then cold-start a new one (a double penalty on slow CI). Give it a short budget, then
// hard-kill regardless. The `exit` notification expects no reply, so it stays cheap.
try {
await conn.execPeer("sendRequest", { serverId: client.serverId, method: "shutdown", params: null });
await _withTimeout(
conn.execPeer("sendRequest", { serverId: client.serverId, method: "shutdown", params: null }),
SHUTDOWN_TIMEOUT_MS);
await conn.execPeer("sendNotification", { serverId: client.serverId, method: "exit", params: null });
} catch (e) {
// Server may already be dead; fall through to a hard stop.
// Timed out, or the server is already dead - fall through to the hard stop.
}
await conn.execPeer("stopServer", { serverId: client.serverId });
client._stopping = false;
}

exports.registerLanguageServer = registerLanguageServer;
exports.restartLanguageServer = restartLanguageServer;
exports.changeWorkspaceRoot = changeWorkspaceRoot;
exports.pathToServerUri = pathToServerUri;
exports.serverUriToVfsUri = serverUriToVfsUri;
});
11 changes: 11 additions & 0 deletions src/nls/root/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -1736,6 +1736,17 @@ define({
"OPEN_PREFERENNCES": "Open Preferences",

"FIND_ALL_REFERENCES": "Find Usages",
// Project-wide code intelligence (TypeScript/JavaScript LSP) - on by default
"CODE_INTEL_JS": "JavaScript",
"CODE_INTEL_TS": "TypeScript",
"CODE_INTEL_ENABLED_TITLE": "{0} Code Intelligence Enabled",
"CODE_INTEL_ENABLED_MESSAGE": "Find Usages, Rename, and Go to Definition now work across every file in this project.",
"CODE_INTEL_SEE_CONFIG": "See Config",
"CODE_INTEL_ENABLE_TS": "Enable TypeScript",
"CODE_INTEL_LEARN_MORE": "Learn more",
"CODE_INTEL_PANEL_TEXT": "Project-wide code intelligence is off. Enable it for Find Usages, Rename, and Go to Definition across every file — adds a jsconfig.json to the project root.",
"CODE_INTEL_PANEL_ENABLE": "Enable",
"CODE_INTEL_PANEL_DISMISS": "Dismiss",
"REFERENCES_IN_FILES": "references",
"REFERENCE_IN_FILES": "reference",
"REFERENCES_NO_RESULTS": "No References available for current cursor position",
Expand Down
Loading
Loading