@openpresentation/opf-editor 0.26.5 → 0.26.7

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,217 @@
1
+ // Go to definition, peek, references and rename (artifacts/source-editor/SPEC.md, section 8.3). Loaded lazily by the code editor on the first use of F12,
2
+ // Alt+F12, Shift+F12, F2 or Cmd/Ctrl+click. Each command is `(view, context) => boolean` (true: the key was taken); `context` is what the code editor
3
+ // knows: `{ format, catalogs, say, api, options, view, platform, readOnly, pointer }` (see json-editor.js `featureContext`). The reading of the text is
4
+ // source-navigation-model.js, the widgets are source-navigation-ui.js; this file joins them and says what happened.
5
+ import { layoutRegionSummary } from "./layout-templates.js";
6
+ import { NOUNS, analyzeInPieces, definitionOf, lineAt, lineByNumber, lineCount, referenceList, renameBlocker, renameEdits, slideNumberAt, symbolAt, symbolKey, validateName } from "./source-navigation-model.js";
7
+ import { cardPanel, closePanel, peekPanel, referencesPanel, renamePanel, showPanel } from "./source-navigation-ui.js";
8
+
9
+ const SOURCE_LABEL = "Provided by app";
10
+ const PEEK_LINES = 10;
11
+
12
+ // The part of a line to show: all of it when it is short, else a window around `offset` (a minified deck is one line).
13
+ const EXCERPT = 200;
14
+ function excerpt(line, offset) {
15
+ if (line.text.length <= EXCERPT) return line.text;
16
+ const at = Math.max(0, offset - line.from);
17
+ const start = Math.max(0, Math.min(at - 60, line.text.length - EXCERPT));
18
+ return `${start > 0 ? "…" : ""}${line.text.slice(start, start + EXCERPT)}${start + EXCERPT < line.text.length ? "…" : ""}`;
19
+ }
20
+
21
+ const labelOf = (symbol) => `${NOUNS[symbol.kind] ?? symbol.kind} ${symbol.id}`;
22
+ const plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
23
+
24
+ // The text read once: the commands that follow each other on the same text (F12, then Alt+F12, then F2 and its Enter) share one reading, and any edit
25
+ // makes the next one read again. A big text is read in pieces with the main thread given back between them, so a command is asynchronous; when the text
26
+ // changed while it was read, it is read again.
27
+ let kept = null;
28
+ async function read(context) {
29
+ for (let attempt = 0; ; attempt++) {
30
+ const source = context.api.getValue();
31
+ if (kept && kept.source === source && kept.format === context.format && kept.catalogs === context.catalogs) return { analysis: kept.analysis, source };
32
+ const analysis = await analyzeInPieces(source, context.format, { catalogs: context.catalogs });
33
+ if (attempt < 2 && context.api.getValue() !== source) continue;
34
+ kept = { source, format: context.format, catalogs: context.catalogs, analysis };
35
+ return { analysis, source };
36
+ }
37
+ }
38
+
39
+ // The symbol under the caret, read from the text as it is now.
40
+ async function readCaret(context) {
41
+ const { api } = context;
42
+ const { analysis, source } = await read(context);
43
+ const [from, to] = api.getSelection();
44
+ const symbol = symbolAt(analysis, from) ?? (to !== from ? symbolAt(analysis, to) : null);
45
+ return { analysis, symbol, source, offset: from };
46
+ }
47
+
48
+ function wordAt(view, offset) {
49
+ const word = view.state.wordAt(Math.min(offset, view.state.doc.length));
50
+ return word ? view.state.sliceDoc(word.from, word.to) : "";
51
+ }
52
+
53
+ const jump = (context, view, occurrence, message) => {
54
+ closePanel(view, { focus: false });
55
+ context.api.setSelection(occurrence.from, occurrence.to);
56
+ view.focus();
57
+ context.say(message);
58
+ };
59
+
60
+ // The lines of a declaration (the key through the end of its value, up to ten lines) with a line of context on each side.
61
+ function peekLines(analysis, definition) {
62
+ const first = lineAt(analysis, definition.from).number, last = lineAt(analysis, Math.max(definition.from, definition.blockTo ?? definition.to)).number;
63
+ const targetEnd = Math.min(last, first + PEEK_LINES - 1);
64
+ const lines = [];
65
+ for (let number = Math.max(1, first - 1); number <= Math.min(lineCount(analysis), targetEnd + 1); number++) {
66
+ const line = lineByNumber(analysis, number);
67
+ lines.push({ number, text: excerpt(line, number === first ? definition.from : line.from), target: number >= first && number <= targetEnd });
68
+ }
69
+ return lines;
70
+ }
71
+
72
+ function recordModel(definition, context) {
73
+ const record = definition.record ?? {};
74
+ const noun = NOUNS[definition.kind] ?? definition.kind;
75
+ const name = typeof record.name === "string" && record.name ? record.name : "";
76
+ const notes = [];
77
+ if (definition.kind === "layouts") { const summary = layoutRegionSummary(record); if (summary) notes.push(`Regions: ${summary}`); }
78
+ if (definition.member) {
79
+ const member = definition.member;
80
+ const description = definition.kind === "region" ? (member.accepts?.length ? `Accepts ${member.accepts.join(", ")}.` : "") : member.description ?? "";
81
+ return { title: definition.id, type: `${noun} of ${definition.reference}`, description: [member.name && definition.kind === "beat" ? member.name : "", description].filter(Boolean).join(". ") || undefined, notes: [SOURCE_LABEL] };
82
+ }
83
+ if (name && name !== definition.id) notes.unshift(name);
84
+ notes.push(SOURCE_LABEL);
85
+ return { title: definition.id, type: noun, description: typeof record.description === "string" ? record.description : undefined, notes };
86
+ }
87
+
88
+ function cardFor(view, context, source, symbol, definition, analysis) {
89
+ const model = recordModel(definition, context);
90
+ const label = labelOf(symbol);
91
+ const close = () => closePanel(view);
92
+ let preview;
93
+ try { preview = context.options?.resolvePreview?.(definition.kind, definition.id); } catch { preview = undefined; }
94
+ showPanel(view, cardPanel({ label, title: `${label} · ${SOURCE_LABEL}`, model, preview, onClose: close }), lineAt(analysis, symbol.from).to, source);
95
+ context.say(`${label} is provided by the app. ${model.description ?? ""}`.trim());
96
+ }
97
+
98
+ // ---- F12 ----------------------------------------------------------------------------------------------------------------------------
99
+ /** F12, Cmd/Ctrl+click: jump to where the id under the caret is defined (or open the record's card). */
100
+ export function goToDefinition(view, context) {
101
+ void goTo(view, context);
102
+ return true;
103
+ }
104
+ async function goTo(view, context) {
105
+ const { analysis, symbol, source } = await readCaret(context);
106
+ if (!view.dom.isConnected) return;
107
+ // A click that finds nothing is a plain click.
108
+ const quiet = context.pointer === true;
109
+ if (!symbol) { if (!quiet) context.say(`No definition found${wordAt(view, context.api.getSelection()[0]) ? ` for '${wordAt(view, context.api.getSelection()[0])}'` : ""}`); return; }
110
+ const definition = definitionOf(analysis, symbol);
111
+ if (!definition) { if (!quiet) context.say(`No definition found for '${symbol.id}'`); return; }
112
+ if (definition.where === "built-in") { if (!quiet) context.say(`${symbol.id} is a built-in variable; it has no definition in the document`); return; }
113
+ if (definition.where === "app") { cardFor(view, context, source, symbol, definition, analysis); return; }
114
+ const target = definition.occurrence;
115
+ // On the declaration itself the key lists the uses, as VS Code does.
116
+ if (target === symbol) { showReferences(view, context, analysis, symbol, source); return; }
117
+ jump(context, view, target, `Definition of ${labelOf(symbol)}, line ${lineAt(analysis, target.from).number}`);
118
+ }
119
+
120
+ // ---- Alt+F12 ------------------------------------------------------------------------------------------------------------------------
121
+ /** Alt+F12: show the definition inline. */
122
+ export function peekDefinition(view, context) {
123
+ void peek(view, context);
124
+ return true;
125
+ }
126
+ async function peek(view, context) {
127
+ const { analysis, symbol, source } = await readCaret(context);
128
+ if (!view.dom.isConnected) return;
129
+ if (!symbol) { context.say(`No definition found${wordAt(view, context.api.getSelection()[0]) ? ` for '${wordAt(view, context.api.getSelection()[0])}'` : ""}`); return; }
130
+ const definition = definitionOf(analysis, symbol);
131
+ if (!definition) { context.say(`No definition found for '${symbol.id}'`); return; }
132
+ if (definition.where === "built-in") { context.say(`${symbol.id} is a built-in variable; it has no definition in the document`); return; }
133
+ if (definition.where === "app") { cardFor(view, context, source, symbol, definition, analysis); return; }
134
+ const target = definition.occurrence;
135
+ if (target === symbol) { showReferences(view, context, analysis, symbol, source); return; }
136
+ const label = labelOf(symbol), line = lineAt(analysis, target.from).number;
137
+ const close = () => closePanel(view);
138
+ showPanel(view, peekPanel({
139
+ label, title: label, where: `Line ${line}`, lines: peekLines(analysis, target),
140
+ onGo: () => jump(context, view, target, `Definition of ${label}, line ${line}`),
141
+ onClose: close,
142
+ }), lineAt(analysis, symbol.from).to, source);
143
+ context.say(`Peek definition of ${label}, line ${line}. Enter goes to it, Escape closes.`);
144
+ }
145
+
146
+ // ---- Shift+F12 ----------------------------------------------------------------------------------------------------------------------
147
+ function showReferences(view, context, analysis, symbol, source) {
148
+ const list = referenceList(analysis, symbol);
149
+ const label = labelOf(symbol);
150
+ const rows = list.map((occurrence) => {
151
+ const line = lineAt(analysis, occurrence.from);
152
+ return { number: line.number, slide: slideNumberAt(analysis, occurrence.from), text: excerpt(line, occurrence.from), declaration: occurrence.role === "def" };
153
+ });
154
+ const count = plural(list.length, "reference");
155
+ const close = () => closePanel(view);
156
+ showPanel(view, referencesPanel({
157
+ label, title: `${count} to ${label}`, rows,
158
+ onPick: (index) => jump(context, view, list[index], `${label}, ${list[index].role === "def" ? "declaration" : "reference"} at line ${rows[index].number}`),
159
+ onClose: close,
160
+ }), lineAt(analysis, symbol.from).to, source);
161
+ context.say(`${count} to ${label}`);
162
+ }
163
+
164
+ /** Shift+F12: list the places that use the id under the caret. */
165
+ export function findReferences(view, context) {
166
+ void references(view, context);
167
+ return true;
168
+ }
169
+ async function references(view, context) {
170
+ const { analysis, symbol, source } = await readCaret(context);
171
+ if (!view.dom.isConnected) return;
172
+ if (!symbol) { context.say("No references found"); return; }
173
+ showReferences(view, context, analysis, symbol, source);
174
+ }
175
+
176
+ // ---- F2 -----------------------------------------------------------------------------------------------------------------------------
177
+ /** F2: rename the id under the caret in its definition and every reference, as one undo step. */
178
+ export function renameSymbol(view, context) {
179
+ void rename(view, context);
180
+ return true;
181
+ }
182
+ async function rename(view, context) {
183
+ const { analysis, symbol, source } = await readCaret(context);
184
+ if (!view.dom.isConnected) return;
185
+ if (!symbol) { context.say(context.readOnly ? "Cannot rename: the editor is read-only" : "Cannot rename: there is no id at the caret"); return; }
186
+ const blocker = renameBlocker(analysis, symbol, { readOnly: view.state.readOnly });
187
+ if (blocker) { context.say(blocker); return; }
188
+ const label = labelOf(symbol), key = symbolKey(symbol);
189
+ const close = () => closePanel(view);
190
+ let applying = false;
191
+ const apply = async (name) => {
192
+ if (applying) return;
193
+ applying = true;
194
+ // Read the text again: the author may have typed since the input opened.
195
+ const { analysis: now } = await read(context);
196
+ applying = false;
197
+ // The occurrence the caret was on (the nearest one of the symbol, when the text moved).
198
+ const same = now.occurrences.filter((occurrence) => symbolKey(occurrence) === key);
199
+ const current = same.reduce((best, occurrence) => (!best || Math.abs(occurrence.from - symbol.from) < Math.abs(best.from - symbol.from) ? occurrence : best), null);
200
+ if (!current) { close(); context.say(`Rename cancelled: ${label} is no longer in the text`); return; }
201
+ const problem = validateName(now, current, name);
202
+ if (problem) { context.say(problem); return; }
203
+ const result = renameEdits(now, current, name);
204
+ closePanel(view, { focus: false });
205
+ if (!result) { view.focus(); context.say(`${label} keeps its name`); return; }
206
+ // The selection goes to the same occurrence it was on when the input opened.
207
+ const places = result.edits.length;
208
+ context.api.applyEdits(result.edits, { select: result.caret, userEvent: "input.rename" });
209
+ view.focus();
210
+ context.say(`Renamed ${label} to ${name} in ${plural(places, "place")}`);
211
+ };
212
+ showPanel(view, renamePanel({
213
+ label, current: symbol.id, validate: (name) => validateName(analysis, symbol, name),
214
+ onSubmit: apply, onClose: close, say: context.say,
215
+ }), lineAt(analysis, symbol.from).to, source);
216
+ context.say(`Rename ${label}. Type the new name and press Enter, or Escape to cancel.`);
217
+ }
@@ -0,0 +1,112 @@
1
+ // The symbol picker (artifacts/source-editor/SPEC.md, section 8.4): Cmd/Ctrl+Shift+O lists the slides ("3. Highlights") and the other symbols of the
2
+ // document (source-symbols.js) in a box at the top of the editor, filtered as you type. The arrows move, Enter jumps to the symbol and selects its range,
3
+ // Escape closes and returns to where you were. Loaded lazily by the code editor.
4
+ import { EditorView } from "@codemirror/view";
5
+ import { documentSymbols, filterSymbols, symbolAt } from "./source-symbols.js";
6
+
7
+ let counter = 0;
8
+ const open = new WeakMap();
9
+ const MAX_SHOWN = 200;
10
+
11
+ const element = (doc, tag, className, attributes = {}) => {
12
+ const node = doc.createElement(tag);
13
+ if (className) node.className = className;
14
+ for (const [key, value] of Object.entries(attributes)) node.setAttribute(key, value);
15
+ return node;
16
+ };
17
+ const KIND_WORDS = { slide: "slide", section: "section", asset: "asset", dataset: "dataset", variable: "variable", reference: "reference", record: "catalog record" };
18
+
19
+ /** Go to a symbol: select its range, scroll it into view and say where we are. */
20
+ export function goToSymbol(view, context, symbol) {
21
+ context.api.setSelection(symbol.from, symbol.to);
22
+ context.api.focus();
23
+ context.say(`${symbol.label}, ${KIND_WORDS[symbol.kind] ?? symbol.kind}, line ${symbol.line}`);
24
+ }
25
+
26
+ /** Open the picker for `view` (Cmd/Ctrl+Shift+O); a second press closes it. Returns true: the key was taken. */
27
+ export function showSymbolPicker(view, context) {
28
+ const existing = open.get(view);
29
+ if (existing) { existing.close(); return true; }
30
+ const text = context.api.getValue();
31
+ const symbols = documentSymbols(context.format, text);
32
+ const doc = view.dom.ownerDocument;
33
+ const id = `opf-quickpick-${++counter}`;
34
+ const root = element(doc, "div", "opf-quickpick");
35
+ const input = element(doc, "input", "opf-quickpick-input", { type: "text", role: "combobox", "aria-label": "Go to symbol", "aria-expanded": "true", "aria-controls": `${id}-list`, "aria-autocomplete": "list", placeholder: "Go to symbol: type to filter", autocomplete: "off", spellcheck: "false" });
36
+ const list = element(doc, "ul", "opf-quickpick-list", { id: `${id}-list`, role: "listbox", "aria-label": "Symbols" });
37
+ const status = element(doc, "div", "opf-quickpick-status", { role: "status", "aria-live": "polite" });
38
+ root.append(input, list, status);
39
+ view.dom.append(root);
40
+ const [caret] = context.api.getSelection();
41
+ const here = symbolAt(symbols.filter((symbol) => symbol.kind === "slide" || symbol.depth >= 0), caret);
42
+ const scroll = view.scrollDOM.scrollTop;
43
+ let shown = symbols, selected = 0, closed = false, timer = null;
44
+
45
+ function optionId(index) { return `${id}-option-${index}`; }
46
+ function select(index, { preview = true } = {}) {
47
+ if (!shown.length) { selected = 0; input.removeAttribute("aria-activedescendant"); return; }
48
+ selected = (index + shown.length) % shown.length;
49
+ [...list.children].forEach((node, at) => { const on = at === selected; node.setAttribute("aria-selected", String(on)); node.classList.toggle("is-selected", on); if (on) node.scrollIntoView?.({ block: "nearest" }); });
50
+ input.setAttribute("aria-activedescendant", optionId(selected));
51
+ // The editor follows the selection without moving the caret (VS Code shows the symbol as you arrow through the list).
52
+ if (preview) view.dispatch({ effects: EditorView.scrollIntoView(shown[selected].from, { y: "center" }) });
53
+ clearTimeout(timer);
54
+ timer = setTimeout(() => { const symbol = shown[selected]; if (symbol && !closed) context.say(`${symbol.label}, ${KIND_WORDS[symbol.kind] ?? symbol.kind}. ${selected + 1} of ${shown.length}`); }, 100);
55
+ }
56
+ function render(initial) {
57
+ shown = filterSymbols(symbols, input.value);
58
+ list.replaceChildren();
59
+ shown.slice(0, MAX_SHOWN).forEach((symbol, index) => {
60
+ const item = element(doc, "li", `opf-quickpick-item is-${symbol.kind}`, { id: optionId(index), role: "option", "aria-selected": "false", "data-kind": symbol.kind });
61
+ item.style.paddingLeft = `${10 + symbol.depth * 14}px`;
62
+ item.append(Object.assign(element(doc, "span", "opf-quickpick-label"), { textContent: symbol.label }));
63
+ if (symbol.detail) item.append(Object.assign(element(doc, "span", "opf-quickpick-detail"), { textContent: symbol.detail }));
64
+ item.append(Object.assign(element(doc, "span", "opf-quickpick-kind"), { textContent: KIND_WORDS[symbol.kind] ?? symbol.kind }));
65
+ item.addEventListener("pointerdown", (event) => event.preventDefault());
66
+ item.addEventListener("click", () => pick(index));
67
+ list.append(item);
68
+ });
69
+ if (shown.length > MAX_SHOWN) list.append(Object.assign(element(doc, "li", "opf-quickpick-more", { role: "presentation" }), { textContent: `${shown.length - MAX_SHOWN} more: keep typing to narrow the list.` }));
70
+ status.textContent = shown.length ? `${shown.length} ${shown.length === 1 ? "symbol" : "symbols"}` : "No symbols match";
71
+ const start = initial && here ? Math.max(0, shown.findIndex((symbol) => symbol.id === here.id)) : 0;
72
+ select(start, { preview: !initial });
73
+ if (!initial) context.say(status.textContent);
74
+ }
75
+ function pick(index) {
76
+ const symbol = shown[index];
77
+ if (!symbol) return;
78
+ close({ restore: false });
79
+ goToSymbol(view, context, symbol);
80
+ }
81
+ function close({ restore = true } = {}) {
82
+ if (closed) return;
83
+ closed = true;
84
+ clearTimeout(timer);
85
+ open.delete(view);
86
+ doc.removeEventListener("pointerdown", outside, true);
87
+ root.remove();
88
+ if (restore) view.scrollDOM.scrollTop = scroll;
89
+ if (view.dom.isConnected) view.focus();
90
+ }
91
+ function outside(event) { if (!root.contains(event.target)) close(); }
92
+ open.set(view, { close });
93
+ input.addEventListener("input", () => render(false));
94
+ input.addEventListener("keydown", (event) => {
95
+ const page = Math.max(1, Math.floor(list.clientHeight / 28) - 1);
96
+ if (event.key === "ArrowDown") { event.preventDefault(); select(selected + 1); }
97
+ else if (event.key === "ArrowUp") { event.preventDefault(); select(selected - 1); }
98
+ else if (event.key === "PageDown") { event.preventDefault(); select(Math.min(shown.length - 1, selected + page)); }
99
+ else if (event.key === "PageUp") { event.preventDefault(); select(Math.max(0, selected - page)); }
100
+ else if (event.key === "Enter") { event.preventDefault(); pick(selected); }
101
+ else if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(); }
102
+ else if (event.key === "Tab") { event.preventDefault(); close(); }
103
+ // The key that opened the picker closes it, also from the input.
104
+ else if ((context.platform === "mac" ? event.metaKey : event.ctrlKey) && event.shiftKey && !event.altKey && (event.key.toLowerCase() === "o" || event.code === "KeyO")) { event.preventDefault(); event.stopPropagation(); close(); }
105
+ });
106
+ input.addEventListener("blur", () => { setTimeout(() => { if (!closed && !root.contains(doc.activeElement)) close({ restore: false }); }, 0); });
107
+ doc.addEventListener("pointerdown", outside, true);
108
+ render(true);
109
+ input.focus();
110
+ context.say(`Go to symbol, ${symbols.length} ${symbols.length === 1 ? "symbol" : "symbols"}. Type to filter, Enter to go, Escape to close.`);
111
+ return true;
112
+ }
@@ -43,7 +43,42 @@ export interface SourceFile {
43
43
  mime: string;
44
44
  }
45
45
 
46
+ export type ShortcutPlatform = "mac" | "win" | "linux";
47
+ /** One row of the keyboard shortcuts list. */
48
+ export interface Shortcut {
49
+ id: string;
50
+ title: string;
51
+ group: string;
52
+ /** The keys as words: `Cmd+Shift+O` on macOS, `Ctrl+Shift+O` elsewhere; a chord is two strokes separated by a space (`Cmd+K Cmd+0`). */
53
+ keys: string[];
54
+ /** The same keys with the macOS symbols (`⌘⇧O`) on macOS, else the words. */
55
+ symbols: string[];
56
+ /** The keys in CodeMirror's notation (`Mod-Shift-o`). */
57
+ strokes: string[];
58
+ /** What has to be true for the keys to apply (`the list is open`). */
59
+ when?: string;
60
+ }
61
+
62
+ /** One entry of the outline of a document (the symbol picker and the outline panel). Offsets are source offsets. */
63
+ export interface OutlineSymbol {
64
+ id: string;
65
+ kind: "slide" | "section" | "asset" | "dataset" | "variable" | "reference" | "record";
66
+ /** `3. Highlights` for a slide, the id for an asset, `layout hero` for an embedded catalog record. */
67
+ label: string;
68
+ /** `layout cover` for a slide with a layout, the title of a dataset, the catalog group of a record. */
69
+ detail: string;
70
+ /** 1 for the entries under a section (assets, datasets, variables, references). */
71
+ depth: number;
72
+ from: number;
73
+ to: number;
74
+ line: number;
75
+ }
76
+
46
77
  export interface SourcePaneOptions {
78
+ /** Add an Outline button to the bar and a panel listing the slides and other symbols of the document (`"open"` shows the panel at the start). The symbol picker (Cmd/Ctrl+Shift+O) is always there. */
79
+ outline?: boolean | "open";
80
+ /** Called with `getShortcuts()` when the shortcuts dialog is about to open; returning `true` means the host shows its own and the editor's dialog stays closed. */
81
+ onShowShortcuts?(shortcuts: Shortcut[]): boolean | void;
47
82
  /** The editor session. Every edit in any format reaches it as a patch (`applyPatch`), so it is validated and undoable. */
48
83
  editor: EditorSession;
49
84
  /** File name without extension (default `deck`). Spaces and slashes become `-`. */
@@ -75,6 +110,17 @@ export interface SourcePaneOptions {
75
110
  completions?: boolean;
76
111
  /** Enter continues a list: a new string item after a string in a JSON list, the next `- ` in YAML, the next marker (or `> `) in Markdown; an empty one ends it. Default true. */
77
112
  smartListEnter?: boolean;
113
+ /**
114
+ * Run the pane's text checks (reading YAML or Markdown, the syntax pass of JSON and the document validation) in a Web Worker, so typing in a deck
115
+ * of a thousand slides never blocks the main thread. A function returns the Worker (`() => new Worker(new URL("@openpresentation/opf-editor/source-worker", import.meta.url), { type: "module" })`
116
+ * with a bundler that supports that form); a Worker is used as given; `true` starts `source-worker.js` beside the package's own module, which works
117
+ * when a bundler follows `new URL(..., import.meta.url)`. Only the typing delay uses the worker: every explicit action (`getText`, switching the format,
118
+ * `replaceDocument`, Format, Undo) is applied on the main thread at once. When the worker cannot start, fails or stops answering, the pane checks on the
119
+ * main thread from then on and `onError` is called once; no edit is lost. Default `false` (everything on the main thread).
120
+ */
121
+ worker?: boolean | Worker | (() => Worker);
122
+ /** A preview for the card F12 and Alt+F12 open on a catalog record only the app provides (see `JsonCodeEditorOptions.resolvePreview`). */
123
+ resolvePreview?: (kind: string, id: string) => HTMLElement | string | undefined | Promise<HTMLElement | string | undefined>;
78
124
  onError?(error: unknown): void;
79
125
  }
80
126
 
@@ -136,8 +182,25 @@ export interface SourcePane {
136
182
  showProblem(index: number): boolean;
137
183
  /** Open the suggestion list at the caret, as Ctrl+Space does. */
138
184
  triggerCompletion(): boolean;
185
+ /**
186
+ * Every key binding the editor installed, for `platform` (default: the page's), grouped and titled as the shortcuts dialog shows them. The list is produced from the
187
+ * table that installs the keymap, so it cannot drift from the keys.
188
+ */
189
+ getShortcuts(platform?: ShortcutPlatform): Shortcut[];
190
+ /** Open the keyboard shortcuts dialog (the `?` button, F1, Cmd/Ctrl+K then Cmd/Ctrl+S). `onShowShortcuts` returning true replaces it with the host's own. */
191
+ showShortcuts(): boolean;
192
+ /** Open the symbol picker (Cmd/Ctrl+Shift+O). */
193
+ showSymbols(): boolean;
194
+ /** The outline of the text as it is now, in document order. Works on text that does not parse. */
195
+ getOutline(): OutlineSymbol[];
196
+ /** Show or hide the outline panel (needs the `outline` option); without an argument it toggles. Returns whether the panel is open. */
197
+ toggleOutline(open?: boolean): boolean;
139
198
  /** The underlying code editor (`mountJsonCodeEditor`). */
140
199
  readonly editor: JsonCodeEditor;
200
+ /** Resolves when no typed edit waits for its apply delay and no check (a worker's included) is running: the text on screen has been read and applied. */
201
+ whenIdle(): Promise<void>;
202
+ /** How the applied edits were checked: how many by the worker and how many on the main thread, and whether the worker can still be used. */
203
+ checkerStats(): { worker: number; inline: number; available: boolean };
141
204
  destroy(): void;
142
205
  }
143
206