Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support contributable find toggles in async trees #229148

Merged
merged 1 commit into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 113 additions & 69 deletions src/vs/base/browser/ui/tree/abstractTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -581,8 +581,6 @@ export type LabelFuzzyScore = { label: string; score: FuzzyScore };
export interface IFindFilter<T> extends ITreeFilter<T, FuzzyScore | LabelFuzzyScore> {
filter(element: T, parentVisibility: TreeVisibility): TreeFilterResult<FuzzyScore | LabelFuzzyScore>;
pattern: string;
totalCount: number;
matchCount: number;
}

class FindFilter<T> implements IFindFilter<T>, IDisposable {
Expand Down Expand Up @@ -686,7 +684,7 @@ class FindFilter<T> implements IFindFilter<T>, IDisposable {
}
}

interface ITreeFindToggleContribution {
export interface ITreeFindToggleContribution {
id: string;
title: string;
icon: ThemeIcon;
Expand All @@ -712,7 +710,39 @@ class TreeFindToggle extends Toggle {
}
}

interface ITreeFindToggleChangeEvent {
export class FindToggles {
private stateMap: Map<string, ITreeFindToggleContribution>;

constructor(startStates: ITreeFindToggleContribution[]) {
this.stateMap = new Map(startStates.map(state => [state.id, { ...state }]));
}

states(): ITreeFindToggleContribution[] {
return Array.from(this.stateMap.values());
}

get(id: string): boolean {
const state = this.stateMap.get(id);
if (state === undefined) {
throw new Error(`No state found for toggle id ${id}`);
}
return state.isChecked;
}

set(id: string, value: boolean): boolean {
const state = this.stateMap.get(id);
if (state === undefined) {
throw new Error(`No state found for toggle id ${id}`);
}
if (state.isChecked === value) {
return false;
}
state.isChecked = value;
return true;
}
}

export interface ITreeFindToggleChangeEvent {
readonly id: string;
readonly isChecked: boolean;
}
Expand Down Expand Up @@ -802,13 +832,13 @@ class FindWidget<T, TFilterData> extends Disposable {
}

const toggleHoverDelegate = this._register(createInstantHoverDelegate());
const toggles = toggleContributions.map(contribution => this._register(new TreeFindToggle(contribution, styles.toggleStyles, toggleHoverDelegate)));
this.onDidToggleChange = Event.any(...toggles.map(toggle => Event.map(toggle.onChange, () => ({ id: toggle.id, isChecked: toggle.checked }))));
this.toggles = toggleContributions.map(contribution => this._register(new TreeFindToggle(contribution, styles.toggleStyles, toggleHoverDelegate)));
this.onDidToggleChange = Event.any(...this.toggles.map(toggle => Event.map(toggle.onChange, () => ({ id: toggle.id, isChecked: toggle.checked }))));

this.findInput = this._register(new FindInput(this.elements.findInput, contextViewProvider, {
label: localize('type to search', "Type to search"),
placeholder,
additionalToggles: toggles,
additionalToggles: this.toggles,
showCommonFindToggles: false,
inputBoxStyles: styles.inputBoxStyles,
toggleStyles: styles.toggleStyles,
Expand Down Expand Up @@ -988,6 +1018,8 @@ enum DefaultTreeToggles {

interface IAbstractFindControllerOptions extends IFindWidgetOptions {
placeholder?: string;
toggles?: ITreeFindToggleContribution[];
showNotFoundMessage?: boolean;
}

interface IFindControllerOptions extends IAbstractFindControllerOptions {
Expand All @@ -1003,7 +1035,7 @@ export abstract class AbstractFindController<T, TFilterData> implements IDisposa
get pattern(): string { return this._pattern; }
private previousPattern = '';

protected abstract toggles: ITreeFindToggleContribution[];
protected readonly toggles: FindToggles;

private _placeholder: string;
protected get placeholder(): string { return this._placeholder; }
Expand All @@ -1022,14 +1054,15 @@ export abstract class AbstractFindController<T, TFilterData> implements IDisposa
readonly onDidChangeOpenState = this._onDidChangeOpenState.event;

private readonly enabledDisposables = new DisposableStore();
private readonly disposables = new DisposableStore();
protected readonly disposables = new DisposableStore();

constructor(
protected tree: AbstractTree<T, TFilterData, any>,
protected filter: IFindFilter<T>,
protected readonly contextViewProvider: IContextViewProvider,
protected readonly options: IAbstractFindControllerOptions = {}
) {
this.toggles = new FindToggles(options.toggles ?? []);
this._placeholder = options.placeholder ?? localize('type to search', "Type to search");
}

Expand All @@ -1044,7 +1077,7 @@ export abstract class AbstractFindController<T, TFilterData> implements IDisposa
return;
}

this.widget = new FindWidget(this.tree.getHTMLElement(), this.tree, this.contextViewProvider, this.placeholder, this.toggles, { ...this.options, history: this._history });
this.widget = new FindWidget(this.tree.getHTMLElement(), this.tree, this.contextViewProvider, this.placeholder, this.toggles.states(), { ...this.options, history: this._history });
this.enabledDisposables.add(this.widget);

this.widget.onDidChangeValue(this.onDidChangeValue, this, this.enabledDisposables);
Expand Down Expand Up @@ -1088,28 +1121,31 @@ export abstract class AbstractFindController<T, TFilterData> implements IDisposa
protected abstract applyPattern(pattern: string): void;

protected onDidToggleChange(e: ITreeFindToggleChangeEvent): void {
// Subclasses can override
this.toggles.set(e.id, e.isChecked);
}

protected setToggleState(id: string, checked: boolean): void {
protected updateToggleState(id: string, checked: boolean): void {
this.toggles.set(id, checked);
this.widget?.setToggleState(id, checked);
}

render(): void {
const noMatches = this.filter.matchCount === 0 && this.filter.totalCount > 0;

if (this.pattern && noMatches) {
alert(localize('replFindNoResults', "No results"));
protected renderMessage(showNotFound: boolean): void {
if (showNotFound) {
if (this.tree.options.showNotFoundMessage ?? true) {
this.widget?.showMessage({ type: MessageType.WARNING, content: localize('not found', "No elements found.") });
} else {
this.widget?.showMessage({ type: MessageType.WARNING });
}
} else {
this.widget?.clearMessage();
if (this.pattern) {
alert(localize('replFindResults', "{0} results", this.filter.matchCount));
}
}
}

protected alertResults(results: number): void {
if (!results) {
alert(localize('replFindNoResults', "No results"));
} else {
alert(localize('foundResults', "{0} results", results));
}
}

Expand All @@ -1128,47 +1164,28 @@ export abstract class AbstractFindController<T, TFilterData> implements IDisposa

class FindController<T, TFilterData> extends AbstractFindController<T, TFilterData> {

protected get toggles(): ITreeFindToggleContribution[] {
return [{
id: DefaultTreeToggles.Mode,
icon: Codicon.listFilter,
title: localize('filter', "Filter"),
isChecked: this.mode === TreeFindMode.Filter,
}, {
id: DefaultTreeToggles.MatchType,
icon: Codicon.searchFuzzy,
title: localize('fuzzySearch', "Fuzzy Match"),
isChecked: this.matchType === TreeFindMatchType.Fuzzy,
}];
}

private _mode: TreeFindMode;
get mode(): TreeFindMode { return this._mode; }
get mode(): TreeFindMode { return this.toggles.get(DefaultTreeToggles.Mode) ? TreeFindMode.Filter : TreeFindMode.Highlight; }
set mode(mode: TreeFindMode) {
if (mode === this._mode) {
if (mode === this.mode) {
return;
}

this._mode = mode;

this.setToggleState(DefaultTreeToggles.Mode, mode === TreeFindMode.Filter);
this.placeholder = this.mode === TreeFindMode.Filter ? localize('type to filter', "Type to filter") : localize('type to search', "Type to search");
const isFilterMode = mode === TreeFindMode.Filter;
this.updateToggleState(DefaultTreeToggles.Mode, isFilterMode);
this.placeholder = isFilterMode ? localize('type to filter', "Type to filter") : localize('type to search', "Type to search");

this.tree.refilter();
this.render();
this._onDidChangeMode.fire(mode);
}

private _matchType: TreeFindMatchType;
get matchType(): TreeFindMatchType { return this._matchType; }
get matchType(): TreeFindMatchType { return this.toggles.get(DefaultTreeToggles.MatchType) ? TreeFindMatchType.Fuzzy : TreeFindMatchType.Contiguous; }
set matchType(matchType: TreeFindMatchType) {
if (matchType === this._matchType) {
if (matchType === this.matchType) {
return;
}

this._matchType = matchType;

this.setToggleState(DefaultTreeToggles.MatchType, matchType === TreeFindMatchType.Fuzzy);
this.updateToggleState(DefaultTreeToggles.MatchType, matchType === TreeFindMatchType.Fuzzy);

this.tree.refilter();
this.render();
Expand All @@ -1183,13 +1200,38 @@ class FindController<T, TFilterData> extends AbstractFindController<T, TFilterDa

constructor(
tree: AbstractTree<T, TFilterData, any>,
filter: IFindFilter<T>,
protected override filter: FindFilter<T>,
contextViewProvider: IContextViewProvider,
options: IFindControllerOptions = {}
) {
super(tree, filter, contextViewProvider, options);
this._mode = options.defaultFindMode ?? TreeFindMode.Highlight;
this._matchType = options.defaultFindMatchType ?? TreeFindMatchType.Fuzzy;
const defaultFindMode = options.defaultFindMode ?? TreeFindMode.Highlight;
const defaultFindMatchType = options.defaultFindMatchType ?? TreeFindMatchType.Fuzzy;

const toggleContributions: ITreeFindToggleContribution[] = [{
id: DefaultTreeToggles.Mode,
icon: Codicon.listFilter,
title: localize('filter', "Filter"),
isChecked: defaultFindMode === TreeFindMode.Filter,
}, {
id: DefaultTreeToggles.MatchType,
icon: Codicon.searchFuzzy,
title: localize('fuzzySearch', "Fuzzy Match"),
isChecked: defaultFindMatchType === TreeFindMatchType.Fuzzy,
}];

super(tree, filter, contextViewProvider, { ...options, toggles: toggleContributions });

this.disposables.add(this.tree.onDidChangeModel(() => {
if (!this.isOpened()) {
return;
}

if (this.pattern.length !== 0) {
this.tree.refilter();
}

this.render();
}));
}

updateOptions(optionsUpdate: IAbstractTreeOptionsUpdate = {}): void {
Expand Down Expand Up @@ -1222,17 +1264,6 @@ class FindController<T, TFilterData> extends AbstractFindController<T, TFilterDa
this.render();
}

protected override onDidToggleChange(e: ITreeFindToggleChangeEvent): void {
switch (e.id) {
case DefaultTreeToggles.Mode:
this.mode = e.isChecked ? TreeFindMode.Filter : TreeFindMode.Highlight;
break;
case DefaultTreeToggles.MatchType:
this.matchType = e.isChecked ? TreeFindMatchType.Fuzzy : TreeFindMatchType.Contiguous;
break;
}
}

shouldAllowFocus(node: ITreeNode<T, TFilterData>): boolean {
if (!this.isOpened() || !this.pattern) {
return true;
Expand All @@ -1244,6 +1275,25 @@ class FindController<T, TFilterData> extends AbstractFindController<T, TFilterDa

return !FuzzyScore.isDefault(node.filterData as any as FuzzyScore);
}

protected override onDidToggleChange(e: ITreeFindToggleChangeEvent): void {
if (e.id === DefaultTreeToggles.Mode) {
this.mode = e.isChecked ? TreeFindMode.Filter : TreeFindMode.Highlight;
} else if (e.id === DefaultTreeToggles.MatchType) {
this.matchType = e.isChecked ? TreeFindMatchType.Fuzzy : TreeFindMatchType.Contiguous;
}
}

private render(): void {
const noMatches = this.filter.matchCount === 0 && this.filter.totalCount > 0;
const showNotFound = noMatches && this.pattern.length > 0;

this.renderMessage(showNotFound);

if (this.pattern.length) {
this.alertResults(this.filter.matchCount);
}
}
}

export interface StickyScrollNode<T, TFilterData> {
Expand Down Expand Up @@ -2645,19 +2695,13 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
styles: _options.findWidgetStyles,
defaultFindMode: _options.defaultFindMode,
defaultFindMatchType: _options.defaultFindMatchType,
showNotFoundMessage: _options.showNotFoundMessage,
};
this.findController = this.disposables.add(new FindController(this, this.findFilter!, _options.contextViewProvider, findOptions));
this.focusNavigationFilter = node => this.findController!.shouldAllowFocus(node);
this.onDidChangeFindOpenState = this.findController.onDidChangeOpenState;
this.onDidChangeFindMode = this.findController.onDidChangeMode;
this.onDidChangeFindMatchType = this.findController.onDidChangeMatchType;
this.disposables.add(this.onDidSpliceModelRelay.event(() => {
if (!this.findController!.isOpened() || this.findController!.pattern.length === 0) {
return;
}
this.refilter();
this.findController!.render();
}));
} else {
this.onDidChangeFindMode = Event.None;
this.onDidChangeFindMatchType = Event.None;
Expand Down
Loading
Loading