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 {