From ac3707a190db2f69fab6a867072220794092e7af Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 28 Sep 2022 07:06:35 -0700 Subject: [PATCH] Allow opt-in multi-line quick fixes Fixes #161997 --- .../commandDetectionCapability.ts | 42 ++++++++++++++----- .../contrib/terminal/browser/terminal.ts | 25 +---------- .../browser/terminalQuickFixBuiltinActions.ts | 2 +- .../contrib/terminal/common/terminal.ts | 23 ++++++++-- .../test/browser/quickFixAddon.test.ts | 3 +- 5 files changed, 56 insertions(+), 39 deletions(-) diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index 0beddabbf5961..fee60af2df08a 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -490,7 +490,7 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { commandStartLineContent: this._currentCommand.commandStartLineContent, hasOutput: () => !executedMarker?.isDisposed && !endMarker?.isDisposed && !!(executedMarker && endMarker && executedMarker?.line < endMarker!.line), getOutput: () => getOutputForCommand(executedMarker, endMarker, buffer), - getOutputMatch: (outputMatcher?: { lineMatcher: string | RegExp; anchor?: 'top' | 'bottom'; offset?: number; length?: number }) => getOutputMatchForCommand(executedMarker, endMarker, buffer, this._terminal.cols, outputMatcher), + getOutputMatch: (outputMatcher: { lineMatcher: string | RegExp; anchor?: 'top' | 'bottom'; offset?: number; length?: number }) => getOutputMatchForCommand(executedMarker, endMarker, buffer, this._terminal.cols, outputMatcher), markProperties: options?.markProperties }; this._commands.push(newCommand); @@ -647,7 +647,7 @@ function getOutputForCommand(executedMarker: IMarker | undefined, endMarker: IMa return output === '' ? undefined : output; } -export function getOutputMatchForCommand(executedMarker: IMarker | undefined, endMarker: IMarker | undefined, buffer: IBuffer, cols: number, outputMatcher: { lineMatcher: string | RegExp; anchor?: 'top' | 'bottom'; offset?: number; length?: number } | undefined): RegExpMatchArray | undefined { +export function getOutputMatchForCommand(executedMarker: IMarker | undefined, endMarker: IMarker | undefined, buffer: IBuffer, cols: number, outputMatcher: { lineMatcher: string | RegExp; anchor?: 'top' | 'bottom'; offset?: number; length?: number }): RegExpMatchArray | undefined { if (!executedMarker || !endMarker) { return undefined; } @@ -657,23 +657,31 @@ export function getOutputMatchForCommand(executedMarker: IMarker | undefined, en if (startLine === endLine) { return undefined; } - if (outputMatcher?.length && (endLine - startLine) < outputMatcher.length) { + if (outputMatcher.length && (endLine - startLine) < outputMatcher.length) { return undefined; } - let line: string | undefined; - if (outputMatcher?.anchor === 'bottom') { + const matcher = outputMatcher.lineMatcher; + const linesToCheck = typeof matcher === 'string' ? 1 : countNewLines(matcher); + const lines: string[] = []; + if (outputMatcher.anchor === 'bottom') { for (let i = endLine - (outputMatcher.offset || 0); i >= startLine; i--) { - line = getXtermLineContent(buffer, i, i, cols); - const match = line.match(outputMatcher.lineMatcher); + lines.unshift(getXtermLineContent(buffer, i, i, cols)); + if (lines.length > linesToCheck) { + lines.pop(); + } + const match = lines.join('\n').match(matcher); if (match) { return match; } } } else { - for (let i = startLine + (outputMatcher?.offset || 0); i < endLine; i++) { - line = getXtermLineContent(buffer, i, i, cols); + for (let i = startLine + (outputMatcher.offset || 0); i < endLine; i++) { + lines.push(getXtermLineContent(buffer, i, i, cols)); + if (lines.length === linesToCheck) { + lines.shift(); + } if (outputMatcher) { - const match = line.match(outputMatcher.lineMatcher); + const match = lines.join('\n').match(matcher); if (match) { return match; } @@ -699,3 +707,17 @@ function getXtermLineContent(buffer: IBuffer, lineStart: number, lineEnd: number } return content; } + +function countNewLines(regex: RegExp): number { + if (!regex.multiline) { + return 1; + } + const source = regex.source; + let count = 1; + let i = source.indexOf('\\n'); + while (i !== -1) { + count++; + i = source.indexOf('\\n', i + 1); + } + return count; +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 9bb8e35938c4f..a7f3c9312771c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -20,7 +20,7 @@ import { IEditableData } from 'vs/workbench/common/views'; import { TerminalFindWidget } from 'vs/workbench/contrib/terminal/browser/terminalFindWidget'; import { ITerminalStatusList } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; import { ITerminalQuickFix } from 'vs/workbench/contrib/terminal/browser/xterm/quickFixAddon'; -import { INavigationMode, IRegisterContributedProfileArgs, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalBackend, ITerminalConfigHelper, ITerminalFont, ITerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/common/terminal'; +import { INavigationMode, IRegisterContributedProfileArgs, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalBackend, ITerminalConfigHelper, ITerminalFont, ITerminalOutputMatcher, ITerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/common/terminal'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { IMarker } from 'xterm'; @@ -933,29 +933,6 @@ export interface ITerminalQuickFixAction extends IAction { addNewLine?: boolean; } -/** - * A matcher that runs on a sub-section of a terminal command's output - */ -export interface ITerminalOutputMatcher { - /** - * A string or regex to match against the unwrapped line. - */ - lineMatcher: string | RegExp; - /** - * Which side of the output to anchor the {@link offset} and {@link length} against. - */ - anchor: 'top' | 'bottom'; - /** - * How far from either the top or the bottom of the butter to start matching against. - */ - offset: number; - /** - * The number of rows to match against, this should be as small as possible for performance - * reasons. - */ - length: number; -} - export interface IXtermTerminal { /** * An object that tracks when commands are run and enables navigating and selecting between diff --git a/src/vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions.ts index 61d15833cfbe4..578ce42b18102 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions.ts @@ -12,7 +12,7 @@ import { ITerminalCommand } from 'vs/workbench/contrib/terminal/common/terminal' export const GitCommandLineRegex = /git/; export const GitPushCommandLineRegex = /git\s+push/; export const AnyCommandLineRegex = /.+/; -export const GitSimilarOutputRegex = /most similar command is\s*([^\s]{3,})/; +export const GitSimilarOutputRegex = /most similar command is\s*\n\s*([^\s]{3,})/m; export const FreePortOutputRegex = /address already in use \d+\.\d+\.\d+\.\d+:(\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]+)/; // The previous line starts with "Create a pull request for \'([^\s]+)\' on GitHub by visiting:\s*", diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index c165c6cc43e12..319de723435ed 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -96,11 +96,28 @@ export interface IShellLaunchConfigResolveOptions { allowAutomationShell?: boolean; } +/** + * A matcher that runs on a sub-section of a terminal command's output + */ export interface ITerminalOutputMatcher { + /** + * A string or regex to match against the unwrapped line. If this is a regex with the multiline + * flag, it will scan an amount of lines equal to `\n` instances in the regex + 1. + */ lineMatcher: string | RegExp; - anchor?: 'top' | 'bottom'; - offset?: number; - length?: number; + /** + * Which side of the output to anchor the {@link offset} and {@link length} against. + */ + anchor: 'top' | 'bottom'; + /** + * How far from either the top or the bottom of the butter to start matching against. + */ + offset: number; + /** + * The number of rows to match against, this should be as small as possible for performance + * reasons. + */ + length: number; } export interface ITerminalBackend { 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 6de36f45a01a3..415a9d0663aea 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/quickFixAddon.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/quickFixAddon.test.ts @@ -14,9 +14,10 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { CommandDetectionCapability } from 'vs/platform/terminal/common/capabilities/commandDetectionCapability'; import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; -import { ITerminalQuickFixAction, ITerminalInstance, ITerminalOutputMatcher } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalQuickFixAction, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { freePort, FreePortOutputRegex, gitCreatePr, GitCreatePrOutputRegex, GitPushOutputRegex, gitPushSetUpstream, gitSimilarCommand, GitSimilarOutputRegex } from 'vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions'; import { TerminalQuickFixAddon, getQuickFixes } from 'vs/workbench/contrib/terminal/browser/xterm/quickFixAddon'; +import { ITerminalOutputMatcher } from 'vs/workbench/contrib/terminal/common/terminal'; import { Terminal } from 'xterm'; suite('QuickFixAddon', () => {