diff --git a/crates/ide-host/src/ops/sdk.rs b/crates/ide-host/src/ops/sdk.rs index 702c35c2..78c5aee0 100644 --- a/crates/ide-host/src/ops/sdk.rs +++ b/crates/ide-host/src/ops/sdk.rs @@ -52,6 +52,14 @@ pub fn sdk_python_effective(host: &Host, window: &str) -> IdeResult IdeResult> { + host.require_trust(window)?; + let project = host.project(window)?; + let settings = project.settings.effective(); + Ok(ide_sdk::apply::setting_path(&project.root, &settings.languages.php.interpreter).map(Into::into)) +} + /// The PHP language level of the project `path` belongs to (a workspace member, by path, as /// the language server configuration picks it). #[derive(Clone, Debug, serde::Serialize, specta::Type)] @@ -198,6 +206,7 @@ pub async fn sdk_python_pip( pub(crate) fn register(d: &mut Dispatcher) { d.add("sdk_detect", |c| async move { sdk_detect(&c.host, &c.window, c.arg("kind")?).await }); d.add("sdk_python_effective", |c| async move { sdk_python_effective(&c.host, &c.window) }); + d.add("sdk_php_effective", |c| async move { sdk_php_effective(&c.host, &c.window) }); d.add( "sdk_php_language_level", |c| async move { sdk_php_language_level(&c.host, &c.window, c.arg("path")?).await }, diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index aca0338a..72ed8e0e 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -398,6 +398,7 @@ pub fn builder() -> tauri_specta::Builder { tools::jdk_download, sdk::sdk_detect, sdk::sdk_python_effective, + sdk::sdk_php_effective, sdk::sdk_php_language_level, sdk::sdk_project_libraries, sdk::sdk_locate_tools, diff --git a/src-tauri/src/commands/sdk.rs b/src-tauri/src/commands/sdk.rs index db2692f5..bb3db7d3 100644 --- a/src-tauri/src/commands/sdk.rs +++ b/src-tauri/src/commands/sdk.rs @@ -24,6 +24,13 @@ pub fn sdk_python_effective(window: Window, state: State<'_, AppState>) -> IdeRe ide_host::ops::sdk::sdk_python_effective(&state.host, window.label()) } +/// The PHP interpreter chosen in Settings › Languages & Frameworks › PHP; `None`: the `php` on `PATH`. +#[tauri::command] +#[specta::specta] +pub fn sdk_php_effective(window: Window, state: State<'_, AppState>) -> IdeResult> { + ide_host::ops::sdk::sdk_php_effective(&state.host, window.label()) +} + /// The PHP language level of the project `path` belongs to: the effective one and Auto's. #[tauri::command] #[specta::specta] diff --git a/src/bindings.ts b/src/bindings.ts index 8fba6499..2e9cb6dc 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -772,6 +772,8 @@ export const commands = { sdkDetect: (kind: SdkKind) => typedError(__TAURI_INVOKE("sdk_detect", { kind })), /** The interpreter the project uses: the configured one, else its virtualenv. */ sdkPythonEffective: () => typedError(__TAURI_INVOKE("sdk_python_effective")), + /** The PHP interpreter chosen in Settings › Languages & Frameworks › PHP; `None`: the `php` on `PATH`. */ + sdkPhpEffective: () => typedError(__TAURI_INVOKE("sdk_php_effective")), /** The PHP language level of the project `path` belongs to: the effective one and Auto's. */ sdkPhpLanguageLevel: (path: string) => typedError(__TAURI_INVOKE("sdk_php_language_level", { path })), /** diff --git a/src/features/run/contextRun.ts b/src/features/run/contextRun.ts index 8e7f1bed..f600cddb 100644 --- a/src/features/run/contextRun.ts +++ b/src/features/run/contextRun.ts @@ -98,7 +98,13 @@ export async function produceResolved(input: ContextInput, dir = false): Promise // Project configurations (tests, cargo, gradle…) need no runtime lookup. if (named.config && !named.config.id.startsWith("context:file:")) return named; const lang = scriptLanguage(input.path, input.content); - const env = await toolEnv(neededTools(fresh), input.path, fresh.root, lang === "python" || lang === "shebang"); + const env = await toolEnv( + neededTools(fresh), + input.path, + fresh.root, + lang === "python" || lang === "shebang", + lang === "php" || lang === "shebang", + ); return produceContext(fresh, env); } diff --git a/src/features/run/producers.test.ts b/src/features/run/producers.test.ts index 4c3812cf..194982d7 100644 --- a/src/features/run/producers.test.ts +++ b/src/features/run/producers.test.ts @@ -226,6 +226,15 @@ describe("single-file producers (scratches)", () => { it("Ruby, PHP, Perl, Lua, R, Swift, Dart, Elixir, Scala CLI, Kotlin script", () => { expect(run(scratch("a.rb"))).toMatchObject({ command: "/bin/ruby", args: ["a.rb"] }); expect(run(scratch("a.php"))).toMatchObject({ command: "/bin/php", args: ["a.php"] }); + // The PHP chosen in Settings wins over the PATH one, also with no PHP on PATH (#1555). + const php81 = "/opt/homebrew/opt/php@8.1/bin/php"; + expect(run(scratch("a.php"), { ...ALL, php: php81 })).toMatchObject({ command: php81, args: ["a.php"] }); + expect(run(scratch("a.php"), env({}, { php: php81 }))).toMatchObject({ command: php81, args: ["a.php"] }); + expect(run(scratch("tool", "#!/usr/bin/env php\n = { js: "Install Node.js from https://nodejs.org (or `brew install node`).", python: "Install Python (`brew install python`) or choose an interpreter in Settings › Python Interpreter.", ruby: "Install Ruby (`brew install ruby`).", - php: "Install PHP (`brew install php`).", + php: "Install PHP (`brew install php`) or choose an interpreter in Settings › Languages & Frameworks › PHP.", perl: "Install Perl (`brew install perl`).", lua: "Install Lua (`brew install lua`).", r: "Install R (`brew install r`) so that `Rscript` is on the PATH.", @@ -762,6 +764,8 @@ export function produceFile(input: ContextInput, env: ToolEnv): Produced | null config: fileConfig(s, "python", py.command, [...py.prefix, s.arg], "debugpy"), }; } + if (interp === "php" && env.php) + return { name: s.file, scope: "file", config: fileConfig(s, "php", env.php, [...sb.slice(1), s.arg]) }; const t = sb[0]!.startsWith("/") ? { command: sb[0]!, name: interp } : pick(env, [interp]); if (!t) return { name: s.file, scope: "file", missing: missing(lang, s.file, [interp]) }; const kind = /^(ba|z|k|da|fi)?sh$/.test(interp) ? "shell" : interp === "node" ? "node" : interp; @@ -805,6 +809,8 @@ export function produceFile(input: ContextInput, env: ToolEnv): Produced | null // Sources roots go on the load path (`-Ilib`), as `rake test` does; paths are project-relative. return simple(lang, s, env, lang, () => [...(s.inProject ? rubyLoadPath(currentRoots()) : []), s.arg]); case "php": + if (env.php) return { name: s.file, scope: "file", config: fileConfig(s, "php", env.php, [s.arg]) }; + return simple(lang, s, env, lang, () => [s.arg]); case "perl": case "lua": case "swift": diff --git a/src/features/run/runtimes.ts b/src/features/run/runtimes.ts index b6d21d7b..883d2c7b 100644 --- a/src/features/run/runtimes.ts +++ b/src/features/run/runtimes.ts @@ -86,6 +86,14 @@ async function projectPython(): Promise { } } +async function projectPhp(): Promise { + try { + return await call(commands.sdkPhpEffective()); + } catch { + return null; + } +} + async function rootMarkers(root: string | null): Promise<{ deno: boolean; bun: boolean }> { if (!root) return { deno: false, bun: false }; try { @@ -106,10 +114,12 @@ export async function toolEnv( file: string | null, root: string | null, python: boolean, + php = false, ): Promise { - const [tools, py, markers] = await Promise.all([ + const [tools, py, phpInterpreter, markers] = await Promise.all([ locateTools(names, file), python ? projectPython() : Promise.resolve(null), + php ? projectPhp() : Promise.resolve(null), names.includes("deno") || names.includes("bun") ? rootMarkers(root) : Promise.resolve({ deno: false, bun: false }), ]); return { @@ -117,6 +127,7 @@ export async function toolEnv( compileDir: compileDir ?? "/tmp/openidea-run", windows: isWindows(), python: py, + php: phpInterpreter, ...markers, }; }