@adecore/editor 0.22.0 → 0.23.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.
Files changed (94) hide show
  1. package/dist/attribution.d.ts +9 -8
  2. package/dist/attribution.js +44 -9
  3. package/dist/bidi.d.ts +44 -0
  4. package/dist/bidi.js +26 -0
  5. package/dist/column-selection.js +2 -1
  6. package/dist/controller.d.ts +6 -1
  7. package/dist/controller.js +93 -3
  8. package/dist/editor.css +81 -1
  9. package/dist/engine.js +184 -41
  10. package/dist/fake.d.ts +43 -5
  11. package/dist/fake.js +117 -13
  12. package/dist/find-search.d.ts +26 -0
  13. package/dist/find-search.js +130 -0
  14. package/dist/find.d.ts +50 -2
  15. package/dist/find.js +575 -25
  16. package/dist/geometry-stops.d.ts +6 -0
  17. package/dist/geometry-stops.js +47 -0
  18. package/dist/grammar-preparation.d.ts +29 -0
  19. package/dist/grammar-preparation.js +147 -0
  20. package/dist/index.d.ts +3 -1
  21. package/dist/index.js +2 -0
  22. package/dist/keymap-table.d.ts +10 -1
  23. package/dist/keymap-table.js +37 -1
  24. package/dist/keymap.d.ts +1 -0
  25. package/dist/keymap.js +4 -1
  26. package/dist/layout-changes.d.ts +6 -0
  27. package/dist/layout-changes.js +27 -0
  28. package/dist/layout.d.ts +45 -2
  29. package/dist/layout.js +202 -58
  30. package/dist/offsets.d.ts +1 -0
  31. package/dist/offsets.js +23 -0
  32. package/dist/outline.js +5 -4
  33. package/dist/paint.d.ts +11 -3
  34. package/dist/paint.js +180 -34
  35. package/dist/search-client.d.ts +27 -0
  36. package/dist/search-client.js +210 -0
  37. package/dist/search-protocol.d.ts +103 -0
  38. package/dist/search-protocol.js +3 -0
  39. package/dist/search-worker.d.ts +11 -0
  40. package/dist/search-worker.js +137 -0
  41. package/dist/semantic.d.ts +2 -1
  42. package/dist/semantic.js +6 -1
  43. package/dist/shiki-document.d.ts +8 -0
  44. package/dist/shiki-document.js +93 -0
  45. package/dist/shiki-worker.d.ts +65 -0
  46. package/dist/shiki-worker.js +265 -0
  47. package/dist/shiki.d.ts +1 -0
  48. package/dist/shiki.js +96 -33
  49. package/dist/testing.d.ts +1 -0
  50. package/dist/tokens.d.ts +2 -0
  51. package/dist/tokens.js +37 -7
  52. package/dist/touch.d.ts +27 -0
  53. package/dist/touch.js +100 -0
  54. package/dist/types.d.ts +84 -2
  55. package/dist/uniform-geometry.d.ts +7 -0
  56. package/dist/uniform-geometry.js +49 -0
  57. package/dist/view.d.ts +68 -17
  58. package/dist/view.js +424 -97
  59. package/dist/worker-client.d.ts +37 -0
  60. package/dist/worker-client.js +195 -0
  61. package/package.json +17 -2
  62. package/src/attribution.ts +50 -14
  63. package/src/bidi.ts +32 -0
  64. package/src/column-selection.ts +2 -1
  65. package/src/controller.ts +97 -3
  66. package/src/editor.css +81 -1
  67. package/src/engine.ts +216 -42
  68. package/src/fake.ts +152 -14
  69. package/src/find-search.ts +165 -0
  70. package/src/find.ts +622 -21
  71. package/src/geometry-stops.ts +51 -0
  72. package/src/grammar-preparation.ts +158 -0
  73. package/src/index.ts +10 -0
  74. package/src/keymap-table.ts +44 -2
  75. package/src/keymap.ts +5 -1
  76. package/src/layout-changes.ts +33 -0
  77. package/src/layout.ts +262 -63
  78. package/src/offsets.ts +24 -0
  79. package/src/outline.ts +5 -4
  80. package/src/paint.ts +202 -36
  81. package/src/search-client.ts +306 -0
  82. package/src/search-protocol.ts +100 -0
  83. package/src/search-worker.ts +171 -0
  84. package/src/semantic.ts +8 -2
  85. package/src/shiki-document.ts +143 -0
  86. package/src/shiki-worker.ts +344 -0
  87. package/src/shiki.ts +103 -33
  88. package/src/testing.ts +2 -0
  89. package/src/tokens.ts +36 -7
  90. package/src/touch.ts +134 -0
  91. package/src/types.ts +174 -8
  92. package/src/uniform-geometry.ts +57 -0
  93. package/src/view.ts +500 -102
  94. package/src/worker-client.ts +212 -0
@@ -1,28 +1,29 @@
1
1
  import type { DocumentChange } from '@adecore/editor-core';
2
+ import type { EditorLineHighlight } from './types.ts';
2
3
  export declare const AGENT_COLORS: readonly ['--agent-1', '--agent-2', '--agent-3', '--agent-4', '--agent-5', '--agent-6'];
3
4
  export interface AttributedLines {
4
5
  readonly id: string;
5
6
  readonly color: string;
6
7
  readonly sign?: string;
7
8
  readonly fill?: string;
9
+ readonly bar?: 'solid' | 'hatched';
10
+ readonly emphasis?: readonly {
11
+ from: number;
12
+ to: number;
13
+ }[];
8
14
  }
9
15
  export declare function tintValue(color: string): string;
10
16
  export declare function colorValue(color: string): string;
11
17
  export declare class AttributionRuns {
12
18
  private runs;
13
19
  get size(): number;
14
- set(marks: readonly {
20
+ set(marks: readonly (EditorLineHighlight & {
15
21
  id: string;
16
- startLine: number;
17
- endLine: number;
18
- color: string;
19
- sign?: string;
20
- fill?: string;
21
- }[], lineCount: number, lineBounds: (line: number) => {
22
+ })[], lineCount: number, lineBounds: (line: number) => {
22
23
  start: number;
23
24
  end: number;
24
25
  }): void;
25
26
  clear(): void;
26
- map(changes: readonly DocumentChange[]): void;
27
+ map(changes: readonly DocumentChange[], map?: (offset: number) => number): void;
27
28
  linesIn(first: number, last: number, from: number, to: number, lineOf: (offset: number) => number): Map<number, AttributedLines>;
28
29
  }
@@ -1,4 +1,4 @@
1
- import { mapOffset } from './offsets.js';
1
+ import { offsetMapper } from './offsets.js';
2
2
  /* The custom properties of the agent palette, defined in `editor.css` for both themes. */
3
3
  export const AGENT_COLORS = ['--agent-1', '--agent-2', '--agent-3', '--agent-4', '--agent-5', '--agent-6'];
4
4
  /* A color washed out to a background that text stays readable on in either theme. */
@@ -11,8 +11,8 @@ export function colorValue(color) {
11
11
  }
12
12
  /*
13
13
  * The runs of lines a host attributed, anchored by offsets so they follow their text through edits. An
14
- * edit costs one mapping of two offsets per run, and a read works out the lines of only the runs that
15
- * reach the lines asked for.
14
+ * edit maps each run and its emphasized ranges, and a read works out the lines of only the runs
15
+ * that reach the lines asked for.
16
16
  */
17
17
  export class AttributionRuns {
18
18
  runs = [];
@@ -25,16 +25,38 @@ export class AttributionRuns {
25
25
  this.runs = marks.map((mark) => {
26
26
  const start = clamp(mark.startLine);
27
27
  const end = Math.max(start, clamp(mark.endLine));
28
- return { id: mark.id, color: mark.color, sign: mark.sign, fill: mark.fill, from: lineBounds(start).start, to: lineBounds(end).end };
28
+ const emphasis = (mark.emphasis ?? []).slice(0, end - start + 1).flatMap((ranges, index) => {
29
+ const bounds = lineBounds(start + index);
30
+ const length = bounds.end - bounds.start;
31
+ return ranges.flatMap(([from, to]) => {
32
+ if (!Number.isFinite(from) || !Number.isFinite(to)) {
33
+ return [];
34
+ }
35
+ const first = Math.min(length, Math.max(0, Math.trunc(from)));
36
+ const last = Math.min(length, Math.max(0, Math.trunc(to)));
37
+ return last > first ? [{ from: bounds.start + first, to: bounds.start + last }] : [];
38
+ });
39
+ });
40
+ return {
41
+ id: mark.id,
42
+ color: mark.color,
43
+ sign: mark.sign,
44
+ fill: mark.fill,
45
+ bar: mark.bar,
46
+ from: lineBounds(start).start,
47
+ to: lineBounds(end).end,
48
+ emphasis
49
+ };
29
50
  });
30
51
  }
31
52
  clear() {
32
53
  this.runs = [];
33
54
  }
34
- map(changes) {
55
+ map(changes, map = offsetMapper(changes)) {
35
56
  for (const run of this.runs) {
36
- run.from = mapOffset(run.from, changes);
37
- run.to = mapOffset(run.to, changes);
57
+ run.from = map(run.from);
58
+ run.to = map(run.to);
59
+ run.emphasis = run.emphasis.map((range) => ({ from: map(range.from), to: map(range.to) })).filter((range) => range.to > range.from);
38
60
  }
39
61
  }
40
62
  /* The run on each line from `first` to `last`, zero-based, where `from` and `to` are the offsets they span; where runs overlap the one set last wins. */
@@ -44,10 +66,23 @@ export class AttributionRuns {
44
66
  if (run.to < from || run.from > to) {
45
67
  continue;
46
68
  }
47
- const value = { id: run.id, color: run.color, sign: run.sign, fill: run.fill };
69
+ const value = { id: run.id, color: run.color, sign: run.sign, fill: run.fill, bar: run.bar };
70
+ const emphasis = new Map();
71
+ for (const range of run.emphasis) {
72
+ if (range.to <= from || range.from > to) {
73
+ continue;
74
+ }
75
+ const endLine = Math.min(lineOf(range.to - 1), last);
76
+ for (let line = Math.max(lineOf(range.from), first); line <= endLine; line++) {
77
+ const ranges = emphasis.get(line) ?? [];
78
+ ranges.push(range);
79
+ emphasis.set(line, ranges);
80
+ }
81
+ }
48
82
  const end = Math.min(Math.max(lineOf(run.from), lineOf(run.to)), last);
49
83
  for (let line = Math.max(lineOf(run.from), first); line <= end; line++) {
50
- lines.set(line, value);
84
+ const ranges = emphasis.get(line);
85
+ lines.set(line, ranges === undefined ? value : { ...value, emphasis: ranges });
51
86
  }
52
87
  }
53
88
  return lines;
package/dist/bidi.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ export declare const BIDI_CONTROLS: {
2
+ readonly '‪': {
3
+ readonly label: 'LRE';
4
+ };
5
+ readonly '‫': {
6
+ readonly label: 'RLE';
7
+ };
8
+ readonly '‬': {
9
+ readonly label: 'PDF';
10
+ };
11
+ readonly '‭': {
12
+ readonly label: 'LRO';
13
+ };
14
+ readonly '‮': {
15
+ readonly label: 'RLO';
16
+ };
17
+ readonly '⁦': {
18
+ readonly label: 'LRI';
19
+ };
20
+ readonly '⁧': {
21
+ readonly label: 'RLI';
22
+ };
23
+ readonly '⁨': {
24
+ readonly label: 'FSI';
25
+ };
26
+ readonly '⁩': {
27
+ readonly label: 'PDI';
28
+ };
29
+ readonly '‎': {
30
+ readonly label: 'LRM';
31
+ };
32
+ readonly '‏': {
33
+ readonly label: 'RLM';
34
+ };
35
+ readonly '؜': {
36
+ readonly label: 'ALM';
37
+ };
38
+ };
39
+ type BidiControl = keyof typeof BIDI_CONTROLS;
40
+ export declare const BIDI_COLUMNS = 3;
41
+ export declare const BIDI_PATTERN: RegExp;
42
+ export declare function bidiControl(character: string): (typeof BIDI_CONTROLS)[BidiControl] | null;
43
+ export declare function visibleBidi(text: string): string;
44
+ export {};
package/dist/bidi.js ADDED
@@ -0,0 +1,26 @@
1
+ /* The label drawn for each control character. The boxes are visual: assistive technology reads the raw characters from the input. */
2
+ export const BIDI_CONTROLS = {
3
+ '‪': { label: 'LRE' },
4
+ '‫': { label: 'RLE' },
5
+ '‬': { label: 'PDF' },
6
+ '‭': { label: 'LRO' },
7
+ '‮': { label: 'RLO' },
8
+ '⁦': { label: 'LRI' },
9
+ '⁧': { label: 'RLI' },
10
+ '⁨': { label: 'FSI' },
11
+ '⁩': { label: 'PDI' },
12
+ '‎': { label: 'LRM' },
13
+ '‏': { label: 'RLM' },
14
+ '؜': { label: 'ALM' }
15
+ };
16
+ /* Every label is this many columns wide, so the placeholder is a box of fixed size in the grid. */
17
+ export const BIDI_COLUMNS = 3;
18
+ export const BIDI_PATTERN = /[‪-‮⁦-⁩‎‏؜]/g;
19
+ /* The placeholder of a bidirectional control character, or null for any other text. */
20
+ export function bidiControl(character) {
21
+ return character.length === 1 ? (BIDI_CONTROLS[character] ?? null) : null;
22
+ }
23
+ /* Text a person reads but the editor never edits, such as a hint or a suggestion: each control is spelled out in brackets so none reaches the DOM. */
24
+ export function visibleBidi(text) {
25
+ return text.replace(BIDI_PATTERN, (character) => `[${BIDI_CONTROLS[character].label}]`);
26
+ }
@@ -1,3 +1,4 @@
1
+ import { stopCount, xOfStop } from './geometry-stops.js';
1
2
  import { TEXT_PADDING } from './layout.js';
2
3
  /* The cell of a monospaced grid an x falls in, which stands in for the visual column since a tab is a whole number of cells. */
3
4
  export function cellAt(layout, x) {
@@ -25,7 +26,7 @@ export function columnSelections(layout, from, to) {
25
26
  continue;
26
27
  }
27
28
  const geometry = layout.geometry(row);
28
- const endX = geometry.rowStarts.length > 1 ? geometry.rowEnds[0] : geometry.before[geometry.before.length - 1];
29
+ const endX = geometry.rowStarts.length > 1 ? geometry.rowEnds[0] : xOfStop(geometry, stopCount(geometry) - 1);
29
30
  const offsetAt = (cell) => layout.offsetAtX(geometry, 0, TEXT_PADDING + cell * charWidth).offset;
30
31
  if (cellAt(layout, endX + TEXT_PADDING) < left) {
31
32
  if (left === right) {
@@ -1,5 +1,5 @@
1
1
  import type { Keymap } from './keymap-table.ts';
2
- import type { EditorClickHandler, EditorContextMenu, EditorKeyHandler, EditorRunCommand, EditorSelectionRanges, KeyChord } from './types.ts';
2
+ import type { EditorClickHandler, EditorInlayClickHandler, EditorContextMenu, EditorKeyHandler, EditorRunCommand, EditorSelectionRanges, KeyChord } from './types.ts';
3
3
  import type { EditorView } from './view.ts';
4
4
  export interface ControllerOptions {
5
5
  keymap?: Keymap;
@@ -11,10 +11,13 @@ export interface ControllerOptions {
11
11
  export declare class InputController {
12
12
  private readonly native;
13
13
  private readonly pointer;
14
+ private readonly touch;
14
15
  private readonly cleanup;
15
16
  private readonly keyHandlers;
16
17
  private readonly gesture;
17
18
  private readonly clickHandlers;
19
+ private readonly inlayHandlers;
20
+ private lastInlayPress;
18
21
  private readonly contextListeners;
19
22
  private readonly subscription;
20
23
  private historyGroup;
@@ -52,11 +55,13 @@ export declare class InputController {
52
55
  private run;
53
56
  onKeyDown(handler: EditorKeyHandler): () => void;
54
57
  onClick(handler: EditorClickHandler): () => void;
58
+ onInlayClick(handler: EditorInlayClickHandler): () => void;
55
59
  onContextMenu(listener: (menu: EditorContextMenu) => void): () => void;
56
60
  private keydown;
57
61
  private beforeInput;
58
62
  private copy;
59
63
  private pointerDown;
64
+ private takenByInlayHandler;
60
65
  private takenByHost;
61
66
  private positionOf;
62
67
  private contextMenu;
@@ -5,6 +5,7 @@ import { DoubleModifierGesture } from './modifier-gesture.js';
5
5
  import { NativeInput } from './native-input.js';
6
6
  import { columnSelections, sameCell } from './column-selection.js';
7
7
  import { PointerSelection } from './pointer.js';
8
+ import { TouchSelection } from './touch.js';
8
9
  const MODIFYING = new Set([
9
10
  'deleteWordLeft',
10
11
  'deleteWordRight',
@@ -53,6 +54,8 @@ function selectionSignature(selections) {
53
54
  }
54
55
  /* Typing in the same stretch is one undo step until a pause this long. */
55
56
  const TYPING_PAUSE_MS = 750;
57
+ /* A second press on the same hint within this many milliseconds is a double click. */
58
+ const DOUBLE_PRESS_MS = 500;
56
59
  /*
57
60
  * What a person does to the editor: keys, typing, the clipboard, an input method, the mouse. It turns
58
61
  * each into a command on the model and leaves the drawing to the view.
@@ -60,10 +63,13 @@ const TYPING_PAUSE_MS = 750;
60
63
  export class InputController {
61
64
  native;
62
65
  pointer;
66
+ touch;
63
67
  cleanup = [];
64
68
  keyHandlers = new Set();
65
69
  gesture;
66
70
  clickHandlers = new Set();
71
+ inlayHandlers = new Set();
72
+ lastInlayPress = null;
67
73
  contextListeners = new Set();
68
74
  subscription;
69
75
  historyGroup = 0;
@@ -117,6 +123,16 @@ export class InputController {
117
123
  focus: () => view.focus(),
118
124
  scrolled: () => view.requestRender()
119
125
  });
126
+ this.touch = new TouchSelection({
127
+ model,
128
+ offsetAt: (x, y) => view.offsetAtPoint(x, y),
129
+ camelHumps: () => view.settings.smartKeys.camelHumps,
130
+ lineAt: (offset) => {
131
+ const row = view.layout.rowForLine(model.positionAt(offset).line);
132
+ return row.kind === 'text' ? { from: model.getLine(row.line).start, to: model.getLine(row.lastLine).next } : { from: offset, to: offset };
133
+ },
134
+ focus: () => view.focus()
135
+ });
120
136
  this.subscription = model.subscribe(() => {
121
137
  if (!this.keepDesiredXs) {
122
138
  this.desiredXs = [];
@@ -165,22 +181,45 @@ export class InputController {
165
181
  this.listen(input, 'cut', (event) => this.copy(event, true));
166
182
  this.listen(viewport, 'scroll', () => this.view.scrolled());
167
183
  this.listen(viewport, 'wheel', () => this.view.cancelScroll());
168
- this.listen(viewport, 'touchstart', () => this.view.cancelScroll());
169
184
  this.listen(viewport, 'pointermove', (event) => this.view.hoverMoved(event));
170
- this.listen(viewport, 'pointerleave', () => this.view.setHover(null));
185
+ this.listen(viewport, 'pointerleave', () => {
186
+ this.view.setHover(null);
187
+ this.view.setInlayHover(null);
188
+ });
171
189
  this.listen(viewport, 'pointerdown', (event) => this.pointerDown(event));
172
190
  this.listen(viewport, 'contextmenu', (event) => this.contextMenu(event));
173
191
  this.listen(this.view.gutterElement, 'pointerdown', (event) => this.gutterDown(event));
174
192
  this.listen(this.view.stickyElement, 'pointerdown', (event) => this.stickyDown(event));
175
- const move = (event) => this.pointer.move(event);
193
+ const move = (event) => {
194
+ this.pointer.move(event);
195
+ this.touch.move(event);
196
+ };
176
197
  const end = (event) => {
177
198
  this.pointer.end(event);
199
+ this.touch.end(event);
178
200
  this.historyGroup++;
179
201
  };
180
202
  document.addEventListener('pointermove', move);
181
203
  document.addEventListener('pointerup', end);
182
204
  document.addEventListener('pointercancel', end);
205
+ // The on-screen keyboard shrinks the visual viewport and not the layout one, which would leave the caret behind it.
206
+ const visualViewport = document.defaultView?.visualViewport;
207
+ let keyboardHeight = visualViewport?.height ?? 0;
208
+ let keyboardScale = visualViewport?.scale ?? 1;
209
+ const keyboard = () => {
210
+ const height = visualViewport?.height ?? 0;
211
+ const scale = visualViewport?.scale ?? 1;
212
+ // Zooming in shrinks the visual viewport too, and must not pull the view back to the caret.
213
+ const shrunk = height < keyboardHeight && scale === keyboardScale;
214
+ keyboardHeight = height;
215
+ keyboardScale = scale;
216
+ if (shrunk && this.view.focused) {
217
+ this.view.revealCaret();
218
+ }
219
+ };
220
+ visualViewport?.addEventListener('resize', keyboard);
183
221
  this.cleanup.push(() => {
222
+ visualViewport?.removeEventListener('resize', keyboard);
184
223
  document.removeEventListener('pointermove', move);
185
224
  document.removeEventListener('pointerup', end);
186
225
  document.removeEventListener('pointercancel', end);
@@ -189,6 +228,7 @@ export class InputController {
189
228
  dispose() {
190
229
  this.subscription.dispose();
191
230
  this.pointer.dispose();
231
+ this.touch.dispose();
192
232
  for (const remove of this.cleanup) {
193
233
  remove();
194
234
  }
@@ -267,6 +307,9 @@ export class InputController {
267
307
  /* Folding is the view's, since it knows where the ranges are. */
268
308
  viewCommand(name) {
269
309
  const { view } = this;
310
+ if (view.settings.folding === false && name !== 'toggleColumnMode') {
311
+ return false;
312
+ }
270
313
  const run = {
271
314
  collapseRegion: () => view.collapseRegion(),
272
315
  expandRegion: () => view.expandRegion(),
@@ -580,6 +623,9 @@ export class InputController {
580
623
  onClick(handler) {
581
624
  return subscribe(this.clickHandlers, handler);
582
625
  }
626
+ onInlayClick(handler) {
627
+ return subscribe(this.inlayHandlers, handler);
628
+ }
583
629
  onContextMenu(listener) {
584
630
  return subscribe(this.contextListeners, listener);
585
631
  }
@@ -680,13 +726,48 @@ export class InputController {
680
726
  if (event.button !== 0 || target.closest('button, a, input, textarea, [contenteditable], .se-block, .se-gutter, .se-sticky')) {
681
727
  return;
682
728
  }
729
+ // Cancels the compatibility mouse events, whose focus change would close the keyboard a tap opened; scrolling is not affected.
683
730
  event.preventDefault();
731
+ if (this.takenByInlayHandler(event)) {
732
+ return;
733
+ }
734
+ if (event.pointerType === 'touch') {
735
+ this.touch.start(event, 'text', () => this.takenByHost(event));
736
+ return;
737
+ }
684
738
  if (this.takenByHost(event)) {
685
739
  this.view.focus();
686
740
  return;
687
741
  }
688
742
  this.pointer.start(event);
689
743
  }
744
+ /* Whether a handler of the host took a press on a hint, which then moves neither the caret nor the focus. */
745
+ takenByInlayHandler(event) {
746
+ const inlay = this.inlayHandlers.size === 0 ? null : this.view.inlayAt(event.target);
747
+ if (inlay === null) {
748
+ return false;
749
+ }
750
+ const now = Date.now();
751
+ const again = this.lastInlayPress?.id === inlay.id && now - this.lastInlayPress.at <= DOUBLE_PRESS_MS;
752
+ this.lastInlayPress = again ? null : { id: inlay.id, at: now };
753
+ const touch = event.pointerType === 'touch';
754
+ const click = {
755
+ id: inlay.id,
756
+ part: inlay.part,
757
+ mod: this.options.apple ? event.metaKey : event.ctrlKey,
758
+ alt: event.altKey,
759
+ shift: event.shiftKey,
760
+ clicks: again ? 2 : 1,
761
+ touch
762
+ };
763
+ if (![...this.inlayHandlers].some((handler) => handler(click))) {
764
+ return false;
765
+ }
766
+ if (!touch) {
767
+ this.view.focus();
768
+ }
769
+ return true;
770
+ }
690
771
  /* Whether a handler of the host took a press on a character, such as a Mod+click that follows a name. */
691
772
  takenByHost(event) {
692
773
  if (this.clickHandlers.size === 0) {
@@ -709,6 +790,10 @@ export class InputController {
709
790
  return { line, character: column };
710
791
  }
711
792
  contextMenu(event) {
793
+ if (this.contextListeners.size === 0 && this.touch.pressed) {
794
+ event.preventDefault();
795
+ return;
796
+ }
712
797
  if (this.contextListeners.size === 0 || event.target?.closest?.('.se-sticky')) {
713
798
  return;
714
799
  }
@@ -732,6 +817,7 @@ export class InputController {
732
817
  }
733
818
  /* A press on a line number selects the line. */
734
819
  gutterDown(event) {
820
+ this.view.cancelScroll();
735
821
  if (event.button !== 0) {
736
822
  return;
737
823
  }
@@ -753,6 +839,10 @@ export class InputController {
753
839
  this.view.focus();
754
840
  return;
755
841
  }
842
+ if (event.pointerType === 'touch') {
843
+ this.touch.start(event, 'gutter');
844
+ return;
845
+ }
756
846
  this.pointer.start(event, 'gutter');
757
847
  this.view.focus();
758
848
  }
package/dist/editor.css CHANGED
@@ -12,10 +12,14 @@
12
12
  --editor-guide-active: color-mix(in srgb, var(--text) 24%, transparent);
13
13
  --editor-margin: color-mix(in srgb, var(--text) 14%, transparent);
14
14
  --editor-whitespace: color-mix(in srgb, var(--text) 30%, transparent);
15
+ --editor-bidi: var(--status-needs-you);
15
16
  --editor-hatch: color-mix(in srgb, var(--text) 12%, transparent);
16
17
  --editor-added: var(--status-idle);
17
18
  --editor-modified: var(--accent);
18
19
  --editor-deleted: var(--status-error);
20
+ --editor-mark-number-mix: 72%;
21
+ --editor-mark-bar-width: 3px;
22
+ --editor-mark-gap: 4px;
19
23
  --editor-sticky-shadow: color-mix(in srgb, black 12%, transparent);
20
24
  --agent-1: #c4602f;
21
25
  --agent-2: #7c4dd8;
@@ -34,6 +38,7 @@
34
38
  --editor-margin: color-mix(in srgb, var(--text) 16%, transparent);
35
39
  --editor-whitespace: color-mix(in srgb, var(--text) 32%, transparent);
36
40
  --editor-hatch: color-mix(in srgb, var(--text) 10%, transparent);
41
+ --editor-mark-number-mix: 82%;
37
42
  --editor-sticky-shadow: color-mix(in srgb, black 60%, transparent);
38
43
  }
39
44
 
@@ -69,6 +74,7 @@
69
74
  overflow: auto;
70
75
  overflow-anchor: none;
71
76
  overscroll-behavior: contain;
77
+ touch-action: pan-x pan-y pinch-zoom;
72
78
  scrollbar-width: thin;
73
79
  scrollbar-color: color-mix(in srgb, var(--text) 25%, transparent) transparent;
74
80
  }
@@ -177,10 +183,23 @@
177
183
  white-space: pre;
178
184
  letter-spacing: 0;
179
185
  direction: ltr;
186
+ unicode-bidi: bidi-override;
180
187
  }
181
188
  .se-run {
182
189
  position: absolute;
183
190
  white-space: pre;
191
+ unicode-bidi: bidi-override;
192
+ }
193
+ .se-bidi {
194
+ position: absolute;
195
+ box-sizing: border-box;
196
+ overflow: hidden;
197
+ border-radius: var(--radius-xs);
198
+ box-shadow: inset 0 0 0 1px var(--editor-bidi);
199
+ color: var(--editor-bidi);
200
+ text-align: center;
201
+ user-select: none;
202
+ pointer-events: none;
184
203
  }
185
204
 
186
205
  .se-gutter {
@@ -216,6 +235,31 @@
216
235
  .se-active-number {
217
236
  color: var(--text-muted);
218
237
  }
238
+ /* A line a host marked with a bar: the number in a shade of the mark that reads on the tint, and a bar along the edge. */
239
+ /* The tint stops short of the code, so the cell and the line read as two surfaces. */
240
+ .se-line-marked {
241
+ align-items: flex-start;
242
+ background: linear-gradient(to right, var(--se-tint, transparent) calc(100% - var(--editor-mark-gap)), transparent 0);
243
+ }
244
+ .se-line-marked > span:first-child {
245
+ display: flex;
246
+ align-items: center;
247
+ height: var(--se-line);
248
+ color: color-mix(in srgb, var(--se-mark) var(--editor-mark-number-mix), var(--text));
249
+ }
250
+ .se-line-bar {
251
+ position: absolute;
252
+ top: 0;
253
+ left: 0;
254
+ width: var(--editor-mark-bar-width);
255
+ height: 100%;
256
+ }
257
+ .se-line-bar-solid {
258
+ background: var(--se-mark);
259
+ }
260
+ .se-line-bar-hatched {
261
+ background: repeating-linear-gradient(135deg, var(--se-mark) 0 2px, transparent 2px 4px);
262
+ }
219
263
  .se-fold-toggle {
220
264
  position: absolute;
221
265
  top: 0;
@@ -299,6 +343,14 @@
299
343
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z'/%3E%3C/svg%3E")
300
344
  center / contain no-repeat;
301
345
  }
346
+ .se-gutter-marker[aria-pressed='true'] {
347
+ color: var(--accent);
348
+ }
349
+ /* Lucide's book-open. */
350
+ .se-gutter-marker[data-icon='book-open']::before {
351
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 7v14'/%3E%3Cpath d='M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z'/%3E%3C/svg%3E")
352
+ center / contain no-repeat;
353
+ }
302
354
  .se-change {
303
355
  position: absolute;
304
356
  top: 0;
@@ -337,9 +389,13 @@
337
389
  font-size: max(12px, calc(var(--code-font-size, 13px) - 2px));
338
390
  line-height: 1;
339
391
  }
340
- .se-line-highlight {
392
+ .se-line-highlight,
393
+ .se-line-emphasis {
341
394
  position: absolute;
342
395
  }
396
+ .se-line-emphasis {
397
+ border-radius: 2px;
398
+ }
343
399
 
344
400
  /* Code a host draws in a widget row: the editor's face, with the gutter column the document has. */
345
401
  .se-code-block {
@@ -685,6 +741,11 @@
685
741
  white-space: pre;
686
742
  cursor: default;
687
743
  }
744
+ .se-inlay-link {
745
+ text-decoration: underline;
746
+ text-underline-offset: 2px;
747
+ cursor: pointer;
748
+ }
688
749
  .se-composition {
689
750
  position: absolute;
690
751
  white-space: pre;
@@ -717,6 +778,13 @@
717
778
  white-space: pre-wrap;
718
779
  overflow-wrap: anywhere;
719
780
  }
781
+ /* Lines the host drew in place of the text: a row in the text column that scrolls with the code, selectable as the page's own text is. */
782
+ .se-block.se-replacement {
783
+ padding: 0;
784
+ color: var(--text);
785
+ white-space: normal;
786
+ user-select: text;
787
+ }
720
788
  /* The host's own row runs the width of the editor over the gutter and under the pinned headers, so the code layer has no stacking of its own. */
721
789
  .se-block.se-widget {
722
790
  z-index: 4;
@@ -790,6 +858,18 @@
790
858
  white-space: pre;
791
859
  pointer-events: none;
792
860
  }
861
+ .se-sr-only,
862
+ .se-live {
863
+ position: absolute;
864
+ width: 1px;
865
+ height: 1px;
866
+ margin: -1px;
867
+ padding: 0;
868
+ overflow: hidden;
869
+ clip-path: inset(50%);
870
+ white-space: nowrap;
871
+ border: 0;
872
+ }
793
873
  .se-notice {
794
874
  position: absolute;
795
875
  right: 16px;