From aeb190ffda25287cbb6ac0341c42bb8445fb53ca Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Thu, 29 Oct 2026 02:16:37 +0800 Subject: [PATCH] feat(debugger): Browse the debug console history and open diff settings from the keyboard (#162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- src/features/debugger/actions.ts | 13 +++- src/features/debugger/ui/DebugToolWindow.tsx | 28 +++++++- src/features/debugger/ui/expressionHistory.ts | 17 ++++- src/features/vcs/DiffEditor.tsx | 69 +++++++++++++++++-- src/features/vcs/compare.tsx | 9 +++ src/features/vcs/diffNavigator.ts | 2 + src/platform/keymaps/intellij-macos.ts | 2 + src/platform/keymaps/intellij-windows.ts | 2 + src/platform/keymaps/vscode-macos.ts | 2 + src/platform/locales/text/zh.json | 8 ++- 10 files changed, 142 insertions(+), 10 deletions(-) diff --git a/src/features/debugger/actions.ts b/src/features/debugger/actions.ts index fe8ad307..dd8bf36a 100644 --- a/src/features/debugger/actions.ts +++ b/src/features/debugger/actions.ts @@ -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", diff --git a/src/features/debugger/ui/DebugToolWindow.tsx b/src/features/debugger/ui/DebugToolWindow.tsx index f331b5ae..7adf6242 100644 --- a/src/features/debugger/ui/DebugToolWindow.tsx +++ b/src/features/debugger/ui/DebugToolWindow.tsx @@ -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(null); const stick = useRef(true); const [input, setInput] = useState(""); - const history = useRef([]); + const history = useRef(readHistory("console")); const historyPos = useRef(-1); + const inputRef = useRef(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) => { if (e.key === "Enter") { e.preventDefault(); @@ -607,7 +628,10 @@ function ConsoleView({ session }: { session: DebugSession }) { › setConsoleHistoryBrowser(browseHistory)} + onBlur={() => setConsoleHistoryBrowser(null)} value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={onKeyDown} diff --git a/src/features/debugger/ui/expressionHistory.ts b/src/features/debugger/ui/expressionHistory.ts index b6cd023e..b6af5e77 100644 --- a/src/features/debugger/ui/expressionHistory.ts +++ b/src/features/debugger/ui/expressionHistory.ts @@ -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; +} diff --git a/src/features/vcs/DiffEditor.tsx b/src/features/vcs/DiffEditor.tsx index 658d02c0..8f4bb9be 100644 --- a/src/features/vcs/DiffEditor.tsx +++ b/src/features/vcs/DiffEditor.tsx @@ -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({ next: () => step(1), prev: () => step(-1), jumpToSource, applyChange, file }); + const toolbarRef = useRef(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({ + 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 }) { } }} > -
+
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", diff --git a/src/features/vcs/diffNavigator.ts b/src/features/vcs/diffNavigator.ts index afe80df4..92b9fe72 100644 --- a/src/features/vcs/diffNavigator.ts +++ b/src/features/vcs/diffNavigator.ts @@ -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; diff --git a/src/platform/keymaps/intellij-macos.ts b/src/platform/keymaps/intellij-macos.ts index e19f2323..40a2386d 100644 --- a/src/platform/keymaps/intellij-macos.ts +++ b/src/platform/keymaps/intellij-macos.ts @@ -282,6 +282,7 @@ export const intellijMacos: Record = { "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 = { "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 diff --git a/src/platform/keymaps/intellij-windows.ts b/src/platform/keymaps/intellij-windows.ts index ace2711b..f427af74 100644 --- a/src/platform/keymaps/intellij-windows.ts +++ b/src/platform/keymaps/intellij-windows.ts @@ -276,6 +276,7 @@ export const intellijWindows: Record = { "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 = { "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 diff --git a/src/platform/keymaps/vscode-macos.ts b/src/platform/keymaps/vscode-macos.ts index a0ed485e..cfcbf00f 100644 --- a/src/platform/keymaps/vscode-macos.ts +++ b/src/platform/keymaps/vscode-macos.ts @@ -54,6 +54,7 @@ export const vscodeMacos: Record = { "project.copyElement": [], "run.chooseDebugConfig": [], "debug.quickEvaluate": [], + "debug.browseConsoleHistory": [], "editor.nextSplitter": [], "editor.prevSplitter": [], "toolwindow.resizeLeft": [], @@ -329,6 +330,7 @@ export const vscodeMacos: Record = { "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. diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index 76a1b496..7472ad9d 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -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": "无历史记录" }