0
# Search and Replace
1
2
Advanced search provider with regex support, match highlighting, and comprehensive find/replace functionality for editors.
3
4
## Capabilities
5
6
### EditorSearchProvider
7
8
Abstract base class for implementing search functionality in editors.
9
10
```typescript { .api }
11
/**
12
* Search provider for editors
13
* Abstract base class providing search and replace functionality
14
*/
15
abstract class EditorSearchProvider<T extends CodeEditor.IModel = CodeEditor.IModel>
16
implements IBaseSearchProvider {
17
18
constructor();
19
20
/**
21
* Current match index (null if no matches)
22
*/
23
readonly currentMatchIndex: number | null;
24
25
/**
26
* Whether the search is currently active
27
*/
28
readonly isActive: boolean;
29
30
/**
31
* Whether the search provider is disposed
32
*/
33
readonly isDisposed: boolean;
34
35
/**
36
* Number of matches found
37
*/
38
readonly matchesCount: number;
39
40
/**
41
* Signal emitted when search state changes
42
*/
43
readonly stateChanged: ISignal<IBaseSearchProvider, void>;
44
45
// Abstract properties (must be implemented by subclasses)
46
protected abstract readonly editor: CodeEditor.IEditor | null;
47
protected abstract readonly model: T;
48
49
// Lifecycle
50
dispose(): void;
51
52
// Search state management
53
setIsActive(active: boolean): Promise<void>;
54
setSearchSelection(selection: CodeEditor.IRange | null): Promise<void>;
55
setProtectSelection(protect: boolean): void;
56
57
// Query management
58
startQuery(query: RegExp | null, filters?: IFilters): Promise<void>;
59
endQuery(): Promise<void>;
60
61
// Match highlighting and navigation
62
clearHighlight(): Promise<void>;
63
highlightNext(loop?: boolean, options?: IHighlightAdjacentMatchOptions): Promise<ISearchMatch | undefined>;
64
highlightPrevious(loop?: boolean, options?: IHighlightAdjacentMatchOptions): Promise<ISearchMatch | undefined>;
65
getCurrentMatch(): ISearchMatch | undefined;
66
67
// Text replacement
68
replaceCurrentMatch(newText: string, loop?: boolean, options?: IReplaceOptions): Promise<boolean>;
69
replaceAllMatches(newText: string, options?: IReplaceOptions): Promise<boolean>;
70
}
71
```
72
73
**Usage Examples:**
74
75
```typescript
76
import { EditorSearchProvider } from "@jupyterlab/codemirror";
77
import { CodeEditor } from "@jupyterlab/codeeditor";
78
79
// Implement search provider for specific editor
80
class MyEditorSearchProvider extends EditorSearchProvider<CodeEditor.IModel> {
81
constructor(private _editor: CodeEditor.IEditor, private _model: CodeEditor.IModel) {
82
super();
83
}
84
85
protected get editor(): CodeEditor.IEditor | null {
86
return this._editor;
87
}
88
89
protected get model(): CodeEditor.IModel {
90
return this._model;
91
}
92
}
93
94
// Use search provider
95
const searchProvider = new MyEditorSearchProvider(editor, model);
96
97
// Start search query
98
await searchProvider.startQuery(/function\s+\w+/g);
99
100
// Navigate through matches
101
const nextMatch = await searchProvider.highlightNext();
102
const prevMatch = await searchProvider.highlightPrevious();
103
104
// Replace matches
105
await searchProvider.replaceCurrentMatch('const functionName =');
106
await searchProvider.replaceAllMatches('const replacement =');
107
108
// Listen for state changes
109
searchProvider.stateChanged.connect(() => {
110
console.log(`Found ${searchProvider.matchesCount} matches`);
111
console.log(`Current match: ${searchProvider.currentMatchIndex}`);
112
});
113
```
114
115
### CodeMirrorSearchHighlighter
116
117
Helper class for highlighting search matches in CodeMirror editors.
118
119
```typescript { .api }
120
/**
121
* Helper class to highlight texts in a CodeMirror editor
122
* Manages match decorations and navigation
123
*/
124
class CodeMirrorSearchHighlighter {
125
constructor(editor: CodeMirrorEditor | null);
126
127
/**
128
* Current index of the selected match (null if no selection)
129
*/
130
readonly currentIndex: number | null;
131
132
/**
133
* List of search matches
134
*/
135
matches: ISearchMatch[];
136
137
/**
138
* Whether cursor/selection should not be modified
139
*/
140
protectSelection: boolean;
141
142
// Highlighting management
143
clearHighlight(): void;
144
endQuery(): Promise<void>;
145
146
// Match navigation
147
highlightNext(options?: IHighlightAdjacentMatchOptions): Promise<ISearchMatch | undefined>;
148
highlightPrevious(options?: IHighlightAdjacentMatchOptions): Promise<ISearchMatch | undefined>;
149
150
// Editor management
151
setEditor(editor: CodeMirrorEditor): void;
152
}
153
```
154
155
**Usage Examples:**
156
157
```typescript
158
import { CodeMirrorSearchHighlighter, CodeMirrorEditor } from "@jupyterlab/codemirror";
159
160
// Create highlighter for editor
161
const highlighter = new CodeMirrorSearchHighlighter(editor as CodeMirrorEditor);
162
163
// Set matches to highlight
164
highlighter.matches = [
165
{ text: 'function', start: { line: 0, column: 0 }, end: { line: 0, column: 8 } },
166
{ text: 'function', start: { line: 5, column: 4 }, end: { line: 5, column: 12 } },
167
{ text: 'function', start: { line: 10, column: 0 }, end: { line: 10, column: 8 } }
168
];
169
170
// Navigate through matches
171
await highlighter.highlightNext(); // Highlights first match
172
await highlighter.highlightNext(); // Highlights second match
173
await highlighter.highlightPrevious(); // Back to first match
174
175
// Protect selection from modification
176
highlighter.protectSelection = true;
177
178
// Clear all highlights
179
highlighter.clearHighlight();
180
```
181
182
### Search Options and Configuration
183
184
Types and interfaces for configuring search behavior.
185
186
```typescript { .api }
187
/**
188
* Search start anchor defines from which position search should be executed
189
*/
190
type SearchStartAnchor = 'auto' | 'selection' | 'selection-start' | 'previous-match' | 'start';
191
192
/**
193
* Options for highlighting matches
194
*/
195
interface IHighlightMatchOptions {
196
/**
197
* Whether the highlighted match should be scrolled into view (default: true)
198
*/
199
scroll?: boolean;
200
201
/**
202
* Whether the user cursor should be moved to select the match (default: true)
203
*/
204
select?: boolean;
205
}
206
207
/**
208
* Options for highlighting adjacent matches
209
*/
210
interface IHighlightAdjacentMatchOptions extends IHighlightMatchOptions {
211
/**
212
* What should be used as anchor when searching for adjacent match (default: 'auto')
213
*/
214
from?: SearchStartAnchor;
215
}
216
217
/**
218
* Options for replace operations
219
*/
220
interface IReplaceOptions {
221
/**
222
* Whether to preserve case when replacing
223
*/
224
preserveCase?: boolean;
225
226
/**
227
* Whether to use regex replacement patterns
228
*/
229
useRegex?: boolean;
230
}
231
232
/**
233
* Search filter options
234
*/
235
interface IFilters {
236
/**
237
* Case sensitive search
238
*/
239
caseSensitive?: boolean;
240
241
/**
242
* Whole word matching
243
*/
244
wholeWord?: boolean;
245
246
/**
247
* Use regular expressions
248
*/
249
regex?: boolean;
250
}
251
```
252
253
### Advanced Search Features
254
255
Complex search scenarios and advanced usage patterns.
256
257
```typescript
258
// Case-sensitive regex search with word boundaries
259
const searchQuery = /\bclass\s+\w+\b/g;
260
const filters: IFilters = {
261
caseSensitive: true,
262
regex: true,
263
wholeWord: false
264
};
265
266
await searchProvider.startQuery(searchQuery, filters);
267
268
// Search within selection
269
const selection = {
270
start: { line: 10, column: 0 },
271
end: { line: 20, column: 0 }
272
};
273
await searchProvider.setSearchSelection(selection);
274
275
// Navigate with custom options
276
const nextMatch = await searchProvider.highlightNext(true, {
277
from: 'selection-start',
278
scroll: true,
279
select: false // Don't modify cursor position
280
});
281
282
// Replace with regex capture groups
283
await searchProvider.replaceCurrentMatch('interface $1', false, {
284
useRegex: true,
285
preserveCase: false
286
});
287
288
// Batch replace all matches
289
const replaceCount = await searchProvider.replaceAllMatches('interface $1', {
290
useRegex: true
291
});
292
console.log(`Replaced ${replaceCount} matches`);
293
```
294
295
### Custom Search Implementation
296
297
Implementing a custom search provider with additional features.
298
299
```typescript
300
class AdvancedSearchProvider extends EditorSearchProvider {
301
private _searchHistory: string[] = [];
302
private _replaceHistory: string[] = [];
303
304
constructor(
305
private _editor: CodeMirrorEditor,
306
private _model: CodeEditor.IModel
307
) {
308
super();
309
}
310
311
protected get editor(): CodeMirrorEditor {
312
return this._editor;
313
}
314
315
protected get model(): CodeEditor.IModel {
316
return this._model;
317
}
318
319
// Enhanced search with history
320
async startQueryWithHistory(query: RegExp | null, filters?: IFilters): Promise<void> {
321
if (query) {
322
const queryString = query.source;
323
this._searchHistory.unshift(queryString);
324
this._searchHistory = this._searchHistory.slice(0, 10); // Keep last 10
325
}
326
327
return this.startQuery(query, filters);
328
}
329
330
// Enhanced replace with history and validation
331
async replaceCurrentMatchWithValidation(
332
newText: string,
333
validator?: (text: string) => boolean
334
): Promise<boolean> {
335
const currentMatch = this.getCurrentMatch();
336
if (!currentMatch) return false;
337
338
// Validate replacement text
339
if (validator && !validator(newText)) {
340
throw new Error('Invalid replacement text');
341
}
342
343
// Add to history
344
this._replaceHistory.unshift(newText);
345
this._replaceHistory = this._replaceHistory.slice(0, 10);
346
347
return this.replaceCurrentMatch(newText);
348
}
349
350
// Search history accessors
351
getSearchHistory(): string[] {
352
return [...this._searchHistory];
353
}
354
355
getReplaceHistory(): string[] {
356
return [...this._replaceHistory];
357
}
358
359
// Search with preview
360
async previewReplace(newText: string): Promise<Array<{ match: ISearchMatch; preview: string }>> {
361
const matches = this.getAllMatches();
362
return matches.map(match => ({
363
match,
364
preview: this.generatePreview(match, newText)
365
}));
366
}
367
368
private generatePreview(match: ISearchMatch, replacement: string): string {
369
// Generate preview text showing before/after replacement
370
const beforeText = match.text;
371
const afterText = replacement;
372
return `${beforeText} → ${afterText}`;
373
}
374
375
private getAllMatches(): ISearchMatch[] {
376
// Implementation to get all current matches
377
return [];
378
}
379
}
380
```
381
382
### Search UI Integration
383
384
Integrating search functionality with user interface components.
385
386
```typescript
387
class SearchWidget {
388
private searchProvider: EditorSearchProvider;
389
private searchInput: HTMLInputElement;
390
private replaceInput: HTMLInputElement;
391
private matchCounter: HTMLElement;
392
393
constructor(searchProvider: EditorSearchProvider) {
394
this.searchProvider = searchProvider;
395
this.createUI();
396
this.connectSignals();
397
}
398
399
private createUI(): void {
400
// Create search input
401
this.searchInput = document.createElement('input');
402
this.searchInput.type = 'text';
403
this.searchInput.placeholder = 'Search...';
404
405
// Create replace input
406
this.replaceInput = document.createElement('input');
407
this.replaceInput.type = 'text';
408
this.replaceInput.placeholder = 'Replace...';
409
410
// Create match counter
411
this.matchCounter = document.createElement('span');
412
this.matchCounter.className = 'search-match-counter';
413
414
// Add event listeners
415
this.searchInput.addEventListener('input', () => this.onSearchInput());
416
this.searchInput.addEventListener('keydown', (e) => this.onSearchKeydown(e));
417
}
418
419
private connectSignals(): void {
420
this.searchProvider.stateChanged.connect(() => {
421
this.updateMatchCounter();
422
});
423
}
424
425
private async onSearchInput(): Promise<void> {
426
const query = this.searchInput.value;
427
if (query) {
428
const regex = new RegExp(query, 'gi');
429
await this.searchProvider.startQuery(regex);
430
} else {
431
await this.searchProvider.endQuery();
432
}
433
}
434
435
private async onSearchKeydown(event: KeyboardEvent): Promise<void> {
436
if (event.key === 'Enter') {
437
if (event.shiftKey) {
438
await this.searchProvider.highlightPrevious();
439
} else {
440
await this.searchProvider.highlightNext();
441
}
442
} else if (event.key === 'Escape') {
443
await this.searchProvider.endQuery();
444
}
445
}
446
447
private updateMatchCounter(): void {
448
const current = this.searchProvider.currentMatchIndex;
449
const total = this.searchProvider.matchesCount;
450
451
if (total === 0) {
452
this.matchCounter.textContent = 'No matches';
453
} else {
454
this.matchCounter.textContent = `${current + 1} of ${total}`;
455
}
456
}
457
458
async replaceNext(): Promise<void> {
459
const replaceText = this.replaceInput.value;
460
await this.searchProvider.replaceCurrentMatch(replaceText, true);
461
}
462
463
async replaceAll(): Promise<void> {
464
const replaceText = this.replaceInput.value;
465
const count = await this.searchProvider.replaceAllMatches(replaceText);
466
console.log(`Replaced ${count} matches`);
467
}
468
}
469
```
470
471
## Types
472
473
```typescript { .api }
474
interface ISearchMatch {
475
text: string;
476
start: CodeEditor.IPosition;
477
end: CodeEditor.IPosition;
478
}
479
480
interface IBaseSearchProvider extends IDisposable {
481
readonly currentMatchIndex: number | null;
482
readonly isActive: boolean;
483
readonly matchesCount: number;
484
readonly stateChanged: ISignal<IBaseSearchProvider, void>;
485
486
clearHighlight(): Promise<void>;
487
setIsActive(active: boolean): Promise<void>;
488
startQuery(query: RegExp | null, filters?: IFilters): Promise<void>;
489
endQuery(): Promise<void>;
490
highlightNext(loop?: boolean, options?: IHighlightAdjacentMatchOptions): Promise<ISearchMatch | undefined>;
491
highlightPrevious(loop?: boolean, options?: IHighlightAdjacentMatchOptions): Promise<ISearchMatch | undefined>;
492
replaceCurrentMatch(newText: string, loop?: boolean, options?: IReplaceOptions): Promise<boolean>;
493
replaceAllMatches(newText: string, options?: IReplaceOptions): Promise<boolean>;
494
}
495
496
type SearchStartAnchor = 'auto' | 'selection' | 'selection-start' | 'previous-match' | 'start';
497
498
interface IHighlightMatchOptions {
499
scroll?: boolean;
500
select?: boolean;
501
}
502
503
interface IHighlightAdjacentMatchOptions extends IHighlightMatchOptions {
504
from?: SearchStartAnchor;
505
}
506
507
interface IReplaceOptions {
508
preserveCase?: boolean;
509
useRegex?: boolean;
510
}
511
512
interface IFilters {
513
caseSensitive?: boolean;
514
wholeWord?: boolean;
515
regex?: boolean;
516
}
517
```