@adecore/editor 0.41.1 → 0.43.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/src/controller.ts CHANGED
@@ -8,13 +8,16 @@ import { NativeInput } from './native-input.ts';
8
8
  import { columnSelections, sameCell } from './column-selection.ts';
9
9
  import { PointerSelection } from './pointer.ts';
10
10
  import { TouchSelection } from './touch.ts';
11
+ import { WheelZoom } from './zoom.ts';
11
12
  import type {
12
13
  EditorClick,
13
14
  EditorClickHandler,
14
15
  EditorInlayClick,
15
16
  EditorInlayClickHandler,
16
17
  EditorContextMenu,
18
+ EditorCopy,
17
19
  EditorKeyHandler,
20
+ EditorPasteOptions,
18
21
  EditorPosition,
19
22
  EditorRunCommand,
20
23
  EditorSelectionRanges,
@@ -29,6 +32,10 @@ export interface ControllerOptions {
29
32
  apple: boolean;
30
33
  save(): void;
31
34
  blur(): void;
35
+ /* One pixel larger (1) or smaller (-1), or back to the page's size (0). */
36
+ zoom(step: -1 | 0 | 1): void;
37
+ /* Whether the zoom modifier with the wheel, or a pinch, zooms. */
38
+ wheelZooms(): boolean;
32
39
  }
33
40
 
34
41
  const MODIFYING = new Set<EditorCommand>([
@@ -40,9 +47,12 @@ const MODIFYING = new Set<EditorCommand>([
40
47
  'deleteLine',
41
48
  'moveLineUp',
42
49
  'moveLineDown',
50
+ 'moveStatementUp',
51
+ 'moveStatementDown',
43
52
  'toggleLineComment',
44
53
  'toggleBlockComment',
45
54
  'startNewLine',
55
+ 'completeCurrentStatement',
46
56
  'startNewLineBefore',
47
57
  'splitLine',
48
58
  'joinLines',
@@ -53,9 +63,18 @@ const MODIFYING = new Set<EditorCommand>([
53
63
  'outdent',
54
64
  'insertNewline',
55
65
  'smartBackspace',
56
- 'deleteForward'
66
+ 'deleteForward',
67
+ 'sortLines',
68
+ 'reverseLines',
69
+ 'uniqueLines',
70
+ 'convertIndentsToSpaces',
71
+ 'convertIndentsToTabs',
72
+ 'hippieCompletion',
73
+ 'hippieBackwardCompletion'
57
74
  ]);
58
75
 
76
+ const CODE_BLOCK_COMMANDS = new Set<EditorCommand>(['codeBlockStart', 'codeBlockEnd', 'codeBlockStartWithSelection', 'codeBlockEndWithSelection']);
77
+
59
78
  const VIEW_COMMANDS = new Set<EditorViewCommand>([
60
79
  'collapseRegion',
61
80
  'expandRegion',
@@ -71,12 +90,30 @@ const VIEW_COMMANDS = new Set<EditorViewCommand>([
71
90
  'expandAllToLevel3',
72
91
  'expandAllToLevel4',
73
92
  'expandAllToLevel5',
74
- 'toggleColumnMode'
93
+ 'toggleColumnMode',
94
+ 'increaseFontSize',
95
+ 'decreaseFontSize',
96
+ 'resetFontSize',
97
+ 'methodUp',
98
+ 'methodDown',
99
+ 'nextChange',
100
+ 'previousChange',
101
+ 'pasteAsPlainText'
75
102
  ]);
76
103
 
104
+ const ZOOM_STEPS = { increaseFontSize: 1, decreaseFontSize: -1, resetFontSize: 0 } as const satisfies Partial<Record<EditorViewCommand, -1 | 0 | 1>>;
105
+
77
106
  /* The clipboard text of the last copy or cut of a bare caret, which pastes back as whole lines. Shared, since it can be pasted in another editor. */
78
107
  let lineClip: string | null = null;
79
108
 
109
+ /*
110
+ * Makes `text` the copy that pastes back as whole lines (or not), for a paste from a history that kept what the
111
+ * copy was. The clipboard itself is the caller's.
112
+ */
113
+ export function rememberCopy(text: string, wholeLines: boolean): void {
114
+ lineClip = wholeLines ? text : null;
115
+ }
116
+
80
117
  function selectionSignature(selections: readonly Selection[]): string {
81
118
  return selections.map((selection) => `${selection.anchor}:${selection.head}`).join(',');
82
119
  }
@@ -96,6 +133,9 @@ export class InputController {
96
133
  private readonly touch: TouchSelection;
97
134
  private readonly cleanup: (() => void)[] = [];
98
135
  private readonly keyHandlers = new Set<EditorKeyHandler>();
136
+ private readonly navigateListeners = new Set<(from: number) => void>();
137
+ private readonly copyListeners = new Set<(copy: EditorCopy) => void>();
138
+ private wordSources: (() => readonly string[]) | null = null;
99
139
  private readonly gesture: DoubleModifierGesture;
100
140
  private readonly clickHandlers = new Set<EditorClickHandler>();
101
141
  private readonly inlayHandlers = new Set<EditorInlayClickHandler>();
@@ -109,6 +149,7 @@ export class InputController {
109
149
  /* The Extend Selection presses that wait for a server's ranges, which are answered one after the other. */
110
150
  private expansions: Promise<void> = Promise.resolve();
111
151
  private lastInputAt = 0;
152
+ private readonly wheelZoom = new WheelZoom();
112
153
  private desiredXs: number[] = [];
113
154
  private keepDesiredXs = false;
114
155
  private readonly view: EditorView;
@@ -224,13 +265,13 @@ export class InputController {
224
265
 
225
266
  this.listen(input, 'paste', (event) => {
226
267
  event.preventDefault();
227
- this.paste(event.clipboardData?.getData('text/plain') ?? '');
268
+ this.pasteText(event.clipboardData?.getData('text/plain') ?? '');
228
269
  });
229
270
 
230
271
  this.listen(input, 'copy', (event) => this.copy(event, false));
231
272
  this.listen(input, 'cut', (event) => this.copy(event, true));
232
273
  this.listen(viewport, 'scroll', () => this.view.scrolled());
233
- this.listen(viewport, 'wheel', () => this.view.cancelScroll());
274
+ this.listen(viewport, 'wheel', (event) => this.wheel(event));
234
275
  this.listen(viewport, 'pointermove', (event) => this.view.hoverMoved(event));
235
276
 
236
277
  this.listen(viewport, 'pointerleave', () => {
@@ -296,6 +337,8 @@ export class InputController {
296
337
 
297
338
  this.cleanup.length = 0;
298
339
  this.keyHandlers.clear();
340
+ this.navigateListeners.clear();
341
+ this.copyListeners.clear();
299
342
  }
300
343
 
301
344
  private blockedByReadOnly(): boolean {
@@ -340,16 +383,30 @@ export class InputController {
340
383
  }
341
384
  }
342
385
 
343
- private paste(text: string): void {
386
+ /* Pastes as a paste from the clipboard does; false when a read-only editor refused. */
387
+ pasteText(text: string, options: EditorPasteOptions = {}): boolean {
344
388
  if (this.blockedByReadOnly()) {
345
- return;
389
+ return false;
346
390
  }
347
391
 
348
392
  this.unfoldCarets();
349
393
  this.historyGroup++;
350
- this.view.model.paste(this.native.normalizeNewlines(text), { ...this.commandOptions(), wholeLines: text !== '' && text === lineClip });
394
+
395
+ const pasted = this.view.model.paste(this.native.normalizeNewlines(text), {
396
+ ...this.commandOptions(),
397
+ ...(options.reindent === false ? { indentOnPaste: false } : {}),
398
+ wholeLines: options.wholeLines ?? (text !== '' && text === lineClip)
399
+ });
400
+
351
401
  this.historyGroup++;
352
402
  this.view.revealCaret();
403
+
404
+ return pasted;
405
+ }
406
+
407
+ /* Told what a copy or cut put on the clipboard. */
408
+ onCopy(listener: (copy: EditorCopy) => void): () => void {
409
+ return subscribe(this.copyListeners, listener);
353
410
  }
354
411
 
355
412
  private commandOptions(): CommandOptions {
@@ -365,24 +422,70 @@ export class InputController {
365
422
  tabOutOfClosers: keys.tabOutOfClosers,
366
423
  smartEnter: keys.smartIndentOnEnter,
367
424
  indentOnPaste: keys.indentOnPaste,
425
+ indentGuides: settings.guides,
368
426
  smartSemicolon: keys.smartSemicolon,
369
427
  smartArrow: keys.smartArrow,
370
428
  language: settings.language ?? 'plaintext',
371
429
  camelCase: keys.camelHumps,
372
430
  visualLine: (offset) => this.view.rowOf(offset),
373
- lineSpan: (line) => this.view.lineSpan(line)
431
+ lineSpan: (line) => this.view.lineSpan(line),
432
+ structure: () => this.view.structureRanges(),
433
+ ...(this.wordSources === null ? {} : { wordSources: this.wordSources })
374
434
  };
375
435
  }
376
436
 
437
+ /* The texts Hippie completion takes words from after those of this document; null takes them away. */
438
+ setWordSources(source: (() => readonly string[]) | null): void {
439
+ this.wordSources = source;
440
+ }
441
+
377
442
  /* Folding is the view's, since it knows where the ranges are. */
378
443
  private viewCommand(name: EditorViewCommand): boolean {
379
444
  const { view } = this;
380
445
 
446
+ if (name in ZOOM_STEPS) {
447
+ this.options.zoom(ZOOM_STEPS[name as keyof typeof ZOOM_STEPS]);
448
+
449
+ return true;
450
+ }
451
+
452
+ if (name === 'methodUp' || name === 'methodDown') {
453
+ const from = view.model.getPrimary().head;
454
+ const moved = view.goToMethod(name === 'methodUp' ? -1 : 1);
455
+
456
+ if (moved) {
457
+ emit(this.navigateListeners, from);
458
+ }
459
+
460
+ return moved;
461
+ }
462
+
463
+ if (name === 'pasteAsPlainText') {
464
+ // The clipboard is read once a person asks, and an editor with no way to read it does nothing.
465
+ if (this.blockedByReadOnly() || globalThis.navigator?.clipboard?.readText === undefined) {
466
+ return false;
467
+ }
468
+
469
+ void navigator.clipboard.readText().then(
470
+ (text) => this.pasteText(text, { reindent: false }),
471
+ () => undefined
472
+ );
473
+
474
+ return true;
475
+ }
476
+
477
+ if (name === 'nextChange' || name === 'previousChange') {
478
+ return view.goToChange(name === 'nextChange' ? 1 : -1);
479
+ }
480
+
381
481
  if (view.settings.folding === false && name !== 'toggleColumnMode') {
382
482
  return false;
383
483
  }
384
484
 
385
- const run: Record<EditorViewCommand, () => void> = {
485
+ const run: Record<
486
+ Exclude<EditorViewCommand, keyof typeof ZOOM_STEPS | 'methodUp' | 'methodDown' | 'nextChange' | 'previousChange' | 'pasteAsPlainText'>,
487
+ () => void
488
+ > = {
386
489
  collapseRegion: () => view.collapseRegion(),
387
490
  expandRegion: () => view.expandRegion(),
388
491
  collapseAllRegions: () => view.foldAll(true),
@@ -400,11 +503,27 @@ export class InputController {
400
503
  toggleColumnMode: () => this.toggleColumnMode()
401
504
  };
402
505
 
403
- run[name]();
506
+ run[name as keyof typeof run]();
404
507
 
405
508
  return true;
406
509
  }
407
510
 
511
+ /* A pinch comes as the wheel with Ctrl, on every platform. */
512
+ private wheel(event: WheelEvent): void {
513
+ this.view.cancelScroll();
514
+
515
+ if (!(event.ctrlKey || (this.options.apple && event.metaKey)) || !this.options.wheelZooms()) {
516
+ return;
517
+ }
518
+
519
+ event.preventDefault();
520
+ const step = this.wheelZoom.step(event);
521
+
522
+ if (step !== 0) {
523
+ this.options.zoom(step as -1 | 1);
524
+ }
525
+ }
526
+
408
527
  /* Runs a model command on every caret, with the folds that would hide its result opened first. */
409
528
  command(name: EditorRunCommand): boolean {
410
529
  if (VIEW_COMMANDS.has(name as EditorViewCommand)) {
@@ -491,7 +610,11 @@ export class InputController {
491
610
  }
492
611
  }
493
612
 
494
- const folded = name === 'moveLineUp' || name === 'moveLineDown' ? this.view.getFolds().collapsed : [];
613
+ const before = model.getPrimary().head;
614
+ const folded =
615
+ name === 'moveLineUp' || name === 'moveLineDown' || name === 'moveStatementUp' || name === 'moveStatementDown'
616
+ ? this.view.getFolds().collapsed
617
+ : [];
495
618
 
496
619
  const result = model.execute(name, {
497
620
  ...this.commandOptions(),
@@ -506,11 +629,24 @@ export class InputController {
506
629
  this.view.markRowEnd(model.getPrimary().head);
507
630
  }
508
631
 
632
+ if (name === 'hippieCompletion' || name === 'hippieBackwardCompletion') {
633
+ this.view.setHippieSource(model.hippieSource);
634
+ }
635
+
509
636
  this.view.revealCaret();
510
637
 
638
+ if (CODE_BLOCK_COMMANDS.has(name) && model.getPrimary().head !== before) {
639
+ emit(this.navigateListeners, before);
640
+ }
641
+
511
642
  return result;
512
643
  }
513
644
 
645
+ /* Told the offset the primary caret left when a command that navigates moved it. */
646
+ onNavigate(listener: (from: number) => void): () => void {
647
+ return subscribe(this.navigateListeners, listener);
648
+ }
649
+
514
650
  private typeText(text: string): void {
515
651
  if (this.blockedByReadOnly()) {
516
652
  return;
@@ -799,6 +935,8 @@ export class InputController {
799
935
  return;
800
936
  }
801
937
 
938
+ this.view.setHippieSource(null);
939
+
802
940
  const added = this.gesture.keydown(event);
803
941
 
804
942
  if (added !== null) {
@@ -895,6 +1033,7 @@ export class InputController {
895
1033
 
896
1034
  event.clipboardData?.setData('text/plain', text);
897
1035
  lineClip = empty ? text : null;
1036
+ emit(this.copyListeners, { text, wholeLines: empty, cut });
898
1037
 
899
1038
  if (!cut || this.blockedByReadOnly()) {
900
1039
  return;
package/src/editor.css CHANGED
@@ -17,6 +17,7 @@
17
17
  --editor-added: var(--status-idle);
18
18
  --editor-modified: var(--accent);
19
19
  --editor-deleted: var(--status-error);
20
+ --editor-todo: var(--status-running);
20
21
  --editor-mark-number-mix: 72%;
21
22
  --editor-mark-bar-width: 3px;
22
23
  --editor-mark-gap: 4px;
@@ -38,6 +39,7 @@
38
39
  --editor-margin: color-mix(in srgb, var(--text) 16%, transparent);
39
40
  --editor-whitespace: color-mix(in srgb, var(--text) 32%, transparent);
40
41
  --editor-hatch: color-mix(in srgb, var(--text) 10%, transparent);
42
+ --editor-todo: var(--status-idle);
41
43
  --editor-mark-number-mix: 82%;
42
44
  --editor-sticky-shadow: color-mix(in srgb, black 60%, transparent);
43
45
  }
@@ -478,11 +480,15 @@
478
480
  left: 6px;
479
481
  background: var(--editor-occurrence);
480
482
  }
483
+ .se-tick-todo,
481
484
  .se-tick-error,
482
485
  .se-tick-warning,
483
486
  .se-tick-info {
484
487
  left: 10px;
485
488
  }
489
+ .se-tick-todo {
490
+ background: var(--editor-todo);
491
+ }
486
492
  /* The only part of the track that takes the pointer, with a wider reach than the 4px it draws. */
487
493
  .se-tick[data-offset] {
488
494
  pointer-events: auto;
@@ -646,11 +652,16 @@
646
652
  .se-find-match,
647
653
  .se-find-current,
648
654
  .se-occurrence,
655
+ .se-hippie-source,
649
656
  .se-bracket-match,
650
657
  .se-bracket-unmatched {
651
658
  position: absolute;
652
659
  border-radius: var(--radius-xs);
653
660
  }
661
+ .se-hippie-source {
662
+ background: color-mix(in srgb, var(--accent) 22%, transparent);
663
+ border-radius: var(--radius-sm);
664
+ }
654
665
  .se-find-match {
655
666
  background: var(--find-match);
656
667
  }
@@ -741,6 +752,14 @@
741
752
  white-space: pre;
742
753
  cursor: default;
743
754
  }
755
+ .se-swatch {
756
+ height: 12px;
757
+ margin-top: calc((var(--code-line-height, 20px) - 12px) / 2);
758
+ padding: 0;
759
+ border-radius: var(--radius-sm);
760
+ box-shadow: inset 0 0 0 1px var(--border);
761
+ cursor: pointer;
762
+ }
744
763
  .se-inlay-link {
745
764
  text-decoration: underline;
746
765
  text-underline-offset: 2px;
package/src/engine.ts CHANGED
@@ -17,6 +17,7 @@ import type {
17
17
  EditorCodeVision,
18
18
  EditorClickHandler,
19
19
  EditorContextMenu,
20
+ EditorCopy,
20
21
  EditorContentChange,
21
22
  EditorEngine,
22
23
  EditorFindQuery,
@@ -36,6 +37,7 @@ import type {
36
37
  EditorInlayHint,
37
38
  EditorInlayTarget,
38
39
  EditorKeyHandler,
40
+ EditorPasteOptions,
39
41
  EditorInputPopup,
40
42
  EditorMarker,
41
43
  EditorMessages,
@@ -53,11 +55,13 @@ import type {
53
55
  EditorTextChange,
54
56
  EditorTrackedRange,
55
57
  EditorTheme,
58
+ EditorWhitespaceCleanup,
56
59
  EditorWidget,
57
60
  SmartEditorEngineOptions
58
61
  } from './types.ts';
59
62
  import { fillLens, lensSignature } from './lens.ts';
60
63
  import { EditorView, type ViewSettings } from './view.ts';
64
+ import { clampFontSize, type FontSizeRange, fontSizeRange } from './zoom.ts';
61
65
 
62
66
  const DEFAULT_TAB_SIZE = 4;
63
67
  const DEFAULT_LABEL = 'Code editor';
@@ -77,6 +81,9 @@ class SmartEditor implements Editor {
77
81
  private caretOffset = 0;
78
82
  private readonly saves = new Set<Listener>();
79
83
  private readonly blurs = new Set<Listener>();
84
+ private readonly fontSizes = new Set<(size: number | null) => void>();
85
+ private readonly fontSizeRange: FontSizeRange;
86
+ private fontSize: number | null = null;
80
87
  private readonly subscription: { dispose(): void };
81
88
  private language: string | undefined;
82
89
  private theme: EditorTheme;
@@ -85,12 +92,15 @@ class SmartEditor implements Editor {
85
92
  private lensRevisions = new Map<string, { signature: string; revision: number }>();
86
93
  private tokenizerRequest = 0;
87
94
  private settingText = false;
95
+ /* The hints of each owner, in the order the owners first set theirs. */
96
+ private readonly inlayOwners = new Map<string, readonly EditorInlayHint[]>();
88
97
  private disposed = false;
89
98
 
90
99
  private readonly engine: SmartEditorEngineOptions;
91
100
 
92
101
  constructor(engine: SmartEditorEngineOptions, element: HTMLElement, options: EditorOptions) {
93
102
  this.engine = engine;
103
+ this.fontSizeRange = fontSizeRange(options.zoom);
94
104
  this.model = new DocumentModel(options.text);
95
105
  this.language = options.language;
96
106
  this.theme = options.theme;
@@ -111,7 +121,8 @@ class SmartEditor implements Editor {
111
121
  whitespace: options.whitespace === true,
112
122
  rightMargin: options.rightMargin ?? null,
113
123
  foldOutline: options.foldOutline ?? 'hover',
114
- folding: options.folding !== false
124
+ folding: options.folding !== false,
125
+ todoWords: options.todoWords
115
126
  };
116
127
 
117
128
  this.view = new EditorView(element, this.model, this.settings);
@@ -122,9 +133,15 @@ class SmartEditor implements Editor {
122
133
  keymap: engine.keymap,
123
134
  apple: engine.apple ?? false,
124
135
  save: () => emit(this.saves),
125
- blur: () => emit(this.blurs)
136
+ blur: () => emit(this.blurs),
137
+ zoom: (step) => this.zoom(step),
138
+ wheelZooms: () => options.zoom?.wheel !== false
126
139
  });
127
140
 
141
+ if (options.fontSize !== undefined) {
142
+ this.setFontSize(options.fontSize);
143
+ }
144
+
128
145
  this.revision = this.model.getRevision();
129
146
  this.caretOffset = this.model.getPrimary().head;
130
147
 
@@ -392,6 +409,17 @@ class SmartEditor implements Editor {
392
409
  }
393
410
  }
394
411
 
412
+ cleanUpWhitespace(options: EditorWhitespaceCleanup): boolean {
413
+ const changed = !this.settings.readOnly && this.model.cleanUpWhitespace({ ...options, language: this.settings.language });
414
+ this.model.markSaved();
415
+
416
+ return changed;
417
+ }
418
+
419
+ markSaved(): void {
420
+ this.model.markSaved();
421
+ }
422
+
395
423
  setMarkers(markers: readonly EditorMarker[]): void {
396
424
  this.view.setMarkers(
397
425
  markers.map((marker) => ({
@@ -405,15 +433,21 @@ class SmartEditor implements Editor {
405
433
  );
406
434
  }
407
435
 
408
- setInlayHints(hints: readonly EditorInlayHint[]): void {
436
+ setInlayHints(hints: readonly EditorInlayHint[], owner: string = DEFAULT_WIDGET_OWNER): void {
437
+ if (hints.length === 0) {
438
+ this.inlayOwners.delete(owner);
439
+ } else {
440
+ this.inlayOwners.set(owner, hints);
441
+ }
409
442
  this.view.setInlays(
410
- hints.map((hint, index) => ({
443
+ [...this.inlayOwners.values()].flat().map((hint, index) => ({
411
444
  id: hint.id ?? `hint-${index}`,
412
445
  at: this.offsetAt(hint.position),
413
446
  text: hint.label,
414
447
  ...(hint.parts === undefined ? {} : { parts: hint.parts }),
415
448
  ...(hint.paddingLeft ? { gapLeft: INLAY_GAP } : {}),
416
- ...(hint.paddingRight ? { gapRight: INLAY_GAP } : {})
449
+ ...(hint.paddingRight ? { gapRight: INLAY_GAP } : {}),
450
+ ...(hint.swatch === undefined ? {} : { swatch: hint.swatch })
417
451
  }))
418
452
  );
419
453
  }
@@ -905,10 +939,39 @@ class SmartEditor implements Editor {
905
939
  this.view.applySettings();
906
940
  }
907
941
 
942
+ setTodoWords(words: readonly string[]): void {
943
+ this.settings.todoWords = [...words];
944
+ this.view.applySettings();
945
+ }
946
+
908
947
  setFoldHints(hints: EditorFoldHints | null): void {
909
948
  this.view.setFoldHints(hints === null ? null : this.foldHintsByOffset(hints));
910
949
  }
911
950
 
951
+ setDeclarations(positions: readonly EditorPosition[] | null): void {
952
+ this.view.setDeclarations(positions === null ? null : positions.map((position) => this.offsetAt(position)));
953
+ }
954
+
955
+ onNavigate(listener: (from: EditorPosition) => void): () => void {
956
+ return this.controller.onNavigate((from) => listener(this.positionAt(from)));
957
+ }
958
+
959
+ hasFocus(): boolean {
960
+ return this.view.focused;
961
+ }
962
+
963
+ setWordSources(source: (() => readonly string[]) | null): void {
964
+ this.controller.setWordSources(source);
965
+ }
966
+
967
+ onCopy(listener: (copy: EditorCopy) => void): () => void {
968
+ return this.controller.onCopy(listener);
969
+ }
970
+
971
+ paste(text: string, options?: EditorPasteOptions): boolean {
972
+ return this.controller.pasteText(text, options);
973
+ }
974
+
912
975
  private foldHintsByOffset(hints: EditorFoldHints): FoldHints {
913
976
  const lineOf = (line: number): { start: number; end: number } => this.model.getLine(Math.min(line, this.model.getLineCount() - 1));
914
977
 
@@ -960,6 +1023,40 @@ class SmartEditor implements Editor {
960
1023
  this.view.refreshFont();
961
1024
  }
962
1025
 
1026
+ setFontSize(size: number | null): void {
1027
+ const next = size === null || !Number.isFinite(size) ? null : clampFontSize(size, this.fontSizeRange);
1028
+
1029
+ if (next !== this.fontSize) {
1030
+ this.fontSize = next;
1031
+ this.view.setFontSize(next);
1032
+ }
1033
+ }
1034
+
1035
+ getFontSize(): number {
1036
+ return this.view.getFontSize();
1037
+ }
1038
+
1039
+ onFontSizeChange(listener: (size: number | null) => void): () => void {
1040
+ return subscribe(this.fontSizes, listener);
1041
+ }
1042
+
1043
+ /* Steps from the size on screen, so the first step from the page's size is one pixel away from it. */
1044
+ private zoom(step: -1 | 0 | 1): void {
1045
+ const before = this.fontSize;
1046
+ const current = this.view.getFontSize();
1047
+
1048
+ // A page size outside the range must not make a step the other way.
1049
+ if (step !== 0 && Math.sign(clampFontSize(current + step, this.fontSizeRange) - current) !== step) {
1050
+ return;
1051
+ }
1052
+
1053
+ this.setFontSize(step === 0 ? null : current + step);
1054
+
1055
+ if (this.fontSize !== before) {
1056
+ emit(this.fontSizes, this.fontSize);
1057
+ }
1058
+ }
1059
+
963
1060
  setReadOnly(readOnly: boolean, reason?: string): void {
964
1061
  this.settings.readOnly = readOnly;
965
1062
  this.settings.readOnlyReason = reason;
@@ -994,6 +1091,7 @@ class SmartEditor implements Editor {
994
1091
  this.carets.clear();
995
1092
  this.saves.clear();
996
1093
  this.blurs.clear();
1094
+ this.fontSizes.clear();
997
1095
  this.subscription.dispose();
998
1096
  this.controller.dispose();
999
1097
  this.view.dispose();