Skip to content

Commit

Permalink
move builtin terminal quick fixes to contribution model (microsoft#16…
Browse files Browse the repository at this point in the history
  • Loading branch information
meganrogge authored and formigoni committed Oct 27, 2022
1 parent 23183f4 commit 97c9ce2
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 51 deletions.
16 changes: 15 additions & 1 deletion src/vs/platform/terminal/common/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Event } from 'vs/base/common/event';
import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform';
import { URI, UriComponents } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IPtyHostProcessReplayEvent, ISerializedCommandDetectionCapability, ITerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/capabilities';
import { IPtyHostProcessReplayEvent, ISerializedCommandDetectionCapability, ITerminalCapabilityStore, ITerminalOutputMatcher } from 'vs/platform/terminal/common/capabilities/capabilities';
import { IGetTerminalLayoutInfoArgs, IProcessDetails, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { ISerializableEnvironmentVariableCollections } from 'vs/platform/terminal/common/environmentVariable';
Expand Down Expand Up @@ -797,6 +797,16 @@ export interface ITerminalProfileSource extends IBaseUnresolvedTerminalProfile {

export interface ITerminalContributions {
profiles?: ITerminalProfileContribution[];
quickFixes?: ITerminalQuickFixContribution[];
}

export interface ITerminalQuickFixContribution {
id: string;
commandLineMatcher: string | RegExp;
outputMatcher: ITerminalOutputMatcher;
exitStatus?: boolean;
commandToRun?: string;
linkToOpen?: string;
}

export interface ITerminalProfileContribution {
Expand All @@ -810,6 +820,10 @@ export interface IExtensionTerminalProfile extends ITerminalProfileContribution
extensionIdentifier: string;
}

export interface IExtensionTerminalQuickFix extends ITerminalQuickFixContribution {
extensionIdentifier: string;
}

export type ITerminalProfileObject = ITerminalExecutable | ITerminalProfileSource | IExtensionTerminalProfile | null;
export type ITerminalProfileType = ITerminalProfile | IExtensionTerminalProfile;

Expand Down
4 changes: 3 additions & 1 deletion src/vs/workbench/contrib/terminal/browser/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -949,8 +949,9 @@ export interface ITerminalInstance {

/**
* Attempts to detect and kill the process listening on specified port.
* If successful, places commandToRun on the command line
*/
freePortKillProcess(port: string): Promise<void>;
freePortKillProcess(port: string, commandToRun: string): Promise<void>;
}

export interface ITerminalQuickFixOptions {
Expand All @@ -959,6 +960,7 @@ export interface ITerminalQuickFixOptions {
outputMatcher?: ITerminalOutputMatcher;
getQuickFixes: TerminalQuickFixCallback;
exitStatus?: boolean;
source: string;
}
export type TerminalQuickFixMatchResult = { commandLineMatch: RegExpMatchArray; outputMatch?: RegExpMatchArray | null };
export type TerminalQuickFixAction = IAction | ITerminalQuickFixCommandAction | ITerminalQuickFixOpenerAction;
Expand Down
7 changes: 4 additions & 3 deletions src/vs/workbench/contrib/terminal/browser/terminalInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ import { IDetectedLinks, TerminalLinkManager } from 'vs/workbench/contrib/termin
import { TerminalLinkQuickpick } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkQuickpick';
import { IRequestAddInstanceToGroupEvent, ITerminalQuickFixOptions, ITerminalExternalLinkProvider, ITerminalInstance, TerminalDataTransfers } from 'vs/workbench/contrib/terminal/browser/terminal';
import { TerminalLaunchHelpAction } from 'vs/workbench/contrib/terminal/browser/terminalActions';
import { gitSimilarCommand, gitCreatePr, gitPushSetUpstream, freePort, gitTwoDashes } from 'vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions';
import { freePort, gitTwoDashes } from 'vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions';
import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper';
import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput';
import { TerminalFindWidget } from 'vs/workbench/contrib/terminal/browser/terminalFindWidget';
Expand Down Expand Up @@ -731,7 +731,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
this.xterm = xterm;
this._quickFixAddon = this._scopedInstantiationService.createInstance(TerminalQuickFixAddon, this.capabilities);
this.xterm?.raw.loadAddon(this._quickFixAddon);
this.registerQuickFixProvider(gitSimilarCommand(), gitTwoDashes(), gitCreatePr(), gitPushSetUpstream(), freePort(this));
this.registerQuickFixProvider(gitTwoDashes(), freePort(this));
this._register(this._quickFixAddon.onDidRequestRerunCommand(async (e) => await this.runCommand(e.command, e.addNewLine || false)));
const lineDataEventAddon = new LineDataEventAddon();
this.xterm.raw.loadAddon(lineDataEventAddon);
Expand Down Expand Up @@ -1541,8 +1541,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
this.xterm?.markTracker.scrollToClosestMarker(startMarkId, endMarkId, highlight);
}

public async freePortKillProcess(port: string): Promise<void> {
public async freePortKillProcess(port: string, command: string): Promise<void> {
await this._processManager?.freePortKillProcess(port);
this.runCommand(command, false);
}

private _onProcessData(ev: IProcessDataEvent): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@
import { localize } from 'vs/nls';
import { TerminalQuickFixMatchResult, ITerminalQuickFixOptions, ITerminalInstance, TerminalQuickFixAction } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ITerminalCommand } from 'vs/workbench/contrib/terminal/common/terminal';
import { URI } from 'vs/base/common/uri';

import { IExtensionTerminalQuickFix } from 'vs/platform/terminal/common/terminal';
export const GitCommandLineRegex = /git/;
export const GitPushCommandLineRegex = /git\s+push/;
export const GitTwoDashesRegex = /error: did you mean `--(.+)` \(with two dashes\)\?/;
export const AnyCommandLineRegex = /.+/;
export const GitSimilarOutputRegex = /(?:(most similar (command|commands) (is|are)))((\n\s*[^\s]+)+)/m;
export const GitSimilarOutputRegex = /(?:(most similar (command|commands) (is|are)))((\n\s*(?<fixedCommand>[^\s]+))+)/m;
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})/;
export const GitPushOutputRegex = /git push --set-upstream origin ([^\s]+)/;
export const GitPushOutputRegex = /git push --set-upstream origin (?<branchName>[^\s]+)/;
// The previous line starts with "Create a pull request for \'([^\s]+)\' on GitHub by visiting:\s*"
// it's safe to assume it's a github pull request if the URL includes `/pull/`
export const GitCreatePrOutputRegex = /remote:\s*(https:\/\/github\.com\/.+\/.+\/pull\/new\/.+)/;
export const GitCreatePrOutputRegex = /remote:\s*(?<link>https:\/\/github\.com\/.+\/.+\/pull\/new\/.+)/;

export function gitSimilarCommand(): ITerminalQuickFixOptions {
export function gitSimilar(): ITerminalQuickFixOptions {
return {
source: 'builtin',
id: 'Git Similar',
commandLineMatcher: GitCommandLineRegex,
outputMatcher: {
Expand Down Expand Up @@ -50,8 +50,10 @@ export function gitSimilarCommand(): ITerminalQuickFixOptions {
}
};
}

export function gitTwoDashes(): ITerminalQuickFixOptions {
return {
source: 'builtin',
id: 'Git Two Dashes',
commandLineMatcher: GitCommandLineRegex,
outputMatcher: {
Expand All @@ -76,6 +78,7 @@ export function gitTwoDashes(): ITerminalQuickFixOptions {
}
export function freePort(terminalInstance?: Partial<ITerminalInstance>): ITerminalQuickFixOptions {
return {
source: 'builtin',
id: 'Free Port',
commandLineMatcher: AnyCommandLineRegex,
outputMatcher: {
Expand All @@ -97,12 +100,15 @@ export function freePort(terminalInstance?: Partial<ITerminalInstance>): ITermin
id: 'terminal.freePort',
label,
enabled: true,
run: async () => terminalInstance?.freePortKillProcess?.(port)
run: async () => {
await terminalInstance?.freePortKillProcess?.(port, command.command);
}
};
}
};
}
export function gitPushSetUpstream(): ITerminalQuickFixOptions {

export function gitPushSetUpstream(): IExtensionTerminalQuickFix {
return {
id: 'Git Push Set Upstream',
commandLineMatcher: GitPushCommandLineRegex,
Expand All @@ -113,21 +119,12 @@ export function gitPushSetUpstream(): ITerminalQuickFixOptions {
length: 5
},
exitStatus: false,
getQuickFixes: (matchResult: TerminalQuickFixMatchResult, command: ITerminalCommand) => {
const branch = matchResult?.outputMatch?.[1];
if (!branch) {
return;
}
return {
type: 'command',
command: `git push --set-upstream origin ${branch}`,
addNewLine: true
};
}
commandToRun: 'git push --set-upstream origin ${group:branchName}',
extensionIdentifier: 'git'
};
}

export function gitCreatePr(): ITerminalQuickFixOptions {
export function gitCreatePr(): IExtensionTerminalQuickFix {
return {
id: 'Git Create Pr',
commandLineMatcher: GitPushCommandLineRegex,
Expand All @@ -138,18 +135,7 @@ export function gitCreatePr(): ITerminalQuickFixOptions {
length: 5
},
exitStatus: true,
getQuickFixes: (matchResult: TerminalQuickFixMatchResult, command?: ITerminalCommand) => {
if (!command) {
return;
}
const link = matchResult?.outputMatch?.[1];
if (!link) {
return;
}
return {
type: 'opener',
uri: URI.parse(link)
};
}
linkToOpen: '${group:link}',
extensionIdentifier: 'git'
};
}
74 changes: 73 additions & 1 deletion src/vs/workbench/contrib/terminal/browser/xterm/quickFixAddon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { PANEL_BACKGROUND } from 'vs/workbench/common/theme';
import { AudioCue, IAudioCueService } from 'vs/workbench/contrib/audioCues/browser/audioCueService';
import { ITerminalQuickFixOptions } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ITerminalQuickFixOpenerAction, ITerminalQuickFixOptions, TerminalQuickFixAction, TerminalQuickFixMatchResult } from 'vs/workbench/contrib/terminal/browser/terminal';
import { DecorationSelector, TerminalDecorationHoverManager, updateLayout } from 'vs/workbench/contrib/terminal/browser/xterm/decorationStyles';
import { TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal';
import { TERMINAL_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry';
Expand All @@ -28,6 +28,10 @@ import { IDecoration, Terminal } from 'xterm';
import type { ITerminalAddon } from 'xterm-headless';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ILogService } from 'vs/platform/log/common/log';
import { ITerminalContributionService } from 'vs/workbench/contrib/terminal/common/terminalExtensionPoints';
import { IExtensionTerminalQuickFix } from 'vs/platform/terminal/common/terminal';
import { URI } from 'vs/base/common/uri';
import { gitCreatePr, gitPushSetUpstream, gitSimilar } from 'vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions';
const quickFixTelemetryTitle = 'terminal/quick-fix';
type QuickFixResultTelemetryEvent = {
id: string;
Expand Down Expand Up @@ -75,6 +79,7 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon,
constructor(private readonly _capabilities: ITerminalCapabilityStore,
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ITerminalContributionService private readonly _terminalContributionService: ITerminalContributionService,
@IInstantiationService instantiationService: IInstantiationService,
@IAudioCueService private readonly _audioCueService: IAudioCueService,
@IKeybindingService private readonly _keybindingService: IKeybindingService,
Expand All @@ -94,6 +99,12 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon,
});
}
this._terminalDecorationHoverService = instantiationService.createInstance(TerminalDecorationHoverManager);
for (const quickFix of this._terminalContributionService.quickFixes) {
this.registerCommandFinishedListener(convertToQuickFixOptions(quickFix));
}
this.registerCommandFinishedListener(gitSimilar());
this.registerCommandFinishedListener(convertToQuickFixOptions(gitCreatePr()));
this.registerCommandFinishedListener(convertToQuickFixOptions(gitPushSetUpstream()));
}

activate(terminal: Terminal): void {
Expand Down Expand Up @@ -331,3 +342,64 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) =
collector.addRule(`.${DecorationSelector.CommandDecoration}.${DecorationSelector.QuickFix} { background-color: ${backgroundColor.toString()}; } `);
}
});

export function convertToQuickFixOptions(quickFix: IExtensionTerminalQuickFix): ITerminalQuickFixOptions {
const type = quickFix.commandToRun ? 'command' : quickFix.linkToOpen ? 'opener' : undefined;
const options = {
id: quickFix.id,
commandLineMatcher: quickFix.commandLineMatcher,
outputMatcher: quickFix.outputMatcher,
type,
getQuickFixes: type === 'command' ? (matchResult: TerminalQuickFixMatchResult) => {
const matches = matchResult.outputMatch;
const commandToRun = quickFix.commandToRun;
if (!matches || !commandToRun) {
return;
}
const groups = matches.groups;
if (!groups) {
return;
}
const actions: TerminalQuickFixAction[] = [];
let fixedCommand = commandToRun;
for (const [key, value] of Object.entries(groups)) {
const varToResolve = '${group:' + `${key}` + '}';
if (!commandToRun.includes(varToResolve)) {
return [];
}
fixedCommand = fixedCommand.replaceAll(varToResolve, value);
}
if (fixedCommand) {
actions.push({
type: 'command',
command: fixedCommand,
addNewLine: true
});
return actions;
}
return;
} : (matchResult: TerminalQuickFixMatchResult) => {
const matches = matchResult.outputMatch;
const linkToOpen = quickFix.linkToOpen;
if (!matches || !linkToOpen) {
return;
}
const groups = matches.groups;
if (!groups) {
return;
}
let link = linkToOpen;
for (const [key, value] of Object.entries(groups)) {
const varToResolve = '${group:' + `${key}` + '}';
if (!linkToOpen?.includes(varToResolve)) {
return [];
}
link = link.replaceAll(varToResolve, value);
}
return link ? { type: 'opener', uri: URI.parse(link) } as ITerminalQuickFixOpenerAction : [];
},
exitStatus: quickFix.exitStatus,
source: quickFix.extensionIdentifier
};
return options;
}
58 changes: 58 additions & 0 deletions src/vs/workbench/contrib/terminal/common/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,64 @@ export const terminalContributionsDescriptor: IExtensionPointDescriptor = {
description: nls.localize('vscode.extension.contributes.terminal', 'Contributes terminal functionality.'),
type: 'object',
properties: {
quickFixes: {
type: 'array',
description: nls.localize('vscode.extension.contributes.terminal.quickFixes', "Defines quick fixes for terminals with shell integration enabled."),
items: {
type: 'object',
required: ['id', 'commandLineMatcher', 'outputMatcher'],
defaultSnippets: [{
body: {
id: '$1',
commandLineMatcher: '$2',
outputMatcher: '$3',
commandToRun: '$4',
linkToOpen: '$5'
}
}],
properties: {
id: {
description: nls.localize('vscode.extension.contributes.terminal.quickFixes.id', "The ID of the quick fix."),
type: 'string',
},
commandLineMatcher: {
description: nls.localize('vscode.extension.contributes.terminal.quickFixes.commandLineMatcher', "The command line to match."),
type: 'string',
},
outputMatcher: {
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."),
type: 'object',
required: ['lineMatcher', 'anchor', 'offset', 'length'],
properties: {
lineMatcher: {
description: 'The command line to match',
type: 'string'
},
anchor: {
description: 'Which side of the output to anchor the offset and length against',
enum: ['top', 'bottom']
},
offset: {
description: 'How far from either the top or the bottom of the butter to start matching against.',
type: 'number'
},
length: {
description: 'The number of rows to match against, this should be as small as possible for performance reasons',
type: 'number'
}
}
},
commandToRun: {
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.',
type: 'string'
},
linkToOpen: {
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.',
type: 'string'
}
},
}
},
profiles: {
type: 'array',
description: nls.localize('vscode.extension.contributes.terminal.profiles', "Defines additional terminal profiles that the user can create."),
Expand Down
Loading

0 comments on commit 97c9ce2

Please sign in to comment.