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)} + />