Skip to content

Commit e19f06e

Browse files
authored
Merge pull request microsoft#166326 from microsoft/merogge/quick-fix-api
add terminal quick fix API
2 parents 1cdfaf2 + 13d83da commit e19f06e

30 files changed

+786
-360
lines changed

src/vs/platform/actionWidget/browser/actionList.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55
import * as dom from 'vs/base/browser/dom';
6+
import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
67
import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel';
78
import { IListEvent, IListMouseEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
89
import { List } from 'vs/base/browser/ui/list/listWidget';
@@ -29,6 +30,7 @@ export interface IListMenuItem<T extends IActionItem> {
2930
group?: { kind?: any; icon?: { codicon: Codicon; color?: string }; title: string };
3031
disabled?: boolean;
3132
label?: string;
33+
description?: string;
3234
}
3335

3436
interface IActionMenuTemplateData {
@@ -131,6 +133,11 @@ class ActionItemRenderer<T extends IListMenuItem<IActionItem>> implements IListR
131133
} else {
132134
data.container.title = '';
133135
}
136+
if (element.description) {
137+
const label = new HighlightedLabel(dom.append(data.container, dom.$('span.label-description')));
138+
label.element.classList.add('action-list-description');
139+
label.set(element.description);
140+
}
134141
}
135142

136143
disposeTemplate(_templateData: IActionMenuTemplateData): void {

src/vs/platform/actionWidget/browser/actionWidget.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
.action-list-description {
7+
opacity: .7;
8+
margin-left: 0.5em;
9+
font-size: .9em;
10+
white-space: pre;
11+
}
12+
613
.action-widget {
714
font-size: 13px;
815
border-radius: 0;

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

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { Event } from 'vs/base/common/event';
77
import { IDisposable } from 'vs/base/common/lifecycle';
88
import { ReplayEntry } from 'vs/platform/terminal/common/terminalProcess';
9+
import { ITerminalOutputMatch, ITerminalOutputMatcher } from 'vs/platform/terminal/common/xterm/terminalQuickFix';
910

1011
interface IEvent<T, U = void> {
1112
(listener: (arg1: T, arg2: U) => any): IDisposable;
@@ -220,36 +221,12 @@ export interface ITerminalCommand {
220221
executedMarker?: IXtermMarker;
221222
commandStartLineContent?: string;
222223
markProperties?: IMarkProperties;
224+
aliases?: string[][];
223225
getOutput(): string | undefined;
224-
getOutputMatch(outputMatcher: ITerminalOutputMatcher): RegExpMatchArray | undefined;
226+
getOutputMatch(outputMatcher: ITerminalOutputMatcher): ITerminalOutputMatch | undefined;
225227
hasOutput(): boolean;
226228
}
227229

228-
229-
/**
230-
* A matcher that runs on a sub-section of a terminal command's output
231-
*/
232-
export interface ITerminalOutputMatcher {
233-
/**
234-
* A string or regex to match against the unwrapped line. If this is a regex with the multiline
235-
* flag, it will scan an amount of lines equal to `\n` instances in the regex + 1.
236-
*/
237-
lineMatcher: string | RegExp;
238-
/**
239-
* Which side of the output to anchor the {@link offset} and {@link length} against.
240-
*/
241-
anchor: 'top' | 'bottom';
242-
/**
243-
* How far from either the top or the bottom of the butter to start matching against.
244-
*/
245-
offset: number;
246-
/**
247-
* The number of rows to match against, this should be as small as possible for performance
248-
* reasons.
249-
*/
250-
length: number;
251-
}
252-
253230
/**
254231
* A clone of the IMarker from xterm which cannot be imported from common
255232
*/

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

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { timeout } from 'vs/base/common/async';
77
import { debounce } from 'vs/base/common/decorators';
88
import { Emitter } from 'vs/base/common/event';
99
import { ILogService } from 'vs/platform/log/common/log';
10-
import { ICommandDetectionCapability, TerminalCapability, ITerminalCommand, IHandleCommandOptions, ICommandInvalidationRequest, CommandInvalidationReason, ISerializedCommand, ISerializedCommandDetectionCapability, ITerminalOutputMatcher } from 'vs/platform/terminal/common/capabilities/capabilities';
10+
import { ICommandDetectionCapability, TerminalCapability, ITerminalCommand, IHandleCommandOptions, ICommandInvalidationRequest, CommandInvalidationReason, ISerializedCommand, ISerializedCommandDetectionCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
11+
import { ITerminalOutputMatch, ITerminalOutputMatcher } from 'vs/platform/terminal/common/xterm/terminalQuickFix';
1112

1213
// Importing types is safe in any layer
1314
// eslint-disable-next-line local/code-import-patterns
@@ -559,7 +560,8 @@ export class CommandDetectionCapability implements ICommandDetectionCapability {
559560
exitCode: e.exitCode,
560561
commandStartLineContent: e.commandStartLineContent,
561562
timestamp: e.timestamp,
562-
markProperties: e.markProperties
563+
markProperties: e.markProperties,
564+
aliases: e.aliases
563565
};
564566
});
565567
if (this._currentCommand.commandStartMarker) {
@@ -647,7 +649,7 @@ function getOutputForCommand(executedMarker: IMarker | undefined, endMarker: IMa
647649
return output === '' ? undefined : output;
648650
}
649651

650-
export function getOutputMatchForCommand(executedMarker: IMarker | undefined, endMarker: IMarker | undefined, buffer: IBuffer, cols: number, outputMatcher: ITerminalOutputMatcher): RegExpMatchArray | undefined {
652+
export function getOutputMatchForCommand(executedMarker: IMarker | undefined, endMarker: IMarker | undefined, buffer: IBuffer, cols: number, outputMatcher: ITerminalOutputMatcher): ITerminalOutputMatch | undefined {
651653
if (!executedMarker || !endMarker) {
652654
return undefined;
653655
}
@@ -657,6 +659,7 @@ export function getOutputMatchForCommand(executedMarker: IMarker | undefined, en
657659
const matcher = outputMatcher.lineMatcher;
658660
const linesToCheck = typeof matcher === 'string' ? 1 : outputMatcher.length || countNewLines(matcher);
659661
const lines: string[] = [];
662+
let match: RegExpMatchArray | null | undefined;
660663
if (outputMatcher.anchor === 'bottom') {
661664
for (let i = endLine - (outputMatcher.offset || 0); i >= startLine; i--) {
662665
let wrappedLineStart = i;
@@ -669,9 +672,11 @@ export function getOutputMatchForCommand(executedMarker: IMarker | undefined, en
669672
if (lines.length > linesToCheck) {
670673
lines.pop();
671674
}
672-
const match = lines.join('\n').match(matcher);
673-
if (match) {
674-
return match;
675+
if (!match) {
676+
match = lines.join('\n').match(matcher);
677+
if (!outputMatcher.multipleMatches && match) {
678+
return { regexMatch: match };
679+
}
675680
}
676681
}
677682
} else {
@@ -686,17 +691,62 @@ export function getOutputMatchForCommand(executedMarker: IMarker | undefined, en
686691
if (lines.length === linesToCheck) {
687692
lines.shift();
688693
}
689-
if (outputMatcher) {
690-
const match = lines.join('\n').match(matcher);
691-
if (match) {
692-
return match;
694+
if (!match) {
695+
match = lines.join('\n').match(matcher);
696+
if (!outputMatcher.multipleMatches && match) {
697+
return { regexMatch: match };
693698
}
694699
}
695700
}
696701
}
697-
return undefined;
702+
return match ? { regexMatch: match, outputLines: lines } : undefined;
703+
}
704+
705+
export function getLinesForCommand(buffer: IBuffer, command: ITerminalCommand, cols: number, outputMatcher?: ITerminalOutputMatcher): string[] | undefined {
706+
if (!outputMatcher) {
707+
return undefined;
708+
}
709+
const executedMarker = command.executedMarker;
710+
const endMarker = command.endMarker;
711+
if (!executedMarker || !endMarker) {
712+
throw new Error('No marker for command');
713+
}
714+
const startLine = executedMarker.line;
715+
const endLine = endMarker.line;
716+
717+
const linesToCheck = outputMatcher.length;
718+
const lines: string[] = [];
719+
if (outputMatcher.anchor === 'bottom') {
720+
for (let i = endLine - (outputMatcher.offset || 0); i >= startLine; i--) {
721+
let wrappedLineStart = i;
722+
const wrappedLineEnd = i;
723+
while (wrappedLineStart >= startLine && buffer.getLine(wrappedLineStart)?.isWrapped) {
724+
wrappedLineStart--;
725+
}
726+
i = wrappedLineStart;
727+
lines.unshift(getXtermLineContent(buffer, wrappedLineStart, wrappedLineEnd, cols));
728+
if (lines.length > linesToCheck) {
729+
lines.pop();
730+
}
731+
}
732+
} else {
733+
for (let i = startLine + (outputMatcher.offset || 0); i < endLine; i++) {
734+
const wrappedLineStart = i;
735+
let wrappedLineEnd = i;
736+
while (wrappedLineEnd + 1 < endLine && buffer.getLine(wrappedLineEnd + 1)?.isWrapped) {
737+
wrappedLineEnd++;
738+
}
739+
i = wrappedLineEnd;
740+
lines.push(getXtermLineContent(buffer, wrappedLineStart, wrappedLineEnd, cols));
741+
if (lines.length === linesToCheck) {
742+
lines.shift();
743+
}
744+
}
745+
}
746+
return lines;
698747
}
699748

749+
700750
function getXtermLineContent(buffer: IBuffer, lineStart: number, lineEnd: number, cols: number): string {
701751
// Cap the maximum number of lines generated to prevent potential performance problems. This is
702752
// more of a sanity check as the wrapped line should already be trimmed down at this point.

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

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ 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, ITerminalOutputMatcher } from 'vs/platform/terminal/common/capabilities/capabilities';
10+
import { IPtyHostProcessReplayEvent, ISerializedCommandDetectionCapability, ITerminalCapabilityStore } 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';
14+
import { ITerminalCommandSelector } from 'vs/platform/terminal/common/xterm/terminalQuickFix';
15+
1416

1517
export const enum TerminalSettingPrefix {
1618
Shell = 'terminal.integrated.shell.',
@@ -794,21 +796,6 @@ export interface ITerminalProfileSource extends IBaseUnresolvedTerminalProfile {
794796
source: ProfileSource;
795797
}
796798

797-
798-
export interface ITerminalContributions {
799-
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;
810-
}
811-
812799
export interface ITerminalProfileContribution {
813800
title: string;
814801
id: string;
@@ -820,10 +807,6 @@ export interface IExtensionTerminalProfile extends ITerminalProfileContribution
820807
extensionIdentifier: string;
821808
}
822809

823-
export interface IExtensionTerminalQuickFix extends ITerminalQuickFixContribution {
824-
extensionIdentifier: string;
825-
}
826-
827810
export type ITerminalProfileObject = ITerminalExecutable | ITerminalProfileSource | IExtensionTerminalProfile | null;
828811

829812
export interface IShellIntegration {
@@ -835,6 +818,11 @@ export interface IShellIntegration {
835818
deserialize(serialized: ISerializedCommandDetectionCapability): void;
836819
}
837820

821+
export interface ITerminalContributions {
822+
profiles?: ITerminalProfileContribution[];
823+
quickFixes?: ITerminalCommandSelector[];
824+
}
825+
838826
export const enum ShellIntegrationStatus {
839827
/** No shell integration sequences have been encountered. */
840828
Off,
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
7+
import { IAction } from 'vs/base/common/actions';
8+
import { CancellationToken } from 'vs/base/common/cancellation';
9+
import { UriComponents } from 'vs/base/common/uri';
10+
import { ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities';
11+
12+
export interface ITerminalCommandSelector {
13+
id: string;
14+
commandLineMatcher: string | RegExp;
15+
outputMatcher?: ITerminalOutputMatcher;
16+
exitStatus: boolean;
17+
}
18+
19+
20+
export interface ITerminalQuickFixOptions {
21+
type: 'internal' | 'resolved' | 'unresolved';
22+
id: string;
23+
commandLineMatcher: string | RegExp;
24+
outputMatcher?: ITerminalOutputMatcher;
25+
exitStatus: boolean;
26+
}
27+
28+
export interface ITerminalQuickFix {
29+
type: 'command' | 'opener';
30+
id: string;
31+
source: string;
32+
}
33+
34+
export interface ITerminalQuickFixCommandAction extends ITerminalQuickFix {
35+
type: 'command';
36+
terminalCommand: string;
37+
// TODO: Should this depend on whether alt is held?
38+
addNewLine?: boolean;
39+
}
40+
export interface ITerminalQuickFixOpenerAction extends ITerminalQuickFix {
41+
type: 'opener';
42+
uri: UriComponents;
43+
}
44+
45+
export interface ITerminalCommandSelector {
46+
commandLineMatcher: string | RegExp;
47+
outputMatcher?: ITerminalOutputMatcher;
48+
exitStatus: boolean;
49+
}
50+
51+
export type TerminalQuickFixActionInternal = IAction | ITerminalQuickFixCommandAction | ITerminalQuickFixOpenerAction;
52+
export type TerminalQuickFixCallback = (matchResult: ITerminalCommandMatchResult) => TerminalQuickFixActionInternal[] | TerminalQuickFixActionInternal | undefined;
53+
export type TerminalQuickFixCallbackExtension = (terminalCommand: ITerminalCommand, lines: string[] | undefined, option: ITerminalQuickFixOptions, token: CancellationToken) => Promise<ITerminalQuickFix[] | ITerminalQuickFix | undefined>;
54+
55+
export interface ITerminalQuickFixProvider {
56+
/**
57+
* Provides terminal quick fixes
58+
* @param commandMatchResult The command match result for which to provide quick fixes
59+
* @param token A cancellation token indicating the result is no longer needed
60+
* @return Terminal quick fix(es) if any
61+
*/
62+
provideTerminalQuickFixes(terminalCommand: ITerminalCommand, lines: string[] | undefined, option: ITerminalQuickFixOptions, token: CancellationToken): Promise<ITerminalQuickFix[] | ITerminalQuickFix | undefined>;
63+
}
64+
export interface ITerminalCommandMatchResult {
65+
commandLine: string;
66+
commandLineMatch: RegExpMatchArray;
67+
outputMatch?: ITerminalOutputMatch;
68+
}
69+
70+
export interface ITerminalOutputMatch {
71+
regexMatch: RegExpMatchArray;
72+
outputLines?: string[];
73+
}
74+
75+
export interface IInternalOptions extends ITerminalQuickFixOptions {
76+
type: 'internal';
77+
getQuickFixes: TerminalQuickFixCallback;
78+
}
79+
80+
export interface IResolvedExtensionOptions extends ITerminalQuickFixOptions {
81+
type: 'resolved';
82+
getQuickFixes: TerminalQuickFixCallbackExtension;
83+
}
84+
85+
export interface IUnresolvedExtensionOptions extends ITerminalQuickFixOptions {
86+
type: 'unresolved';
87+
}
88+
89+
90+
/**
91+
* A matcher that runs on a sub-section of a terminal command's output
92+
*/
93+
export interface ITerminalOutputMatcher {
94+
/**
95+
* A string or regex to match against the unwrapped line. If this is a regex with the multiline
96+
* flag, it will scan an amount of lines equal to `\n` instances in the regex + 1.
97+
*/
98+
lineMatcher: string | RegExp;
99+
/**
100+
* Which side of the output to anchor the {@link offset} and {@link length} against.
101+
*/
102+
anchor: 'top' | 'bottom';
103+
/**
104+
* The number of rows above or below the {@link anchor} to start matching against.
105+
*/
106+
offset: number;
107+
/**
108+
* The number of rows to match against, this should be as small as possible for performance
109+
* reasons. This is capped at 40.
110+
*/
111+
length: number;
112+
113+
/**
114+
* If multiple matches are expected - this will result in {@link outputLines} being returned
115+
* when there's a {@link regexMatch} from {@link offset} to {@link length}
116+
*/
117+
multipleMatches?: boolean;
118+
}

0 commit comments

Comments
 (0)