fix(debugger): coloured Spring Boot output, Soft-Wrap and Scroll to End in the debug console (#1353)

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
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-05 02:58:59 +08:00
1 джерело 2fc3535300
коміт c83264ae75
4 files changed
+57 -7

No files matched your search

+12 -1
Переглянути файл
@@ -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",
+3 -2
Переглянути файл
@@ -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",
+36 -4
Переглянути файл
@@ -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<HTMLDivElement>(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<boolean | null>(null);
const softWrap = wrapOverride ?? defaultWrap;
const programmatic = useRef(false);
const [input, setInput] = useState("");
const history = useRef<string[]>(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
<div className={styles.console}>
<div
ref={scroller}
className={clsx(styles.consoleOutput, ansi.palette)}
className={clsx(styles.consoleOutput, ansi.palette, !softWrap && styles.noWrap)}
onScroll={(e) => {
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}
</>
)}
<IconButton
icon="arrow-down"
title="Scroll to End"
size="small"
iconSize={12}
pressed={follow}
onClick={() => setFollow((v) => !v)}
/>
<IconButton
icon="soft-wrap"
title="Soft-Wrap"
size="small"
iconSize={12}
pressed={softWrap}
onClick={() => setWrapOverride(!softWrap)}
/>
<IconButton
icon="trash"
title="Clear Console"
+6
Переглянути файл
@@ -251,6 +251,12 @@
cursor: text;
}
/* Soft-Wrap off: long lines scroll horizontally. */
.noWrap {
white-space: pre;
word-break: normal;
}
.out {
position: absolute;
top: 0;