/** Debug tool window: toolbar, Threads & Variables (frames + variables), Console. */ import { useEffect, useMemo, useRef, useState, type KeyboardEvent } from "react"; import clsx from "clsx"; 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, closeSessionTab, evaluate, selectFrame, selectSession, selectThread, revealFrame, rootOf, sessionSupports, useDebug, type DebugSession, } from "../state"; import { remoteWaitingNote } from "../remote"; import { DebugConsole } from "./DebugConsole"; import { VariablesView, type VarRoot } from "./VariablesView"; import { useExpressionCompletion } from "./ExpressionCompletion"; import { sortVariables } from "../values"; import { workflowSettings } from "@/platform/workflowSettings"; 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", closable: true, tooltip: statusText(s), })); return ( selectSession(+id)} onClose={(id) => void closeSessionTab(+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 [view, setView] = useState<{ session: number | undefined; status: DebugSession["status"] | undefined; tab: Tab; }>({ session: undefined, status: undefined, tab: "variables" }); if (active && (view.session !== active.startedAt || view.status !== active.status)) setView({ session: active.startedAt, status: active.status, tab: active.status === "starting" ? "console" : active.status === "paused" ? "variables" : view.tab, }); const tab = view.tab; 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). */} {rootOf(active).adapter === "dart" && ( <> )} 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); completion.onInput(); }} onBlur={completion.close} onKeyDown={(e) => { if (completion.onKeyDown(e)) return; 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} /> {completion.popup} { addWatch(expression); setExpression(""); }} />
{frame == null && !watches.length && !lastEval ? (
{session.status === "terminated" ? tx("The session has finished.") : tx("Variables are available while suspended.")}
) : ( )} ); }