From b372c21e6f1548ead4eee898bfd0564a4fa5a004 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Thu, 5 Nov 2026 23:54:27 +0800 Subject: [PATCH] feat(php): list PHP versions newest first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The interpreter list kept discovery order (PATH, Homebrew, phpbrew, phpenv, Herd…) and the language levels ran from 5.6 up to 8.5, so finding a version meant reading every row. Detected interpreters are now sorted newest first, number by number (8.3.12 before 8.3.9; stable, so the PATH install stays first among equal versions), and the language level select and status bar menu list Auto, then 8.5 down to 5.6. Saved levels are unaffected. With Default selected, the Version row showed the first list entry, which after sorting is not necessarily the php Default runs; it now shows the version of the php found on PATH. --- 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 bc25dd6d..e088df72 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 4870f4b0..0cb75532 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 b0b2561e..1cf235e5 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 f542a9fe..35268e63 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 fc1cab3d..3276b1f1 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 519c648c..00295ae1 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 {