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

Filter button in search box #8540

Merged
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
17 changes: 16 additions & 1 deletion packages/core/src/browser/style/search-box.css
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,28 @@
color: var(--theia-editorWidget-foreground);
}

.theia-search-button.codicon.codicon-filter {
line-height: var(--theia-private-horizontal-tab-height);
color: var(--theia-editorWidget-foreground);
align-self: flex-end;
}

.theia-search-button.codicon-filter:not(.filter-active):before {
content: "\eb85";
}

.theia-search-button.codicon-filter.filter-active:before {
content: "\eb83";
}

.theia-search-button-next:before {
content: "\f107";
}

.theia-search-button-next:hover,
.theia-search-button-previous:hover,
.theia-search-button-close:hover {
.theia-search-button-close:hover,
.theia-search-button.codicon-filter:hover {
cursor: pointer;
background-color: var(--theia-editorHoverWidget-background);
}
Expand Down
56 changes: 55 additions & 1 deletion packages/core/src/browser/tree/search-box.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export interface SearchBoxProps extends SearchBoxDebounceOptions {
*/
readonly showButtons?: boolean;

/**
* If `true`, `Filter` and `Close` buttons will be visible, and clicking the `Filter` button will triggers filter on the search term. Defaults to `false`.
*/
readonly showFilter?: boolean;

}

export namespace SearchBoxProps {
Expand All @@ -54,7 +59,10 @@ export class SearchBox extends BaseWidget {
protected readonly previousEmitter = new Emitter<void>();
protected readonly closeEmitter = new Emitter<void>();
protected readonly textChangeEmitter = new Emitter<string | undefined>();
protected readonly filterToggleEmitter = new Emitter<boolean>();
protected readonly input: HTMLInputElement;
protected readonly filter: HTMLElement | undefined;
protected _isFiltering: boolean = false;

constructor(protected readonly props: SearchBoxProps,
protected readonly debounce: SearchBoxDebounce) {
Expand All @@ -65,13 +73,15 @@ export class SearchBox extends BaseWidget {
this.previousEmitter,
this.closeEmitter,
this.textChangeEmitter,
this.filterToggleEmitter,
this.debounce,
this.debounce.onChanged(data => this.fireTextChange(data))
]);
this.hide();
this.update();
const { input } = this.createContent();
const { input, filter } = this.createContent();
this.input = input;
this.filter = filter;
}

get onPrevious(): Event<void> {
Expand All @@ -90,6 +100,14 @@ export class SearchBox extends BaseWidget {
return this.textChangeEmitter.event;
}

get onFilterToggled(): Event<boolean> {
return this.filterToggleEmitter.event;
}

get isFiltering(): boolean {
return this._isFiltering;
}

get keyCodePredicate(): KeyCode.Predicate {
return this.canHandle.bind(this);
}
Expand All @@ -110,6 +128,23 @@ export class SearchBox extends BaseWidget {
this.textChangeEmitter.fire(input);
}

protected fireFilterToggle(): void {
this.doFireFilterToggle();
}

protected doFireFilterToggle(toggleTo: boolean = !this._isFiltering): void {
if (this.filter) {
if (toggleTo) {
this.filter.classList.add(SearchBox.Styles.FILTER_ON);
} else {
this.filter.classList.remove(SearchBox.Styles.FILTER_ON);
}
this._isFiltering = toggleTo;
this.filterToggleEmitter.fire(toggleTo);
this.update();
}
}

handle(event: KeyboardEvent): void {
event.preventDefault();
const keyCode = KeyCode.createKeyCode(event);
Expand All @@ -132,6 +167,7 @@ export class SearchBox extends BaseWidget {
}

onBeforeHide(): void {
this.doFireFilterToggle(false);
this.debounce.append(undefined);
this.fireClose();
}
Expand Down Expand Up @@ -164,6 +200,7 @@ export class SearchBox extends BaseWidget {
protected createContent(): {
container: HTMLElement,
input: HTMLInputElement,
filter: HTMLElement | undefined,
previous: HTMLElement | undefined,
next: HTMLElement | undefined,
close: HTMLElement | undefined
Expand All @@ -181,6 +218,18 @@ export class SearchBox extends BaseWidget {
);
this.node.appendChild(input);

let filter: HTMLElement | undefined;
if (this.props.showFilter) {
filter = document.createElement('div');
filter.classList.add(
SearchBox.Styles.BUTTON,
...SearchBox.Styles.FILTER,
);
filter.title = 'Enable Filter on Type';
this.node.appendChild(filter);
filter.onclick = this.fireFilterToggle.bind(this);
}

let previous: HTMLElement | undefined;
let next: HTMLElement | undefined;
let close: HTMLElement | undefined;
Expand All @@ -203,7 +252,9 @@ export class SearchBox extends BaseWidget {
next.title = 'Next (Down)';
this.node.appendChild(next);
next.onclick = () => this.fireNext.bind(this)();
}

if (this.props.showButtons || this.props.showFilter) {
close = document.createElement('div');
close.classList.add(
SearchBox.Styles.BUTTON,
Expand All @@ -217,6 +268,7 @@ export class SearchBox extends BaseWidget {
return {
container: this.node,
input,
filter,
previous,
next,
close
Expand All @@ -242,6 +294,8 @@ export namespace SearchBox {
export const SEARCH_BOX = 'theia-search-box';
export const SEARCH_INPUT = 'theia-search-input';
export const BUTTON = 'theia-search-button';
export const FILTER = ['codicon', 'codicon-filter'];
export const FILTER_ON = 'filter-active';
export const BUTTON_PREVIOUS = 'theia-search-button-previous';
export const BUTTON_NEXT = 'theia-search-button-next';
export const BUTTON_CLOSE = 'theia-search-button-close';
Expand Down
24 changes: 20 additions & 4 deletions packages/core/src/browser/tree/tree-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class TreeSearch implements Disposable {

protected _filterResult: FuzzySearch.Match<TreeNode>[] = [];
protected _filteredNodes: ReadonlyArray<Readonly<TreeNode>> = [];
protected _filteredNodesAndParents: Set<string> = new Set();

@postConstruct()
protected init(): void {
Expand All @@ -55,22 +56,34 @@ export class TreeSearch implements Disposable {
*/
async filter(pattern: string | undefined): Promise<ReadonlyArray<Readonly<TreeNode>>> {
const { root } = this.tree;
this._filteredNodesAndParents = new Set();
if (!pattern || !root) {
this._filterResult = [];
this._filteredNodes = [];
this.fireFilteredNodesChanged(this._filteredNodes);
return [];
}
const items = [...new TopDownTreeIterator(root, { pruneCollapsed: true })];
const items = [...new TopDownTreeIterator(root)];
const transform = (node: TreeNode) => this.labelProvider.getName(node);
this._filterResult = await this.fuzzySearch.filter({
items,
pattern,
transform
});
this._filteredNodes = this._filterResult.map(match => match.item);
this._filteredNodes = this._filterResult.map(({ item }) => {
this.addAllParentsToFilteredSet(item);
return item;
});
this.fireFilteredNodesChanged(this._filteredNodes);
return this._filteredNodes!.slice();
return this._filteredNodes.slice();
}

protected addAllParentsToFilteredSet(node: TreeNode): void {
let toAdd: TreeNode | undefined = node;
while (toAdd && !this._filteredNodesAndParents.has(toAdd.id)) {
this._filteredNodesAndParents.add(toAdd.id);
toAdd = toAdd.parent;
};
}

/**
Expand All @@ -87,6 +100,10 @@ export class TreeSearch implements Disposable {
return this.filteredNodesEmitter.event;
}

passesFilters(node: TreeNode): boolean {
return this._filteredNodesAndParents.has(node.id);
}

dispose(): void {
this.disposables.dispose();
}
Expand All @@ -108,5 +125,4 @@ export class TreeSearch implements Disposable {
length
};
}

}
17 changes: 12 additions & 5 deletions packages/core/src/browser/tree/tree-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
@postConstruct()
protected init(): void {
if (this.props.search) {
this.searchBox = this.searchBoxFactory({ ...SearchBoxProps.DEFAULT, showButtons: true });
this.searchBox = this.searchBoxFactory({ ...SearchBoxProps.DEFAULT, showButtons: true, showFilter: true });
this.toDispose.pushAll([
this.searchBox,
this.searchBox.onTextChange(async data => {
Expand All @@ -207,16 +207,19 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
this.model.selectPrevNode();
}
}),
this.searchBox.onFilterToggled(e => {
this.updateRows();
}),
this.treeSearch,
this.treeSearch.onFilteredNodesChanged(nodes => {
if (this.searchBox.isFiltering) {
this.updateRows();
}
const node = nodes.find(SelectableTreeNode.is);
if (node) {
this.model.selectNode(node);
}
}),
this.model.onExpansionChanged(() => {
this.searchBox.hide();
})
]);
}
this.toDispose.pushAll([
Expand Down Expand Up @@ -282,7 +285,7 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
pruneCollapsed: true,
pruneSiblings: true
})) {
if (TreeNode.isVisible(node)) {
if (this.shouldDisplayNode(node)) {
const parentDepth = depths.get(node.parent);
const depth = parentDepth === undefined ? 0 : TreeNode.isVisible(node.parent) ? parentDepth + 1 : parentDepth;
if (CompositeTreeNode.is(node)) {
Expand All @@ -300,6 +303,10 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
this.updateScrollToRow();
}

protected shouldDisplayNode(node: TreeNode): boolean {
return TreeNode.isVisible(node) && (!this.searchBox?.isFiltering || this.treeSearch.passesFilters(node));
}

/**
* Row index to ensure visibility.
* - Used to forcefully scroll if necessary.
Expand Down
3 changes: 3 additions & 0 deletions packages/scm/src/browser/scm-tree-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ export class ScmTreeWidget extends TreeWidget {
}

set viewMode(id: 'tree' | 'list') {
// Close the search box because the structure of the tree will change dramatically
// and the search results will be out of date.
this.searchBox.hide();
this.model.viewMode = id;
}
get viewMode(): 'tree' | 'list' {
Expand Down