feat(php): list PHP versions newest first

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.
This commit is contained in:
l0ng-ai
2026-11-05 23:54:27 +08:00
parent 97177486e9
commit b372c21e6f
6 changed files with 71 additions and 19 deletions
+1 -1
View File
@@ -143,7 +143,7 @@ fn nvm_bins(dir: &Path) -> Vec<PathBuf> {
}
/// `v22.10.1` as `[22, 10, 1]`: v22 is newer than v9, v20.10 newer than v20.9.
fn version_key(name: &str) -> Vec<u64> {
pub fn version_key(name: &str) -> Vec<u64> {
name.trim_start_matches('v').split('.').map(|n| n.parse().unwrap_or(0)).collect()
}
+11 -2
View File
@@ -367,7 +367,7 @@ fn php() -> Vec<Sdk> {
}
}
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<Sdk> {
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<Sdk> {
+23 -1
View File
@@ -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}");
}
@@ -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", () => {
@@ -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<string | null>(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<string>("languages.php.interpreter");
const [level, setLevel] = useSetting<string>("languages.php.languageLevel");
const [includePaths, setIncludePaths] = useSetting<string[]>("languages.php.includePaths");
const [stubs, setStubs] = useSetting<string[]>("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 (
<div className={styles.page}>
<Section>
+12 -12
View File
@@ -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 {