@fusion.dev/helix 0.172.0 → 0.174.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 (41) hide show
  1. package/custom-elements.json +2140 -93
  2. package/dist/copy-button/root/copy-button-root.css.js +1 -1
  3. package/dist/element-events.generated.js +8 -1
  4. package/dist/graph/root/graph-root.css.js +2 -1
  5. package/dist/orb/root/orb-root.css.js +1 -1
  6. package/dist/orb/root/orb-root.d.ts +24 -5
  7. package/dist/orb/root/orb-root.js +24 -6
  8. package/dist/sandbox/index.d.ts +1 -0
  9. package/dist/sandbox/index.js +1 -0
  10. package/dist/sandbox/preview/sandbox-preview.d.ts +39 -2
  11. package/dist/sandbox/preview/sandbox-preview.js +37 -5
  12. package/dist/sandbox/root/sandbox-root.css.js +1 -1
  13. package/dist/sandbox/root/sandbox-root.d.ts +44 -4
  14. package/dist/sandbox/root/sandbox-root.js +92 -17
  15. package/dist/sandbox/source/editing.d.ts +111 -0
  16. package/dist/sandbox/source/editing.js +185 -0
  17. package/dist/sandbox/source/sandbox-source.css.js +2 -1
  18. package/dist/sandbox/source/sandbox-source.d.ts +131 -6
  19. package/dist/sandbox/source/sandbox-source.js +301 -17
  20. package/dist/strands/palette.d.ts +1 -1
  21. package/dist/strands/path.d.ts +17 -6
  22. package/dist/strands/path.js +149 -43
  23. package/dist/strands/renderer.d.ts +14 -0
  24. package/dist/strands/renderer.js +14 -4
  25. package/dist/strands/root/strands-root.css.js +1 -1
  26. package/dist/strands/root/strands-root.d.ts +41 -10
  27. package/dist/strands/root/strands-root.js +32 -5
  28. package/dist/strands/shader.d.ts +3 -3
  29. package/dist/strands/shader.js +35 -12
  30. package/dist/strands/stop/strands-stop.d.ts +5 -5
  31. package/dist/timeline/root/timeline-root.css.js +1 -1
  32. package/dist/virtual-list/reorder/index.d.ts +1 -0
  33. package/dist/virtual-list/reorder/index.js +5 -0
  34. package/dist/virtual-list/reorder/virtual-list-reorder.css.d.ts +1 -0
  35. package/dist/virtual-list/reorder/virtual-list-reorder.css.js +12 -0
  36. package/dist/virtual-list/root/virtual-list-root.d.ts +9 -0
  37. package/lint.json +1 -1
  38. package/package.json +6 -6
  39. package/types/jsx.d.ts +919 -79
  40. package/types/vue.d.ts +214 -15
  41. package/types/words.d.ts +27 -0
@@ -28,8 +28,37 @@ import { type SandboxTokenizer } from '../tokens.js';
28
28
  * that wraps keeps one number. The numbers are drawn, and hidden from screen
29
29
  * readers, which read the field.
30
30
  *
31
- * Enter keeps the indentation of the line it breaks, and the new line is
32
- * undone with the next undo as typing is.
31
+ * Enter keeps the indentation of the line it breaks, one level deeper after
32
+ * an opening bracket, and puts the closing bracket of an empty pair on a line
33
+ * of its own. The new line is undone with the next undo as typing is.
34
+ *
35
+ * **The editing keys are CodeMirror's.** Tab with text selected indents every
36
+ * line the selection touches. `Mod+]` indents the lines and `Mod+[` outdents
37
+ * them, and `Mod+/` comments them out or back in: `//` in a `js` pane, and one
38
+ * block comment in an `html` or `css` pane. `Mod` is Command on a Mac and
39
+ * Control elsewhere. Each is one step of undo.
40
+ *
41
+ * **`close-brackets` closes brackets and quotes** as you type them: `(`, `[`,
42
+ * `{` and the three quotes. A closing character typed before the same one
43
+ * steps over it, Backspace between an empty pair deletes both, and a bracket
44
+ * typed with text selected goes around the text. **`match-brackets` marks
45
+ * the bracket beside the caret and its pair** while the field has focus, each
46
+ * a `bracket` part, and a bracket with no pair is also `bracket-unmatched`.
47
+ * A bracket in a string or a comment is not matched, where `tokenize` says
48
+ * which runs those are.
49
+ *
50
+ * **`complete` offers completions as you type**: the words of the source and
51
+ * of `completions` that start with the word before the caret. They open in a
52
+ * list under the word. The arrow keys move through it, Enter or Tab takes the
53
+ * word, and Escape closes it. `Ctrl+Space` opens it where it is closed. The
54
+ * field is `aria-autocomplete="list"` and points at the word chosen with
55
+ * `aria-activedescendant`, so a screen reader reads each one. Give
56
+ * `completions` the names a page uses, such as the tag and attribute names of
57
+ * a custom elements manifest through `manifestWords`.
58
+ *
59
+ * **The pane is `:state(modified)` while its source differs from the source it
60
+ * was given**, the first `value` or the last one set from outside. `reset()`
61
+ * puts that source back, as an edit the sandbox passes on to its preview.
33
62
  *
34
63
  * **The source can be written inside the pane** where `value` is not set: its
35
64
  * text, or the markup of a `<template>` it holds, without the indentation the
@@ -45,15 +74,23 @@ import { type SandboxTokenizer } from '../tokens.js';
45
74
  * @event {HelixCaretEvent} helix-caret - Fired when the cursor moves.
46
75
  * @event {HelixEditEvent} helix-edit - Fired after the source was edited, with the new source.
47
76
  *
48
- * @csspart field - The box that scrolls the field and its colors together, under `tokenize` or `line-numbers`.
49
- * @csspart highlight - The colored copy of the source drawn under the field, under `tokenize` or `line-numbers`.
77
+ * @csspart bracket - The bracket beside the caret and its pair, under `match-brackets`.
78
+ * @csspart bracket-unmatched - A bracket beside the caret with no pair, under `match-brackets`.
79
+ * @csspart completion - One word in the list of completions, under `complete`.
80
+ * @csspart completion-active - The word chosen in that list, which Enter or Tab takes.
81
+ * @csspart completions - The list of completions under the word being typed, under `complete`.
82
+ * @csspart field - The box that scrolls the field and its colors together, under `tokenize`, `line-numbers`, `match-brackets` or `complete`.
83
+ * @csspart highlight - The colored copy of the source drawn under the field, under `tokenize`, `line-numbers`, `match-brackets` or `complete`.
50
84
  * @csspart line - One line of that copy, under `line-numbers`, numbered in the gutter by its `::before`.
51
85
  * @csspart source - The editable region.
52
86
  * @csspart token - One run of the source that has a kind, under `tokenize`.
53
87
  *
54
88
  * @cssproperty --sandbox-source-background-color - Fill behind the source.
89
+ * @cssproperty --sandbox-source-bracket-color - Fill behind a matched bracket, under `match-brackets`. Defaults to `--helix-color-accent-subtle`.
55
90
  * @cssproperty --sandbox-source-callable-color - Color of a `callable` run, under `tokenize`. Defaults to `--helix-color-syntax-callable`.
56
91
  * @cssproperty --sandbox-source-comment-color - Color of a `comment` run, under `tokenize`. Defaults to `--helix-color-syntax-comment`.
92
+ * @cssproperty --sandbox-source-completions-border-width - Width of the edge of the list of completions, under `complete`. Defaults to `--helix-border-floating`.
93
+ * @cssproperty --sandbox-source-completions-shadow - Shadow under the list of completions, under `complete`. Defaults to `--helix-shadow-floating`.
57
94
  * @cssproperty --sandbox-source-constant-color - Color of a `constant` run, under `tokenize`. Defaults to `--helix-color-syntax-constant`.
58
95
  * @cssproperty --sandbox-source-invalid-color - Color of an `invalid` run, under `tokenize`. Defaults to `--helix-color-syntax-invalid`.
59
96
  * @cssproperty --sandbox-source-keyword-color - Color of a `keyword` run, under `tokenize`. Defaults to `--helix-color-syntax-keyword`.
@@ -62,6 +99,8 @@ import { type SandboxTokenizer } from '../tokens.js';
62
99
  * @cssproperty --sandbox-source-name-color - Color of a `name` run, under `tokenize`. Defaults to `--helix-color-syntax-name`.
63
100
  * @cssproperty --sandbox-source-string-color - Color of a `string` run, under `tokenize`. Defaults to `--helix-color-syntax-string`.
64
101
  * @cssproperty --sandbox-source-tag-color - Color of a `tag` run, under `tokenize`. Defaults to `--helix-color-syntax-tag`.
102
+ *
103
+ * @cssstate modified - While the source differs from the source the pane was given, until `reset()` or a new `value`.
65
104
  */
66
105
  export declare class HelixSandboxSource extends HelixElement {
67
106
  /**
@@ -69,6 +108,26 @@ export declare class HelixSandboxSource extends HelixElement {
69
108
  * `registerWords('de', { 'helix-sandbox-source': { fieldLabel: 'Quelltext' } })`.
70
109
  */
71
110
  static words: string;
111
+ /**
112
+ * Whether typing an opening bracket or quote types its closing one too, and
113
+ * a bracket typed with text selected goes around it.
114
+ */
115
+ closeBrackets: boolean;
116
+ /**
117
+ * Whether the pane offers completions for the word before the caret, from
118
+ * the words of the source and of `completions`.
119
+ */
120
+ complete: boolean;
121
+ /**
122
+ * Words the pane offers under `complete` beside the words of the source,
123
+ * such as tag and attribute names. `manifestWords` reads them from a custom
124
+ * elements manifest. Set it as JSON in the attribute or as an array.
125
+ */
126
+ completions: readonly string[];
127
+ /**
128
+ * The name of the list of completions, read by screen readers.
129
+ */
130
+ completionsLabel: string;
72
131
  /**
73
132
  * The editable region's name, read by screen readers.
74
133
  */
@@ -92,6 +151,11 @@ export declare class HelixSandboxSource extends HelixElement {
92
151
  * Whether each line of the source is numbered, in a gutter beside it.
93
152
  */
94
153
  lineNumbers: boolean;
154
+ /**
155
+ * Whether the bracket beside the caret and its pair are marked while the
156
+ * field has focus.
157
+ */
158
+ matchBrackets: boolean;
95
159
  /**
96
160
  * Which source this pane holds. Every `helix-before-edit` event includes it.
97
161
  *
@@ -101,6 +165,12 @@ export declare class HelixSandboxSource extends HelixElement {
101
165
  * Not reflected: nothing selects on it, so it keeps a real default instead.
102
166
  */
103
167
  name: string;
168
+ /**
169
+ * Whether the source is read only: it can be selected and copied, and not
170
+ * edited, as Sandpack's `readOnly` file is. Tab moves focus out of a field
171
+ * that is read only.
172
+ */
173
+ readOnly: boolean;
104
174
  /**
105
175
  * Whether Tab indents the source. When it is off, Tab moves focus.
106
176
  */
@@ -125,6 +195,29 @@ export declare class HelixSandboxSource extends HelixElement {
125
195
  value: string;
126
196
  /** Set by Escape, so the next Tab leaves the field. */
127
197
  private escaped;
198
+ /** The source the pane was given, which `reset()` puts back. */
199
+ private baseline;
200
+ /** The source the last edit left, to tell an edit from a `value` set from outside. */
201
+ private edited;
202
+ /** Where the caret is, while nothing is selected, for `match-brackets`. */
203
+ private caret;
204
+ /** The completions open under the word being typed, under `complete`. */
205
+ private offered;
206
+ /** Which of the completions Enter or Tab takes. */
207
+ private chosen;
208
+ /** Set while a completion is written, so writing it does not offer more. */
209
+ private accepting;
210
+ /**
211
+ * Whether the source differs from the source the pane was given: its first
212
+ * `value`, or the last one set from outside.
213
+ */
214
+ get modified(): boolean;
215
+ /**
216
+ * Puts back the source the pane was given, as an edit: `helix-before-edit`
217
+ * is sent first and can refuse it, and the sandbox passes it to its preview.
218
+ */
219
+ reset(): void;
220
+ private get field();
128
221
  /** Whether the source written inside the pane has been read. */
129
222
  private read;
130
223
  /**
@@ -141,6 +234,8 @@ export declare class HelixSandboxSource extends HelixElement {
141
234
  private readWritten;
142
235
  connectedCallback(): void;
143
236
  protected willUpdate(changed: PropertyValues): void;
237
+ /** Places the completions under the word they complete, and keeps the chosen one in view. */
238
+ protected updated(changed: PropertyValues): void;
144
239
  /**
145
240
  * Reports where the cursor is, for a status bar to read.
146
241
  *
@@ -172,6 +267,14 @@ export declare class HelixSandboxSource extends HelixElement {
172
267
  * without sending the caret to the end. Exactly what `Input` does with the
173
268
  * same helper for the same reason. */
174
269
  private readonly handleInput;
270
+ /** Opens the completions for the word before the caret, or closes them where there are none. */
271
+ private offer;
272
+ /** Writes the chosen completion over the word it completes. */
273
+ private accept;
274
+ /**
275
+ * The keys of the open completions. Returns whether the key was theirs.
276
+ */
277
+ private completionKey;
175
278
  /**
176
279
  * The one place an edit is taken, so a refusal cannot be forgotten at one of
177
280
  * them. Two paths reach it, a keystroke and the tab that indents.
@@ -179,8 +282,21 @@ export declare class HelixSandboxSource extends HelixElement {
179
282
  private edit;
180
283
  private readonly handleKeydown;
181
284
  /**
182
- * Breaks the line with the indentation it has, where it has some. A line at
183
- * the margin is left to the platform's own Enter.
285
+ * The editing commands: the `Mod` keys for indenting and comments, and, where
286
+ * they are on, closing brackets and opening completions. Returns whether the
287
+ * key was one, so it does nothing else.
288
+ */
289
+ private command;
290
+ /**
291
+ * Makes a change as typing would, through `insert`, and puts the selection
292
+ * where the change says. A change that refuses nothing and replaces nothing
293
+ * only moves the caret.
294
+ */
295
+ private apply;
296
+ /**
297
+ * Breaks the line with the indentation it has, one level deeper after an
298
+ * opening bracket. A line at the margin with no bracket is left to the
299
+ * platform's own Enter.
184
300
  */
185
301
  private breakLine;
186
302
  /**
@@ -197,6 +313,15 @@ export declare class HelixSandboxSource extends HelixElement {
197
313
  */
198
314
  private insert;
199
315
  protected render(): TemplateResult;
316
+ /**
317
+ * The runs with the brackets beside the caret and the completions' anchor
318
+ * marked, where either is on.
319
+ */
320
+ private mark;
321
+ /** The list of completions, while it is open. */
322
+ private renderCompletions;
323
+ /** Closes the completions, as the field loses focus. */
324
+ private readonly close;
200
325
  /** The field itself and its description, which is the whole pane without `tokenize`. */
201
326
  private renderField;
202
327
  static styles: CSSResultGroup;
@@ -6,8 +6,9 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { customElement, formatWords, HelixBeforeEditEvent, HelixCaretEvent, HelixEditEvent, HelixElement, onUnlessFalse, restoreValue, words, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
- import { property } from 'lit/decorators.js';
9
+ import { property, state } from 'lit/decorators.js';
10
10
  import { drawnRuns } from '../tokens.js';
11
+ import { breakLine, closeBracket, deletePair, indentLines, matchBracket, outdentLines, suggest, syntaxOf, toggleComment, } from './editing.js';
11
12
  import { styles } from './sandbox-source.css.js';
12
13
  const MODIFIERS = new Set(['Alt', 'AltGraph', 'Control', 'Meta', 'Shift']);
13
14
  const dedent = (text) => {
@@ -20,37 +21,90 @@ const dedent = (text) => {
20
21
  .map((line) => /^[ \t]*/.exec(line)?.[0].length ?? 0));
21
22
  return Number.isFinite(shared) ? lines.map((line) => line.slice(shared)).join('\n') : '';
22
23
  };
24
+ const cut = (runs, marks) => {
25
+ if (marks.size === 0) {
26
+ return [...runs];
27
+ }
28
+ const points = [...marks.keys()].sort((a, b) => a - b);
29
+ const pieces = [];
30
+ let at = 0;
31
+ let next = 0;
32
+ for (const { kind, text } of runs) {
33
+ let from = 0;
34
+ for (let point = points[next]; point !== undefined && point < at + text.length; point = points[++next]) {
35
+ const mark = marks.get(point);
36
+ const index = point - at;
37
+ if (index > from) {
38
+ pieces.push({ kind, text: text.slice(from, index) });
39
+ }
40
+ from = mark === 'anchor' ? index : index + 1;
41
+ pieces.push({ kind, mark, text: text.slice(index, from) });
42
+ }
43
+ if (from < text.length) {
44
+ pieces.push({ kind, text: text.slice(from) });
45
+ }
46
+ at += text.length;
47
+ }
48
+ for (const point of points.slice(next)) {
49
+ pieces.push({ mark: marks.get(point), text: '' });
50
+ }
51
+ return pieces;
52
+ };
23
53
  const linesOf = (runs) => {
24
54
  const lines = [[]];
25
- for (const { kind, text } of runs) {
55
+ for (const { kind, mark, text } of runs) {
26
56
  text.split('\n').forEach((piece, at) => {
27
57
  if (at > 0) {
28
58
  lines.push([]);
29
59
  }
30
- if (piece !== '') {
31
- lines.at(-1)?.push(kind ? { kind, text: piece } : { text: piece });
60
+ if (piece !== '' || mark === 'anchor') {
61
+ lines.at(-1)?.push({ kind, mark, text: piece });
32
62
  }
33
63
  });
34
64
  }
35
65
  return lines;
36
66
  };
67
+ const quietOf = (runs, length) => {
68
+ const quiet = new Uint8Array(length);
69
+ let at = 0;
70
+ for (const { kind, text } of runs) {
71
+ if (kind === 'string' || kind === 'comment') {
72
+ quiet.fill(1, at, at + text.length);
73
+ }
74
+ at += text.length;
75
+ }
76
+ return quiet;
77
+ };
37
78
  let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
38
79
  constructor() {
39
80
  super(...arguments);
81
+ this.closeBrackets = false;
82
+ this.complete = false;
83
+ this.completions = [];
84
+ this.completionsLabel = 'Completions';
40
85
  this.fieldLabel = 'Example source';
41
86
  this.fieldNameLabel = '{field}, {name}';
42
87
  this.keysDescription = 'Tab inserts two spaces. To leave, press Escape and then Tab, or press Shift+Tab to go back.';
43
88
  this.label = '';
44
89
  this.lineNumbers = false;
90
+ this.matchBrackets = false;
45
91
  this.name = 'html';
92
+ this.readOnly = false;
46
93
  this.tabIndents = true;
47
94
  this.value = '';
48
95
  this.escaped = false;
96
+ this.baseline = '';
97
+ this.caret = -1;
98
+ this.chosen = 0;
99
+ this.accepting = false;
49
100
  this.read = false;
50
101
  this.reportCaret = (event) => {
51
102
  const field = event.target;
52
103
  const at = field.selectionStart ?? 0;
53
104
  const before = field.value.slice(0, at);
105
+ if (this.matchBrackets) {
106
+ this.caret = at === field.selectionEnd ? at : -1;
107
+ }
54
108
  this.dispatchEvent(new HelixCaretEvent({
55
109
  column: at - (before.lastIndexOf('\n') + 1) + 1,
56
110
  line: before.split('\n').length,
@@ -60,8 +114,24 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
60
114
  const field = event.target;
61
115
  this.edit(field, field.value);
62
116
  this.reportCaret(event);
117
+ const typed = event.inputType ?? '';
118
+ if (this.complete &&
119
+ !this.accepting &&
120
+ (this.offered || /^insert(?:Text|Replacement)/.test(typed))) {
121
+ this.offer(field);
122
+ }
123
+ else {
124
+ this.offered = undefined;
125
+ }
63
126
  };
64
127
  this.handleKeydown = (event) => {
128
+ const field = event.target;
129
+ if (this.readOnly) {
130
+ return;
131
+ }
132
+ if (this.completionKey(event, field)) {
133
+ return;
134
+ }
65
135
  if (event.key === 'Escape') {
66
136
  this.escaped = true;
67
137
  return;
@@ -69,6 +139,11 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
69
139
  if (MODIFIERS.has(event.key)) {
70
140
  return;
71
141
  }
142
+ if (this.command(event, field)) {
143
+ this.escaped = false;
144
+ event.preventDefault();
145
+ return;
146
+ }
72
147
  if (event.key === 'Enter' &&
73
148
  !event.isComposing &&
74
149
  !(event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)) {
@@ -81,10 +156,30 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
81
156
  return;
82
157
  }
83
158
  event.preventDefault();
84
- this.insert(event.target, ' ');
159
+ if (field.selectionStart === field.selectionEnd) {
160
+ this.insert(field, ' ');
161
+ }
162
+ else {
163
+ this.apply(field, indentLines(field.value, field.selectionStart, field.selectionEnd));
164
+ }
165
+ };
166
+ this.close = () => {
167
+ this.offered = undefined;
85
168
  };
86
169
  }
87
170
  static { this.words = 'helix-sandbox-source'; }
171
+ get modified() {
172
+ return this.value === this.edited && this.value !== this.baseline;
173
+ }
174
+ reset() {
175
+ const field = this.field;
176
+ if (field && this.modified) {
177
+ this.edit(field, this.baseline);
178
+ }
179
+ }
180
+ get field() {
181
+ return this.renderRoot.querySelector('textarea');
182
+ }
88
183
  readWritten() {
89
184
  if (this.read ||
90
185
  this.children === undefined ||
@@ -108,11 +203,77 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
108
203
  if (!this.hasUpdated) {
109
204
  this.readWritten();
110
205
  }
206
+ if (this.value !== this.edited) {
207
+ this.baseline = this.value;
208
+ this.edited = undefined;
209
+ }
210
+ this.toggleState('modified', this.modified);
211
+ }
212
+ updated(changed) {
213
+ super.updated(changed);
214
+ const list = this.renderRoot.querySelector('[part="completions"]');
215
+ const anchor = this.renderRoot.querySelector('#anchor');
216
+ const box = this.renderRoot.querySelector('[part="field"]');
217
+ if (!list || !anchor || !box) {
218
+ return;
219
+ }
220
+ const at = anchor.getBoundingClientRect();
221
+ const frame = box.getBoundingClientRect();
222
+ list.style.top = `${at.bottom - frame.top + box.scrollTop}px`;
223
+ list.style.left = `${at.left - frame.left + box.scrollLeft}px`;
224
+ list.querySelector('[aria-selected="true"]')?.scrollIntoView?.({ block: 'nearest' });
225
+ }
226
+ offer(field) {
227
+ const at = field.selectionStart;
228
+ this.offered =
229
+ at === field.selectionEnd ? suggest(field.value, at, this.completions) : undefined;
230
+ this.chosen = 0;
231
+ }
232
+ accept(field, index) {
233
+ const offered = this.offered;
234
+ const word = offered?.words[index];
235
+ this.offered = undefined;
236
+ if (!offered || word === undefined) {
237
+ return;
238
+ }
239
+ this.accepting = true;
240
+ this.apply(field, {
241
+ from: offered.from,
242
+ selection: [offered.from + word.length, offered.from + word.length],
243
+ text: word,
244
+ to: field.selectionEnd,
245
+ });
246
+ this.accepting = false;
247
+ }
248
+ completionKey(event, field) {
249
+ const offered = this.offered;
250
+ if (!offered) {
251
+ return false;
252
+ }
253
+ const count = offered.words.length;
254
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
255
+ this.chosen = (this.chosen + (event.key === 'ArrowDown' ? 1 : count - 1)) % count;
256
+ }
257
+ else if (event.key === 'Enter' || (event.key === 'Tab' && !event.shiftKey)) {
258
+ this.accept(field, this.chosen);
259
+ }
260
+ else if (event.key === 'Escape') {
261
+ this.offered = undefined;
262
+ }
263
+ else {
264
+ if (/^(?:Arrow|Home|End|Page)/.test(event.key)) {
265
+ this.offered = undefined;
266
+ }
267
+ return false;
268
+ }
269
+ event.preventDefault();
270
+ return true;
111
271
  }
112
272
  edit(field, next) {
113
273
  const request = new HelixBeforeEditEvent({ value: next });
114
274
  const accepted = this.dispatchEvent(request);
115
275
  if (accepted) {
276
+ this.edited = next;
116
277
  this.value = next;
117
278
  }
118
279
  restoreValue(field, this.value);
@@ -120,14 +281,56 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
120
281
  this.dispatchEvent(new HelixEditEvent({ value: next }));
121
282
  }
122
283
  }
284
+ command(event, field) {
285
+ const { selectionEnd: to, selectionStart: from, value } = field;
286
+ const mod = (event.ctrlKey || event.metaKey) && !event.altKey;
287
+ if (event.isComposing) {
288
+ return false;
289
+ }
290
+ if (mod && (event.key === ']' || event.key === '[')) {
291
+ this.apply(field, (event.key === ']' ? indentLines : outdentLines)(value, from, to));
292
+ return true;
293
+ }
294
+ const syntax = syntaxOf(this.name);
295
+ if (mod && event.key === '/' && syntax) {
296
+ this.apply(field, toggleComment(value, from, to, syntax));
297
+ return true;
298
+ }
299
+ if (this.complete && event.ctrlKey && event.key === ' ') {
300
+ this.offer(field);
301
+ return true;
302
+ }
303
+ if (!this.closeBrackets || mod || event.altKey) {
304
+ return false;
305
+ }
306
+ const change = event.key === 'Backspace'
307
+ ? deletePair(value, from, to)
308
+ : event.key.length === 1
309
+ ? closeBracket(value, from, to, event.key)
310
+ : undefined;
311
+ this.apply(field, change);
312
+ return change !== undefined;
313
+ }
314
+ apply(field, change) {
315
+ if (!change) {
316
+ return;
317
+ }
318
+ const { from, selection, text, to } = change;
319
+ const next = `${field.value.slice(0, from)}${text}${field.value.slice(to)}`;
320
+ if (text !== '' || from !== to) {
321
+ field.setSelectionRange(from, to);
322
+ this.insert(field, text);
323
+ }
324
+ if (field.value === next) {
325
+ field.setSelectionRange(...selection);
326
+ }
327
+ }
123
328
  breakLine(event) {
124
329
  const field = event.target;
125
- const at = field.selectionStart ?? 0;
126
- const start = field.value.lastIndexOf('\n', at - 1) + 1;
127
- const indent = /^[ \t]*/.exec(field.value.slice(start, at))?.[0] ?? '';
128
- if (indent !== '') {
330
+ const change = breakLine(field.value, field.selectionStart, field.selectionEnd);
331
+ if (change) {
129
332
  event.preventDefault();
130
- this.insert(field, `\n${indent}`);
333
+ this.apply(field, change);
131
334
  }
132
335
  }
133
336
  insert(field, text) {
@@ -135,7 +338,9 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
135
338
  const focused = field.getRootNode().activeElement === field;
136
339
  if (focused &&
137
340
  typeof ownerDocument.execCommand === 'function' &&
138
- ownerDocument.execCommand('insertText', false, text)) {
341
+ (text === ''
342
+ ? ownerDocument.execCommand('delete')
343
+ : ownerDocument.execCommand('insertText', false, text))) {
139
344
  return;
140
345
  }
141
346
  field.setRangeText(text, field.selectionStart, field.selectionEnd, 'end');
@@ -143,7 +348,7 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
143
348
  }
144
349
  render() {
145
350
  const field = this.renderField();
146
- if (!this.tokenize && !this.lineNumbers) {
351
+ if (!this.tokenize && !this.lineNumbers && !this.matchBrackets && !this.complete) {
147
352
  return field;
148
353
  }
149
354
  let given = [];
@@ -153,18 +358,63 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
153
358
  catch (error) {
154
359
  globalThis.reportError?.(error);
155
360
  }
156
- const runs = drawnRuns(this.value, given);
157
- const draw = (some) => some.map(({ kind, text }) => kind ? html `<span part="token" data-kind=${kind}>${text}</span>` : text);
361
+ const runs = this.mark(drawnRuns(this.value, given));
362
+ const draw = (some) => some.map(({ kind, mark, text }) => {
363
+ const lone = mark === 'unmatched';
364
+ const shown = mark === 'anchor'
365
+ ? html `<span id="anchor"></span>`
366
+ : mark
367
+ ? html `<span part="bracket${lone ? ' bracket-unmatched' : ''}">${text}</span>`
368
+ : text;
369
+ return kind ? html `<span part="token" data-kind=${kind}>${shown}</span>` : shown;
370
+ });
158
371
  if (this.lineNumbers) {
159
372
  const lines = linesOf(runs);
160
373
  return html `<div part="field" style="--_sandbox-source-gutter: ${String(lines.length).length}ch">
161
374
  <pre part="highlight" aria-hidden="true">${lines.map((line) => html `<span part="line">${draw(line)}</span>`)}</pre>
162
- ${field}
375
+ ${field}${this.renderCompletions()}
163
376
  </div>`;
164
377
  }
165
378
  return html `<div part="field">
166
379
  <pre part="highlight" aria-hidden="true">${draw(runs)}${'\n'}</pre>
167
- ${field}
380
+ ${field}${this.renderCompletions()}
381
+ </div>`;
382
+ }
383
+ mark(runs) {
384
+ const marks = new Map();
385
+ if (this.matchBrackets && this.caret >= 0) {
386
+ const quiet = quietOf(runs, this.value.length);
387
+ const found = matchBracket(this.value, this.caret, (index) => quiet[index] === 1);
388
+ if (found) {
389
+ marks.set(found.at, found.match === undefined ? 'unmatched' : 'bracket');
390
+ if (found.match !== undefined) {
391
+ marks.set(found.match, 'bracket');
392
+ }
393
+ }
394
+ }
395
+ if (this.offered) {
396
+ marks.set(this.offered.from, 'anchor');
397
+ }
398
+ return cut(runs, marks);
399
+ }
400
+ renderCompletions() {
401
+ const offered = this.offered;
402
+ if (!offered) {
403
+ return nothing;
404
+ }
405
+ return html `<div part="completions" id="completions" role="listbox" aria-label=${this.completionsLabel}>
406
+ ${offered.words.map((word, at) => {
407
+ const chosen = at === this.chosen;
408
+ return html `<div part="completion${chosen ? ' completion-active' : ''}" id=${`completion-${at}`}
409
+ role="option" aria-selected=${chosen ? 'true' : 'false'}
410
+ @pointerdown=${(event) => {
411
+ event.preventDefault();
412
+ const field = this.field;
413
+ if (field) {
414
+ this.accept(field, at);
415
+ }
416
+ }}>${word}</div>`;
417
+ })}
168
418
  </div>`;
169
419
  }
170
420
  renderField() {
@@ -173,10 +423,16 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
173
423
  aria-label=${this.label
174
424
  ? formatWords(this, 'fieldNameLabel', { field: this.fieldLabel, name: this.label })
175
425
  : this.fieldLabel}
176
- aria-describedby=${this.tabIndents ? 'keys' : nothing}
426
+ aria-describedby=${this.tabIndents && !this.readOnly ? 'keys' : nothing}
427
+ aria-autocomplete=${this.complete ? 'list' : nothing}
428
+ aria-controls=${this.offered ? 'completions' : nothing}
429
+ aria-activedescendant=${this.offered ? `completion-${this.chosen}` : nothing}
177
430
  spellcheck="false"
431
+ ?readonly=${this.readOnly}
178
432
  .value=${this.value}
433
+ @blur=${this.close}
179
434
  @focus=${this.reportCaret}
435
+ @pointerdown=${this.close}
180
436
  @input=${this.handleInput}
181
437
  @keydown=${this.handleKeydown}
182
438
  @keyup=${this.reportCaret}
@@ -186,6 +442,19 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
186
442
  }
187
443
  static { this.styles = styles; }
188
444
  };
445
+ __decorate([
446
+ property({ attribute: 'close-brackets', type: Boolean })
447
+ ], HelixSandboxSource.prototype, "closeBrackets", void 0);
448
+ __decorate([
449
+ property({ type: Boolean })
450
+ ], HelixSandboxSource.prototype, "complete", void 0);
451
+ __decorate([
452
+ property({ type: Array })
453
+ ], HelixSandboxSource.prototype, "completions", void 0);
454
+ __decorate([
455
+ words,
456
+ property({ attribute: 'completions-label' })
457
+ ], HelixSandboxSource.prototype, "completionsLabel", void 0);
189
458
  __decorate([
190
459
  words,
191
460
  property({ attribute: 'field-label' })
@@ -204,9 +473,15 @@ __decorate([
204
473
  __decorate([
205
474
  property({ attribute: 'line-numbers', reflect: true, type: Boolean })
206
475
  ], HelixSandboxSource.prototype, "lineNumbers", void 0);
476
+ __decorate([
477
+ property({ attribute: 'match-brackets', type: Boolean })
478
+ ], HelixSandboxSource.prototype, "matchBrackets", void 0);
207
479
  __decorate([
208
480
  property()
209
481
  ], HelixSandboxSource.prototype, "name", void 0);
482
+ __decorate([
483
+ property({ attribute: 'readonly', reflect: true, type: Boolean })
484
+ ], HelixSandboxSource.prototype, "readOnly", void 0);
210
485
  __decorate([
211
486
  property({ attribute: 'tab-indents', converter: onUnlessFalse })
212
487
  ], HelixSandboxSource.prototype, "tabIndents", void 0);
@@ -216,6 +491,15 @@ __decorate([
216
491
  __decorate([
217
492
  property()
218
493
  ], HelixSandboxSource.prototype, "value", void 0);
494
+ __decorate([
495
+ state()
496
+ ], HelixSandboxSource.prototype, "caret", void 0);
497
+ __decorate([
498
+ state()
499
+ ], HelixSandboxSource.prototype, "offered", void 0);
500
+ __decorate([
501
+ state()
502
+ ], HelixSandboxSource.prototype, "chosen", void 0);
219
503
  HelixSandboxSource = __decorate([
220
504
  customElement('helix-sandbox-source')
221
505
  ], HelixSandboxSource);