/** * PyCharm's "Add Python Interpreter" (local): generate a new virtualenv from a * base interpreter (`python -m venv`) 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 { tildify, useDetected } from "./components"; import styles from "./Sdk.module.css"; import { tx } from "@/platform/i18n"; type Mode = "new" | "existing"; export function AddInterpreterDialog({ root, onClose, onAdded, }: { root: string; onClose: () => void; onAdded: (python: string) => void; }) { const { list, loading } = useDetected("python"); const [mode, setMode] = useState("new"); const bases = useMemo(() => list.filter((s) => s.source !== "venv"), [list]); const [base, setBase] = useState(null); const [location, setLocation] = useState(`${root}/.venv`); const [existing, setExisting] = useState(null); const [busy, setBusy] = useState<{ cancel: () => void } | null>(null); const [log, setLog] = useState([]); const [error, setError] = useState(null); const ids = { base: useId(), location: useId(), existing: useId() }; const baseValue = base ?? bases[0]?.path ?? null; const toOption = (s: { path: string; name: string }): SelectOption => ({ value: s.path, label: s.name, detail: tildify(s.path), }); const baseOptions = bases.map(toOption); const existingOptions = useMemo(() => { const opts = list.map(toOption); if (existing && !opts.some((o) => o.value === existing)) opts.push({ value: existing, label: tildify(existing), detail: "custom" }); return opts; }, [list, existing]); const ok = () => { setError(null); if (mode === "existing") { if (existing) onAdded(existing); return; } if (!baseValue || !location.trim()) return; setLog([]); const job = createVenv(baseValue, location.trim(), (line) => setLog((l) => [...l.slice(-200), line])); setBusy({ cancel: job.cancel }); job.promise .then((sdk) => { rememberSdk("python", sdk); onAdded(sdk.path); }) .catch((e: unknown) => setError(isCancelled(e) ? null : String((e as Error)?.message ?? e))) .finally(() => setBusy(null)); }; const canOk = mode === "new" ? !!baseValue && !!location.trim() && !busy : !!existing; return ( (busy ? busy.cancel() : onClose())} title="Add Python Interpreter" width={680} aria-label="Add Python Interpreter" footerStart={busy && } buttons={[ { label: "OK", role: "default", disabled: !canOk, onClick: ok }, { label: "Cancel", role: "cancel" }, ]} >
{mode === "new" ? ( <>
setLocation(e.target.value)} /> { const p = await choosePath({ directory: true, title: "Virtualenv Location", defaultPath: root }); if (p) setLocation(p); }} />
{ const p = await choosePath({ directory: false, title: "Select Python Interpreter" }); if (p) setExisting(p); }} />
)} {error && (
{error}
)} {log.length > 0 &&
{log.join("\n")}
}
); }