fix(debugger): type input for the program in the debug console (#1614)

A program started through runInTerminal had stdin from /dev/null, and CodeLLDB's launcher only hands the program a terminal it can read from when it runs on one. runInTerminal now starts the command on a pseudo-terminal, forwards its output to the console (CRLF as LF, without CodeLLDB's clear-screen request) and reports that the program takes input; while it runs, the console's input line goes to it, while paused it evaluates as before. debugpy and js-debug programs started in a terminal get the same.
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-06 03:02:42 +08:00
1 джерело 653f2ff276
коміт 0a47a685d9
13 files changed
+204 -24

No files matched your search

+8 -1
Переглянути файл
@@ -735,6 +735,8 @@ export const commands = {
debugCompletions: (session: DebugSessionId, frameId: number | null, text: string, line: number, column: number) => typedError<DebugCompletion[], IdeError>(__TAURI_INVOKE("debug_completions", { session, frameId, text, line, column })),
debugResume: (session: DebugSessionId, threadId: number) => typedError<null, IdeError>(__TAURI_INVOKE("debug_resume", { session, threadId })),
debugPause: (session: DebugSessionId, threadId: number) => typedError<null, IdeError>(__TAURI_INVOKE("debug_pause", { session, threadId })),
/** Types `text` into the session's program (a `runInTerminal` program). */
debugSendInput: (session: DebugSessionId, text: string) => typedError<null, IdeError>(__TAURI_INVOKE("debug_send_input", { session, text })),
debugHotReload: (session: DebugSessionId) => typedError<null, IdeError>(__TAURI_INVOKE("debug_hot_reload", { session })),
debugHotRestart: (session: DebugSessionId) => typedError<null, IdeError>(__TAURI_INVOKE("debug_hot_restart", { session })),
debugStep: (session: DebugSessionId, threadId: number, kind: StepKind) => typedError<null, IdeError>(__TAURI_INVOKE("debug_step", { session, threadId, kind })),
@@ -2855,7 +2857,12 @@ export type DebugEvent =
* First event of every session (only build output of a root session's
* `cargo` step can precede it). `parent` is set for adapter-started child sessions.
*/
{ kind: "started"; name: string; adapter: string; parent: DebugSessionId | null } | { kind: "initialized" } | { kind: "stopped"; threadId: number; reason: string; description: string | null; allThreadsStopped: boolean } | { kind: "continued"; threadId: number; allThreadsContinued: boolean } | { kind: "output"; category: string; output: string } | { kind: "thread"; threadId: number; started: boolean } | { kind: "breakpointsChanged"; path: IpcPath } | { kind: "terminated" } | { kind: "exited"; code: number } |
{ kind: "started"; name: string; adapter: string; parent: DebugSessionId | null } | { kind: "initialized" } | { kind: "stopped"; threadId: number; reason: string; description: string | null; allThreadsStopped: boolean } | { kind: "continued"; threadId: number; allThreadsContinued: boolean } | { kind: "output"; category: string; output: string } | { kind: "thread"; threadId: number; started: boolean } |
/**
* The program runs in a terminal the IDE gave it (`runInTerminal`): what is typed for it
* in the debug console reaches its input.
*/
{ kind: "programInput" } | { kind: "breakpointsChanged"; path: IpcPath } | { kind: "terminated" } | { kind: "exited"; code: number } |
/**
* java-debug's `hotcodereplace` event: `change_type` is `BUILD_COMPLETE` (classes
* changed and can be reloaded), `STARTING`, `END`, `WARNING` or `ERROR`.
+15
Переглянути файл
@@ -45,6 +45,8 @@ export interface DebugSession {
parent: DebugSessionId | null;
/** Launch configuration (root sessions), used by Rerun. */
launch: LaunchConfig | null;
/** The program runs in a terminal the IDE gave it: the console types into it while it runs. */
programInput?: boolean;
/** Process the session is attached to (a Run tab's build tool); stopped with the session. */
process: RunId | null;
status: SessionStatus;
@@ -944,6 +946,16 @@ export async function setVariableValue(
return result;
}
/** Whether the console types into the session's program (it runs, in a terminal the IDE gave it). */
export function readsProgramInput(s: DebugSession): boolean {
return !!s.programInput && s.status === "running";
}
/** Types `text` into the session's program; its terminal echoes it to the console. */
export async function sendProgramInput(session: DebugSession, text: string): Promise<void> {
await call(commands.debugSendInput(session.id, text));
}
export async function evaluate(
expression: string,
context: "repl" | "watch" | "hover" | "clipboard",
@@ -1022,6 +1034,9 @@ export function handleDebugEvent(id: DebugSessionId, event: DebugEvent) {
case "thread":
if (s.status === "paused") void loadThreads(id);
break;
case "programInput":
patch(id, { programInput: true });
break;
case "breakpointsChanged":
void refreshStates(event.path);
break;
+28 -3
Переглянути файл
@@ -16,7 +16,16 @@ import {
type FindOptions,
type FindResult,
} from "@/ui/consoleFind";
import { appendConsole, clearConsole, evaluate, isLive, useDebug, type DebugSession } from "../state";
import {
appendConsole,
clearConsole,
evaluate,
isLive,
readsProgramInput,
sendProgramInput,
useDebug,
type DebugSession,
} from "../state";
import { pushHistory, readHistory, setConsoleHistoryBrowser } from "./expressionHistory";
import { useExpressionCompletion } from "./ExpressionCompletion";
import styles from "./DebugToolWindow.module.css";
@@ -245,6 +254,16 @@ export function DebugConsole({ session, interactive = true }: { session: DebugSe
}, [findOpen, current, virtualizer]);
const submit = async () => {
if (readsProgramInput(session)) {
// Like the Run console: the line as typed, empty or not, goes to the program.
setInput("");
try {
await sendProgramInput(session, `${input}\n`);
} catch (err) {
appendConsole(session.id, "stderr", `${err instanceof Error ? err.message : String(err)}\n`);
}
return;
}
const e = input.trim();
if (!e) return;
pushHistory("console", e);
@@ -466,10 +485,16 @@ export function DebugConsole({ session, interactive = true }: { session: DebugSe
value={input}
onChange={(e) => {
setInput(e.target.value);
completion.onInput();
if (!readsProgramInput(session)) completion.onInput();
}}
onKeyDown={onKeyDown}
placeholder={isLive(session) ? "Evaluate in the debug console" : "Session finished"}
placeholder={
!isLive(session)
? "Session finished"
: readsProgramInput(session)
? "Input for the program (pause to evaluate)"
: "Evaluate in the debug console"
}
disabled={!isLive(session)}
aria-label={tx("Debug console input")}
spellCheck={false}
+2 -1
Переглянути файл
@@ -6299,5 +6299,6 @@
"Idle": "空闲",
"Composer dependencies are not installed": "未安装 Composer 依赖",
"Installed Composer packages differ from composer.lock": "已安装的 Composer 包与 composer.lock 不一致",
"Composer Install": "安装 Composer 依赖"
"Composer Install": "安装 Composer 依赖",
"Input for the program (pause to evaluate)": "程序输入(暂停后可计算)"
}