@ruckc/truss-code 0.0.0-stage → 1.12.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.
@@ -0,0 +1,75 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { loadMonaco } from './monaco.js';
4
+ import { highlight, segmentLine, splitLines, paletteVars, type Seg } from './highlight.js';
5
+ import { resolveLanguage } from './lang.js';
6
+ import { readTheme, watchTheme, type Theme } from './theme.js';
7
+
8
+ let {
9
+ code,
10
+ language,
11
+ path,
12
+ lineNumbers = false,
13
+ wrap = false,
14
+ label = 'Code',
15
+ maxHeight
16
+ }: {
17
+ /** The text to show. */
18
+ code: string;
19
+ /** A Monaco language id. Defaults to the language of `path`, else plain text. */
20
+ language?: string;
21
+ path?: string;
22
+ lineNumbers?: boolean;
23
+ /** Wrap long lines instead of scrolling sideways. */
24
+ wrap?: boolean;
25
+ /** Accessible name of the scrollable region. */
26
+ label?: string;
27
+ /** A CSS length; the block scrolls beyond it. */
28
+ maxHeight?: string | number;
29
+ } = $props();
30
+
31
+ let el: HTMLPreElement;
32
+ let theme = $state.raw<Theme>();
33
+ let tokens = $state.raw<{ code: string; lang: string; segs: Seg[][] }>();
34
+ const lang = $derived(resolveLanguage(language, path));
35
+ // until the tokenizer has run (and on the server) the text is shown as it is
36
+ const lines = $derived(tokens && tokens.code === code && tokens.lang === lang ? tokens.segs : splitLines(code).map((l) => segmentLine(l, [])));
37
+ const style = $derived((theme ? paletteVars(theme.palette) + `;font-size:${theme.metrics.fontSize}px;line-height:${theme.metrics.lineHeight}px` : '') + (maxHeight != null ? `;max-height:${typeof maxHeight === 'number' ? maxHeight + 'px' : maxHeight}` : ''));
38
+
39
+ onMount(() => {
40
+ theme = readTheme(el);
41
+ return watchTheme(el, () => (theme = readTheme(el)));
42
+ });
43
+
44
+ $effect(() => {
45
+ const text = code, language = lang;
46
+ let stale = false;
47
+ loadMonaco().then(
48
+ (m) => highlight(m, text, language).then((segs) => { if (!stale) tokens = { code: text, lang: language, segs }; }),
49
+ () => {} // without Monaco the block stays plain text
50
+ );
51
+ return () => { stale = true; };
52
+ });
53
+ </script>
54
+
55
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
56
+ <pre bind:this={el} role="region" class="cb" class:wrap class:nums={lineNumbers} {style} tabindex="0" aria-label={label}><code>{#each lines as segs, i (i)}<span class="ln">{#if lineNumbers}<span class="no" aria-hidden="true">{i + 1}</span>{/if}<span class="tx">{#each segs as s}<span class={s.role ? `s-${s.role}` : undefined}>{s.text}</span>{/each}{'\n'}</span></span>{/each}</code></pre>
57
+
58
+ <style>
59
+ .cb {
60
+ margin: 0; padding: var(--tr-space-3) var(--tr-space-4); overflow: auto; border: 1px solid var(--tr-outline-variant); border-radius: var(--tr-radius-md);
61
+ background: var(--code-bg, var(--tr-surface)); color: var(--code-fg, var(--tr-on-surface)); font-family: var(--tr-font-mono); font-size: var(--tr-text-md); line-height: 1.5; tab-size: 4;
62
+ }
63
+ .cb:focus-visible { outline: none; border-color: var(--tr-primary); box-shadow: var(--tr-focus-ring); }
64
+ code { font: inherit; }
65
+ .ln { display: block; }
66
+ .wrap .ln { white-space: pre-wrap; overflow-wrap: anywhere; }
67
+ .nums .ln { display: grid; grid-template-columns: 3ch 1fr; column-gap: var(--tr-space-3); }
68
+ .no { text-align: right; color: var(--code-comment, var(--tr-on-surface-variant)); user-select: none; }
69
+ .tx { white-space: inherit; }
70
+ .s-keyword { color: var(--code-keyword); } .s-string { color: var(--code-string); } .s-number { color: var(--code-number); }
71
+ .s-type { color: var(--code-type); } .s-function { color: var(--code-function); } .s-variable { color: var(--code-variable); }
72
+ .s-constant { color: var(--code-constant); } .s-operator { color: var(--code-operator); } .s-tag { color: var(--code-tag); }
73
+ .s-attribute { color: var(--code-attribute); } .s-regexp { color: var(--code-regexp); } .s-comment { color: var(--code-comment); font-style: italic; }
74
+ .s-invalid { color: var(--code-invalid); } .s-delimiter { color: var(--code-delimiter); } .s-meta { color: var(--code-meta); }
75
+ </style>
@@ -0,0 +1,17 @@
1
+ type $$ComponentProps = {
2
+ /** The text to show. */
3
+ code: string;
4
+ /** A Monaco language id. Defaults to the language of `path`, else plain text. */
5
+ language?: string;
6
+ path?: string;
7
+ lineNumbers?: boolean;
8
+ /** Wrap long lines instead of scrolling sideways. */
9
+ wrap?: boolean;
10
+ /** Accessible name of the scrollable region. */
11
+ label?: string;
12
+ /** A CSS length; the block scrolls beyond it. */
13
+ maxHeight?: string | number;
14
+ };
15
+ declare const CodeBlock: import("svelte").Component<$$ComponentProps, {}, "">;
16
+ type CodeBlock = ReturnType<typeof CodeBlock>;
17
+ export default CodeBlock;
@@ -0,0 +1,227 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { Skeleton } from '@ruckc/truss-ui';
4
+ import type * as Monaco from 'monaco-editor';
5
+ import { loadMonaco, applyTheme, type MonacoApi } from './monaco.js';
6
+ import { resolveLanguage } from './lang.js';
7
+ import { watchTheme, fontMetrics, type Metrics } from './theme.js';
8
+ import { toMonacoMarker, toMonacoCompletion } from './editor-util.js';
9
+ import type { CodeMarker, CompletionContext, CompletionItem } from './types.js';
10
+
11
+ let {
12
+ value = $bindable(''),
13
+ language,
14
+ path,
15
+ readonly = false,
16
+ minimap = false,
17
+ lineNumbers = true,
18
+ wordWrap = false,
19
+ height = 320,
20
+ screenReader = 'auto',
21
+ label = 'Code editor',
22
+ markers = [],
23
+ completions,
24
+ options,
25
+ onchange,
26
+ onsave,
27
+ onrun,
28
+ onready
29
+ }: {
30
+ /** The document text. One model per `path` keeps each tab's undo history; the text you bind belongs to the current `path`. */
31
+ value?: string;
32
+ /** A Monaco language id (`sql`, `typescript`, `yaml`, ...). Defaults to the language of `path`'s extension, else plain text. */
33
+ language?: string;
34
+ /** A file name or path. Each distinct path gets its own model (text, undo history, view state), so switching `path` is switching tabs. */
35
+ path?: string;
36
+ readonly?: boolean;
37
+ minimap?: boolean;
38
+ lineNumbers?: boolean;
39
+ wordWrap?: boolean;
40
+ /** A pixel height, any CSS length, or `'fill'` to take the height of the parent (which needs a definite height). */
41
+ height?: number | string | 'fill';
42
+ /** `auto` follows the operating system's screen reader detection; `on` forces Monaco's screen-reader mode, `off` disables it. */
43
+ screenReader?: 'auto' | 'on' | 'off';
44
+ /** The accessible name of the editor. */
45
+ label?: string;
46
+ /** Diagnostics for the current model. */
47
+ markers?: CodeMarker[];
48
+ /** Called as the user types or invokes suggestions; return the items to offer (may be async). */
49
+ completions?: (ctx: CompletionContext) => CompletionItem[] | undefined | Promise<CompletionItem[] | undefined>;
50
+ /** Any Monaco `IStandaloneEditorConstructionOptions`, applied after the props above (for example `{ lightbulb: { enabled: 'on' } }` to bring back the quick-fix bulb, which is off by default because Monaco gives it no accessible name). */
51
+ options?: Monaco.editor.IStandaloneEditorConstructionOptions;
52
+ /** Fires with the new text after every edit made by the user. */
53
+ onchange?: (value: string) => void;
54
+ /** Ctrl/Cmd+S. While unset, the browser's own shortcut is left alone. */
55
+ onsave?: (value: string) => void;
56
+ /** Ctrl/Cmd+Enter. While unset, Monaco's "insert line below" keeps the shortcut. */
57
+ onrun?: (value: string) => void;
58
+ /** The Monaco editor is created: gives you the instance and the Monaco namespace for anything not covered by props. */
59
+ onready?: (editor: Monaco.editor.IStandaloneCodeEditor, monaco: MonacoApi) => void;
60
+ } = $props();
61
+
62
+ const id = $props.id();
63
+ let host: HTMLDivElement;
64
+ let monaco = $state.raw<MonacoApi>();
65
+ let editor = $state.raw<Monaco.editor.IStandaloneCodeEditor>();
66
+ let model = $state.raw<Monaco.editor.ITextModel>();
67
+ let metrics = $state.raw<Metrics>(fontMetrics());
68
+ let failed = $state<string>();
69
+ const ready = $derived(!!editor);
70
+
71
+ const models = new Map<string, Monaco.editor.ITextModel>();
72
+ const views = new Map<string, Monaco.editor.ICodeEditorViewState | null>();
73
+ let current: string | undefined;
74
+ let external = false;
75
+ let hasSave: Monaco.editor.IContextKey<boolean> | undefined;
76
+ let hasRun: Monaco.editor.IContextKey<boolean> | undefined;
77
+ let contentSub: Monaco.IDisposable | undefined;
78
+
79
+ const lang = $derived(resolveLanguage(language, path));
80
+ const css = $derived(height === 'fill' ? '100%' : typeof height === 'number' ? `${height}px` : height);
81
+
82
+ /** The focusable Monaco editor instance, once loaded. */
83
+ export const getEditor = () => editor;
84
+ export const focus = () => editor?.focus();
85
+
86
+ function modelFor(m: MonacoApi, key: string): Monaco.editor.ITextModel {
87
+ let mdl = models.get(key);
88
+ if (!mdl) {
89
+ const uri = m.Uri.from({ scheme: 'inmemory', authority: `truss-${id}`, path: '/' + key.replace(/^\/+/, '') });
90
+ mdl = m.editor.createModel(value, lang, uri);
91
+ models.set(key, mdl);
92
+ }
93
+ return mdl;
94
+ }
95
+
96
+ onMount(() => {
97
+ let dead = false;
98
+ let stop = () => {};
99
+ loadMonaco().then(
100
+ (m) => {
101
+ if (dead) return;
102
+ monaco = m;
103
+ metrics = applyTheme(m, host).metrics;
104
+ const ed = m.editor.create(host, { automaticLayout: true, fixedOverflowWidgets: true, scrollBeyondLastLine: false, model: null });
105
+ // shortcuts only claim the keys while the matching callback is set, so Monaco's own bindings stay otherwise
106
+ hasSave = ed.createContextKey('trussHasSave', false);
107
+ hasRun = ed.createContextKey('trussHasRun', false);
108
+ ed.addCommand(m.KeyMod.CtrlCmd | m.KeyCode.KeyS, () => onsave?.(ed.getValue()), 'trussHasSave');
109
+ ed.addCommand(m.KeyMod.CtrlCmd | m.KeyCode.Enter, () => onrun?.(ed.getValue()), 'trussHasRun');
110
+ stop = watchTheme(host, () => (metrics = applyTheme(m, host).metrics));
111
+ editor = ed;
112
+ onready?.(ed, m);
113
+ },
114
+ (e: unknown) => { failed = String(e); }
115
+ );
116
+ return () => {
117
+ dead = true;
118
+ stop();
119
+ contentSub?.dispose();
120
+ editor?.dispose();
121
+ for (const mdl of models.values()) mdl.dispose();
122
+ models.clear();
123
+ };
124
+ });
125
+
126
+ // the model follows `path`; its text follows `value`
127
+ $effect(() => {
128
+ const m = monaco, ed = editor;
129
+ if (!m || !ed) return;
130
+ const key = path ?? '';
131
+ const mdl = modelFor(m, key);
132
+ if (current !== key) {
133
+ if (current !== undefined) views.set(current, ed.saveViewState());
134
+ ed.setModel(mdl);
135
+ if (views.get(key)) ed.restoreViewState(views.get(key)!);
136
+ current = key;
137
+ model = mdl;
138
+ contentSub?.dispose();
139
+ contentSub = mdl.onDidChangeContent(() => {
140
+ if (external) return;
141
+ value = mdl.getValue();
142
+ onchange?.(value);
143
+ });
144
+ }
145
+ if (mdl.getLanguageId() !== lang) m.editor.setModelLanguage(mdl, lang);
146
+ if (mdl.getValue() !== value) {
147
+ external = true;
148
+ ed.executeEdits('truss-bind', [{ range: mdl.getFullModelRange(), text: value }]);
149
+ ed.pushUndoStop();
150
+ external = false;
151
+ }
152
+ });
153
+
154
+ // options: props first, then the passthrough, then the theme's type metrics
155
+ $effect(() => {
156
+ if (!editor) return;
157
+ editor.updateOptions({
158
+ readOnly: readonly,
159
+ minimap: { enabled: minimap },
160
+ lineNumbers: lineNumbers ? 'on' : 'off',
161
+ wordWrap: wordWrap ? 'on' : 'off',
162
+ accessibilitySupport: screenReader,
163
+ ariaLabel: label,
164
+ // Monaco's quick-fix bulb is a listbox without an accessible name; keep it off unless you opt in through `options`
165
+ lightbulb: { enabled: 'off' as Monaco.editor.ShowLightbulbIconMode },
166
+ fontSize: metrics.fontSize,
167
+ lineHeight: metrics.lineHeight,
168
+ fontFamily: metrics.fontFamily,
169
+ ...options
170
+ });
171
+ });
172
+
173
+ $effect(() => {
174
+ if (!editor) return;
175
+ hasSave?.set(!!onsave);
176
+ hasRun?.set(!!onrun);
177
+ });
178
+
179
+ $effect(() => {
180
+ const m = monaco, mdl = model;
181
+ if (!m || !mdl) return;
182
+ m.editor.setModelMarkers(mdl, 'truss', markers.map((k) => toMonacoMarker(k, m.MarkerSeverity, mdl.getLineContent(Math.min(Math.max(k.line, 1), mdl.getLineCount())))));
183
+ return () => m.editor.setModelMarkers(mdl, 'truss', []);
184
+ });
185
+
186
+ $effect(() => {
187
+ const m = monaco, language = lang, on = !!completions;
188
+ if (!m || !on) return;
189
+ const d = m.languages.registerCompletionItemProvider(language, {
190
+ provideCompletionItems: async (mdl, pos) => {
191
+ if (mdl !== model) return { suggestions: [] };
192
+ const w = mdl.getWordUntilPosition(pos);
193
+ const items = (await completions?.({ value: mdl.getValue(), language, path, line: pos.lineNumber, col: pos.column, word: w.word, prefix: mdl.getLineContent(pos.lineNumber).slice(0, pos.column - 1) })) ?? [];
194
+ const range = { startLineNumber: pos.lineNumber, endLineNumber: pos.lineNumber, startColumn: w.startColumn, endColumn: w.endColumn };
195
+ return { suggestions: items.map((i) => toMonacoCompletion(i, m.languages.CompletionItemKind, range, m.languages.CompletionItemInsertTextRule.InsertAsSnippet)) };
196
+ }
197
+ });
198
+ return () => d.dispose();
199
+ });
200
+ </script>
201
+
202
+ <div class="ce" style:height={css} data-ready={ready} role="group" aria-label={label} aria-describedby="ce-hint-{id}">
203
+ <div class="mount" bind:this={host}></div>
204
+ {#if !ready}
205
+ <div class="ph">
206
+ {#if failed}<p role="alert">The editor could not be loaded: {failed}</p>{:else}<Skeleton variant="rect" height="100%" label="Loading editor" />{/if}
207
+ </div>
208
+ {/if}
209
+ <span class="vh" id="ce-hint-{id}">Tab moves focus out of the editor when you press Control plus M; press Control plus M again to make Tab insert indentation.</span>
210
+ </div>
211
+
212
+ <style>
213
+ .ce {
214
+ position: relative; min-height: 96px; border: 1px solid var(--tr-outline-variant); border-radius: var(--tr-radius-md);
215
+ overflow: hidden; background: var(--tr-surface); font-family: var(--tr-font-sans);
216
+ }
217
+ .ce:focus-within { border-color: var(--tr-primary); box-shadow: var(--tr-focus-ring); }
218
+ .mount { position: absolute; inset: 0; }
219
+ .ph { position: absolute; inset: 0; display: grid; }
220
+ .ph :global(.sk-root) { display: block; width: 100%; height: 100%; }
221
+ .ph p { margin: auto; padding: var(--tr-space-4); color: var(--tr-danger); font-size: var(--tr-text-md); }
222
+ .vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
223
+ /* hovers, suggestions and menus get the tooltip look */
224
+ .ce :global(.monaco-hover), .ce :global(.suggest-widget) {
225
+ border-radius: var(--tr-radius-sm); box-shadow: var(--tr-elevation-2); font-family: var(--tr-font-sans); font-size: var(--tr-text-sm);
226
+ }
227
+ </style>
@@ -0,0 +1,41 @@
1
+ import type * as Monaco from 'monaco-editor';
2
+ import { type MonacoApi } from './monaco.js';
3
+ import type { CodeMarker, CompletionContext, CompletionItem } from './types.js';
4
+ type $$ComponentProps = {
5
+ /** The document text. One model per `path` keeps each tab's undo history; the text you bind belongs to the current `path`. */
6
+ value?: string;
7
+ /** A Monaco language id (`sql`, `typescript`, `yaml`, ...). Defaults to the language of `path`'s extension, else plain text. */
8
+ language?: string;
9
+ /** A file name or path. Each distinct path gets its own model (text, undo history, view state), so switching `path` is switching tabs. */
10
+ path?: string;
11
+ readonly?: boolean;
12
+ minimap?: boolean;
13
+ lineNumbers?: boolean;
14
+ wordWrap?: boolean;
15
+ /** A pixel height, any CSS length, or `'fill'` to take the height of the parent (which needs a definite height). */
16
+ height?: number | string | 'fill';
17
+ /** `auto` follows the operating system's screen reader detection; `on` forces Monaco's screen-reader mode, `off` disables it. */
18
+ screenReader?: 'auto' | 'on' | 'off';
19
+ /** The accessible name of the editor. */
20
+ label?: string;
21
+ /** Diagnostics for the current model. */
22
+ markers?: CodeMarker[];
23
+ /** Called as the user types or invokes suggestions; return the items to offer (may be async). */
24
+ completions?: (ctx: CompletionContext) => CompletionItem[] | undefined | Promise<CompletionItem[] | undefined>;
25
+ /** Any Monaco `IStandaloneEditorConstructionOptions`, applied after the props above (for example `{ lightbulb: { enabled: 'on' } }` to bring back the quick-fix bulb, which is off by default because Monaco gives it no accessible name). */
26
+ options?: Monaco.editor.IStandaloneEditorConstructionOptions;
27
+ /** Fires with the new text after every edit made by the user. */
28
+ onchange?: (value: string) => void;
29
+ /** Ctrl/Cmd+S. While unset, the browser's own shortcut is left alone. */
30
+ onsave?: (value: string) => void;
31
+ /** Ctrl/Cmd+Enter. While unset, Monaco's "insert line below" keeps the shortcut. */
32
+ onrun?: (value: string) => void;
33
+ /** The Monaco editor is created: gives you the instance and the Monaco namespace for anything not covered by props. */
34
+ onready?: (editor: Monaco.editor.IStandaloneCodeEditor, monaco: MonacoApi) => void;
35
+ };
36
+ declare const CodeEditor: import("svelte").Component<$$ComponentProps, {
37
+ /** The focusable Monaco editor instance, once loaded. */ getEditor: () => Monaco.editor.IStandaloneCodeEditor | undefined;
38
+ focus: () => void | undefined;
39
+ }, "value">;
40
+ type CodeEditor = ReturnType<typeof CodeEditor>;
41
+ export default CodeEditor;
@@ -0,0 +1,142 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { Skeleton } from '@ruckc/truss-ui';
4
+ import type * as Monaco from 'monaco-editor';
5
+ import { loadMonaco, applyTheme, type MonacoApi } from './monaco.js';
6
+ import { resolveLanguage } from './lang.js';
7
+ import { watchTheme, fontMetrics, type Metrics } from './theme.js';
8
+
9
+ let {
10
+ original = '',
11
+ modified = $bindable(''),
12
+ language,
13
+ path,
14
+ sideBySide = true,
15
+ readonly = false,
16
+ originalEditable = false,
17
+ ignoreTrimWhitespace = true,
18
+ height = 320,
19
+ label = 'Diff editor',
20
+ options,
21
+ onchange,
22
+ onready
23
+ }: {
24
+ /** The left side. */
25
+ original?: string;
26
+ /** The right side, the one you edit. */
27
+ modified?: string;
28
+ /** A Monaco language id; defaults to the language of `path`, else plain text. */
29
+ language?: string;
30
+ path?: string;
31
+ /** Two panes (true) or one inline pane (false). Monaco also switches to inline by itself in a narrow container. */
32
+ sideBySide?: boolean;
33
+ /** Make the modified side read-only. */
34
+ readonly?: boolean;
35
+ /** Allow edits on the original side too. */
36
+ originalEditable?: boolean;
37
+ ignoreTrimWhitespace?: boolean;
38
+ /** A pixel height, any CSS length, or `'fill'` for the parent's height. */
39
+ height?: number | string | 'fill';
40
+ label?: string;
41
+ /** Any Monaco `IDiffEditorConstructionOptions`, applied after the props above. */
42
+ options?: Monaco.editor.IDiffEditorConstructionOptions;
43
+ /** Fires with the modified text after every edit made by the user. */
44
+ onchange?: (value: string) => void;
45
+ onready?: (editor: Monaco.editor.IStandaloneDiffEditor, monaco: MonacoApi) => void;
46
+ } = $props();
47
+
48
+ const id = $props.id();
49
+ let host: HTMLDivElement;
50
+ let monaco = $state.raw<MonacoApi>();
51
+ let editor = $state.raw<Monaco.editor.IStandaloneDiffEditor>();
52
+ let metrics = $state.raw<Metrics>(fontMetrics());
53
+ let failed = $state<string>();
54
+ let models: { original: Monaco.editor.ITextModel; modified: Monaco.editor.ITextModel } | undefined;
55
+ let external = false;
56
+ const ready = $derived(!!editor);
57
+ const lang = $derived(resolveLanguage(language, path));
58
+ const css = $derived(height === 'fill' ? '100%' : typeof height === 'number' ? `${height}px` : height);
59
+
60
+ /** The Monaco diff editor, once loaded. */
61
+ export const getEditor = () => editor;
62
+ export const focus = () => editor?.getModifiedEditor().focus();
63
+
64
+ onMount(() => {
65
+ let dead = false;
66
+ let stop = () => {};
67
+ const subs: Monaco.IDisposable[] = [];
68
+ loadMonaco().then(
69
+ (m) => {
70
+ if (dead) return;
71
+ monaco = m;
72
+ metrics = applyTheme(m, host).metrics;
73
+ const uri = (side: string) => m.Uri.from({ scheme: 'inmemory', authority: `truss-diff-${id}`, path: `/${side}/${(path ?? 'file').replace(/^\/+/, '')}` });
74
+ models = { original: m.editor.createModel(original, lang, uri('original')), modified: m.editor.createModel(modified, lang, uri('modified')) };
75
+ const ed = m.editor.createDiffEditor(host, { automaticLayout: true, fixedOverflowWidgets: true, scrollBeyondLastLine: false });
76
+ ed.setModel(models);
77
+ subs.push(models.modified.onDidChangeContent(() => {
78
+ if (external) return;
79
+ modified = models!.modified.getValue();
80
+ onchange?.(modified);
81
+ }));
82
+ stop = watchTheme(host, () => (metrics = applyTheme(m, host).metrics));
83
+ editor = ed;
84
+ onready?.(ed, m);
85
+ },
86
+ (e: unknown) => { failed = String(e); }
87
+ );
88
+ return () => {
89
+ dead = true;
90
+ stop();
91
+ subs.forEach((s) => s.dispose());
92
+ editor?.dispose();
93
+ models?.original.dispose();
94
+ models?.modified.dispose();
95
+ };
96
+ });
97
+
98
+ $effect(() => {
99
+ const m = monaco;
100
+ if (!m || !models || !editor) return;
101
+ for (const x of [models.original, models.modified]) if (x.getLanguageId() !== lang) m.editor.setModelLanguage(x, lang);
102
+ if (models.original.getValue() !== original) models.original.setValue(original);
103
+ if (models.modified.getValue() !== modified) {
104
+ external = true;
105
+ editor.getModifiedEditor().executeEdits('truss-bind', [{ range: models.modified.getFullModelRange(), text: modified }]);
106
+ external = false;
107
+ }
108
+ });
109
+
110
+ $effect(() => {
111
+ if (!editor) return;
112
+ editor.updateOptions({
113
+ renderSideBySide: sideBySide,
114
+ readOnly: readonly,
115
+ originalEditable,
116
+ ignoreTrimWhitespace,
117
+ ariaLabel: label,
118
+ fontSize: metrics.fontSize,
119
+ lineHeight: metrics.lineHeight,
120
+ fontFamily: metrics.fontFamily,
121
+ ...options
122
+ });
123
+ });
124
+ </script>
125
+
126
+ <div class="de" style:height={css} data-ready={ready} role="group" aria-label={label}>
127
+ <div class="mount" bind:this={host}></div>
128
+ {#if !ready}
129
+ <div class="ph">
130
+ {#if failed}<p role="alert">The editor could not be loaded: {failed}</p>{:else}<Skeleton variant="rect" height="100%" label="Loading editor" />{/if}
131
+ </div>
132
+ {/if}
133
+ </div>
134
+
135
+ <style>
136
+ .de { position: relative; min-height: 96px; border: 1px solid var(--tr-outline-variant); border-radius: var(--tr-radius-md); overflow: hidden; background: var(--tr-surface); }
137
+ .de:focus-within { border-color: var(--tr-primary); box-shadow: var(--tr-focus-ring); }
138
+ .mount { position: absolute; inset: 0; }
139
+ .ph { position: absolute; inset: 0; display: grid; }
140
+ .ph :global(.sk-root) { display: block; width: 100%; height: 100%; }
141
+ .ph p { margin: auto; padding: var(--tr-space-4); color: var(--tr-danger); font-size: var(--tr-text-md); }
142
+ </style>
@@ -0,0 +1,32 @@
1
+ import type * as Monaco from 'monaco-editor';
2
+ import { type MonacoApi } from './monaco.js';
3
+ type $$ComponentProps = {
4
+ /** The left side. */
5
+ original?: string;
6
+ /** The right side, the one you edit. */
7
+ modified?: string;
8
+ /** A Monaco language id; defaults to the language of `path`, else plain text. */
9
+ language?: string;
10
+ path?: string;
11
+ /** Two panes (true) or one inline pane (false). Monaco also switches to inline by itself in a narrow container. */
12
+ sideBySide?: boolean;
13
+ /** Make the modified side read-only. */
14
+ readonly?: boolean;
15
+ /** Allow edits on the original side too. */
16
+ originalEditable?: boolean;
17
+ ignoreTrimWhitespace?: boolean;
18
+ /** A pixel height, any CSS length, or `'fill'` for the parent's height. */
19
+ height?: number | string | 'fill';
20
+ label?: string;
21
+ /** Any Monaco `IDiffEditorConstructionOptions`, applied after the props above. */
22
+ options?: Monaco.editor.IDiffEditorConstructionOptions;
23
+ /** Fires with the modified text after every edit made by the user. */
24
+ onchange?: (value: string) => void;
25
+ onready?: (editor: Monaco.editor.IStandaloneDiffEditor, monaco: MonacoApi) => void;
26
+ };
27
+ declare const DiffEditor: import("svelte").Component<$$ComponentProps, {
28
+ /** The Monaco diff editor, once loaded. */ getEditor: () => Monaco.editor.IStandaloneDiffEditor | undefined;
29
+ focus: () => void | undefined;
30
+ }, "modified">;
31
+ type DiffEditor = ReturnType<typeof DiffEditor>;
32
+ export default DiffEditor;