@fusion.dev/helix 0.172.0 → 0.173.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/custom-elements.json +1963 -69
- package/dist/copy-button/root/copy-button-root.css.js +1 -1
- package/dist/element-events.generated.js +8 -1
- package/dist/graph/root/graph-root.css.js +2 -1
- package/dist/orb/root/orb-root.css.js +1 -1
- package/dist/orb/root/orb-root.d.ts +24 -5
- package/dist/orb/root/orb-root.js +24 -6
- package/dist/sandbox/index.d.ts +1 -0
- package/dist/sandbox/index.js +1 -0
- package/dist/sandbox/preview/sandbox-preview.d.ts +39 -2
- package/dist/sandbox/preview/sandbox-preview.js +37 -5
- package/dist/sandbox/root/sandbox-root.css.js +1 -1
- package/dist/sandbox/root/sandbox-root.d.ts +44 -4
- package/dist/sandbox/root/sandbox-root.js +92 -17
- package/dist/sandbox/source/editing.d.ts +111 -0
- package/dist/sandbox/source/editing.js +185 -0
- package/dist/sandbox/source/sandbox-source.css.js +2 -1
- package/dist/sandbox/source/sandbox-source.d.ts +131 -6
- package/dist/sandbox/source/sandbox-source.js +301 -17
- package/dist/timeline/root/timeline-root.css.js +1 -1
- package/dist/virtual-list/reorder/index.d.ts +1 -0
- package/dist/virtual-list/reorder/index.js +5 -0
- package/dist/virtual-list/reorder/virtual-list-reorder.css.d.ts +1 -0
- package/dist/virtual-list/reorder/virtual-list-reorder.css.js +12 -0
- package/dist/virtual-list/root/virtual-list-root.d.ts +9 -0
- package/lint.json +1 -1
- package/package.json +6 -6
- package/types/jsx.d.ts +802 -34
- package/types/vue.d.ts +184 -5
- package/types/words.d.ts +27 -0
|
@@ -13,7 +13,19 @@ const DEFAULT_SPLIT = 50;
|
|
|
13
13
|
const DEFAULT_PANEL = 160;
|
|
14
14
|
const PANEL_MAX = 0.8;
|
|
15
15
|
const PANE_KEYS = { css: 'styles', html: 'source', js: 'script' };
|
|
16
|
-
const
|
|
16
|
+
const MODULE = /\.m?js$/;
|
|
17
|
+
const MANIFEST = 'package.json';
|
|
18
|
+
const isPaneName = (name) => typeof name === 'string' &&
|
|
19
|
+
(Object.hasOwn(PANE_KEYS, name) || MODULE.test(name) || name === MANIFEST);
|
|
20
|
+
const dependenciesOf = (text) => {
|
|
21
|
+
try {
|
|
22
|
+
const { dependencies } = JSON.parse(text);
|
|
23
|
+
return { ...dependencies };
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
17
29
|
const nameOf = (pane) => {
|
|
18
30
|
const { name } = pane;
|
|
19
31
|
return typeof name === 'string' ? name : (pane.getAttribute('name') ?? '');
|
|
@@ -30,11 +42,15 @@ let HelixSandbox = class HelixSandbox extends HelixElement {
|
|
|
30
42
|
this.defaultSplit = DEFAULT_SPLIT;
|
|
31
43
|
this.filesLabel = 'Files';
|
|
32
44
|
this.lead = 'source';
|
|
45
|
+
this.modifiedLabel = 'Modified';
|
|
33
46
|
this.panelCollapsedSize = 0;
|
|
34
47
|
this.panelLabel = 'Resize panel';
|
|
35
48
|
this.panelSize = DEFAULT_PANEL;
|
|
36
49
|
this.previewDelay = 300;
|
|
37
50
|
this.edits = new Map();
|
|
51
|
+
this.given = new Map();
|
|
52
|
+
this.texts = new Map();
|
|
53
|
+
this.drawnModified = new Map();
|
|
38
54
|
this.resting = new DelayController(this, { renders: false });
|
|
39
55
|
this.runEdits = () => {
|
|
40
56
|
for (const [name, value] of this.edits) {
|
|
@@ -65,7 +81,7 @@ let HelixSandbox = class HelixSandbox extends HelixElement {
|
|
|
65
81
|
event.preventDefault();
|
|
66
82
|
this.chooseFile(pane);
|
|
67
83
|
void this.updateComplete.then(() => {
|
|
68
|
-
this.renderRoot.querySelector(`[part
|
|
84
|
+
this.renderRoot.querySelector(`[part~="file"][aria-selected="true"]`)?.focus();
|
|
69
85
|
});
|
|
70
86
|
};
|
|
71
87
|
this.bodyHeight = 0;
|
|
@@ -81,13 +97,22 @@ let HelixSandbox = class HelixSandbox extends HelixElement {
|
|
|
81
97
|
const { value } = event.detail;
|
|
82
98
|
const pane = event.composedPath()[0];
|
|
83
99
|
const name = pane?.name;
|
|
84
|
-
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
100
|
+
const source = this.sources.find((each) => each === pane || each.contains(pane));
|
|
87
101
|
queueMicrotask(() => {
|
|
88
102
|
if (event.defaultPrevented) {
|
|
89
103
|
return;
|
|
90
104
|
}
|
|
105
|
+
if (source) {
|
|
106
|
+
this.texts.set(source, value);
|
|
107
|
+
const modified = this.modifiedOf(source);
|
|
108
|
+
if (this.drawnModified.get(source) !== modified) {
|
|
109
|
+
this.drawnModified.set(source, modified);
|
|
110
|
+
this.requestUpdate();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!isPaneName(name)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
91
116
|
this.edits.set(name, value);
|
|
92
117
|
if (this.previewDelay > 0) {
|
|
93
118
|
this.resting.start(this.previewDelay, this.runEdits);
|
|
@@ -99,10 +124,12 @@ let HelixSandbox = class HelixSandbox extends HelixElement {
|
|
|
99
124
|
};
|
|
100
125
|
this.seed = () => {
|
|
101
126
|
this.readFiles();
|
|
102
|
-
const
|
|
103
|
-
for (const pane of slot?.assignedElements({ flatten: true }) ?? []) {
|
|
127
|
+
for (const pane of this.sources) {
|
|
104
128
|
const { name, value } = pane;
|
|
105
129
|
const text = typeof value === 'string' ? value : pane.getAttribute('value');
|
|
130
|
+
if (text !== null && !this.given.has(pane)) {
|
|
131
|
+
this.given.set(pane, text);
|
|
132
|
+
}
|
|
106
133
|
if (isPaneName(name) && text) {
|
|
107
134
|
this.write(name, text, true);
|
|
108
135
|
}
|
|
@@ -166,12 +193,42 @@ let HelixSandbox = class HelixSandbox extends HelixElement {
|
|
|
166
193
|
};
|
|
167
194
|
}
|
|
168
195
|
static { this.words = 'helix-sandbox'; }
|
|
196
|
+
get sources() {
|
|
197
|
+
const slot = this.renderRoot.querySelector('slot[name="source"]');
|
|
198
|
+
return slot?.assignedElements({ flatten: true }) ?? [];
|
|
199
|
+
}
|
|
200
|
+
modifiedOf(pane) {
|
|
201
|
+
const { modified } = pane;
|
|
202
|
+
if (typeof modified === 'boolean') {
|
|
203
|
+
return modified;
|
|
204
|
+
}
|
|
205
|
+
return this.texts.has(pane) && this.texts.get(pane) !== this.given.get(pane);
|
|
206
|
+
}
|
|
207
|
+
reset() {
|
|
208
|
+
for (const pane of this.sources) {
|
|
209
|
+
const { reset } = pane;
|
|
210
|
+
const text = this.given.get(pane);
|
|
211
|
+
const name = nameOf(pane);
|
|
212
|
+
if (typeof reset === 'function') {
|
|
213
|
+
reset.call(pane);
|
|
214
|
+
}
|
|
215
|
+
else if (text !== undefined && this.modifiedOf(pane)) {
|
|
216
|
+
;
|
|
217
|
+
pane.value = text;
|
|
218
|
+
this.texts.delete(pane);
|
|
219
|
+
if (isPaneName(name)) {
|
|
220
|
+
this.write(name, text, false);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
this.drawnModified.clear();
|
|
225
|
+
this.requestUpdate();
|
|
226
|
+
}
|
|
169
227
|
get shownFile() {
|
|
170
228
|
return this.files.find((pane) => nameOf(pane) === this.file) ?? this.files[0];
|
|
171
229
|
}
|
|
172
230
|
readFiles() {
|
|
173
|
-
const
|
|
174
|
-
const panes = slot?.assignedElements({ flatten: true }) ?? [];
|
|
231
|
+
const panes = this.sources;
|
|
175
232
|
const files = panes.length > 1 ? panes : [];
|
|
176
233
|
if (files.length !== this.files.length || files.some((pane, at) => pane !== this.files[at])) {
|
|
177
234
|
this.files = files;
|
|
@@ -192,10 +249,13 @@ let HelixSandbox = class HelixSandbox extends HelixElement {
|
|
|
192
249
|
return html `<div part="files" role="tablist" aria-label=${this.filesLabel} @keydown=${this.handleFileKey}>
|
|
193
250
|
${this.files.map((pane, at) => {
|
|
194
251
|
const selected = pane === shown;
|
|
195
|
-
|
|
196
|
-
|
|
252
|
+
const modified = this.modifiedOf(pane);
|
|
253
|
+
return html `<button part="file${modified ? ' file-modified' : ''}" id=${`file-${at}`}
|
|
254
|
+
role="tab" type="button" aria-controls="files-panel"
|
|
255
|
+
aria-describedby=${modified ? 'modified' : nothing} aria-selected=${selected ? 'true' : 'false'}
|
|
197
256
|
tabindex=${selected ? '0' : '-1'} @click=${() => this.chooseFile(pane)}>${fileOf(pane)}</button>`;
|
|
198
257
|
})}
|
|
258
|
+
<span id="modified" hidden>${this.modifiedLabel}</span>
|
|
199
259
|
</div>`;
|
|
200
260
|
}
|
|
201
261
|
showFile() {
|
|
@@ -240,14 +300,24 @@ let HelixSandbox = class HelixSandbox extends HelixElement {
|
|
|
240
300
|
if (!(target instanceof Node)) {
|
|
241
301
|
return false;
|
|
242
302
|
}
|
|
243
|
-
|
|
244
|
-
return (slot?.assignedElements({ flatten: true }) ?? []).some((pane) => pane === target || pane.contains(target));
|
|
303
|
+
return this.sources.some((pane) => pane === target || pane.contains(target));
|
|
245
304
|
}
|
|
246
305
|
write(name, value, seeding) {
|
|
306
|
+
const dependencies = name === MANIFEST ? dependenciesOf(value) : undefined;
|
|
247
307
|
for (const preview of this.previews) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
preview[key]
|
|
308
|
+
if (Object.hasOwn(PANE_KEYS, name)) {
|
|
309
|
+
const key = PANE_KEYS[name];
|
|
310
|
+
if (!seeding || !preview[key]) {
|
|
311
|
+
preview[key] = value;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
else if (dependencies) {
|
|
315
|
+
if (!seeding || !preview.dependencies) {
|
|
316
|
+
preview.dependencies = dependencies;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
else if (MODULE.test(name) && (!seeding || preview.modules?.[name] === undefined)) {
|
|
320
|
+
preview.modules = { ...preview.modules, [name]: value };
|
|
251
321
|
}
|
|
252
322
|
}
|
|
253
323
|
}
|
|
@@ -394,9 +464,10 @@ let HelixSandbox = class HelixSandbox extends HelixElement {
|
|
|
394
464
|
if (changed.has('files') || changed.has('file')) {
|
|
395
465
|
this.showFile();
|
|
396
466
|
}
|
|
467
|
+
this.toggleState('modified', this.sources.some((pane) => this.modifiedOf(pane)));
|
|
397
468
|
if (this.files.length > 0) {
|
|
398
469
|
auditName(this, () => {
|
|
399
|
-
const tabs = [...this.renderRoot.querySelectorAll('[part
|
|
470
|
+
const tabs = [...this.renderRoot.querySelectorAll('[part~="file"]')];
|
|
400
471
|
return tabs.find((tab) => !tab.textContent?.trim()) ?? tabs[0];
|
|
401
472
|
}, 'Give each source pane a `label` or a `name`, which its file tab shows.');
|
|
402
473
|
}
|
|
@@ -459,6 +530,10 @@ __decorate([
|
|
|
459
530
|
__decorate([
|
|
460
531
|
property()
|
|
461
532
|
], HelixSandbox.prototype, "lead", void 0);
|
|
533
|
+
__decorate([
|
|
534
|
+
words,
|
|
535
|
+
property({ attribute: 'modified-label' })
|
|
536
|
+
], HelixSandbox.prototype, "modifiedLabel", void 0);
|
|
462
537
|
__decorate([
|
|
463
538
|
property()
|
|
464
539
|
], HelixSandbox.prototype, "orientation", void 0);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The editing commands of the source pane, as functions from the source and
|
|
3
|
+
* the selection to the change they make. Kept apart from the element so each
|
|
4
|
+
* can be read and tested on text alone.
|
|
5
|
+
*/
|
|
6
|
+
/** A change to the source: `text` in place of what is between `from` and `to`, and the selection after it. */
|
|
7
|
+
export interface SourceEdit {
|
|
8
|
+
/** Where the replaced text starts. */
|
|
9
|
+
from: number;
|
|
10
|
+
/** The selection after the change, its start and its end. */
|
|
11
|
+
selection: readonly [number, number];
|
|
12
|
+
/** What replaces the text between `from` and `to`. Empty where the change only moves the caret. */
|
|
13
|
+
text: string;
|
|
14
|
+
/** Where the replaced text ends. */
|
|
15
|
+
to: number;
|
|
16
|
+
}
|
|
17
|
+
/** The comment markers of each syntax: a line marker, or the two around a block. */
|
|
18
|
+
declare const COMMENTS: {
|
|
19
|
+
readonly css: readonly ['/*', '*/'];
|
|
20
|
+
readonly html: readonly ['<!--', '-->'];
|
|
21
|
+
readonly js: readonly ['//'];
|
|
22
|
+
};
|
|
23
|
+
/** A syntax the comment command knows. */
|
|
24
|
+
export type CommentSyntax = keyof typeof COMMENTS;
|
|
25
|
+
/**
|
|
26
|
+
* The syntax of a pane, read from its `name`: `html`, `css` and `js`, or the
|
|
27
|
+
* extension of a file name such as `utils.js`. `ts`, `jsx`, `tsx`, `mjs` and
|
|
28
|
+
* `cjs` are `js`. Anything else has no comments.
|
|
29
|
+
*/
|
|
30
|
+
export declare const syntaxOf: (name: string) => CommentSyntax | undefined;
|
|
31
|
+
/** Indents each line a selection touches by `unit`, leaving blank lines as they are. */
|
|
32
|
+
export declare const indentLines: (source: string, from: number, to: number, unit?: string) => SourceEdit | undefined;
|
|
33
|
+
/** Takes one level of indentation, two spaces or a tab, off each line a selection touches. */
|
|
34
|
+
export declare const outdentLines: (source: string, from: number, to: number) => SourceEdit | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* What Enter does: a new line with the indentation of the line it breaks, one
|
|
37
|
+
* level deeper after an opening bracket, and with the closing bracket on a
|
|
38
|
+
* line of its own where the caret is between a pair. `undefined` for a line
|
|
39
|
+
* at the margin with no bracket before the caret, which takes the platform's
|
|
40
|
+
* own Enter.
|
|
41
|
+
*/
|
|
42
|
+
export declare const breakLine: (source: string, from: number, to: number, unit?: string) => SourceEdit | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Comments out the lines a selection touches, or uncomments them where they
|
|
45
|
+
* already are a comment.
|
|
46
|
+
*
|
|
47
|
+
* `js` comments each line with `//` at the indentation the lines share. `css`
|
|
48
|
+
* and `html`, which have no line comment, put one block comment around the
|
|
49
|
+
* text of the lines.
|
|
50
|
+
*/
|
|
51
|
+
export declare const toggleComment: (source: string, from: number, to: number, syntax: CommentSyntax) => SourceEdit | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* What typing `key` does where brackets close: a bracket or quote typed with
|
|
54
|
+
* text selected goes around the text, one typed before a space or the end of
|
|
55
|
+
* the line closes as well, and a closing one typed before the same character
|
|
56
|
+
* steps over it. `undefined` where the key types as usual.
|
|
57
|
+
*
|
|
58
|
+
* A quote after a letter or a digit is an apostrophe and does not close.
|
|
59
|
+
*/
|
|
60
|
+
export declare const closeBracket: (source: string, from: number, to: number, key: string) => SourceEdit | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* What Backspace does between a bracket or quote and the character that
|
|
63
|
+
* closes it, where brackets close: it deletes both.
|
|
64
|
+
*/
|
|
65
|
+
export declare const deletePair: (source: string, from: number, to: number) => SourceEdit | undefined;
|
|
66
|
+
/** A bracket beside the caret, and the bracket that pairs with it where there is one. */
|
|
67
|
+
export interface BracketMatch {
|
|
68
|
+
/** Where the bracket beside the caret is. */
|
|
69
|
+
at: number;
|
|
70
|
+
/** Where the bracket that pairs with it is, or `undefined` where none does. */
|
|
71
|
+
match: number | undefined;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The bracket beside the caret and its pair: the one before the caret first,
|
|
75
|
+
* then the one after it. `skip` leaves out positions inside strings and
|
|
76
|
+
* comments, where a highlighter has said which those are.
|
|
77
|
+
*/
|
|
78
|
+
export declare const matchBracket: (source: string, at: number, skip?: (index: number) => boolean) => BracketMatch | undefined;
|
|
79
|
+
/** The completions for the word before the caret, and where that word starts. */
|
|
80
|
+
export interface Completions {
|
|
81
|
+
/** Where the word being completed starts. */
|
|
82
|
+
from: number;
|
|
83
|
+
/** The words that complete it, in alphabetical order. */
|
|
84
|
+
words: string[];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The completions for the word before the caret: every longer word in the
|
|
88
|
+
* source or in `extra` that starts with it, whatever its case. `undefined`
|
|
89
|
+
* where the caret follows no word or nothing completes it.
|
|
90
|
+
*/
|
|
91
|
+
export declare const suggest: (source: string, at: number, extra?: readonly string[]) => Completions | undefined;
|
|
92
|
+
/** The parts of a custom elements manifest that `manifestWords` reads. */
|
|
93
|
+
export interface ManifestLike {
|
|
94
|
+
modules?: readonly {
|
|
95
|
+
declarations?: readonly {
|
|
96
|
+
attributes?: readonly {
|
|
97
|
+
name: string;
|
|
98
|
+
}[];
|
|
99
|
+
cssProperties?: readonly {
|
|
100
|
+
name: string;
|
|
101
|
+
}[];
|
|
102
|
+
tagName?: string;
|
|
103
|
+
}[];
|
|
104
|
+
}[];
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The tag names, attribute names and CSS custom properties a custom elements
|
|
108
|
+
* manifest lists, for a source pane's `completions`. Each name is listed once.
|
|
109
|
+
*/
|
|
110
|
+
export declare const manifestWords: (manifest: ManifestLike) => string[];
|
|
111
|
+
export {};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const PAIRS = {
|
|
2
|
+
"'": "'",
|
|
3
|
+
'"': '"',
|
|
4
|
+
'(': ')',
|
|
5
|
+
'[': ']',
|
|
6
|
+
'{': '}',
|
|
7
|
+
'`': '`',
|
|
8
|
+
};
|
|
9
|
+
const OPENERS = '([{';
|
|
10
|
+
const CLOSERS = ')]}';
|
|
11
|
+
const BEFORE = /^(?:$|[\s)\]}>;:,"'`])/;
|
|
12
|
+
const COMMENTS = {
|
|
13
|
+
css: ['/*', '*/'],
|
|
14
|
+
html: ['<!--', '-->'],
|
|
15
|
+
js: ['//'],
|
|
16
|
+
};
|
|
17
|
+
export const syntaxOf = (name) => {
|
|
18
|
+
const extension = name.slice(name.lastIndexOf('.') + 1);
|
|
19
|
+
if (extension === 'css' || extension === 'html') {
|
|
20
|
+
return extension;
|
|
21
|
+
}
|
|
22
|
+
return /^[cm]?[jt]sx?$/.test(extension) ? 'js' : undefined;
|
|
23
|
+
};
|
|
24
|
+
const linesOf = (source, from, to) => {
|
|
25
|
+
const start = source.lastIndexOf('\n', from - 1) + 1;
|
|
26
|
+
const last = to > from && source[to - 1] === '\n' ? to - 1 : to;
|
|
27
|
+
const end = source.indexOf('\n', last);
|
|
28
|
+
return [start, end < 0 ? source.length : end];
|
|
29
|
+
};
|
|
30
|
+
const clamp = (at, low, high) => Math.min(high, Math.max(low, at));
|
|
31
|
+
const eachLine = (source, from, to, change) => {
|
|
32
|
+
const [start, end] = linesOf(source, from, to);
|
|
33
|
+
const before = source.slice(start, end).split('\n');
|
|
34
|
+
const after = before.map(change);
|
|
35
|
+
const text = after.join('\n');
|
|
36
|
+
if (text === source.slice(start, end)) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const moved = from + (after[0] ?? '').length - (before[0] ?? '').length;
|
|
40
|
+
const head = from === start && from !== to ? from : clamp(moved, start, start + text.length);
|
|
41
|
+
const tail = from === to ? head : Math.max(head, to + text.length - (end - start));
|
|
42
|
+
return { from: start, selection: [head, tail], text, to: end };
|
|
43
|
+
};
|
|
44
|
+
export const indentLines = (source, from, to, unit = ' ') => eachLine(source, from, to, (line) => (line.trim() === '' ? line : `${unit}${line}`));
|
|
45
|
+
export const outdentLines = (source, from, to) => eachLine(source, from, to, (line) => line.replace(/^(?: {1,2}|\t)/, ''));
|
|
46
|
+
export const breakLine = (source, from, to, unit = ' ') => {
|
|
47
|
+
const start = source.lastIndexOf('\n', from - 1) + 1;
|
|
48
|
+
const indent = /^[ \t]*/.exec(source.slice(start, from))?.[0] ?? '';
|
|
49
|
+
const opens = OPENERS.indexOf(source[from - 1] ?? '\n');
|
|
50
|
+
if (opens < 0 && indent === '') {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const line = `\n${indent}${opens < 0 ? '' : unit}`;
|
|
54
|
+
const text = opens >= 0 && source[to] === CLOSERS[opens] ? `${line}\n${indent}` : line;
|
|
55
|
+
return { from, selection: [from + line.length, from + line.length], text, to };
|
|
56
|
+
};
|
|
57
|
+
const literal = (text) => text.replace(/[*/]/g, '\\$&');
|
|
58
|
+
export const toggleComment = (source, from, to, syntax) => {
|
|
59
|
+
const [open, close] = COMMENTS[syntax];
|
|
60
|
+
const [start, end] = linesOf(source, from, to);
|
|
61
|
+
const block = source.slice(start, end);
|
|
62
|
+
if (close === undefined) {
|
|
63
|
+
const filled = block.split('\n').filter((line) => line.trim() !== '');
|
|
64
|
+
const on = filled.length > 0 && filled.every((line) => line.trimStart().startsWith(open));
|
|
65
|
+
const indent = Math.min(...filled.map((line) => line.length - line.trimStart().length));
|
|
66
|
+
const marker = new RegExp(`^(\\s*)${literal(open)} ?`);
|
|
67
|
+
return eachLine(source, from, to, (line) => {
|
|
68
|
+
if (line.trim() === '') {
|
|
69
|
+
return line;
|
|
70
|
+
}
|
|
71
|
+
return on
|
|
72
|
+
? line.replace(marker, '$1')
|
|
73
|
+
: `${line.slice(0, indent)}${open} ${line.slice(indent)}`;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const lead = block.length - block.trimStart().length;
|
|
77
|
+
const body = block.trim();
|
|
78
|
+
const trail = block.slice(lead + body.length);
|
|
79
|
+
const commented = body.length >= open.length + close.length && body.startsWith(open) && body.endsWith(close);
|
|
80
|
+
const inner = commented
|
|
81
|
+
? body
|
|
82
|
+
.slice(open.length, body.length - close.length)
|
|
83
|
+
.replace(/^ /, '')
|
|
84
|
+
.replace(/ $/, '')
|
|
85
|
+
: `${open} ${body} ${close}`;
|
|
86
|
+
const text = `${block.slice(0, lead)}${inner}${trail}`;
|
|
87
|
+
const shift = commented ? -(open.length + (body[open.length] === ' ' ? 1 : 0)) : open.length + 1;
|
|
88
|
+
const head = clamp(from >= start + lead ? from + shift : from, start, start + text.length);
|
|
89
|
+
const tail = from === to ? head : Math.max(head, to + text.length - block.length);
|
|
90
|
+
return { from: start, selection: [head, tail], text, to: end };
|
|
91
|
+
};
|
|
92
|
+
export const closeBracket = (source, from, to, key) => {
|
|
93
|
+
const closer = PAIRS[key];
|
|
94
|
+
const next = source.slice(to);
|
|
95
|
+
if (from === to && (CLOSERS.includes(key) || closer === key) && next.startsWith(key)) {
|
|
96
|
+
return { from, selection: [from + 1, from + 1], text: '', to };
|
|
97
|
+
}
|
|
98
|
+
if (closer === undefined) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
if (from !== to) {
|
|
102
|
+
return {
|
|
103
|
+
from,
|
|
104
|
+
selection: [from + 1, to + 1],
|
|
105
|
+
text: `${key}${source.slice(from, to)}${closer}`,
|
|
106
|
+
to,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (!BEFORE.test(next) || (closer === key && /[\w'"`]$/.test(source.slice(0, from)))) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
return { from, selection: [from + 1, from + 1], text: `${key}${closer}`, to };
|
|
113
|
+
};
|
|
114
|
+
export const deletePair = (source, from, to) => {
|
|
115
|
+
const opener = source[from - 1];
|
|
116
|
+
if (from !== to || opener === undefined || PAIRS[opener] !== source[from]) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
return { from: from - 1, selection: [from - 1, from - 1], text: '', to: from + 1 };
|
|
120
|
+
};
|
|
121
|
+
const REACH = 10_000;
|
|
122
|
+
export const matchBracket = (source, at, skip = () => false) => {
|
|
123
|
+
for (const index of [at - 1, at]) {
|
|
124
|
+
const bracket = source[index];
|
|
125
|
+
if (bracket === undefined || skip(index)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const opens = OPENERS.indexOf(bracket);
|
|
129
|
+
const closes = CLOSERS.indexOf(bracket);
|
|
130
|
+
if (opens < 0 && closes < 0) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const step = opens < 0 ? -1 : 1;
|
|
134
|
+
const other = opens < 0 ? OPENERS[closes] : CLOSERS[opens];
|
|
135
|
+
let depth = 0;
|
|
136
|
+
for (let look = index; look >= 0 && look < source.length && Math.abs(look - index) < REACH; look += step) {
|
|
137
|
+
if (skip(look)) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (source[look] === bracket) {
|
|
141
|
+
depth += 1;
|
|
142
|
+
}
|
|
143
|
+
else if (source[look] === other) {
|
|
144
|
+
depth -= 1;
|
|
145
|
+
if (depth === 0) {
|
|
146
|
+
return { at: index, match: look };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { at: index, match: undefined };
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
};
|
|
154
|
+
const OFFERED = 50;
|
|
155
|
+
const WORDS = /[\w$-]*[A-Za-z_$][\w$-]*/g;
|
|
156
|
+
export const suggest = (source, at, extra = []) => {
|
|
157
|
+
const word = /[\w$-]*$/.exec(source.slice(Math.max(0, at - 100), at))?.[0] ?? '';
|
|
158
|
+
if (!/[A-Za-z_$]/.test(word)) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
const start = word.toLowerCase();
|
|
162
|
+
const pool = new Set([...extra, ...(source.match(WORDS) ?? [])]);
|
|
163
|
+
const words = [...pool]
|
|
164
|
+
.filter((each) => each.length > word.length && each.toLowerCase().startsWith(start))
|
|
165
|
+
.sort((a, b) => a.localeCompare(b))
|
|
166
|
+
.slice(0, OFFERED);
|
|
167
|
+
return words.length > 0 ? { from: at - word.length, words } : undefined;
|
|
168
|
+
};
|
|
169
|
+
export const manifestWords = (manifest) => {
|
|
170
|
+
const names = new Set();
|
|
171
|
+
for (const module of manifest.modules ?? []) {
|
|
172
|
+
for (const declaration of module.declarations ?? []) {
|
|
173
|
+
if (declaration.tagName) {
|
|
174
|
+
names.add(declaration.tagName);
|
|
175
|
+
}
|
|
176
|
+
for (const { name } of [
|
|
177
|
+
...(declaration.attributes ?? []),
|
|
178
|
+
...(declaration.cssProperties ?? []),
|
|
179
|
+
]) {
|
|
180
|
+
names.add(name);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return [...names];
|
|
185
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createSheet } from '@fusion.dev/helix-core';
|
|
2
2
|
export const styles = createSheet(`:host {display: block;block-size: 100%;}[part="highlight"],
|
|
3
3
|
[part="source"] {border: none;color: var(--helix-color-foreground-muted);font-family: var(--helix-font-mono);font-size: max(var(--helix-text-field-min), var(--helix-text-xs));inline-size: 100%;line-height: var(--helix-leading-normal);min-block-size: var(--sandbox-source-min-block-size, 12rem);padding: var(--helix-space-sm);tab-size: 2;}[part="source"] {background-color: var(--sandbox-source-background-color, var(--helix-color-background));block-size: 100%;display: block;resize: none;}[part="source"]:focus-visible {background-color: var(--helix-color-surface);outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: calc(var(--helix-size-focus-offset) * -1);}[part="field"] {background-color: var(--sandbox-source-background-color, var(--helix-color-background));block-size: 100%;display: grid;grid-template-rows: minmax(max-content, auto);overflow: auto;}[part="field"]:focus-within {background-color: var(--helix-color-surface);}[part="field"]:has(> [part="source"]:focus-visible) {outline: var(--helix-size-focus-ring) solid var(--helix-color-focus-ring);outline-offset: calc(var(--helix-size-focus-offset) * -1);}[part="field"] > [part] {grid-area: 1 / 1;letter-spacing: normal;margin: 0;overflow-wrap: break-word;white-space: pre-wrap;}[part="field"] > [part="source"],
|
|
4
|
-
[part="field"] > [part="source"]:focus-visible {-webkit-text-fill-color: transparent;align-self: stretch;background-color: transparent;block-size: auto;caret-color: var(--helix-color-foreground);color: transparent;outline: none;overflow: hidden;}:host([line-numbers]) [part="field"] > [part] {padding-inline-start: calc(var(--helix-space-sm) * 3 + var(--_sandbox-source-gutter, 1ch));}:host([line-numbers]) [part="highlight"] {counter-reset: line;}[part="line"] {counter-increment: line;display: block;min-block-size: 1lh;position: relative;}[part="line"]::before {color: var(--sandbox-source-line-number-color, var(--helix-color-foreground-subtle));content: counter(line);inset-inline-end: calc(100% + var(--helix-space-sm) * 2);position: absolute;text-align: end;}[data-kind="callable"] {color: var(--sandbox-source-callable-color, var(--helix-color-syntax-callable));}[data-kind="comment"] {color: var(--sandbox-source-comment-color, var(--helix-color-syntax-comment));}[data-kind="constant"] {color: var(--sandbox-source-constant-color, var(--helix-color-syntax-constant));}[data-kind="invalid"] {color: var(--sandbox-source-invalid-color, var(--helix-color-syntax-invalid));}[data-kind="keyword"] {color: var(--sandbox-source-keyword-color, var(--helix-color-syntax-keyword));}[data-kind="name"] {color: var(--sandbox-source-name-color, var(--helix-color-syntax-name));}[data-kind="string"] {color: var(--sandbox-source-string-color, var(--helix-color-syntax-string));}[data-kind="tag"] {color: var(--sandbox-source-tag-color, var(--helix-color-syntax-tag));}
|
|
4
|
+
[part="field"] > [part="source"]:focus-visible {-webkit-text-fill-color: transparent;align-self: stretch;background-color: transparent;block-size: auto;caret-color: var(--helix-color-foreground);color: transparent;outline: none;overflow: hidden;}:host([line-numbers]) [part="field"] > [part] {padding-inline-start: calc(var(--helix-space-sm) * 3 + var(--_sandbox-source-gutter, 1ch));}:host([line-numbers]) [part="highlight"] {counter-reset: line;}[part="line"] {counter-increment: line;display: block;min-block-size: 1lh;position: relative;}[part="line"]::before {color: var(--sandbox-source-line-number-color, var(--helix-color-foreground-subtle));content: counter(line);inset-inline-end: calc(100% + var(--helix-space-sm) * 2);position: absolute;text-align: end;}[data-kind="callable"] {color: var(--sandbox-source-callable-color, var(--helix-color-syntax-callable));}[data-kind="comment"] {color: var(--sandbox-source-comment-color, var(--helix-color-syntax-comment));}[data-kind="constant"] {color: var(--sandbox-source-constant-color, var(--helix-color-syntax-constant));}[data-kind="invalid"] {color: var(--sandbox-source-invalid-color, var(--helix-color-syntax-invalid));}[data-kind="keyword"] {color: var(--sandbox-source-keyword-color, var(--helix-color-syntax-keyword));}[data-kind="name"] {color: var(--sandbox-source-name-color, var(--helix-color-syntax-name));}[data-kind="string"] {color: var(--sandbox-source-string-color, var(--helix-color-syntax-string));}[data-kind="tag"] {color: var(--sandbox-source-tag-color, var(--helix-color-syntax-tag));}[part="field"]:focus-within [part~="bracket"] {background-color: var(--sandbox-source-bracket-color, var(--helix-color-accent-subtle));border-radius: var(--helix-radii-sm);}[part="field"]:focus-within [part~="bracket-unmatched"] {background-color: transparent;color: var(--sandbox-source-invalid-color, var(--helix-color-syntax-invalid));}[part="field"] {position: relative;}[part="completions"] {background-color: var(--helix-color-surface-overlay);border: var(--sandbox-source-completions-border-width, var(--helix-border-floating)) solid
|
|
5
|
+
var(--helix-color-border);border-radius: var(--helix-radii-sm);box-shadow: var(--sandbox-source-completions-shadow, var(--helix-shadow-floating));font-family: var(--helix-font-mono);font-size: max(var(--helix-text-field-min), var(--helix-text-xs));max-block-size: 12rem;min-inline-size: 8rem;overflow-y: auto;padding: var(--helix-space-2xs);position: absolute;z-index: 1;}[part~="completion"] {border-radius: var(--helix-radii-sm);color: var(--helix-color-foreground);cursor: pointer;padding: var(--helix-space-2xs) var(--helix-space-xs);white-space: nowrap;}[part~="completion-active"] {background-color: var(--helix-color-accent-surface);}`);
|