or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

editor-commands.mdeditor-core.mdeditor-factory.mdextension-system.mdindex.mdlanguage-support.mdmime-type-service.mdsearch-replace.mdspecial-extensions.mdtheme-system.md
tile.json

search-replace.mddocs/

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

```