дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
feat(php): switch the PHP language level from the status bar (#1371)
With a PHP file focused, the status bar shows the PHP level Intelephense checks against (`PHP 7.4`); the tooltip says where it comes from (the settings, config.platform.php or require.php). Clicking it opens Auto (with the composer.json version) and 5.6 to 8.5; picking one writes the same project setting as Settings › Languages & Frameworks › PHP, so diagnostics follow without reopening the project. The widget is hidden for other files. For a file of a linked workspace project, the level comes from that project's settings and the choices are disabled. The menu is also the action "PHP Language Level". Closes #1091 Co-authored-by: DanielADK <[email protected]>
Цей коміт міститься в:
1 джерело
742e504719
коміт
2ba731f0ec
14 files changed
+665
-12
No files matched your search
@@ -0,0 +1,188 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { create } from "zustand";
|
||||
import { executeAction, registerActions } from "@/platform/actions";
|
||||
import { getContext, useContextKeys } from "@/platform/context";
|
||||
import { call, commands, onIdeEvent, type PhpLanguageLevel, type PhpLanguageLevels } from "@/platform/ipc";
|
||||
import { PHP_LANGUAGE_LEVELS } from "@/platform/languageSettings";
|
||||
import { basename, useProject } from "@/platform/project";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { notifications, registerStatusItem } from "@/workbench/contributions";
|
||||
import { BarButton, Menu, type MenuItem } from "@/ui";
|
||||
import { phpLanguageLevel } from "./api";
|
||||
|
||||
/** What the widget last resolved for a file, so the popup action can read it without a render. */
|
||||
interface Status extends PhpLanguageLevels {
|
||||
root: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
const usePopup = create<{ open: boolean; status: Status | null }>(() => ({ open: false, status: null }));
|
||||
const close = () => usePopup.setState({ open: false });
|
||||
const reset = () => usePopup.setState({ open: false, status: null });
|
||||
|
||||
/** Whether the widget is on screen, read straight from the stores (the action runs outside a render). */
|
||||
function popupShown(): boolean {
|
||||
const root = useProject.getState().project?.root;
|
||||
const status = usePopup.getState().status;
|
||||
const context = getContext();
|
||||
return !!root && status?.root === root && status.path === context.editorPath && context.editorLangId === "php";
|
||||
}
|
||||
|
||||
/** `8.1.0` as `8.1`, the way the levels are listed. */
|
||||
const shortVersion = (level: PhpLanguageLevel) => level.version.split(".").slice(0, 2).join(".");
|
||||
|
||||
function origin(level: PhpLanguageLevel | null): string {
|
||||
if (!level) return tx("No PHP version in the settings or composer.json: Intelephense uses its default");
|
||||
const version = level.version;
|
||||
switch (level.source) {
|
||||
case "settings":
|
||||
return tx`PHP ${version}, set in the settings`;
|
||||
case "platform":
|
||||
return tx`PHP ${version}, from config.platform.php in composer.json`;
|
||||
case "require":
|
||||
return tx`PHP ${version}, the lowest version require.php in composer.json allows`;
|
||||
}
|
||||
}
|
||||
|
||||
function PhpStatus() {
|
||||
const root = useProject((s) => s.project?.root);
|
||||
const linked = useProject((s) => s.project?.workspace?.projects);
|
||||
const configured = useProject((s) => s.settings?.languages?.php?.languageLevel ?? "");
|
||||
const path = useContextKeys((s) =>
|
||||
s.editorLangId === "php" && typeof s.editorPath === "string" ? s.editorPath : null,
|
||||
);
|
||||
const status = usePopup((s) => s.status);
|
||||
const open = usePopup((s) => s.open);
|
||||
const anchor = useRef<HTMLButtonElement>(null);
|
||||
useEffect(() => reset, [root]);
|
||||
// The menu belongs to the focused file.
|
||||
useEffect(() => close, [path]);
|
||||
useEffect(() => {
|
||||
if (!root || !path) return;
|
||||
let live = true;
|
||||
let sequence = 0;
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
// Auto reads the composer.json of the project or of the linked project the file is in.
|
||||
const manifests = new Set([root, ...(linked ?? [])].map((dir) => `${dir}/composer.json`));
|
||||
const refresh = async () => {
|
||||
const request = ++sequence;
|
||||
try {
|
||||
const level = await phpLanguageLevel(path);
|
||||
if (live && sequence === request) usePopup.setState({ status: level && { root, path, ...level } });
|
||||
} catch (e) {
|
||||
console.warn("Cannot read the PHP language level", e);
|
||||
}
|
||||
};
|
||||
void refresh();
|
||||
const off = onIdeEvent("fsChanged", (e) => {
|
||||
if (![...e.created, ...e.modified, ...e.removed].some((file) => manifests.has(file))) return;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => void refresh(), 300);
|
||||
});
|
||||
return () => {
|
||||
live = false;
|
||||
clearTimeout(timer);
|
||||
off();
|
||||
};
|
||||
}, [root, linked, path, configured]);
|
||||
// The widget only renders once its project's level is in the store, so the popup action
|
||||
// sees the same state as the button without waiting for an effect to publish a flag.
|
||||
if (!path || !popupShown() || !status) return null;
|
||||
const { effective, auto, member } = status;
|
||||
const label = effective ? `PHP ${shortVersion(effective)}` : "PHP";
|
||||
const chosen = effective?.source === "settings" ? shortVersion(effective) : "";
|
||||
// The choices write the workspace's settings, which a member does not read.
|
||||
const name = member && basename(member);
|
||||
const items: MenuItem[] = [
|
||||
{ kind: "header", label: "PHP Language Level" },
|
||||
...(name
|
||||
? [{ kind: "header", label: tx`Set in the settings of the linked project ${name}` } satisfies MenuItem]
|
||||
: []),
|
||||
...PHP_LANGUAGE_LEVELS.map((level): MenuItem =>
|
||||
level
|
||||
? {
|
||||
label: level,
|
||||
verbatim: true,
|
||||
checked: level === chosen,
|
||||
disabled: !!member,
|
||||
run: () => void executeAction("php.selectLanguageLevel", level),
|
||||
}
|
||||
: {
|
||||
label: "Auto",
|
||||
shortcut: auto ? shortVersion(auto) : undefined,
|
||||
checked: !chosen,
|
||||
disabled: !!member,
|
||||
run: () => void executeAction("php.selectLanguageLevel", ""),
|
||||
},
|
||||
),
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<BarButton
|
||||
ref={anchor}
|
||||
tooltip={origin(effective)}
|
||||
aria-label={tx`PHP Language Level: ${label}`}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={open}
|
||||
onClick={() => void executeAction("php.languageLevelPopup")}
|
||||
>
|
||||
{label}
|
||||
</BarButton>
|
||||
<Menu
|
||||
open={open}
|
||||
onClose={close}
|
||||
anchor={anchor}
|
||||
placement="top-end"
|
||||
items={items}
|
||||
aria-label="PHP Language Level"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function activatePhpStatus(): () => void {
|
||||
const dispose = [
|
||||
registerStatusItem({
|
||||
id: "php.languageLevel",
|
||||
title: "PHP Language Level",
|
||||
align: "right",
|
||||
priority: 89,
|
||||
component: PhpStatus,
|
||||
}),
|
||||
registerActions([
|
||||
{
|
||||
id: "php.languageLevelPopup",
|
||||
title: "PHP Language Level",
|
||||
category: "Settings",
|
||||
keywords: ["php version"],
|
||||
when: "projectOpen",
|
||||
run: () =>
|
||||
popupShown()
|
||||
? usePopup.setState((s) => ({ open: !s.open }))
|
||||
: void executeAction("file.settings", "languages.php"),
|
||||
},
|
||||
{
|
||||
id: "php.selectLanguageLevel",
|
||||
title: "Select PHP Language Level",
|
||||
category: "Settings",
|
||||
hidden: true,
|
||||
run: async ({ arg }) => {
|
||||
const project = useProject.getState().project;
|
||||
if (!project || typeof arg !== "string" || !PHP_LANGUAGE_LEVELS.some((level) => level === arg)) return;
|
||||
try {
|
||||
const settings = await call(
|
||||
commands.settingsSet("project", "languages.php.languageLevel", JSON.stringify(arg)),
|
||||
);
|
||||
if (useProject.getState().project === project) useProject.setState({ settings });
|
||||
} catch (e) {
|
||||
notifications().notify({ level: "error", title: "Cannot select PHP language level", body: String(e) });
|
||||
}
|
||||
},
|
||||
},
|
||||
]),
|
||||
];
|
||||
return () => {
|
||||
reset();
|
||||
dispose.reverse().forEach((d) => d());
|
||||
};
|
||||
}
|
||||
@@ -4,7 +4,16 @@
|
||||
*/
|
||||
import { Channel } from "@tauri-apps/api/core";
|
||||
import { create } from "zustand";
|
||||
import { call, commands, isTauri, nextRequestId, type PythonPackage, type Sdk, type SdkKind } from "@/platform/ipc";
|
||||
import {
|
||||
call,
|
||||
commands,
|
||||
isTauri,
|
||||
nextRequestId,
|
||||
type PhpLanguageLevels,
|
||||
type PythonPackage,
|
||||
type Sdk,
|
||||
type SdkKind,
|
||||
} from "@/platform/ipc";
|
||||
|
||||
export type { PythonPackage, Sdk, SdkKind };
|
||||
|
||||
@@ -97,6 +106,11 @@ export function effectivePython(): Promise<string | null> {
|
||||
return isTauri() ? call(commands.sdkPythonEffective()) : Promise.resolve(null);
|
||||
}
|
||||
|
||||
/** The PHP language level of the project `path` belongs to; null outside Tauri. */
|
||||
export function phpLanguageLevel(path: string): Promise<PhpLanguageLevels | null> {
|
||||
return isTauri() ? call(commands.sdkPhpLanguageLevel(path)) : Promise.resolve(null);
|
||||
}
|
||||
|
||||
/** File / folder chooser (Tauri dialog); null when cancelled or outside Tauri. */
|
||||
export async function choosePath(opts: {
|
||||
directory: boolean;
|
||||
|
||||
Reference in new issue
Заблокувати користувача