Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions src/vs/workbench/contrib/terminal/browser/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { IKeyMods } from 'vs/platform/quickinput/common/quickInput';
import { IMarkProperties, ITerminalCapabilityStore, ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities';
import { IExtensionTerminalProfile, IReconnectionProperties, IShellIntegration, IShellLaunchConfig, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, TerminalExitReason, TerminalIcon, TerminalLocation, TerminalShellType, TerminalType, TitleEventSource, WaitOnExitValue } from 'vs/platform/terminal/common/terminal';
import { ITerminalQuickFixOptions } from 'vs/platform/terminal/common/xterm/terminalQuickFix';
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { IEditableData } from 'vs/workbench/common/views';
Expand Down Expand Up @@ -931,11 +930,6 @@ export interface ITerminalInstance {
*/
openRecentLink(type: 'localFile' | 'url'): Promise<void>;

/**
* Registers quick fix providers
*/
registerQuickFixProvider(...options: ITerminalQuickFixOptions[]): void;

/**
* Attempts to detect and kill the process listening on specified port.
* If successful, places commandToRun on the command line
Expand Down
8 changes: 4 additions & 4 deletions src/vs/workbench/contrib/terminal/browser/terminalInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ import { IDetectedLinks, TerminalLinkManager } from 'vs/workbench/contrib/termin
import { TerminalLinkQuickpick } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkQuickpick';
import { IRequestAddInstanceToGroupEvent, ITerminalExternalLinkProvider, ITerminalInstance, TerminalDataTransfers } from 'vs/workbench/contrib/terminal/browser/terminal';
import { TerminalLaunchHelpAction } from 'vs/workbench/contrib/terminal/browser/terminalActions';
import { freePort, gitCreatePr, gitPushSetUpstream, gitSimilar, gitTwoDashes } from 'vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions';
import { freePort, gitCreatePr, gitPushSetUpstream, gitSimilar, gitTwoDashes, pwshGeneralError as pwshGeneralError, pwshUnixCommandNotFoundError } 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 @@ -621,9 +621,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
return undefined;
}

registerQuickFixProvider(...options: ITerminalQuickFixOptions[]): void {
private _registerQuickFixProvider(quickFixAddon: ITerminalQuickFixAddon, ...options: ITerminalQuickFixOptions[]): void {
for (const actionOption of options) {
this.quickFix?.registerCommandFinishedListener(actionOption);
quickFixAddon.registerCommandFinishedListener(actionOption);
}
}

Expand Down Expand Up @@ -738,7 +738,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
this.xterm = xterm;
this._quickFixAddon = this._scopedInstantiationService.createInstance(TerminalQuickFixAddon, this._aliases, this.capabilities);
this.xterm?.raw.loadAddon(this._quickFixAddon);
this.registerQuickFixProvider(gitTwoDashes(), freePort(this), gitSimilar(), gitPushSetUpstream(), gitCreatePr());
this._registerQuickFixProvider(this._quickFixAddon, gitTwoDashes(), freePort(this), gitSimilar(), gitPushSetUpstream(), gitCreatePr(), pwshUnixCommandNotFoundError(), pwshGeneralError());
this._register(this._quickFixAddon.onDidRequestRerunCommand(async (e) => await this.runCommand(e.command, e.addNewLine || false)));
this.updateAccessibilitySupport();
this.xterm.onDidRequestRunCommand(e => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { IInternalOptions, ITerminalCommandMatchResult, TerminalQuickFixActionInternal, TerminalQuickFixType } from 'vs/platform/terminal/common/xterm/terminalQuickFix';
import { IInternalOptions, ITerminalCommandMatchResult, ITerminalQuickFixCommandAction, TerminalQuickFixActionInternal, TerminalQuickFixType } from 'vs/platform/terminal/common/xterm/terminalQuickFix';
import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal';

export const GitCommandLineRegex = /git/;
Expand All @@ -17,6 +17,8 @@ export const GitPushOutputRegex = /git push --set-upstream origin (?<branchName>
// 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*(?<link>https:\/\/github\.com\/.+\/.+\/pull\/new\/.+)/;
export const PwshGeneralErrorOutputRegex = /Suggestion \[General\]:/;
export const PwshUnixCommandNotFoundErrorOutputRegex = /Suggestion \[cmd-not-found\]:/;

export const enum QuickFixSource {
Builtin = 'builtin'
Expand Down Expand Up @@ -193,3 +195,120 @@ export function gitCreatePr(): IInternalOptions {
}
};
}

export function pwshGeneralError(): IInternalOptions {
return {
id: 'Pwsh General Error',
type: 'internal',
commandLineMatcher: /.+/,
outputMatcher: {
lineMatcher: PwshGeneralErrorOutputRegex,
anchor: 'bottom',
offset: 0,
length: 10
},
commandExitResult: 'error',
getQuickFixes: (matchResult: ITerminalCommandMatchResult) => {
const lines = matchResult.outputMatch?.regexMatch.input?.split('\n');
if (!lines) {
return;
}

// Find the start
let i = 0;
let inFeedbackProvider = false;
for (; i < lines.length; i++) {
if (lines[i].match(PwshGeneralErrorOutputRegex)) {
inFeedbackProvider = true;
break;
}
}
if (!inFeedbackProvider) {
return;
}

const suggestions = lines[i + 1].match(/The most similar commands are: (?<values>.+)./)?.groups?.values?.split(', ');
if (!suggestions) {
return;
}
const result: ITerminalQuickFixCommandAction[] = [];
for (const suggestion of suggestions) {
result.push({
id: 'Pwsh General Error',
type: TerminalQuickFixType.Command,
terminalCommand: suggestion,
source: QuickFixSource.Builtin
});
}
return result;
}
};
}

export function pwshUnixCommandNotFoundError(): IInternalOptions {
return {
id: 'Unix Command Not Found',
type: 'internal',
commandLineMatcher: /.+/,
outputMatcher: {
lineMatcher: PwshUnixCommandNotFoundErrorOutputRegex,
anchor: 'bottom',
offset: 0,
length: 10
},
commandExitResult: 'error',
getQuickFixes: (matchResult: ITerminalCommandMatchResult) => {
const lines = matchResult.outputMatch?.regexMatch.input?.split('\n');
if (!lines) {
return;
}

// Find the start
let i = 0;
let inFeedbackProvider = false;
for (; i < lines.length; i++) {
if (lines[i].match(PwshUnixCommandNotFoundErrorOutputRegex)) {
inFeedbackProvider = true;
break;
}
}
if (!inFeedbackProvider) {
return;
}

// Always remove the first element as it's the "Suggestion [cmd-not-found]"" line
const result: ITerminalQuickFixCommandAction[] = [];
let inSuggestions = false;
for (; i < lines.length; i++) {
const line = lines[i].trim();
if (line.length === 0) {
break;
}
const installCommand = line.match(/You also have .+ installed, you can run '(?<command>.+)' instead./)?.groups?.command;
if (installCommand) {
result.push({
id: 'Pwsh Unix Command Not Found Error',
type: TerminalQuickFixType.Command,
terminalCommand: installCommand,
source: QuickFixSource.Builtin
});
inSuggestions = false;
continue;
}
if (line.match(/Command '.+' not found, but can be installed with:/)) {
inSuggestions = true;
continue;
}
if (inSuggestions) {
result.push({
id: 'Pwsh Unix Command Not Found Error',
type: TerminalQuickFixType.Command,
terminalCommand: line.trim(),
source: QuickFixSource.Builtin
});
}
}
return result;
}
};
}
12 changes: 12 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/xterm/quickFixAddon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,10 @@ export async function getQuickFixesForCommand(
onDidRequestRerunCommand?: Emitter<{ command: string; addNewLine?: boolean }>,
getResolvedFixes?: (selector: ITerminalQuickFixOptions, lines?: string[]) => Promise<ITerminalQuickFix | ITerminalQuickFix[] | undefined>
): Promise<ITerminalAction[] | undefined> {
// Prevent duplicates by tracking added entries
const commandQuickFixSet: Set<string> = new Set();
const openQuickFixSet: Set<string> = new Set();

const fixes: ITerminalAction[] = [];
const newCommand = terminalCommand.command;
for (const options of quickFixOptions.values()) {
Expand Down Expand Up @@ -329,6 +333,10 @@ export async function getQuickFixesForCommand(
switch (quickFix.type) {
case TerminalQuickFixType.Command: {
const fix = quickFix as ITerminalQuickFixCommandAction;
if (commandQuickFixSet.has(fix.terminalCommand)) {
continue;
}
commandQuickFixSet.add(fix.terminalCommand);
const label = localize('quickFix.command', 'Run: {0}', fix.terminalCommand);
action = {
type: TerminalQuickFixType.Command,
Expand All @@ -353,6 +361,10 @@ export async function getQuickFixesForCommand(
if (!fix.uri) {
return;
}
if (openQuickFixSet.has(fix.uri.toString())) {
continue;
}
openQuickFixSet.add(fix.uri.toString());
const isUrl = (fix.uri.scheme === Schemas.http || fix.uri.scheme === Schemas.https);
const uriLabel = isUrl ? encodeURI(fix.uri.toString(true)) : labelService.getUriLabel(fix.uri);
const label = localize('quickFix.opener', 'Open: {0}', uriLabel);
Expand Down
121 changes: 108 additions & 13 deletions src/vs/workbench/contrib/terminal/test/browser/quickFixAddon.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/commo
import { CommandDetectionCapability } from 'vs/platform/terminal/common/capabilities/commandDetectionCapability';
import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore';
import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal';
import { gitSimilar, freePort, FreePortOutputRegex, gitCreatePr, GitCreatePrOutputRegex, GitPushOutputRegex, gitPushSetUpstream, GitSimilarOutputRegex, gitTwoDashes, GitTwoDashesRegex } from 'vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions';
import { gitSimilar, freePort, FreePortOutputRegex, gitCreatePr, GitCreatePrOutputRegex, GitPushOutputRegex, gitPushSetUpstream, GitSimilarOutputRegex, gitTwoDashes, GitTwoDashesRegex, pwshUnixCommandNotFoundError, PwshUnixCommandNotFoundErrorOutputRegex, pwshGeneralError, PwshGeneralErrorOutputRegex } from 'vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions';
import { TerminalQuickFixAddon, getQuickFixesForCommand } from 'vs/workbench/contrib/terminal/browser/xterm/quickFixAddon';
import { URI } from 'vs/base/common/uri';
import { Terminal } from 'xterm';
Expand Down Expand Up @@ -333,6 +333,102 @@ suite('QuickFixAddon', () => {
});
});
});
suite('pwsh feedback providers', () => {
suite('General', () => {
const expectedMap = new Map();
const command = `not important`;
const output = [
`...`,
``,
`Suggestion [General]:`,
` The most similar commands are: python3, python3m, pamon, python3.6, rtmon, echo, pushd, etsn, pwsh, pwconv.`,
``,
`Suggestion [cmd-not-found]:`,
` Command 'python' not found, but can be installed with:`,
` sudo apt install python3`,
` sudo apt install python`,
` sudo apt install python-minimal`,
` You also have python3 installed, you can run 'python3' instead.'`,
``,
].join('\n');
const exitCode = 128;
const actions = [
'python3',
'python3m',
'pamon',
'python3.6',
'rtmon',
'echo',
'pushd',
'etsn',
'pwsh',
'pwconv',
].map(command => {
return {
id: 'Pwsh General Error',
enabled: true,
label: `Run: ${command}`,
tooltip: `Run: ${command}`,
command: command
};
});
setup(() => {
const pushCommand = pwshGeneralError();
quickFixAddon.registerCommandFinishedListener(pushCommand);
expectedMap.set(pushCommand.commandLineMatcher.toString(), [pushCommand]);
});
test('returns undefined when output does not match', async () => {
strictEqual((await getQuickFixesForCommand([], terminal, createCommand(command, `invalid output`, PwshGeneralErrorOutputRegex, exitCode), expectedMap, openerService, labelService)), undefined);
});
test('returns actions when output matches', async () => {
assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, PwshGeneralErrorOutputRegex, exitCode), expectedMap, openerService, labelService)), actions);
});
});
suite('Unix cmd-not-found', () => {
const expectedMap = new Map();
const command = `not important`;
const output = [
`...`,
``,
`Suggestion [General]`,
` The most similar commands are: python3, python3m, pamon, python3.6, rtmon, echo, pushd, etsn, pwsh, pwconv.`,
``,
`Suggestion [cmd-not-found]:`,
` Command 'python' not found, but can be installed with:`,
` sudo apt install python3`,
` sudo apt install python`,
` sudo apt install python-minimal`,
` You also have python3 installed, you can run 'python3' instead.'`,
``,
].join('\n');
const exitCode = 128;
const actions = [
'sudo apt install python3',
'sudo apt install python',
'sudo apt install python-minimal',
'python3',
].map(command => {
return {
id: 'Pwsh Unix Command Not Found Error',
enabled: true,
label: `Run: ${command}`,
tooltip: `Run: ${command}`,
command: command
};
});
setup(() => {
const pushCommand = pwshUnixCommandNotFoundError();
quickFixAddon.registerCommandFinishedListener(pushCommand);
expectedMap.set(pushCommand.commandLineMatcher.toString(), [pushCommand]);
});
test('returns undefined when output does not match', async () => {
strictEqual((await getQuickFixesForCommand([], terminal, createCommand(command, `invalid output`, PwshUnixCommandNotFoundErrorOutputRegex, exitCode), expectedMap, openerService, labelService)), undefined);
});
test('returns actions when output matches', async () => {
assertMatchOptions((await getQuickFixesForCommand([], terminal, createCommand(command, output, PwshUnixCommandNotFoundErrorOutputRegex, exitCode), expectedMap, openerService, labelService)), actions);
});
});
});
});

function createCommand(command: string, output: string, outputMatcher?: RegExp | string, exitCode?: number): ITerminalCommand {
Expand All @@ -357,19 +453,18 @@ function createCommand(command: string, output: string, outputMatcher?: RegExp |
type TestAction = Pick<IAction, 'id' | 'label' | 'tooltip' | 'enabled'> & { command?: string; uri?: URI };
function assertMatchOptions(actual: TestAction[] | undefined, expected: TestAction[]): void {
strictEqual(actual?.length, expected.length);
let index = 0;
for (const i of actual) {
const j = expected[index];
strictEqual(i.id, j.id, `ID`);
strictEqual(i.enabled, j.enabled, `enabled`);
strictEqual(i.label, j.label, `label`);
strictEqual(i.tooltip, j.tooltip, `tooltip`);
if (j.command) {
strictEqual(i.command, j.command);
for (let i = 0; i < expected.length; i++) {
const expectedItem = expected[i];
const actualItem: any = actual[i];
strictEqual(actualItem.id, expectedItem.id, `ID`);
strictEqual(actualItem.enabled, expectedItem.enabled, `enabled`);
strictEqual(actualItem.label, expectedItem.label, `label`);
strictEqual(actualItem.tooltip, expectedItem.tooltip, `tooltip`);
if (expectedItem.command) {
strictEqual(actualItem.command, expectedItem.command);
}
if (j.uri) {
strictEqual(i.uri!.toString(), j.uri.toString());
if (expectedItem.uri) {
strictEqual(actualItem.uri!.toString(), expectedItem.uri.toString());
}
index++;
}
}