@singapore-editor/find 0.1.1

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.
@@ -0,0 +1,681 @@
1
+ import { FIND_MATCHES_LIMIT, FIND_REPLACE_ALL_LIMIT, countMatches, escapeRegExpCharacters, findLineRange, findMatchIndex, findMatches, findNextMatchFrom, findPreviousMatchFrom, findTextSourceFromSnapshot } from "./search.js";
2
+ import { ReplacePattern, parseReplaceString } from "./replacePattern.js";
3
+ import { wordRangeAtOffset } from "@singapore-editor/core/document";
4
+ import { EditorSecondaryViewScheduler } from "@singapore-editor/core/secondary-views";
5
+ //#region src/findController.ts
6
+ var FIND_HIGHLIGHT_Z_INDEX = {
7
+ scope: 4,
8
+ match: 5,
9
+ current: 6
10
+ };
11
+ var FIND_MATCH_STYLE = {
12
+ dimmable: false,
13
+ backgroundColor: "rgba(234, 179, 8, 0.34)",
14
+ zIndex: FIND_HIGHLIGHT_Z_INDEX.match
15
+ };
16
+ var FIND_CURRENT_STYLE = {
17
+ dimmable: false,
18
+ backgroundColor: "rgba(245, 158, 11, 0.72)",
19
+ color: "#111827",
20
+ zIndex: FIND_HIGHLIGHT_Z_INDEX.current
21
+ };
22
+ var FIND_SCOPE_STYLE = {
23
+ dimmable: false,
24
+ backgroundColor: "rgba(59, 130, 246, 0.22)",
25
+ zIndex: FIND_HIGHLIGHT_Z_INDEX.scope
26
+ };
27
+ function findReveal(match, committed = true) {
28
+ return {
29
+ ...committed ? { jumpCause: "find" } : {},
30
+ revealOffset: match.end,
31
+ revealBlock: "center-if-outside"
32
+ };
33
+ }
34
+ var SEARCH_STRING_MAX_LENGTH = 524288;
35
+ var FIND_RESEARCH_KEY = "find.research";
36
+ var FIND_RESEARCH_DELAY_MS = 100;
37
+ var FIND_RESEARCH_MAX_DELAY_MS = 400;
38
+ var EditorFindController = class {
39
+ options;
40
+ listeners = /* @__PURE__ */ new Set();
41
+ host = null;
42
+ editHost = null;
43
+ matchHighlightName = "";
44
+ currentHighlightName = "";
45
+ scopeHighlightName = "";
46
+ state = {
47
+ searchString: "",
48
+ replaceString: "",
49
+ isRegex: false,
50
+ matchCase: false,
51
+ wholeWord: false,
52
+ preserveCase: false,
53
+ revealed: false,
54
+ replaceRevealed: false,
55
+ inSelection: false
56
+ };
57
+ scheduler = new EditorSecondaryViewScheduler();
58
+ matches = [];
59
+ highlightsTruncated = false;
60
+ totalMatches = 0;
61
+ scope = null;
62
+ trackedMatches = null;
63
+ currentMatch = null;
64
+ replacing = false;
65
+ pendingResearch = false;
66
+ constructor(options = {}) {
67
+ this.options = resolveFindOptions(options);
68
+ }
69
+ attachHost(host, highlightPrefix) {
70
+ if (this.host && this.host !== host) this.clearHighlights();
71
+ this.host = host;
72
+ this.matchHighlightName = `${highlightPrefix}-find-match`;
73
+ this.currentHighlightName = `${highlightPrefix}-find-current`;
74
+ this.scopeHighlightName = `${highlightPrefix}-find-scope`;
75
+ return { dispose: () => this.detachHost(host) };
76
+ }
77
+ attachEditHost(host) {
78
+ this.editHost = host;
79
+ return { dispose: () => this.detachEditHost(host) };
80
+ }
81
+ subscribe(listener) {
82
+ this.listeners.add(listener);
83
+ return { dispose: () => this.listeners.delete(listener) };
84
+ }
85
+ dispose() {
86
+ this.pendingResearch = false;
87
+ this.scheduler.dispose();
88
+ this.clearHighlights();
89
+ this.listeners.clear();
90
+ this.host = null;
91
+ this.editHost = null;
92
+ }
93
+ isVisible() {
94
+ return this.state.revealed;
95
+ }
96
+ openFind() {
97
+ return this.open({
98
+ replace: false,
99
+ focus: "find"
100
+ });
101
+ }
102
+ toggleFind() {
103
+ if (this.state.revealed) return this.close();
104
+ return this.openFind();
105
+ }
106
+ openFindReplace() {
107
+ return this.open({
108
+ replace: true,
109
+ focus: "replace"
110
+ });
111
+ }
112
+ close() {
113
+ const host = this.host;
114
+ if (!this.state.revealed || !host) return false;
115
+ this.pendingResearch = false;
116
+ this.state = {
117
+ ...this.state,
118
+ revealed: false,
119
+ inSelection: false
120
+ };
121
+ this.scope = null;
122
+ this.trackedMatches = null;
123
+ this.currentMatch = null;
124
+ this.scheduler.cancel(FIND_RESEARCH_KEY);
125
+ this.clearHighlights();
126
+ this.emit({ type: "hide" });
127
+ host.focusEditor();
128
+ return true;
129
+ }
130
+ findNext() {
131
+ if (!this.ensureFindReady("none")) return false;
132
+ const startOffset = this.primarySelection()?.endOffset ?? 0;
133
+ const match = this.nextMatchAt(startOffset, true);
134
+ if (match && match.start < startOffset) this.host?.announce("Wrapped to the first match");
135
+ return this.selectMatch(match);
136
+ }
137
+ findPrevious() {
138
+ if (!this.ensureFindReady("none")) return false;
139
+ const startOffset = this.primarySelection()?.startOffset ?? 0;
140
+ const match = this.searchFrom(findPreviousMatchFrom, startOffset, { escapeEmptyMatchAtOffset: true });
141
+ if (match && match.start > startOffset) this.host?.announce("Wrapped to the last match");
142
+ return this.selectMatch(match);
143
+ }
144
+ replaceOne() {
145
+ if (!this.ensureFindReady("replace")) return false;
146
+ if (!this.editHost) return false;
147
+ const selection = this.primarySelection();
148
+ if (!selection) return false;
149
+ const search = this.commandSearchTarget();
150
+ if (!search) return false;
151
+ const match = findNextMatchFrom(search.source, this.state, selection.startOffset, search.scopes, {
152
+ captureMatches: true,
153
+ loop: this.options.loop
154
+ });
155
+ if (!match) return false;
156
+ if (match.start !== selection.startOffset || match.end !== selection.endOffset) return this.selectMatch(match);
157
+ const replaceText = this.replacePattern().buildReplaceString(match.matches, this.state.preserveCase);
158
+ this.applyReplacement([{
159
+ from: match.start,
160
+ to: match.end,
161
+ text: replaceText
162
+ }], "input.findReplaceOne", {
163
+ anchor: match.start + replaceText.length,
164
+ head: match.start + replaceText.length
165
+ });
166
+ this.selectNextReplacementMatch();
167
+ this.research(false);
168
+ return true;
169
+ }
170
+ replaceAll() {
171
+ if (!this.ensureFindReady("replace")) return false;
172
+ if (!this.editHost) return false;
173
+ const search = this.commandSearchTarget();
174
+ if (!search) return false;
175
+ const pattern = this.replacePattern();
176
+ const matches = findMatches(search.source, this.state, search.scopes, pattern.hasReplacementPatterns || this.state.preserveCase, FIND_REPLACE_ALL_LIMIT);
177
+ if (matches.length === 0) return false;
178
+ const edits = mergeAdjacentReplaceEdits(matches.map((match) => ({
179
+ from: match.start,
180
+ to: match.end,
181
+ text: pattern.buildReplaceString(match.matches, this.state.preserveCase)
182
+ })));
183
+ this.applyReplacement(edits, "input.findReplaceAll");
184
+ this.research(false);
185
+ return true;
186
+ }
187
+ selectAllMatches() {
188
+ const host = this.host;
189
+ if (!host) return false;
190
+ if (!this.ensureFindReady("none")) return false;
191
+ const search = this.commandSearchTarget();
192
+ if (!search) return false;
193
+ const matches = findMatches(search.source, this.state, search.scopes, false, FIND_REPLACE_ALL_LIMIT);
194
+ if (matches.length === 0) return false;
195
+ const selections = orderedMatchSelections(matches, this.primarySelection());
196
+ host.setSelections(selections, "input.findSelectAll", selections[0]?.head);
197
+ return true;
198
+ }
199
+ toggleMatchCase() {
200
+ this.state = {
201
+ ...this.state,
202
+ matchCase: !this.state.matchCase
203
+ };
204
+ this.research(this.options.cursorMoveOnType);
205
+ return true;
206
+ }
207
+ toggleWholeWord() {
208
+ this.state = {
209
+ ...this.state,
210
+ wholeWord: !this.state.wholeWord
211
+ };
212
+ this.research(this.options.cursorMoveOnType);
213
+ return true;
214
+ }
215
+ toggleRegex() {
216
+ this.state = {
217
+ ...this.state,
218
+ isRegex: !this.state.isRegex
219
+ };
220
+ this.research(this.options.cursorMoveOnType);
221
+ return true;
222
+ }
223
+ togglePreserveCase() {
224
+ this.state = {
225
+ ...this.state,
226
+ preserveCase: !this.state.preserveCase
227
+ };
228
+ this.updateWidget();
229
+ return true;
230
+ }
231
+ toggleReplace() {
232
+ this.state = {
233
+ ...this.state,
234
+ replaceRevealed: !this.state.replaceRevealed
235
+ };
236
+ this.updateWidget();
237
+ return true;
238
+ }
239
+ toggleFindInSelection() {
240
+ if (this.state.inSelection) {
241
+ this.state = {
242
+ ...this.state,
243
+ inSelection: false
244
+ };
245
+ this.scope = null;
246
+ this.research(this.options.cursorMoveOnType);
247
+ return true;
248
+ }
249
+ const host = this.host;
250
+ if (!host) return false;
251
+ const scopes = nonEmptySelectionRanges(this.commandSelections());
252
+ if (scopes.length === 0) return false;
253
+ this.state = {
254
+ ...this.state,
255
+ inSelection: true
256
+ };
257
+ this.scope = host.trackRanges(scopes);
258
+ this.research(this.options.cursorMoveOnType);
259
+ return true;
260
+ }
261
+ setSearchString(value) {
262
+ this.state = {
263
+ ...this.state,
264
+ searchString: value
265
+ };
266
+ if (this.options.findOnType) this.research(this.options.cursorMoveOnType);
267
+ }
268
+ setReplaceString(value) {
269
+ this.state = {
270
+ ...this.state,
271
+ replaceString: value
272
+ };
273
+ this.updateWidget();
274
+ }
275
+ handleViewUpdate(kind, change) {
276
+ if (!this.state.revealed) return;
277
+ if (!isFindDocumentUpdate(kind)) return;
278
+ if (change?.kind === "selection" || change?.kind === "none") return;
279
+ if (kind === "clear") {
280
+ this.close();
281
+ return;
282
+ }
283
+ if (this.replacing) return;
284
+ if (kind === "document") {
285
+ this.pendingResearch = false;
286
+ this.scope = null;
287
+ this.state = {
288
+ ...this.state,
289
+ inSelection: false
290
+ };
291
+ this.research(false);
292
+ return;
293
+ }
294
+ if (this.pendingResearch) {
295
+ this.research(false);
296
+ return;
297
+ }
298
+ this.scheduleResearch();
299
+ }
300
+ detachHost(host) {
301
+ if (this.host !== host) return;
302
+ this.pendingResearch = false;
303
+ this.clearHighlights();
304
+ this.host = null;
305
+ this.matchHighlightName = "";
306
+ this.currentHighlightName = "";
307
+ this.scopeHighlightName = "";
308
+ this.state = {
309
+ ...this.state,
310
+ revealed: false,
311
+ inSelection: false
312
+ };
313
+ this.matches = [];
314
+ this.highlightsTruncated = false;
315
+ this.totalMatches = 0;
316
+ this.scope = null;
317
+ this.trackedMatches = null;
318
+ this.currentMatch = null;
319
+ this.scheduler.cancel(FIND_RESEARCH_KEY);
320
+ }
321
+ detachEditHost(host) {
322
+ if (this.editHost !== host) return;
323
+ this.pendingResearch = false;
324
+ this.editHost = null;
325
+ }
326
+ open(options) {
327
+ const host = this.host;
328
+ if (!host || !host.hasDocument()) return false;
329
+ const searchString = this.seedSearchString();
330
+ this.state = {
331
+ ...this.state,
332
+ searchString: searchString || this.state.searchString,
333
+ revealed: true,
334
+ replaceRevealed: options.replace || this.state.replaceRevealed
335
+ };
336
+ this.applyAutoFindInSelection(host);
337
+ this.emit({
338
+ type: "show",
339
+ replaceVisible: this.state.replaceRevealed
340
+ });
341
+ this.research(false);
342
+ this.focusWidget(options.focus);
343
+ return true;
344
+ }
345
+ ensureFindReady(focus) {
346
+ if (!this.state.revealed) return this.open({
347
+ replace: focus === "replace",
348
+ focus
349
+ });
350
+ if (this.matches.length === 0) this.research(false);
351
+ return this.state.searchString.length > 0;
352
+ }
353
+ research(moveCursor) {
354
+ if (!this.host) return;
355
+ if (!this.canPaintCurrentDocument()) {
356
+ this.pendingResearch = true;
357
+ if (moveCursor) this.selectFirstMatchFromSelection();
358
+ return;
359
+ }
360
+ this.pendingResearch = false;
361
+ this.scheduler.cancel(FIND_RESEARCH_KEY);
362
+ const found = this.findAll(false, FIND_MATCHES_LIMIT + 1);
363
+ this.highlightsTruncated = found.length > FIND_MATCHES_LIMIT;
364
+ this.totalMatches = this.highlightsTruncated ? this.countAll() : found.length;
365
+ this.adoptMatches(this.highlightsTruncated ? found.slice(0, FIND_MATCHES_LIMIT) : found);
366
+ this.currentMatch = matchAtSelection(this.matches, this.primarySelection());
367
+ this.updateHighlights();
368
+ this.updateWidget();
369
+ if (moveCursor && found.length > 0) this.selectFirstMatchFromSelection();
370
+ }
371
+ scheduleResearch() {
372
+ this.followPendingMatches();
373
+ this.scheduler.schedule({
374
+ key: FIND_RESEARCH_KEY,
375
+ taskClass: "background-derived",
376
+ delayMs: FIND_RESEARCH_DELAY_MS,
377
+ maxDelayMs: FIND_RESEARCH_MAX_DELAY_MS,
378
+ run: () => this.research(false)
379
+ });
380
+ }
381
+ /**
382
+ * Adopts a match set and hands it to the host to follow as far as it is painted.
383
+ *
384
+ * Handed over now rather than when an edit arrives: find is told about an edit
385
+ * once the text has already moved, and a range recorded against the document
386
+ * as it is by then is a range that will never shift.
387
+ *
388
+ * What the reader can see is the bound. A set stopping at the paint cap holds
389
+ * twenty thousand matches, and asking the host to carry all of them costs more
390
+ * per keystroke than the search this deferral exists to keep off it.
391
+ */
392
+ adoptMatches(matches) {
393
+ this.matches = matches;
394
+ this.trackedMatches = this.host?.trackPaintedRanges(matches) ?? null;
395
+ }
396
+ /**
397
+ * Carries the painted matches through the edit that deferred their re-search.
398
+ *
399
+ * Until it lands they are what the reader sees, and the offsets they were found
400
+ * at address text that has since moved — so they are read back from where that
401
+ * text went. A match whose text is gone resolves to nothing and stops being
402
+ * painted.
403
+ */
404
+ /** Takes the offsets again from what the host is following, after the line it follows to moved. */
405
+ refreshTrackedMatches() {
406
+ this.followPendingMatches();
407
+ }
408
+ followPendingMatches() {
409
+ if (!this.canPaintCurrentDocument()) return;
410
+ const tracked = this.trackedMatches;
411
+ if (!tracked || this.matches.length === 0) return;
412
+ this.matches = tracked.resolve().map((range) => ({
413
+ ...range,
414
+ matches: null
415
+ }));
416
+ this.currentMatch = matchAtSelection(this.matches, this.primarySelection());
417
+ this.updateHighlights();
418
+ this.updateWidget();
419
+ }
420
+ applyReplacement(edits, timingName, selection) {
421
+ const editHost = this.editHost;
422
+ if (!editHost) return;
423
+ this.replacing = true;
424
+ try {
425
+ editHost.applyEdits(edits, timingName, selection);
426
+ } finally {
427
+ this.replacing = false;
428
+ }
429
+ }
430
+ selectNextReplacementMatch() {
431
+ const offset = this.primarySelection()?.endOffset ?? 0;
432
+ const match = this.searchFrom(findNextMatchFrom, offset, {});
433
+ if (!match) return;
434
+ this.host?.setSelection(match.start, match.end, "input.findNavigate", findReveal(match));
435
+ }
436
+ canPaintCurrentDocument() {
437
+ if (!this.editHost) return true;
438
+ const snapshot = this.editHost.textSnapshot();
439
+ return snapshot !== null && this.host?.hasTextSnapshot(snapshot) === true;
440
+ }
441
+ commandTextSource() {
442
+ if (!this.editHost) return this.host?.textSource();
443
+ const snapshot = this.editHost.textSnapshot();
444
+ return snapshot ? findTextSourceFromSnapshot(snapshot) : void 0;
445
+ }
446
+ commandSelections() {
447
+ return this.editHost?.getSelections() ?? this.host?.getSelections() ?? [];
448
+ }
449
+ commandSearchTarget() {
450
+ const source = this.commandTextSource();
451
+ return source ? this.searchTarget(source) : null;
452
+ }
453
+ findAll(captureMatches, limit = FIND_MATCHES_LIMIT) {
454
+ const search = this.searchTarget();
455
+ if (!search) return [];
456
+ return findMatches(search.source, this.state, search.scopes, captureMatches, limit);
457
+ }
458
+ countAll(before) {
459
+ const search = this.searchTarget();
460
+ return search ? countMatches(search.source, this.state, search.scopes, before) : 0;
461
+ }
462
+ searchFrom(find, offset, options) {
463
+ const search = this.commandSearchTarget();
464
+ if (!search) return null;
465
+ return find(search.source, this.state, offset, search.scopes, {
466
+ loop: this.options.loop,
467
+ ...options
468
+ });
469
+ }
470
+ searchTarget(source = this.host?.textSource()) {
471
+ if (!source) return null;
472
+ const scopes = this.scopeRanges();
473
+ if (scopes && scopes.length === 0) return null;
474
+ return {
475
+ source,
476
+ scopes
477
+ };
478
+ }
479
+ scopeRanges() {
480
+ return this.scope?.resolve() ?? null;
481
+ }
482
+ selectFirstMatchFromSelection() {
483
+ const offset = this.primarySelection()?.endOffset ?? 0;
484
+ this.selectMatch(this.nextMatchAt(offset, false), false);
485
+ }
486
+ nextMatchAt(offset, escapeEmptyMatchAtOffset) {
487
+ return this.searchFrom(findNextMatchFrom, offset, { escapeEmptyMatchAtOffset });
488
+ }
489
+ selectMatch(match, committed = true) {
490
+ const host = this.host;
491
+ if (!match || !host) return false;
492
+ if (this.canPaintCurrentDocument()) this.currentMatch = match;
493
+ host.setSelection(match.start, match.end, "input.findNavigate", findReveal(match, committed));
494
+ if (!this.canPaintCurrentDocument()) return true;
495
+ this.updateHighlights();
496
+ this.updateWidget();
497
+ return true;
498
+ }
499
+ updateHighlights() {
500
+ if (!this.canPaintCurrentDocument()) return;
501
+ const host = this.host;
502
+ if (!host) return;
503
+ host.setRangeHighlight(this.matchHighlightName, this.matches, FIND_MATCH_STYLE);
504
+ host.setRangeHighlight(this.currentHighlightName, this.currentMatchRanges(), FIND_CURRENT_STYLE);
505
+ const scopes = this.scopeRanges();
506
+ if (scopes) {
507
+ host.setRangeHighlight(this.scopeHighlightName, scopes, FIND_SCOPE_STYLE);
508
+ return;
509
+ }
510
+ host.clearRangeHighlight(this.scopeHighlightName);
511
+ }
512
+ currentMatchRanges() {
513
+ return this.currentMatch ? [this.currentMatch] : [];
514
+ }
515
+ clearHighlights() {
516
+ const host = this.host;
517
+ if (!host || !this.matchHighlightName) return;
518
+ host.clearRangeHighlight(this.matchHighlightName);
519
+ host.clearRangeHighlight(this.currentHighlightName);
520
+ host.clearRangeHighlight(this.scopeHighlightName);
521
+ }
522
+ updateWidget() {
523
+ this.emit({
524
+ type: "update",
525
+ state: this.widgetState()
526
+ });
527
+ }
528
+ widgetState() {
529
+ return {
530
+ ...this.state,
531
+ matchesCount: this.highlightsTruncated ? this.totalMatches : this.matches.length,
532
+ matchesPosition: this.currentMatchPosition(),
533
+ highlightsTruncated: this.highlightsTruncated
534
+ };
535
+ }
536
+ currentMatchPosition() {
537
+ if (!this.currentMatch) return 0;
538
+ const painted = findMatchIndex(this.matches, this.currentMatch);
539
+ if (painted >= 0 || !this.highlightsTruncated) return painted + 1;
540
+ return this.countAll(this.currentMatch.start) + 1;
541
+ }
542
+ focusWidget(focus) {
543
+ if (focus === "find") this.emit({
544
+ type: "focus",
545
+ target: "find"
546
+ });
547
+ if (focus === "replace") this.emit({
548
+ type: "focus",
549
+ target: "replace"
550
+ });
551
+ }
552
+ seedSearchString() {
553
+ if (this.options.seedSearchStringFromSelection === "never") return "";
554
+ const selection = this.primarySelection();
555
+ if (!selection) return "";
556
+ const source = this.commandTextSource();
557
+ if (!source) return "";
558
+ if (!selection.collapsed) return this.seedFromLiteralText(selectedSingleLineText(source, selection));
559
+ if (this.options.seedSearchStringFromSelection === "selection") return "";
560
+ return this.seedFromLiteralText(wordTextAtOffset(source, selection.headOffset));
561
+ }
562
+ seedFromLiteralText(value) {
563
+ return this.state.isRegex ? escapeRegExpCharacters(value) : value;
564
+ }
565
+ applyAutoFindInSelection(host) {
566
+ const scopes = nonEmptySelectionRanges(this.commandSelections());
567
+ if (scopes.length === 0) return;
568
+ if (this.options.autoFindInSelection === "never") return;
569
+ if (this.options.autoFindInSelection === "always") {
570
+ this.state = {
571
+ ...this.state,
572
+ inSelection: true
573
+ };
574
+ this.scope = host.trackRanges(scopes);
575
+ return;
576
+ }
577
+ const source = this.commandTextSource();
578
+ if (!source || !hasMultilineScope(source, scopes)) return;
579
+ this.state = {
580
+ ...this.state,
581
+ inSelection: true
582
+ };
583
+ this.scope = host.trackRanges(scopes);
584
+ }
585
+ replacePattern() {
586
+ if (this.state.isRegex) return parseReplaceString(this.state.replaceString);
587
+ return ReplacePattern.fromStaticValue(this.state.replaceString);
588
+ }
589
+ primarySelection() {
590
+ return this.commandSelections()[0] ?? null;
591
+ }
592
+ emit(event) {
593
+ for (const listener of this.listeners) listener(event);
594
+ }
595
+ };
596
+ function resolveFindOptions(options) {
597
+ return {
598
+ loop: options.loop ?? true,
599
+ seedSearchStringFromSelection: options.seedSearchStringFromSelection ?? "always",
600
+ findOnType: options.findOnType ?? true,
601
+ cursorMoveOnType: options.cursorMoveOnType ?? true,
602
+ autoFindInSelection: options.autoFindInSelection ?? "never"
603
+ };
604
+ }
605
+ function isFindDocumentUpdate(kind) {
606
+ return kind === "document" || kind === "content" || kind === "clear";
607
+ }
608
+ function selectedSingleLineText(source, selection) {
609
+ if (selection.endOffset - selection.startOffset >= SEARCH_STRING_MAX_LENGTH) return "";
610
+ if (spansLines(source, selection.startOffset, selection.endOffset)) return "";
611
+ return source.readRange(selection.startOffset, selection.endOffset);
612
+ }
613
+ function wordTextAtOffset(source, offset) {
614
+ const line = findLineRange(source, offset);
615
+ const text = source.readRange(line.start, line.end);
616
+ const word = wordRangeAtOffset(text, offset - line.start);
617
+ return text.slice(word.start, word.end);
618
+ }
619
+ function nonEmptySelectionRanges(selections) {
620
+ return selections.filter((selection) => !selection.collapsed).map((selection) => ({
621
+ start: selection.startOffset,
622
+ end: selection.endOffset
623
+ }));
624
+ }
625
+ function hasMultilineScope(source, scopes) {
626
+ return scopes.some((scope) => spansLines(source, scope.start, scope.end));
627
+ }
628
+ function spansLines(source, start, end) {
629
+ const lineStartsView = source.lineStartsView;
630
+ return lineStartsView.indexForOffset(start) !== lineStartsView.indexForOffset(end);
631
+ }
632
+ function matchAtSelection(matches, selection) {
633
+ if (!selection) return null;
634
+ return matches.find((match) => match.start === selection.startOffset && match.end === selection.endOffset) ?? null;
635
+ }
636
+ function orderedMatchSelections(matches, currentSelection) {
637
+ const selections = matches.map(selectionForMatch);
638
+ const currentMatch = matchAtSelection(matches, currentSelection);
639
+ const currentIndex = currentMatch ? findMatchIndex(matches, currentMatch) : -1;
640
+ if (!currentSelection || currentIndex < 0) return selections;
641
+ const current = {
642
+ anchor: currentSelection.anchorOffset,
643
+ head: currentSelection.headOffset,
644
+ affinity: currentSelection.affinity
645
+ };
646
+ if (currentIndex === 0) return [current, ...selections.slice(1)];
647
+ return [
648
+ current,
649
+ ...selections.slice(0, currentIndex),
650
+ ...selections.slice(currentIndex + 1)
651
+ ];
652
+ }
653
+ function selectionForMatch(match) {
654
+ return {
655
+ anchor: match.start,
656
+ head: match.end,
657
+ affinity: "after"
658
+ };
659
+ }
660
+ function mergeAdjacentReplaceEdits(edits) {
661
+ const sorted = edits.toSorted((left, right) => left.from - right.from || left.to - right.to);
662
+ const merged = [];
663
+ for (const edit of sorted) mergeReplaceEdit(merged, edit);
664
+ return merged;
665
+ }
666
+ function mergeReplaceEdit(merged, edit) {
667
+ const previous = merged.at(-1);
668
+ if (!previous || previous.to !== edit.from) {
669
+ merged.push({ ...edit });
670
+ return;
671
+ }
672
+ merged[merged.length - 1] = {
673
+ from: previous.from,
674
+ to: edit.to,
675
+ text: previous.text + edit.text
676
+ };
677
+ }
678
+ //#endregion
679
+ export { EditorFindController, FIND_HIGHLIGHT_Z_INDEX };
680
+
681
+ //# sourceMappingURL=findController.js.map