diff --git a/crates/ide-host/src/ops/sdk.rs b/crates/ide-host/src/ops/sdk.rs index f40eb221a..46db0eaad 100644 --- a/crates/ide-host/src/ops/sdk.rs +++ b/crates/ide-host/src/ops/sdk.rs @@ -186,10 +186,11 @@ pub async fn sdk_locate_tools( .map_err(join_err) } -/// `base -m venv location`; output lines stream through `on_output`. +/// Creates a Python environment with `tool` (`venv` / `uv` / `poetry`); output streams through `on_output`. pub async fn sdk_python_create_venv( host: &Host, window: &str, + tool: ide_sdk::python::VenvTool, base: PathBuf, location: PathBuf, request: RequestId, @@ -198,7 +199,7 @@ pub async fn sdk_python_create_venv( host.require_trust(window)?; let cancel = host.begin_project_request(window, request); let result = tokio::task::spawn_blocking(move || { - ide_sdk::python::create_venv(&base, &location, &cancel, &|line| { + ide_sdk::python::create_venv(tool, &base, &location, &cancel, &|line| { let _ = on_output.send(line); }) }) @@ -275,6 +276,7 @@ pub(crate) fn register(d: &mut Dispatcher) { sdk_python_create_venv( &c.host, &c.window, + c.arg("tool")?, c.arg("base")?, c.arg("location")?, c.arg("request")?, diff --git a/crates/ide-sdk/src/python.rs b/crates/ide-sdk/src/python.rs index dcaecebe3..35c33beee 100644 --- a/crates/ide-sdk/src/python.rs +++ b/crates/ide-sdk/src/python.rs @@ -16,25 +16,105 @@ pub struct PythonPackage { pub version: String, } -/// Creates a virtualenv at `location` with `base -m venv` and returns its interpreter. +/// The tool that creates a new Python environment in "Add Python Interpreter". +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "lowercase")] +pub enum VenvTool { + /// `base -m venv `. + Venv, + /// `uv venv --python ` (uv must be on PATH). + Uv, + /// `poetry env use ` in the project, then `poetry env info --path`. + Poetry, +} + +/// Creates a virtualenv at `location` with `tool` and returns its interpreter. +/// `base` is the interpreter uv / venv build on, or the one poetry's env uses. pub fn create_venv( + tool: VenvTool, base: &Path, location: &Path, cancel: &CancellationToken, on_line: &dyn Fn(String), ) -> IdeResult { - if location.join("pyvenv.cfg").is_file() { - return Err(IdeError::new( - ide_core::error::ErrorKind::AlreadyExists, - format!("{} already contains a virtual environment", location.display()), - )); + create_venv_with(tool, base, location, which, cancel, on_line) +} + +/// [`create_venv`] with the tool lookup injectable (tests fake `uv` / `poetry`). +pub(crate) fn create_venv_with( + tool: VenvTool, + base: &Path, + location: &Path, + which: impl Fn(&str) -> Option, + cancel: &CancellationToken, + on_line: &dyn Fn(String), +) -> IdeResult { + match tool { + VenvTool::Venv | VenvTool::Uv => { + if location.join("pyvenv.cfg").is_file() { + return Err(IdeError::new( + ide_core::error::ErrorKind::AlreadyExists, + format!("{} already contains a virtual environment", location.display()), + )); + } + if let Some(parent) = location.parent() { + std::fs::create_dir_all(parent)?; + } + let (program, args) = match tool { + VenvTool::Uv => { + let uv = which("uv").ok_or_else(|| { + IdeError::unavailable("uv is not installed (install it from https://docs.astral.sh/uv)") + })?; + ( + uv, + vec![ + "venv".to_owned(), + location.to_string_lossy().into_owned(), + "--python".to_owned(), + base.to_string_lossy().into_owned(), + ], + ) + } + _ => (base.to_path_buf(), venv_args(location)), + }; + stream(&program, &args, None, &[], cancel, on_line)?; + let python = location.join(bin_dir()).join(exe("python")); + crate::discover::python_sdk(&python, "venv", location.parent().and_then(Path::parent)) + .ok_or_else(|| IdeError::internal(format!("{} was not created", python.display()))) + } + VenvTool::Poetry => create_poetry_env(base, location, &which, cancel, on_line), } - if let Some(parent) = location.parent() { - std::fs::create_dir_all(parent)?; - } - stream(base, &venv_args(location), None, &[], cancel, on_line)?; - let python = location.join(bin_dir()).join(exe("python")); - crate::discover::python_sdk(&python, "venv", location.parent().and_then(Path::parent)) +} + +/// `poetry env use ` in the project directory (the parent of `location`, which the +/// dialog fills with `/.venv` only as a hint — poetry keeps its envs under its own +/// cache), then `poetry env info --path` for the interpreter it made or reused. +fn create_poetry_env( + base: &Path, + location: &Path, + which: &dyn Fn(&str) -> Option, + cancel: &CancellationToken, + on_line: &dyn Fn(String), +) -> IdeResult { + let poetry = which("poetry") + .ok_or_else(|| IdeError::unavailable("Poetry is not installed (install it from https://python-poetry.org)"))?; + let project = location + .parent() + .filter(|p| p.is_dir()) + .ok_or_else(|| IdeError::invalid("Poetry environments need an open project directory"))?; + stream( + &poetry, + &["env".to_owned(), "use".to_owned(), base.to_string_lossy().into_owned()], + Some(project), + &[], + cancel, + on_line, + )?; + let home = query_in(&poetry, &["env", "info", "--path"], Some(project), &[], Duration::from_secs(30)) + .map(PathBuf::from) + .ok_or_else(|| IdeError::internal("poetry created no environment".to_owned()))?; + let python = home.join(bin_dir()).join(exe("python")); + crate::discover::python_sdk(&python, "venv", Some(project)) .ok_or_else(|| IdeError::internal(format!("{} was not created", python.display()))) } diff --git a/crates/ide-sdk/src/tests.rs b/crates/ide-sdk/src/tests.rs index 2d1043a98..ceb43473b 100644 --- a/crates/ide-sdk/src/tests.rs +++ b/crates/ide-sdk/src/tests.rs @@ -720,6 +720,50 @@ fn venv_creation_ignores_modules_in_the_working_directory() { assert!(location.join("pyvenv.cfg").is_file()); } +#[test] +fn env_tools_are_reported_missing_before_they_run() { + // Without uv / Poetry on PATH the error names the install, not a spawn failure. + let dir = tempfile::tempdir().unwrap(); + let location = dir.path().join(".venv"); + let base = dir.path().join("python"); + fn none(_: &str) -> Option { + None + } + let uv = crate::python::create_venv_with( + crate::python::VenvTool::Uv, + &base, + &location, + none, + &Default::default(), + &|_| {}, + ) + .unwrap_err(); + assert!(uv.message.contains("uv is not installed"), "{}", uv.message); + let poetry = crate::python::create_venv_with( + crate::python::VenvTool::Poetry, + &base, + &location, + none, + &Default::default(), + &|_| {}, + ) + .unwrap_err(); + assert!(poetry.message.contains("Poetry is not installed"), "{}", poetry.message); + // An existing environment is refused before any tool runs. + std::fs::create_dir_all(&location).unwrap(); + std::fs::write(location.join("pyvenv.cfg"), "home = /python").unwrap(); + let again = crate::python::create_venv_with( + crate::python::VenvTool::Uv, + &base, + &location, + none, + &Default::default(), + &|_| {}, + ) + .unwrap_err(); + assert!(again.message.contains("already contains a virtual environment"), "{}", again.message); +} + #[test] fn neutral_dir_is_fresh_and_empty() { let a = crate::python::neutral_dir().unwrap(); diff --git a/src-tauri/src/commands/sdk.rs b/src-tauri/src/commands/sdk.rs index 6546b3e90..a8053f8c4 100644 --- a/src-tauri/src/commands/sdk.rs +++ b/src-tauri/src/commands/sdk.rs @@ -93,12 +93,13 @@ pub async fn sdk_locate_tools( ide_host::ops::sdk::sdk_locate_tools(&state.host, window.label(), names, near).await } -/// `base -m venv location`; output lines stream through `on_output`. +/// Creates a Python environment with `tool` (`venv` / `uv` / `poetry`); output streams through `on_output`. #[tauri::command] #[specta::specta] pub async fn sdk_python_create_venv( window: Window, state: State<'_, AppState>, + tool: ide_sdk::python::VenvTool, base: PathBuf, location: PathBuf, request: RequestId, @@ -107,6 +108,7 @@ pub async fn sdk_python_create_venv( ide_host::ops::sdk::sdk_python_create_venv( &state.host, window.label(), + tool, base, location, request, diff --git a/src/bindings.ts b/src/bindings.ts index 9f4fb684a..0ba380462 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -820,8 +820,8 @@ export const commands = { sdkProjectLibraries: () => typedError(__TAURI_INVOKE("sdk_project_libraries")), /** Looks up `names` (bare command names): `node_modules/.bin` above `near`, then the search path. */ sdkLocateTools: (names: string[], near: string | null) => typedError(__TAURI_INVOKE("sdk_locate_tools", { names, near })), - /** `base -m venv location`; output lines stream through `on_output`. */ - sdkPythonCreateVenv: (base: string, location: string, request: RequestId, onOutput: Channel) => typedError(__TAURI_INVOKE("sdk_python_create_venv", { base, location, request, onOutput })), + /** Creates a Python environment with `tool` (`venv` / `uv` / `poetry`); output streams through `on_output`. */ + sdkPythonCreateVenv: (tool: VenvTool, base: string, location: string, request: RequestId, onOutput: Channel) => typedError(__TAURI_INVOKE("sdk_python_create_venv", { tool, base, location, request, onOutput })), sdkPythonPackages: (python: string, request: RequestId) => typedError(__TAURI_INVOKE("sdk_python_packages", { python, request })), /** Installs (`uninstall = false`) or removes a package; output streams through `on_output`. */ sdkPythonPip: (python: string, requirement: string, uninstall: boolean, upgrade: boolean, request: RequestId, onOutput: Channel) => typedError(__TAURI_INVOKE("sdk_python_pip", { python, requirement, uninstall, upgrade, request, onOutput })), @@ -7778,6 +7778,15 @@ export type VcsSettings = { issueNavigation?: IssueLink[], }; +/** The tool that creates a new Python environment in "Add Python Interpreter". */ +export type VenvTool = +/** `base -m venv `. */ +"venv" | +/** `uv venv --python ` (uv must be on PATH). */ +"uv" | +/** `poetry env use ` in the project, then `poetry env info --path`. */ +"poetry"; + /** What a view or custom editor shows. */ export type View = { toolbar: ViewButton[], diff --git a/src/features/settings/__tests__/addInterpreter.test.tsx b/src/features/settings/__tests__/addInterpreter.test.tsx new file mode 100644 index 000000000..b7198e382 --- /dev/null +++ b/src/features/settings/__tests__/addInterpreter.test.tsx @@ -0,0 +1,126 @@ +import { act, fireEvent, render, screen, waitFor, cleanup } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { Sdk } from "@/bindings"; + +const mocks = vi.hoisted(() => ({ + detect: vi.fn(), + createVenv: vi.fn(), + choosePath: vi.fn(), +})); +vi.mock("@/platform/ipc", async (importOriginal) => { + const real = await importOriginal(); + return { + ...real, + isTauri: () => true, + commands: { + ...real.commands, + sdkDetect: mocks.detect, + sdkPythonCreateVenv: mocks.createVenv, + cancelRequest: vi.fn(), + }, + }; +}); +vi.mock("@tauri-apps/plugin-dialog", () => ({ open: mocks.choosePath })); + +const { AddInterpreterDialog } = await import("../languages/AddInterpreterDialog"); +const { useSdks } = await import("../languages/api"); + +const SYSTEM: Sdk = { + path: "/usr/bin/python3", + name: "Python 3.12", + version: "3.12.0", + source: "system", + home: "/usr", + extra: null, +}; +const VENV: Sdk = { + path: "/project/.venv/bin/python", + name: "Python 3.12 (.venv)", + version: "3.12.0", + source: "venv", + home: "/project/.venv", + extra: null, +}; + +const ok = (data: T) => ({ status: "ok" as const, data }); + +beforeEach(() => { + Object.assign(window, { __TAURI_INTERNALS__: { transformCallback: () => 1 } }); + mocks.detect.mockResolvedValue(ok([VENV, SYSTEM])); + mocks.createVenv.mockResolvedValue(new Promise(() => {})); + useSdks.setState({ byKind: {}, loading: {}, errors: {} }); +}); +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +const open = (onAdded = vi.fn()) => { + render( {}} onAdded={onAdded} />); + return onAdded; +}; + +describe("Add Python Interpreter", () => { + it("lists the environment types PyCharm offers", async () => { + open(); + await waitFor(() => expect(mocks.detect).toHaveBeenCalled()); + for (const label of ["Virtualenv Environment", "uv Environment", "Poetry Environment"]) { + expect(screen.getByRole("button", { name: label })).toBeInTheDocument(); + } + }); + + it("creates a virtualenv with the base interpreter by default", async () => { + let finish: (sdk: Sdk) => void = () => {}; + mocks.createVenv.mockReturnValue(new Promise((r) => (finish = (s) => r(ok(s))))); + const onAdded = open(); + await waitFor(() => expect(screen.getByRole("button", { name: "Base interpreter" })).toBeInTheDocument()); + fireEvent.click(screen.getByRole("button", { name: "OK" })); + expect(mocks.createVenv).toHaveBeenCalledWith( + "venv", + "/usr/bin/python3", + "/project/.venv", + expect.anything(), + expect.anything(), + ); + await act(async () => finish(VENV)); + await waitFor(() => expect(onAdded).toHaveBeenCalledWith("/project/.venv/bin/python")); + }); + + it("passes uv as the tool and Poetry hides the location field", async () => { + let finish: (v: unknown) => void = () => {}; + mocks.createVenv.mockImplementation(() => new Promise((r) => (finish = r))); + open(); + await waitFor(() => expect(screen.getByRole("button", { name: "Base interpreter" })).toBeInTheDocument()); + fireEvent.click(screen.getByRole("button", { name: "uv Environment" })); + fireEvent.click(screen.getByRole("button", { name: "OK" })); + expect(mocks.createVenv).toHaveBeenCalledWith( + "uv", + "/usr/bin/python3", + "/project/.venv", + expect.anything(), + expect.anything(), + ); + await act(async () => finish(ok(VENV))); + fireEvent.click(screen.getByRole("button", { name: "Poetry Environment" })); + await waitFor(() => expect(screen.getByLabelText("Location:")).toBeDisabled()); + fireEvent.click(screen.getByRole("button", { name: "OK" })); + expect(mocks.createVenv).toHaveBeenLastCalledWith( + "poetry", + "/usr/bin/python3", + "/project/.venv", + expect.anything(), + expect.anything(), + ); + }); + + it("adds an existing interpreter without creating anything", async () => { + const onAdded = open(); + await waitFor(() => expect(screen.getByRole("button", { name: "Base interpreter" })).toBeInTheDocument()); + fireEvent.click(screen.getByRole("radio", { name: "Existing environment" })); + fireEvent.click(screen.getByRole("button", { name: "Interpreter" })); + fireEvent.click(await screen.findByRole("option", { name: /Python 3\.12 \(\.venv\)/ })); + fireEvent.click(screen.getByRole("button", { name: "OK" })); + expect(mocks.createVenv).not.toHaveBeenCalled(); + expect(onAdded).toHaveBeenCalledWith("/project/.venv/bin/python"); + }); +}); diff --git a/src/features/settings/languages/AddInterpreterDialog.tsx b/src/features/settings/languages/AddInterpreterDialog.tsx index cfa4148a0..3d50a2a00 100644 --- a/src/features/settings/languages/AddInterpreterDialog.tsx +++ b/src/features/settings/languages/AddInterpreterDialog.tsx @@ -1,17 +1,24 @@ /** - * PyCharm's "Add Python Interpreter" (local): generate a new virtualenv from a - * base interpreter (`python -m venv`) or select an existing interpreter. + * PyCharm's "Add Python Interpreter" (local): generate a new environment with a + * tool (virtualenv / uv / Poetry) or select an existing interpreter. */ import { isCancelled } from "@/platform/ipc"; import { useId, useMemo, useState } from "react"; import { Dialog, IconButton, Input, Select, Spinner, type SelectOption, T } from "@/ui"; -import { choosePath, createVenv, rememberSdk } from "./api"; +import { choosePath, createVenv, rememberSdk, type VenvTool } from "./api"; import { tildify, useDetected } from "./components"; import styles from "./Sdk.module.css"; import { tx } from "@/platform/i18n"; type Mode = "new" | "existing"; +/** Environment types of the left column, in PyCharm's order. */ +const KINDS: { tool: VenvTool; label: string; missing: string }[] = [ + { tool: "venv", label: "Virtualenv Environment", missing: "" }, + { tool: "uv", label: "uv Environment", missing: "uv is not installed" }, + { tool: "poetry", label: "Poetry Environment", missing: "Poetry is not installed" }, +]; + export function AddInterpreterDialog({ root, onClose, @@ -22,6 +29,7 @@ export function AddInterpreterDialog({ onAdded: (python: string) => void; }) { const { list, loading } = useDetected("python"); + const [kind, setKind] = useState("venv"); const [mode, setMode] = useState("new"); const bases = useMemo(() => list.filter((s) => s.source !== "venv"), [list]); const [base, setBase] = useState(null); @@ -54,7 +62,7 @@ export function AddInterpreterDialog({ } if (!baseValue || !location.trim()) return; setLog([]); - const job = createVenv(baseValue, location.trim(), (line) => setLog((l) => [...l.slice(-200), line])); + const job = createVenv(kind, baseValue, location.trim(), (line) => setLog((l) => [...l.slice(-200), line])); setBusy({ cancel: job.cancel }); job.promise .then((sdk) => { @@ -66,6 +74,13 @@ export function AddInterpreterDialog({ }; const canOk = mode === "new" ? !!baseValue && !!location.trim() && !busy : !!existing; + const note = { + venv: tx("Runs “ -m venv ”; the new interpreter becomes the project interpreter."), + uv: tx("Runs “uv venv --python ”; the new interpreter becomes the project interpreter."), + poetry: tx( + "Runs “poetry env use ” in the project; poetry keeps the environment under its own cache directory.", + ), + }[kind]; return ( } + footerStart={busy && } buttons={[ { label: "OK", role: "default", disabled: !canOk, onClick: ok }, { label: "Cancel", role: "cancel" }, @@ -82,11 +97,21 @@ export function AddInterpreterDialog({ >
    -
  • - -
  • + {KINDS.map((k) => ( +
  • + +
  • + ))}
@@ -110,11 +135,13 @@ export function AddInterpreterDialog({ className={styles.pathInput} value={location} onChange={(e) => setLocation(e.target.value)} + disabled={kind === "poetry"} /> { const p = await choosePath({ directory: true, title: "Virtualenv Location", defaultPath: root }); if (p) setLocation(p); @@ -143,9 +170,7 @@ export function AddInterpreterDialog({ }} />
-
- {tx("Runs “ -m venv ”; the new interpreter becomes the project interpreter.")} -
+
{note}
) : ( <> diff --git a/src/features/settings/languages/api.ts b/src/features/settings/languages/api.ts index 5001dba99..00ed41afb 100644 --- a/src/features/settings/languages/api.ts +++ b/src/features/settings/languages/api.ts @@ -81,8 +81,16 @@ function streamed( return { promise: run(request, channel), cancel: () => void commands.cancelRequest(request) }; } -export function createVenv(base: string, location: string, onLine: (line: string) => void): Streamed { - return streamed((req, ch) => call(commands.sdkPythonCreateVenv(base, location, req, ch)), onLine); +/** Tool that creates a new Python environment (`VenvTool` in `ide-sdk`). */ +export type VenvTool = "venv" | "uv" | "poetry"; + +export function createVenv( + tool: VenvTool, + base: string, + location: string, + onLine: (line: string) => void, +): Streamed { + return streamed((req, ch) => call(commands.sdkPythonCreateVenv(tool, base, location, req, ch)), onLine); } export function listPackages(python: string): Promise { diff --git a/src/platform/locales/text/es.json b/src/platform/locales/text/es.json index c83127272..e1274fb80 100644 --- a/src/platform/locales/text/es.json +++ b/src/platform/locales/text/es.json @@ -601,7 +601,7 @@ "Create Terminal": "Crear terminal", "Create Terminal in Container": "Crear terminal en el contenedor", "Create UTF-8 files": "Crear archivos UTF-8", - "Creating virtual environment": "Creando entorno virtual", + "Creating environment": "Creando entorno virtual", "Current": "Actual", "Current Branch": "Rama actual", "Current File": "Archivo actual", diff --git a/src/platform/locales/text/fr.json b/src/platform/locales/text/fr.json index da2d181b6..990e299ae 100644 --- a/src/platform/locales/text/fr.json +++ b/src/platform/locales/text/fr.json @@ -541,7 +541,7 @@ "Create Terminal": "Créer un terminal", "Create Terminal in Container": "Créer un terminal dans le conteneur", "Create UTF-8 files": "Créer des fichiers UTF-8", - "Creating virtual environment": "Création de l'environnement virtuel", + "Creating environment": "Création de l'environnement virtuel", "Current": "Actuel", "Current Branch": "Branche actuelle", "Current File": "Fichier actuel", diff --git a/src/platform/locales/text/ko.json b/src/platform/locales/text/ko.json index 20db74aa9..d061836d4 100644 --- a/src/platform/locales/text/ko.json +++ b/src/platform/locales/text/ko.json @@ -643,7 +643,7 @@ "Create Terminal": "터미널 생성", "Create Terminal in Container": "컨테이너에서 터미널 생성", "Create UTF-8 files": "UTF-8 파일 생성", - "Creating virtual environment": "가상 환경 생성 중", + "Creating environment": "가상 환경 생성 중", "Current": "현재", "Current Branch": "현재 브랜치", "Current File": "현재 파일", diff --git a/src/platform/locales/text/pl.json b/src/platform/locales/text/pl.json index 34a27de5b..3435a68c4 100644 --- a/src/platform/locales/text/pl.json +++ b/src/platform/locales/text/pl.json @@ -593,7 +593,7 @@ "Create Terminal": "Utwórz terminal", "Create Terminal in Container": "Utwórz terminal w kontenerze", "Create UTF-8 files": "Twórz pliki w kodowaniu UTF-8", - "Creating virtual environment": "Tworzenie środowiska wirtualnego", + "Creating environment": "Tworzenie środowiska wirtualnego", "Current": "Bieżący", "Current Branch": "Bieżąca gałąź", "Current File": "Bieżący plik", diff --git a/src/platform/locales/text/pt.json b/src/platform/locales/text/pt.json index fc835b6d0..5f01749db 100644 --- a/src/platform/locales/text/pt.json +++ b/src/platform/locales/text/pt.json @@ -593,7 +593,7 @@ "Create Terminal": "Criar terminal", "Create Terminal in Container": "Criar terminal no contêiner", "Create UTF-8 files": "Criar arquivos UTF-8", - "Creating virtual environment": "Criando ambiente virtual", + "Creating environment": "Criando ambiente virtual", "Current": "Atual", "Current Branch": "Branch atual", "Current File": "Arquivo atual", diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index 46cb6a388..d4b498ebf 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -648,7 +648,7 @@ "Create Terminal": "创建终端", "Create Terminal in Container": "在容器中创建终端", "Create UTF-8 files": "创建 UTF-8 文件", - "Creating virtual environment": "正在创建虚拟环境", + "Creating environment": "正在创建环境", "Current": "当前", "Current Branch": "当前分支", "Current File": "当前文件", @@ -1599,6 +1599,7 @@ "Popup type": "弹出类型", "Port:": "端口:", "Postfix Completion": "后缀补全", + "Poetry Environment": "Poetry 环境", "Postfix templates": "后缀模板", "Prefer": "首选", "Prefer moving caret line to minimize editor scrolling": "优先移动光标所在行,以尽量减少编辑器滚动", @@ -2519,6 +2520,7 @@ "Virtual Space": "虚拟空间", "Virtualenv Environment": "Virtualenv 环境", "Virtualenv Location": "Virtualenv 位置", + "uv Environment": "uv 环境", "Visual guides": "视觉参考线", "WHERE filter": "WHERE 筛选器", "Warn if CRLF line separators are about to be committed": "即将提交 CRLF 行分隔符时发出警告", @@ -4680,6 +4682,8 @@ "{count} modified action": "已修改 {count} 个操作", "{count} modified actions": "已修改 {count} 个操作", "Runs “ -m venv ”; the new interpreter becomes the project interpreter.": "运行“ -m venv ”;新的解释器将成为项目解释器。", + "Runs “poetry env use ” in the project; poetry keeps the environment under its own cache directory.": "在项目中运行“poetry env use ”;Poetry 会将环境保存在其自己的缓存目录下。", + "Runs “uv venv --python ”; the new interpreter becomes the project interpreter.": "运行“uv venv --python ”;新的解释器将成为项目解释器。", "{vendor} {version} is unpacked into a folder of its own in {location}, where JDK detection finds it.": "{vendor} {version} 将解压到 {location} 下它自己的文件夹中,JDK 检测会在那里找到它。", "detected from {source}": "检测自 {source}", "modified": "已修改",