Compare commits

...
Author SHA1 Message Date
l0ng-ai 418422547d fix(debugger): coloured Spring Boot output, Soft-Wrap and Scroll to End in the debug console
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
2026-11-05 02:58:37 +08:00
4 changed files with 57 additions and 7 deletions

No files matched your search

+12 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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"
@@ -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;