@adecore/editor 0.41.0 → 0.42.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/view.js CHANGED
@@ -5,6 +5,7 @@ import { createSearchClient } from './search-client.js';
5
5
  import { FindController } from './find.js';
6
6
  import { EditorLayout, RIGHT_PADDING, subRowBand } from './layout.js';
7
7
  import { createMetrics, readEditorFont } from './metrics.js';
8
+ import { zoomedFont } from './zoom.js';
8
9
  import { AttributionRuns, colorValue, tintValue } from './attribution.js';
9
10
  import { renderCodeBlock } from './code-block.js';
10
11
  import { emit, subscribe } from './listeners.js';
@@ -16,6 +17,7 @@ import { Outline, scopeChain, stickyCover, stickyPlacements, structuralEntries }
16
17
  import { overviewTicks, paintOverview } from './overview.js';
17
18
  import { paintCarets, paintGutter, paintOver, paintOverlays, paintRemoteCarets, paintSigns, paintSticky, RowPainter } from './paint.js';
18
19
  import { overlayTokens } from './semantic.js';
20
+ import { TodoMarks } from './todos.js';
19
21
  import { TokenCache } from './tokens.js';
20
22
  function sameRect(left, right) {
21
23
  return left.left === right.left && left.top === right.top && left.right === right.right && left.bottom === right.bottom;
@@ -79,6 +81,7 @@ export class EditorView {
79
81
  layout;
80
82
  find;
81
83
  tokens;
84
+ todos;
82
85
  content;
83
86
  gutter;
84
87
  gutterLines;
@@ -147,6 +150,8 @@ export class EditorView {
147
150
  caretTimer;
148
151
  caretKey = '';
149
152
  renderedScroll = { top: -1, left: -1 };
153
+ /* A size the code is zoomed to, over the page's; null draws it at the page's. */
154
+ fontSize = null;
150
155
  font;
151
156
  layoutVersion = 0;
152
157
  revision;
@@ -281,6 +286,7 @@ export class EditorView {
281
286
  }
282
287
  });
283
288
  this.tokens = new TokenCache({ getLineCount: () => model.getLineCount(), getLineText: (line) => model.getLine(line).text });
289
+ this.todos = new TodoMarks(model, () => this.requestRender());
284
290
  const ResizeObserverClass = document.defaultView?.ResizeObserver;
285
291
  this.blockObserver = ResizeObserverClass
286
292
  ? new ResizeObserverClass((entries) => {
@@ -311,6 +317,7 @@ export class EditorView {
311
317
  this.root.dataset.readonly = String(this.settings.readOnly);
312
318
  this.root.dataset.wrap = String(this.settings.wrap);
313
319
  this.root.dataset.foldOutline = this.settings.foldOutline;
320
+ this.todos.configure(this.settings.language, this.settings.todoWords);
314
321
  this.layoutVersion++;
315
322
  this.layout.configure({ metrics: createMetrics(this.font, this.settings.tabSize, this.document) });
316
323
  this.render();
@@ -320,8 +327,26 @@ export class EditorView {
320
327
  this.render();
321
328
  }
322
329
  refreshFont() {
323
- this.font = readEditorFont(this.root);
324
- this.applySettings();
330
+ const style = this.root.style;
331
+ style.removeProperty('--code-font-size');
332
+ style.removeProperty('--code-line-height');
333
+ const base = readEditorFont(this.root);
334
+ this.font = this.fontSize === null ? base : zoomedFont(base, this.fontSize);
335
+ // On the element itself, so everything the editor draws at the code's size follows, and the page's size stays the base.
336
+ if (this.fontSize !== null) {
337
+ style.setProperty('--code-font-size', `${this.font.size}px`);
338
+ style.setProperty('--code-line-height', `${this.font.lineHeight}px`);
339
+ }
340
+ this.anchorScroll(() => this.applySettings());
341
+ }
342
+ /* Draws the code at a size of its own in whole pixels; null goes back to the page's. */
343
+ setFontSize(size) {
344
+ this.fontSize = size;
345
+ this.refreshFont();
346
+ }
347
+ /* The size the code is drawn at, in pixels. */
348
+ getFontSize() {
349
+ return this.font.size;
325
350
  }
326
351
  setTokenizer(tokenizer) {
327
352
  this.tokens.setTokenizer(tokenizer);
@@ -421,8 +446,12 @@ export class EditorView {
421
446
  this.semanticLines.clear();
422
447
  this.render();
423
448
  }
424
- /* The grammar's colors of a line with the servers' laid over them, the same object for as long as neither changed. */
449
+ /* The colors of a line with its TODOs drawn over them, the same object for as long as nothing under them changed. */
425
450
  styledTokensOf(line) {
451
+ return this.todos.styled(line, this.semanticTokensOf(line));
452
+ }
453
+ /* The grammar's colors of a line with the servers' laid over them, the same object for as long as neither changed. */
454
+ semanticTokensOf(line) {
426
455
  const base = this.lineTokenProvider === null ? this.tokens.tokensOf(line) : (this.lineTokenProvider(line) ?? null);
427
456
  if (base === null || this.semantic.length === 0 || this.scopeColors === null) {
428
457
  return base;
@@ -615,6 +644,7 @@ export class EditorView {
615
644
  this.find.refresh();
616
645
  this.announceFind();
617
646
  this.scheduleFolds();
647
+ this.todos.schedule();
618
648
  this.colorAhead(true);
619
649
  }
620
650
  if (this.rowEndCaret !== null && this.model.getPrimary().head !== this.rowEndCaret) {
@@ -2228,7 +2258,7 @@ export class EditorView {
2228
2258
  const matches = this.find.matches;
2229
2259
  const current = this.find.current;
2230
2260
  const alike = this.selectionOccurrences();
2231
- const key = `${this.revision}|${this.changeVersion}|${this.markerVersion}|${this.layout.height}|${trackHeight}|${matches.length}|${current}|${matches[0]?.from}|${matches.at(-1)?.from}|${this.selectionOccurrenceCache.key}`;
2261
+ const key = `${this.revision}|${this.changeVersion}|${this.markerVersion}|${this.todos.version}|${this.layout.height}|${trackHeight}|${matches.length}|${current}|${matches[0]?.from}|${matches.at(-1)?.from}|${this.selectionOccurrenceCache.key}`;
2232
2262
  if (key === this.overviewKey) {
2233
2263
  return;
2234
2264
  }
@@ -2255,6 +2285,11 @@ export class EditorView {
2255
2285
  });
2256
2286
  }
2257
2287
  }
2288
+ for (const todo of this.todos.marks) {
2289
+ if (todo.line < this.model.getLineCount()) {
2290
+ spans.push({ kind: 'todo', top: rowTop(todo.line), bottom: rowBottom(todo.line), offset: todo.from, title: todo.text });
2291
+ }
2292
+ }
2258
2293
  for (const range of alike) {
2259
2294
  const line = this.model.positionAt(range.from).line;
2260
2295
  spans.push({ kind: 'occurrence', top: rowTop(line), bottom: rowBottom(line) });
@@ -2427,6 +2462,7 @@ export class EditorView {
2427
2462
  this.blockObserver?.disconnect();
2428
2463
  clearTimeout(this.bracketTimer);
2429
2464
  clearTimeout(this.foldTimer);
2465
+ this.todos.dispose();
2430
2466
  clearTimeout(this.colorTimer);
2431
2467
  clearTimeout(this.noticeTimer);
2432
2468
  clearTimeout(this.liveTimer);
package/dist/zoom.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import type { EditorFont } from './metrics.ts';
2
+ import type { EditorZoom } from './types.ts';
3
+ export declare const MIN_FONT_SIZE = 12;
4
+ export declare const MAX_FONT_SIZE = 90;
5
+ export interface FontSizeRange {
6
+ readonly min: number;
7
+ readonly max: number;
8
+ }
9
+ export declare function fontSizeRange(zoom: EditorZoom | undefined): FontSizeRange;
10
+ export declare function clampFontSize(size: number, range: FontSizeRange): number;
11
+ export declare function zoomedFont(base: EditorFont, size: number): EditorFont;
12
+ export declare class WheelZoom {
13
+ private pending;
14
+ step(event: {
15
+ deltaY: number;
16
+ deltaMode: number;
17
+ }): number;
18
+ }
package/dist/zoom.js ADDED
@@ -0,0 +1,41 @@
1
+ /* Nothing a person reads is smaller. */
2
+ export const MIN_FONT_SIZE = 12;
3
+ export const MAX_FONT_SIZE = 90;
4
+ /* A trackpad or a pinch sends the wheel in small steps; this many pixels of them are one step of the size. */
5
+ const PIXELS_PER_STEP = 20;
6
+ export function fontSizeRange(zoom) {
7
+ const min = Math.max(MIN_FONT_SIZE, Math.round(zoom?.min ?? MIN_FONT_SIZE));
8
+ return { min, max: Math.max(min, Math.round(zoom?.max ?? MAX_FONT_SIZE)) };
9
+ }
10
+ /* In whole pixels inside the range. */
11
+ export function clampFontSize(size, range) {
12
+ return Math.min(range.max, Math.max(range.min, Math.round(size)));
13
+ }
14
+ /* The face at another size, with the line as much taller as the page's line is to its text, in whole pixels. */
15
+ export function zoomedFont(base, size) {
16
+ return { family: base.family, size, lineHeight: Math.round((size * base.lineHeight) / base.size) };
17
+ }
18
+ /* Turns the wheel with the zoom modifier, or a pinch, into steps of one pixel: up and apart make the text larger. */
19
+ export class WheelZoom {
20
+ pending = 0;
21
+ /* -1, 0 or 1. A mouse notch is one step however far the platform says it went. */
22
+ step(event) {
23
+ if (event.deltaY === 0) {
24
+ return 0;
25
+ }
26
+ if (event.deltaMode !== 0 || Math.abs(event.deltaY) >= PIXELS_PER_STEP) {
27
+ this.pending = 0;
28
+ return event.deltaY < 0 ? 1 : -1;
29
+ }
30
+ if (Math.sign(this.pending) !== 0 && Math.sign(this.pending) !== Math.sign(event.deltaY)) {
31
+ this.pending = 0;
32
+ }
33
+ this.pending += event.deltaY;
34
+ if (Math.abs(this.pending) < PIXELS_PER_STEP) {
35
+ return 0;
36
+ }
37
+ const direction = this.pending < 0 ? 1 : -1;
38
+ this.pending = 0;
39
+ return direction;
40
+ }
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/editor",
3
- "version": "0.41.0",
3
+ "version": "0.42.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -54,7 +54,7 @@
54
54
  "test": "bun test --conditions=source"
55
55
  },
56
56
  "dependencies": {
57
- "@adecore/editor-core": "0.41.0",
57
+ "@adecore/editor-core": "0.42.0",
58
58
  "linkedom": "^0.18.14",
59
59
  "shiki": "^4.5.0"
60
60
  },
package/src/controller.ts CHANGED
@@ -8,6 +8,7 @@ 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,
@@ -29,6 +30,10 @@ export interface ControllerOptions {
29
30
  apple: boolean;
30
31
  save(): void;
31
32
  blur(): void;
33
+ /* One pixel larger (1) or smaller (-1), or back to the page's size (0). */
34
+ zoom(step: -1 | 0 | 1): void;
35
+ /* Whether the zoom modifier with the wheel, or a pinch, zooms. */
36
+ wheelZooms(): boolean;
32
37
  }
33
38
 
34
39
  const MODIFYING = new Set<EditorCommand>([
@@ -53,7 +58,12 @@ const MODIFYING = new Set<EditorCommand>([
53
58
  'outdent',
54
59
  'insertNewline',
55
60
  'smartBackspace',
56
- 'deleteForward'
61
+ 'deleteForward',
62
+ 'sortLines',
63
+ 'reverseLines',
64
+ 'uniqueLines',
65
+ 'convertIndentsToSpaces',
66
+ 'convertIndentsToTabs'
57
67
  ]);
58
68
 
59
69
  const VIEW_COMMANDS = new Set<EditorViewCommand>([
@@ -71,9 +81,14 @@ const VIEW_COMMANDS = new Set<EditorViewCommand>([
71
81
  'expandAllToLevel3',
72
82
  'expandAllToLevel4',
73
83
  'expandAllToLevel5',
74
- 'toggleColumnMode'
84
+ 'toggleColumnMode',
85
+ 'increaseFontSize',
86
+ 'decreaseFontSize',
87
+ 'resetFontSize'
75
88
  ]);
76
89
 
90
+ const ZOOM_STEPS = { increaseFontSize: 1, decreaseFontSize: -1, resetFontSize: 0 } as const satisfies Partial<Record<EditorViewCommand, -1 | 0 | 1>>;
91
+
77
92
  /* 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
93
  let lineClip: string | null = null;
79
94
 
@@ -109,6 +124,7 @@ export class InputController {
109
124
  /* The Extend Selection presses that wait for a server's ranges, which are answered one after the other. */
110
125
  private expansions: Promise<void> = Promise.resolve();
111
126
  private lastInputAt = 0;
127
+ private readonly wheelZoom = new WheelZoom();
112
128
  private desiredXs: number[] = [];
113
129
  private keepDesiredXs = false;
114
130
  private readonly view: EditorView;
@@ -230,7 +246,7 @@ export class InputController {
230
246
  this.listen(input, 'copy', (event) => this.copy(event, false));
231
247
  this.listen(input, 'cut', (event) => this.copy(event, true));
232
248
  this.listen(viewport, 'scroll', () => this.view.scrolled());
233
- this.listen(viewport, 'wheel', () => this.view.cancelScroll());
249
+ this.listen(viewport, 'wheel', (event) => this.wheel(event));
234
250
  this.listen(viewport, 'pointermove', (event) => this.view.hoverMoved(event));
235
251
 
236
252
  this.listen(viewport, 'pointerleave', () => {
@@ -378,11 +394,17 @@ export class InputController {
378
394
  private viewCommand(name: EditorViewCommand): boolean {
379
395
  const { view } = this;
380
396
 
397
+ if (name in ZOOM_STEPS) {
398
+ this.options.zoom(ZOOM_STEPS[name as keyof typeof ZOOM_STEPS]);
399
+
400
+ return true;
401
+ }
402
+
381
403
  if (view.settings.folding === false && name !== 'toggleColumnMode') {
382
404
  return false;
383
405
  }
384
406
 
385
- const run: Record<EditorViewCommand, () => void> = {
407
+ const run: Record<Exclude<EditorViewCommand, keyof typeof ZOOM_STEPS>, () => void> = {
386
408
  collapseRegion: () => view.collapseRegion(),
387
409
  expandRegion: () => view.expandRegion(),
388
410
  collapseAllRegions: () => view.foldAll(true),
@@ -400,11 +422,27 @@ export class InputController {
400
422
  toggleColumnMode: () => this.toggleColumnMode()
401
423
  };
402
424
 
403
- run[name]();
425
+ run[name as keyof typeof run]();
404
426
 
405
427
  return true;
406
428
  }
407
429
 
430
+ /* A pinch comes as the wheel with Ctrl, on every platform. */
431
+ private wheel(event: WheelEvent): void {
432
+ this.view.cancelScroll();
433
+
434
+ if (!(event.ctrlKey || (this.options.apple && event.metaKey)) || !this.options.wheelZooms()) {
435
+ return;
436
+ }
437
+
438
+ event.preventDefault();
439
+ const step = this.wheelZoom.step(event);
440
+
441
+ if (step !== 0) {
442
+ this.options.zoom(step as -1 | 1);
443
+ }
444
+ }
445
+
408
446
  /* Runs a model command on every caret, with the folds that would hide its result opened first. */
409
447
  command(name: EditorRunCommand): boolean {
410
448
  if (VIEW_COMMANDS.has(name as EditorViewCommand)) {
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;
package/src/engine.ts CHANGED
@@ -53,11 +53,13 @@ import type {
53
53
  EditorTextChange,
54
54
  EditorTrackedRange,
55
55
  EditorTheme,
56
+ EditorWhitespaceCleanup,
56
57
  EditorWidget,
57
58
  SmartEditorEngineOptions
58
59
  } from './types.ts';
59
60
  import { fillLens, lensSignature } from './lens.ts';
60
61
  import { EditorView, type ViewSettings } from './view.ts';
62
+ import { clampFontSize, type FontSizeRange, fontSizeRange } from './zoom.ts';
61
63
 
62
64
  const DEFAULT_TAB_SIZE = 4;
63
65
  const DEFAULT_LABEL = 'Code editor';
@@ -77,6 +79,9 @@ class SmartEditor implements Editor {
77
79
  private caretOffset = 0;
78
80
  private readonly saves = new Set<Listener>();
79
81
  private readonly blurs = new Set<Listener>();
82
+ private readonly fontSizes = new Set<(size: number | null) => void>();
83
+ private readonly fontSizeRange: FontSizeRange;
84
+ private fontSize: number | null = null;
80
85
  private readonly subscription: { dispose(): void };
81
86
  private language: string | undefined;
82
87
  private theme: EditorTheme;
@@ -91,6 +96,7 @@ class SmartEditor implements Editor {
91
96
 
92
97
  constructor(engine: SmartEditorEngineOptions, element: HTMLElement, options: EditorOptions) {
93
98
  this.engine = engine;
99
+ this.fontSizeRange = fontSizeRange(options.zoom);
94
100
  this.model = new DocumentModel(options.text);
95
101
  this.language = options.language;
96
102
  this.theme = options.theme;
@@ -111,7 +117,8 @@ class SmartEditor implements Editor {
111
117
  whitespace: options.whitespace === true,
112
118
  rightMargin: options.rightMargin ?? null,
113
119
  foldOutline: options.foldOutline ?? 'hover',
114
- folding: options.folding !== false
120
+ folding: options.folding !== false,
121
+ todoWords: options.todoWords
115
122
  };
116
123
 
117
124
  this.view = new EditorView(element, this.model, this.settings);
@@ -122,9 +129,15 @@ class SmartEditor implements Editor {
122
129
  keymap: engine.keymap,
123
130
  apple: engine.apple ?? false,
124
131
  save: () => emit(this.saves),
125
- blur: () => emit(this.blurs)
132
+ blur: () => emit(this.blurs),
133
+ zoom: (step) => this.zoom(step),
134
+ wheelZooms: () => options.zoom?.wheel !== false
126
135
  });
127
136
 
137
+ if (options.fontSize !== undefined) {
138
+ this.setFontSize(options.fontSize);
139
+ }
140
+
128
141
  this.revision = this.model.getRevision();
129
142
  this.caretOffset = this.model.getPrimary().head;
130
143
 
@@ -392,6 +405,17 @@ class SmartEditor implements Editor {
392
405
  }
393
406
  }
394
407
 
408
+ cleanUpWhitespace(options: EditorWhitespaceCleanup): boolean {
409
+ const changed = !this.settings.readOnly && this.model.cleanUpWhitespace({ ...options, language: this.settings.language });
410
+ this.model.markSaved();
411
+
412
+ return changed;
413
+ }
414
+
415
+ markSaved(): void {
416
+ this.model.markSaved();
417
+ }
418
+
395
419
  setMarkers(markers: readonly EditorMarker[]): void {
396
420
  this.view.setMarkers(
397
421
  markers.map((marker) => ({
@@ -905,6 +929,11 @@ class SmartEditor implements Editor {
905
929
  this.view.applySettings();
906
930
  }
907
931
 
932
+ setTodoWords(words: readonly string[]): void {
933
+ this.settings.todoWords = [...words];
934
+ this.view.applySettings();
935
+ }
936
+
908
937
  setFoldHints(hints: EditorFoldHints | null): void {
909
938
  this.view.setFoldHints(hints === null ? null : this.foldHintsByOffset(hints));
910
939
  }
@@ -960,6 +989,40 @@ class SmartEditor implements Editor {
960
989
  this.view.refreshFont();
961
990
  }
962
991
 
992
+ setFontSize(size: number | null): void {
993
+ const next = size === null || !Number.isFinite(size) ? null : clampFontSize(size, this.fontSizeRange);
994
+
995
+ if (next !== this.fontSize) {
996
+ this.fontSize = next;
997
+ this.view.setFontSize(next);
998
+ }
999
+ }
1000
+
1001
+ getFontSize(): number {
1002
+ return this.view.getFontSize();
1003
+ }
1004
+
1005
+ onFontSizeChange(listener: (size: number | null) => void): () => void {
1006
+ return subscribe(this.fontSizes, listener);
1007
+ }
1008
+
1009
+ /* Steps from the size on screen, so the first step from the page's size is one pixel away from it. */
1010
+ private zoom(step: -1 | 0 | 1): void {
1011
+ const before = this.fontSize;
1012
+ const current = this.view.getFontSize();
1013
+
1014
+ // A page size outside the range must not make a step the other way.
1015
+ if (step !== 0 && Math.sign(clampFontSize(current + step, this.fontSizeRange) - current) !== step) {
1016
+ return;
1017
+ }
1018
+
1019
+ this.setFontSize(step === 0 ? null : current + step);
1020
+
1021
+ if (this.fontSize !== before) {
1022
+ emit(this.fontSizes, this.fontSize);
1023
+ }
1024
+ }
1025
+
963
1026
  setReadOnly(readOnly: boolean, reason?: string): void {
964
1027
  this.settings.readOnly = readOnly;
965
1028
  this.settings.readOnlyReason = reason;
@@ -994,6 +1057,7 @@ class SmartEditor implements Editor {
994
1057
  this.carets.clear();
995
1058
  this.saves.clear();
996
1059
  this.blurs.clear();
1060
+ this.fontSizes.clear();
997
1061
  this.subscription.dispose();
998
1062
  this.controller.dispose();
999
1063
  this.view.dispose();
package/src/fake.ts CHANGED
@@ -49,6 +49,7 @@ import type {
49
49
  EditorTextChange,
50
50
  EditorTrackedRange,
51
51
  EditorTheme,
52
+ EditorWhitespaceCleanup,
52
53
  EditorWidget
53
54
  } from './types.ts';
54
55
  import { emit, type Listener, subscribe } from './listeners.ts';
@@ -254,6 +255,8 @@ export class FakeEditor implements Editor {
254
255
  this.readOnly = options.readOnly ?? false;
255
256
  this.readOnlyReason = options.readOnlyReason;
256
257
  this.label = options.label ?? 'Code editor';
258
+ this.todoWords = options.todoWords;
259
+ this.fontSize = options.fontSize ?? null;
257
260
  this.revealedLine = options.line ?? null;
258
261
  this.column = options.column ?? null;
259
262
  this.scrollTop = options.scrollTop ?? null;
@@ -810,6 +813,12 @@ export class FakeEditor implements Editor {
810
813
  this.label = label;
811
814
  }
812
815
 
816
+ todoWords: readonly string[] | undefined;
817
+
818
+ setTodoWords(words: readonly string[]): void {
819
+ this.todoWords = [...words];
820
+ }
821
+
813
822
  setFoldHints(hints: EditorFoldHints | null): void {
814
823
  this.foldHints = hints;
815
824
  }
@@ -1126,6 +1135,22 @@ export class FakeEditor implements Editor {
1126
1135
  return this.edit(ordered, 'command', options.mergeUndo === true, place);
1127
1136
  }
1128
1137
 
1138
+ /* The options of every cleanup asked for, in order; the fake changes no text for them. */
1139
+ readonly cleanups: EditorWhitespaceCleanup[] = [];
1140
+ /* How often the text was marked as saved, by a cleanup or on its own. */
1141
+ savedMarks = 0;
1142
+
1143
+ cleanUpWhitespace(options: EditorWhitespaceCleanup): boolean {
1144
+ this.cleanups.push({ ...options });
1145
+ this.markSaved();
1146
+
1147
+ return false;
1148
+ }
1149
+
1150
+ markSaved(): void {
1151
+ this.savedMarks += 1;
1152
+ }
1153
+
1129
1154
  setTheme(theme: EditorTheme): void {
1130
1155
  this.theme = theme;
1131
1156
  }
@@ -1134,6 +1159,28 @@ export class FakeEditor implements Editor {
1134
1159
  this.fontRefreshes += 1;
1135
1160
  }
1136
1161
 
1162
+ /* The size `setFontSize` set, null for the page's. */
1163
+ fontSize: number | null = null;
1164
+ private readonly fontSizeListeners = new Set<(size: number | null) => void>();
1165
+
1166
+ setFontSize(size: number | null): void {
1167
+ this.fontSize = size;
1168
+ }
1169
+
1170
+ getFontSize(): number {
1171
+ return this.fontSize ?? 13;
1172
+ }
1173
+
1174
+ onFontSizeChange(listener: (size: number | null) => void): () => void {
1175
+ return subscribe(this.fontSizeListeners, listener);
1176
+ }
1177
+
1178
+ /* What a person zooming would do: the new size goes to the listeners. */
1179
+ zoomTo(size: number | null): void {
1180
+ this.fontSize = size;
1181
+ emit(this.fontSizeListeners, size);
1182
+ }
1183
+
1137
1184
  setReadOnly(readOnly: boolean, reason?: string): void {
1138
1185
  this.readOnly = readOnly;
1139
1186
  this.readOnlyReason = reason;
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { MAX_REPLACEMENT_PAGE, MAX_REPLACEMENT_CHARACTERS } from './search-protocol.ts';
2
2
  export { AGENT_COLORS } from './attribution.ts';
3
3
  export { findDocComments, MAX_DOC_COMMENTS, type DocComment } from '@adecore/editor-core';
4
+ export { DEFAULT_TODO_WORDS, findTodos, type TodoMatch, type TodoOptions } from '@adecore/editor-core';
5
+ export { detectIndentation, type DetectedIndentation } from '@adecore/editor-core';
4
6
  export { createSmartEditorEngine } from './engine.ts';
5
7
  export { DEFAULT_SMART_KEYS } from './smart-keys.ts';
6
8
  export { shikiScopeColors, shikiTokenizers } from './shiki.ts';
@@ -65,7 +67,9 @@ export type {
65
67
  EditorSelectionRanges,
66
68
  EditorTheme,
67
69
  EditorViewCommand,
70
+ EditorWhitespaceCleanup,
68
71
  EditorWidget,
72
+ EditorZoom,
69
73
  FoldRole,
70
74
  KeyChord,
71
75
  LineToken,
@@ -41,6 +41,12 @@ export const KEYMAP = {
41
41
  splitLine: bind('Mod+Enter'),
42
42
  toggleCase: bind('Mod+Shift+U'),
43
43
  autoIndentLines: bind('Ctrl+Alt+I'),
44
+ // The platform binds no chord to these, so they live in the menu and the palette.
45
+ sortLines: bind(null),
46
+ reverseLines: bind(null),
47
+ uniqueLines: bind(null),
48
+ convertIndentsToSpaces: bind(null),
49
+ convertIndentsToTabs: bind(null),
44
50
 
45
51
  expandSelection: bind('Alt+ArrowUp', 'Ctrl+W'),
46
52
  shrinkSelection: bind('Alt+ArrowDown'),
@@ -85,6 +91,10 @@ export const KEYMAP = {
85
91
  expandAllToLevel3: bind(null),
86
92
  expandAllToLevel4: bind(null),
87
93
  expandAllToLevel5: bind(null),
94
+ // The platform binds no chord to these, and the zoom chords of other editors are the fold keys here.
95
+ increaseFontSize: bind(null),
96
+ decreaseFontSize: bind(null),
97
+ resetFontSize: bind(null),
88
98
 
89
99
  triggerCompletion: bind('Ctrl+Space'),
90
100
  parameterInfo: bind('Mod+P'),
@@ -123,7 +133,16 @@ export const KEYMAP = {
123
133
  export type KeymapId = keyof typeof KEYMAP;
124
134
 
125
135
  /* Commands added after a host may have written a full keymap of its own; a keymap without one falls back to the built-in chord. */
126
- export type OptionalKeymapId = 'toggleRenderedDocs';
136
+ export type OptionalKeymapId =
137
+ | 'toggleRenderedDocs'
138
+ | 'increaseFontSize'
139
+ | 'decreaseFontSize'
140
+ | 'resetFontSize'
141
+ | 'sortLines'
142
+ | 'reverseLines'
143
+ | 'uniqueLines'
144
+ | 'convertIndentsToSpaces'
145
+ | 'convertIndentsToTabs';
127
146
 
128
147
  export type Keymap = Readonly<Record<Exclude<KeymapId, OptionalKeymapId>, KeyBinding> & Partial<Record<OptionalKeymapId, KeyBinding>>>;
129
148
  export type KeymapOverrides = Partial<Record<KeymapId, Partial<KeyBinding>>>;
package/src/keymap.ts CHANGED
@@ -52,6 +52,11 @@ const ACTIONS: Partial<Record<KeymapId, KeyAction>> = {
52
52
  splitLine: command('splitLine'),
53
53
  toggleCase: command('toggleCase'),
54
54
  autoIndentLines: command('autoIndentLines'),
55
+ sortLines: command('sortLines'),
56
+ reverseLines: command('reverseLines'),
57
+ uniqueLines: command('uniqueLines'),
58
+ convertIndentsToSpaces: command('convertIndentsToSpaces'),
59
+ convertIndentsToTabs: command('convertIndentsToTabs'),
55
60
  expandSelection: command('expandSelection'),
56
61
  shrinkSelection: command('shrinkSelection'),
57
62
  matchBrace: { type: 'bracket' },
@@ -87,7 +92,10 @@ const ACTIONS: Partial<Record<KeymapId, KeyAction>> = {
87
92
  expandAllToLevel3: view('expandAllToLevel3'),
88
93
  expandAllToLevel4: view('expandAllToLevel4'),
89
94
  expandAllToLevel5: view('expandAllToLevel5'),
90
- toggleColumnMode: view('toggleColumnMode')
95
+ toggleColumnMode: view('toggleColumnMode'),
96
+ increaseFontSize: view('increaseFontSize'),
97
+ decreaseFontSize: view('decreaseFontSize'),
98
+ resetFontSize: view('resetFontSize')
91
99
  };
92
100
 
93
101
  const PUNCTUATION_CODES: Readonly<Record<string, string>> = {
package/src/overview.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type OverviewKind = 'added' | 'modified' | 'deleted' | 'find' | 'find-current' | 'occurrence' | 'info' | 'warning' | 'error';
1
+ export type OverviewKind = 'added' | 'modified' | 'deleted' | 'find' | 'find-current' | 'occurrence' | 'todo' | 'info' | 'warning' | 'error';
2
2
 
3
3
  /* A stretch of the content, in the pixels the content is laid out in. */
4
4
  export interface OverviewSpan {