Skip to content

Commit 189ec9f

Browse files
authored
Merge pull request microsoft#154305 from microsoft/tyriar/154016
Match run recent command behavior to reverse-i-search
2 parents e194e88 + 986d2e5 commit 189ec9f

File tree

4 files changed

+114
-33
lines changed

4 files changed

+114
-33
lines changed

src/vs/base/parts/quickinput/browser/quickInput.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
450450
private _matchOnDescription = false;
451451
private _matchOnDetail = false;
452452
private _matchOnLabel = true;
453+
private _matchOnLabelMode: 'fuzzy' | 'contiguous' = 'fuzzy';
453454
private _sortByLabel = true;
454455
private _autoFocusOnList = true;
455456
private _keepScrollPosition = false;
@@ -595,6 +596,15 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
595596
this.update();
596597
}
597598

599+
get matchOnLabelMode() {
600+
return this._matchOnLabelMode;
601+
}
602+
603+
set matchOnLabelMode(matchOnLabelMode: 'fuzzy' | 'contiguous') {
604+
this._matchOnLabelMode = matchOnLabelMode;
605+
this.update();
606+
}
607+
598608
get sortByLabel() {
599609
return this._sortByLabel;
600610
}
@@ -994,6 +1004,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
9941004
this.ui.list.matchOnDescription = this.matchOnDescription;
9951005
this.ui.list.matchOnDetail = this.matchOnDetail;
9961006
this.ui.list.matchOnLabel = this.matchOnLabel;
1007+
this.ui.list.matchOnLabelMode = this.matchOnLabelMode;
9971008
this.ui.list.sortByLabel = this.sortByLabel;
9981009
if (this.itemsUpdated) {
9991010
this.itemsUpdated = false;

src/vs/base/parts/quickinput/browser/quickInputList.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ import { compareAnything } from 'vs/base/common/comparers';
1717
import { memoize } from 'vs/base/common/decorators';
1818
import { Emitter, Event } from 'vs/base/common/event';
1919
import { IMatch } from 'vs/base/common/filters';
20-
import { matchesFuzzyIconAware, parseLabelWithIcons } from 'vs/base/common/iconLabels';
20+
import { IParsedLabelWithIcons, matchesFuzzyIconAware, parseLabelWithIcons } from 'vs/base/common/iconLabels';
2121
import { KeyCode } from 'vs/base/common/keyCodes';
2222
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
2323
import * as platform from 'vs/base/common/platform';
24+
import { ltrim } from 'vs/base/common/strings';
2425
import { withNullAsUndefined } from 'vs/base/common/types';
2526
import { IQuickInputOptions } from 'vs/base/parts/quickinput/browser/quickInput';
2627
import { getIconClass } from 'vs/base/parts/quickinput/browser/quickInputUtils';
@@ -258,6 +259,7 @@ export class QuickInputList {
258259
matchOnDescription = false;
259260
matchOnDetail = false;
260261
matchOnLabel = true;
262+
matchOnLabelMode: 'fuzzy' | 'contiguous' = 'fuzzy';
261263
matchOnMeta = true;
262264
sortByLabel = true;
263265
private readonly _onChangedAllVisibleChecked = new Emitter<boolean>();
@@ -628,7 +630,12 @@ export class QuickInputList {
628630
else {
629631
let currentSeparator: IQuickPickSeparator | undefined;
630632
this.elements.forEach(element => {
631-
const labelHighlights = this.matchOnLabel ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneLabel))) : undefined;
633+
let labelHighlights: IMatch[] | undefined;
634+
if (this.matchOnLabelMode === 'fuzzy') {
635+
labelHighlights = this.matchOnLabel ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneLabel))) : undefined;
636+
} else {
637+
labelHighlights = this.matchOnLabel ? withNullAsUndefined(matchesContiguousIconAware(query, parseLabelWithIcons(element.saneLabel))) : undefined;
638+
}
632639
const descriptionHighlights = this.matchOnDescription ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneDescription || ''))) : undefined;
633640
const detailHighlights = this.matchOnDetail ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneDetail || ''))) : undefined;
634641
const metaHighlights = this.matchOnMeta ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneMeta || ''))) : undefined;
@@ -726,6 +733,43 @@ export class QuickInputList {
726733
}
727734
}
728735

736+
export function matchesContiguousIconAware(query: string, target: IParsedLabelWithIcons, enableSeparateSubstringMatching = false): IMatch[] | null {
737+
738+
const { text, iconOffsets } = target;
739+
740+
// Return early if there are no icon markers in the word to match against
741+
if (!iconOffsets || iconOffsets.length === 0) {
742+
return matchesContiguous(query, text, enableSeparateSubstringMatching);
743+
}
744+
745+
// Trim the word to match against because it could have leading
746+
// whitespace now if the word started with an icon
747+
const wordToMatchAgainstWithoutIconsTrimmed = ltrim(text, ' ');
748+
const leadingWhitespaceOffset = text.length - wordToMatchAgainstWithoutIconsTrimmed.length;
749+
750+
// match on value without icon
751+
const matches = matchesContiguous(query, wordToMatchAgainstWithoutIconsTrimmed, enableSeparateSubstringMatching);
752+
753+
// Map matches back to offsets with icon and trimming
754+
if (matches) {
755+
for (const match of matches) {
756+
const iconOffset = iconOffsets[match.start + leadingWhitespaceOffset] /* icon offsets at index */ + leadingWhitespaceOffset /* overall leading whitespace offset */;
757+
match.start += iconOffset;
758+
match.end += iconOffset;
759+
}
760+
}
761+
762+
return matches;
763+
}
764+
765+
function matchesContiguous(word: string, wordToMatchAgainst: string, enableSeparateSubstringMatching = false): IMatch[] | null {
766+
const matchIndex = wordToMatchAgainst.toLowerCase().indexOf(word.toLowerCase());
767+
if (matchIndex !== -1) {
768+
return [{ start: matchIndex, end: matchIndex + word.length }];
769+
}
770+
return null;
771+
}
772+
729773
function compareEntries(elementA: ListElement, elementB: ListElement, lookFor: string): number {
730774

731775
const labelHighlightsA = elementA.labelHighlights || [];

src/vs/base/parts/quickinput/common/quickInput.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,13 @@ export interface IQuickPick<T extends IQuickPickItem> extends IQuickInput {
292292

293293
matchOnLabel: boolean;
294294

295+
/**
296+
* The mode to filter label with. Fuzzy will use fuzzy searching and
297+
* contiguous will make filter entries that do not contain the exact string
298+
* (including whitespace). This defaults to `'fuzzy'`.
299+
*/
300+
matchOnLabelMode: 'fuzzy' | 'contiguous';
301+
295302
sortByLabel: boolean;
296303

297304
autoFocusOnList: boolean;

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

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -821,7 +821,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
821821
this._linkManager.openRecentLink(type);
822822
}
823823

824-
async runRecent(type: 'command' | 'cwd'): Promise<void> {
824+
async runRecent(type: 'command' | 'cwd', filterMode?: 'fuzzy' | 'contiguous', value?: string): Promise<void> {
825825
if (!this.xterm) {
826826
return;
827827
}
@@ -968,42 +968,61 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
968968
}
969969
const outputProvider = this._instantiationService.createInstance(TerminalOutputProvider);
970970
const quickPick = this._quickInputService.createQuickPick();
971-
quickPick.items = items;
971+
const originalItems = items;
972+
quickPick.items = [...originalItems];
972973
quickPick.sortByLabel = false;
973974
quickPick.placeholder = placeholder;
974-
return new Promise<void>(r => {
975-
quickPick.onDidTriggerItemButton(async e => {
976-
if (e.button === removeFromCommandHistoryButton) {
977-
if (type === 'command') {
978-
this._instantiationService.invokeFunction(getCommandHistory)?.remove(e.item.label);
979-
} else {
980-
this._instantiationService.invokeFunction(getDirectoryHistory)?.remove(e.item.label);
981-
}
982-
} else {
983-
const selectedCommand = (e.item as Item).command;
984-
const output = selectedCommand?.getOutput();
985-
if (output && selectedCommand?.command) {
986-
const textContent = await outputProvider.provideTextContent(URI.from(
987-
{
988-
scheme: TerminalOutputProvider.scheme,
989-
path: `${selectedCommand.command}... ${fromNow(selectedCommand.timestamp, true)}`,
990-
fragment: output,
991-
query: `terminal-output-${selectedCommand.timestamp}-${this.instanceId}`
992-
}));
993-
if (textContent) {
994-
await this._editorService.openEditor({
995-
resource: textContent.uri
996-
});
997-
}
998-
}
999-
}
975+
quickPick.customButton = true;
976+
quickPick.matchOnLabelMode = filterMode || 'contiguous';
977+
if (filterMode === 'fuzzy') {
978+
quickPick.customLabel = nls.localize('terminal.contiguousSearch', 'Use Contiguous Search');
979+
quickPick.onDidCustom(() => {
1000980
quickPick.hide();
981+
this.runRecent(type, 'contiguous', quickPick.value);
1001982
});
1002-
quickPick.onDidAccept(() => {
1003-
const result = quickPick.activeItems[0];
1004-
this.sendText(type === 'cwd' ? `cd ${result.label}` : result.label, !quickPick.keyMods.alt);
983+
} else {
984+
quickPick.customLabel = nls.localize('terminal.fuzzySearch', 'Use Fuzzy Search');
985+
quickPick.onDidCustom(() => {
1005986
quickPick.hide();
987+
this.runRecent(type, 'fuzzy', quickPick.value);
1006988
});
989+
}
990+
quickPick.onDidTriggerItemButton(async e => {
991+
if (e.button === removeFromCommandHistoryButton) {
992+
if (type === 'command') {
993+
this._instantiationService.invokeFunction(getCommandHistory)?.remove(e.item.label);
994+
} else {
995+
this._instantiationService.invokeFunction(getDirectoryHistory)?.remove(e.item.label);
996+
}
997+
} else {
998+
const selectedCommand = (e.item as Item).command;
999+
const output = selectedCommand?.getOutput();
1000+
if (output && selectedCommand?.command) {
1001+
const textContent = await outputProvider.provideTextContent(URI.from(
1002+
{
1003+
scheme: TerminalOutputProvider.scheme,
1004+
path: `${selectedCommand.command}... ${fromNow(selectedCommand.timestamp, true)}`,
1005+
fragment: output,
1006+
query: `terminal-output-${selectedCommand.timestamp}-${this.instanceId}`
1007+
}));
1008+
if (textContent) {
1009+
await this._editorService.openEditor({
1010+
resource: textContent.uri
1011+
});
1012+
}
1013+
}
1014+
}
1015+
quickPick.hide();
1016+
});
1017+
quickPick.onDidAccept(() => {
1018+
const result = quickPick.activeItems[0];
1019+
this.sendText(type === 'cwd' ? `cd ${result.label}` : result.label, !quickPick.keyMods.alt);
1020+
quickPick.hide();
1021+
});
1022+
if (value) {
1023+
quickPick.value = value;
1024+
}
1025+
return new Promise<void>(r => {
10071026
quickPick.show();
10081027
quickPick.onDidHide(() => r());
10091028
});

0 commit comments

Comments
 (0)