@adecore/editor 0.41.1 → 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/controller.d.ts +4 -0
- package/dist/controller.js +30 -3
- package/dist/editor.css +6 -0
- package/dist/engine.js +52 -2
- package/dist/fake.d.ts +13 -1
- package/dist/fake.js +35 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/keymap-table.d.ts +9 -1
- package/dist/keymap-table.js +10 -0
- package/dist/keymap.js +9 -1
- package/dist/overview.d.ts +1 -1
- package/dist/todos.d.ts +23 -0
- package/dist/todos.js +86 -0
- package/dist/types.d.ts +17 -2
- package/dist/view.d.ts +6 -0
- package/dist/view.js +40 -4
- package/dist/zoom.d.ts +18 -0
- package/dist/zoom.js +41 -0
- package/package.json +2 -2
- package/src/controller.ts +43 -5
- package/src/editor.css +6 -0
- package/src/engine.ts +66 -2
- package/src/fake.ts +47 -0
- package/src/index.ts +4 -0
- package/src/keymap-table.ts +20 -1
- package/src/keymap.ts +9 -1
- package/src/overview.ts +1 -1
- package/src/todos.ts +111 -0
- package/src/types.ts +40 -3
- package/src/view.ts +48 -4
- package/src/zoom.ts +62 -0
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
|
-
|
|
324
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
package/src/keymap-table.ts
CHANGED
|
@@ -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 =
|
|
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 {
|