@adecore/editor 0.0.1 → 0.17.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +82 -0
  3. package/dist/attribution.d.ts +28 -0
  4. package/dist/attribution.js +55 -0
  5. package/dist/code-block.d.ts +2 -0
  6. package/dist/code-block.js +89 -0
  7. package/dist/column-selection.d.ts +9 -0
  8. package/dist/column-selection.js +52 -0
  9. package/dist/controller.d.ts +64 -0
  10. package/dist/controller.js +772 -0
  11. package/dist/editor.css +813 -0
  12. package/dist/engine.d.ts +2 -0
  13. package/dist/engine.js +604 -0
  14. package/dist/fake.d.ts +190 -0
  15. package/dist/fake.js +658 -0
  16. package/dist/find.d.ts +30 -0
  17. package/dist/find.js +108 -0
  18. package/dist/index.d.ts +5 -0
  19. package/dist/index.js +4 -0
  20. package/dist/input.d.ts +9 -0
  21. package/dist/input.js +57 -0
  22. package/dist/keymap-table.d.ts +102 -0
  23. package/dist/keymap-table.js +130 -0
  24. package/dist/keymap.d.ts +36 -0
  25. package/dist/keymap.js +163 -0
  26. package/dist/layout.d.ts +167 -0
  27. package/dist/layout.js +583 -0
  28. package/dist/lens.d.ts +3 -0
  29. package/dist/lens.js +29 -0
  30. package/dist/listeners.d.ts +3 -0
  31. package/dist/listeners.js +12 -0
  32. package/dist/metrics.d.ts +8 -0
  33. package/dist/metrics.js +54 -0
  34. package/dist/modifier-gesture.d.ts +24 -0
  35. package/dist/modifier-gesture.js +98 -0
  36. package/dist/native-input.d.ts +26 -0
  37. package/dist/native-input.js +147 -0
  38. package/dist/offsets.d.ts +2 -0
  39. package/dist/offsets.js +14 -0
  40. package/dist/outline.d.ts +55 -0
  41. package/dist/outline.js +181 -0
  42. package/dist/overview.d.ts +17 -0
  43. package/dist/overview.js +55 -0
  44. package/dist/paint.d.ts +86 -0
  45. package/dist/paint.js +481 -0
  46. package/dist/php-html.d.ts +9 -0
  47. package/dist/php-html.js +36 -0
  48. package/dist/pointer.d.ts +49 -0
  49. package/dist/pointer.js +243 -0
  50. package/dist/scroll-animation.d.ts +3 -0
  51. package/dist/scroll-animation.js +45 -0
  52. package/dist/scroll.d.ts +28 -0
  53. package/dist/scroll.js +76 -0
  54. package/dist/semantic.d.ts +8 -0
  55. package/dist/semantic.js +47 -0
  56. package/dist/shiki.d.ts +7 -0
  57. package/dist/shiki.js +106 -0
  58. package/dist/smart-keys.d.ts +3 -0
  59. package/dist/smart-keys.js +14 -0
  60. package/dist/testing.d.ts +34 -0
  61. package/dist/testing.js +117 -0
  62. package/dist/theme-scopes.d.ts +9 -0
  63. package/dist/theme-scopes.js +61 -0
  64. package/dist/tokens.d.ts +28 -0
  65. package/dist/tokens.js +152 -0
  66. package/dist/tracked-range.d.ts +5 -0
  67. package/dist/tracked-range.js +20 -0
  68. package/dist/types.d.ts +342 -0
  69. package/dist/types.js +1 -0
  70. package/dist/view.d.ts +382 -0
  71. package/dist/view.js +2134 -0
  72. package/package.json +63 -3
  73. package/src/attribution.ts +85 -0
  74. package/src/code-block.ts +100 -0
  75. package/src/column-selection.ts +63 -0
  76. package/src/controller.ts +838 -0
  77. package/src/editor.css +813 -0
  78. package/src/engine.ts +757 -0
  79. package/src/fake.ts +831 -0
  80. package/src/find.ts +128 -0
  81. package/src/index.ts +69 -0
  82. package/src/input.ts +63 -0
  83. package/src/keymap-table.ts +163 -0
  84. package/src/keymap.ts +204 -0
  85. package/src/layout.ts +754 -0
  86. package/src/lens.ts +32 -0
  87. package/src/listeners.ts +15 -0
  88. package/src/metrics.ts +65 -0
  89. package/src/modifier-gesture.ts +115 -0
  90. package/src/native-input.ts +179 -0
  91. package/src/offsets.ts +16 -0
  92. package/src/outline.ts +248 -0
  93. package/src/overview.ts +80 -0
  94. package/src/paint.ts +608 -0
  95. package/src/php-html.ts +43 -0
  96. package/src/pointer.ts +326 -0
  97. package/src/scroll-animation.ts +48 -0
  98. package/src/scroll.ts +102 -0
  99. package/src/semantic.ts +54 -0
  100. package/src/shiki.ts +118 -0
  101. package/src/smart-keys.ts +17 -0
  102. package/src/testing.ts +157 -0
  103. package/src/theme-scopes.ts +75 -0
  104. package/src/tokens.ts +175 -0
  105. package/src/tracked-range.ts +22 -0
  106. package/src/types.ts +656 -0
  107. package/src/view.ts +2436 -0
@@ -0,0 +1,243 @@
1
+ const SCROLL_STEP = 40;
2
+ /* A press this soon after the last one, this close to it, is the next click of a double or triple click. */
3
+ const MULTI_CLICK_MS = 500;
4
+ const MULTI_CLICK_DISTANCE = 4;
5
+ /* How far the pointer goes, in pixels, before a press on a selection starts carrying it. */
6
+ const CARRY_THRESHOLD = 5;
7
+ /* 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. */
8
+ export class PointerSelection {
9
+ drag;
10
+ frame;
11
+ lastPress;
12
+ host;
13
+ constructor(host) {
14
+ this.host = host;
15
+ }
16
+ get dragging() {
17
+ return this.drag !== undefined;
18
+ }
19
+ /* A press in the text, or on a line number, which selects the line and drags by lines. */
20
+ start(event, area = 'text') {
21
+ const { model, viewport } = this.host;
22
+ const count = area === 'gutter' ? 1 : this.countClick(event);
23
+ const head = this.host.offsetAt(event.clientX, event.clientY);
24
+ const selections = model.getSelections();
25
+ const base = event.altKey && area === 'text' ? selections : [];
26
+ this.drag = {
27
+ id: event.pointerId,
28
+ anchor: event.shiftKey && !event.altKey ? model.getPrimary().anchor : head,
29
+ selections,
30
+ add: event.altKey && area === 'text',
31
+ unit: 'character',
32
+ keep: event.altKey && event.shiftKey,
33
+ box: area === 'text' && (event.altKey || this.host.columnMode()),
34
+ saved: { from: head, to: head },
35
+ anchorPoint: this.host.contentPoint(event.clientX, event.clientY),
36
+ x: event.clientX,
37
+ y: event.clientY
38
+ };
39
+ this.host.focus();
40
+ viewport.setPointerCapture?.(event.pointerId);
41
+ // Alt on a caret that is there takes it away; there is no selection to drag after that.
42
+ if (area === 'text' && event.altKey && count === 1 && selections.length > 1 && selections.some((selection) => selection.head === head)) {
43
+ model.setSelections(selections.filter((selection) => selection.head !== head));
44
+ this.drag = undefined;
45
+ return;
46
+ }
47
+ const selected = count === 1 && area === 'text' && !event.shiftKey && !event.altKey ? this.host.selectionAt(event.clientX, event.clientY) : null;
48
+ if (selected !== null) {
49
+ this.drag.unit = 'drop';
50
+ this.drag.carry = {
51
+ from: Math.min(selected.anchor, selected.head),
52
+ to: Math.max(selected.anchor, selected.head),
53
+ pressOffset: head,
54
+ startX: event.clientX,
55
+ startY: event.clientY,
56
+ active: false,
57
+ target: null
58
+ };
59
+ }
60
+ else if (area === 'gutter') {
61
+ this.pressLineNumber(event, head);
62
+ }
63
+ else if (count >= 3) {
64
+ this.selectUnit('line', this.host.lineAt(head), base);
65
+ }
66
+ else if (count === 2) {
67
+ const word = model.wordSelectionAt(head, this.host.camelHumps());
68
+ if (word === null) {
69
+ this.update();
70
+ }
71
+ else {
72
+ this.selectUnit('word', word, base);
73
+ }
74
+ }
75
+ else {
76
+ this.update();
77
+ }
78
+ if (this.drag && this.frame === undefined) {
79
+ this.frame = viewport.ownerDocument.defaultView?.requestAnimationFrame?.(() => this.autoScroll());
80
+ }
81
+ }
82
+ /* A line number selects its line, and with shift grows or shrinks the selection by lines. */
83
+ pressLineNumber(event, offset) {
84
+ const { model } = this.host;
85
+ const primary = model.getPrimary();
86
+ if (event.shiftKey && primary.anchor !== primary.head) {
87
+ model.setSelections([this.tweakedByLines(primary, offset)]);
88
+ this.drag = undefined;
89
+ return;
90
+ }
91
+ this.selectUnit('line', this.host.lineAt(offset), []);
92
+ }
93
+ selectUnit(unit, saved, base) {
94
+ if (this.drag === undefined) {
95
+ return;
96
+ }
97
+ this.drag.unit = unit;
98
+ this.drag.saved = saved;
99
+ this.drag.selections = base;
100
+ this.host.model.setSelections([...base, { anchor: saved.from, head: saved.to }]);
101
+ }
102
+ /* The selection after a shift press on a line number: the clicked line comes in, or goes out when it is inside. */
103
+ tweakedByLines(selection, offset) {
104
+ const { host } = this;
105
+ const from = Math.min(selection.anchor, selection.head);
106
+ const to = Math.max(selection.anchor, selection.head);
107
+ const first = host.visualLine(from);
108
+ const last = host.visualLine(to - 1);
109
+ const clicked = host.visualLine(offset);
110
+ if (clicked.y < first.y) {
111
+ return { anchor: to, head: clicked.start };
112
+ }
113
+ if (clicked.y > last.y) {
114
+ return { anchor: from, head: clicked.end };
115
+ }
116
+ if (first.y === last.y) {
117
+ return { anchor: selection.head, head: selection.head };
118
+ }
119
+ if (selection.anchor === to) {
120
+ const line = clicked.y === first.y ? host.visualLine(first.next) : clicked;
121
+ return { anchor: to, head: line.start };
122
+ }
123
+ const line = clicked.y === last.y ? host.visualLine(last.start - 1) : clicked;
124
+ return { anchor: from, head: line.end };
125
+ }
126
+ /* Pointer events carry no click count, so the presses are counted here. */
127
+ countClick(event) {
128
+ const last = this.lastPress;
129
+ const repeated = last !== undefined &&
130
+ event.timeStamp - last.time <= MULTI_CLICK_MS &&
131
+ Math.abs(event.clientX - last.x) <= MULTI_CLICK_DISTANCE &&
132
+ Math.abs(event.clientY - last.y) <= MULTI_CLICK_DISTANCE;
133
+ const count = repeated ? Math.min(3, last.count + 1) : 1;
134
+ this.lastPress = { time: event.timeStamp, x: event.clientX, y: event.clientY, count };
135
+ return count;
136
+ }
137
+ move(event) {
138
+ if (!this.drag || event.pointerId !== this.drag.id) {
139
+ return;
140
+ }
141
+ this.drag.x = event.clientX;
142
+ this.drag.y = event.clientY;
143
+ this.update();
144
+ }
145
+ /* The pointer went up, or was taken away: a selection that was carried lands, and one that was only pressed gives way to a caret. */
146
+ end(event) {
147
+ const { viewport } = this.host;
148
+ const carry = this.drag?.carry;
149
+ if (this.drag) {
150
+ viewport.releasePointerCapture?.(this.drag.id);
151
+ }
152
+ this.drag = undefined;
153
+ this.cancelFrame();
154
+ if (carry === undefined) {
155
+ return;
156
+ }
157
+ this.host.setDropCaret(null);
158
+ if (event?.type === 'pointercancel') {
159
+ return;
160
+ }
161
+ if (!carry.active) {
162
+ this.host.model.setSelections([{ anchor: carry.pressOffset, head: carry.pressOffset }]);
163
+ }
164
+ else if (carry.target !== null && event !== undefined) {
165
+ this.host.drop(carry.from, carry.to, carry.target, this.host.copyHeld(event));
166
+ }
167
+ }
168
+ /* Where the carried text would land at the pointer, once the pointer has moved far enough to carry it. */
169
+ updateCarry(drag, carry) {
170
+ if (!carry.active) {
171
+ if (Math.abs(drag.x - carry.startX) < CARRY_THRESHOLD && Math.abs(drag.y - carry.startY) < CARRY_THRESHOLD) {
172
+ return;
173
+ }
174
+ carry.active = true;
175
+ }
176
+ const offset = this.host.offsetAt(drag.x, drag.y);
177
+ carry.target = offset >= carry.from && offset <= carry.to ? null : offset;
178
+ this.host.setDropCaret(carry.target);
179
+ }
180
+ dispose() {
181
+ this.drag = undefined;
182
+ this.cancelFrame();
183
+ }
184
+ cancelFrame() {
185
+ if (this.frame !== undefined) {
186
+ this.host.viewport.ownerDocument.defaultView?.cancelAnimationFrame?.(this.frame);
187
+ }
188
+ this.frame = undefined;
189
+ }
190
+ update() {
191
+ if (!this.drag) {
192
+ return;
193
+ }
194
+ if (this.drag.carry !== undefined) {
195
+ this.updateCarry(this.drag, this.drag.carry);
196
+ return;
197
+ }
198
+ if (this.drag.box && this.drag.unit === 'character') {
199
+ const box = this.host.columnSelections(this.drag.anchorPoint, this.host.contentPoint(this.drag.x, this.drag.y));
200
+ if (box) {
201
+ this.host.model.setSelections(this.drag.keep ? [...this.drag.selections, ...box] : box);
202
+ return;
203
+ }
204
+ }
205
+ const head = this.host.offsetAt(this.drag.x, this.drag.y);
206
+ const { anchor, target } = this.reach(this.drag, head);
207
+ this.host.model.setSelections([...(this.drag.add ? this.drag.selections : []), { anchor, head: target }]);
208
+ }
209
+ /* Where a drag that has got to `head` selects from and to, by the unit the press began with. */
210
+ reach(drag, head) {
211
+ const { model } = this.host;
212
+ const backwards = head < drag.saved.from;
213
+ if (drag.unit === 'word') {
214
+ const camel = this.host.camelHumps();
215
+ return backwards
216
+ ? { anchor: drag.saved.to, target: model.wordStartBefore(head, camel) }
217
+ : { anchor: drag.saved.from, target: model.wordEndAfter(head, camel) };
218
+ }
219
+ if (drag.unit === 'line') {
220
+ const line = this.host.visualLine(head);
221
+ return backwards ? { anchor: drag.saved.to, target: line.start } : { anchor: drag.saved.from, target: line.next };
222
+ }
223
+ return { anchor: drag.anchor, target: head };
224
+ }
225
+ /* Holding the pointer past an edge of the view keeps scrolling that way. */
226
+ autoScroll() {
227
+ const { viewport } = this.host;
228
+ if (!this.drag) {
229
+ this.frame = undefined;
230
+ return;
231
+ }
232
+ const rect = viewport.getBoundingClientRect();
233
+ const dy = this.drag.y < rect.top ? this.drag.y - rect.top : this.drag.y > rect.bottom ? this.drag.y - rect.bottom : 0;
234
+ const dx = this.drag.x < rect.left ? this.drag.x - rect.left : this.drag.x > rect.right ? this.drag.x - rect.right : 0;
235
+ if (dy || dx) {
236
+ viewport.scrollTop += Math.max(-SCROLL_STEP, Math.min(SCROLL_STEP, dy));
237
+ viewport.scrollLeft += Math.max(-SCROLL_STEP, Math.min(SCROLL_STEP, dx));
238
+ this.host.scrolled();
239
+ this.update();
240
+ }
241
+ this.frame = viewport.ownerDocument.defaultView?.requestAnimationFrame?.(() => this.autoScroll());
242
+ }
243
+ }
@@ -0,0 +1,3 @@
1
+ export declare const SCROLL_DURATION_MS = 100;
2
+ export declare function scrollDuration(distance: number, lineHeight: number): number;
3
+ export declare function easeOut(fraction: number): number;
@@ -0,0 +1,45 @@
1
+ /*
2
+ * How a scroll that moves the view a distance goes there: not at all for a line or less, and for more
3
+ * over a moment that grows with the distance up to a tenth of a second, starting fast and easing out.
4
+ */
5
+ /* The longest the view takes to get anywhere. */
6
+ export const SCROLL_DURATION_MS = 100;
7
+ /* How many lines away the view has to go before it takes any time, and how many before it takes all of it. */
8
+ const FREE_LINES = 1;
9
+ const FULL_LINES = 11;
10
+ export function scrollDuration(distance, lineHeight) {
11
+ const lines = distance / lineHeight;
12
+ const part = Math.min(1, Math.max(0, (lines - FREE_LINES) / (FULL_LINES - FREE_LINES)));
13
+ return Math.round(part * SCROLL_DURATION_MS);
14
+ }
15
+ /* The curve `cubic-bezier(0, 0, 0.58, 1)`: how much of the way has been gone after a fraction of the time. */
16
+ export function easeOut(fraction) {
17
+ if (fraction <= 0) {
18
+ return 0;
19
+ }
20
+ if (fraction >= 1) {
21
+ return 1;
22
+ }
23
+ const x1 = 0;
24
+ const y1 = 0;
25
+ const x2 = 0.58;
26
+ const y2 = 1;
27
+ const bezier = (t, a, b) => 3 * (1 - t) * (1 - t) * t * a + 3 * (1 - t) * t * t * b + t * t * t;
28
+ let low = 0;
29
+ let high = 1;
30
+ let t = fraction;
31
+ for (let step = 0; step < 24; step++) {
32
+ const x = bezier(t, x1, x2);
33
+ if (Math.abs(x - fraction) < 1e-5) {
34
+ break;
35
+ }
36
+ if (x < fraction) {
37
+ low = t;
38
+ }
39
+ else {
40
+ high = t;
41
+ }
42
+ t = (low + high) / 2;
43
+ }
44
+ return bezier(t, y1, y2);
45
+ }
@@ -0,0 +1,28 @@
1
+ export type ScrollKind = 'relative' | 'makeVisible' | 'center' | 'centerDown' | 'centerUp';
2
+ export interface ScrollRequest {
3
+ readonly target: {
4
+ readonly x: number;
5
+ readonly y: number;
6
+ };
7
+ readonly view: {
8
+ readonly x: number;
9
+ readonly y: number;
10
+ readonly width: number;
11
+ readonly height: number;
12
+ };
13
+ readonly content: {
14
+ readonly width: number;
15
+ readonly height: number;
16
+ };
17
+ readonly lineHeight: number;
18
+ readonly charWidth: number;
19
+ readonly topBound: number;
20
+ readonly bottomBound: number;
21
+ readonly refrain: boolean;
22
+ readonly horizontal: boolean;
23
+ readonly inset?: number;
24
+ }
25
+ export declare function scrollPosition(request: ScrollRequest, kind: ScrollKind): {
26
+ x: number;
27
+ y: number;
28
+ };
package/dist/scroll.js ADDED
@@ -0,0 +1,76 @@
1
+ /*
2
+ * Where the view scrolls to when a target has to be seen. The rules are the platform's scrolling model
3
+ * with its defaults: a line of margin above and below the target, three characters at its sides, and a
4
+ * target out of reach lands a third of the way down the view.
5
+ */
6
+ /* The characters kept in view to the left and right of the target. */
7
+ const SIDE_MARGIN = 3;
8
+ function clamp(value, maximum) {
9
+ return Math.min(Math.max(0, value), Math.max(0, maximum));
10
+ }
11
+ /* The one third mark when the margins fit above it, the middle of the view when they do not. */
12
+ function centerOf(request) {
13
+ const { target, view, lineHeight, topBound } = request;
14
+ const third = target.y - Math.trunc(view.height / 3);
15
+ return third < topBound ? third : target.y - Math.trunc(Math.max(0, view.height - lineHeight) / 2);
16
+ }
17
+ function verticalPosition(request, kind) {
18
+ const { target, view, content, topBound, bottomBound } = request;
19
+ const height = view.height;
20
+ const center = centerOf(request);
21
+ const above = view.y > topBound;
22
+ const below = view.y + height < bottomBound;
23
+ let position = view.y;
24
+ if (kind === 'center') {
25
+ position = center;
26
+ }
27
+ else if (kind === 'centerUp') {
28
+ position = above || below || view.y > center ? center : position;
29
+ }
30
+ else if (kind === 'centerDown') {
31
+ position = above || below || view.y < center ? center : position;
32
+ }
33
+ else if (kind === 'makeVisible') {
34
+ position = above || below ? center : position;
35
+ }
36
+ else if (bottomBound - topBound > height) {
37
+ position = center;
38
+ }
39
+ else if (below) {
40
+ const lowest = target.y + height > content.height ? content.height : target.y + height - (target.y - topBound);
41
+ position = Math.max(bottomBound - height, Math.min(lowest, view.y + height) - height);
42
+ }
43
+ else if (above) {
44
+ const highest = target.y - height < 0 ? 0 : bottomBound - height;
45
+ position = Math.min(topBound, Math.max(highest, view.y));
46
+ }
47
+ const inset = request.inset ?? 0;
48
+ return Math.min(Math.max(inset, position), Math.max(inset, content.height - height));
49
+ }
50
+ function horizontalPosition(request, kind) {
51
+ const { target, view, content, charWidth } = request;
52
+ const width = view.width;
53
+ const left = target.x - SIDE_MARGIN * charWidth;
54
+ const right = target.x + SIDE_MARGIN * charWidth;
55
+ let position = view.x;
56
+ if (right - left > width) {
57
+ position = target.x - Math.trunc(width / 2);
58
+ }
59
+ else if (left < view.x) {
60
+ position = kind === 'makeVisible' && right < width ? 0 : left;
61
+ }
62
+ else if (right > view.x + width) {
63
+ position = right - width;
64
+ }
65
+ return clamp(position, content.width - width);
66
+ }
67
+ /* The scroll position that shows the target the way `kind` asks. */
68
+ export function scrollPosition(request, kind) {
69
+ const { target, view } = request;
70
+ const inView = target.x >= view.x && target.x < view.x + view.width && target.y >= view.y && target.y < view.y + view.height;
71
+ const effective = request.refrain && inView && (kind === 'center' || kind === 'centerDown' || kind === 'centerUp') ? 'relative' : kind;
72
+ return {
73
+ x: request.horizontal ? horizontalPosition(request, effective) : view.x,
74
+ y: verticalPosition(request, effective) - (request.inset ?? 0)
75
+ };
76
+ }
@@ -0,0 +1,8 @@
1
+ import type { LineToken } from './types.ts';
2
+ export interface SemanticSpan {
3
+ readonly from: number;
4
+ readonly to: number;
5
+ readonly color: string;
6
+ readonly fontStyle: number;
7
+ }
8
+ export declare function overlayTokens(base: readonly LineToken[], spans: readonly SemanticSpan[]): LineToken[];
@@ -0,0 +1,47 @@
1
+ /*
2
+ * The colors of a line with the spans laid over them: inside a span its color and font style replace
3
+ * the grammar's, and everywhere else the grammar's stay. The tokens of the result add up to the line
4
+ * as before. Spans are in order and do not overlap.
5
+ */
6
+ export function overlayTokens(base, spans) {
7
+ const result = [];
8
+ const push = (length, color, fontStyle) => {
9
+ if (length <= 0) {
10
+ return;
11
+ }
12
+ const last = result[result.length - 1];
13
+ if (last !== undefined && last.color === color && last.fontStyle === fontStyle) {
14
+ result[result.length - 1] = { length: last.length + length, color, fontStyle };
15
+ }
16
+ else {
17
+ result.push({ length, color, fontStyle });
18
+ }
19
+ };
20
+ let span = 0;
21
+ let start = 0;
22
+ for (const token of base) {
23
+ const end = start + token.length;
24
+ let at = start;
25
+ while (at < end) {
26
+ while (span < spans.length && spans[span].to <= at) {
27
+ span++;
28
+ }
29
+ const current = spans[span];
30
+ if (current === undefined || current.from >= end) {
31
+ push(end - at, token.color, token.fontStyle);
32
+ at = end;
33
+ }
34
+ else if (current.from > at) {
35
+ push(current.from - at, token.color, token.fontStyle);
36
+ at = current.from;
37
+ }
38
+ else {
39
+ const stop = Math.min(end, current.to);
40
+ push(stop - at, current.color, current.fontStyle);
41
+ at = stop;
42
+ }
43
+ }
44
+ start = end;
45
+ }
46
+ return result;
47
+ }
@@ -0,0 +1,7 @@
1
+ import type { Highlighter } from 'shiki';
2
+ import { documentLanguageOf } from './php-html.ts';
3
+ import type { ScopeColorSource, TokenizerSource } from './types.ts';
4
+ export { documentLanguageOf };
5
+ export declare function loadGrammar(highlighter: Highlighter, grammar: string, text?: string): Promise<boolean>;
6
+ export declare function shikiTokenizers(highlighter: () => Promise<Highlighter>): TokenizerSource;
7
+ export declare function shikiScopeColors(highlighter: () => Promise<Highlighter>): ScopeColorSource;
package/dist/shiki.js ADDED
@@ -0,0 +1,106 @@
1
+ import { documentLanguageOf, loadDocumentLanguage } from './php-html.js';
2
+ import { scopeColorsOf } from './theme-scopes.js';
3
+ export { documentLanguageOf };
4
+ const PLAIN_TEXT = new Set(['', 'text', 'txt', 'plain', 'plaintext']);
5
+ /* Colors one line at a time and hands the grammar's state from one line to the next. A grammar Shiki rebuilt does not know the states of the old one. */
6
+ function lineTokenizer(highlighter, language, theme) {
7
+ let grammar = highlighter.getLanguage(language);
8
+ return {
9
+ stale() {
10
+ const current = highlighter.getLanguage(language);
11
+ const changed = current !== grammar;
12
+ grammar = current;
13
+ return changed;
14
+ },
15
+ tokenizeLine(text, state) {
16
+ const options = { lang: language, theme, grammarState: (state ?? undefined) };
17
+ const lines = highlighter.codeToTokensBase(text, options);
18
+ const tokens = (lines[0] ?? []).map((token) => ({
19
+ length: token.content.length,
20
+ color: token.color ?? '',
21
+ fontStyle: token.fontStyle ?? 0
22
+ }));
23
+ return { tokens, state: highlighter.getLastGrammarState(lines) };
24
+ },
25
+ sameState(a, b) {
26
+ if (a === b) {
27
+ return true;
28
+ }
29
+ const left = a?.getInternalStack(theme);
30
+ const right = b?.getInternalStack(theme);
31
+ return left !== undefined && right !== undefined && left.equals(right);
32
+ }
33
+ };
34
+ }
35
+ /* The words of a document that name a language, an alias or the id itself, and not part of a longer word. */
36
+ function mentions(text, names) {
37
+ return names.some((name) => new RegExp(`(?<![\\w-])${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w-])`, 'i').test(text));
38
+ }
39
+ /*
40
+ * A grammar embeds some languages only on demand (Vue's `lang="scss"` styles, the fences of Markdown),
41
+ * and Shiki builds the host grammar anew each time one of them loads later, which drops the states
42
+ * of every line colored before. So the ones a document names are loaded together with the host.
43
+ */
44
+ async function loadEmbedded(highlighter, grammar, text) {
45
+ const { bundledLanguages, bundledLanguagesInfo } = await import('shiki');
46
+ const bundled = bundledLanguages[grammar];
47
+ if (bundled === undefined) {
48
+ return;
49
+ }
50
+ const registrations = (await bundled()).default;
51
+ const lazy = registrations.find((registration) => registration.name === grammar)?.embeddedLangsLazy ?? [];
52
+ const named = lazy.filter((id) => {
53
+ const aliases = bundledLanguagesInfo.find((info) => info.id === id)?.aliases ?? [];
54
+ return !highlighter.getLoadedLanguages().includes(id) && mentions(text, [id, ...aliases]);
55
+ });
56
+ await Promise.allSettled(named.map((id) => highlighter.loadLanguage(id)));
57
+ }
58
+ /*
59
+ * Loads the grammar of a language with what a document in it embeds, and says whether Shiki has it.
60
+ * `text` is the document, which decides the embedded languages that load only when named.
61
+ */
62
+ export async function loadGrammar(highlighter, grammar, text = '') {
63
+ await loadDocumentLanguage(highlighter, grammar);
64
+ if (!highlighter.getLoadedLanguages().includes(grammar)) {
65
+ await highlighter.loadLanguage(grammar);
66
+ }
67
+ await loadEmbedded(highlighter, grammar, text);
68
+ return highlighter.getLoadedLanguages().includes(grammar);
69
+ }
70
+ /*
71
+ * Colors with the viewer's highlighter, so a grammar loads once for both. A language Shiki has no
72
+ * grammar for, or a theme it cannot load, is plain text.
73
+ */
74
+ export function shikiTokenizers(highlighter) {
75
+ return async (language, theme, text) => {
76
+ if (language === undefined || PLAIN_TEXT.has(language)) {
77
+ return null;
78
+ }
79
+ const grammar = documentLanguageOf(language);
80
+ const shiki = await highlighter();
81
+ try {
82
+ if (!shiki.getLoadedThemes().includes(theme)) {
83
+ await shiki.loadTheme(theme);
84
+ }
85
+ return (await loadGrammar(shiki, grammar, text)) ? lineTokenizer(shiki, grammar, theme) : null;
86
+ }
87
+ catch {
88
+ return null;
89
+ }
90
+ };
91
+ }
92
+ /* The colors of a theme's own rules by scope, so what a language server classifies is drawn in the theme's colors. */
93
+ export function shikiScopeColors(highlighter) {
94
+ return async (theme) => {
95
+ const shiki = await highlighter();
96
+ try {
97
+ if (!shiki.getLoadedThemes().includes(theme)) {
98
+ await shiki.loadTheme(theme);
99
+ }
100
+ return scopeColorsOf(shiki.getTheme(theme).settings);
101
+ }
102
+ catch {
103
+ return null;
104
+ }
105
+ };
106
+ }
@@ -0,0 +1,3 @@
1
+ import type { EditorSmartKeys } from './types.ts';
2
+ export declare const DEFAULT_SMART_KEYS: EditorSmartKeys;
3
+ export declare function resolveSmartKeys(keys: Partial<EditorSmartKeys> | undefined): EditorSmartKeys;
@@ -0,0 +1,14 @@
1
+ export const DEFAULT_SMART_KEYS = {
2
+ autoPairBrackets: true,
3
+ autoPairQuotes: true,
4
+ surroundSelection: true,
5
+ tabOutOfClosers: true,
6
+ smartIndentOnEnter: true,
7
+ indentOnPaste: true,
8
+ smartSemicolon: true,
9
+ camelHumps: false
10
+ };
11
+ /* The keys with what a host left out filled in from the defaults. */
12
+ export function resolveSmartKeys(keys) {
13
+ return { ...DEFAULT_SMART_KEYS, ...keys };
14
+ }
@@ -0,0 +1,34 @@
1
+ import type { Editor, EditorOptions, SmartEditorEngineOptions } from './types.ts';
2
+ export declare function createPage(): {
3
+ document: Document;
4
+ window: Window & typeof globalThis;
5
+ host: HTMLElement;
6
+ };
7
+ export interface KeyOptions {
8
+ ctrlKey?: boolean;
9
+ metaKey?: boolean;
10
+ altKey?: boolean;
11
+ shiftKey?: boolean;
12
+ }
13
+ export declare function press(window: Window, target: Element, key: string, options?: KeyOptions): boolean;
14
+ export declare function release(window: Window, target: Element, key: string, options?: KeyOptions): void;
15
+ export declare function typeInto(window: Window, target: Element, text: string): void;
16
+ export declare function clipboard(window: Window, target: Element, type: 'copy' | 'cut' | 'paste', data?: {
17
+ text: string;
18
+ }): {
19
+ prevented: boolean;
20
+ text: string;
21
+ };
22
+ export declare function pointer(window: Window, target: Element, type: 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel', x: number, y: number, options?: KeyOptions): void;
23
+ export interface MountedEditor {
24
+ editor: Editor;
25
+ page: ReturnType<typeof createPage>;
26
+ input: HTMLTextAreaElement;
27
+ viewport: HTMLElement;
28
+ press(key: string, modifiers?: KeyOptions): boolean;
29
+ release(key: string, modifiers?: KeyOptions): void;
30
+ type(text: string): void;
31
+ clip(type: 'copy' | 'cut' | 'paste', text?: string): string;
32
+ click(x: number, y: number, options?: KeyOptions): void;
33
+ }
34
+ export declare function mountEditor(options?: Partial<EditorOptions>, engineOptions?: Partial<SmartEditorEngineOptions>): MountedEditor;