/** * Settings › Python › Interpreter (PyCharm's "Python Interpreter" page): the * project interpreter and its packages (pip / uv pip). The interpreter feeds * run configurations, basedpyright / pyright (`python.pythonPath`) and the * terminal's virtualenv activation. */ import { isCancelled } from "@/platform/ipc"; import { useCallback, useEffect, useMemo, useState, type KeyboardEvent, type MouseEvent } from "react"; import clsx from "clsx"; import { Button, IconButton, Input, Spinner, showContextMenu, T } from "@/ui"; import { useProject } from "@/platform/project"; import { Section, Toggle } from "../components/Form"; import { effectivePython, listPackages, pip, useSdks, type PythonPackage } from "./api"; import { AddInterpreterDialog } from "./AddInterpreterDialog"; import { useSetting } from "../components/context"; import { SdkField, tildify } from "./components"; import styles from "./Sdk.module.css"; type Op = "install" | "upgrade" | "uninstall"; interface Job { op: Op; target: string; cancel: () => void; } export function filterPackages(packages: readonly PythonPackage[], query: string): PythonPackage[] { const q = query.trim().toLowerCase(); return q ? packages.filter((p) => p.name.toLowerCase().includes(q)) : [...packages]; } function usePackages(python: string | null) { const [tick, setTick] = useState(0); const [result, setResult] = useState<{ key: string; python: string; packages: PythonPackage[] | null; error: string | null; } | null>(null); useEffect(() => { if (!python) return; let live = true; const key = `${python}#${tick}`; listPackages(python) .then((packages) => live && setResult({ key, python, packages, error: null })) .catch( (e: unknown) => live && setResult({ key, python, packages: null, error: String((e as Error)?.message ?? e) }), ); return () => { live = false; }; }, [python, tick]); const reload = useCallback(() => setTick((t) => t + 1), []); // While reloading the same interpreter, keep showing its previous list. const same = !!python && result?.python === python; return { packages: same ? result.packages : null, error: same ? result.error : null, loading: !!python && result?.key !== `${python}#${tick}`, reload, }; } export function PythonInterpreterPage() { const [interpreter, setInterpreter] = useSetting("languages.python.interpreter"); const [activate, setActivate] = useSetting("languages.python.activateVirtualenv"); const root = useProject((s) => s.project?.root ?? null); const [auto, setAuto] = useState(null); const detected = useSdks((s) => s.byKind.python); useEffect(() => { let live = true; effectivePython() .then((p) => live && setAuto(p)) .catch(() => live && setAuto(null)); return () => { live = false; }; }, [root]); // Without a configured interpreter the project's virtualenv is used (like run configurations). const python = interpreter || auto; const { packages, error, loading, reload } = usePackages(python); const [selected, setSelected] = useState(null); const [query, setQuery] = useState(""); const [installing, setInstalling] = useState(false); const [requirement, setRequirement] = useState(""); const [job, setJob] = useState(null); const [log, setLog] = useState([]); const [jobError, setJobError] = useState(null); const [adding, setAdding] = useState(false); const shown = useMemo(() => filterPackages(packages ?? [], query), [packages, query]); const run = (op: Op, target: string) => { if (!python || job) return; setLog([]); setJobError(null); const s = pip(python, target, op, (line) => setLog((l) => [...l.slice(-400), line])); setJob({ op, target, cancel: s.cancel }); s.promise .then(() => { if (op === "install") { setRequirement(""); setInstalling(false); } if (op === "uninstall") setSelected(null); reload(); }) .catch((e: unknown) => setJobError(isCancelled(e) ? null : String((e as Error)?.message ?? e))) .finally(() => setJob(null)); }; const menu = (e: MouseEvent, pkg: PythonPackage) => { e.preventDefault(); setSelected(pkg.name); showContextMenu(e, [ { label: "Upgrade", icon: "arrow-up", disabled: !!job, run: () => run("upgrade", pkg.name) }, { label: "Uninstall", icon: "minus", disabled: !!job, run: () => run("uninstall", pkg.name) }, ]); }; const onRowKey = (e: KeyboardEvent, i: number) => { const next = e.key === "ArrowDown" ? i + 1 : e.key === "ArrowUp" ? i - 1 : -1; if (next >= 0 && next < shown.length) { e.preventDefault(); setSelected(shown[next]!.name); (e.currentTarget.parentElement?.children[next] as HTMLElement | undefined)?.focus(); } else if ((e.key === "Delete" || e.key === "Backspace") && selected) { e.preventDefault(); run("uninstall", selected); } }; const autoDetail = auto ? tildify(auto) : "No virtualenv in the project"; const current = detected?.find((s) => s.path === python); return (
Python Interpreter "} emptyDetail={autoDetail} browseTitle="Select Python Interpreter" aria-label="Python interpreter" />
{python && (
{current ? `${current.name} — ` : ""} {tildify(python)}
)}
setInstalling((v) => !v)} /> selected && run("uninstall", selected)} /> selected && run("upgrade", selected)} /> {(loading || job) && } setQuery(e.target.value)} />
{installing && (
{ e.preventDefault(); if (requirement.trim()) run("install", requirement.trim()); }} > setRequirement(e.target.value)} onKeyDown={(e) => { if (e.key === "Escape") { e.stopPropagation(); setInstalling(false); } }} />
)}
{shown.map((p, i) => ( setSelected(p.name)} onContextMenu={(e) => menu(e, p)} onKeyDown={(e) => onRowKey(e, i)} > ))}
Package Version
{p.name} {p.version}
{!python && (
No interpreter selected. Choose one above or add a new one.
)} {python && error && (
{error}
)} {python && !error && packages && shown.length === 0 && (
{query ? "No packages match" : "No packages installed"}
)} {python && !error && !packages && loading && (
Loading packages…
)}
{job && (
{job.op === "install" ? "Installing" : job.op === "upgrade" ? "Upgrading" : "Uninstalling"} {job.target}…
)} {jobError && (
{jobError}
)} {log.length > 0 && (
          {log.join("\n")}
        
)}
{adding && root && ( setAdding(false)} onAdded={(path) => { setInterpreter(path); setAdding(false); }} /> )}
); } export default PythonInterpreterPage;