Skip to content
1 change: 1 addition & 0 deletions src/vs/workbench/contrib/terminal/browser/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,7 @@ export interface ITerminalInstance {
}

export interface ITerminalQuickFixOptions {
id: string;
commandLineMatcher: string | RegExp;
outputMatcher?: ITerminalOutputMatcher;
getQuickFixes: TerminalQuickFixCallback;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const GitCreatePrOutputRegex = /remote:\s*(https:\/\/github\.com\/.+\/.+\

export function gitSimilarCommand(): ITerminalQuickFixOptions {
return {
id: 'Git Similar',
commandLineMatcher: GitCommandLineRegex,
outputMatcher: {
lineMatcher: GitSimilarOutputRegex,
Expand Down Expand Up @@ -51,6 +52,7 @@ export function gitSimilarCommand(): ITerminalQuickFixOptions {
}
export function gitTwoDashes(): ITerminalQuickFixOptions {
return {
id: 'Git Two Dashes',
commandLineMatcher: GitCommandLineRegex,
outputMatcher: {
lineMatcher: GitTwoDashesRegex,
Expand All @@ -74,6 +76,7 @@ export function gitTwoDashes(): ITerminalQuickFixOptions {
}
export function freePort(terminalInstance?: Partial<ITerminalInstance>): ITerminalQuickFixOptions {
return {
id: 'Free Port',
commandLineMatcher: AnyCommandLineRegex,
outputMatcher: {
lineMatcher: FreePortOutputRegex,
Expand Down Expand Up @@ -101,6 +104,7 @@ export function freePort(terminalInstance?: Partial<ITerminalInstance>): ITermin
}
export function gitPushSetUpstream(): ITerminalQuickFixOptions {
return {
id: 'Git Push Set Upstream',
commandLineMatcher: GitPushCommandLineRegex,
outputMatcher: {
lineMatcher: GitPushOutputRegex,
Expand All @@ -125,6 +129,7 @@ export function gitPushSetUpstream(): ITerminalQuickFixOptions {

export function gitCreatePr(): ITerminalQuickFixOptions {
return {
id: 'Git Create Pr',
commandLineMatcher: GitPushCommandLineRegex,
outputMatcher: {
lineMatcher: GitCreatePrOutputRegex,
Expand Down
80 changes: 69 additions & 11 deletions src/vs/workbench/contrib/terminal/browser/xterm/quickFixAddon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,21 @@ import { IDecoration, Terminal } from 'xterm';
// Importing types is safe in any layer
// eslint-disable-next-line local/code-import-patterns
import type { ITerminalAddon } from 'xterm-headless';


import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ILogService } from 'vs/platform/log/common/log';
const quickFixTelemetryTitle = 'terminal/quick-fix';
type QuickFixResultTelemetryEvent = {
id: string;
fixesShown: boolean;
ranQuickFixCommand?: boolean;
};
type QuickFixClassification = {
owner: 'meganrogge';
id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The quick fix ID' };
fixesShown: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the fixes were shown by the user' };
ranQuickFixCommand?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If the command that was executed matched a quick fix suggested one. Undefined if no command is expected.' };
comment: 'Terminal quick fixes';
};
const quickFixSelectors = [DecorationSelector.QuickFix, DecorationSelector.LightBulb, DecorationSelector.Codicon, DecorationSelector.CommandDecoration, DecorationSelector.XtermDecoration];
export interface ITerminalQuickFix {
showMenu(): void;
Expand Down Expand Up @@ -56,13 +69,18 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon,

private readonly _terminalDecorationHoverService: TerminalDecorationHoverManager;

private _fixesShown: boolean = false;
private _expectedCommands: string[] | undefined;

constructor(private readonly _capabilities: ITerminalCapabilityStore,
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IInstantiationService instantiationService: IInstantiationService,
@IAudioCueService private readonly _audioCueService: IAudioCueService,
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@IOpenerService private readonly _openerService: IOpenerService
@IOpenerService private readonly _openerService: IOpenerService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@ILogService private readonly _logService: ILogService
) {
super();
const commandDetectionCapability = this._capabilities.get(TerminalCapability.CommandDetection);
Expand All @@ -83,6 +101,7 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon,
}

showMenu(): void {
this._fixesShown = true;
this._decoration?.element?.click();
}

Expand All @@ -99,7 +118,25 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon,
if (!terminal || !commandDetection) {
return;
}
this._register(commandDetection.onCommandFinished(command => this._resolveQuickFixes(command)));
this._register(commandDetection.onCommandFinished(command => {
if (this._expectedCommands) {
const ranQuickFixCommand = this._expectedCommands.includes(command.command);
this._logService.debug(quickFixTelemetryTitle, {
id: this._expectedCommands.join(' '),
fixesShown: this._fixesShown,
ranQuickFixCommand
});
this._telemetryService?.publicLog2<QuickFixResultTelemetryEvent, QuickFixClassification>(quickFixTelemetryTitle, {
id: this._expectedCommands.join(' '),
fixesShown: this._fixesShown,
ranQuickFixCommand
});
this._expectedCommands = undefined;
}
this._resolveQuickFixes(command);
this._fixesShown = false;
}));

// The buffer is not ready by the time command finish
// is called. Add the decoration on command start if there are corresponding quick fixes
this._register(commandDetection.onCommandStarted(() => {
Expand All @@ -120,9 +157,24 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon,
if (!result) {
return;
}
const { fixes, onDidRunQuickFix } = result;
const { fixes, onDidRunQuickFix, expectedCommands } = result;
this._expectedCommands = expectedCommands;
this._quickFixes = fixes;
onDidRunQuickFix(() => this._disposeQuickFix());
this._register(onDidRunQuickFix((id) => {
const ranQuickFixCommand = (this._expectedCommands?.includes(command.command) || false);
this._logService.debug(quickFixTelemetryTitle, {
id,
fixesShown: this._fixesShown,
ranQuickFixCommand
});
this._telemetryService?.publicLog2<QuickFixResultTelemetryEvent, QuickFixClassification>(quickFixTelemetryTitle, {
id,
fixesShown: this._fixesShown,
ranQuickFixCommand
});
this._disposeQuickFix();
this._fixesShown = false;
}));
}

private _disposeQuickFix(): void {
Expand Down Expand Up @@ -177,11 +229,12 @@ export function getQuickFixesForCommand(
quickFixOptions: Map<string, ITerminalQuickFixOptions[]>,
openerService: IOpenerService,
onDidRequestRerunCommand?: Emitter<{ command: string; addNewLine?: boolean }>
): { fixes: IAction[]; onDidRunQuickFix: Event<void> } | undefined {
const onDidRunQuickFixEmitter = new Emitter<void>();
): { fixes: IAction[]; onDidRunQuickFix: Event<string>; expectedCommands?: string[] } | undefined {
const onDidRunQuickFixEmitter = new Emitter<string>();
const onDidRunQuickFix = onDidRunQuickFixEmitter.event;
const fixes: IAction[] = [];
const newCommand = command.command;
const expectedCommands = [];
for (const options of quickFixOptions.values()) {
for (const option of options) {
if (option.exitStatus !== undefined && option.exitStatus !== (command.exitCode === 0)) {
Expand All @@ -196,6 +249,7 @@ export function getQuickFixesForCommand(
if (outputMatcher) {
outputMatch = command.getOutputMatch(outputMatcher);
}
const id = option.id;
const quickFixes = option.getQuickFixes({ commandLineMatch, outputMatch }, command);
if (quickFixes) {
for (const quickFix of asArray(quickFixes)) {
Expand All @@ -218,6 +272,7 @@ export function getQuickFixesForCommand(
tooltip: label,
command: quickFix.command
} as IAction;
expectedCommands.push(quickFix.command);
break;
}
case 'opener': {
Expand All @@ -231,7 +286,7 @@ export function getQuickFixesForCommand(
openerService.open(quickFix.uri);
// since no command gets run here, need to
// clear the decoration and quick fix
onDidRunQuickFixEmitter.fire();
onDidRunQuickFixEmitter.fire(id);
},
tooltip: label,
uri: quickFix.uri
Expand All @@ -245,7 +300,10 @@ export function getQuickFixesForCommand(
label: quickFix.label,
class: quickFix.class,
enabled: quickFix.enabled,
run: () => quickFix.run(),
run: () => {
quickFix.run();
onDidRunQuickFixEmitter.fire(id);
},
tooltip: quickFix.tooltip
};
}
Expand All @@ -256,7 +314,7 @@ export function getQuickFixesForCommand(
}
}
}
return fixes.length > 0 ? { fixes, onDidRunQuickFix } : undefined;
return fixes.length > 0 ? { fixes, onDidRunQuickFix, expectedCommands } : undefined;
}


Expand Down