1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | import { inject, injectable, postConstruct } from 'inversify';
|
18 | import { Disposable, DisposableCollection } from '../../common/disposable';
|
19 | import { Event, Emitter } from '../../common/event';
|
20 | import { Tree, TreeNode } from './tree';
|
21 | import { TreeDecoration } from './tree-decorator';
|
22 | import { FuzzySearch } from './fuzzy-search';
|
23 | import { TopDownTreeIterator } from './tree-iterator';
|
24 | import { LabelProvider } from '../label-provider';
|
25 |
|
26 | @injectable()
|
27 | export class TreeSearch implements Disposable {
|
28 |
|
29 | @inject(Tree)
|
30 | protected readonly tree: Tree;
|
31 |
|
32 | @inject(FuzzySearch)
|
33 | protected readonly fuzzySearch: FuzzySearch;
|
34 |
|
35 | @inject(LabelProvider)
|
36 | protected readonly labelProvider: LabelProvider;
|
37 |
|
38 | protected readonly disposables = new DisposableCollection();
|
39 | protected readonly filteredNodesEmitter = new Emitter<ReadonlyArray<Readonly<TreeNode>>>();
|
40 |
|
41 | protected _filterResult: FuzzySearch.Match<TreeNode>[] = [];
|
42 | protected _filteredNodes: ReadonlyArray<Readonly<TreeNode>> = [];
|
43 | protected _filteredNodesAndParents: Set<string> = new Set();
|
44 |
|
45 | @postConstruct()
|
46 | protected init(): void {
|
47 | this.disposables.push(this.filteredNodesEmitter);
|
48 | }
|
49 |
|
50 | getHighlights(): Map<string, TreeDecoration.CaptionHighlight> {
|
51 | return new Map(this._filterResult.map(m => [m.item.id, this.toCaptionHighlight(m)] as [string, TreeDecoration.CaptionHighlight]));
|
52 | }
|
53 |
|
54 | |
55 |
|
56 |
|
57 | async filter(pattern: string | undefined): Promise<ReadonlyArray<Readonly<TreeNode>>> {
|
58 | const { root } = this.tree;
|
59 | this._filteredNodesAndParents = new Set();
|
60 | if (!pattern || !root) {
|
61 | this._filterResult = [];
|
62 | this._filteredNodes = [];
|
63 | this.fireFilteredNodesChanged(this._filteredNodes);
|
64 | return [];
|
65 | }
|
66 | const items = [...new TopDownTreeIterator(root)];
|
67 | const transform = (node: TreeNode) => this.labelProvider.getName(node);
|
68 | this._filterResult = await this.fuzzySearch.filter({
|
69 | items,
|
70 | pattern,
|
71 | transform
|
72 | });
|
73 | this._filteredNodes = this._filterResult.map(({ item }) => {
|
74 | this.addAllParentsToFilteredSet(item);
|
75 | return item;
|
76 | });
|
77 | this.fireFilteredNodesChanged(this._filteredNodes);
|
78 | return this._filteredNodes.slice();
|
79 | }
|
80 |
|
81 | protected addAllParentsToFilteredSet(node: TreeNode): void {
|
82 | let toAdd: TreeNode | undefined = node;
|
83 | while (toAdd && !this._filteredNodesAndParents.has(toAdd.id)) {
|
84 | this._filteredNodesAndParents.add(toAdd.id);
|
85 | toAdd = toAdd.parent;
|
86 | };
|
87 | }
|
88 |
|
89 | |
90 |
|
91 |
|
92 | get filteredNodes(): ReadonlyArray<Readonly<TreeNode>> {
|
93 | return this._filteredNodes.slice();
|
94 | }
|
95 |
|
96 | |
97 |
|
98 |
|
99 | get onFilteredNodesChanged(): Event<ReadonlyArray<Readonly<TreeNode>>> {
|
100 | return this.filteredNodesEmitter.event;
|
101 | }
|
102 |
|
103 | passesFilters(node: TreeNode): boolean {
|
104 | return this._filteredNodesAndParents.has(node.id);
|
105 | }
|
106 |
|
107 | dispose(): void {
|
108 | this.disposables.dispose();
|
109 | }
|
110 |
|
111 | protected fireFilteredNodesChanged(nodes: ReadonlyArray<Readonly<TreeNode>>): void {
|
112 | this.filteredNodesEmitter.fire(nodes);
|
113 | }
|
114 |
|
115 | protected toCaptionHighlight(match: FuzzySearch.Match<TreeNode>): TreeDecoration.CaptionHighlight {
|
116 | return {
|
117 | ranges: match.ranges.map(this.mapRange.bind(this))
|
118 | };
|
119 | }
|
120 |
|
121 | protected mapRange(range: FuzzySearch.Range): TreeDecoration.CaptionHighlight.Range {
|
122 | const { offset, length } = range;
|
123 | return {
|
124 | offset,
|
125 | length
|
126 | };
|
127 | }
|
128 | }
|