/** Debug tool window: toolbar, Threads & Variables (frames + variables), Console. */ import { copySelectionOnKeyDown } from "@/ui/selectableText"; import { useEffect, useLayoutEffect, useMemo, useRef, useState, type KeyboardEvent } from "react"; import clsx from "clsx"; import { useVirtualizer } from "@tanstack/react-virtual"; import { executeAction, getAction } from "@/platform/actions"; import { evaluateWhen, useContextKeys } from "@/platform/context"; import { basename, useProject } from "@/platform/project"; import { call, commands, type Scope, type Variable } from "@/platform/ipc"; import { EmptyState, Icon, IconButton, Kbd, Menu, Select, SplitGroup, SplitHandle, SplitPanel, Tabs, showContextMenu, type MenuItem, type TabItem, T, } from "@/ui"; import { setMuted, useBreakpoints } from "../breakpoints/store"; import { frameLabel, groupFrames } from "../frames"; import { blurDebugView, focusDebugView } from "./focusedView"; import { addWatch, clearConsole, evaluate, isLive, selectFrame, selectSession, selectThread, appendConsole, revealFrame, sessionSupports, useDebug, type DebugSession, } from "../state"; import { VariablesView, type VarRoot } from "./VariablesView"; import { pushHistory, readHistory, setConsoleHistoryBrowser } from "./expressionHistory"; import { sortVariables } from "../values"; import { workflowSettings } from "@/platform/workflowSettings"; import { spanStyle } from "@/ui/ansi"; import ansi from "@/ui/ansi.module.css"; import styles from "./DebugToolWindow.module.css"; import { tx } from "@/platform/i18n"; function useActionEnabled(id: string): boolean { const ctx = useContextKeys(); const a = getAction(id); return !!a && evaluateWhen(a.when, ctx); } function ToolButton({ action, icon, tone }: { action: string; icon: string; tone?: "danger" }) { const enabled = useActionEnabled(action); return ( ); } /** IDEA's `XDebugger.ToolWindow.TopToolbar3.Extra` ("More" in the debugger toolbar). */ const MORE_ITEMS: MenuItem[] = [ { kind: "action", action: "debug.forceStepOver", icon: "force-step-over" }, { kind: "action", action: "debug.forceStepInto", icon: "force-step-into" }, { kind: "action", action: "debug.smartStepInto", icon: "smart-step-into" }, { kind: "separator" }, { kind: "action", action: "debug.runToCursor", icon: "run-to-cursor" }, { kind: "action", action: "debug.forceRunToCursor", icon: "force-run-to-cursor" }, { kind: "separator" }, { kind: "action", action: "debug.showExecutionPoint", icon: "show-execution-point" }, { kind: "separator" }, { kind: "action", action: "debug.evaluate", icon: "evaluate-expression" }, { kind: "action", action: "debug.resetFrame", icon: "reset-frame" }, ]; function MoreActions() { const ref = useRef(null); const [open, setOpen] = useState(false); return ( <> setOpen((v) => !v)} /> setOpen(false)} /> ); } type Tab = "variables" | "console"; function statusText(s: DebugSession): string { if (s.status === "terminated") return s.exitCode != null ? `Finished · exit code ${s.exitCode}` : "Finished"; if (s.status === "starting") return "Starting…"; return s.status === "paused" ? "Suspended" : "Running"; } /** Tool window header: one tab per debug session (`main ×`), like IDEA's content tabs. */ export function DebugTabs() { const sessions = useDebug((s) => s.sessions); const active = useDebug((s) => s.active); const list = Object.values(sessions); if (!list.length) return null; const items: TabItem[] = list.map((s) => ({ id: String(s.id), label: s.parent != null ? tx`${s.name} (child)` : s.name, text: s.name, icon: "debug", italic: s.status === "terminated", tooltip: statusText(s), })); return ( selectSession(+id)} panelIdPrefix="openidea-debug" /> ); } export function DebugToolWindow() { const active = useDebug((s) => (s.active != null ? s.sessions[s.active] : undefined)); const muted = useBreakpoints((s) => s.muted); const [tab, setTab] = useState("variables"); if (!active) { return (
Start a debug configuration with {" "} or click a line number to set a breakpoint.
); } return (
{( [ ["variables", "Threads & Variables"], ["console", "Console"], ] as const ).map(([id, label]) => ( ))}
{/* IDEA's XDebugger.ToolWindow.TopToolbar3 (+ its "More" group). */} setMuted(!muted)} />
{tab === "variables" ? : }
); } // --- threads & frames ---------------------------------------------------------- function ThreadsAndVariables({ session }: { session: DebugSession }) { return ( ); } function FramesPane({ session }: { session: DebugSession }) { const root = useProject((s) => s.project?.root ?? null); const [showAll, setShowAll] = useState(false); const items = useMemo( () => groupFrames(session.frames, root, session.selectedFrame, showAll), [session.frames, root, session.selectedFrame, showAll], ); const paused = session.status === "paused"; const onKeyDown = (e: KeyboardEvent) => { if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return; const frames = items.flatMap((i) => (i.kind === "frame" ? [i.frame] : [])); const idx = frames.findIndex((f) => f.id === session.selectedFrame); const next = frames[idx + (e.key === "ArrowDown" ? 1 : -1)]; if (next) { e.preventDefault(); selectFrame(session.id, next.id); } }; return (
{session.threads.length > 0 && ( setExpression(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey) && e.shiftKey) { e.preventDefault(); addWatch(expression); setExpression(""); } else if (e.key === "Enter") { e.preventDefault(); void run(); } }} placeholder={frame != null ? "Evaluate expression (↩) or add a watch (⇧⌘↩)" : "Evaluate expression"} aria-label={tx("Evaluate expression")} spellCheck={false} autoComplete="off" disabled={frame == null} /> { addWatch(expression); setExpression(""); }} />
{frame == null && !watches.length && !lastEval ? (
{session.status === "terminated" ? tx("The session has finished.") : tx("Variables are available while suspended.")}
) : ( )} ); } // --- console ------------------------------------------------------------------- function ConsoleView({ session }: { session: DebugSession }) { const lines = useDebug((s) => s.console[session.id]); const scroller = useRef(null); const stick = useRef(true); const programmatic = useRef(false); const [input, setInput] = useState(""); const history = useRef(readHistory("console")); const historyPos = useRef(-1); const inputRef = useRef(null); const virtualizer = useVirtualizer({ count: lines?.length ?? 0, getScrollElement: () => scroller.current, estimateSize: () => 18, overscan: 30, getItemKey: (i) => lines?.[i]?.id ?? i, }); useLayoutEffect(() => { const el = scroller.current; if (!el || !stick.current || !lines?.length) return; programmatic.current = true; virtualizer.scrollToIndex(lines.length - 1, { align: "end" }); el.scrollTop = el.scrollHeight; requestAnimationFrame(() => (programmatic.current = false)); }, [lines, virtualizer]); const submit = async () => { const e = input.trim(); if (!e) return; pushHistory("console", e); history.current = readHistory("console"); historyPos.current = -1; setInput(""); appendConsole(session.id, "input", `> ${e}\n`); try { const v = await evaluate(e, "repl", session); appendConsole(session.id, "result", `${v.value}\n`); } catch (err) { appendConsole(session.id, "stderr", `${err instanceof Error ? err.message : String(err)}\n`); } }; /** IDEA's Browse History popup: the expressions typed before, most recent first. */ const browseHistory = () => { const el = inputRef.current; const r = el?.getBoundingClientRect(); const items: MenuItem[] = history.current.length ? history.current.slice(0, 30).map((h, i) => ({ id: String(i), label: h.length > 120 ? `${h.slice(0, 120)}…` : h, run: () => { setInput(h); historyPos.current = i; el?.focus(); }, })) : [{ id: "none", label: "No history", disabled: true }]; showContextMenu({ x: r?.left ?? 120, y: (r?.top ?? 200) - 4 }, items); }; const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); void submit(); } else if (e.key === "ArrowUp" || e.key === "ArrowDown") { const h = history.current; if (!h.length) return; e.preventDefault(); const pos = Math.max(-1, Math.min(h.length - 1, historyPos.current + (e.key === "ArrowUp" ? 1 : -1))); historyPos.current = pos; setInput(pos < 0 ? "" : h[pos]!); } }; return (
{ if (programmatic.current) return; const el = e.currentTarget; stick.current = el.scrollHeight - el.scrollTop - el.clientHeight < 24; }} onContextMenu={(e) => { e.preventDefault(); showContextMenu(e, [{ label: "Clear All", icon: "trash", run: () => clearConsole(session.id) }]); }} role="log" onKeyDown={copySelectionOnKeyDown} aria-label={tx("Debug console")} tabIndex={0} >
{virtualizer.getVirtualItems().map((item) => { const l = lines?.[item.index]; if (!l) return null; return ( {l.spans.map((sp, i) => ( {sp.text} ))} ); })}
); }