From 4948a4bb06eebd72da030095b2c5bb54ea05e28d Mon Sep 17 00:00:00 2001 From: abose Date: Fri, 3 Jul 2026 10:44:22 +0530 Subject: [PATCH 1/3] feat(php): PHP code intelligence via Intelephense - on-demand install, full LSP parity PHP now gets the same intelligence as JS/TS: completion with a signature docs popup, hover with full PHP-manual docs, signature help, diagnostics, jump-to-definition, find references and quickfix plumbing - all on the shared LSP framework via a new desktop-only PHPSupport default extension. Licensing shapes the design: the Intelephense server is proprietary freeware whose license forbids bundling, but permits individual users to pair it with an LSP-capable editor. So it is NOT shipped - on the first PHP file the user is offered it (SUBTLE prompt toast with Install/Not Now and a "Powered by Intelephense" link, once per project per session, plus a persistent Problems-panel Install row mirroring the TS enable flow). Consent triggers a pinned `npm install intelephense@1.18.5` into /lspServers/intelephense/ through the existing bundled-npm node plumbing, tracked as a status-bar TaskManager task with phase progress; success auto-starts the server. Version upgrades (pin bumps) reinstall silently and pass clearCache once. Preferences: php.codeIntelligence (master switch) and php.licenseKey (premium licenceKey; the standard global licence file also works untouched - the server reads it itself). Cross-platform by construction: no .bin shims, no shell, array-args npm, central VFS->platform path conversion. Framework changes: - src-node/lsp-client.js: an absolute command path ending .js is spawned on our own node runtime (process.execPath) - user-local servers run identically on Windows/macOS/Linux. - LSPClient client capabilities now declare completionItem.resolveSupport (documentation, detail) - LSP 3.16 servers may gate lazy resolution on it. Verified empirically that intelephense provides NO completion-item documentation either way (its rich docs come via hover); hence: - DefaultProviders composes the docs-popup signature from label + labelDetails.detail + return type when a server splits the signature across fields (intelephense) - previously the popup showed a bare return type. vtsls (whole signature in `detail`) is untouched. Long `detail` strings (>32 chars) no longer render inline in hint rows; the docs popup carries them. Ecosystem fixes for mixed php/html documents: - JavaScriptCodeHints: inline-script host languages (html, php) keep their Tern session even when an LSP claims the document language - intelephense serves only PHP, so without this every embedded + + diff --git a/test/spec/PHPSupport-test-files/error.php b/test/spec/PHPSupport-test-files/error.php new file mode 100644 index 0000000000..f33d20c28e --- /dev/null +++ b/test/spec/PHPSupport-test-files/error.php @@ -0,0 +1,5 @@ + +} diff --git a/test/spec/PHPSupport-test-files/funcs.php b/test/spec/PHPSupport-test-files/funcs.php new file mode 100644 index 0000000000..487b685803 --- /dev/null +++ b/test/spec/PHPSupport-test-files/funcs.php @@ -0,0 +1,7 @@ + Date: Fri, 3 Jul 2026 10:56:52 +0530 Subject: [PATCH 2/3] fix(emmet): no markup abbreviations inside php code regions Emmet's markup provider registers for ["html", "php"], but a php file's HTML regions already report "html" at the cursor and are served by the html registration - so the php registration only ever receives cursors inside real code, where a markup abbreviation is never right (typing `addition(` summoned an Emmet hint mid-function-declaration). Guard in EmmetMarkupHints.hasHints: decline when the cursor's language is "php". HTML regions of php files keep full Emmet. New spec in the Emmet suite covers both sides: no hints after `function addition(` in a php region; `div` still offered in the same file's HTML region. unit: HTML Code Hinting 76/76. --- src/extensions/default/HTMLCodeHints/main.js | 8 +++++ .../default/HTMLCodeHints/unittests.js | 29 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/extensions/default/HTMLCodeHints/main.js b/src/extensions/default/HTMLCodeHints/main.js index ccbee8c02a..ef2c8fec36 100644 --- a/src/extensions/default/HTMLCodeHints/main.js +++ b/src/extensions/default/HTMLCodeHints/main.js @@ -93,6 +93,14 @@ define(function (require, exports, module) { if (enabled) { this.editor = editor; + // php files are mixed-mode: their HTML regions report "html" at the cursor and are + // served through the html registration. So a cursor whose language is actually "php" + // is inside real PHP code (), where markup abbreviations are never right - + // typing `addition(` there must not summon an Emmet hint. + if (editor.getLanguageForSelection().getId() === "php") { + return false; + } + // check the context before showing emmet hints, because we don't want to show // emmet hints when its a Attribute name or value // cause for those cases AttrHints should handle it diff --git a/src/extensions/default/HTMLCodeHints/unittests.js b/src/extensions/default/HTMLCodeHints/unittests.js index 84d870dccd..73f4afbb06 100644 --- a/src/extensions/default/HTMLCodeHints/unittests.js +++ b/src/extensions/default/HTMLCodeHints/unittests.js @@ -695,6 +695,35 @@ define(function (require, exports, module) { describe("Emmet hint provider", function () { + it("should not offer emmet inside php code regions, only in html regions of php files", function () { + // php is a mixed mode: regions are real PHP (emmet is never right + // there - e.g. typing `addition(`), while the surrounding markup is HTML where + // emmet must keep working. + const phpContent = "\n" + + "
\n" + + "div\n" + + "
\n"; + const phpDocument = SpecRunnerUtils.createMockDocument(phpContent, "php"); + $("body").append("
"); + const phpEditor = new Editor(phpDocument, true, $("#php-editor").get(0)); + + // cursor inside the php code region, right after `addition(` + phpEditor.setCursorPos({ line: 2, ch: 18 }); + expect(phpEditor.getLanguageForSelection().getId()).toBe("php"); + expect(HTMLCodeHints.emmetHintProvider.hasHints(phpEditor, null)).toBe(false); + + // cursor in the html region after "div" - emmet abbreviation expansion still works + phpEditor.setCursorPos({ line: 5, ch: 3 }); + expect(phpEditor.getLanguageForSelection().getId()).not.toBe("php"); + expect(HTMLCodeHints.emmetHintProvider.hasHints(phpEditor, null)).toBe(true); + + phpEditor.destroy(); + $("#php-editor").remove(); + }); + it("should display emmet hint and expand to boilerplate code on ! press", function () { let emmetBoilerPlate = [ From 85426b054c507f01061ff6f33c5ceaf82b13957a Mon Sep 17 00:00:00 2001 From: abose Date: Fri, 3 Jul 2026 13:30:29 +0530 Subject: [PATCH 3/3] feat(php): find-bar-style install prompt with benefits tooltip Replace the install prompt toast with a ModalBar banner across the top of the editor - the same surface as the find bar, impossible to miss on the very file that triggered it, yet passive (autoClose off: clicking back into the code doesn't dismiss it). - Terse copy: "Install advanced PHP code intelligence?" followed by an (i) icon whose hover shows a compact benefits card (the reusable rich tooltip): Completions / Docs on hover / Error checking / Navigation, one plain line each. Right side: "Powered by Intelephense" link, Not Now, and a primary Install button. - Reappears on every PHP file switch in the project until explicitly dismissed via Not Now (session-scoped per project); switching away just closes it for that moment. The Problems-panel row stays as the quiet persistent affordance, and php.codeIntelligence remains the durable off-switch. Install click closes the bar and hands off to the existing TaskManager install task + auto-start. Verified live: fresh consent -> bar -> Install -> server up; re-show on file switch; benefits tooltip; bar closes on non-php files. --- .../default/PHPSupport/ServerInstaller.js | 102 ++++++++++++------ src/nls/root/strings.js | 10 +- src/styles/brackets.less | 32 ++++++ 3 files changed, 108 insertions(+), 36 deletions(-) diff --git a/src/extensions/default/PHPSupport/ServerInstaller.js b/src/extensions/default/PHPSupport/ServerInstaller.js index cd07273678..82bca7b0c2 100644 --- a/src/extensions/default/PHPSupport/ServerInstaller.js +++ b/src/extensions/default/PHPSupport/ServerInstaller.js @@ -47,6 +47,7 @@ define(function (require, exports, module) { const NodeUtils = brackets.getModule("utils/NodeUtils"), ProjectManager = brackets.getModule("project/ProjectManager"), NativeApp = brackets.getModule("utils/NativeApp"), + ModalBar = brackets.getModule("widgets/ModalBar").ModalBar, NotificationUI = brackets.getModule("widgets/NotificationUI"), TaskManager = brackets.getModule("features/TaskManager"), PreferencesManager = brackets.getModule("preferences/PreferencesManager"), @@ -60,8 +61,12 @@ define(function (require, exports, module) { let _onInstalled = null; // main.js callback: ({entryPath, upgraded}) => void let _inFlight = null; // single-flight install promise - const _promptShownForProject = new Set(); // prompt toast: once per project per session - let _promptToast = null; + // projects where the user clicked "Not Now" - the bar stops reappearing there for the + // session. Until then it returns on every php file switch (closing on switch-away is not a + // dismissal - only the explicit button is). + const _promptDismissedForProject = new Set(); + let _promptBar = null; // the ModalBar install prompt, when showing + let _promptBarTip = null; // the benefits tooltip binding on the bar's info icon let _panelRowDismissed = false; // Problems-panel row dismissed this session function _installDirVfs() { @@ -202,49 +207,71 @@ define(function (require, exports, module) { // ----- consent UI: prompt toast + Problems-panel row (mirrors the TS enable affordances) ------ - function _dismissPromptToast() { - if (_promptToast) { - _promptToast.close(); - _promptToast = null; + function _closePromptBar() { + if (_promptBarTip) { + _promptBarTip.detach(); + _promptBarTip = null; + } + if (_promptBar) { + _promptBar.close(); + _promptBar = null; } } - function _showPromptToast() { + // Compact benefits card for the bar's (i) icon - term -> what it means, one line each. + function _benefitsTipHtml() { + const $tip = $("
"); + $("
").text(Strings.PHP_INSTALL_TITLE).appendTo($tip); + const $rows = $("
").appendTo($tip); + [ + [Strings.PHP_BENEFIT_COMPLETIONS, Strings.PHP_BENEFIT_COMPLETIONS_SUB], + [Strings.PHP_BENEFIT_DOCS, Strings.PHP_BENEFIT_DOCS_SUB], + [Strings.PHP_BENEFIT_ERRORS, Strings.PHP_BENEFIT_ERRORS_SUB], + [Strings.PHP_BENEFIT_NAV, Strings.PHP_BENEFIT_NAV_SUB] + ].forEach(function (row) { + $("").text(row[0]).appendTo($rows); + $("").text(row[1]).appendTo($rows); + }); + return $tip.html(); + } + + // A find-bar-style banner across the top of the editor - impossible to miss on the file that + // triggered it, but passive (autoClose false: clicking back into the code doesn't dismiss it). + function _showPromptBar() { const root = ProjectManager.getProjectRoot(); const rootPath = (root && root.fullPath) || ""; - if (_promptShownForProject.has(rootPath)) { + if (_promptDismissedForProject.has(rootPath) || _promptBar) { return; } - _promptShownForProject.add(rootPath); - const $tpl = $("
"); - $("
").text(Strings.PHP_INSTALL_MESSAGE).appendTo($tpl); + // built as detached DOM then serialized (ModalBar takes an HTML string); all click + // handling is delegated on the live bar root below + const $tpl = $("
"); + $("").text(Strings.PHP_INSTALL_MESSAGE).appendTo($tpl); + $("").appendTo($tpl); // credit where due (and where premium lives) - not license-required, just right - $("").text(Strings.PHP_POWERED_BY_INTELEPHENSE) - .attr("href", "#") - .on("click", function (e) { - e.preventDefault(); - e.stopPropagation(); - NativeApp.openURLInDefaultBrowser(INTELEPHENSE_HOME_URL); - }) + $("").text(Strings.PHP_POWERED_BY_INTELEPHENSE) .appendTo($tpl); - const $btns = $("
").appendTo($tpl); - const $install = $("