feat(debugger): Browse the debug console history and open diff settings from the keyboard (#162)

- Debug console: typed expressions are kept per project, and Browse Console
  History (⌥⌘E / Ctrl+Alt+E) lists them, most recent first, to put one back
  into the input.
- Diff viewer: Diff Settings (⇧⌘D / Ctrl+Shift+D) opens the viewer options
  (side-by-side / unified, whitespace, highlighting, collapsing, scroll sync)
  as a popup menu.
This commit is contained in:
l0ng-ai authored and GitHub committed 2026-10-29 02:16:37 +08:00
1 parent a28600c55a
commit aeb190ffda
10 files changed
+142 -10

No files matched your search

+12 -1
View File
@@ -4,7 +4,7 @@ import { call, commands, isCancelled, isTauri, nextRequestId, type RunConfig, ty
import { downloadJdkActions } from "@/platform/jdk";
import { useProject } from "@/platform/project";
import { activeEditor } from "@/editor/api";
import { maybeEditorService, notifications, toolWindowService } from "@/workbench/contributions";
import { maybeEditorService, notifications, showStatusInfo, toolWindowService } from "@/workbench/contributions";
import { canPlaceBreakpoint } from "./breakpoints/languages";
import {
removeAllBreakpoints,
@@ -35,6 +35,7 @@ import {
type DebugSession,
} from "./state";
import { focusedDebugView } from "./ui/focusedView";
import { browseFocusedConsoleHistory } from "./ui/expressionHistory";
import { expressionAt, valueHint } from "./editor/valueTooltip";
import { showHint } from "@/editor/lsp/hint";
import { pausedLineTargets, startInlineSmartStep } from "./editor/smartStep";
@@ -610,6 +611,16 @@ export function registerDebugActions(): () => void {
when: "debugSessionActive",
run: () => openEvaluateDialog(evaluateSeed()),
},
{
id: "debug.browseConsoleHistory",
title: "Browse Console History",
category: "Debug",
keywords: ["history", "previous expressions", "repl"],
when: "debugSessionActive",
run: () => {
if (!browseFocusedConsoleHistory()) showStatusInfo(tx("Focus the debug console input first"));
},
},
{
// ⌥⌘F8: the selection (or expression at the caret) evaluated in a value hint, no dialog.
id: "debug.quickEvaluate",
+26 -2
View File
@@ -39,6 +39,7 @@ import {
type DebugSession,
} from "../state";
import { VariablesView, type VarRoot } from "./VariablesView";
import { pushHistory, readHistory, setConsoleHistoryBrowser } from "./expressionHistory";
import styles from "./DebugToolWindow.module.css";
import { tx } from "@/platform/i18n";
@@ -542,8 +543,9 @@ function ConsoleView({ session }: { session: DebugSession }) {
const scroller = useRef<HTMLDivElement>(null);
const stick = useRef(true);
const [input, setInput] = useState("");
const history = useRef<string[]>([]);
const history = useRef<string[]>(readHistory("console"));
const historyPos = useRef(-1);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
const el = scroller.current;
@@ -553,7 +555,8 @@ function ConsoleView({ session }: { session: DebugSession }) {
const submit = async () => {
const e = input.trim();
if (!e) return;
history.current = [e, ...history.current.filter((h) => h !== e)].slice(0, 100);
pushHistory("console", e);
history.current = readHistory("console");
historyPos.current = -1;
setInput("");
appendConsole(session.id, "input", `> ${e}\n`);
@@ -565,6 +568,24 @@ function ConsoleView({ session }: { session: DebugSession }) {
}
};
/** 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<HTMLInputElement>) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -607,7 +628,10 @@ function ConsoleView({ session }: { session: DebugSession }) {
›
</span>
<input
ref={inputRef}
className={styles.evalInput}
onFocus={() => setConsoleHistoryBrowser(browseHistory)}
onBlur={() => setConsoleHistoryBrowser(null)}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={onKeyDown}
+15 -2
View File
@@ -1,10 +1,10 @@
/**
* Per-project history of expressions typed into the debugger (breakpoint
* conditions, Evaluate), most recent first, as IDEA keeps them.
* conditions, Evaluate, the debug console), most recent first, as IDEA keeps them.
*/
import { useProject } from "@/platform/project";
export type HistoryKind = "condition" | "evaluate";
export type HistoryKind = "condition" | "evaluate" | "console";
const PREFIX = "openidea.debugger.history.";
const MAX = 50;
@@ -36,3 +36,16 @@ export function pushHistory(kind: HistoryKind, expression: string) {
// Storage unavailable: no history.
}
}
/** Browse History (⌥⌘E) of the focused debug console input, while one has focus. */
let browseConsoleHistory: (() => void) | null = null;
export function setConsoleHistoryBrowser(fn: (() => void) | null) {
browseConsoleHistory = fn;
}
export function browseFocusedConsoleHistory(): boolean {
if (!browseConsoleHistory) return false;
browseConsoleHistory();
return true;
}
+65 -4
View File
@@ -21,7 +21,7 @@ import { setContext } from "@/platform/context";
import { call, commands } from "@/platform/ipc";
import { bindingsFor, normalizeBinding, shortcutLabel, strokeOf } from "@/platform/keymap";
import { tx } from "@/platform/i18n";
import { IconButton, Select, Spinner, T } from "@/ui";
import { IconButton, Select, Spinner, T, showContextMenu } from "@/ui";
import { editorService, type EditorInput } from "@/workbench/contributions";
import { languageFor, viewerBase } from "./cm";
import { applyChunk, arrowControls, chunkAt, hoverRevert } from "./diffControls";
@@ -602,9 +602,70 @@ export function DiffEditor({ input }: { input: EditorInput }) {
const neighbour = chainNeighbour(data, dir);
if (neighbour) void openDiff(neighbour, { preview: true });
};
const nav = useRef<DiffNavigator>({ next: () => step(1), prev: () => step(-1), jumpToSource, applyChange, file });
const toolbarRef = useRef<HTMLDivElement | null>(null);
const showSettings = () => {
const r = toolbarRef.current?.getBoundingClientRect();
showContextMenu({ x: r ? r.left + 8 : 120, y: r ? r.bottom : 120 }, [
{ kind: "header", label: "Viewer" },
{
id: "split",
label: "Side-by-side Viewer",
checked: opts.mode === "split",
run: () => setOpts({ mode: "split" }),
},
{
id: "unified",
label: "Unified Viewer",
checked: opts.mode === "unified",
run: () => setOpts({ mode: "unified" }),
},
{ kind: "separator" },
{
id: "ignore",
label: "Ignore Whitespace",
submenu: IGNORE_OPTIONS.map((o) => ({
id: o.value,
label: o.label,
checked: opts.ignore === o.value,
run: () => setOpts({ ignore: o.value }),
})),
},
{
id: "highlight",
label: "Highlighting",
submenu: HIGHLIGHT_OPTIONS.map((o) => ({
id: o.value,
label: o.label,
checked: opts.highlight === o.value,
run: () => setOpts({ highlight: o.value }),
})),
},
{ kind: "separator" },
{
id: "collapse",
label: "Collapse Unchanged Fragments",
checked: opts.collapse,
run: () => setOpts({ collapse: !opts.collapse }),
},
{
id: "sync",
label: "Synchronize Scrolling",
checked: opts.syncScroll,
disabled: opts.mode !== "split",
run: () => setOpts({ syncScroll: !opts.syncScroll }),
},
]);
};
const nav = useRef<DiffNavigator>({
next: () => step(1),
prev: () => step(-1),
jumpToSource,
applyChange,
file,
showSettings,
});
useEffect(() => {
nav.current = { next: () => step(1), prev: () => step(-1), jumpToSource, applyChange, file };
nav.current = { next: () => step(1), prev: () => step(-1), jumpToSource, applyChange, file, showSettings };
});
useEffect(
@@ -638,7 +699,7 @@ export function DiffEditor({ input }: { input: EditorInput }) {
}
}}
>
<div className={styles.toolbar} role="toolbar" aria-label={tx("Diff")}>
<div ref={toolbarRef} className={styles.toolbar} role="toolbar" aria-label={tx("Diff")}>
<IconButton
icon="arrow-up"
title="Previous Difference"
+9
View File
@@ -125,6 +125,15 @@ export function registerCompareActions(): () => void {
when: "diffEditorFocus",
run: () => focusedDiffNavigator()?.prev(),
},
{
id: "diff.showSettings",
title: "Diff Settings",
category: "Diff",
icon: "settings",
keywords: ["ignore whitespace", "highlighting", "side-by-side", "unified"],
when: "diffEditorFocus",
run: () => focusedDiffNavigator()?.showSettings(),
},
{
id: "diff.nextFile",
title: "Compare Next File",
+2
View File
@@ -8,6 +8,8 @@ export interface DiffNavigator {
applyChange(source: "a" | "b"): boolean;
/** Open the next / previous file of the diff chain (changes of a commit or change list). */
file(dir: 1 | -1): void;
/** The viewer's settings as a popup menu (⇧⌘D). */
showSettings(): void;
}
let focused: DiffNavigator | null = null;