Skip to content

Commit ab67de5

Browse files
authored
Merge pull request microsoft#243442 from microsoft/tyriar/237208_2
Support tracking sub-executions within a single executeCommand
2 parents 9281597 + 79e31e7 commit ab67de5

File tree

2 files changed

+329
-19
lines changed

2 files changed

+329
-19
lines changed

src/vs/workbench/api/common/extHostTerminalShellIntegration.ts

Lines changed: 119 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -149,12 +149,20 @@ export class ExtHostTerminalShellIntegration extends Disposable implements IExtH
149149
}
150150
}
151151

152-
class InternalTerminalShellIntegration extends Disposable {
152+
interface IExecutionProperties {
153+
isMultiLine: boolean;
154+
unresolvedCommandLines: string[] | undefined;
155+
}
156+
157+
export class InternalTerminalShellIntegration extends Disposable {
153158
private _pendingExecutions: InternalTerminalShellExecution[] = [];
159+
private _pendingEndingExecution: InternalTerminalShellExecution | undefined;
154160

161+
private _currentExecutionProperties: IExecutionProperties | undefined;
155162
private _currentExecution: InternalTerminalShellExecution | undefined;
156163
get currentExecution(): InternalTerminalShellExecution | undefined { return this._currentExecution; }
157164

165+
158166
private _env: vscode.TerminalShellIntegrationEnvironment | undefined;
159167
private _cwd: URI | undefined;
160168
private _hasRichCommandDetection: boolean = false;
@@ -226,27 +234,70 @@ class InternalTerminalShellIntegration extends Disposable {
226234

227235
requestNewShellExecution(commandLine: vscode.TerminalShellExecutionCommandLine, cwd: URI | undefined) {
228236
const execution = new InternalTerminalShellExecution(commandLine, cwd ?? this._cwd);
237+
const unresolvedCommandLines = splitAndSanitizeCommandLine(commandLine.value);
238+
if (unresolvedCommandLines.length > 1) {
239+
this._currentExecutionProperties = {
240+
isMultiLine: true,
241+
unresolvedCommandLines: splitAndSanitizeCommandLine(commandLine.value),
242+
};
243+
}
229244
this._pendingExecutions.push(execution);
230245
this._onDidRequestNewExecution.fire(commandLine.value);
231246
return execution;
232247
}
233248

234-
startShellExecution(commandLine: vscode.TerminalShellExecutionCommandLine, cwd: URI | undefined): InternalTerminalShellExecution {
249+
startShellExecution(commandLine: vscode.TerminalShellExecutionCommandLine, cwd: URI | undefined): undefined {
250+
// Since an execution is starting, fire the end event for any execution that is awaiting to
251+
// end. When this happens it means that the data stream may not be flushed and therefore may
252+
// fire events after the end event.
253+
if (this._pendingEndingExecution) {
254+
this._onDidRequestEndExecution.fire({ terminal: this._terminal, shellIntegration: this.value, execution: this._pendingEndingExecution.value, exitCode: undefined });
255+
this._pendingEndingExecution = undefined;
256+
}
257+
235258
if (this._currentExecution) {
259+
// If the current execution is multi-line, check if this command line is part of it.
260+
if (this._currentExecutionProperties?.isMultiLine && this._currentExecutionProperties.unresolvedCommandLines) {
261+
const subExecutionResult = isSubExecution(this._currentExecutionProperties.unresolvedCommandLines, commandLine);
262+
if (subExecutionResult) {
263+
this._currentExecutionProperties.unresolvedCommandLines = subExecutionResult.unresolvedCommandLines;
264+
return;
265+
}
266+
}
236267
if (this._hasRichCommandDetection) {
237268
console.warn('Rich command detection is enabled but an execution started before the last ended');
238269
}
239270
this._currentExecution.endExecution(undefined);
271+
this._currentExecution.flush();
240272
this._onDidRequestEndExecution.fire({ terminal: this._terminal, shellIntegration: this.value, execution: this._currentExecution.value, exitCode: undefined });
241273
}
242274

243275
// Get the matching pending execution, how strict this is depends on the confidence of the
244276
// command line
245277
let currentExecution: InternalTerminalShellExecution | undefined;
246278
if (commandLine.confidence === TerminalShellExecutionCommandLineConfidence.High) {
247-
const index = this._pendingExecutions.findIndex(e => e.value.commandLine.value === commandLine.value);
248-
if (index !== -1) {
249-
currentExecution = this._pendingExecutions.splice(index, 1)[0];
279+
for (const [i, execution] of this._pendingExecutions.entries()) {
280+
if (execution.value.commandLine.value === commandLine.value) {
281+
currentExecution = execution;
282+
this._currentExecutionProperties = {
283+
isMultiLine: false,
284+
unresolvedCommandLines: undefined,
285+
};
286+
currentExecution = execution;
287+
this._pendingExecutions.splice(i, 1);
288+
break;
289+
} else {
290+
const subExecutionResult = isSubExecution(splitAndSanitizeCommandLine(execution.value.commandLine.value), commandLine);
291+
if (subExecutionResult) {
292+
this._currentExecutionProperties = {
293+
isMultiLine: true,
294+
unresolvedCommandLines: subExecutionResult.unresolvedCommandLines,
295+
};
296+
currentExecution = execution;
297+
this._pendingExecutions.splice(i, 1);
298+
break;
299+
}
300+
}
250301
}
251302
} else {
252303
currentExecution = this._pendingExecutions.shift();
@@ -259,27 +310,36 @@ class InternalTerminalShellIntegration extends Disposable {
259310
}
260311

261312
this._currentExecution = currentExecution;
262-
263313
this._onDidStartTerminalShellExecution.fire({ terminal: this._terminal, shellIntegration: this.value, execution: this._currentExecution.value });
264-
return this._currentExecution;
265314
}
266315

267316
emitData(data: string): void {
268317
this.currentExecution?.emitData(data);
269318
}
270319

271320
endShellExecution(commandLine: vscode.TerminalShellExecutionCommandLine | undefined, exitCode: number | undefined): void {
321+
// If the current execution is multi-line, don't end it until the next command line is
322+
// confirmed to not be a part of it.
323+
if (this._currentExecutionProperties?.isMultiLine) {
324+
if (this._currentExecutionProperties.unresolvedCommandLines && this._currentExecutionProperties.unresolvedCommandLines.length > 0) {
325+
return;
326+
}
327+
}
328+
272329
if (this._currentExecution) {
273-
this._currentExecution.endExecution(commandLine);
330+
const commandLineForEvent = this._currentExecutionProperties?.isMultiLine ? this._currentExecution.value.commandLine : commandLine;
331+
this._currentExecution.endExecution(commandLineForEvent);
274332
const currentExecution = this._currentExecution;
333+
this._pendingEndingExecution = currentExecution;
334+
this._currentExecution = undefined;
275335
// IMPORTANT: Ensure the current execution's data events are flushed in order to
276336
// prevent data events firing after the end event fires.
277337
currentExecution.flush().then(() => {
278338
// Only fire if it's still the same execution, if it's changed it would have already
279339
// been fired.
280-
if (this._currentExecution === currentExecution) {
340+
if (this._pendingEndingExecution === currentExecution) {
281341
this._onDidRequestEndExecution.fire({ terminal: this._terminal, shellIntegration: this.value, execution: currentExecution.value, exitCode });
282-
this._currentExecution = undefined;
342+
this._pendingEndingExecution = undefined;
283343
}
284344
});
285345
}
@@ -320,12 +380,11 @@ class InternalTerminalShellIntegration extends Disposable {
320380
}
321381

322382
class InternalTerminalShellExecution {
323-
private _dataStream: ShellExecutionDataStream | undefined;
324-
325-
private _ended: boolean = false;
326-
327383
readonly value: vscode.TerminalShellExecution;
328384

385+
private _dataStream: ShellExecutionDataStream | undefined;
386+
private _isEnded: boolean = false;
387+
329388
constructor(
330389
private _commandLine: vscode.TerminalShellExecutionCommandLine,
331390
readonly cwd: URI | undefined,
@@ -346,7 +405,7 @@ class InternalTerminalShellExecution {
346405

347406
private _createDataStream(): AsyncIterable<string> {
348407
if (!this._dataStream) {
349-
if (this._ended) {
408+
if (this._isEnded) {
350409
return AsyncIterableObject.EMPTY;
351410
}
352411
this._dataStream = new ShellExecutionDataStream();
@@ -355,20 +414,25 @@ class InternalTerminalShellExecution {
355414
}
356415

357416
emitData(data: string): void {
358-
this._dataStream?.emitData(data);
417+
if (!this._isEnded) {
418+
this._dataStream?.emitData(data);
419+
}
359420
}
360421

361422
endExecution(commandLine: vscode.TerminalShellExecutionCommandLine | undefined): void {
362423
if (commandLine) {
363424
this._commandLine = commandLine;
364425
}
365426
this._dataStream?.endExecution();
366-
this._dataStream = undefined;
367-
this._ended = true;
427+
this._isEnded = true;
368428
}
369429

370430
async flush(): Promise<void> {
371-
await this._dataStream?.flush();
431+
if (this._dataStream) {
432+
await this._dataStream.flush();
433+
this._dataStream.dispose();
434+
this._dataStream = undefined;
435+
}
372436
}
373437
}
374438

@@ -405,3 +469,39 @@ class ShellExecutionDataStream extends Disposable {
405469
await Promise.all(this._iterables.map(e => e.toPromise()));
406470
}
407471
}
472+
473+
function splitAndSanitizeCommandLine(commandLine: string): string[] {
474+
return commandLine
475+
.split('\n')
476+
.map(line => line.trim())
477+
.filter(line => line.length > 0);
478+
}
479+
480+
/**
481+
* When executing something that the shell considers multiple commands, such as
482+
* a comment followed by a command, this needs to all be tracked under a single
483+
* execution.
484+
*/
485+
function isSubExecution(unresolvedCommandLines: string[], commandLine: vscode.TerminalShellExecutionCommandLine): { unresolvedCommandLines: string[] } | false {
486+
if (unresolvedCommandLines.length === 0) {
487+
return false;
488+
}
489+
const newUnresolvedCommandLines = [...unresolvedCommandLines];
490+
const subExecutionLines = splitAndSanitizeCommandLine(commandLine.value);
491+
if (newUnresolvedCommandLines && newUnresolvedCommandLines.length > 0) {
492+
// If all sub-execution lines are in the command line, this is part of the
493+
// multi-line execution.
494+
for (let i = 0; i < newUnresolvedCommandLines.length; i++) {
495+
if (newUnresolvedCommandLines[i] !== subExecutionLines[i]) {
496+
break;
497+
}
498+
newUnresolvedCommandLines.shift();
499+
subExecutionLines.shift();
500+
}
501+
502+
if (subExecutionLines.length === 0) {
503+
return { unresolvedCommandLines: newUnresolvedCommandLines };
504+
}
505+
}
506+
return false;
507+
}

0 commit comments

Comments
 (0)