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 <[email protected]>
This commit is contained in:
l0ng-aiandClaude Opus 5.5 authored and GitHub committed 2026-11-05 23:54:45 +08:00
1 parent 97177486e9
commit 2c6131999c
6 files changed
+71 -19

No files matched your search

+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/[email protected]/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/[email protected]/bin/php", "8.1.31"),
sdk("/opt/homebrew/opt/[email protected]/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 {