@adecore/editor 0.21.0 → 0.21.2

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/src/fake.ts CHANGED
@@ -53,7 +53,7 @@ function positionIn(text: string, offset: number): EditorPosition {
53
53
  return { line: lines.length - 1, character: lines.at(-1)!.length };
54
54
  }
55
55
 
56
- /* An editor without a DOM, for tests: `type`, `save` and `blur` do what a person would, the rest says what the client asked of it. */
56
+ /* An editor without a DOM, for tests: `type`, `save` and `blur` do what a person would, the rest says what the host asked of it. */
57
57
  export class FakeEditor implements Editor {
58
58
  readonly element: HTMLElement;
59
59
  readonly language: string | undefined;
@@ -67,10 +67,10 @@ export class FakeEditor implements Editor {
67
67
  revealedLine: number | null;
68
68
  focused = false;
69
69
  indentation: EditorIndentation;
70
- /* How often the client asked it to read the code face off the page again. */
70
+ /* How often the host asked it to read the code face off the page again. */
71
71
  fontRefreshes = 0;
72
72
  disposed = false;
73
- /* What the client asked to find last; null once it ended the find. */
73
+ /* What the host asked to find last; null once it ended the find. */
74
74
  findQuery: EditorFindQuery | null = null;
75
75
  findState: EditorFindState = { count: 0, current: null };
76
76
  private text: string;
@@ -147,9 +147,7 @@ export class FakeEditor implements Editor {
147
147
  source,
148
148
  changes: [{ range: { start: positionIn(before, span.start), end: positionIn(before, span.end) }, text: span.text }]
149
149
  };
150
- for (const listener of [...this.textChanges]) {
151
- listener(change);
152
- }
150
+ emit(this.textChanges, change);
153
151
  }
154
152
 
155
153
  private readonly tracked = new Set<{ from: number; to: number; lost: boolean }>();
@@ -180,10 +178,7 @@ export class FakeEditor implements Editor {
180
178
  }
181
179
 
182
180
  onTextChange(listener: (change: EditorTextChange) => void): () => void {
183
- this.textChanges.add(listener);
184
- return () => {
185
- this.textChanges.delete(listener);
186
- };
181
+ return subscribe(this.textChanges, listener);
187
182
  }
188
183
 
189
184
  positionAt(offset: number): EditorPosition {
@@ -216,7 +211,7 @@ export class FakeEditor implements Editor {
216
211
  return subscribe(this.blurs, listener);
217
212
  }
218
213
 
219
- /* How the client last asked the view to follow a jump. */
214
+ /* How the host last asked the view to follow a jump. */
220
215
  lastReveal: EditorReveal | null = null;
221
216
 
222
217
  revealLine(line: number, reveal: EditorReveal = 'center'): void {
@@ -224,7 +219,7 @@ export class FakeEditor implements Editor {
224
219
  this.lastReveal = reveal;
225
220
  }
226
221
 
227
- /* Counts plain occurrences only; the matcher itself is the editor's, and a test here is about the client's side. */
222
+ /* Counts plain occurrences only; the matcher itself is the editor's, and a test here is about the host's side. */
228
223
  find(query: EditorFindQuery | null): void {
229
224
  this.findQuery = query;
230
225
  const needle = query === null || query.text === '' ? '' : query.caseSensitive ? query.text : query.text.toLowerCase();
@@ -233,12 +228,12 @@ export class FakeEditor implements Editor {
233
228
  this.announceFind({ count, current: count === 0 ? null : 0 });
234
229
  }
235
230
 
236
- /* What the client asked to replace, one match at a time and all at once. */
231
+ /* What the host asked to replace, one match at a time and all at once. */
237
232
  replacements: string[] = [];
238
233
  replaceOptions: EditorReplaceOptions[] = [];
239
- /* What the client last asked to show under the match. */
234
+ /* What the host last asked to show under the match. */
240
235
  replacePreview: string | null = null;
241
- /* What the client asked to select all matches of, and how many it was told there were. */
236
+ /* What the host asked to select all matches of, and how many it was told there were. */
242
237
  selectedMatches = 0;
243
238
  lastFindFromCursor: { query: EditorFindQuery; direction: 1 | -1 } | null = null;
244
239
  replacementsOfAll: string[] = [];
@@ -278,10 +273,7 @@ export class FakeEditor implements Editor {
278
273
  }
279
274
 
280
275
  onFind(listener: (state: EditorFindState) => void): () => void {
281
- this.finds.add(listener);
282
- return () => {
283
- this.finds.delete(listener);
284
- };
276
+ return subscribe(this.finds, listener);
285
277
  }
286
278
 
287
279
  endFind(): void {
@@ -292,39 +284,39 @@ export class FakeEditor implements Editor {
292
284
  this.wrap = wrap;
293
285
  }
294
286
 
295
- /* What the client handed it last; null while it reads the structure itself. */
287
+ /* What the host handed it last; null while it reads the structure itself. */
296
288
  blocks: readonly EditorBlock[] | null = null;
297
289
  private readonly scopes = new Set<(scope: readonly EditorBlock[]) => void>();
298
290
 
299
- /* What the client marked as changed last. */
291
+ /* What the host marked as changed last. */
300
292
  changeMarks: readonly EditorChangeMark[] = [];
301
293
 
302
294
  setChangeMarks(marks: readonly EditorChangeMark[]): void {
303
295
  this.changeMarks = marks;
304
296
  }
305
297
 
306
- /* The tinted lines the client set last. */
298
+ /* The tinted lines the host set last. */
307
299
  lineHighlights: readonly EditorLineHighlight[] = [];
308
300
 
309
301
  setLineHighlights(highlights: readonly EditorLineHighlight[]): void {
310
302
  this.lineHighlights = highlights;
311
303
  }
312
304
 
313
- /* What the client drew as code outside the document: the text and how, and into which element. */
305
+ /* What the host drew as code outside the document: the text and how, and into which element. */
314
306
  codeBlocks: { container: HTMLElement; text: string; options: EditorCodeBlockOptions }[] = [];
315
307
 
316
308
  renderCode(container: HTMLElement, text: string, options: EditorCodeBlockOptions = {}): void {
317
309
  this.codeBlocks.push({ container, text, options });
318
310
  }
319
311
 
320
- /* The cursors of agents the client set last. */
312
+ /* The cursors of agents the host set last. */
321
313
  remoteCursors: readonly EditorRemoteCursor[] = [];
322
314
 
323
315
  setRemoteCursors(cursors: readonly EditorRemoteCursor[]): void {
324
316
  this.remoteCursors = cursors;
325
317
  }
326
318
 
327
- /* What the client marked as written by an agent last. */
319
+ /* What the host marked as written by an agent last. */
328
320
  attributionMarks: readonly EditorAttributionMark[] = [];
329
321
  private readonly attributionHovers = new Set<(hover: EditorAttributionHover | null) => void>();
330
322
 
@@ -333,10 +325,7 @@ export class FakeEditor implements Editor {
333
325
  }
334
326
 
335
327
  onAttributionHover(listener: (hover: EditorAttributionHover | null) => void): () => void {
336
- this.attributionHovers.add(listener);
337
- return () => {
338
- this.attributionHovers.delete(listener);
339
- };
328
+ return subscribe(this.attributionHovers, listener);
340
329
  }
341
330
 
342
331
  /* The pointer on the bar of a mark, or leaving it. The rect is where `rectAt` puts the mark's first line unless given. */
@@ -346,9 +335,7 @@ export class FakeEditor implements Editor {
346
335
  mark === undefined
347
336
  ? null
348
337
  : { id: mark.id, rect: rect ?? this.rectAt({ line: mark.startLine - 1, character: 0 }) ?? { left: 0, top: 0, right: 0, bottom: 0 } };
349
- for (const listener of [...this.attributionHovers]) {
350
- listener(hover);
351
- }
338
+ emit(this.attributionHovers, hover);
352
339
  }
353
340
 
354
341
  /* What each owner put next to lines of the text last. */
@@ -388,14 +375,14 @@ export class FakeEditor implements Editor {
388
375
  }
389
376
  }
390
377
 
391
- /* The code vision rows the client set last. */
378
+ /* The code vision rows the host set last. */
392
379
  codeVision: readonly EditorCodeVision[] = [];
393
380
 
394
381
  setCodeVision(rows: readonly EditorCodeVision[]): void {
395
382
  this.codeVision = rows;
396
383
  }
397
384
 
398
- /* The button the client set in the gutter last. */
385
+ /* The button the host set in the gutter last. */
399
386
  gutterAction: EditorGutterAction | null = null;
400
387
  private readonly gutterActionListeners = new Set<(line: number) => void>();
401
388
 
@@ -404,10 +391,7 @@ export class FakeEditor implements Editor {
404
391
  }
405
392
 
406
393
  onGutterAction(listener: (line: number) => void): () => void {
407
- this.gutterActionListeners.add(listener);
408
- return () => {
409
- this.gutterActionListeners.delete(listener);
410
- };
394
+ return subscribe(this.gutterActionListeners, listener);
411
395
  }
412
396
 
413
397
  /* What each owner put in the gutter last. */
@@ -423,25 +407,18 @@ export class FakeEditor implements Editor {
423
407
  }
424
408
 
425
409
  onGutterMarker(listener: (id: string) => void): () => void {
426
- this.gutterMarkerListeners.add(listener);
427
- return () => {
428
- this.gutterMarkerListeners.delete(listener);
429
- };
410
+ return subscribe(this.gutterMarkerListeners, listener);
430
411
  }
431
412
 
432
413
  /* A press on a gutter marker. */
433
414
  pressGutterMarker(id: string): void {
434
- for (const listener of [...this.gutterMarkerListeners]) {
435
- listener(id);
436
- }
415
+ emit(this.gutterMarkerListeners, id);
437
416
  }
438
417
 
439
418
  /* A press on the gutter button, if there is one. */
440
419
  pressGutterAction(): void {
441
420
  if (this.gutterAction !== null) {
442
- for (const listener of [...this.gutterActionListeners]) {
443
- listener(this.gutterAction.line);
444
- }
421
+ emit(this.gutterActionListeners, this.gutterAction.line);
445
422
  }
446
423
  }
447
424
 
@@ -450,17 +427,12 @@ export class FakeEditor implements Editor {
450
427
  }
451
428
 
452
429
  onScope(listener: (scope: readonly EditorBlock[]) => void): () => void {
453
- this.scopes.add(listener);
454
- return () => {
455
- this.scopes.delete(listener);
456
- };
430
+ return subscribe(this.scopes, listener);
457
431
  }
458
432
 
459
- /* The caret moving into blocks, for a test of what the client draws from it. */
433
+ /* The caret moving into blocks, for a test of what the host draws from it. */
460
434
  enterScope(scope: readonly EditorBlock[]): void {
461
- for (const listener of [...this.scopes]) {
462
- listener(scope);
463
- }
435
+ emit(this.scopes, scope);
464
436
  }
465
437
 
466
438
  smartKeys: Partial<EditorSmartKeys>;
@@ -506,7 +478,7 @@ export class FakeEditor implements Editor {
506
478
  this.indentation = indentation;
507
479
  }
508
480
 
509
- /* What the client marked and highlighted last. */
481
+ /* What the host marked and highlighted last. */
510
482
  markers: readonly EditorMarker[] = [];
511
483
  highlights: readonly EditorHighlight[] = [];
512
484
  caret: EditorPosition = { line: 0, character: 0 };
@@ -522,7 +494,7 @@ export class FakeEditor implements Editor {
522
494
  this.markers = markers;
523
495
  }
524
496
 
525
- /* What the client drew as hints last. */
497
+ /* What the host drew as hints last. */
526
498
  inlayHints: readonly EditorInlayHint[] = [];
527
499
 
528
500
  setInlayHints(hints: readonly EditorInlayHint[]): void {
@@ -536,14 +508,14 @@ export class FakeEditor implements Editor {
536
508
  this.ghost = ghost;
537
509
  }
538
510
 
539
- /* What the client classified last. */
511
+ /* What the host classified last. */
540
512
  semanticTokens: readonly EditorSemanticToken[] | null = null;
541
513
 
542
514
  setSemanticTokens(tokens: readonly EditorSemanticToken[] | null): void {
543
515
  this.semanticTokens = tokens;
544
516
  }
545
517
 
546
- /* What the client drew as a link last. */
518
+ /* What the host drew as a link last. */
547
519
  link: EditorRange | null = null;
548
520
 
549
521
  setLink(range: EditorRange | null): void {
@@ -558,7 +530,7 @@ export class FakeEditor implements Editor {
558
530
  return this.caret;
559
531
  }
560
532
 
561
- /* The folds the client asked to keep. */
533
+ /* The folds the host asked to keep. */
562
534
  folds: EditorFolds = { collapsed: [], custom: [] };
563
535
 
564
536
  getFolds(): EditorFolds {
@@ -616,40 +588,28 @@ export class FakeEditor implements Editor {
616
588
  this.lastReveal = reveal;
617
589
  this.caret = range.end;
618
590
  this.selection = range;
619
- for (const listener of [...this.carets]) {
620
- listener(range.end);
621
- }
591
+ emit(this.carets, range.end);
622
592
  }
623
593
 
624
594
  /* The caret a person's click or key moved. */
625
595
  moveCaret(position: EditorPosition): void {
626
596
  this.caret = position;
627
597
  this.selection = null;
628
- for (const listener of [...this.carets]) {
629
- listener(position);
630
- }
598
+ emit(this.carets, position);
631
599
  }
632
600
 
633
601
  onCaret(listener: (position: EditorPosition) => void): () => void {
634
- this.carets.add(listener);
635
- return () => {
636
- this.carets.delete(listener);
637
- };
602
+ return subscribe(this.carets, listener);
638
603
  }
639
604
 
640
605
  onHover(listener: (hover: EditorHover | null) => void): () => void {
641
- this.hovers.add(listener);
642
- return () => {
643
- this.hovers.delete(listener);
644
- };
606
+ return subscribe(this.hovers, listener);
645
607
  }
646
608
 
647
609
  /* The pointer resting on a position, or leaving the text. */
648
610
  hover(position: EditorPosition | null): void {
649
611
  const hover = position === null ? null : { position, rect: this.rectAt(position)! };
650
- for (const listener of [...this.hovers]) {
651
- listener(hover);
652
- }
612
+ emit(this.hovers, hover);
653
613
  }
654
614
 
655
615
  rectAt(position: EditorPosition): EditorRect | null {
@@ -685,27 +645,18 @@ export class FakeEditor implements Editor {
685
645
  }
686
646
 
687
647
  onKeyDown(handler: EditorKeyHandler): () => void {
688
- this.keys.add(handler);
689
- return () => {
690
- this.keys.delete(handler);
691
- };
648
+ return subscribe(this.keys, handler);
692
649
  }
693
650
 
694
651
  private readonly clickHandlers = new Set<EditorClickHandler>();
695
652
  private readonly contextListeners = new Set<(menu: EditorContextMenu) => void>();
696
653
 
697
654
  onClick(handler: EditorClickHandler): () => void {
698
- this.clickHandlers.add(handler);
699
- return () => {
700
- this.clickHandlers.delete(handler);
701
- };
655
+ return subscribe(this.clickHandlers, handler);
702
656
  }
703
657
 
704
658
  onContextMenu(listener: (menu: EditorContextMenu) => void): () => void {
705
- this.contextListeners.add(listener);
706
- return () => {
707
- this.contextListeners.delete(listener);
708
- };
659
+ return subscribe(this.contextListeners, listener);
709
660
  }
710
661
 
711
662
  /* A press on a character as the editor sees it first; true when a handler took it, and the caret stays where it is otherwise only until the test moves it. */
@@ -715,9 +666,7 @@ export class FakeEditor implements Editor {
715
666
 
716
667
  /* The context menu asked for at a position. */
717
668
  openContextMenu(menu: EditorContextMenu): void {
718
- for (const listener of [...this.contextListeners]) {
719
- listener(menu);
720
- }
669
+ emit(this.contextListeners, menu);
721
670
  }
722
671
 
723
672
  /* A key as the editor sees it first; true when a handler took it. */
@@ -805,12 +754,10 @@ export class FakeEditor implements Editor {
805
754
 
806
755
  private announceFind(state: EditorFindState): void {
807
756
  this.findState = state;
808
- for (const listener of [...this.finds]) {
809
- listener(state);
810
- }
757
+ emit(this.finds, state);
811
758
  }
812
759
 
813
- /* A test that drives an editor its client already disposed has found a leak. */
760
+ /* A test that drives an editor its host already disposed has found a leak. */
814
761
  private assertLive(): void {
815
762
  if (this.disposed) {
816
763
  throw new Error('The editor is disposed');
package/src/keymap.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import type { EditorCommand } from '@adecore/editor-core';
2
- import type { EditorViewCommand } from './types.ts';
3
2
  import { chordOf, KEYMAP, KEYMAP_IDS, type Keymap, type KeymapId, parseChord } from './keymap-table.ts';
4
- import type { KeyChord } from './types.ts';
3
+ import type { EditorViewCommand, KeyChord } from './types.ts';
5
4
 
6
5
  export interface KeyLike {
7
6
  key: string;
@@ -30,6 +29,10 @@ function command(name: EditorCommand): KeyAction {
30
29
  return { type: 'command', command: name };
31
30
  }
32
31
 
32
+ function view(name: EditorViewCommand): KeyAction {
33
+ return { type: 'view', command: name };
34
+ }
35
+
33
36
  /* What each key of the table does in the editor. A key of the table that is not here is a language command, which the host answers. */
34
37
  const ACTIONS: Partial<Record<KeymapId, KeyAction>> = {
35
38
  undo: command('undo'),
@@ -69,21 +72,21 @@ const ACTIONS: Partial<Record<KeymapId, KeyAction>> = {
69
72
  selectTextStart: { type: 'edge', end: false, extend: true },
70
73
  textEnd: { type: 'edge', end: true, extend: false },
71
74
  selectTextEnd: { type: 'edge', end: true, extend: true },
72
- collapse: { type: 'view', command: 'collapseRegion' },
73
- expand: { type: 'view', command: 'expandRegion' },
74
- collapseAll: { type: 'view', command: 'collapseAllRegions' },
75
- expandAll: { type: 'view', command: 'expandAllRegions' },
76
- collapseRecursively: { type: 'view', command: 'collapseRegionRecursively' },
77
- expandRecursively: { type: 'view', command: 'expandRegionRecursively' },
78
- foldSelection: { type: 'view', command: 'foldSelection' },
79
- collapseDocComments: { type: 'view', command: 'collapseDocComments' },
80
- expandDocComments: { type: 'view', command: 'expandDocComments' },
81
- expandAllToLevel1: { type: 'view', command: 'expandAllToLevel1' },
82
- expandAllToLevel2: { type: 'view', command: 'expandAllToLevel2' },
83
- expandAllToLevel3: { type: 'view', command: 'expandAllToLevel3' },
84
- expandAllToLevel4: { type: 'view', command: 'expandAllToLevel4' },
85
- expandAllToLevel5: { type: 'view', command: 'expandAllToLevel5' },
86
- toggleColumnMode: { type: 'view', command: 'toggleColumnMode' }
75
+ collapse: view('collapseRegion'),
76
+ expand: view('expandRegion'),
77
+ collapseAll: view('collapseAllRegions'),
78
+ expandAll: view('expandAllRegions'),
79
+ collapseRecursively: view('collapseRegionRecursively'),
80
+ expandRecursively: view('expandRegionRecursively'),
81
+ foldSelection: view('foldSelection'),
82
+ collapseDocComments: view('collapseDocComments'),
83
+ expandDocComments: view('expandDocComments'),
84
+ expandAllToLevel1: view('expandAllToLevel1'),
85
+ expandAllToLevel2: view('expandAllToLevel2'),
86
+ expandAllToLevel3: view('expandAllToLevel3'),
87
+ expandAllToLevel4: view('expandAllToLevel4'),
88
+ expandAllToLevel5: view('expandAllToLevel5'),
89
+ toggleColumnMode: view('toggleColumnMode')
87
90
  };
88
91
 
89
92
  const PUNCTUATION_CODES: Readonly<Record<string, string>> = {
package/src/layout.ts CHANGED
@@ -136,6 +136,7 @@ const DEFAULT_BLOCK_HEIGHT = 40;
136
136
  const WRAP_INDENT_STEP = 2;
137
137
  const GEOMETRY_CACHE_SIZE = 400;
138
138
  const PLAIN = /^[\x20-\x7e\t]*$/;
139
+ const GRAPHEME_LIMIT = 20000;
139
140
  const SEGMENTER = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
140
141
 
141
142
  function lowerBound(values: readonly number[], value: number): number {
@@ -152,10 +153,10 @@ function lowerBound(values: readonly number[], value: number): number {
152
153
  return low;
153
154
  }
154
155
 
155
- /* Where each character starts, with the length last. A line that long is cut by code unit, since splitting it by grapheme would cost more than it shows. */
156
+ /* Where each character starts, with the length last. A very long line is cut by code unit, since splitting it by grapheme would cost more than it shows. */
156
157
  function characterOffsets(text: string): number[] {
157
158
  const offsets: number[] = [];
158
- if (text.length > 20000 || PLAIN.test(text)) {
159
+ if (text.length > GRAPHEME_LIMIT || PLAIN.test(text)) {
159
160
  for (let i = 0; i < text.length; i++) {
160
161
  offsets.push(i);
161
162
  }
@@ -432,8 +433,8 @@ export class EditorLayout {
432
433
  }
433
434
  const fold = folds.get(line);
434
435
  const last = Math.max(line, Math.min(count - 1, fold?.endLine ?? line));
435
- const tail = fold?.closer !== undefined && last > line ? this.tailOf(fold.closer, last) : null;
436
- const head = fold?.head !== undefined && last > line ? this.tailOf(fold.head, line) : null;
436
+ const tail = fold?.closer !== undefined && last > line ? this.clampToLine(fold.closer, last) : null;
437
+ const head = fold?.head !== undefined && last > line ? this.clampToLine(fold.head, line) : null;
437
438
  const index = this.rows.length;
438
439
  const bounds = this.wrapWidth === null ? null : this.document.getLine(line);
439
440
  const subRows = this.rowCountOf(line, bounds === null ? 0 : bounds.end - bounds.start);
@@ -461,9 +462,9 @@ export class EditorLayout {
461
462
  }
462
463
 
463
464
  /* An offset of a fold clamped to its line, since the fold may be a moment older than an edit. */
464
- private tailOf(closer: number, line: number): number {
465
+ private clampToLine(offset: number, line: number): number {
465
466
  const bounds = this.document.getLine(line);
466
- return Math.min(bounds.end, Math.max(bounds.start, closer));
467
+ return Math.min(bounds.end, Math.max(bounds.start, offset));
467
468
  }
468
469
 
469
470
  private reflow(): void {
package/src/listeners.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export type Listener = () => void;
2
2
 
3
- export function subscribe(listeners: Set<Listener>, listener: Listener): () => void {
3
+ export function subscribe<T>(listeners: Set<T>, listener: T): () => void {
4
4
  listeners.add(listener);
5
5
  return () => {
6
6
  listeners.delete(listener);
@@ -8,8 +8,8 @@ export function subscribe(listeners: Set<Listener>, listener: Listener): () => v
8
8
  }
9
9
 
10
10
  /* Over a copy, so a listener that unsubscribes itself does not skip the next one. */
11
- export function emit(listeners: Set<Listener>): void {
11
+ export function emit<Args extends unknown[]>(listeners: Set<(...args: Args) => void>, ...args: Args): void {
12
12
  for (const listener of [...listeners]) {
13
- listener();
13
+ listener(...args);
14
14
  }
15
15
  }
@@ -45,8 +45,8 @@ export class DoubleModifierGesture {
45
45
 
46
46
  /* Another modifier held with the one that counts spoils the gesture. */
47
47
  private spoiled(event: GestureEvent): boolean {
48
- const others = this.modifier === 'Alt' ? event.ctrlKey || event.metaKey || event.shiftKey : event.altKey || event.metaKey || event.shiftKey;
49
- return others;
48
+ const other = this.modifier === 'Alt' ? event.ctrlKey : event.altKey;
49
+ return other || event.metaKey || event.shiftKey;
50
50
  }
51
51
 
52
52
  keydown(event: GestureEvent): GestureAction | null {
@@ -119,8 +119,7 @@ export class NativeInput {
119
119
  return;
120
120
  }
121
121
  if (this.model.getSelections().length > 1) {
122
- const suffix = context.value.length - context.end;
123
- this.host.replace(this.element.value.slice(context.start, Math.max(context.start, this.element.value.length - suffix)));
122
+ this.host.replace(this.replacedText(context));
124
123
  return;
125
124
  }
126
125
  const edit = {
@@ -172,7 +171,11 @@ export class NativeInput {
172
171
  }
173
172
 
174
173
  private composedText(): string {
175
- const context = this.compositionContext!;
174
+ return this.replacedText(this.compositionContext!);
175
+ }
176
+
177
+ /* What the textarea now holds where the selection of `context` was, the text after it kept. */
178
+ private replacedText(context: InputContext): string {
176
179
  const suffix = context.value.length - context.end;
177
180
  return this.element.value.slice(context.start, Math.max(context.start, this.element.value.length - suffix));
178
181
  }
package/src/outline.ts CHANGED
@@ -120,7 +120,7 @@ export function structuralEntries(
120
120
  return entries;
121
121
  }
122
122
 
123
- function toEditorLines(entry: OutlineEntry, document: OutlineDocument): OutlineBlock {
123
+ function toBlock(entry: OutlineEntry, document: OutlineDocument): OutlineBlock {
124
124
  const startLine = document.positionAt(entry.from).line;
125
125
  return {
126
126
  startLine,
@@ -187,7 +187,7 @@ export class Outline {
187
187
  blocks(document: OutlineDocument): readonly OutlineBlock[] {
188
188
  if (this.cache?.version !== this.version) {
189
189
  const sorted = (this.provided ?? this.structure)
190
- .map((entry) => toEditorLines(entry, document))
190
+ .map((entry) => toBlock(entry, document))
191
191
  .sort((left, right) => left.startLine - right.startLine || right.endLine - left.endLine);
192
192
  this.cache = { version: this.version, blocks: sorted.filter((block, index) => index === 0 || block.startLine !== sorted[index - 1]!.startLine) };
193
193
  }
package/src/overview.ts CHANGED
@@ -56,7 +56,6 @@ export function overviewTicks(spans: readonly OverviewSpan[], contentHeight: num
56
56
  /* What is painted later is on top: the current find match, and the worse a problem is the later it goes. */
57
57
  const PAINT_ORDER: Partial<Record<OverviewKind, number>> = { warning: 1, error: 2, 'find-current': 3 };
58
58
 
59
- /* Draws the ticks. */
60
59
  export function paintOverview(container: HTMLElement, ticks: readonly OverviewTick[]): void {
61
60
  const document = container.ownerDocument;
62
61
  const order = (tick: OverviewTick): number => PAINT_ORDER[tick.kind] ?? 0;
package/src/paint.ts CHANGED
@@ -345,6 +345,15 @@ export interface GutterPaint {
345
345
  markers: ReadonlyMap<number, { id: string; label: string }>;
346
346
  }
347
347
 
348
+ function gutterButton(document: Document, className: string, label: string): HTMLButtonElement {
349
+ const button = document.createElement('button');
350
+ button.type = 'button';
351
+ button.className = className;
352
+ button.tabIndex = -1;
353
+ button.setAttribute('aria-label', label);
354
+ return button;
355
+ }
356
+
348
357
  /* The line numbers of the visible rows, and the fold control of the lines that have one. */
349
358
  export function paintGutter(container: HTMLElement, layout: EditorLayout, rows: readonly LayoutRow[], paint: GutterPaint): void {
350
359
  const document = container.ownerDocument;
@@ -365,22 +374,14 @@ export function paintGutter(container: HTMLElement, layout: EditorLayout, rows:
365
374
  number.textContent = String(row.line + 1);
366
375
  item.append(number);
367
376
  if (paint.action?.line === row.line) {
368
- const button = document.createElement('button');
369
- button.type = 'button';
370
- button.className = 'se-gutter-action';
377
+ const button = gutterButton(document, 'se-gutter-action', paint.action.label);
371
378
  button.dataset.gutterAction = String(row.line);
372
- button.tabIndex = -1;
373
- button.setAttribute('aria-label', paint.action.label);
374
379
  item.append(button);
375
380
  }
376
381
  const marker = paint.markers.get(row.line);
377
382
  if (marker !== undefined) {
378
- const button = document.createElement('button');
379
- button.type = 'button';
380
- button.className = 'se-gutter-marker';
383
+ const button = gutterButton(document, 'se-gutter-marker', marker.label);
381
384
  button.dataset.gutterMarker = marker.id;
382
- button.tabIndex = -1;
383
- button.setAttribute('aria-label', marker.label);
384
385
  item.append(button);
385
386
  }
386
387
  const sign = paint.highlights.get(row.line)?.sign;
@@ -412,12 +413,8 @@ export function paintGutter(container: HTMLElement, layout: EditorLayout, rows:
412
413
  }
413
414
  const collapsed = paint.foldable.get(row.line);
414
415
  if (collapsed !== undefined) {
415
- const button = document.createElement('button');
416
- button.type = 'button';
417
- button.className = collapsed ? 'se-fold-toggle se-folded' : 'se-fold-toggle';
416
+ const button = gutterButton(document, collapsed ? 'se-fold-toggle se-folded' : 'se-fold-toggle', collapsed ? 'Expand' : 'Collapse');
418
417
  button.dataset.foldLine = String(row.line);
419
- button.tabIndex = -1;
420
- button.setAttribute('aria-label', collapsed ? 'Expand' : 'Collapse');
421
418
  item.append(button);
422
419
  }
423
420
  fragment.append(item);
package/src/pointer.ts CHANGED
@@ -77,6 +77,14 @@ const MULTI_CLICK_DISTANCE = 4;
77
77
  /* How far the pointer goes, in pixels, before a press on a selection starts carrying it. */
78
78
  const CARRY_THRESHOLD = 5;
79
79
 
80
+ /* How far `value` lies outside `[low, high]`, negative below it and zero inside. */
81
+ function overshoot(value: number, low: number, high: number): number {
82
+ if (value < low) {
83
+ return value - low;
84
+ }
85
+ return value > high ? value - high : 0;
86
+ }
87
+
80
88
  /* The selection a mouse makes: a click, a drag, shift to extend, alt to add a caret or, dragged, a column, a double click for a word and a triple for a line, which a drag then grows by words and lines. */
81
89
  export class PointerSelection {
82
90
  private drag: Drag | undefined;
@@ -313,8 +321,8 @@ export class PointerSelection {
313
321
  return;
314
322
  }
315
323
  const rect = viewport.getBoundingClientRect();
316
- const dy = this.drag.y < rect.top ? this.drag.y - rect.top : this.drag.y > rect.bottom ? this.drag.y - rect.bottom : 0;
317
- const dx = this.drag.x < rect.left ? this.drag.x - rect.left : this.drag.x > rect.right ? this.drag.x - rect.right : 0;
324
+ const dy = overshoot(this.drag.y, rect.top, rect.bottom);
325
+ const dx = overshoot(this.drag.x, rect.left, rect.right);
318
326
  if (dy || dx) {
319
327
  viewport.scrollTop += Math.max(-SCROLL_STEP, Math.min(SCROLL_STEP, dy));
320
328
  viewport.scrollLeft += Math.max(-SCROLL_STEP, Math.min(SCROLL_STEP, dx));