Skip to content

Commit ecb3421

Browse files
authored
move builtin terminal quick fixes to contribution model (microsoft#164099)
1 parent ab3926a commit ecb3421

File tree

11 files changed

+193
-51
lines changed

11 files changed

+193
-51
lines changed

src/vs/platform/terminal/common/terminal.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Event } from 'vs/base/common/event';
77
import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform';
88
import { URI, UriComponents } from 'vs/base/common/uri';
99
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
10-
import { IPtyHostProcessReplayEvent, ISerializedCommandDetectionCapability, ITerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/capabilities';
10+
import { IPtyHostProcessReplayEvent, ISerializedCommandDetectionCapability, ITerminalCapabilityStore, ITerminalOutputMatcher } from 'vs/platform/terminal/common/capabilities/capabilities';
1111
import { IGetTerminalLayoutInfoArgs, IProcessDetails, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess';
1212
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
1313
import { ISerializableEnvironmentVariableCollections } from 'vs/platform/terminal/common/environmentVariable';
@@ -797,6 +797,16 @@ export interface ITerminalProfileSource extends IBaseUnresolvedTerminalProfile {
797797

798798
export interface ITerminalContributions {
799799
profiles?: ITerminalProfileContribution[];
800+
quickFixes?: ITerminalQuickFixContribution[];
801+
}
802+
803+
export interface ITerminalQuickFixContribution {
804+
id: string;
805+
commandLineMatcher: string | RegExp;
806+
outputMatcher: ITerminalOutputMatcher;
807+
exitStatus?: boolean;
808+
commandToRun?: string;
809+
linkToOpen?: string;
800810
}
801811

802812
export interface ITerminalProfileContribution {
@@ -810,6 +820,10 @@ export interface IExtensionTerminalProfile extends ITerminalProfileContribution
810820
extensionIdentifier: string;
811821
}
812822

823+
export interface IExtensionTerminalQuickFix extends ITerminalQuickFixContribution {
824+
extensionIdentifier: string;
825+
}
826+
813827
export type ITerminalProfileObject = ITerminalExecutable | ITerminalProfileSource | IExtensionTerminalProfile | null;
814828
export type ITerminalProfileType = ITerminalProfile | IExtensionTerminalProfile;
815829

src/vs/workbench/contrib/terminal/browser/terminal.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -949,8 +949,9 @@ export interface ITerminalInstance {
949949

950950
/**
951951
* Attempts to detect and kill the process listening on specified port.
952+
* If successful, places commandToRun on the command line
952953
*/
953-
freePortKillProcess(port: string): Promise<void>;
954+
freePortKillProcess(port: string, commandToRun: string): Promise<void>;
954955
}
955956

956957
export interface ITerminalQuickFixOptions {
@@ -959,6 +960,7 @@ export interface ITerminalQuickFixOptions {
959960
outputMatcher?: ITerminalOutputMatcher;
960961
getQuickFixes: TerminalQuickFixCallback;
961962
exitStatus?: boolean;
963+
source: string;
962964
}
963965
export type TerminalQuickFixMatchResult = { commandLineMatch: RegExpMatchArray; outputMatch?: RegExpMatchArray | null };
964966
export type TerminalQuickFixAction = IAction | ITerminalQuickFixCommandAction | ITerminalQuickFixOpenerAction;

src/vs/workbench/contrib/terminal/browser/terminalInstance.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ import { IDetectedLinks, TerminalLinkManager } from 'vs/workbench/contrib/termin
6262
import { TerminalLinkQuickpick } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkQuickpick';
6363
import { IRequestAddInstanceToGroupEvent, ITerminalQuickFixOptions, ITerminalExternalLinkProvider, ITerminalInstance, TerminalDataTransfers } from 'vs/workbench/contrib/terminal/browser/terminal';
6464
import { TerminalLaunchHelpAction } from 'vs/workbench/contrib/terminal/browser/terminalActions';
65-
import { gitSimilarCommand, gitCreatePr, gitPushSetUpstream, freePort, gitTwoDashes } from 'vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions';
65+
import { freePort, gitTwoDashes } from 'vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions';
6666
import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper';
6767
import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput';
6868
import { TerminalFindWidget } from 'vs/workbench/contrib/terminal/browser/terminalFindWidget';
@@ -731,7 +731,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
731731
this.xterm = xterm;
732732
this._quickFixAddon = this._scopedInstantiationService.createInstance(TerminalQuickFixAddon, this.capabilities);
733733
this.xterm?.raw.loadAddon(this._quickFixAddon);
734-
this.registerQuickFixProvider(gitSimilarCommand(), gitTwoDashes(), gitCreatePr(), gitPushSetUpstream(), freePort(this));
734+
this.registerQuickFixProvider(gitTwoDashes(), freePort(this));
735735
this._register(this._quickFixAddon.onDidRequestRerunCommand(async (e) => await this.runCommand(e.command, e.addNewLine || false)));
736736
const lineDataEventAddon = new LineDataEventAddon();
737737
this.xterm.raw.loadAddon(lineDataEventAddon);
@@ -1541,8 +1541,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
15411541
this.xterm?.markTracker.scrollToClosestMarker(startMarkId, endMarkId, highlight);
15421542
}
15431543

1544-
public async freePortKillProcess(port: string): Promise<void> {
1544+
public async freePortKillProcess(port: string, command: string): Promise<void> {
15451545
await this._processManager?.freePortKillProcess(port);
1546+
this.runCommand(command, false);
15461547
}
15471548

15481549
private _onProcessData(ev: IProcessDataEvent): void {

src/vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions.ts

Lines changed: 19 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,21 @@
66
import { localize } from 'vs/nls';
77
import { TerminalQuickFixMatchResult, ITerminalQuickFixOptions, ITerminalInstance, TerminalQuickFixAction } from 'vs/workbench/contrib/terminal/browser/terminal';
88
import { ITerminalCommand } from 'vs/workbench/contrib/terminal/common/terminal';
9-
import { URI } from 'vs/base/common/uri';
10-
9+
import { IExtensionTerminalQuickFix } from 'vs/platform/terminal/common/terminal';
1110
export const GitCommandLineRegex = /git/;
1211
export const GitPushCommandLineRegex = /git\s+push/;
1312
export const GitTwoDashesRegex = /error: did you mean `--(.+)` \(with two dashes\)\?/;
1413
export const AnyCommandLineRegex = /.+/;
15-
export const GitSimilarOutputRegex = /(?:(most similar (command|commands) (is|are)))((\n\s*[^\s]+)+)/m;
14+
export const GitSimilarOutputRegex = /(?:(most similar (command|commands) (is|are)))((\n\s*(?<fixedCommand>[^\s]+))+)/m;
1615
export const FreePortOutputRegex = /address already in use (0\.0\.0\.0|127\.0\.0\.1|localhost|::):(?<portNumber>\d{4,5})|Unable to bind [^ ]*:(\d{4,5})|can't listen on port (\d{4,5})|listen EADDRINUSE [^ ]*:(\d{4,5})/;
17-
export const GitPushOutputRegex = /git push --set-upstream origin ([^\s]+)/;
16+
export const GitPushOutputRegex = /git push --set-upstream origin (?<branchName>[^\s]+)/;
1817
// The previous line starts with "Create a pull request for \'([^\s]+)\' on GitHub by visiting:\s*"
1918
// it's safe to assume it's a github pull request if the URL includes `/pull/`
20-
export const GitCreatePrOutputRegex = /remote:\s*(https:\/\/github\.com\/.+\/.+\/pull\/new\/.+)/;
19+
export const GitCreatePrOutputRegex = /remote:\s*(?<link>https:\/\/github\.com\/.+\/.+\/pull\/new\/.+)/;
2120

22-
export function gitSimilarCommand(): ITerminalQuickFixOptions {
21+
export function gitSimilar(): ITerminalQuickFixOptions {
2322
return {
23+
source: 'builtin',
2424
id: 'Git Similar',
2525
commandLineMatcher: GitCommandLineRegex,
2626
outputMatcher: {
@@ -50,8 +50,10 @@ export function gitSimilarCommand(): ITerminalQuickFixOptions {
5050
}
5151
};
5252
}
53+
5354
export function gitTwoDashes(): ITerminalQuickFixOptions {
5455
return {
56+
source: 'builtin',
5557
id: 'Git Two Dashes',
5658
commandLineMatcher: GitCommandLineRegex,
5759
outputMatcher: {
@@ -76,6 +78,7 @@ export function gitTwoDashes(): ITerminalQuickFixOptions {
7678
}
7779
export function freePort(terminalInstance?: Partial<ITerminalInstance>): ITerminalQuickFixOptions {
7880
return {
81+
source: 'builtin',
7982
id: 'Free Port',
8083
commandLineMatcher: AnyCommandLineRegex,
8184
outputMatcher: {
@@ -97,12 +100,15 @@ export function freePort(terminalInstance?: Partial<ITerminalInstance>): ITermin
97100
id: 'terminal.freePort',
98101
label,
99102
enabled: true,
100-
run: async () => terminalInstance?.freePortKillProcess?.(port)
103+
run: async () => {
104+
await terminalInstance?.freePortKillProcess?.(port, command.command);
105+
}
101106
};
102107
}
103108
};
104109
}
105-
export function gitPushSetUpstream(): ITerminalQuickFixOptions {
110+
111+
export function gitPushSetUpstream(): IExtensionTerminalQuickFix {
106112
return {
107113
id: 'Git Push Set Upstream',
108114
commandLineMatcher: GitPushCommandLineRegex,
@@ -113,21 +119,12 @@ export function gitPushSetUpstream(): ITerminalQuickFixOptions {
113119
length: 5
114120
},
115121
exitStatus: false,
116-
getQuickFixes: (matchResult: TerminalQuickFixMatchResult, command: ITerminalCommand) => {
117-
const branch = matchResult?.outputMatch?.[1];
118-
if (!branch) {
119-
return;
120-
}
121-
return {
122-
type: 'command',
123-
command: `git push --set-upstream origin ${branch}`,
124-
addNewLine: true
125-
};
126-
}
122+
commandToRun: 'git push --set-upstream origin ${group:branchName}',
123+
extensionIdentifier: 'git'
127124
};
128125
}
129126

130-
export function gitCreatePr(): ITerminalQuickFixOptions {
127+
export function gitCreatePr(): IExtensionTerminalQuickFix {
131128
return {
132129
id: 'Git Create Pr',
133130
commandLineMatcher: GitPushCommandLineRegex,
@@ -138,18 +135,7 @@ export function gitCreatePr(): ITerminalQuickFixOptions {
138135
length: 5
139136
},
140137
exitStatus: true,
141-
getQuickFixes: (matchResult: TerminalQuickFixMatchResult, command?: ITerminalCommand) => {
142-
if (!command) {
143-
return;
144-
}
145-
const link = matchResult?.outputMatch?.[1];
146-
if (!link) {
147-
return;
148-
}
149-
return {
150-
type: 'opener',
151-
uri: URI.parse(link)
152-
};
153-
}
138+
linkToOpen: '${group:link}',
139+
extensionIdentifier: 'git'
154140
};
155141
}

src/vs/workbench/contrib/terminal/browser/xterm/quickFixAddon.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
1616
import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
1717
import { PANEL_BACKGROUND } from 'vs/workbench/common/theme';
1818
import { AudioCue, IAudioCueService } from 'vs/workbench/contrib/audioCues/browser/audioCueService';
19-
import { ITerminalQuickFixOptions } from 'vs/workbench/contrib/terminal/browser/terminal';
19+
import { ITerminalQuickFixOpenerAction, ITerminalQuickFixOptions, TerminalQuickFixAction, TerminalQuickFixMatchResult } from 'vs/workbench/contrib/terminal/browser/terminal';
2020
import { DecorationSelector, TerminalDecorationHoverManager, updateLayout } from 'vs/workbench/contrib/terminal/browser/xterm/decorationStyles';
2121
import { TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal';
2222
import { TERMINAL_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry';
@@ -28,6 +28,10 @@ import { IDecoration, Terminal } from 'xterm';
2828
import type { ITerminalAddon } from 'xterm-headless';
2929
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
3030
import { ILogService } from 'vs/platform/log/common/log';
31+
import { ITerminalContributionService } from 'vs/workbench/contrib/terminal/common/terminalExtensionPoints';
32+
import { IExtensionTerminalQuickFix } from 'vs/platform/terminal/common/terminal';
33+
import { URI } from 'vs/base/common/uri';
34+
import { gitCreatePr, gitPushSetUpstream, gitSimilar } from 'vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions';
3135
const quickFixTelemetryTitle = 'terminal/quick-fix';
3236
type QuickFixResultTelemetryEvent = {
3337
id: string;
@@ -75,6 +79,7 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon,
7579
constructor(private readonly _capabilities: ITerminalCapabilityStore,
7680
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
7781
@IConfigurationService private readonly _configurationService: IConfigurationService,
82+
@ITerminalContributionService private readonly _terminalContributionService: ITerminalContributionService,
7883
@IInstantiationService instantiationService: IInstantiationService,
7984
@IAudioCueService private readonly _audioCueService: IAudioCueService,
8085
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@@ -94,6 +99,12 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon,
9499
});
95100
}
96101
this._terminalDecorationHoverService = instantiationService.createInstance(TerminalDecorationHoverManager);
102+
for (const quickFix of this._terminalContributionService.quickFixes) {
103+
this.registerCommandFinishedListener(convertToQuickFixOptions(quickFix));
104+
}
105+
this.registerCommandFinishedListener(gitSimilar());
106+
this.registerCommandFinishedListener(convertToQuickFixOptions(gitCreatePr()));
107+
this.registerCommandFinishedListener(convertToQuickFixOptions(gitPushSetUpstream()));
97108
}
98109

99110
activate(terminal: Terminal): void {
@@ -331,3 +342,64 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) =
331342
collector.addRule(`.${DecorationSelector.CommandDecoration}.${DecorationSelector.QuickFix} { background-color: ${backgroundColor.toString()}; } `);
332343
}
333344
});
345+
346+
export function convertToQuickFixOptions(quickFix: IExtensionTerminalQuickFix): ITerminalQuickFixOptions {
347+
const type = quickFix.commandToRun ? 'command' : quickFix.linkToOpen ? 'opener' : undefined;
348+
const options = {
349+
id: quickFix.id,
350+
commandLineMatcher: quickFix.commandLineMatcher,
351+
outputMatcher: quickFix.outputMatcher,
352+
type,
353+
getQuickFixes: type === 'command' ? (matchResult: TerminalQuickFixMatchResult) => {
354+
const matches = matchResult.outputMatch;
355+
const commandToRun = quickFix.commandToRun;
356+
if (!matches || !commandToRun) {
357+
return;
358+
}
359+
const groups = matches.groups;
360+
if (!groups) {
361+
return;
362+
}
363+
const actions: TerminalQuickFixAction[] = [];
364+
let fixedCommand = commandToRun;
365+
for (const [key, value] of Object.entries(groups)) {
366+
const varToResolve = '${group:' + `${key}` + '}';
367+
if (!commandToRun.includes(varToResolve)) {
368+
return [];
369+
}
370+
fixedCommand = fixedCommand.replaceAll(varToResolve, value);
371+
}
372+
if (fixedCommand) {
373+
actions.push({
374+
type: 'command',
375+
command: fixedCommand,
376+
addNewLine: true
377+
});
378+
return actions;
379+
}
380+
return;
381+
} : (matchResult: TerminalQuickFixMatchResult) => {
382+
const matches = matchResult.outputMatch;
383+
const linkToOpen = quickFix.linkToOpen;
384+
if (!matches || !linkToOpen) {
385+
return;
386+
}
387+
const groups = matches.groups;
388+
if (!groups) {
389+
return;
390+
}
391+
let link = linkToOpen;
392+
for (const [key, value] of Object.entries(groups)) {
393+
const varToResolve = '${group:' + `${key}` + '}';
394+
if (!linkToOpen?.includes(varToResolve)) {
395+
return [];
396+
}
397+
link = link.replaceAll(varToResolve, value);
398+
}
399+
return link ? { type: 'opener', uri: URI.parse(link) } as ITerminalQuickFixOpenerAction : [];
400+
},
401+
exitStatus: quickFix.exitStatus,
402+
source: quickFix.extensionIdentifier
403+
};
404+
return options;
405+
}

src/vs/workbench/contrib/terminal/common/terminal.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,64 @@ export const terminalContributionsDescriptor: IExtensionPointDescriptor = {
717717
description: nls.localize('vscode.extension.contributes.terminal', 'Contributes terminal functionality.'),
718718
type: 'object',
719719
properties: {
720+
quickFixes: {
721+
type: 'array',
722+
description: nls.localize('vscode.extension.contributes.terminal.quickFixes', "Defines quick fixes for terminals with shell integration enabled."),
723+
items: {
724+
type: 'object',
725+
required: ['id', 'commandLineMatcher', 'outputMatcher'],
726+
defaultSnippets: [{
727+
body: {
728+
id: '$1',
729+
commandLineMatcher: '$2',
730+
outputMatcher: '$3',
731+
commandToRun: '$4',
732+
linkToOpen: '$5'
733+
}
734+
}],
735+
properties: {
736+
id: {
737+
description: nls.localize('vscode.extension.contributes.terminal.quickFixes.id', "The ID of the quick fix."),
738+
type: 'string',
739+
},
740+
commandLineMatcher: {
741+
description: nls.localize('vscode.extension.contributes.terminal.quickFixes.commandLineMatcher', "The command line to match."),
742+
type: 'string',
743+
},
744+
outputMatcher: {
745+
description: nls.localize('vscode.extension.contributes.terminal.quickFixes.outputMatcher', "The output to match, which provides groups of the form <group_name> to be referenced via ${group:group_name} in commandToRun and linkToOpen."),
746+
type: 'object',
747+
required: ['lineMatcher', 'anchor', 'offset', 'length'],
748+
properties: {
749+
lineMatcher: {
750+
description: 'The command line to match',
751+
type: 'string'
752+
},
753+
anchor: {
754+
description: 'Which side of the output to anchor the offset and length against',
755+
enum: ['top', 'bottom']
756+
},
757+
offset: {
758+
description: 'How far from either the top or the bottom of the butter to start matching against.',
759+
type: 'number'
760+
},
761+
length: {
762+
description: 'The number of rows to match against, this should be as small as possible for performance reasons',
763+
type: 'number'
764+
}
765+
}
766+
},
767+
commandToRun: {
768+
description: 'The command to run in the terminal for this match. Refer to a group found in the outputMatcher via ${group:group_name}. When provided, will take precedence over linkToOpen.',
769+
type: 'string'
770+
},
771+
linkToOpen: {
772+
description: 'The link to open for this match. Refer to a group found in the outputMatcher via ${group:group_name}. If a commandToRun is provided, this will be ignored.',
773+
type: 'string'
774+
}
775+
},
776+
}
777+
},
720778
profiles: {
721779
type: 'array',
722780
description: nls.localize('vscode.extension.contributes.terminal.profiles', "Defines additional terminal profiles that the user can create."),

0 commit comments

Comments
 (0)