@difizen/libro-search 0.0.2-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/es/abstract-search-provider.d.ts +113 -0
- package/es/abstract-search-provider.d.ts.map +1 -0
- package/es/abstract-search-provider.js +126 -0
- package/es/index.d.ts +10 -0
- package/es/index.d.ts.map +1 -0
- package/es/index.js +9 -0
- package/es/index.less +107 -0
- package/es/libro-cell-search-provider.d.ts +10 -0
- package/es/libro-cell-search-provider.d.ts.map +1 -0
- package/es/libro-cell-search-provider.js +54 -0
- package/es/libro-search-engine-html.d.ts +3 -0
- package/es/libro-search-engine-html.d.ts.map +1 -0
- package/es/libro-search-engine-html.js +59 -0
- package/es/libro-search-engine-text.d.ts +10 -0
- package/es/libro-search-engine-text.d.ts.map +1 -0
- package/es/libro-search-engine-text.js +32 -0
- package/es/libro-search-generic-provider.d.ts +107 -0
- package/es/libro-search-generic-provider.d.ts.map +1 -0
- package/es/libro-search-generic-provider.js +467 -0
- package/es/libro-search-manager.d.ts +22 -0
- package/es/libro-search-manager.d.ts.map +1 -0
- package/es/libro-search-manager.js +102 -0
- package/es/libro-search-model.d.ts +111 -0
- package/es/libro-search-model.d.ts.map +1 -0
- package/es/libro-search-model.js +395 -0
- package/es/libro-search-protocol.d.ts +176 -0
- package/es/libro-search-protocol.d.ts.map +1 -0
- package/es/libro-search-protocol.js +36 -0
- package/es/libro-search-provider.d.ts +138 -0
- package/es/libro-search-provider.d.ts.map +1 -0
- package/es/libro-search-provider.js +759 -0
- package/es/libro-search-utils.d.ts +25 -0
- package/es/libro-search-utils.d.ts.map +1 -0
- package/es/libro-search-utils.js +85 -0
- package/es/libro-search-view.d.ts +59 -0
- package/es/libro-search-view.d.ts.map +1 -0
- package/es/libro-search-view.js +455 -0
- package/es/module.d.ts +4 -0
- package/es/module.d.ts.map +1 -0
- package/es/module.js +35 -0
- package/package.json +66 -0
- package/src/abstract-search-provider.ts +160 -0
- package/src/index.less +107 -0
- package/src/index.ts +9 -0
- package/src/libro-cell-search-provider.ts +39 -0
- package/src/libro-search-engine-html.ts +74 -0
- package/src/libro-search-engine-text.ts +34 -0
- package/src/libro-search-generic-provider.ts +303 -0
- package/src/libro-search-manager.ts +86 -0
- package/src/libro-search-model.ts +266 -0
- package/src/libro-search-protocol.ts +209 -0
- package/src/libro-search-provider.ts +507 -0
- package/src/libro-search-utils.spec.ts +37 -0
- package/src/libro-search-utils.ts +83 -0
- package/src/libro-search-view.tsx +404 -0
- package/src/module.ts +59 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { CellView } from '@difizen/libro-core';
|
|
2
|
+
import type { Disposable, Event } from '@difizen/mana-app';
|
|
3
|
+
import type { View } from '@difizen/mana-app';
|
|
4
|
+
import { Syringe } from '@difizen/mana-app';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Base search match interface
|
|
8
|
+
*/
|
|
9
|
+
export interface SearchMatch {
|
|
10
|
+
/**
|
|
11
|
+
* Text of the exact match itself
|
|
12
|
+
*/
|
|
13
|
+
readonly text: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Start location of the match (in a text, this is the column)
|
|
17
|
+
*/
|
|
18
|
+
position: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* HTML search match interface
|
|
23
|
+
*/
|
|
24
|
+
export interface HTMLSearchMatch extends SearchMatch {
|
|
25
|
+
/**
|
|
26
|
+
* Node containing the match
|
|
27
|
+
*/
|
|
28
|
+
readonly node: Text;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Filter interface
|
|
33
|
+
*/
|
|
34
|
+
export interface SearchFilter {
|
|
35
|
+
/**
|
|
36
|
+
* Filter title
|
|
37
|
+
*/
|
|
38
|
+
title: string;
|
|
39
|
+
/**
|
|
40
|
+
* Filter description
|
|
41
|
+
*/
|
|
42
|
+
description: string;
|
|
43
|
+
/**
|
|
44
|
+
* Default value
|
|
45
|
+
*/
|
|
46
|
+
default: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Does the filter support replace?
|
|
49
|
+
*/
|
|
50
|
+
supportReplace: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Type of filters
|
|
54
|
+
*
|
|
55
|
+
*/
|
|
56
|
+
export interface SearchFilters {
|
|
57
|
+
searchCellOutput: boolean;
|
|
58
|
+
onlySearchSelectedCells: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Base search provider interface
|
|
63
|
+
*
|
|
64
|
+
* #### Notes
|
|
65
|
+
* It is implemented by subprovider like searching on a single cell.
|
|
66
|
+
*/
|
|
67
|
+
export interface BaseSearchProvider extends Disposable {
|
|
68
|
+
/**
|
|
69
|
+
* Get an initial query value if applicable so that it can be entered
|
|
70
|
+
* into the search box as an initial query
|
|
71
|
+
*
|
|
72
|
+
* @returns Initial value used to populate the search box.
|
|
73
|
+
*/
|
|
74
|
+
getInitialQuery?(): string;
|
|
75
|
+
/**
|
|
76
|
+
* Start a search
|
|
77
|
+
*
|
|
78
|
+
* @param query Regular expression to test for
|
|
79
|
+
* @param filters Filters to apply when searching
|
|
80
|
+
*/
|
|
81
|
+
startQuery(query: RegExp, filters?: SearchFilters): Promise<void>;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Stop a search and clear any internal state of the provider
|
|
85
|
+
*/
|
|
86
|
+
endQuery(): Promise<void>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Clear currently highlighted match.
|
|
90
|
+
*/
|
|
91
|
+
clearHighlight(): Promise<void>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Highlight the next match
|
|
95
|
+
*
|
|
96
|
+
* @param loop Whether to loop within the matches list.
|
|
97
|
+
*
|
|
98
|
+
* @returns The next match if it exists
|
|
99
|
+
*/
|
|
100
|
+
highlightNext(loop?: boolean): Promise<SearchMatch | undefined>;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Highlight the previous match
|
|
104
|
+
*
|
|
105
|
+
* @param loop Whether to loop within the matches list.
|
|
106
|
+
*
|
|
107
|
+
* @returns The previous match if it exists.
|
|
108
|
+
*/
|
|
109
|
+
highlightPrevious(loop?: boolean): Promise<SearchMatch | undefined>;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Replace the currently selected match with the provided text
|
|
113
|
+
* and highlight the next match.
|
|
114
|
+
*
|
|
115
|
+
* @param newText The replacement text
|
|
116
|
+
* @param loop Whether to loop within the matches list.
|
|
117
|
+
*
|
|
118
|
+
* @returns A promise that resolves with a boolean indicating whether a replace occurred.
|
|
119
|
+
*/
|
|
120
|
+
replaceCurrentMatch(newText: string, loop?: boolean): Promise<boolean>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Replace all matches in the widget with the provided text
|
|
124
|
+
*
|
|
125
|
+
* @param newText The replacement text.
|
|
126
|
+
*
|
|
127
|
+
* @returns A promise that resolves with a boolean indicating whether a replace occurred.
|
|
128
|
+
*/
|
|
129
|
+
replaceAllMatches(newText: string): Promise<boolean>;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Signal indicating that something in the search has changed, so the UI should update
|
|
133
|
+
*/
|
|
134
|
+
readonly stateChanged: Event<void>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The current index of the selected match.
|
|
138
|
+
*/
|
|
139
|
+
readonly currentMatchIndex: number | undefined;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The number of matches.
|
|
143
|
+
*/
|
|
144
|
+
readonly matchesCount: number | undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Search provider interface
|
|
149
|
+
*/
|
|
150
|
+
export interface SearchProvider extends BaseSearchProvider {
|
|
151
|
+
/**
|
|
152
|
+
* Set to true if the widget under search is read-only, false
|
|
153
|
+
* if it is editable. Will be used to determine whether to show
|
|
154
|
+
* the replace option.
|
|
155
|
+
*/
|
|
156
|
+
readonly isReadOnly: boolean;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get the filters definition for the given provider.
|
|
160
|
+
*
|
|
161
|
+
* @returns The filters definition.
|
|
162
|
+
*
|
|
163
|
+
* ### Notes
|
|
164
|
+
* TODO For now it only supports boolean filters (represented with checkboxes)
|
|
165
|
+
*/
|
|
166
|
+
getFilters?(): Record<string, SearchFilter>;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Validate a new filter value for the widget.
|
|
170
|
+
*
|
|
171
|
+
* @param name The filter name
|
|
172
|
+
* @param value The filter value candidate
|
|
173
|
+
*
|
|
174
|
+
* @returns The valid filter value
|
|
175
|
+
*/
|
|
176
|
+
validateFilter?(name: string, value: boolean): Promise<boolean>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// export const SearchProvider = Syringe.defineToken('SearchProvider');
|
|
180
|
+
|
|
181
|
+
export interface CellSearchProvider extends BaseSearchProvider {
|
|
182
|
+
isActive: boolean;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface CellSearchProviderContribution {
|
|
186
|
+
canHandle: (cell: CellView) => number;
|
|
187
|
+
factory: (cell: CellView) => CellSearchProvider;
|
|
188
|
+
getInitialQuery?: (cell: CellView) => string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export const CellSearchProviderContribution = Syringe.defineToken(
|
|
192
|
+
'CellSearchProviderContribution',
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
export const LIBRO_SEARCH_FOUND_CLASSES = [
|
|
196
|
+
'cm-string',
|
|
197
|
+
'cm-overlay',
|
|
198
|
+
'cm-searching',
|
|
199
|
+
'libro-searching',
|
|
200
|
+
];
|
|
201
|
+
export const LIBRO_SEARCH_SELECTED_CLASSES = [
|
|
202
|
+
'CodeMirror-selectedtext',
|
|
203
|
+
'libro-selectedtext',
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
export interface SearchProviderOption {
|
|
207
|
+
view: View;
|
|
208
|
+
}
|
|
209
|
+
export const SearchProviderOption = Symbol('SearchProviderOption');
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import type { CellView } from '@difizen/libro-core';
|
|
2
|
+
import { LibroView } from '@difizen/libro-core';
|
|
3
|
+
import { inject, prop, transient, watch, equals } from '@difizen/mana-app';
|
|
4
|
+
import { Deferred, DisposableCollection } from '@difizen/mana-app';
|
|
5
|
+
import { l10n } from '@difizen/mana-l10n';
|
|
6
|
+
|
|
7
|
+
import { AbstractSearchProvider } from './abstract-search-provider.js';
|
|
8
|
+
import { LibroCellSearchProvider } from './libro-cell-search-provider.js';
|
|
9
|
+
import type {
|
|
10
|
+
CellSearchProvider,
|
|
11
|
+
SearchFilter,
|
|
12
|
+
SearchMatch,
|
|
13
|
+
SearchFilters,
|
|
14
|
+
} from './libro-search-protocol.js';
|
|
15
|
+
import { SearchProviderOption } from './libro-search-protocol.js';
|
|
16
|
+
|
|
17
|
+
export type LibroSearchProviderFactory = (
|
|
18
|
+
option: SearchProviderOption,
|
|
19
|
+
) => LibroSearchProvider;
|
|
20
|
+
export const LibroSearchProviderFactory = Symbol('LibroSearchProviderFactory');
|
|
21
|
+
/**
|
|
22
|
+
* Libro view search provider
|
|
23
|
+
*/
|
|
24
|
+
@transient()
|
|
25
|
+
export class LibroSearchProvider extends AbstractSearchProvider {
|
|
26
|
+
@inject(LibroCellSearchProvider) libroCellSearchProvider: LibroCellSearchProvider;
|
|
27
|
+
protected cellsChangeDeferred: Deferred<void> | undefined;
|
|
28
|
+
|
|
29
|
+
protected toDispose = new DisposableCollection();
|
|
30
|
+
@prop() protected currentProviderIndex: number | undefined = undefined;
|
|
31
|
+
@prop() searchCellOutput = true;
|
|
32
|
+
@prop() protected onlySearchSelectedCells = false;
|
|
33
|
+
@prop() replaceMode = false;
|
|
34
|
+
|
|
35
|
+
protected get filters(): SearchFilters {
|
|
36
|
+
return {
|
|
37
|
+
searchCellOutput: this.searchCellOutput && !this.replaceMode,
|
|
38
|
+
onlySearchSelectedCells: this.onlySearchSelectedCells,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
protected query: RegExp | undefined = undefined;
|
|
43
|
+
@prop() protected searchProviders: (CellSearchProvider | undefined)[] = [];
|
|
44
|
+
@prop() protected providerMap = new Map<string, CellSearchProvider>();
|
|
45
|
+
protected documentHasChanged = false;
|
|
46
|
+
protected override view: LibroView;
|
|
47
|
+
|
|
48
|
+
updateSearchCellOutput(value: boolean): void {
|
|
49
|
+
this.searchCellOutput = value;
|
|
50
|
+
this.filters.searchCellOutput = value;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* @param option Provide the view to search in
|
|
54
|
+
*/
|
|
55
|
+
constructor(@inject(SearchProviderOption) option: SearchProviderOption) {
|
|
56
|
+
super(option);
|
|
57
|
+
this.view = option.view as LibroView;
|
|
58
|
+
this.toDispose.push(watch(this.view.model, 'active', this.onActiveCellChanged));
|
|
59
|
+
this.toDispose.push(watch(this.view.model, 'cells', this.onCellsChanged));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
protected getProvider = (cell: CellView) => {
|
|
63
|
+
return this.providerMap.get(cell.id);
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Report whether or not this provider has the ability to search on the given object
|
|
67
|
+
*
|
|
68
|
+
* @param domain Widget to test
|
|
69
|
+
* @returns Search ability
|
|
70
|
+
*/
|
|
71
|
+
static isApplicable(domain: LibroView): domain is LibroView {
|
|
72
|
+
// check to see if the CMSearchProvider can search on the
|
|
73
|
+
// first cell, false indicates another editor is present
|
|
74
|
+
return domain instanceof LibroView;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The current index of the selected match.
|
|
79
|
+
*/
|
|
80
|
+
override get currentMatchIndex(): number | undefined {
|
|
81
|
+
let agg = 0;
|
|
82
|
+
let found = false;
|
|
83
|
+
for (let idx = 0; idx < this.searchProviders.length; idx++) {
|
|
84
|
+
const provider = this.searchProviders[idx];
|
|
85
|
+
const localMatch = provider?.currentMatchIndex;
|
|
86
|
+
if (localMatch !== undefined) {
|
|
87
|
+
agg += localMatch;
|
|
88
|
+
found = true;
|
|
89
|
+
break;
|
|
90
|
+
} else {
|
|
91
|
+
agg += provider?.matchesCount ?? 0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return found ? agg : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The number of matches.
|
|
99
|
+
*/
|
|
100
|
+
override get matchesCount(): number | undefined {
|
|
101
|
+
const count = this.view.model.cells.reduce((sum, cell) => {
|
|
102
|
+
const provider = this.getProvider(cell);
|
|
103
|
+
sum += provider?.matchesCount || 0;
|
|
104
|
+
return sum;
|
|
105
|
+
}, 0);
|
|
106
|
+
if (count === 0) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
return count;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Set to true if the widget under search is read-only, false
|
|
114
|
+
* if it is editable. Will be used to determine whether to show
|
|
115
|
+
* the replace option.
|
|
116
|
+
*/
|
|
117
|
+
get isReadOnly(): boolean {
|
|
118
|
+
return this.view?.model?.readOnly ?? false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Dispose of the resources held by the search provider.
|
|
123
|
+
*
|
|
124
|
+
* #### Notes
|
|
125
|
+
* If the object's `dispose` method is called more than once, all
|
|
126
|
+
* calls made after the first will be a no-op.
|
|
127
|
+
*
|
|
128
|
+
* #### Undefined Behavior
|
|
129
|
+
* It is undefined behavior to use any functionality of the object
|
|
130
|
+
* after it has been disposed unless otherwise explicitly noted.
|
|
131
|
+
*/
|
|
132
|
+
override dispose(): void {
|
|
133
|
+
if (this.isDisposed) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.toDispose.dispose();
|
|
137
|
+
this.providerMap.clear();
|
|
138
|
+
super.dispose();
|
|
139
|
+
|
|
140
|
+
// const index = this.view.model.active;
|
|
141
|
+
this.endQuery()
|
|
142
|
+
.then(() => {
|
|
143
|
+
if (!this.view.isDisposed) {
|
|
144
|
+
// this.view.model.active = index;
|
|
145
|
+
// TODO: should active cell?
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
})
|
|
149
|
+
.catch((reason) => {
|
|
150
|
+
console.error(`Fail to end search query in notebook:\n${reason}`);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get the filters for the given provider.
|
|
156
|
+
*
|
|
157
|
+
* @returns The filters.
|
|
158
|
+
*/
|
|
159
|
+
override getFilters(): Record<string, SearchFilter> {
|
|
160
|
+
return {
|
|
161
|
+
output: {
|
|
162
|
+
title: l10n.t('在 Output 中查找'),
|
|
163
|
+
description: l10n.t('在 Output 中查找'),
|
|
164
|
+
default: false,
|
|
165
|
+
supportReplace: false,
|
|
166
|
+
},
|
|
167
|
+
selectedCells: {
|
|
168
|
+
title: l10n.t('仅在选中 cell 中查找'),
|
|
169
|
+
description: l10n.t('仅在选中 cell 中查找'),
|
|
170
|
+
default: false,
|
|
171
|
+
supportReplace: true,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get an initial query value if applicable so that it can be entered
|
|
178
|
+
* into the search box as an initial query
|
|
179
|
+
*
|
|
180
|
+
* @returns Initial value used to populate the search box.
|
|
181
|
+
*/
|
|
182
|
+
override getInitialQuery = (): string => {
|
|
183
|
+
const activeCell = this.view.model.active;
|
|
184
|
+
if (activeCell) {
|
|
185
|
+
return this.libroCellSearchProvider.getInitialQuery(activeCell);
|
|
186
|
+
}
|
|
187
|
+
return '';
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Clear currently highlighted match.
|
|
192
|
+
*/
|
|
193
|
+
clearHighlight = async (): Promise<void> => {
|
|
194
|
+
if (this.currentProviderIndex !== undefined) {
|
|
195
|
+
await this.searchProviders[this.currentProviderIndex]?.clearHighlight();
|
|
196
|
+
this.currentProviderIndex = undefined;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Highlight the next match.
|
|
202
|
+
*
|
|
203
|
+
* @param loop Whether to loop within the matches list.
|
|
204
|
+
*
|
|
205
|
+
* @returns The next match if available.
|
|
206
|
+
*/
|
|
207
|
+
highlightNext = async (loop = true): Promise<SearchMatch | undefined> => {
|
|
208
|
+
const match = await this.stepNext(false, loop);
|
|
209
|
+
return match ?? undefined;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Highlight the previous match.
|
|
214
|
+
*
|
|
215
|
+
* @param loop Whether to loop within the matches list.
|
|
216
|
+
*
|
|
217
|
+
* @returns The previous match if available.
|
|
218
|
+
*/
|
|
219
|
+
highlightPrevious = async (loop = true): Promise<SearchMatch | undefined> => {
|
|
220
|
+
const match = await this.stepNext(true, loop);
|
|
221
|
+
return match ?? undefined;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Search for a regular expression with optional filters.
|
|
226
|
+
*
|
|
227
|
+
* @param query A regular expression to test for
|
|
228
|
+
* @param filters Filter parameters to pass to provider
|
|
229
|
+
*
|
|
230
|
+
*/
|
|
231
|
+
startQuery = async (
|
|
232
|
+
query: RegExp,
|
|
233
|
+
_filters?: SearchFilters,
|
|
234
|
+
highlightNext = true,
|
|
235
|
+
): Promise<void> => {
|
|
236
|
+
if (!this.view) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
await this.endQuery();
|
|
240
|
+
const cells = this.view.model.cells;
|
|
241
|
+
|
|
242
|
+
this.query = query;
|
|
243
|
+
|
|
244
|
+
// TODO: support selected cells
|
|
245
|
+
if (this.filters?.onlySearchSelectedCells) {
|
|
246
|
+
// watch(this.view.model, 'selections', this._onSelectionChanged);
|
|
247
|
+
// this.view.model.selectionChanged.connect(this._onSelectionChanged, this);
|
|
248
|
+
}
|
|
249
|
+
// For each cell, create a search provider
|
|
250
|
+
this.searchProviders = await Promise.all(
|
|
251
|
+
cells.map(async (cell) => {
|
|
252
|
+
let cellSearchProvider;
|
|
253
|
+
if (this.providerMap.has(cell.id)) {
|
|
254
|
+
cellSearchProvider = this.providerMap.get(cell.id);
|
|
255
|
+
} else {
|
|
256
|
+
cellSearchProvider =
|
|
257
|
+
this.libroCellSearchProvider.createCellSearchProvider(cell);
|
|
258
|
+
}
|
|
259
|
+
if (cellSearchProvider) {
|
|
260
|
+
this.providerMap.set(cell.id, cellSearchProvider);
|
|
261
|
+
}
|
|
262
|
+
// cellSearchProvider.stateChanged(this._onSearchProviderChanged);
|
|
263
|
+
// await cellSearchProvider.setIsActive(
|
|
264
|
+
// !this._filters!.selectedCells || this.widget.content.isSelectedOrActive(cell),
|
|
265
|
+
// );
|
|
266
|
+
await cellSearchProvider?.startQuery(query, this.filters);
|
|
267
|
+
return cellSearchProvider;
|
|
268
|
+
}),
|
|
269
|
+
);
|
|
270
|
+
this.currentProviderIndex = this.getActiveIndex();
|
|
271
|
+
|
|
272
|
+
if (!this.documentHasChanged && highlightNext) {
|
|
273
|
+
await this.highlightNext(false);
|
|
274
|
+
}
|
|
275
|
+
this.documentHasChanged = false;
|
|
276
|
+
|
|
277
|
+
return Promise.resolve();
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Stop the search and clear all internal state.
|
|
282
|
+
*/
|
|
283
|
+
endQuery = async (): Promise<void> => {
|
|
284
|
+
await Promise.all(
|
|
285
|
+
this.searchProviders.map((provider) => {
|
|
286
|
+
// provider?.stateChanged(this._onSearchProviderChanged);
|
|
287
|
+
return provider?.endQuery();
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
this.searchProviders.length = 0;
|
|
291
|
+
this.currentProviderIndex = undefined;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Replace the currently selected match with the provided text
|
|
296
|
+
*
|
|
297
|
+
* @param newText The replacement text.
|
|
298
|
+
* @param loop Whether to loop within the matches list.
|
|
299
|
+
*
|
|
300
|
+
* @returns A promise that resolves with a boolean indicating whether a replace occurred.
|
|
301
|
+
*/
|
|
302
|
+
replaceCurrentMatch = async (newText: string, loop = true): Promise<boolean> => {
|
|
303
|
+
let replaceOccurred = false;
|
|
304
|
+
// TODO: makrdown unrendered
|
|
305
|
+
// const unrenderMarkdownCell = async (highlightNext = false): Promise<void> => {
|
|
306
|
+
// // Unrendered markdown cell
|
|
307
|
+
// const activeCell = this.view?.model.active;
|
|
308
|
+
// if (activeCell?.model.type === 'markdown' && (activeCell as MarkdownCell).rendered) {
|
|
309
|
+
// (activeCell as MarkdownCell).rendered = false;
|
|
310
|
+
// if (highlightNext) {
|
|
311
|
+
// await this.highlightNext(loop);
|
|
312
|
+
// }
|
|
313
|
+
// }
|
|
314
|
+
// };
|
|
315
|
+
|
|
316
|
+
if (this.currentProviderIndex !== undefined) {
|
|
317
|
+
// await unrenderMarkdownCell();
|
|
318
|
+
|
|
319
|
+
const searchEngine = this.searchProviders[this.currentProviderIndex];
|
|
320
|
+
replaceOccurred = !!(await searchEngine?.replaceCurrentMatch(newText));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
await this.highlightNext(loop);
|
|
324
|
+
// Force highlighting the first hit in the unrendered cell
|
|
325
|
+
// await unrenderMarkdownCell(true);
|
|
326
|
+
return replaceOccurred;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Replace all matches in the notebook with the provided text
|
|
331
|
+
*
|
|
332
|
+
* @param newText The replacement text.
|
|
333
|
+
*
|
|
334
|
+
* @returns A promise that resolves with a boolean indicating whether a replace occurred.
|
|
335
|
+
*/
|
|
336
|
+
replaceAllMatches = async (newText: string): Promise<boolean> => {
|
|
337
|
+
const replacementOccurred = await Promise.all(
|
|
338
|
+
this.searchProviders.map((provider) => {
|
|
339
|
+
return provider?.replaceAllMatches(newText);
|
|
340
|
+
}),
|
|
341
|
+
);
|
|
342
|
+
return replacementOccurred.includes(true);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
protected addCellProvider = (index: number) => {
|
|
346
|
+
const cell = this.view.model.cells[index];
|
|
347
|
+
const cellSearchProvider =
|
|
348
|
+
this.libroCellSearchProvider.createCellSearchProvider(cell);
|
|
349
|
+
const current = this.searchProviders.slice();
|
|
350
|
+
current.splice(index, 0, cellSearchProvider);
|
|
351
|
+
this.searchProviders = current;
|
|
352
|
+
if (cellSearchProvider) {
|
|
353
|
+
cellSearchProvider.stateChanged(this.onSearchProviderChanged);
|
|
354
|
+
if (this.query) {
|
|
355
|
+
cellSearchProvider.startQuery(this.query, this.filters);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// void cellSearchProvider
|
|
359
|
+
// .setIsActive(
|
|
360
|
+
// !(this._filters?.selectedCells ?? false) || this.widget.content.isSelectedOrActive(cell),
|
|
361
|
+
// )
|
|
362
|
+
// .then(() => {
|
|
363
|
+
// void cellSearchProvider.startQuery(this._query, this._filters);
|
|
364
|
+
// });
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
protected removeCellProvider = (index: number) => {
|
|
368
|
+
const current = this.searchProviders.slice();
|
|
369
|
+
const provider = current.slice(index, 1)[0];
|
|
370
|
+
provider?.dispose();
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
protected doCellsChanged = async (): Promise<void> => {
|
|
374
|
+
if (this.query) {
|
|
375
|
+
this.startQuery(this.query);
|
|
376
|
+
} else {
|
|
377
|
+
this.endQuery();
|
|
378
|
+
}
|
|
379
|
+
this.onSearchProviderChanged();
|
|
380
|
+
this.cellsChangeDeferred = undefined;
|
|
381
|
+
};
|
|
382
|
+
protected onCellsChanged = async (): Promise<void> => {
|
|
383
|
+
if (!this.cellsChangeDeferred) {
|
|
384
|
+
this.cellsChangeDeferred = new Deferred();
|
|
385
|
+
this.cellsChangeDeferred.promise.then(this.doCellsChanged).catch(() => {
|
|
386
|
+
//
|
|
387
|
+
});
|
|
388
|
+
this.cellsChangeDeferred.resolve();
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
protected getActiveIndex = (): number | undefined => {
|
|
393
|
+
if (!this.view.activeCell) {
|
|
394
|
+
return undefined;
|
|
395
|
+
}
|
|
396
|
+
const index = this.view.model.cells.findIndex((cell) =>
|
|
397
|
+
equals(cell, this.view.activeCell),
|
|
398
|
+
);
|
|
399
|
+
if (index < 0) {
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
return index;
|
|
403
|
+
};
|
|
404
|
+
protected stepNext = async (
|
|
405
|
+
reverse = false,
|
|
406
|
+
loop = false,
|
|
407
|
+
): Promise<SearchMatch | undefined> => {
|
|
408
|
+
const activateNewMatch = async () => {
|
|
409
|
+
// if (this.getActiveIndex() !== this._currentProviderIndex!) {
|
|
410
|
+
// this.widget.content.activeCellIndex = this._currentProviderIndex!;
|
|
411
|
+
// }
|
|
412
|
+
// const activeCell = this.view.activeCell;
|
|
413
|
+
// if (!activeCell.inViewport) {
|
|
414
|
+
// try {
|
|
415
|
+
// if (this.view.activeCell) {
|
|
416
|
+
// this.view.model.scrollToView(this.view.activeCell);
|
|
417
|
+
// }
|
|
418
|
+
// } catch (error) {
|
|
419
|
+
// // no-op
|
|
420
|
+
// }
|
|
421
|
+
// }
|
|
422
|
+
// // Unhide cell
|
|
423
|
+
// if (activeCell.inputHidden) {
|
|
424
|
+
// activeCell.inputHidden = false;
|
|
425
|
+
// }
|
|
426
|
+
// if (!activeCell.inViewport) {
|
|
427
|
+
// // It will not be possible the cell is not in the view
|
|
428
|
+
// return;
|
|
429
|
+
// }
|
|
430
|
+
// await activeCell.ready;
|
|
431
|
+
// const editor = activeCell.editor! as CodeMirrorEditor;
|
|
432
|
+
// editor.revealSelection(editor.getSelection());
|
|
433
|
+
};
|
|
434
|
+
if (this.currentProviderIndex === undefined) {
|
|
435
|
+
this.currentProviderIndex = this.getActiveIndex()!;
|
|
436
|
+
}
|
|
437
|
+
const startIndex = this.currentProviderIndex;
|
|
438
|
+
do {
|
|
439
|
+
const searchEngine = this.searchProviders[this.currentProviderIndex];
|
|
440
|
+
const match = reverse
|
|
441
|
+
? await searchEngine?.highlightPrevious()
|
|
442
|
+
: await searchEngine?.highlightNext();
|
|
443
|
+
if (match) {
|
|
444
|
+
await activateNewMatch();
|
|
445
|
+
return match;
|
|
446
|
+
} else {
|
|
447
|
+
this.currentProviderIndex = this.currentProviderIndex + (reverse ? -1 : 1);
|
|
448
|
+
if (loop) {
|
|
449
|
+
// We loop on all cells, not hit found
|
|
450
|
+
if (this.currentProviderIndex === startIndex) {
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
this.currentProviderIndex =
|
|
454
|
+
(this.currentProviderIndex + this.searchProviders.length) %
|
|
455
|
+
this.searchProviders.length;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
} while (
|
|
459
|
+
0 <= this.currentProviderIndex &&
|
|
460
|
+
this.currentProviderIndex < this.searchProviders.length
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
if (loop) {
|
|
464
|
+
// Search a last time in the first provider as it may contain more
|
|
465
|
+
// than one matches
|
|
466
|
+
const searchEngine = this.searchProviders[this.currentProviderIndex];
|
|
467
|
+
const match = reverse
|
|
468
|
+
? await searchEngine?.highlightPrevious()
|
|
469
|
+
: await searchEngine?.highlightNext();
|
|
470
|
+
|
|
471
|
+
if (match) {
|
|
472
|
+
await activateNewMatch();
|
|
473
|
+
return match;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
this.currentProviderIndex = undefined;
|
|
478
|
+
return undefined;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
protected onActiveCellChanged = async () => {
|
|
482
|
+
await this._onSelectionChanged();
|
|
483
|
+
|
|
484
|
+
if (this.getActiveIndex() !== this.currentProviderIndex) {
|
|
485
|
+
await this.clearHighlight();
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
protected onSearchProviderChanged = () => {
|
|
490
|
+
// Don't highlight the next occurrence when the query
|
|
491
|
+
// follows a document change
|
|
492
|
+
this.documentHasChanged = true;
|
|
493
|
+
this._stateChanged.fire();
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
protected _onSelectionChanged = async () => {
|
|
497
|
+
// if (this.onlySelectedCells) {
|
|
498
|
+
// const cells = this.widget.content.widgets;
|
|
499
|
+
// await Promise.all(
|
|
500
|
+
// this._searchProviders.map((provider, index) =>
|
|
501
|
+
// provider.setIsActive(this.widget.content.isSelectedOrActive(cells[index])),
|
|
502
|
+
// ),
|
|
503
|
+
// );
|
|
504
|
+
// this._onSearchProviderChanged();
|
|
505
|
+
// }
|
|
506
|
+
};
|
|
507
|
+
}
|