From c347e331ab5682b9de414c641931f5d1ec9a1f64 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 4 Nov 2026 05:55:20 +0800 Subject: [PATCH] feat(run): browse for .env files from the Environment variables field (#1106) A folder button next to the Environment variables input opens a file dialog at the project root and appends the chosen files to the configuration's .env files (relative to the root inside the project, duplicates skipped). Hidden in remote windows, where the dialog would pick local paths. --- src/features/run/ConfigForm.tsx | 28 ++++++++++++++++++- src/features/run/envFiles.test.tsx | 45 ++++++++++++++++++++++++++++++ src/platform/locales/text/zh.json | 1 + 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/features/run/envFiles.test.tsx 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": "相对于项目根目录的文件",