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 CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Never include `Co-Authored-By` lines in commit messages.

## Code Style
- This is a long-lived, partly legacy codebase — you will see older patterns like `var` in existing files. Leave surrounding legacy style alone unless you're deliberately refactoring it, but **any new code you write must use `const`/`let`, never `var`** (see below). Match the file's other conventions where they don't conflict with these rules.
- 4-space indentation, never tabs.
- Always use semicolons.
- Brace style: (`if (x) {`), single-line blocks allowed.
Expand Down
30 changes: 27 additions & 3 deletions src/extensions/default/JSLint/JSHint.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,29 @@ define(function (require, exports, module) {
FileSystem = brackets.getModule("filesystem/FileSystem"),
IndexingWorker = brackets.getModule("worker/IndexingWorker"),
Metrics = brackets.getModule("utils/Metrics"),
LanguageManager = brackets.getModule("language/LanguageManager"),
ESLint = require("./ESLint");

// JSHint defers to the TypeScript language service (vtsls) when it is linting the file (see
// canInspect). Its module (languageTools/LSPClient) is loaded lazily and only on desktop - it's
// intentionally kept out of the browser build - so resolve a handle there to query its state. When
// a server starts/stops, LSPClient re-runs inspection itself, so JSHint doesn't track that here. In
// the browser there is no LSP, so _lspClient stays null and JSHint keeps linting as before.
let _lspClient = null;
if (Phoenix.isNativeApp) {
brackets.getModule(["languageTools/LSPClient"], function (LSPClient) {
_lspClient = LSPClient;
});
}

function _lspLintingActive(fullPath) {
if (!_lspClient) {
return false;
}
const language = LanguageManager.getLanguageForPath(fullPath);
return !!language && _lspClient.isLintingProviderActive(language.getId());
}

if(Phoenix.isTestWindow) {
IndexingWorker.on("JsHint_extension_Loaded", ()=>{
window._JsHintExtensionReadyToIntegTest = true;
Expand Down Expand Up @@ -269,9 +290,12 @@ define(function (require, exports, module) {
scanFileAsync: lintOneFile,
canInspect: function (fullPath) {
return !prefs.get(PREFS_JSHINT_DISABLED) && fullPath && !fullPath.endsWith(".min.js")
&& (isJSHintConfigActive() || !ESLint.isESLintActive());
// if there is no linter, then we use jsHint as the default linter as it works in browser and native apps.
// remove ESLint.isESLintActive() once we add typescript language service that supports browser.
&& (isJSHintConfigActive()
|| (!ESLint.isESLintActive() && !_lspLintingActive(fullPath)));
// JSHint is the default JS linter only when nothing richer is linting the file: it defers
// to ESLint and to the TypeScript language service (vtsls, desktop). It still runs when a
// .jshintrc opts in explicitly, and as a fallback when neither is active (e.g. the browser,
// where there is no LSP, or when the language server isn't running).
}
});

Expand Down
59 changes: 58 additions & 1 deletion src/extensions/default/JavaScriptCodeHints/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,44 @@ define(function (require, exports, module) {
var _inlineScriptLanguages = ["html", "php"],
phProvider = new JSParameterHintsProvider();

// --- Defer to the TypeScript language server (vtsls) when it is active ------------------------
// When vtsls serves a JS/TS file, Tern is redundant: its hints lose to the server's higher-
// priority ones (see the provider registrations at priority 0 below) and its background project
// indexing only duplicates work the server already does. So on desktop we resolve a handle to
// LSPClient (intentionally kept out of the browser build) and, for any language the server serves,
// skip creating a Tern session - ScopeManager then never indexes the project in parallel with the
// server. The server starts lazily and can come up *after* Tern already indexed the first file, so
// we also listen for its start event to drop that data and re-evaluate the active editor; on stop
// we let Tern take over again. In the browser _lspClient stays null and Tern behaves as before.
// (We piggy-back on isLintingProviderActive as the "server is up and serving this language"
// signal - the same one JSHint uses to defer its linting.)
let _lspClient = null;
let _reinstallActiveEditorListeners = null; // assigned at appReady, once the listeners exist

function _lspServesLanguage(languageId) {
return !!(_lspClient && languageId && _lspClient.isLintingProviderActive(languageId));
}

if (Phoenix.isNativeApp) {
brackets.getModule(["languageTools/LSPClient"], function (LSPClient) {
_lspClient = LSPClient;
LSPClient.on(LSPClient.EVENT_LANGUAGE_SERVER_STARTED, function () {
// The server may have started after Tern already indexed the first file - drop that
// analysis to reclaim the memory, then re-gate the active editor so it defers now.
ScopeManager.handleProjectClose();
if (_reinstallActiveEditorListeners) {
_reinstallActiveEditorListeners();
}
});
LSPClient.on(LSPClient.EVENT_LANGUAGE_SERVER_STOPPED, function () {
// Server gone - let Tern resume handling the active editor.
if (_reinstallActiveEditorListeners) {
_reinstallActiveEditorListeners();
}
});
});
}

// Define the detectedExclusions which are files that have been detected to cause Tern to run out of control.
PreferencesManager.definePreference("jscodehints.detectedExclusions", "array", [], {
description: Strings.DESCRIPTION_DETECTED_EXCLUSIONS
Expand Down Expand Up @@ -663,7 +701,18 @@ define(function (require, exports, module) {
return;
}

if (editor && HintUtils.isSupportedLanguage(LanguageManager.getLanguageForPath(editor.document.file.fullPath).getId())) {
const languageId = editor
? LanguageManager.getLanguageForPath(editor.document.file.fullPath).getId()
: null;

// When the TypeScript language server serves this language, defer to it entirely: don't
// create a Tern session, so ScopeManager never indexes the project alongside the server.
if (_lspServesLanguage(languageId)) {
session = null;
return;
}

if (editor && HintUtils.isSupportedLanguage(languageId)) {
initializeSession(editor, previousEditor);
editor
.on(HintUtils.eventName("change"), function (event, editor, changeList) {
Expand Down Expand Up @@ -845,6 +894,14 @@ define(function (require, exports, module) {
ScopeManager.handleProjectOpen();
});

// Lets the LSP start/stop handlers re-evaluate the active editor (gate Tern off when the
// server comes up, back on when it goes away) - defined here, where the listeners exist.
_reinstallActiveEditorListeners = function () {
const activeEditor = EditorManager.getActiveEditor();
uninstallEditorListeners(activeEditor);
installEditorListeners(activeEditor);
};

// immediately install the current editor
installEditorListeners(EditorManager.getActiveEditor());

Expand Down
89 changes: 70 additions & 19 deletions src/extensions/default/TypeScriptSupport/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
const AppInit = brackets.getModule("utils/AppInit"),
ProjectManager = brackets.getModule("project/ProjectManager"),
DocumentManager = brackets.getModule("document/DocumentManager"),
EditorManager = brackets.getModule("editor/EditorManager"),
FileSystem = brackets.getModule("filesystem/FileSystem"),
NodeConnector = brackets.getModule("NodeConnector"),
CodeIntelligence = require("./CodeIntelligence");
Expand Down Expand Up @@ -203,8 +204,7 @@
* @return {boolean}
*/
function canRun() {
return typeof Phoenix !== "undefined" && Phoenix.isNativeApp &&
NodeConnector.isNodeAvailable && NodeConnector.isNodeAvailable();
return Phoenix.isNativeApp && NodeConnector.isNodeAvailable();
}

/**
Expand Down Expand Up @@ -252,23 +252,71 @@
}
}

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

/**
* True when the active editor holds a language this server handles (JS/TS/JSX/TSX).
* @return {boolean}
*/
function _isServedLanguageActive() {
const editor = EditorManager.getActiveEditor();
return !!(editor && SUPPORTED_LANGUAGES.indexOf(editor.getLanguageForSelection().getId()) !== -1);

Check warning on line 268 in src/extensions/default/TypeScriptSupport/main.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use `.includes()`, rather than `.indexOf()`, when checking for existence.

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

let starting = false;
let pendingRepoint = false; // a project switch happened; repoint once a served file is active there
let initErrorReported = false; // start() is retried lazily, so report a failure to telemetry only once

/**
* Lazily start the language server when a served-language file is active, and - only right after a
* project switch - repoint the running server at the new root. Mirrors VS Code's onLanguage model:
* a project with no JS/TS file opened never spawns vtsls; switching to a non-JS project leaves the
* idle server where it was; and plain file switches within a project never touch the
* workspace-folder / restart machinery (so they can't interfere with a crash auto-restart).
*/
function _ensureServerForActiveEditor() {
if (!canRun() || !_isServedLanguageActive()) {
return;
}

// Not running yet: lazily start it (a fresh start already points at the current project root).
if (!registered) {
if (starting) {
return; // a start kicked off by a previous activeEditorChange is still in flight
}
starting = true;
pendingRepoint = false;
start().catch(function (err) {
if (!initErrorReported) {
initErrorReported = true;
window.logger && window.logger.reportError(err, "[TypeScriptSupport] LSP init failed");

Check warning on line 297 in src/extensions/default/TypeScriptSupport/main.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

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

Check warning on line 297 in src/extensions/default/TypeScriptSupport/main.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=AZ8IERTUIHStKnsF7P3z&open=AZ8IERTUIHStKnsF7P3z&pullRequest=2994

Check warning on line 297 in src/extensions/default/TypeScriptSupport/main.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AZ8IERTUIHStKnsF7P3y&open=AZ8IERTUIHStKnsF7P3y&pullRequest=2994
}
}).finally(function () {
starting = false;
});
return;
}

// Running: repoint at the current project, but only when a project switch armed it - never on
// ordinary file switches.
if (pendingRepoint) {
pendingRepoint = false;
loadLSPClient().then(function (LSPClient) {
LSPClient.changeWorkspaceRoot(SERVER_ID);
});
}
}

AppInit.appReady(function () {
if (!canRun()) {
return;
}
_refreshCheckJs();
start().catch(function (err) {
console.error("[TypeScriptSupport] init failed", err && (err.message || err));
}).finally(function () {
// Signal for integration tests that the server start has been attempted/settled.
window._TypeScriptSupportReadyToIntegTest = true;
});

// 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.
Expand All @@ -283,17 +331,20 @@
}
});

// 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.
// Lazily start / repoint the server from the active editor's language (VS Code's onLanguage
// model). Evaluate the editor already open at startup (session restore), then track switches.
EditorManager.on("activeEditorChange", _ensureServerForActiveEditor);
_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
// project's file opens. Plain file switches within a project never set this, so they don't
// repoint.
ProjectManager.on(ProjectManager.EVENT_PROJECT_OPEN, function () {
_refreshCheckJs();
if (registered) {
loadLSPClient().then(function (LSPClient) {
LSPClient.changeWorkspaceRoot(SERVER_ID);
});
}
pendingRepoint = true;
_ensureServerForActiveEditor();
});

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