Skip to content

Commit 95e8eca

Browse files
authored
add find count to the terminal (microsoft#146358)
1 parent cbc3164 commit 95e8eca

File tree

9 files changed

+105
-19
lines changed

9 files changed

+105
-19
lines changed

src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBo
1515
import { SimpleButton, findPreviousMatchIcon, findNextMatchIcon } from 'vs/editor/contrib/find/browser/findWidget';
1616
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
1717
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
18-
import { editorWidgetBackground, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, widgetShadow, editorWidgetForeground } from 'vs/platform/theme/common/colorRegistry';
18+
import { editorWidgetBackground, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, widgetShadow, editorWidgetForeground, errorForeground } from 'vs/platform/theme/common/colorRegistry';
1919
import { IColorTheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
2020
import { ContextScopedFindInput } from 'vs/platform/history/browser/contextScopedHistoryWidget';
2121
import { widgetClose } from 'vs/platform/theme/common/iconRegistry';
@@ -26,6 +26,12 @@ const NLS_PREVIOUS_MATCH_BTN_LABEL = nls.localize('label.previousMatchButton', "
2626
const NLS_NEXT_MATCH_BTN_LABEL = nls.localize('label.nextMatchButton', "Next Match");
2727
const NLS_CLOSE_BTN_LABEL = nls.localize('label.closeButton', "Close");
2828

29+
interface IFindOptions {
30+
showOptionButtons?: boolean;
31+
checkImeCompletionState?: boolean;
32+
showResultCount?: boolean;
33+
}
34+
2935
export abstract class SimpleFindWidget extends Widget {
3036
private readonly _findInput: FindInput;
3137
private readonly _domNode: HTMLElement;
@@ -35,16 +41,16 @@ export abstract class SimpleFindWidget extends Widget {
3541
private readonly _updateHistoryDelayer: Delayer<void>;
3642
private readonly prevBtn: SimpleButton;
3743
private readonly nextBtn: SimpleButton;
44+
private _matchesCount: HTMLElement | undefined;
3845

3946
private _isVisible: boolean = false;
40-
private foundMatch: boolean = false;
47+
private _foundMatch: boolean = false;
4148

4249
constructor(
4350
@IContextViewService private readonly _contextViewService: IContextViewService,
4451
@IContextKeyService contextKeyService: IContextKeyService,
4552
private readonly _state: FindReplaceState = new FindReplaceState(),
46-
showOptionButtons?: boolean,
47-
checkImeCompletionState?: boolean
53+
private readonly _options: IFindOptions
4854
) {
4955
super();
5056

@@ -59,20 +65,23 @@ export abstract class SimpleFindWidget extends Widget {
5965
new RegExp(value);
6066
return null;
6167
} catch (e) {
62-
this.foundMatch = false;
63-
this.updateButtons(this.foundMatch);
68+
this._foundMatch = false;
69+
this.updateButtons(this._foundMatch);
6470
return { content: e.message };
6571
}
6672
}
67-
}, contextKeyService, showOptionButtons));
73+
}, contextKeyService, _options.showOptionButtons));
6874

6975
// Find History with update delayer
7076
this._updateHistoryDelayer = new Delayer<void>(500);
7177

72-
this._register(this._findInput.onInput((e) => {
73-
if (!checkImeCompletionState || !this._findInput.isImeSessionInProgress) {
74-
this.foundMatch = this._onInputChanged();
75-
this.updateButtons(this.foundMatch);
78+
this._register(this._findInput.onInput(async (e) => {
79+
if (!_options.checkImeCompletionState || !this._findInput.isImeSessionInProgress) {
80+
this._foundMatch = this._onInputChanged();
81+
if (this._options.showResultCount) {
82+
await this.updateResultCount();
83+
}
84+
this.updateButtons(this._foundMatch);
7685
this.focusFindBox();
7786
this._delayedUpdateHistory();
7887
}
@@ -152,6 +161,14 @@ export abstract class SimpleFindWidget extends Widget {
152161
this._register(dom.addDisposableListener(this._innerDomNode, 'click', (event) => {
153162
event.stopPropagation();
154163
}));
164+
165+
if (_options?.showResultCount) {
166+
this._domNode.classList.add('result-count');
167+
this._register(this._findInput.onDidChange(() => {
168+
this.updateResultCount();
169+
this.updateButtons(this._foundMatch);
170+
}));
171+
}
155172
}
156173

157174
protected abstract _onInputChanged(): boolean;
@@ -161,6 +178,7 @@ export abstract class SimpleFindWidget extends Widget {
161178
protected abstract _onFocusTrackerBlur(): void;
162179
protected abstract _onFindInputFocusTrackerFocus(): void;
163180
protected abstract _onFindInputFocusTrackerBlur(): void;
181+
protected abstract _getResultCount(): Promise<{ resultIndex: number; resultCount: number } | undefined>;
164182

165183
protected get inputValue() {
166184
return this._findInput.getValue();
@@ -214,7 +232,7 @@ export abstract class SimpleFindWidget extends Widget {
214232
}
215233

216234
this._isVisible = true;
217-
this.updateButtons(this.foundMatch);
235+
this.updateButtons(this._foundMatch);
218236

219237
setTimeout(() => {
220238
this._innerDomNode.classList.add('visible', 'visible-transition');
@@ -243,7 +261,7 @@ export abstract class SimpleFindWidget extends Widget {
243261
// Need to delay toggling visibility until after Transition, then visibility hidden - removes from tabIndex list
244262
setTimeout(() => {
245263
this._isVisible = false;
246-
this.updateButtons(this.foundMatch);
264+
this.updateButtons(this._foundMatch);
247265
this._innerDomNode.classList.remove('visible');
248266
}, 200);
249267
}
@@ -281,6 +299,20 @@ export abstract class SimpleFindWidget extends Widget {
281299
this.nextBtn.focus();
282300
this._findInput.inputBox.focus();
283301
}
302+
303+
async updateResultCount(): Promise<void> {
304+
const count = await this._getResultCount();
305+
if (!this._matchesCount) {
306+
this._matchesCount = document.createElement('div');
307+
this._matchesCount.className = 'matchesCount';
308+
}
309+
this._matchesCount.innerText = '';
310+
const label = count === undefined || count.resultCount === 0 ? `No Results` : `${count.resultIndex + 1} of ${count.resultCount}`;
311+
this._matchesCount.appendChild(document.createTextNode(label));
312+
this._matchesCount.classList.toggle('no-results', !count || count.resultCount === 0);
313+
this._findInput?.domNode.insertAdjacentElement('afterend', this._matchesCount);
314+
this._foundMatch = !!count && count.resultCount > 0;
315+
}
284316
}
285317

286318
// theming
@@ -299,4 +331,9 @@ registerThemingParticipant((theme, collector) => {
299331
if (widgetShadowColor) {
300332
collector.addRule(`.monaco-workbench .simple-find-part { box-shadow: 0 0 8px 2px ${widgetShadowColor}; }`);
301333
}
334+
335+
const error = theme.getColor(errorForeground);
336+
if (error) {
337+
collector.addRule(`.no-results.matchesCount { color: ${error}; }`);
338+
}
302339
});

src/vs/workbench/contrib/terminal/browser/media/terminal.css

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,26 @@
8181
z-index: 31;
8282
}
8383

84-
.monaco-workbench .simple-find-part-wrapper {
84+
.xterm .xterm-screen {
85+
cursor: text;
86+
}
87+
88+
.monaco-workbench .simple-find-part-wrapper.result-count {
8589
z-index: 33 !important;
90+
padding-right: 80px;
8691
}
8792

88-
.xterm .xterm-screen {
89-
cursor: text;
93+
.result-count .simple-find-part {
94+
width: 280px;
95+
max-width: 280px;
96+
min-width: 280px;
97+
}
98+
99+
.result-count .matchesCount {
100+
width: 68px;
101+
max-width: 68px;
102+
min-width: 68px;
103+
padding-left: 5px;
90104
}
91105

92106
.xterm .xterm-accessibility {

src/vs/workbench/contrib/terminal/browser/terminal.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,8 @@ export interface ITerminalInstance {
554554
*/
555555
onExit: Event<number | ITerminalLaunchError | undefined>;
556556

557+
onDidChangeFindResults: Event<{ resultIndex: number; resultCount: number } | undefined>;
558+
557559
readonly exitCode: number | undefined;
558560

559561
readonly areLinksReady: boolean;
@@ -864,6 +866,8 @@ export interface IXtermTerminal {
864866
*/
865867
target?: TerminalLocation;
866868

869+
findResult?: { resultIndex: number; resultCount: number };
870+
867871
/**
868872
* Find the next instance of the term
869873
*/

src/vs/workbench/contrib/terminal/browser/terminalEditor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export class TerminalEditor extends EditorPane {
9191
// panel and the editors, this is needed so that the active instance gets set
9292
// when focus changes between them.
9393
this._register(this._editorInput.terminalInstance.onDidFocus(() => this._setActiveInstance()));
94+
this._register(this._editorInput.terminalInstance.onDidChangeFindResults(() => this._findWidget.updateResultCount()));
9495
this._editorInput.setCopyLaunchConfig(this._editorInput.terminalInstance.shellLaunchConfig);
9596
}
9697
}

src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ export class TerminalFindWidget extends SimpleFindWidget {
2323
@ITerminalService private readonly _terminalService: ITerminalService,
2424
@ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService
2525
) {
26-
super(_contextViewService, _contextKeyService, findState, true);
26+
super(_contextViewService, _contextKeyService, findState, { showOptionButtons: true, showResultCount: true });
27+
2728
this._register(findState.onFindReplaceStateChange(() => {
2829
this.show();
2930
}));
@@ -82,6 +83,14 @@ export class TerminalFindWidget extends SimpleFindWidget {
8283
}
8384
}
8485

86+
protected async _getResultCount(): Promise<{ resultIndex: number; resultCount: number } | undefined> {
87+
const instance = this._terminalService.activeInstance;
88+
if (instance) {
89+
return instance.xterm?.findResult;
90+
}
91+
return undefined;
92+
}
93+
8594
protected _onInputChanged() {
8695
// Ignore input changes for now
8796
const instance = this._terminalService.activeInstance;

src/vs/workbench/contrib/terminal/browser/terminalInstance.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
329329
readonly onRequestAddInstanceToGroup = this._onRequestAddInstanceToGroup.event;
330330
private readonly _onDidChangeHasChildProcesses = this._register(new Emitter<boolean>());
331331
readonly onDidChangeHasChildProcesses = this._onDidChangeHasChildProcesses.event;
332+
private readonly _onDidChangeFindResults = new Emitter<{ resultIndex: number; resultCount: number } | undefined>();
333+
readonly onDidChangeFindResults = this._onDidChangeFindResults.event;
332334

333335
constructor(
334336
private readonly _terminalFocusContextKey: IContextKey<boolean>,
@@ -975,6 +977,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
975977

976978
const screenElement = xterm.attachToElement(xtermElement);
977979

980+
xterm.onDidChangeFindResults((results) => this._onDidChangeFindResults.fire(results));
981+
978982
if (!xterm.raw.element || !xterm.raw.textarea) {
979983
throw new Error('xterm elements not set after open');
980984
}

src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,11 @@ export class TerminalTabbedView extends Disposable {
150150
});
151151

152152
this._splitView = new SplitView(parentElement, { orientation: Orientation.HORIZONTAL, proportionalLayout: false });
153-
153+
this._terminalService.onDidCreateInstance(instance => {
154+
instance.onDidChangeFindResults(() => {
155+
this._findWidget.updateResultCount();
156+
});
157+
});
154158
this._setupSplitView(terminalOuterContainer);
155159
}
156160

src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,15 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal {
6868
private _webglAddon?: WebglAddonType;
6969
private _serializeAddon?: SerializeAddonType;
7070

71+
private _lastFindResult: { resultIndex: number; resultCount: number } | undefined;
72+
get findResult(): { resultIndex: number; resultCount: number } | undefined { return this._lastFindResult; }
73+
7174
private readonly _onDidRequestRunCommand = new Emitter<string>();
7275
readonly onDidRequestRunCommand = this._onDidRequestRunCommand.event;
7376

77+
private readonly _onDidChangeFindResults = new Emitter<{ resultIndex: number; resultCount: number } | undefined>();
78+
readonly onDidChangeFindResults = this._onDidChangeFindResults.event;
79+
7480
get commandTracker(): ICommandTracker { return this._commandNavigationAddon; }
7581
get shellIntegration(): IShellIntegration { return this._shellIntegrationAddon; }
7682

@@ -292,6 +298,10 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal {
292298
const AddonCtor = await this._getSearchAddonConstructor();
293299
this._searchAddon = new AddonCtor();
294300
this.raw.loadAddon(this._searchAddon);
301+
this._searchAddon.onDidChangeResults((results: { resultIndex: number; resultCount: number } | undefined) => {
302+
this._lastFindResult = results;
303+
this._onDidChangeFindResults.fire(results);
304+
});
295305
return this._searchAddon;
296306
}
297307

src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export interface WebviewFindDelegate {
2020
}
2121

2222
export class WebviewFindWidget extends SimpleFindWidget {
23+
protected async _getResultCount(dataChanged?: boolean): Promise<{ resultIndex: number; resultCount: number } | undefined> {
24+
return undefined;
25+
}
2326

2427
protected readonly _findWidgetFocused: IContextKey<boolean>;
2528

@@ -28,7 +31,7 @@ export class WebviewFindWidget extends SimpleFindWidget {
2831
@IContextViewService contextViewService: IContextViewService,
2932
@IContextKeyService contextKeyService: IContextKeyService
3033
) {
31-
super(contextViewService, contextKeyService, undefined, false, _delegate.checkImeCompletionState);
34+
super(contextViewService, contextKeyService, undefined, { showOptionButtons: false, checkImeCompletionState: _delegate.checkImeCompletionState });
3235
this._findWidgetFocused = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED.bindTo(contextKeyService);
3336

3437
this._register(_delegate.hasFindResult(hasResult => {

0 commit comments

Comments
 (0)