From 2c6131999cd659637790c7e5b337cb5588087739 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Thu, 5 Nov 2026 23:54:45 +0800 Subject: [PATCH] feat(php): list PHP versions newest first (#1587) Detected PHP interpreters are sorted newest first, number by number and stable (PATH install first among equal versions), and the language level select and status bar menu list Auto, then 8.5 down to 5.6. With Default selected, the Version row now shows the version of the php found on PATH instead of the first list entry. Closes #1557 Co-Authored-By: Claude Opus 5.5 --- crates/ide-core/src/tool_path.rs | 2 +- crates/ide-sdk/src/discover.rs | 13 ++++++++-- crates/ide-sdk/src/tests.rs | 24 ++++++++++++++++++- .../settings/__tests__/languages.test.ts | 4 +++- .../settings/languages/LanguagePages.tsx | 23 ++++++++++++++++-- src/platform/languageSettings.ts | 24 +++++++++---------- 6 files changed, 71 insertions(+), 19 deletions(-) diff --git a/crates/ide-core/src/tool_path.rs b/crates/ide-core/src/tool_path.rs index bc25dd6d2..e088df724 100644 --- a/crates/ide-core/src/tool_path.rs +++ b/crates/ide-core/src/tool_path.rs @@ -143,7 +143,7 @@ fn nvm_bins(dir: &Path) -> Vec { } /// `v22.10.1` as `[22, 10, 1]`: v22 is newer than v9, v20.10 newer than v20.9. -fn version_key(name: &str) -> Vec { +pub fn version_key(name: &str) -> Vec { name.trim_start_matches('v').split('.').map(|n| n.parse().unwrap_or(0)).collect() } diff --git a/crates/ide-sdk/src/discover.rs b/crates/ide-sdk/src/discover.rs index 4870f4b0d..0cb75532b 100644 --- a/crates/ide-sdk/src/discover.rs +++ b/crates/ide-sdk/src/discover.rs @@ -367,7 +367,7 @@ fn php() -> Vec { } } let found = dedupe_by_real_path(found); - par_map(&found, |(path, source)| { + let mut sdks = par_map(&found, |(path, source)| { let version = query(path, &["-r", "echo PHP_VERSION;"], VERSION_TIMEOUT)?; let version = version.trim().to_owned(); if version.is_empty() { @@ -381,7 +381,16 @@ fn php() -> Vec { home: path.parent().and_then(Path::parent).map(Into::into), extra: None, }) - }) + }); + newest_first(&mut sdks); + sdks +} + +/// Newest version first, number by number (8.3.12 before 8.3.9); stable, so on equal +/// versions the `PATH` install stays first. +pub(crate) fn newest_first(sdks: &mut [Sdk]) { + let key = |s: &Sdk| ide_core::tool_path::version_key(s.version.as_deref().unwrap_or("")); + sdks.sort_by_key(|s| std::cmp::Reverse(key(s))); } fn node() -> Vec { diff --git a/crates/ide-sdk/src/tests.rs b/crates/ide-sdk/src/tests.rs index b0b2561e2..1cf235e58 100644 --- a/crates/ide-sdk/src/tests.rs +++ b/crates/ide-sdk/src/tests.rs @@ -1322,7 +1322,29 @@ fn php_sources_and_selected_interpreter() { assert_eq!(source("/opt/homebrew/opt/php@8.2/bin/php"), "homebrew"); assert_eq!(source("/home/me/bin/php"), "path"); - use crate::discover::{is_versioned_php_name, versioned_php_in}; + use crate::discover::{is_versioned_php_name, newest_first, versioned_php_in}; + let sdk = |path: &str, version: &str| crate::Sdk { + path: std::path::PathBuf::from(path).into(), + name: format!("PHP {version}"), + version: Some(version.into()), + source: "path".into(), + home: None, + extra: None, + }; + let mut sdks = vec![ + sdk("/opt/homebrew/bin/php", "8.4.1"), + sdk("/opt/homebrew/opt/php@8.1/bin/php", "8.1.31"), + sdk("/opt/homebrew/opt/php@8.2/bin/php", "8.2.20"), + sdk("/home/me/.phpbrew/php/php-8.3.9/bin/php", "8.3.9"), + sdk("/home/me/.phpbrew/php/php-8.3.12/bin/php", "8.3.12"), + sdk("/home/me/.phpbrew/php/php-7.4.33/bin/php", "7.4.33"), + sdk("/usr/bin/php", "8.4.1"), + ]; + newest_first(&mut sdks); + let order: Vec<_> = sdks.iter().map(|s| s.version.clone().unwrap()).collect(); + assert_eq!(order, ["8.4.1", "8.4.1", "8.3.12", "8.3.9", "8.2.20", "8.1.31", "7.4.33"]); + // Stable: on equal versions the PATH install stays first. + assert_eq!(sdks[0].path.0, std::path::PathBuf::from("/opt/homebrew/bin/php")); for name in ["php8.1", "php81", "php8", "php8.3.exe"] { assert!(is_versioned_php_name(name), "{name}"); } diff --git a/src/features/settings/__tests__/languages.test.ts b/src/features/settings/__tests__/languages.test.ts index f542a9fe6..35268e631 100644 --- a/src/features/settings/__tests__/languages.test.ts +++ b/src/features/settings/__tests__/languages.test.ts @@ -78,7 +78,9 @@ describe("languages settings", () => { } expect(PHP_LANGUAGE_LEVELS[0]).toBe(""); expect(PHP_LANGUAGE_LEVELS).toContain("5.6"); - expect(PHP_LANGUAGE_LEVELS.at(-1)).toBe("8.5"); + // Newest first after Auto (#1557). + expect(PHP_LANGUAGE_LEVELS[1]).toBe("8.5"); + expect(PHP_LANGUAGE_LEVELS.at(-1)).toBe("5.6"); }); it("finds the PHP page by its language level", () => { diff --git a/src/features/settings/languages/LanguagePages.tsx b/src/features/settings/languages/LanguagePages.tsx index fc1cab3de..3276b1f18 100644 --- a/src/features/settings/languages/LanguagePages.tsx +++ b/src/features/settings/languages/LanguagePages.tsx @@ -3,7 +3,8 @@ * Build Tools › Maven: the toolchain each language's server and run configurations * use (applied by `ide-sdk` when language servers start and when run configurations load). */ -import { useId, useMemo } from "react"; +import { useEffect, useId, useMemo, useState } from "react"; +import { call, commands } from "@/platform/ipc"; import { IconButton, Input, type SelectOption } from "@/ui"; import { PHP_LANGUAGE_LEVELS } from "@/platform/languageSettings"; import { GlobListEditor, Row, Section, Select, Toggle } from "../components/Form"; @@ -117,13 +118,31 @@ const PHP_LEVEL_OPTIONS: SelectOption[] = PHP_LANGUAGE_LEVELS.map((level) => level ? { value: level, label: level } : { value: "", label: "Auto", detail: "from composer.json" }, ); +/** The `php` on `PATH` (what the Default interpreter runs), looked up while `enabled`. */ +function usePathPhp(enabled: boolean): string | null { + const [path, setPath] = useState(null); + useEffect(() => { + if (!enabled) return; + let live = true; + call(commands.sdkLocateTools(["php"], null)) + .then((r) => live && setPath(r.tools[0]?.path ?? null)) + .catch(() => {}); + return () => { + live = false; + }; + }, [enabled]); + return path; +} + export function PhpPage() { const [interpreter] = useSetting("languages.php.interpreter"); const [level, setLevel] = useSetting("languages.php.languageLevel"); const [includePaths, setIncludePaths] = useSetting("languages.php.includePaths"); const [stubs, setStubs] = useSetting("languages.php.stubs"); const { list } = useDetected("php"); - const selected = list.find((s) => s.path === interpreter) ?? (interpreter ? undefined : list[0]); + const pathPhp = usePathPhp(!interpreter); + // Default runs the `php` on PATH: the list is newest first, so it isn't necessarily `list[0]`. + const selected = list.find((s) => s.path === (interpreter || pathPhp)); return (
diff --git a/src/platform/languageSettings.ts b/src/platform/languageSettings.ts index 519c648ca..00295ae16 100644 --- a/src/platform/languageSettings.ts +++ b/src/platform/languageSettings.ts @@ -62,21 +62,21 @@ const DEFAULT_RUFF_FORMATTER: ExternalFormatterValid = { languages: ["python"], }; -/** `languages.php.languageLevel` values; `""` = Auto (from `composer.json`). */ +/** `languages.php.languageLevel` values, newest first; `""` = Auto (from `composer.json`). */ export const PHP_LANGUAGE_LEVELS = [ "", - "5.6", - "7.0", - "7.1", - "7.2", - "7.3", - "7.4", - "8.0", - "8.1", - "8.2", - "8.3", - "8.4", "8.5", + "8.4", + "8.3", + "8.2", + "8.1", + "8.0", + "7.4", + "7.3", + "7.2", + "7.1", + "7.0", + "5.6", ] as const; export interface LanguagesValid {