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