diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 91d7b0bf9c731..b6206d61d25ee 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -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'; @@ -931,11 +930,6 @@ export interface ITerminalInstance { */ openRecentLink(type: 'localFile' | 'url'): Promise; - /** - * 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 diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index eb8ee7572f17f..35284a4517940 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -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'; @@ -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); } } @@ -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 => { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions.ts index e28ad32b0688a..41049486563ca 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions.ts @@ -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/; @@ -17,6 +17,8 @@ export const GitPushOutputRegex = /git push --set-upstream origin (? // 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 PwshGeneralErrorOutputRegex = /Suggestion \[General\]:/; +export const PwshUnixCommandNotFoundErrorOutputRegex = /Suggestion \[cmd-not-found\]:/; export const enum QuickFixSource { Builtin = 'builtin' @@ -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: (?.+)./)?.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 '(?.+)' 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; + } + }; +} diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/quickFixAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/quickFixAddon.ts index 0ceeab71cefb4..215eb474c6087 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/quickFixAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/quickFixAddon.ts @@ -290,6 +290,10 @@ export async function getQuickFixesForCommand( onDidRequestRerunCommand?: Emitter<{ command: string; addNewLine?: boolean }>, getResolvedFixes?: (selector: ITerminalQuickFixOptions, lines?: string[]) => Promise ): Promise { + // Prevent duplicates by tracking added entries + const commandQuickFixSet: Set = new Set(); + const openQuickFixSet: Set = new Set(); + const fixes: ITerminalAction[] = []; const newCommand = terminalCommand.command; for (const options of quickFixOptions.values()) { @@ -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, @@ -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); diff --git a/src/vs/workbench/contrib/terminal/test/browser/quickFixAddon.test.ts b/src/vs/workbench/contrib/terminal/test/browser/quickFixAddon.test.ts index f7fe4b7f93bb4..dca44faf67edf 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/quickFixAddon.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/quickFixAddon.test.ts @@ -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'; @@ -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 { @@ -357,19 +453,18 @@ function createCommand(command: string, output: string, outputMatcher?: RegExp | type TestAction = Pick & { 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++; } }