From c83264ae756c0cab378ad5c7952f285864446013 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Thu, 5 Nov 2026 02:58:59 +0800 Subject: [PATCH] fix(debugger): coloured Spring Boot output, Soft-Wrap and Scroll to End in the debug console (#1353) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Debugging a Spring Boot configuration showed its log uncoloured: Run adds -Dspring.output.ansi.enabled=always (Spring only colours output for a terminal, the process runs on pipes), the java-debug and Kotlin debug launches built their VM options without it. They now add the same console VM options as Run, unless the configuration sets the property itself. The debug console also gets the Run console's Soft-Wrap toggle (default from Settings › Console › Use soft wraps) and Scroll to End, which follows new output, turns off when scrolling up and on again at the end. Fixes #1262 --- src/features/debugger/__tests__/java.test.ts | 13 +++++- src/features/debugger/java.ts | 5 ++- src/features/debugger/ui/DebugConsole.tsx | 40 +++++++++++++++++-- .../debugger/ui/DebugToolWindow.module.css | 6 +++ 4 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/features/debugger/__tests__/java.test.ts b/src/features/debugger/__tests__/java.test.ts index 1bc17d92e..a4a16b2a0 100644 --- a/src/features/debugger/__tests__/java.test.ts +++ b/src/features/debugger/__tests__/java.test.ts @@ -54,7 +54,7 @@ const { isJavaDebuggable, javaAttachConfig, javaLaunchConfig } = await import(". const app: RunConfig = { id: "application:a.Main", name: "Main", - kind: "spring-boot", + kind: "application", command: "java", args: ["-Xmx1g", "-cp", "${classpath}", "a.Main", "--port", "8081 x"], cwd: "api", @@ -93,6 +93,17 @@ describe("Java debugging (java-debug through jdtls)", () => { }); }); + it("debugs Spring Boot with coloured console output, as Run does", async () => { + const boot = { ...app, kind: "spring-boot" }; + expect(JSON.parse((await javaLaunchConfig(boot, "/p")).extraJson!).vmArgs).toBe( + "-Dspring.output.ansi.enabled=always -Xmx1g", + ); + const own = { ...boot, args: ["-Dspring.output.ansi.enabled=never", ...app.args] }; + expect(JSON.parse((await javaLaunchConfig(own, "/p")).extraJson!).vmArgs).toBe( + "-Dspring.output.ansi.enabled=never -Xmx1g", + ); + }); + it("passes the configuration's VM options to java-debug's vmArgs, before the main class", async () => { const args = [ "-Ddemo.flag=on", diff --git a/src/features/debugger/java.ts b/src/features/debugger/java.ts index 33cd3d644..8420f841c 100644 --- a/src/features/debugger/java.ts +++ b/src/features/debugger/java.ts @@ -7,6 +7,7 @@ import { executeAction } from "@/platform/actions"; import { call, commands, IpcError, nextRequestId, type LaunchConfig, type RunConfig } from "@/platform/ipc"; import { + consoleVmArgs, formatCompileErrors, isJvmApplication, joinJavaArgs, @@ -117,7 +118,7 @@ export async function javaLaunchConfig( classPaths, modulePaths: info.modulePaths, javaExec: resolved.command, - vmArguments: parsed.vmArgs, + vmArguments: [...consoleVmArgs(rc), ...parsed.vmArgs], programArgs: parsed.programArgs, }), }; @@ -139,7 +140,7 @@ export async function javaLaunchConfig( classPaths, modulePaths: info.modulePaths, javaExec: resolved.command === "java" ? undefined : resolved.command, - vmArgs: joinJavaArgs(parsed.vmArgs), + vmArgs: joinJavaArgs([...consoleVmArgs(rc), ...parsed.vmArgs]), args: joinJavaArgs(parsed.programArgs), // java-debug spawns the JVM itself: it must shorten a classpath too long for the OS the same way Run does. shortenCommandLine: info.shortenCommandLine ?? "none", diff --git a/src/features/debugger/ui/DebugConsole.tsx b/src/features/debugger/ui/DebugConsole.tsx index 083735fa0..3b98217b0 100644 --- a/src/features/debugger/ui/DebugConsole.tsx +++ b/src/features/debugger/ui/DebugConsole.tsx @@ -4,6 +4,7 @@ import { useVirtualizer } from "@tanstack/react-virtual"; import { copySelectionOnKeyDown } from "@/ui/selectableText"; import { EmptyState, IconButton, showContextMenu, T, type MenuItem } from "@/ui"; import { tx } from "@/platform/i18n"; +import { useWorkflowSettings } from "@/platform/workflowSettings"; import { spanStyle } from "@/ui/ansi"; import ansi from "@/ui/ansi.module.css"; import { appendConsole, clearConsole, evaluate, isLive, useDebug, type DebugSession } from "../state"; @@ -23,7 +24,14 @@ export function DebugServiceConsole({ sessionId }: { sessionId: number }) { export function DebugConsole({ session, interactive = true }: { session: DebugSession; interactive?: boolean }) { const lines = useDebug((s) => s.console[session.id]); const scroller = useRef(null); + // Scroll to End: follows new output; scrolling up turns it off, back to the end turns it on. + const [follow, setFollow] = useState(true); const stick = useRef(true); + stick.current = follow; + // Settings › Console › Use soft wraps is the default; the toggle overrides it for this console. + const defaultWrap = useWorkflowSettings((s) => s.console.softWrap); + const [wrapOverride, setWrapOverride] = useState(null); + const softWrap = wrapOverride ?? defaultWrap; const programmatic = useRef(false); const [input, setInput] = useState(""); const history = useRef(readHistory("console")); @@ -39,14 +47,21 @@ export function DebugConsole({ session, interactive = true }: { session: DebugSe getItemKey: (i) => lines?.[i]?.id ?? i, }); + // Wrapping changes every row's height; the end is pinned again below. + useLayoutEffect(() => virtualizer.measure?.(), [softWrap, virtualizer]); + 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]); + // Rows measured after this frame grow the content: pin the end once more. + requestAnimationFrame(() => { + if (stick.current) el.scrollTop = el.scrollHeight; + requestAnimationFrame(() => (programmatic.current = false)); + }); + }, [lines, virtualizer, follow, softWrap]); const submit = async () => { const e = input.trim(); @@ -101,11 +116,12 @@ export function DebugConsole({ session, interactive = true }: { session: DebugSe
{ if (programmatic.current) return; const el = e.currentTarget; - stick.current = el.scrollHeight - el.scrollTop - el.clientHeight < 24; + const atEnd = el.scrollHeight - el.scrollTop - el.clientHeight < 24; + if (atEnd !== stick.current) setFollow(atEnd); }} onContextMenu={(e) => { e.preventDefault(); @@ -170,6 +186,22 @@ export function DebugConsole({ session, interactive = true }: { session: DebugSe {completion.popup} )} + setFollow((v) => !v)} + /> + setWrapOverride(!softWrap)} + />