@input/pen-search 0.1.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/dist/index.mjs ADDED
@@ -0,0 +1,858 @@
1
+ // src/extension.ts
2
+ import { SEARCH_CONTROLLER_SLOT } from "@input/pen-types";
3
+ import {
4
+ createDecorationSet,
5
+ decorationsFacet,
6
+ defineExtension,
7
+ keyBindingPriorityToPrecedence,
8
+ keymapFacet,
9
+ searchControllerFacet
10
+ } from "@input/pen-core";
11
+
12
+ // src/controller.ts
13
+ import { announceEditorA11y } from "@input/pen-core";
14
+
15
+ // src/search.ts
16
+ import {
17
+ foldAndNormalize,
18
+ localeFacet,
19
+ nextGraphemeBoundary,
20
+ wordRangeAt
21
+ } from "@input/pen-core";
22
+ var SEARCH_QUERY_MAX_LENGTH = 1024;
23
+ var SEARCH_EXECUTION_BUDGET_MS = 50;
24
+ var SEARCH_REGEX_SEGMENT_MAX_CODE_UNITS = 64 * 1024;
25
+ var SEARCH_BUDGET_EXCEEDED_CODE = "search-budget-exceeded";
26
+ var SEARCH_INVALID_PATTERN_CODE = "search-invalid-pattern";
27
+ var DEFAULT_SEARCH_LOCALE = "en";
28
+ var DEFAULT_SEARCH_OPTIONS = {
29
+ caseSensitive: false,
30
+ regex: false,
31
+ wholeWord: false
32
+ };
33
+ function createInitialSearchState() {
34
+ return {
35
+ open: false,
36
+ query: "",
37
+ replaceText: "",
38
+ matches: [],
39
+ activeIndex: -1,
40
+ options: DEFAULT_SEARCH_OPTIONS
41
+ };
42
+ }
43
+ function findDocumentMatches(editor, query, options) {
44
+ if (!query) {
45
+ return [];
46
+ }
47
+ if (query.length > SEARCH_QUERY_MAX_LENGTH) {
48
+ emitSearchDiagnostic(
49
+ editor,
50
+ SEARCH_INVALID_PATTERN_CODE,
51
+ `Search query exceeds the ${SEARCH_QUERY_MAX_LENGTH}-character limit.`
52
+ );
53
+ return [];
54
+ }
55
+ const locale = resolveSearchLocale(editor, options);
56
+ const regex = options.regex ? buildSearchRegex(query, options) : null;
57
+ if (options.regex && !regex) {
58
+ emitSearchDiagnostic(
59
+ editor,
60
+ SEARCH_INVALID_PATTERN_CODE,
61
+ "Search pattern is invalid."
62
+ );
63
+ return [];
64
+ }
65
+ const matches = [];
66
+ const deadline = options.regex ? performance.now() + SEARCH_EXECUTION_BUDGET_MS : null;
67
+ const execution = {
68
+ query,
69
+ options,
70
+ locale,
71
+ regex,
72
+ deadline
73
+ };
74
+ for (const handle of editor.documentState.allBlocks()) {
75
+ if (hasExceededDeadline(deadline)) {
76
+ emitBudgetExceeded(editor);
77
+ return matches;
78
+ }
79
+ const blockResult = findMatchesInBlock(handle, execution, matches.length);
80
+ matches.push(...blockResult.matches);
81
+ if (blockResult.exceeded) {
82
+ emitBudgetExceeded(editor);
83
+ return matches;
84
+ }
85
+ }
86
+ return matches;
87
+ }
88
+ function buildSearchRegex(query, options) {
89
+ if (!query || query.length > SEARCH_QUERY_MAX_LENGTH) {
90
+ return null;
91
+ }
92
+ const pattern = options.regex ? query : escapeRegExp(query);
93
+ const flags = options.caseSensitive ? "gu" : "giu";
94
+ try {
95
+ return new RegExp(pattern, flags);
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+ function normalizeActiveIndex(activeIndex, matchCount) {
101
+ if (matchCount === 0) {
102
+ return -1;
103
+ }
104
+ if (activeIndex < 0) {
105
+ return 0;
106
+ }
107
+ if (activeIndex >= matchCount) {
108
+ return matchCount - 1;
109
+ }
110
+ return activeIndex;
111
+ }
112
+ function getNextActiveIndex(activeIndex, matchCount) {
113
+ if (matchCount === 0) {
114
+ return -1;
115
+ }
116
+ if (activeIndex < 0) {
117
+ return 0;
118
+ }
119
+ return (activeIndex + 1) % matchCount;
120
+ }
121
+ function getPreviousActiveIndex(activeIndex, matchCount) {
122
+ if (matchCount === 0) {
123
+ return -1;
124
+ }
125
+ if (activeIndex <= 0) {
126
+ return matchCount - 1;
127
+ }
128
+ return activeIndex - 1;
129
+ }
130
+ function buildReplaceOps(match, replaceText) {
131
+ if (!match) {
132
+ return [];
133
+ }
134
+ if (match.kind === "block") {
135
+ return [
136
+ {
137
+ type: "splice-text",
138
+ blockId: match.blockId,
139
+ from: match.from,
140
+ to: match.from + match.to - match.from,
141
+ insert: ""
142
+ },
143
+ {
144
+ type: "splice-text",
145
+ blockId: match.blockId,
146
+ from: match.from,
147
+ to: match.from,
148
+ insert: replaceText
149
+ }
150
+ ];
151
+ }
152
+ if (match.kind === "table-cell") {
153
+ return [
154
+ {
155
+ type: "splice-text",
156
+ blockId: match.blockId,
157
+ cell: { row: match.row ?? 0, col: match.col ?? 0 },
158
+ from: match.from,
159
+ to: match.to,
160
+ insert: ""
161
+ },
162
+ {
163
+ type: "splice-text",
164
+ blockId: match.blockId,
165
+ cell: { row: match.row ?? 0, col: match.col ?? 0 },
166
+ from: match.from,
167
+ to: match.from,
168
+ insert: replaceText
169
+ }
170
+ ];
171
+ }
172
+ return [];
173
+ }
174
+ function buildReplaceAllOps(matches, replaceText) {
175
+ const matchesByTarget = /* @__PURE__ */ new Map();
176
+ for (const match of matches) {
177
+ const targetMatches = matchesByTarget.get(getMatchTargetKey(match)) ?? [];
178
+ targetMatches.push(match);
179
+ matchesByTarget.set(getMatchTargetKey(match), targetMatches);
180
+ }
181
+ const ops = [];
182
+ for (const [, blockMatches] of matchesByTarget) {
183
+ const sortedMatches = [...blockMatches].sort((left, right) => {
184
+ return right.from - left.from;
185
+ });
186
+ const firstMatch = sortedMatches[0];
187
+ if (!firstMatch) {
188
+ continue;
189
+ }
190
+ for (const match of sortedMatches) {
191
+ if (match.kind === "block") {
192
+ ops.push(
193
+ {
194
+ type: "splice-text",
195
+ blockId: match.blockId,
196
+ from: match.from,
197
+ to: match.from + match.to - match.from,
198
+ insert: ""
199
+ },
200
+ {
201
+ type: "splice-text",
202
+ blockId: match.blockId,
203
+ from: match.from,
204
+ to: match.from,
205
+ insert: replaceText
206
+ }
207
+ );
208
+ continue;
209
+ }
210
+ ops.push(
211
+ {
212
+ type: "splice-text",
213
+ blockId: match.blockId,
214
+ cell: { row: match.row ?? 0, col: match.col ?? 0 },
215
+ from: match.from,
216
+ to: match.to,
217
+ insert: ""
218
+ },
219
+ {
220
+ type: "splice-text",
221
+ blockId: match.blockId,
222
+ cell: { row: match.row ?? 0, col: match.col ?? 0 },
223
+ from: match.from,
224
+ to: match.from,
225
+ insert: replaceText
226
+ }
227
+ );
228
+ }
229
+ }
230
+ return ops;
231
+ }
232
+ function revealActiveMatch(editor, match) {
233
+ if (!match) {
234
+ return;
235
+ }
236
+ if (match.kind === "block") {
237
+ editor.selectText(match.blockId, match.from, match.to);
238
+ } else {
239
+ const row = match.row ?? 0;
240
+ const col = match.col ?? 0;
241
+ editor.selectCellRange(
242
+ match.blockId,
243
+ { row, col },
244
+ { row, col }
245
+ );
246
+ }
247
+ editor.scrollToBlock?.(match.blockId);
248
+ }
249
+ function findMatchesInBlock(handle, execution, startIndex) {
250
+ const matches = [];
251
+ const text = handle.textContent();
252
+ if (text) {
253
+ const textResult = collectMatchesInText(
254
+ text,
255
+ execution,
256
+ startIndex,
257
+ (from, to, matchedText, index) => ({
258
+ kind: "block",
259
+ blockId: handle.id,
260
+ from,
261
+ to,
262
+ text: matchedText,
263
+ index
264
+ })
265
+ );
266
+ matches.push(...textResult.matches);
267
+ if (textResult.exceeded) {
268
+ return { matches, exceeded: true };
269
+ }
270
+ }
271
+ const tableResult = findMatchesInGridCells(
272
+ handle,
273
+ execution,
274
+ startIndex + matches.length
275
+ );
276
+ matches.push(...tableResult.matches);
277
+ if (tableResult.exceeded) {
278
+ return { matches, exceeded: true };
279
+ }
280
+ return { matches, exceeded: false };
281
+ }
282
+ function findMatchesInGridCells(handle, execution, startIndex) {
283
+ const table = handle.as("table");
284
+ if (!table) {
285
+ return { matches: [], exceeded: false };
286
+ }
287
+ const matches = [];
288
+ const rowCount = table.tableRowCount();
289
+ const columnCount = table.tableColumnCount();
290
+ for (let row = 0; row < rowCount; row += 1) {
291
+ for (let col = 0; col < columnCount; col += 1) {
292
+ const cell = table.tableCell(row, col);
293
+ const cellText = cell?.textContent() ?? "";
294
+ if (!cellText) {
295
+ continue;
296
+ }
297
+ const cellResult = collectMatchesInText(
298
+ cellText,
299
+ execution,
300
+ startIndex + matches.length,
301
+ (from, to, matchedText, index) => ({
302
+ kind: "table-cell",
303
+ blockId: handle.id,
304
+ row,
305
+ col,
306
+ from,
307
+ to,
308
+ text: matchedText,
309
+ index,
310
+ cellText
311
+ })
312
+ );
313
+ matches.push(...cellResult.matches);
314
+ if (cellResult.exceeded) {
315
+ return { matches, exceeded: true };
316
+ }
317
+ }
318
+ }
319
+ return { matches, exceeded: false };
320
+ }
321
+ function collectMatchesInText(text, execution, startIndex, createMatch) {
322
+ if (execution.regex) {
323
+ return collectSegmentedMatches(
324
+ text,
325
+ execution,
326
+ startIndex,
327
+ createMatch
328
+ );
329
+ }
330
+ return {
331
+ matches: collectLiteralMatches(text, execution, startIndex, createMatch),
332
+ exceeded: false
333
+ };
334
+ }
335
+ function collectSegmentedMatches(text, execution, startIndex, createMatch) {
336
+ const matches = [];
337
+ const regex = execution.regex;
338
+ if (!regex) {
339
+ return { matches, exceeded: false };
340
+ }
341
+ for (let segmentStart = 0; segmentStart < text.length; segmentStart += SEARCH_REGEX_SEGMENT_MAX_CODE_UNITS) {
342
+ if (hasExceededDeadline(execution.deadline)) {
343
+ return { matches, exceeded: true };
344
+ }
345
+ const segment = text.slice(
346
+ segmentStart,
347
+ segmentStart + SEARCH_REGEX_SEGMENT_MAX_CODE_UNITS
348
+ );
349
+ matches.push(
350
+ ...collectTextMatches(
351
+ text,
352
+ segment,
353
+ segmentStart,
354
+ regex,
355
+ execution,
356
+ startIndex + matches.length,
357
+ createMatch
358
+ )
359
+ );
360
+ }
361
+ return { matches, exceeded: false };
362
+ }
363
+ function collectTextMatches(text, segment, segmentStart, regex, execution, startIndex, createMatch) {
364
+ const matches = [];
365
+ const localRegex = new RegExp(regex.source, regex.flags);
366
+ let match;
367
+ while ((match = localRegex.exec(segment)) !== null) {
368
+ const from = segmentStart + match.index;
369
+ const to = from + match[0].length;
370
+ if (!execution.options.wholeWord || isWholeWordMatch(text, from, to, execution.locale)) {
371
+ matches.push(
372
+ createMatch(from, to, match[0], startIndex + matches.length)
373
+ );
374
+ }
375
+ if (!localRegex.global) {
376
+ break;
377
+ }
378
+ if (match[0].length === 0) {
379
+ localRegex.lastIndex += 1;
380
+ }
381
+ }
382
+ return matches;
383
+ }
384
+ function collectLiteralMatches(text, execution, startIndex, createMatch) {
385
+ const matches = [];
386
+ const ranges = execution.options.caseSensitive ? findExactRanges(text, execution.query) : findFoldedRanges(text, execution.query, execution.locale);
387
+ for (const range of ranges) {
388
+ if (execution.options.wholeWord && !isWholeWordMatch(text, range.from, range.to, execution.locale)) {
389
+ continue;
390
+ }
391
+ matches.push(
392
+ createMatch(
393
+ range.from,
394
+ range.to,
395
+ text.slice(range.from, range.to),
396
+ startIndex + matches.length
397
+ )
398
+ );
399
+ }
400
+ return matches;
401
+ }
402
+ function getMatchTargetKey(match) {
403
+ if (match.kind === "block") {
404
+ return `block:${match.blockId}`;
405
+ }
406
+ return `table:${match.blockId}:${match.row ?? -1}:${match.col ?? -1}`;
407
+ }
408
+ function resolveSearchLocale(editor, options) {
409
+ return options.locale ?? editor.facet(localeFacet);
410
+ }
411
+ function isWordSegmentBoundary(text, offset, locale) {
412
+ if (offset <= 0 || offset >= text.length) {
413
+ return true;
414
+ }
415
+ const range = wordRangeAt(text, offset, locale);
416
+ if (!range) {
417
+ return true;
418
+ }
419
+ return offset === range.start || offset === range.end;
420
+ }
421
+ function isWholeWordMatch(text, from, to, locale) {
422
+ return isWordSegmentBoundary(text, from, locale) && isWordSegmentBoundary(text, to, locale);
423
+ }
424
+ function findExactRanges(text, query) {
425
+ const ranges = [];
426
+ let searchFrom = 0;
427
+ while (searchFrom <= text.length - query.length) {
428
+ const index = text.indexOf(query, searchFrom);
429
+ if (index === -1) {
430
+ break;
431
+ }
432
+ ranges.push({ from: index, to: index + query.length });
433
+ searchFrom = index + query.length;
434
+ }
435
+ return ranges;
436
+ }
437
+ function findFoldedRanges(text, query, locale) {
438
+ const foldedQuery = foldAndNormalize(query, locale);
439
+ if (!foldedQuery) {
440
+ return [];
441
+ }
442
+ const { folded, originAt } = foldTextWithOriginMap(text, locale);
443
+ const ranges = [];
444
+ let searchFrom = 0;
445
+ while (searchFrom <= folded.length - foldedQuery.length) {
446
+ const index = folded.indexOf(foldedQuery, searchFrom);
447
+ if (index === -1) {
448
+ break;
449
+ }
450
+ const from = originAt[index] ?? 0;
451
+ const to = originAt[index + foldedQuery.length] ?? text.length;
452
+ if (to > from) {
453
+ ranges.push({ from, to });
454
+ searchFrom = index + foldedQuery.length;
455
+ continue;
456
+ }
457
+ searchFrom = index + 1;
458
+ }
459
+ return ranges;
460
+ }
461
+ function foldTextWithOriginMap(text, locale) {
462
+ const foldedWhole = foldAndNormalize(text, locale);
463
+ if (foldedWhole.length === text.length) {
464
+ const originAt2 = new Array(foldedWhole.length + 1);
465
+ for (let index = 0; index <= foldedWhole.length; index += 1) {
466
+ originAt2[index] = index;
467
+ }
468
+ return { folded: foldedWhole, originAt: originAt2 };
469
+ }
470
+ let folded = "";
471
+ const originAt = [];
472
+ let offset = 0;
473
+ while (offset < text.length) {
474
+ const next = nextGraphemeBoundary(text, offset, locale);
475
+ const piece = foldAndNormalize(text.slice(offset, next), locale);
476
+ for (let index = 0; index < piece.length; index += 1) {
477
+ originAt.push(offset);
478
+ }
479
+ folded += piece;
480
+ offset = next;
481
+ }
482
+ originAt.push(text.length);
483
+ return { folded, originAt };
484
+ }
485
+ function escapeRegExp(value) {
486
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
487
+ }
488
+ function hasExceededDeadline(deadline) {
489
+ return deadline !== null && performance.now() >= deadline;
490
+ }
491
+ function emitBudgetExceeded(editor) {
492
+ emitSearchDiagnostic(
493
+ editor,
494
+ SEARCH_BUDGET_EXCEEDED_CODE,
495
+ "Search stopped after the execution budget was exceeded."
496
+ );
497
+ }
498
+ function emitSearchDiagnostic(editor, code, message) {
499
+ const event = {
500
+ code,
501
+ level: "warn",
502
+ source: "search",
503
+ extension: "search",
504
+ message
505
+ };
506
+ editor.internals.emit("diagnostic", event);
507
+ }
508
+
509
+ // src/controller.ts
510
+ var SearchControllerImpl = class {
511
+ editor;
512
+ state;
513
+ listeners = /* @__PURE__ */ new Set();
514
+ constructor(editor) {
515
+ this.editor = editor;
516
+ this.state = createInitialSearchState();
517
+ }
518
+ getState() {
519
+ return this.state;
520
+ }
521
+ subscribe(listener) {
522
+ this.listeners.add(listener);
523
+ return () => {
524
+ this.listeners.delete(listener);
525
+ };
526
+ }
527
+ open() {
528
+ this.updateState({
529
+ ...this.state,
530
+ open: true
531
+ });
532
+ }
533
+ close() {
534
+ this.updateState({
535
+ ...this.state,
536
+ open: false
537
+ });
538
+ }
539
+ toggleOpen() {
540
+ this.updateState({
541
+ ...this.state,
542
+ open: !this.state.open
543
+ });
544
+ }
545
+ setQuery(query) {
546
+ const nextState = {
547
+ ...this.state,
548
+ query,
549
+ activeIndex: query ? this.state.activeIndex : -1
550
+ };
551
+ if (!this.updateState(nextState)) {
552
+ return;
553
+ }
554
+ this.recompute();
555
+ }
556
+ setReplaceText(replaceText) {
557
+ this.updateState({
558
+ ...this.state,
559
+ replaceText
560
+ });
561
+ }
562
+ setOptions(options) {
563
+ const nextState = {
564
+ ...this.state,
565
+ options: {
566
+ ...this.state.options,
567
+ ...options
568
+ }
569
+ };
570
+ if (!this.updateState(nextState)) {
571
+ return;
572
+ }
573
+ this.recompute();
574
+ }
575
+ next() {
576
+ const nextIndex = getNextActiveIndex(
577
+ this.state.activeIndex,
578
+ this.state.matches.length
579
+ );
580
+ if (nextIndex === this.state.activeIndex && this.state.matches.length === 0) {
581
+ return;
582
+ }
583
+ this.state = {
584
+ ...this.state,
585
+ activeIndex: nextIndex
586
+ };
587
+ revealActiveMatch(this.editor, this.getActiveMatch());
588
+ this.notify();
589
+ }
590
+ previous() {
591
+ const previousIndex = getPreviousActiveIndex(
592
+ this.state.activeIndex,
593
+ this.state.matches.length
594
+ );
595
+ if (previousIndex === this.state.activeIndex && this.state.matches.length === 0) {
596
+ return;
597
+ }
598
+ this.state = {
599
+ ...this.state,
600
+ activeIndex: previousIndex
601
+ };
602
+ revealActiveMatch(this.editor, this.getActiveMatch());
603
+ this.notify();
604
+ }
605
+ replace() {
606
+ const activeMatch = this.getActiveMatch();
607
+ const ops = buildReplaceOps(activeMatch, this.state.replaceText);
608
+ if (ops.length === 0) {
609
+ return;
610
+ }
611
+ this.editor.apply(ops, {
612
+ origin: "user",
613
+ undoGroup: true
614
+ });
615
+ this.recompute();
616
+ revealActiveMatch(this.editor, this.getActiveMatch());
617
+ }
618
+ replaceAll() {
619
+ const ops = buildReplaceAllOps(this.state.matches, this.state.replaceText);
620
+ if (ops.length === 0) {
621
+ return;
622
+ }
623
+ this.editor.apply(ops, {
624
+ origin: "user",
625
+ undoGroup: true
626
+ });
627
+ this.recompute();
628
+ }
629
+ recompute() {
630
+ const previousCount = this.state.matches.length;
631
+ const matches = findDocumentMatches(
632
+ this.editor,
633
+ this.state.query,
634
+ this.state.options
635
+ );
636
+ this.updateState({
637
+ ...this.state,
638
+ matches,
639
+ activeIndex: normalizeActiveIndex(this.state.activeIndex, matches.length)
640
+ });
641
+ if (this.state.query.length > 0 && matches.length !== previousCount) {
642
+ announceEditorA11y(this.editor, "findMatches", {
643
+ count: matches.length
644
+ });
645
+ }
646
+ }
647
+ getActiveMatch() {
648
+ return this.state.matches[this.state.activeIndex] ?? null;
649
+ }
650
+ notify() {
651
+ for (const listener of this.listeners) {
652
+ listener();
653
+ }
654
+ }
655
+ updateState(nextState) {
656
+ if (searchStatesEqual(this.state, nextState)) {
657
+ return false;
658
+ }
659
+ this.state = nextState;
660
+ this.notify();
661
+ return true;
662
+ }
663
+ };
664
+ function searchStatesEqual(left, right) {
665
+ return left.open === right.open && left.query === right.query && left.replaceText === right.replaceText && left.activeIndex === right.activeIndex && left.options.caseSensitive === right.options.caseSensitive && left.options.regex === right.options.regex && left.options.wholeWord === right.options.wholeWord && left.options.locale === right.options.locale && searchMatchesEqual(left.matches, right.matches);
666
+ }
667
+ function searchMatchesEqual(left, right) {
668
+ if (left.length !== right.length) {
669
+ return false;
670
+ }
671
+ for (let index = 0; index < left.length; index += 1) {
672
+ const leftMatch = left[index];
673
+ const rightMatch = right[index];
674
+ if (leftMatch?.kind !== rightMatch?.kind || leftMatch?.blockId !== rightMatch?.blockId || leftMatch?.row !== rightMatch?.row || leftMatch?.col !== rightMatch?.col || leftMatch?.cellText !== rightMatch?.cellText || leftMatch?.from !== rightMatch?.from || leftMatch?.to !== rightMatch?.to || leftMatch?.text !== rightMatch?.text || leftMatch?.index !== rightMatch?.index) {
675
+ return false;
676
+ }
677
+ }
678
+ return true;
679
+ }
680
+
681
+ // src/decorations.ts
682
+ function buildSearchDecorations(state) {
683
+ if (!state.open || state.matches.length === 0) {
684
+ return [];
685
+ }
686
+ return state.matches.flatMap((match, index) => {
687
+ if (match.kind !== "block") {
688
+ return [];
689
+ }
690
+ const isActive = index === state.activeIndex;
691
+ return [{
692
+ type: "inline",
693
+ blockId: match.blockId,
694
+ from: match.from,
695
+ to: match.to,
696
+ attributes: {
697
+ class: isActive ? "pen-search-match pen-search-match-active" : "pen-search-match",
698
+ "data-pen-search-match": "",
699
+ "data-search-match-index": String(match.index),
700
+ "data-search-match-active": String(isActive)
701
+ }
702
+ }];
703
+ });
704
+ }
705
+
706
+ // src/extension.ts
707
+ var SEARCH_EXTENSION_NAME = "search";
708
+ var SEARCH_KEY_BINDINGS = [
709
+ {
710
+ key: "Mod-f",
711
+ description: "Open search",
712
+ handler: (editor, event) => {
713
+ const controller = getSearchController(editor);
714
+ if (!controller) {
715
+ return false;
716
+ }
717
+ event.preventDefault();
718
+ controller.open();
719
+ return true;
720
+ }
721
+ },
722
+ {
723
+ key: "Mod-g",
724
+ description: "Next search match",
725
+ handler: (editor, event) => {
726
+ const controller = getSearchController(editor);
727
+ const state = controller?.getState();
728
+ if (!controller || !state?.open || state.query.length === 0) {
729
+ return false;
730
+ }
731
+ event.preventDefault();
732
+ controller.next();
733
+ return true;
734
+ }
735
+ },
736
+ {
737
+ key: "Shift-Mod-g",
738
+ description: "Previous search match",
739
+ handler: (editor, event) => {
740
+ const controller = getSearchController(editor);
741
+ const state = controller?.getState();
742
+ if (!controller || !state?.open || state.query.length === 0) {
743
+ return false;
744
+ }
745
+ event.preventDefault();
746
+ controller.previous();
747
+ return true;
748
+ }
749
+ },
750
+ {
751
+ key: "Enter",
752
+ description: "Next search match",
753
+ handler: (editor, event) => {
754
+ const controller = getSearchController(editor);
755
+ const state = controller?.getState();
756
+ if (!controller || !state?.open || state.query.length === 0) {
757
+ return false;
758
+ }
759
+ event.preventDefault();
760
+ controller.next();
761
+ return true;
762
+ }
763
+ },
764
+ {
765
+ key: "Shift-Enter",
766
+ description: "Previous search match",
767
+ handler: (editor, event) => {
768
+ const controller = getSearchController(editor);
769
+ const state = controller?.getState();
770
+ if (!controller || !state?.open || state.query.length === 0) {
771
+ return false;
772
+ }
773
+ event.preventDefault();
774
+ controller.previous();
775
+ return true;
776
+ }
777
+ },
778
+ {
779
+ key: "Escape",
780
+ description: "Close search",
781
+ handler: (editor, event) => {
782
+ const controller = getSearchController(editor);
783
+ const state = controller?.getState();
784
+ if (!controller || !state?.open) {
785
+ return false;
786
+ }
787
+ event.preventDefault();
788
+ controller.close();
789
+ return true;
790
+ }
791
+ }
792
+ ];
793
+ function searchExtension() {
794
+ let activeEditor = null;
795
+ let controller = null;
796
+ let unsubscribeCommit = null;
797
+ let unsubscribeController = null;
798
+ return defineExtension({
799
+ name: SEARCH_EXTENSION_NAME,
800
+ facets: [
801
+ ...searchKeymapProviders(SEARCH_KEY_BINDINGS),
802
+ decorationsFacet.of(() => {
803
+ const state = controller?.getState();
804
+ if (!state || state.matches.length === 0) {
805
+ return createDecorationSet([]);
806
+ }
807
+ return createDecorationSet(buildSearchDecorations(state));
808
+ })
809
+ ],
810
+ activateClient: async ({ editor }) => {
811
+ activeEditor = editor;
812
+ controller = new SearchControllerImpl(editor);
813
+ editor.internals.assignSlot(SEARCH_CONTROLLER_SLOT, controller);
814
+ unsubscribeCommit = editor.on("commit", () => {
815
+ controller?.recompute();
816
+ });
817
+ unsubscribeController = controller.subscribe(() => {
818
+ activeEditor?.requestDecorationUpdate();
819
+ });
820
+ },
821
+ deactivateClient: async () => {
822
+ unsubscribeCommit?.();
823
+ unsubscribeCommit = null;
824
+ unsubscribeController?.();
825
+ unsubscribeController = null;
826
+ activeEditor?.internals.assignSlot(SEARCH_CONTROLLER_SLOT, null);
827
+ controller = null;
828
+ activeEditor = null;
829
+ }
830
+ });
831
+ }
832
+ function getSearchController(editor) {
833
+ return editor.facet(searchControllerFacet) ?? null;
834
+ }
835
+ function searchKeymapProviders(bindings) {
836
+ return bindings.map(
837
+ (binding) => keymapFacet.of(
838
+ [binding],
839
+ keyBindingPriorityToPrecedence(binding.priority ?? 300)
840
+ )
841
+ );
842
+ }
843
+ export {
844
+ DEFAULT_SEARCH_LOCALE,
845
+ DEFAULT_SEARCH_OPTIONS,
846
+ SEARCH_BUDGET_EXCEEDED_CODE,
847
+ SEARCH_EXECUTION_BUDGET_MS,
848
+ SEARCH_EXTENSION_NAME,
849
+ SEARCH_INVALID_PATTERN_CODE,
850
+ SEARCH_QUERY_MAX_LENGTH,
851
+ SEARCH_REGEX_SEGMENT_MAX_CODE_UNITS,
852
+ buildReplaceAllOps,
853
+ buildReplaceOps,
854
+ buildSearchRegex,
855
+ findDocumentMatches,
856
+ getSearchController,
857
+ searchExtension
858
+ };