diff --git a/src/features/run/ConfigForm.tsx b/src/features/run/ConfigForm.tsx index 3c5e8d30a..f7ecf0fe7 100644 --- a/src/features/run/ConfigForm.tsx +++ b/src/features/run/ConfigForm.tsx @@ -6,8 +6,10 @@ import { parseJavaArgs } from "@/platform/javaLaunch"; import { useMemo, useRef, useState, type KeyboardEvent, type ReactNode, type RefObject } from "react"; import clsx from "clsx"; -import type { BeforeLaunchTask, RunConfig } from "@/platform/ipc"; +import { isTauri, type BeforeLaunchTask, type RunConfig } from "@/platform/ipc"; import { useWorkflowSettings } from "@/platform/workflowSettings"; +import { relativePath, useProject } from "@/platform/project"; +import { isRemoteWindow } from "@/platform/remote"; import { useTx, useTxTemplate } from "@/platform/i18n"; import { Button, @@ -169,6 +171,14 @@ export interface ConfigFormProps { modifyRequest: number; } +/** Picks `.env` files; paths inside the project are returned relative to its root. */ +async function chooseEnvFiles(root: string | null, title: string): Promise { + const { open } = await import("@tauri-apps/plugin-dialog"); + const picked = await open({ multiple: true, title, defaultPath: root ?? undefined }); + const paths = picked === null ? [] : Array.isArray(picked) ? picked : [picked]; + return paths.map((p) => (root ? relativePath(root, p) : p)); +} + export function ConfigForm({ config, all, type, errors, template, onChange, modifyRequest }: ConfigFormProps) { const openByDefault = useWorkflowSettings((s) => s.run.showOnStart); const externalTools = useWorkflowSettings((s) => s.externalTools); @@ -176,6 +186,7 @@ export function ConfigForm({ config, all, type, errors, template, onChange, modi const toolWindows = useContributions((s) => s.toolWindows); const tx = useTx(); const txTemplate = useTxTemplate(); + const root = useProject((s) => s.project?.root ?? null); const [shown, setShown] = useState>(() => new Set()); const [argsText, setArgsText] = useState(null); const [envText, setEnvText] = useState(null); @@ -480,6 +491,21 @@ export function ConfigForm({ config, all, type, errors, template, onChange, modi title="Edit Environment Variables" onClick={() => setEnvOpen(true)} /> + {isTauri() && !isRemoteWindow() && ( + + void chooseEnvFiles(root, tx("Browse for .env Files")).then((files) => { + const current = config.envFiles ?? []; + const added = files.filter((f) => !current.includes(f)); + if (added.length) update({ envFiles: [...current, ...added] }); + }) + } + /> + )} {config.includeSystemEnv === false && ( diff --git a/src/features/run/envFiles.test.tsx b/src/features/run/envFiles.test.tsx new file mode 100644 index 000000000..45a47e7c1 --- /dev/null +++ b/src/features/run/envFiles.test.tsx @@ -0,0 +1,45 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; + +const open = vi.hoisted(() => vi.fn()); +vi.mock("@tauri-apps/plugin-dialog", () => ({ open })); +vi.mock("@/platform/ipc", async (importOriginal) => ({ + ...(await importOriginal()), + isTauri: () => true, +})); + +import type { RunConfig } from "@/platform/ipc"; +import { useProject } from "@/platform/project"; +import { ConfigForm } from "./ConfigForm"; + +const config: RunConfig = { + id: "app", + name: "app", + kind: "shell", + command: "python3", + args: [], + cwd: null, + env: {}, + detected: false, + isTest: false, + debugAdapter: null, + envFiles: [".env"], +}; + +afterEach(cleanup); + +describe("Environment variables", () => { + it("adds the .env files chosen in the file dialog, relative to the project root", async () => { + useProject.setState({ project: { root: "/work/app" } as never }); + open.mockResolvedValue(["/work/app/.env", "/work/app/config/dev.env", "/elsewhere/shared.env"]); + const onChange = vi.fn(); + render(); + fireEvent.click(screen.getByRole("button", { name: "Browse for .env Files" })); + await waitFor(() => + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ envFiles: [".env", "config/dev.env", "/elsewhere/shared.env"] }), + ), + ); + expect(open).toHaveBeenCalledWith(expect.objectContaining({ multiple: true, defaultPath: "/work/app" })); + }); +}); diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index 5e221b6e9..837a4513a 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -2935,6 +2935,7 @@ "Modify options": "修改选项", "e.g. ./gradlew or cargo": "例如 ./gradlew 或 cargo", "Edit Environment Variables": "编辑环境变量", + "Browse for .env Files": "浏览 .env 文件", "System environment variables are not passed": "不传递系统环境变量", "Redirect input from": "重定向输入自", "File relative to the project root": "相对于项目根目录的文件",