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
2026-10-29 02:16:37 +08:00
committed by GitHub
parent a28600c55a
commit aeb190ffda
10 changed files with 142 additions and 10 deletions
+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;
+2
View File
@@ -282,6 +282,7 @@ export const intellijMacos: Record<string, string[]> = {
"debug.jumpToSource": ["F4", "Meta+Down"],
"debug.evaluate": ["Alt+F8"],
"debug.quickEvaluate": ["Meta+Alt+F8"],
"debug.browseConsoleHistory": ["Meta+Alt+E"],
// VCS
"vcs.operations": ["Ctrl+V"],
@@ -321,6 +322,7 @@ export const intellijMacos: Record<string, string[]> = {
"diff.applyRightSide": ["Ctrl+Meta+Left"],
"diff.nextFile": ["Ctrl+Shift+Right"],
"diff.prevFile": ["Ctrl+Shift+Left"],
"diff.showSettings": ["Meta+Shift+D"],
// After Show Diff: ⌘D is Compare With… in the project view.
"project.compareWith": ["Meta+D"],
// Editing extras, image viewer (keyWhen: image focused) and Markdown editor layouts
+2
View File
@@ -276,6 +276,7 @@ export const intellijWindows: Record<string, string[]> = {
"debug.jumpToSource": ["F4"],
"debug.evaluate": ["Alt+F8"],
"debug.quickEvaluate": ["Ctrl+Alt+F8"],
"debug.browseConsoleHistory": ["Ctrl+Alt+E"],
// VCS
"vcs.operations": ["Alt+`"],
@@ -319,6 +320,7 @@ export const intellijWindows: Record<string, string[]> = {
"diff.applyRightSide": ["Ctrl+Alt+A"],
"diff.nextFile": ["Alt+Shift+Right"],
"diff.prevFile": ["Alt+Shift+Left"],
"diff.showSettings": ["Ctrl+Shift+D"],
// After Show Diff: Ctrl+D is Compare With… in the project view.
"project.compareWith": ["Ctrl+D"],
// Editing extras, image viewer (keyWhen: image focused) and Markdown editor layouts
+2
View File
@@ -54,6 +54,7 @@ export const vscodeMacos: Record<string, string[]> = {
"project.copyElement": [],
"run.chooseDebugConfig": [],
"debug.quickEvaluate": [],
"debug.browseConsoleHistory": [],
"editor.nextSplitter": [],
"editor.prevSplitter": [],
"toolwindow.resizeLeft": [],
@@ -329,6 +330,7 @@ export const vscodeMacos: Record<string, string[]> = {
"diff.applyRightSide": [],
"diff.nextFile": [],
"diff.prevFile": [],
"diff.showSettings": [],
"diff.prevDifference": ["Shift+F7"],
"diff.jumpToSource": [],
// HTTP Client / Database console: run the request or statement at the caret.
+7 -1
View File
@@ -4607,5 +4607,11 @@
"Compare Next File": "比较下一个文件",
"Compare Previous File": "比较上一个文件",
"Context Info": "上下文信息",
"No context information here": "此处没有上下文信息"
"No context information here": "此处没有上下文信息",
"Diff Settings": "差异设置",
"Viewer": "查看器",
"Ignore Whitespace": "忽略空白",
"Browse Console History": "浏览控制台历史",
"Focus the debug console input first": "请先聚焦调试控制台输入框",
"No history": "无历史记录"
}