Skip to content

Commit 9f9dd61

Browse files
committed
use shell integration on terminals that support it
1 parent 2608547 commit 9f9dd61

File tree

1 file changed

+88
-6
lines changed

1 file changed

+88
-6
lines changed

Extension/src/Debugger/runWithoutDebuggingAdapter.ts

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ const localize = nls.loadMessageBundle();
2020
export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
2121
private readonly sendMessageEmitter = new vscode.EventEmitter<vscode.DebugProtocolMessage>();
2222
public readonly onDidSendMessage: vscode.Event<vscode.DebugProtocolMessage> = this.sendMessageEmitter.event;
23+
private readonly terminalListeners: vscode.Disposable[] = [];
2324

2425
private seq: number = 1;
2526
private childProcess?: cp.ChildProcess;
2627
private terminal?: vscode.Terminal;
28+
private terminalExecution?: vscode.TerminalShellExecution;
29+
private hasTerminated: boolean = false;
2730

2831
public handleMessage(message: vscode.DebugProtocolMessage): void {
2932
const msg = message as { type: string; command: string; seq: number; arguments?: any; };
@@ -79,7 +82,7 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
7982
this.sendResponse(request, {});
8083

8184
if (consoleMode === 'integratedTerminal') {
82-
this.launchIntegratedTerminal(program, args, cwd, env);
85+
await this.launchIntegratedTerminal(program, args, cwd, env);
8386
} else if (consoleMode === 'externalTerminal') {
8487
this.launchExternalTerminal(program, args, cwd, env);
8588
} else {
@@ -91,8 +94,7 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
9194
* Launch the program in a VS Code integrated terminal.
9295
* The terminal will remain open after the program exits and be reused for the next session, if applicable.
9396
*/
94-
private launchIntegratedTerminal(program: string, args: string[], cwd: string | undefined, env: NodeJS.ProcessEnv) {
95-
const shellArgs: string[] = [program, ...args].map(a => this.quoteArg(a));
97+
private async launchIntegratedTerminal(program: string, args: string[], cwd: string | undefined, env: NodeJS.ProcessEnv): Promise<void> {
9698
const terminalName = path.normalize(program);
9799
const existingTerminal = vscode.window.terminals.find(t => t.name === terminalName);
98100
this.terminal = existingTerminal ?? vscode.window.createTerminal({
@@ -101,10 +103,21 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
101103
env: env as Record<string, string>
102104
});
103105
this.terminal.show(true);
104-
this.terminal.sendText(shellArgs.join(' '));
105106

106-
// The terminal manages its own lifecycle; notify VS Code the "debug" session is done.
107-
this.sendEvent('terminated');
107+
const shellIntegration: vscode.TerminalShellIntegration | undefined =
108+
this.terminal.shellIntegration ?? await this.waitForShellIntegration(this.terminal, 3000);
109+
110+
// Not all terminals support shell integration. If it's not available, we'll just send the command as text though we won't be able to monitor its execution.
111+
if (shellIntegration) {
112+
this.monitorIntegratedTerminal(this.terminal);
113+
this.terminalExecution = shellIntegration.executeCommand(program, args);
114+
} else {
115+
const shellArgs: string[] = [program, ...args].map(a => this.quoteArg(a));
116+
this.terminal.sendText(shellArgs.join(' '));
117+
118+
// The terminal manages its own lifecycle; notify VS Code the "debug" session is done.
119+
this.sendEvent('terminated');
120+
}
108121
}
109122

110123
/**
@@ -194,6 +207,65 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
194207
return /\s/.test(arg) ? `"${this.escapeQuotes(arg)}"` : arg;
195208
}
196209

210+
private waitForShellIntegration(terminal: vscode.Terminal, timeoutMs: number): Promise<vscode.TerminalShellIntegration | undefined> {
211+
return new Promise(resolve => {
212+
let resolved: boolean = false;
213+
const done = (shellIntegration: vscode.TerminalShellIntegration | undefined): void => {
214+
if (resolved) {
215+
return;
216+
}
217+
218+
resolved = true;
219+
clearTimeout(timeout);
220+
shellIntegrationChanged.dispose();
221+
terminalClosed.dispose();
222+
resolve(shellIntegration);
223+
};
224+
225+
const timeout = setTimeout(() => done(undefined), timeoutMs);
226+
const shellIntegrationChanged = vscode.window.onDidChangeTerminalShellIntegration(event => {
227+
if (event.terminal === terminal) {
228+
done(event.shellIntegration);
229+
}
230+
});
231+
const terminalClosed = vscode.window.onDidCloseTerminal(closedTerminal => {
232+
if (closedTerminal === terminal) {
233+
done(undefined);
234+
}
235+
});
236+
});
237+
}
238+
239+
private monitorIntegratedTerminal(terminal: vscode.Terminal): void {
240+
this.disposeTerminalListeners();
241+
this.terminalListeners.push(
242+
vscode.window.onDidEndTerminalShellExecution(event => {
243+
if (event.terminal !== terminal || event.execution !== this.terminalExecution || this.hasTerminated) {
244+
return;
245+
}
246+
247+
if (event.exitCode !== undefined) {
248+
this.sendEvent('exited', { exitCode: event.exitCode });
249+
}
250+
251+
this.sendEvent('terminated');
252+
}),
253+
vscode.window.onDidCloseTerminal(closedTerminal => {
254+
if (closedTerminal !== terminal || this.hasTerminated) {
255+
return;
256+
}
257+
258+
this.sendEvent('terminated');
259+
})
260+
);
261+
}
262+
263+
private disposeTerminalListeners(): void {
264+
while (this.terminalListeners.length > 0) {
265+
this.terminalListeners.pop()?.dispose();
266+
}
267+
}
268+
197269
private sendResponse(request: { command: string; seq: number; }, body: object): void {
198270
this.sendMessageEmitter.fire({
199271
type: 'response',
@@ -206,6 +278,15 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
206278
}
207279

208280
private sendEvent(event: string, body?: object): void {
281+
if (event === 'terminated') {
282+
if (this.hasTerminated) {
283+
return;
284+
}
285+
286+
this.hasTerminated = true;
287+
this.disposeTerminalListeners();
288+
}
289+
209290
this.sendMessageEmitter.fire({
210291
type: 'event',
211292
seq: this.seq++,
@@ -216,6 +297,7 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter {
216297

217298
public dispose(): void {
218299
this.terminateProcess();
300+
this.disposeTerminalListeners();
219301
this.sendMessageEmitter.dispose();
220302
}
221303

0 commit comments

Comments
 (0)