@openpresentation/opf-editor 0.26.4 → 0.26.6
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/README.md +8 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +9 -2
- package/dist/json-editor.d.ts +20 -0
- package/dist/json-editor.js +189 -91
- package/dist/source-a11y.js +18 -1
- package/dist/source-check.js +90 -0
- package/dist/source-checker.js +130 -0
- package/dist/source-completions.js +232 -22
- package/dist/source-details.js +17 -0
- package/dist/source-diagnostics.js +3 -2
- package/dist/source-editing.js +53 -33
- package/dist/source-fixes-syntax.js +334 -0
- package/dist/source-fixes-text.js +75 -2
- package/dist/source-fixes.js +25 -1
- package/dist/source-folding.js +173 -0
- package/dist/source-hints.js +376 -0
- package/dist/source-keymap.js +369 -0
- package/dist/source-markdown.js +53 -17
- package/dist/source-messages.js +21 -205
- package/dist/source-navigation-model.js +426 -0
- package/dist/source-navigation-read.js +269 -0
- package/dist/source-navigation-ui.js +244 -0
- package/dist/source-navigation.js +217 -0
- package/dist/source-outline.js +112 -0
- package/dist/source-pane.d.ts +63 -0
- package/dist/source-pane.js +240 -45
- package/dist/source-problems.js +3 -0
- package/dist/source-references.js +274 -0
- package/dist/source-shortcuts.js +106 -0
- package/dist/source-snippets.js +20 -0
- package/dist/source-symbols.js +156 -0
- package/dist/source-undo-groups.js +46 -0
- package/dist/source-worker.d.ts +7 -0
- package/dist/source-worker.js +30 -0
- package/dist/source-yaml.js +94 -9
- package/dist/styles.css +288 -0
- package/package.json +12 -7
package/README.md
CHANGED
|
@@ -120,11 +120,18 @@ Methods: `replaceDocument(document, { source?, format?, name?, meta? })` (below)
|
|
|
120
120
|
- **Completions.** In JSON, YAML and Markdown the pane offers what the schema and the catalogs say belongs at the caret (`completions: false` keeps the field menu only). *Keys*: the properties of the object at the caret, the required ones first, then the common ones in a curated order (a slide: `title`, `subtitle`, `layout`, `items`, `text`, `notes`, ...), then the rest; the promoted region keys of a slide (`top+middle:left+center`) show only once you type three characters of one. Keys already in the object are not offered. Accepting one writes the whole pair: a string key gets `"key": "|"`, a number `"key": |`, an array `"key": [|]`, an object `"key": {|}`, and an object with required members (`organization`: `id`, `name`) writes them one per line, each a tab stop that `Tab` walks (the last `Tab` leaves the braces). A key with a closed set of values (an enum, a catalog id, a boolean) opens the list of its values at once. A comma is added when a property follows, and not before `}`. *Values*: enum values and booleans, and catalog ids (layouts, themes, color and font schemes, narratives, audiences, purposes, tones: the document's own records first, then the registered catalogs', layouts in the order of their intents, each with its description and regions in the documentation panel); after a colon, `{}` and `[]` templates. *Snippets*: `slide`, `slide-text`, `slide-image`, `slide-chart`, `slide-quote`, `slide-metric`, `slide-table`, `slide-code`, `slide-two-column` and `section` in `slides`, `reference` in `references`, `dataset` in `datasets` and `deck` in a blank document, with tab stops (the chart type stop opens the list of chart types). *Triggers*: a quote or a word at a key or value place, `:` and the space after it, and `Ctrl+Space` (`Cmd+I` on macOS) anywhere the schema has something to offer, also right after a finished string; free text (titles, notes, alt text) and numbers never open a list on their own. *Keys in the list*: `Tab` and `Enter` accept (at once, right after a keystroke too), `Shift+Tab` and `Shift+Enter` accept too (an accepted item always replaces the whole token), `Escape` closes it, `Left`, `Right` and `Backspace` inside the word keep it open and filter by the text before the caret, `Ctrl+Space` with the list open shows or hides the documentation panel. With several cursors the accepted item goes in at each of them (as plain text). The data is core's (`describeField` and `catalogKindAt` of `@openpresentation/opf/schema`; `getJsonCompletions` of `/json-options` remains for hosts that want the plain list). `mountJsonCodeEditor` takes the same `completions` option, off by default there.
|
|
121
121
|
- **Completions in YAML.** The same keys, values, ids, templates and snippets, from the same schema, in block and flow style. A line-and-indent reader (`source-yaml.js`) places the caret without parsing, so it works while the text is half typed: after `key: ` the values of that field, at a key place the keys of the mapping (never one already there, above or below the caret), after a list dash the keys of the item and, in `slides` and `references`, the item snippets, inside `[ ]` and `{ }` on one line the items and keys of that collection. Accepting a key writes it the way YAML spells it: `key: ` for a scalar (and the list of its values for an enum or a boolean), `key:` and an indented new line for a mapping (required members one per line, each a tab stop), `key:` and `- ` for a list; enum values and ids are plain scalars, quoted when YAML would read them as another type (`"true"`, `"12"`). Comments, block scalars and free text (titles, notes, numbers) never open a list. `Enter` indents one level after `key:` and `key: |`, aligns under the key after `- key: value`, goes on with the next `- ` after a list item that is a value, and leaves the list on an empty `- `. `[`, `{` and quotes pair (not `(` and not an apostrophe inside a word).
|
|
122
122
|
- **Completions in Markdown.** The front matter and the YAML in `opf-slide` and `opf-block` fences are the YAML above (the front matter never offers `slides`); `<!-- ` offers `slide:` and `block:`; inside a slide options comment the keys (`id`, `type`, `layout`, `section`, `tag`, `hidden`, `beat`) and their values (layout ids, slide types, `true` and `false`), inside a block options comment `id`, `type`, `as` (`bullets`, `video`) and `region` (the regions of the slide's layout, then the promoted ones); after a fence's three backticks the fence kinds (`chart`, `metric`, `timeline`, `opf-block`, `opf-slide` and the code languages), after `chart ` the chart types, after the info word the attributes (`alt=`, `name=`, `description=`, `title=`); the keys and values of a `metric` fence and the status marks (`[x]`, `[>]`, `[ ]`) at the start of a `timeline` event; `{` after `]` the attributes of an inline span (`bold`, `color=`, `size=`, `font=`, `lang=`); and at the start of a line `Note:`, `Notes:` and the snippets (a slide of each kind, with its `---` separator and a blank line above it when it follows another slide). Code fences, chart data, speaker notes and plain comments never open a list, and prose does not either: a word at the start of a line opens one from the third letter on, only while it is the start of a keyword or a snippet (`Ctrl+Space` opens it anywhere). Hover (and `Cmd/Ctrl+K Cmd/Ctrl+I`) explains a front matter key or value and a slide option as in YAML, and the dialect's own syntax: the `#` of a title and the `##` of a subtitle, a list marker, `Note:`, a quote's `>`, the word of a `chart`, `metric`, `timeline`, `opf-slide` or `opf-block` fence, `slide:` and `block:`, and a `---` line, each with the schema's description of the field it writes and how it is written. The vocabulary is core's (`scanMarkdown`, `SLIDE_OPTION_KEYS`, `FENCE_KINDS`, ... of `@openpresentation/opf/source`).
|
|
123
|
-
- **
|
|
123
|
+
- **References inside the document.** In all three formats the lists also offer the ids the document itself declares, read from the text being edited (a draft too): `asset:<id>` from `assets` (and `var:<id>` of image variables) where an image or other asset goes, dataset ids from `datasets` (`chart.data.dataset`, `table.dataset`), the column names of that dataset (or of the chart's own `data.columns`) in `fields`, `mapping`, `line`, `secondaryAxis` and `highlight.series`, `cite` ids from `references`, `beat` ids from the narrative the deck names (in a Markdown slide options comment too), `region` names from the slide's layout (then the promoted regions), `var:<id>` color variables where a color goes, and the variables of `variables` after `{{` in any text (`Tab` writes the name and the closing braces; speaker notes stay verbatim). Which field takes which kind is one table in `source-references.js`, checked against the schema by a unit test. A slide with a `layout` lists the keys that layout's regions call for first (after the required ones). A list of catalog records ends with the line "Records from catalog X are not loaded" when the document names a catalog group whose source the host did not register (its embedded records are still offered). Accepting a key of a closed set of values and then its value is one undo step. The deck's `design`, `organization` and `speaker` keys are accepted as snippets (a tab stop for each member; `design` opens the theme, color scheme and font scheme lists in turn). In YAML (and the YAML of Markdown) the list also works on the lines of a flow collection or a quoted scalar that runs over several lines.
|
|
124
|
+
- **Hover and problems.** Hovering a key shows the schema's description, its type and its allowed values; hovering a catalog id shows the record (name, description, regions, where it comes from); `Cmd/Ctrl+K Cmd/Ctrl+I` shows the same at the caret and `Escape` hides it. Every finding that has a place in the text is underlined (errors, warnings and notes, in every format that gives core a range; JSON gets one for each schema and reference finding too), with a tooltip that says what is wrong, what could come next and how many quick fixes there are. JSON syntax errors come from core's recovery analyzer (`analyzeJsonSyntax` of `@openpresentation/opf/source`): one problem per cause, at the cause (a key with no value is reported on its own line, not at the next token), up to five, 150 ms after the last key; a problem is held back while a list of keys is open next to it, so typing `"des` never flashes red. The panel rows say `Ln 3, Col 5`, the message, `Next: ...`, the rule and the quick fix buttons. `F8` and `Shift+F8` select the next and the previous problem (and say it; they wrap), `Cmd/Ctrl+Shift+M` moves focus to the panel (arrows, Home and End move between rows, Enter selects the range and returns to the code, Escape returns, `Cmd/Ctrl+.` applies the first fix of the row), `Cmd/Ctrl+.` in the code lists the fixes at the caret (the safest first; arrows, Enter, Escape). A fix is one undo step. Quick fixes in this version: the syntax repairs (insert or remove a comma, add `{ }` or remove a key with no value, close a bracket, straight quotes, ...), "Change to ..." for an enum or a reference that resolves nowhere, "Rename to ..." and "Remove property" for an unknown key, "Add `slides`" and the like for a missing required property, "Wrap in a list", "Add a slide", "Remove the first/second" for a duplicate key, and the patch and focus fixes core ships (alt text, readable color, language). Phase 3 writes every problem in plain words from core (`describeFinding`: the schema keywords, all 77 `opf/*` rules, the YAML and Markdown readers; each with a `Next:` line) and adds a Details disclosure per row (`explain(ruleId)`: what the rule checks, why, how to fix it, an example). YAML and Markdown syntax problems have fixes too (`Convert tabs to spaces`, `Insert closing quote`, ``Insert ]``, `Add a colon`, `Quote the value`, `Insert ---`, `Insert closing fence`, `Insert -->`, `Rename to layout`, `Insert column`, ``Add `numbering` ``, ...), and every format gets `Move into extensions`, `Convert to string`, `Wrap in a list` and `Add a slide` as comment-preserving text edits. `pane.getProblems()`, `nextProblem()`, `previousProblem()`, `showProblem(index)` and `triggerCompletion()` expose the same to a host.
|
|
125
|
+
- **Shape hint, built-in variables and image targets.** `Cmd/Ctrl+Shift+Space` shows the fields of the object around the caret as a tooltip (`title set`, `items next`, `notes optional` for a slide with a layout; the required fields of an `organization`, a chart's `type` and `data`, the field of a block's `type`), and typing `{` or `,` between the members of a slide that has a `layout` shows it by itself; it follows the caret across members, closes on `Escape` or when the caret leaves the object, never takes the arrow keys, is said once in the live region, and works in JSON, YAML, Markdown (front matter, a slide's options comment, `opf-slide` fences) and read-only editors (`hints: false` turns the automatic show off). After `{{` the list offers the document's variables first and then the built-in ones (`deck.*`, `speaker.*`, `organization.*`, `slide.*`, from core's `listBuiltinVariables`), each saying whether the document sets it; accepting one with several cursors writes it at every cursor in one undo step. In Markdown, ` from the front matter, the videos first after `<!-- block: as=video -->`. The Markdown quick fixes now cover a property set twice ("Remove `id` from the fence"), an empty quote, an orphan or trailing options comment, a malformed option, an attribute a fence does not take and a metric key given twice.
|
|
124
126
|
- **Keyboard.** The keys are VS Code's. `Tab` accepts a suggestion when the list is open; otherwise it inserts spaces up to the next tab stop (replacing a selection inside one line; a selection over several lines, or a whole line, is indented line by line) and `Shift+Tab` outdents the line. The indentation unit is the document's (JSON: read once when a document is opened, two spaces if nothing is indented, or a tab; YAML and Markdown: two spaces) and typing never changes it. `Alt+Up` and `Alt+Down` move the line, `Shift+Alt+Up` and `Shift+Alt+Down` copy it, `Cmd/Ctrl+Shift+K` deletes it. `Cmd/Ctrl+D` adds the next occurrence, `Cmd/Ctrl+Shift+L` all of them, `Cmd/Ctrl+Alt+Up` and `Down` a cursor above or below, `Alt+click` a cursor, `Shift+Alt+drag` a column; `Escape` goes back to one cursor. The field menu (a searchable dialog of the choices of one field) is on `Alt+Shift+O`; `Tab` and `Shift+Tab` stay inside it and `Escape` returns to the editor. A paste inserts the clipboard text as it is, however many lines it has.
|
|
127
|
+
- **One table of keys, the shortcuts dialog and `getShortcuts()`.** Every binding is declared in `source-keymap.js` (keys per platform, title, group) and the editor installs the keys it reads from there, so `pane.getShortcuts(platform?)` (`{ id, title, group, keys, symbols, strokes, when? }`) lists exactly what is installed. `F1`, `Cmd/Ctrl+K Cmd/Ctrl+S`, the `?` button of the bar, or `?` on any control that is not a text field open the searchable dialog; `onShowShortcuts(shortcuts)` returning `true` lets the host show its own. `Cmd/Ctrl+click` goes to the definition (`Alt+click` adds a cursor, as in VS Code); `Ctrl+G` goes to a line, `Cmd+Alt+F` / `Ctrl+H` replaces, `Cmd/Ctrl+L` expands the line selection, `Shift+Alt+Right/Left` (`Ctrl+Shift+Cmd+Right/Left` on macOS) expands and shrinks the selection, `Cmd/Ctrl+Shift+Enter` inserts a line above.
|
|
128
|
+
- **Folding.** A fold gutter in all three formats (JSON objects and lists, YAML by indent, Markdown: the front matter, each fence and each slide). `Cmd+Alt+[` / `Ctrl+Shift+[` folds the innermost range around the caret and `]` unfolds it; the chords `Cmd/Ctrl+K` then `0` (fold all), `J` (unfold all), `1` to `7` (fold level), `L` (toggle), `[` and `]` (recursively) are VS Code's.
|
|
129
|
+
- **Outline.** `Cmd/Ctrl+Shift+O` opens the symbol picker: the slides (`3. Highlights`) and the document's assets, datasets, variables, references, design/organization/speaker sections and embedded catalog records, filtered as you type; `Enter` goes there and selects its range, `Escape` returns. `pane.getOutline()` returns the same symbols for any text (broken too); `outline: true` (or `"open"`) adds an Outline button and a panel that follows the caret and the text, `pane.toggleOutline()`.
|
|
125
130
|
- **Keyboard in YAML and Markdown.** `Enter` follows the text (see the two bullets above); in Markdown it continues `- `, `* `, `+ ` and `1. ` items (the number counts up), continues `> `, ends the list on an empty marker (one level out when it is nested), and keeps the indentation inside a code fence. `Tab` on a Markdown list item nests it one level (two spaces) and `Shift+Tab` brings it out; `Backspace` right after a marker takes the marker away. `Cmd/Ctrl+/` toggles `# ` on YAML lines and in the front matter, and wraps a Markdown line in `<!-- -->` (a slide or block options comment says it already is one). Typing `*`, `_`, `~` or `"` over a Markdown selection wraps it (`*` twice makes bold). `Cmd/Ctrl+Alt+[` and `]` fold and unfold a YAML mapping and, in Markdown, the front matter, a fenced block and a slide (a `---` inside a fence is not the end of one); the fold gutter and the chords for folding everything come with the navigation round. The squiggle, `F8`, the quick fix menu (`Cmd/Ctrl+.`) and the `Ln n, Col m` of the problems panel are at the right place in YAML and Markdown too: core's `locate` finds a field's key or value in the text (and a Markdown slide's title, list item or `layout=` token); the fixes (`Change to center`, `Rename to design`, `Remove property`, ``Add `name` ``) are text edits that keep the comments.
|
|
131
|
+
- **Go to definition, peek, references and rename (phase 4).** The ids of a document are symbols, in JSON, YAML and Markdown, also while the text is broken: assets (`asset:logo`, a Markdown ``), variables (`var:brand`, `{{name}}`, `{{name|format}}`), datasets and their columns, references (`cite`), slide ids (`"id"`, `id:`, `<!-- slide: id=... -->`), the beats and regions of a narrative or layout, and every catalog id (layout, theme, color scheme, font scheme, narrative, audience, purpose, tone, also as `name:id`). `F12` (and Cmd/Ctrl+click) jumps to the declaration and selects it; a record only a registered catalog has (the app's) opens a read-only card in the widget (name, description, regions, "Provided by app", and the preview from the host's `resolvePreview(kind, id)` option of the pane or the code editor, an element or an `https:` or `data:image/` address); a built-in variable (`organization.name`) or an id nothing declares says so and does not move. On a declaration `F12` lists its uses instead, as VS Code does. `Alt+F12` peeks the declaration (its lines with a line of context) in a widget under the line, with the focus in it: `Enter` goes to the definition, `Escape` closes it and returns to the editor. `Shift+F12` lists the declaration first (marked "Declaration") and then every use with its line, slide number and text: Up, Down, Home and End move, `Enter` (or a click) jumps and selects the use, `Escape` closes. `F2` opens an input at the symbol, filled with the id and selected; the name is checked as you type (not empty, the schema's pattern for that kind of id, no collision with another id of the same kind) and the message appears in the widget and is announced; `Enter` renames the declaration and every use in one transaction, so one undo restores them all, and keeps the selection on the renamed symbol. Renamable: assets, variables (a `{{name|format}}` keeps its format), datasets, references, slide ids and the ids of records embedded in `catalogs` (with the uses of a `name:id` reference of that group). Not renamable, with the reason said: a record only the app provides, a built-in variable, a dataset column, a beat or a region (they are named in their record), an id nothing declares, and anything in a read-only editor. Every command announces what it did in the live region; the widgets are named (`role`, `aria-label`), operable with the keyboard alone and return the focus to the editor; the declaration is marked with the word and an arrow, not by color alone. The widgets move with the edits above them. The work is one read of the text per edit (a big YAML text is read in pieces with the main thread given back between them): on a 1000-slide deck each command answers well under 100 ms (10 to 30 ms measured) and never holds the main thread for 50 ms.
|
|
126
132
|
- **Format document.** `Shift+Alt+F` (and `Cmd/Ctrl+Shift+F`) and the Format button do the same in JSON, YAML and Markdown, and change whitespace only, as one undo step with the cursor kept on its token. JSON is laid out token by token with the document's indentation, also while it does not parse; YAML keeps its comments, blank lines, quote styles, number spellings and key order (the pane says so and does nothing when the YAML has errors); Markdown gets at most one blank line between blocks (code fences untouched), its front matter formatted as YAML, and one newline at the end. A format that would change the document is refused. The pane does not rewrite the text from the document (`toYaml`, `toMarkdown`) any more, so it no longer drops comments.
|
|
127
133
|
- **Canonicalize with OPF.** The old behavior of Format (the text rewritten from the document with `toYaml` or `toMarkdown`, in the order and style OPF writes) is its own button in YAML and Markdown, `pane.canonicalize()`. It drops comments, so when there are any it asks first, names how many (YAML `#` comments; Markdown `<!-- -->` comments other than slide and block options, and the YAML comments of the front matter and fences), and waits for "Canonicalize anyway" or Cancel (`Escape`); undo brings the comments back. It is one undo step.
|
|
134
|
+
- **Checking in a Web Worker (`worker`).** Reading the text (YAML, Markdown), the quick syntax pass of JSON and validating the document take 50 to 100 ms on a thousand slides, so with `worker` the pane does that off the main thread: typing in a 1000-slide deck never blocks the page for 50 ms (the budget is tested in Chromium and WebKit). The worker runs the same functions as the pane's own path (`source-check.js`) and hands back the deck read, the findings and the validation the session would compute, which the session accepts through `editor.applyPatch(operations, meta, { validated })` only for exactly that document and its current `catalogs` (it never lands in the undo history). The host bundles the worker script, `@openpresentation/opf-editor/source-worker` (an ES module with no exports; bundlers such as Vite, webpack and Parcel build it with `new Worker(new URL('@openpresentation/opf-editor/source-worker', import.meta.url), { type: 'module' })`; otherwise build `dist/source-worker.js` as its own entry and serve it next to the page), and passes `worker: () => new Worker(...)` (a function, a `Worker`, or `true` to try `new Worker(new URL('./source-worker.js', import.meta.url), { type: 'module' })` beside the pane's module). Without `worker` (the default) nothing changes: everything runs on the main thread. Only the typing delay (`applyDelay`) uses the worker; `getText()`, switching the format, `replaceDocument`, Format, Undo and `destroy` apply a pending edit on the main thread at once, as before. If the worker cannot start (a CSP, a wrong URL, a bundler), cannot clone a message, throws or stops answering, the pane checks on the main thread from then on, `onError` is called once, and no edit is lost. Results are identical either way (a test compares the problems, squiggles, document and validation for the same texts). `pane.whenIdle()` resolves when no edit waits for its delay and no check runs; `pane.checkerStats()` is `{ worker, inline, available }` (how the applied edits were checked); `data-opf-checking` is on the pane's root while a worker check is running. While you type faster than the worker answers, older answers are dropped and the newest text is applied when you pause.
|
|
128
135
|
- **Accessibility.** One polite live region (`.opf-source-live`) says how many suggestions there are when the list opens, the selected item and the first sentence of its documentation as you arrow (after a short pause), `Inserted design` when you accept, `Tab stop 2 of 3: ...` in a snippet, the problem when the caret enters a squiggle or `F8` reaches it, and `2 problems: 1 error, 1 warning` once the problems stop changing for 600 ms; hosts get every message through the `announce` option. Every suggestion has an accessible name with its kind and its place (`design, property, Design. 1 of 3`), the editor's `aria-description` says how to open the list and how to leave the editor (`Escape` then `Tab`, or `Ctrl+M`; `Ctrl+Shift+M` on macOS), and the problem rows start with the severity as text as well as a shape. The editor's `aria-invalid` is `true` while the findings include an error and `false` otherwise. The format button (`aria-label="Source format"`) opens a `role="menu"` of `menuitemradio` items: Arrow keys, Home, End, Enter, Escape (focus returns to the button) and Tab. The file name is a button that becomes a text input; Enter or Escape commits. Controls show a 2px `--opf-ui-ring` focus outline.
|
|
129
136
|
- **Styles.** `src/styles/source-pane.css` uses only `--opf-ui-*` tokens under the `opf-source-` prefix. It ships in the editor's `styles.css` (`@openpresentation/opf-editor/styles.css`), which also defines the tokens.
|
|
130
137
|
- **Dependency.** `yaml` (ISC; core already depends on it) formats YAML while keeping its comments. `@codemirror/lang-yaml` (MIT) highlights YAML; Markdown uses a small stream tokenizer built on `@codemirror/language`, so no Markdown, HTML, CSS or JavaScript grammar is added.
|
package/dist/index.d.ts
CHANGED
|
@@ -71,6 +71,16 @@ export interface EditorEvent {
|
|
|
71
71
|
export interface EditorHistoryEntry { patches: JsonPatchOperation[]; inversePatches: JsonPatchOperation[]; meta?: Record<string, unknown> }
|
|
72
72
|
export interface EditorHistory { undo: EditorHistoryEntry[]; redo: EditorHistoryEntry[] }
|
|
73
73
|
|
|
74
|
+
/** A validation computed ahead of an `applyPatch`: the report for `document` under `catalogs` (core's `validate` for the editor's categories, `checkFormat`). */
|
|
75
|
+
export interface PrecomputedValidation {
|
|
76
|
+
document: unknown;
|
|
77
|
+
validation: ValidationReport;
|
|
78
|
+
catalogs: readonly Catalog[];
|
|
79
|
+
}
|
|
80
|
+
export interface ApplyPatchOptions {
|
|
81
|
+
validated?: PrecomputedValidation;
|
|
82
|
+
}
|
|
83
|
+
|
|
74
84
|
export interface EditorSession {
|
|
75
85
|
paginateSlide(slideIndex: number, options?: PaginationOptions & EditorDiagnosticOptions, meta?: Record<string, unknown>): { change: EditorChange | null; pagination: PaginationResult };
|
|
76
86
|
/** Composes the slide with `{{slide.number}}`, `{{slide.section}}` and `{{deck.slideCount}}` substituted for `options.slideNumber` and `options.slideCount` (default: its place in the open deck and the deck's slide count); the document keeps the tokens. */
|
|
@@ -91,7 +101,12 @@ export interface EditorSession {
|
|
|
91
101
|
set(path: string | string[], value: unknown, meta?: Record<string, unknown>): EditorChange;
|
|
92
102
|
setGroupComposition(path: string, composition: Composition, meta?: Record<string, unknown>): EditorChange;
|
|
93
103
|
setCatalog(path: string | string[], catalogKind: string, id: string, meta?: Record<string, unknown>): EditorChange;
|
|
94
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Apply JSON Patch operations as one undoable change. `options.validated` hands in a validation the caller already computed off the main
|
|
106
|
+
* thread (the source pane's worker): it is used only when `document` equals the patched document exactly and `catalogs` is the session's
|
|
107
|
+
* current `catalogs` list (the same object); otherwise the session validates the document itself. It is never stored in `meta` or the history.
|
|
108
|
+
*/
|
|
109
|
+
applyPatch(operations: JsonPatchOperation[], meta?: Record<string, unknown>, options?: ApplyPatchOptions): EditorChange;
|
|
95
110
|
/**
|
|
96
111
|
* Open a different document in the session: the whole document is replaced as one undoable change, and the replacement is never
|
|
97
112
|
* read as an edit of the catalog records the two documents share, so nothing under `catalogs.default` or a named group is forked
|
package/dist/index.js
CHANGED
|
@@ -165,7 +165,14 @@ export function createEditorSession(input, options = {}) {
|
|
|
165
165
|
for (const listener of listeners) listener({ ...event, snapshot });
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
|
|
168
|
+
/**
|
|
169
|
+
* `validated`: a report the caller already computed for the document the patches produce (`{ document, validation, catalogs }`, see `applyPatch`).
|
|
170
|
+
* It stands in for `check(next)` only when it is for exactly this document and the session's current catalogs; otherwise the session checks itself.
|
|
171
|
+
*/
|
|
172
|
+
const validationFor = (validated, document) =>
|
|
173
|
+
validated && validated.validation && validated.catalogs === catalogs && jsonEqual(validated.document, document) ? validated.validation : check(document);
|
|
174
|
+
|
|
175
|
+
function commitPatch(operations, meta = {}, applyOptions = {}) {
|
|
169
176
|
assertPatchOperations(operations);
|
|
170
177
|
let patches = operations.map(normalizeOperation);
|
|
171
178
|
const before = presentation;
|
|
@@ -192,7 +199,7 @@ export function createEditorSession(input, options = {}) {
|
|
|
192
199
|
meta = { ...meta, forked: forks, notice: forks.map(forkNotice).join(" ") };
|
|
193
200
|
}
|
|
194
201
|
}
|
|
195
|
-
const nextValidation =
|
|
202
|
+
const nextValidation = validationFor(applyOptions?.validated, next);
|
|
196
203
|
|
|
197
204
|
if ((meta.rejectInvalid ?? rejectInvalid) && !nextValidation.valid) {
|
|
198
205
|
throw new OPFEditorError("invalid-opf-edit", "OPF edit produced an invalid document.", {
|
package/dist/json-editor.d.ts
CHANGED
|
@@ -55,6 +55,8 @@ export interface JsonEditorApi {
|
|
|
55
55
|
format(): boolean;
|
|
56
56
|
}
|
|
57
57
|
export interface JsonCodeEditorOptions {
|
|
58
|
+
/** Called with the shortcuts list when the dialog is about to open; returning `true` means the host shows its own. */
|
|
59
|
+
onShowShortcuts?(shortcuts: import("./source-pane.js").Shortcut[]): boolean | void;
|
|
58
60
|
code: string;
|
|
59
61
|
label: string;
|
|
60
62
|
onChange(source: string): void;
|
|
@@ -77,6 +79,11 @@ export interface JsonCodeEditorOptions {
|
|
|
77
79
|
completions?: boolean;
|
|
78
80
|
/** Enter continues a list the way the text writes it: a new string item after a string in a JSON list, the next `- ` in YAML, the next marker in Markdown. Default true; false keeps only the indentation. */
|
|
79
81
|
smartListEnter?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* A preview of a catalog record for the card that F12 and Alt+F12 open on a layout, theme or other record only the app provides: an element, an `https:` or
|
|
84
|
+
* `data:image/` address, or a promise of either. Optional; the card shows the name, the description and the regions without it.
|
|
85
|
+
*/
|
|
86
|
+
resolvePreview?: (kind: string, id: string) => HTMLElement | string | undefined | Promise<HTMLElement | string | undefined>;
|
|
80
87
|
catalogs?: JsonCatalogContext;
|
|
81
88
|
/** Overrides the built-in menu, allowing an existing host UI to keep its appearance. */
|
|
82
89
|
onOptions?(left?: number, top?: number): void;
|
|
@@ -93,6 +100,11 @@ export interface JsonCodeEditorOptions {
|
|
|
93
100
|
onFormat?(): void;
|
|
94
101
|
/** Hover documentation over keys and catalog ids in JSON, and Cmd/Ctrl+K Cmd/Ctrl+I (default true). */
|
|
95
102
|
hover?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* The shape hint: Cmd/Ctrl+Shift+Space shows the fields of the object around the caret, and typing `{` or `,` between the members of a slide that has a
|
|
105
|
+
* layout shows it by itself (default true; false turns the automatic one off, the key stays).
|
|
106
|
+
*/
|
|
107
|
+
hints?: boolean;
|
|
96
108
|
/** Called with every message the live region says (hosts mirror them into a region of their own). */
|
|
97
109
|
announce?(message: string): void;
|
|
98
110
|
/**
|
|
@@ -125,6 +137,14 @@ export interface JsonCodeEditor {
|
|
|
125
137
|
triggerCompletion(): boolean;
|
|
126
138
|
/** Whether a suggestion list is open or being asked for. */
|
|
127
139
|
isCompleting(): boolean;
|
|
140
|
+
/** Every key binding this editor installed, for `platform` (default: the page's): the keyboard shortcuts list. */
|
|
141
|
+
getShortcuts(platform?: "mac" | "win" | "linux"): import("./source-pane.js").Shortcut[];
|
|
142
|
+
/** Bindings installed without a title in the list (empty unless a binding was added without a place in source-keymap.js). */
|
|
143
|
+
unlistedShortcuts(): string[];
|
|
144
|
+
/** Open the keyboard shortcuts dialog (F1, Cmd/Ctrl+K then Cmd/Ctrl+S). */
|
|
145
|
+
showShortcuts(): boolean;
|
|
146
|
+
/** Open the symbol picker (Cmd/Ctrl+Shift+O). */
|
|
147
|
+
showSymbols(): boolean;
|
|
128
148
|
/** Perform a quick fix. */
|
|
129
149
|
applyFix(fix: EditorFix, problem?: EditorProblem): void;
|
|
130
150
|
setLanguage(language: "json" | "yaml" | "markdown"): void;
|
package/dist/json-editor.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { Annotation, ChangeSet, Compartment, EditorSelection, EditorState, Prec, StateEffect, StateField, Transaction, countColumn } from "@codemirror/state";
|
|
2
2
|
import { Decoration, EditorView, activateHover, closeHoverTooltips, crosshairCursor, drawSelection, hasHoverTooltips, hoverTooltip, keymap, lineNumbers, rectangularSelection } from "@codemirror/view";
|
|
3
|
-
import { defaultKeymap, deleteCharBackward,
|
|
4
|
-
import { HighlightStyle, StreamLanguage, bracketMatching, codeFolding,
|
|
3
|
+
import { defaultKeymap, deleteCharBackward, history, historyKeymap, indentLess, indentMore, insertNewlineAndIndent, isolateHistory, redo, selectAll, selectLine, selectParentSyntax, toggleTabFocusMode, undo, undoDepth } from "@codemirror/commands";
|
|
4
|
+
import { HighlightStyle, StreamLanguage, bracketMatching, codeFolding, indentOnInput, indentUnit, syntaxHighlighting } from "@codemirror/language";
|
|
5
5
|
import { json } from "@codemirror/lang-json";
|
|
6
6
|
import { yaml } from "@codemirror/lang-yaml";
|
|
7
|
-
import { acceptCompletion, autocompletion, closeBrackets, closeBracketsKeymap,
|
|
8
|
-
import { highlightSelectionMatches, search, searchKeymap } from "@codemirror/search";
|
|
7
|
+
import { acceptCompletion, autocompletion, closeBrackets, closeBracketsKeymap, closeCompletion, completionStatus, currentCompletions, deleteBracketPair, hasNextSnippetField, hasPrevSnippetField, moveCompletionSelection, nextSnippetField, prevSnippetField, startCompletion } from "@codemirror/autocomplete";
|
|
8
|
+
import { closeSearchPanel, gotoLine, highlightSelectionMatches, openSearchPanel, search, searchKeymap } from "@codemirror/search";
|
|
9
9
|
import { tags } from "@lezer/highlight";
|
|
10
10
|
import { findNodeAtOffset, getNodePath, parseTree } from "jsonc-parser";
|
|
11
11
|
import {jsonSource,jsonSourceHistory,setJsonSource} from './json-source.js';
|
|
12
12
|
import {textInputMap} from './text-input.js';
|
|
13
13
|
import {detectIndent,formatJsonSource,whitespaceEdits} from './json-format.js';
|
|
14
|
+
import { redoPair, undoPair } from "./source-undo-groups.js";
|
|
14
15
|
import {a11yExtension,createAnnouncer} from './source-a11y.js';
|
|
15
16
|
import { forEachDiagnostic } from "@codemirror/lint";
|
|
16
17
|
import {createDiagnostics,editsTransaction} from './source-diagnostics.js';
|
|
18
|
+
import { createKeymap, domKeymap, strokeText } from './source-keymap.js';
|
|
19
|
+
import { fold, foldEverything, foldGutterExtension, foldLevel, foldRecursively, toggleFoldHere, unfold, unfoldEverything, unfoldRecursively } from './source-folding.js';
|
|
17
20
|
import {languageDataFor,markdownBackspace,markdownEnter,markdownFolding,markdownTab,markdownToggleComment,markdownWrapSelection,yamlEnter} from './source-editing.js';
|
|
18
21
|
const external = Annotation.define();
|
|
19
22
|
const hoverField = StateEffect.define();
|
|
@@ -147,27 +150,30 @@ function backspaceInToken(tokenOf) {
|
|
|
147
150
|
// accepted item always replaces the whole token), Escape closes the list, Ctrl+Space (and Cmd+I on macOS) opens it wherever the schema has
|
|
148
151
|
// something to offer and, with the list open, shows or hides its documentation, and the arrows, the Page keys and Ctrl+N and Ctrl+P (macOS)
|
|
149
152
|
// move the selection. Without a list, none of them is taken: Tab then inserts whitespace (or walks the stops of a snippet) and Enter inserts
|
|
150
|
-
// a line.
|
|
151
|
-
function completionKeys(isReady, tokenOf, onStep, { newline, tab }) {
|
|
153
|
+
// a line. The keys come from the table in source-keymap.js.
|
|
154
|
+
function completionKeys(km, isReady, tokenOf, onStep, { newline, tab }) {
|
|
152
155
|
// Tab and Shift+Tab walk the tab stops of a snippet; the engine says which stop it is and opens the list at a stop that is a choice.
|
|
153
156
|
const walk = (forward, fallback) => (view) => {
|
|
154
157
|
if (forward ? hasNextSnippetField(view.state) && nextSnippetField(view) : hasPrevSnippetField(view.state) && prevSnippetField(view)) { onStep(view, forward); return true; }
|
|
155
158
|
return fallback(view);
|
|
156
159
|
};
|
|
157
160
|
const enter = newline;
|
|
161
|
+
km.note("suggest.details");
|
|
162
|
+
km.note("snippet.next");
|
|
163
|
+
km.note("snippet.previous");
|
|
158
164
|
return Prec.highest([releaseHeldKey, keymap.of([
|
|
159
|
-
...
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
+
...km.bind("suggest.dismiss", closeCompletion),
|
|
166
|
+
...km.bind("suggest.next", moveCompletionSelection(true)),
|
|
167
|
+
...km.bind("suggest.previous", moveCompletionSelection(false)),
|
|
168
|
+
...km.bind("suggest.nextPage", moveCompletionSelection(true, "page")),
|
|
169
|
+
...km.bind("suggest.previousPage", moveCompletionSelection(false, "page")),
|
|
170
|
+
// Ctrl+Space opens the list, or shows and hides the documentation when it is open; Cmd+I (macOS) opens it again.
|
|
171
|
+
...km.bind("suggest.trigger", [(view) => (listIsOpen(view.state) ? toggleDocs(view) : startCompletion(view)), startCompletion]),
|
|
172
|
+
...km.bind("suggest.accept", [acceptOrFallBack(isReady, walk(true, tab)), acceptOrFallBack(isReady, enter)]),
|
|
173
|
+
...km.bind("suggest.acceptReplace", [acceptOrFallBack(isReady, walk(false, indentLess)), acceptOrFallBack(isReady, insertNewlineAndIndent)]),
|
|
165
174
|
{ key: "ArrowLeft", run: moveInToken(false, tokenOf) },
|
|
166
175
|
{ key: "ArrowRight", run: moveInToken(true, tokenOf) },
|
|
167
176
|
{ key: "Backspace", run: backspaceInToken(tokenOf) },
|
|
168
|
-
{ mac: "Mod-i", run: startCompletion },
|
|
169
|
-
{ mac: "Ctrl-n", run: moveCompletionSelection(true) },
|
|
170
|
-
{ mac: "Ctrl-p", run: moveCompletionSelection(false) },
|
|
171
177
|
])]);
|
|
172
178
|
}
|
|
173
179
|
const DEFAULT_INDENT = " ";
|
|
@@ -279,9 +285,12 @@ function skipDelimiter(view, delimiter) {
|
|
|
279
285
|
}
|
|
280
286
|
function mountJsonCodeEditor(parent, options) {
|
|
281
287
|
const attributes = new Compartment(), indent = new Compartment(), language = new Compartment(), editable = new Compartment(), historyField = new Compartment();
|
|
282
|
-
|
|
288
|
+
// A read-only text is not editable, so it takes the keyboard by tabindex: the keys that only read (Find, fold, the hover and the shape hint) work in it.
|
|
289
|
+
const editableExtension = (readOnly) => [EditorState.readOnly.of(Boolean(readOnly)), EditorView.editable.of(!readOnly), EditorView.contentAttributes.of(readOnly ? { tabindex: "0" } : {})];
|
|
283
290
|
let languageName = options.language ?? "json";
|
|
284
291
|
let disposed=false, menu=null, catalogs=options.catalogs??[], generation=0;
|
|
292
|
+
// Every key binding is declared in source-keymap.js; `km` records what this editor installs, for getShortcuts() and the shortcuts dialog.
|
|
293
|
+
const km = createKeymap();
|
|
285
294
|
// The indentation unit belongs to the document, not to the text as it is edited: JSON reads it once when a document is opened (here,
|
|
286
295
|
// and when the host replaces the text), YAML and Markdown always use two spaces. Typing never changes it.
|
|
287
296
|
let jsonIndent = detectIndent(options.code) ?? DEFAULT_INDENT;
|
|
@@ -313,7 +322,6 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
313
322
|
return optionsModule.hoverAt(target.state, position, { catalogs, format: formatOf(languageName) });
|
|
314
323
|
}, { hoverTime: 300 });
|
|
315
324
|
// Cmd/Ctrl+K Cmd/Ctrl+I shows the hover at the caret (VS Code), and says what it holds.
|
|
316
|
-
let chordStartedAt = 0;
|
|
317
325
|
const showHoverAtCaret = (target) => {
|
|
318
326
|
if (options.hover === false) return false;
|
|
319
327
|
const run = () => {
|
|
@@ -338,9 +346,8 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
338
346
|
return loadOptions().then(() => (disposed || context.aborted ? null : completionResult(context)), (error) => { options.onError?.(error); return null; });
|
|
339
347
|
}
|
|
340
348
|
// Undo and Redo run CodeMirror's history first. When it has nothing left, the host's fallback may step further (the source pane routes a whole-document replace to the session); a fallback answers true when it did something.
|
|
341
|
-
const undoCommand = (target) => undo(target) || Boolean(options.historyFallback?.undo?.());
|
|
342
|
-
const redoCommand = (target) => redo(target) || Boolean(options.historyFallback?.redo?.());
|
|
343
|
-
const historyBindings = historyKeymap.map((binding) => binding.run === undo ? { ...binding, run: undoCommand } : binding.run === redo ? { ...binding, run: redoCommand } : binding);
|
|
349
|
+
const undoCommand = (target) => undoPair(target) || undo(target) || Boolean(options.historyFallback?.undo?.());
|
|
350
|
+
const redoCommand = (target) => redoPair(target) || redo(target) || Boolean(options.historyFallback?.redo?.());
|
|
344
351
|
// Accept with no minimum delay: the list is already valid for the typed text, so Tab or Enter right after a keystroke accepts (VS Code does).
|
|
345
352
|
// Screen reader announcements and the problems drawn in the text (squiggles, F8, Cmd/Ctrl+.). The pane supplies the problems.
|
|
346
353
|
const announcer = createAnnouncer(parent, options.announce);
|
|
@@ -354,7 +361,7 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
354
361
|
say(`${fix.title} applied`);
|
|
355
362
|
}
|
|
356
363
|
};
|
|
357
|
-
const diagnostics = createDiagnostics({ getSource: () => view.state.field(jsonSource), say, applyFix: applyProblemFix });
|
|
364
|
+
const diagnostics = createDiagnostics({ getSource: () => view.state.field(jsonSource), say, applyFix: applyProblemFix, km });
|
|
358
365
|
// On its own (no source pane to supply the problems) the editor underlines the syntax errors of JSON: one per cause, 300 ms after the last key.
|
|
359
366
|
let syntaxTimer = null;
|
|
360
367
|
function scheduleSyntax() {
|
|
@@ -375,25 +382,143 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
375
382
|
const smart = options.smartListEnter !== false;
|
|
376
383
|
const newlineCommand = (target) => (languageName === "json" ? smart && nextStringItem(target) : languageName === "yaml" ? yamlEnter(target, { lists: smart }) : markdownEnter(target, { lists: smart }));
|
|
377
384
|
const tabCommandFor = (target) => (languageName === "markdown" && markdownTab(target)) || tabCommand(target);
|
|
378
|
-
const completionExtension = options.completions ? [autocompletion({ override: [completionSource], defaultKeymap: false, icons: false, maxRenderedOptions: 60, interactionDelay: 0, activateOnTypingDelay: 0 }), completionKeys(() => optionsModule !== null, (text, position) => optionsModule?.tokenFor(formatOf(languageName), text, position), (target, forward) => optionsModule?.snippetStep(target, forward, { say }), { newline: (target) => newlineCommand(target) || insertNewlineAndIndent(target), tab: (target) => tabCommandFor(target) })] : null;
|
|
385
|
+
const completionExtension = options.completions ? [autocompletion({ override: [completionSource], defaultKeymap: false, icons: false, maxRenderedOptions: 60, interactionDelay: 0, activateOnTypingDelay: 0 }), completionKeys(km, () => optionsModule !== null, (text, position) => optionsModule?.tokenFor(formatOf(languageName), text, position), (target, forward) => optionsModule?.snippetStep(target, forward, { say }), { newline: (target) => newlineCommand(target) || insertNewlineAndIndent(target), tab: (target) => tabCommandFor(target) })] : null;
|
|
386
|
+
// Format the document: the host's command (the source pane formats every language) or, standalone, the JSON formatter.
|
|
387
|
+
const runFormat = () => { if (options.onFormat) options.onFormat(); else api.format(); return true; };
|
|
388
|
+
|
|
389
|
+
// ---- Features loaded on first use (go to definition and rename, the shape hint, the symbol picker, the shortcuts dialog). Each module exports commands
|
|
390
|
+
// `(view, context) => boolean`; the context is what the editor knows.
|
|
391
|
+
const lazy = (load) => { const slot = { module: null, loading: null, load: () => (slot.loading ??= load().then((module) => (slot.module = module), (error) => { slot.loading = null; throw error; })) }; return slot; };
|
|
392
|
+
const modules = { navigation: lazy(() => import('./source-navigation.js')), hints: lazy(() => import('./source-hints.js')), outline: lazy(() => import('./source-outline.js')), shortcuts: lazy(() => import('./source-shortcuts.js')) };
|
|
393
|
+
const featureContext = () => ({ format: formatOf(languageName), catalogs, say, api, options, view, platform: km.platform, getShortcuts: (platform) => km.shortcuts(platform), readOnly: view.state.readOnly });
|
|
394
|
+
/** A command that runs `module[name](view, context)`, loading the module the first time. The key is always taken. */
|
|
395
|
+
const feature = (moduleName, name, extra) => (target) => {
|
|
396
|
+
const slot = modules[moduleName];
|
|
397
|
+
const run = () => { if (!disposed) slot.module[name]?.(target, extra ? { ...featureContext(), ...extra } : featureContext()); };
|
|
398
|
+
if (slot.module) run(); else slot.load().then(run, (error) => options.onError?.(error));
|
|
399
|
+
return true;
|
|
400
|
+
};
|
|
401
|
+
// After a `{` or a `,` was typed the shape hint may open by itself (source-hints.js decides); it runs once the update that typed the character is done.
|
|
402
|
+
const autoHint = (target) => {
|
|
403
|
+
const head = target.state.selection.main.head, typed = target.state.sliceDoc(head - 1, head);
|
|
404
|
+
if (options.hints === false || (typed !== "{" && typed !== ",")) return;
|
|
405
|
+
queueMicrotask(() => {
|
|
406
|
+
const slot = modules.hints, run = () => { if (!disposed) slot.module.autoShapeHint?.(target, featureContext(), typed); };
|
|
407
|
+
if (slot.module) run(); else slot.load().then(run, (error) => options.onError?.(error));
|
|
408
|
+
});
|
|
409
|
+
};
|
|
410
|
+
const commentCommand = (target) => {
|
|
411
|
+
if (languageName === "markdown") return markdownToggleComment(target, say);
|
|
412
|
+
// JSON has no comments: say so instead of doing nothing (YAML and Markdown comment their lines out).
|
|
413
|
+
if (languageName !== "json") return false;
|
|
414
|
+
say("JSON has no comments. Use YAML or Markdown for notes.");
|
|
415
|
+
return true;
|
|
416
|
+
};
|
|
417
|
+
// Insert line above (Cmd/Ctrl+Shift+Enter): a new line with the indentation of this one, above it, with the caret on it.
|
|
418
|
+
const insertLineAbove = (target) => {
|
|
419
|
+
const { state } = target;
|
|
420
|
+
if (state.readOnly) return false;
|
|
421
|
+
const update = state.changeByRange((range) => {
|
|
422
|
+
const line = state.doc.lineAt(range.from), indent = /^\s*/.exec(line.text)[0];
|
|
423
|
+
return { changes: { from: line.from, insert: `${indent}\n` }, range: EditorSelection.cursor(line.from + indent.length) };
|
|
424
|
+
});
|
|
425
|
+
target.dispatch(state.update(update, { scrollIntoView: true, userEvent: "input" }));
|
|
426
|
+
return true;
|
|
427
|
+
};
|
|
428
|
+
// Expand selection (word, then each enclosing syntax node) and Shrink selection (back, one step at a time) as in VS Code.
|
|
429
|
+
const expansions = new WeakMap();
|
|
430
|
+
const expandSelection = (target) => {
|
|
431
|
+
const before = target.state.selection, main = before.main;
|
|
432
|
+
const word = main.empty ? target.state.wordAt(main.head) : null;
|
|
433
|
+
if (word) target.dispatch({ selection: EditorSelection.single(word.from, word.to), scrollIntoView: true, userEvent: "select.expand" });
|
|
434
|
+
else selectParentSyntax(target);
|
|
435
|
+
if (target.state.selection.eq(before)) return true;
|
|
436
|
+
const history = expansions.get(target) ?? [];
|
|
437
|
+
expansions.set(target, [...(history.length && history.at(-1).after.eq(before) ? history : []), { before, after: target.state.selection }]);
|
|
438
|
+
return true;
|
|
439
|
+
};
|
|
440
|
+
const shrinkSelection = (target) => {
|
|
441
|
+
const history = expansions.get(target);
|
|
442
|
+
const last = history?.at(-1);
|
|
443
|
+
if (!last || !last.after.eq(target.state.selection)) { say("Nothing to shrink"); return true; }
|
|
444
|
+
history.pop();
|
|
445
|
+
target.dispatch({ selection: last.before, scrollIntoView: true, userEvent: "select.shrink" });
|
|
446
|
+
return true;
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// The keys VS Code reads from the event, so the physical key counts where Alt changes the character (macOS) and a lowercase letter is accepted
|
|
450
|
+
// with Shift held (some platforms report it): redo, the problems panel, Format, the field menu, the symbol picker and the chords of Cmd/Ctrl+K.
|
|
451
|
+
// A lowercase "z" with Shift can match CodeMirror's unshifted undo binding first, so redo resolves before it, even with older undo steps still available.
|
|
452
|
+
km.bindDom("edit.redo", (target) => { redoCommand(target); return true; });
|
|
453
|
+
km.bindDom("problem.panel", () => Boolean(options.onFocusProblems?.()));
|
|
454
|
+
km.bindDom("hover.show", (target) => showHoverAtCaret(target));
|
|
455
|
+
km.bindDom("format.document", () => runFormat());
|
|
456
|
+
km.bindDom("edit.fieldMenu", () => { if (languageName !== "json") return false; void openOptions(); return true; });
|
|
457
|
+
// Cmd/Ctrl+L: read from the event because a platform that reports Ctrl+Shift+L as a lowercase "l" would otherwise run it instead of Select All Occurrences.
|
|
458
|
+
km.bindDom("select.line", (target) => selectLine(target));
|
|
459
|
+
km.bindDom("nav.symbol", feature("outline", "showSymbolPicker"));
|
|
460
|
+
km.bindDom("help.shortcuts", feature("shortcuts", "showShortcuts"));
|
|
461
|
+
const saying = (run) => (target) => run(target, say);
|
|
462
|
+
km.bindDom("fold.toggle", saying(toggleFoldHere));
|
|
463
|
+
km.bindDom("fold.foldRecursively", saying(foldRecursively));
|
|
464
|
+
km.bindDom("fold.unfoldRecursively", saying(unfoldRecursively));
|
|
465
|
+
km.bindDom("fold.all", saying(foldEverything));
|
|
466
|
+
km.bindDom("fold.unfoldAll", saying(unfoldEverything));
|
|
467
|
+
for (let level = 1; level <= 7; level++) km.bindDom(`fold.level${level}`, saying(foldLevel(level)));
|
|
468
|
+
const keydownKeys = Prec.high(domKeymap(km, say));
|
|
469
|
+
|
|
470
|
+
// CodeMirror's own bindings, kept as they are except where the table gives a key to something else: the expand-selection key is ours, Ctrl+G goes to a
|
|
471
|
+
// line (Find next is F3 and, on macOS, Cmd+G), and Fold All and Unfold All are chords. Undo and Redo run CodeMirror's history, then the host's.
|
|
472
|
+
const kept = {
|
|
473
|
+
basic: defaultKeymap.filter((binding) => binding.run !== selectParentSyntax && binding.run !== selectLine),
|
|
474
|
+
history: historyKeymap.filter((binding) => binding.run !== undo && binding.run !== redo),
|
|
475
|
+
// Find next on Cmd+G is macOS only; elsewhere F3 (Ctrl+G is Go to line).
|
|
476
|
+
search: searchKeymap.filter((binding) => binding.run !== gotoLine).map((binding) => (binding.key === "Mod-g" ? { mac: "Mod-g", run: binding.run, shift: binding.shift, scope: binding.scope } : binding)),
|
|
477
|
+
};
|
|
478
|
+
const mainKeymap = keymap.of([
|
|
479
|
+
...km.bind("edit.enter", (target) => newlineCommand(target)),
|
|
480
|
+
...km.hidden([
|
|
481
|
+
{ key: '"', run: (view2) => languageName === "json" && skipDelimiter(view2, '"') },
|
|
482
|
+
{ key: ",", run: (view2) => languageName === "json" && skipDelimiter(view2, ",") },
|
|
483
|
+
{ key: "Backspace", run: (target) => languageName === "markdown" && markdownBackspace(target) },
|
|
484
|
+
// The field menu (a searchable dialog of the choices of one field) is on Alt+Shift+O, a chord VS Code does not use. With completions off
|
|
485
|
+
// it is also on Ctrl+Space, because the list is not there to take that key.
|
|
486
|
+
...(options.completions ? [] : [{ key: "Ctrl-Space", run: () => languageName === "json" && (void openOptions(), true) }, { key: "Mod-Space", run: () => languageName === "json" && (void openOptions(), true) }]),
|
|
487
|
+
]),
|
|
488
|
+
...km.bind("edit.tab", (target) => tabCommandFor(target)),
|
|
489
|
+
...km.bind("edit.shiftTab", indentLess),
|
|
490
|
+
...km.bind("lines.comment", commentCommand),
|
|
491
|
+
...km.bind("lines.insertAbove", insertLineAbove),
|
|
492
|
+
...km.bind("focus.toggleTab", toggleTabFocusMode),
|
|
493
|
+
...km.bind("nav.line", gotoLine),
|
|
494
|
+
...km.bind("select.expand", expandSelection),
|
|
495
|
+
...km.bind("select.shrink", shrinkSelection),
|
|
496
|
+
...km.bind("find.replace", openSearchPanel),
|
|
497
|
+
...km.bind("fold.fold", saying(fold)),
|
|
498
|
+
...km.bind("fold.unfold", saying(unfold)),
|
|
499
|
+
...km.bind("edit.undo", undoCommand),
|
|
500
|
+
...km.bind("hint.shape", feature("hints", "showShapeHint")),
|
|
501
|
+
...(km.note("hint.close"), []),
|
|
502
|
+
...km.bind("nav.definition", feature("navigation", "goToDefinition")),
|
|
503
|
+
...km.bind("nav.peek", feature("navigation", "peekDefinition")),
|
|
504
|
+
...km.bind("nav.references", feature("navigation", "findReferences")),
|
|
505
|
+
...km.bind("nav.rename", feature("navigation", "renameSymbol")),
|
|
506
|
+
...km.hidden(closeBracketsKeymap),
|
|
507
|
+
...km.spread(kept.basic),
|
|
508
|
+
...km.spread(kept.history),
|
|
509
|
+
...km.spread(kept.search),
|
|
510
|
+
]);
|
|
511
|
+
const shortcutAttribute = (ids) => ids.flatMap((id) => km.shortcuts().find((row) => row.id === id)?.strokes.slice(0, 1) ?? []).map((stroke) => strokeText(stroke, km.platform).replace(/Cmd/g, "Meta").replace(/Ctrl/g, "Control")).join(" ");
|
|
379
512
|
const contentAttributes = (invalid = options.invalid, describedBy = options.describedBy) => EditorView.contentAttributes.of({
|
|
380
513
|
"aria-label": options.label,
|
|
381
514
|
"aria-invalid": String(Boolean(invalid)),
|
|
382
515
|
"aria-describedby": describedBy ?? "",
|
|
383
|
-
"aria-keyshortcuts":
|
|
384
|
-
"aria-description": options.completions ?
|
|
516
|
+
"aria-keyshortcuts": shortcutAttribute(["suggest.trigger", "problem.next", "problem.previous", "problem.quickFix", "format.document", "nav.symbol"]),
|
|
517
|
+
"aria-description": options.completions ? `Press ${strokeText("Ctrl-Space", km.platform).replace("Ctrl", "Control")} for suggestions. Press F8 for the next problem. Press ${strokeText("Mod-k Mod-s", km.platform).replace(/Cmd/g, "Command").replace(/Ctrl/g, "Control")} for the keyboard shortcuts. Press Escape then Tab to leave the editor.` : "Press Escape then Tab to leave the editor.",
|
|
385
518
|
spellcheck: "false",
|
|
386
519
|
autocapitalize: "off",
|
|
387
520
|
autocorrect: "off"
|
|
388
521
|
});
|
|
389
|
-
// Format the document: the host's command (the source pane formats every language) or, standalone, the JSON formatter.
|
|
390
|
-
const runFormat = () => { if (options.onFormat) options.onFormat(); else api.format(); return true; };
|
|
391
|
-
// These chords are read from the event, not from CodeMirror's keymap: Alt changes the character a key produces on macOS, so the physical key counts.
|
|
392
|
-
const letter = (event, name) => event.code === `Key${name.toUpperCase()}` || event.key.toLowerCase() === name;
|
|
393
|
-
// Cmd/Ctrl+Shift+F (kept from earlier versions) and VS Code's Shift+Alt+F.
|
|
394
|
-
const isFormatKey = (event) => event.shiftKey && !((event.metaKey || event.ctrlKey) && event.altKey) && (event.metaKey || event.ctrlKey || event.altKey) && letter(event, "f");
|
|
395
|
-
// The field menu: Alt+Shift+O, a chord VS Code does not use.
|
|
396
|
-
const isOptionsKey = (event) => event.altKey && event.shiftKey && !event.metaKey && !event.ctrlKey && letter(event, "o");
|
|
397
522
|
const view = new EditorView({ parent, state: EditorState.create({ doc: options.code, extensions: [
|
|
398
523
|
// The problem menu's keys come before the list's: the two are never open together, and Enter must reach whichever is.
|
|
399
524
|
diagnostics.extension,
|
|
@@ -409,14 +534,16 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
409
534
|
indentOnInput(),
|
|
410
535
|
historyField.of(history()),
|
|
411
536
|
drawSelection(),
|
|
412
|
-
// Multiple cursors, as in VS Code: Cmd/Ctrl+D, Cmd/Ctrl+Shift+L, Cmd/Ctrl+Alt+Up and Down, Alt+click
|
|
537
|
+
// Multiple cursors, as in VS Code: Cmd/Ctrl+D, Cmd/Ctrl+Shift+L, Cmd/Ctrl+Alt+Up and Down, Alt+click, and Shift+Alt+drag for a column.
|
|
413
538
|
EditorState.allowMultipleSelections.of(true),
|
|
414
|
-
|
|
539
|
+
// Alt+click adds a cursor (VS Code's default); Cmd/Ctrl+click goes to the definition.
|
|
540
|
+
EditorView.clickAddsSelectionRange.of((event) => event.altKey),
|
|
415
541
|
rectangularSelection({ eventFilter: (event) => event.altKey && event.shiftKey && event.button === 0 }),
|
|
416
542
|
crosshairCursor(),
|
|
417
543
|
bracketMatching(),
|
|
418
544
|
closeBrackets(),
|
|
419
545
|
codeFolding(),
|
|
546
|
+
options.fold === false ? [] : foldGutterExtension,
|
|
420
547
|
search({ top: true }),
|
|
421
548
|
highlightSelectionMatches(),
|
|
422
549
|
syntaxHighlighting(highlightStyle),
|
|
@@ -424,63 +551,8 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
424
551
|
attributes.of(contentAttributes()),
|
|
425
552
|
options.lineNumbers ? lineNumbers() : [],
|
|
426
553
|
EditorView.lineWrapping,
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
Prec.high(EditorView.domEventHandlers({ keydown(event) {
|
|
430
|
-
// A lowercase "z" with Shift can match CodeMirror's unshifted undo
|
|
431
|
-
// binding first. Resolve redo before that fallback, even with older
|
|
432
|
-
// undo steps still available (otherwise it silently performs undo).
|
|
433
|
-
if ((event.metaKey || event.ctrlKey) && event.shiftKey && !event.altKey && event.key.toLowerCase() === "z") {
|
|
434
|
-
event.preventDefault();
|
|
435
|
-
redoCommand(view);
|
|
436
|
-
return true;
|
|
437
|
-
}
|
|
438
|
-
// Cmd/Ctrl+K then Cmd/Ctrl+I (within two seconds) shows the hover at the caret. It is read here, not as a CodeMirror chord, because a chord
|
|
439
|
-
// that starts with Mod+K would also swallow Mod+Shift+K (delete line).
|
|
440
|
-
const mod = /Mac|iPhone|iPad/.test(navigator.platform) ? event.metaKey : event.ctrlKey;
|
|
441
|
-
if (mod && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "k") { chordStartedAt = Date.now(); event.preventDefault(); return true; }
|
|
442
|
-
else if (mod && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "i" && Date.now() - chordStartedAt < 2000) {
|
|
443
|
-
chordStartedAt = 0;
|
|
444
|
-
if (showHoverAtCaret(view)) { event.preventDefault(); return true; }
|
|
445
|
-
}
|
|
446
|
-
// The problems panel: Cmd+Shift+M on macOS, Ctrl+Shift+M elsewhere (read from the physical key, as the format chord is).
|
|
447
|
-
if (mod && event.shiftKey && !event.altKey && event.code === "KeyM" && options.onFocusProblems?.()) { event.preventDefault(); return true; }
|
|
448
|
-
if (isFormatKey(event)) {
|
|
449
|
-
event.preventDefault();
|
|
450
|
-
runFormat();
|
|
451
|
-
return true;
|
|
452
|
-
}
|
|
453
|
-
if (languageName === "json" && isOptionsKey(event)) {
|
|
454
|
-
event.preventDefault();
|
|
455
|
-
void openOptions();
|
|
456
|
-
return true;
|
|
457
|
-
}
|
|
458
|
-
return false;
|
|
459
|
-
} })),
|
|
460
|
-
keymap.of([
|
|
461
|
-
{ key: "Enter", run: (target) => newlineCommand(target) },
|
|
462
|
-
{ key: '"', run: (view2) => languageName === "json" && skipDelimiter(view2, '"') },
|
|
463
|
-
{ key: ",", run: (view2) => languageName === "json" && skipDelimiter(view2, ",") },
|
|
464
|
-
{ key: "Backspace", run: (target) => languageName === "markdown" && markdownBackspace(target) },
|
|
465
|
-
// The field menu (a searchable dialog of the choices of one field) is on Alt+Shift+O, a chord VS Code does not use. With completions off
|
|
466
|
-
// it is also on Ctrl+Space, because the list is not there to take that key.
|
|
467
|
-
...(options.completions ? [] : [{ key: "Ctrl-Space", run: () => languageName === "json" && (void openOptions(), true) }, { key: "Mod-Space", run: () => languageName === "json" && (void openOptions(), true) }]),
|
|
468
|
-
{ key: "Tab", run: (target) => tabCommandFor(target), shift: indentLess },
|
|
469
|
-
// The problems panel (VS Code: Cmd+Shift+M on macOS, Ctrl+Shift+M elsewhere); Ctrl+M (Ctrl+Shift+M on macOS) is VS Code's switch for Tab leaving the editor.
|
|
470
|
-
// JSON has no comments: say so instead of doing nothing (YAML and Markdown comment their lines out).
|
|
471
|
-
{ key: "Mod-/", run: (target) => {
|
|
472
|
-
if (languageName === "markdown") return markdownToggleComment(target, say);
|
|
473
|
-
if (languageName !== "json") return false;
|
|
474
|
-
say("JSON has no comments. Use YAML or Markdown for notes.");
|
|
475
|
-
return true;
|
|
476
|
-
} },
|
|
477
|
-
{ key: "Ctrl-m", mac: "Ctrl-Shift-m", run: toggleTabFocusMode },
|
|
478
|
-
...closeBracketsKeymap,
|
|
479
|
-
...defaultKeymap,
|
|
480
|
-
...historyBindings,
|
|
481
|
-
...searchKeymap,
|
|
482
|
-
...foldKeymap
|
|
483
|
-
]),
|
|
554
|
+
keydownKeys,
|
|
555
|
+
mainKeymap,
|
|
484
556
|
EditorView.domEventHandlers({
|
|
485
557
|
paste(event){
|
|
486
558
|
if(!event.clipboardData||view.state.selection.ranges.length!==1)return false;
|
|
@@ -490,8 +562,21 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
490
562
|
},
|
|
491
563
|
focus() {
|
|
492
564
|
if (options.completions) void loadOptions().catch(() => {});
|
|
565
|
+
if (options.hints !== false) void modules.hints.load().catch(() => {});
|
|
493
566
|
options.onFocus?.();
|
|
494
567
|
},
|
|
568
|
+
mousedown(event, target) {
|
|
569
|
+
// Cmd/Ctrl+click on an id goes to its definition (a click anywhere else is a plain click).
|
|
570
|
+
const mod = km.platform === "mac" ? event.metaKey : event.ctrlKey;
|
|
571
|
+
if (!mod || event.altKey || event.shiftKey || event.button !== 0 || event.detail > 1) return false;
|
|
572
|
+
const position = target.posAtCoords({ x: event.clientX, y: event.clientY });
|
|
573
|
+
if (position === null) return false;
|
|
574
|
+
event.preventDefault();
|
|
575
|
+
target.dispatch({ selection: { anchor: position } });
|
|
576
|
+
target.focus();
|
|
577
|
+
feature("navigation", "goToDefinition", { pointer: true })(target);
|
|
578
|
+
return true;
|
|
579
|
+
},
|
|
495
580
|
mousemove(event) {
|
|
496
581
|
options.onHoverField?.(event.target.closest("[data-field]")?.dataset.field ?? null);
|
|
497
582
|
},
|
|
@@ -506,6 +591,8 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
506
591
|
if (busy !== (dom.dataset.opfBusy === "1")) { if (busy) dom.dataset.opfBusy = "1"; else delete dom.dataset.opfBusy; }
|
|
507
592
|
if (update.docChanged && !update.transactions.every((tr) => tr.annotation(external))) options.onChange(update.state.field(jsonSource));
|
|
508
593
|
if (update.docChanged) scheduleSyntax();
|
|
594
|
+
if (update.selectionSet || update.docChanged) options.onSelectionChange?.();
|
|
595
|
+
if (update.docChanged && update.transactions.some((tr) => tr.isUserEvent("input.type"))) autoHint(update.view);
|
|
509
596
|
})
|
|
510
597
|
] }) });
|
|
511
598
|
const api = {
|
|
@@ -589,6 +676,17 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
589
676
|
triggerQuickFix:()=>diagnostics.openMenu(view),
|
|
590
677
|
/** Open the suggestion list at the caret (Ctrl+Space). */
|
|
591
678
|
triggerCompletion:()=>startCompletion(view),
|
|
679
|
+
/**
|
|
680
|
+
* Every key binding this editor installed, for `platform` ("mac", "win" or "linux"; default: the page's): `{ id, title, group, keys, symbols, strokes,
|
|
681
|
+
* when? }` in the order of the shortcuts list. The same table installs the keys, so the list is the keymap.
|
|
682
|
+
*/
|
|
683
|
+
getShortcuts:(platform)=>km.shortcuts(platform),
|
|
684
|
+
/** Bindings the editor installed that have no title in the list (empty unless a binding was added without a place in source-keymap.js). */
|
|
685
|
+
unlistedShortcuts:()=>km.unlisted(),
|
|
686
|
+
/** Open the keyboard shortcuts dialog (Cmd/Ctrl+K Cmd/Ctrl+S). */
|
|
687
|
+
showShortcuts:()=>feature("shortcuts","showShortcuts")(view),
|
|
688
|
+
/** Open the symbol picker (Cmd/Ctrl+Shift+O). */
|
|
689
|
+
showSymbols:()=>feature("outline","showSymbolPicker")(view),
|
|
592
690
|
/** Whether a suggestion list is open or being asked for. */
|
|
593
691
|
isCompleting:()=>completionStatus(view.state)!==null,
|
|
594
692
|
/** Perform a fix (its edits as one undoable change, or its own action). */
|