@openpresentation/opf-editor 0.26.3 → 0.26.4
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 +6 -2
- package/dist/json-editor.d.ts +7 -2
- package/dist/json-editor.js +51 -24
- package/dist/source-completions.js +192 -26
- package/dist/source-editing.js +265 -0
- package/dist/source-fixes-text.js +87 -0
- package/dist/source-lines.js +4 -0
- package/dist/source-markdown.js +551 -0
- package/dist/source-pane.d.ts +10 -2
- package/dist/source-pane.js +78 -7
- package/dist/source-problems.js +6 -4
- package/dist/source-ranges.js +8 -5
- package/dist/source-snippets.js +139 -0
- package/dist/source-yaml.js +538 -0
- package/dist/styles.css +23 -0
- package/package.json +9 -7
package/README.md
CHANGED
|
@@ -110,17 +110,21 @@ const pane = createSourcePane(container, {
|
|
|
110
110
|
pane.setFormat('yaml'); pane.focusPath('/slides/1/title'); pane.getText(); pane.destroy();
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
Methods: `replaceDocument(document, { source?, format?, name?, meta? })` (below), `setFindings(list | null)`, `setFormat(format)`, `focusPath(pointer)` (moves the cursor to the line of a JSON Pointer in any format
|
|
113
|
+
Methods: `replaceDocument(document, { source?, format?, name?, meta? })` (below), `setFindings(list | null)`, `setFormat(format)`, `focusPath(pointer)` (moves the cursor to the line of a JSON Pointer in any format, found with core's `locate`), `getText()`, `refresh()`, `setName`, `setDensity`, `setReadOnly` and `destroy()`. A finding is `{ path, message, severity, line? }` (core `Finding` objects fit). Rows show the path in mono and the line at the right; a click moves the cursor and calls `onFindingSelect`.
|
|
114
114
|
|
|
115
115
|
- **Opening a document: `pane.replaceDocument(document, { source, format, name })`.** Loading an example or a file into the pane (never by writing its text into the code editor, which the pane applies as a typed edit and forks the catalog records the two documents share) calls `editor.replaceDocument`: one undoable change, no catalog record forked. `document` is a parsed document or JSON text; `name` is the file name. `source` is the text the document was read from, in any of the three formats: the pane shows it as written (the JSON's formatting, the YAML's comments, the Markdown's layout) when the pane is in that format, and core's rewrite of the document otherwise. `format` (`"json"`, `"yaml"` or `"md"`) says which format `source` is; left out, it is inferred as the first of JSON, the pane's format, YAML and Markdown that reads as exactly this document (a `source` that reads as no format, or as another document, is ignored; an unknown `format` throws a `TypeError` before anything changes). The original text of a format other than the one shown is kept too: switching the pane to it shows those bytes, not a rewrite. Undo and Redo bring back each document's text exactly in the format shown (the opened text as written, the replaced document's text as it was, including YAML comments), so one Undo returns to the document and the text the visitor had. The pane keeps the last 8 such texts, which are also what a later load of the same document shows. A refused document throws (`invalid-opf-edit`, with `issues`) and changes nothing, and works in a read-only pane. A diff typed or pasted into the pane is still an edit, and editing a catalog record in it forks the record.
|
|
116
116
|
- **Keyboard Undo and Redo in the pane (one model).** Cmd/Ctrl+Z and Cmd/Ctrl+Shift+Z (Ctrl+Y) work in two layers, in this order. First CodeMirror's history, which holds only the text typed in the pane since its last whole-document change (a `replaceDocument`, or a session Undo, Redo or restore, each of which clears it because the typed edits no longer fit the text). When that has nothing left, the key steps the session over a whole-document replace, exactly as its Undo and Redo do, so the text comes back byte for byte: Undo steps back over the nearest `replaceDocument` together with the pane's own edits typed on it (the typing was already undone, step by step, by CodeMirror's history); Redo steps forward over the replace. The key never reaches over an edit made elsewhere (the canvas, a panel): Undo stops at it, and the host's Undo button (or `editor.undo()`) undoes that edit, after which the key reaches the load again. A read-only pane takes neither key. `mountJsonCodeEditor` exposes the same seam as `historyFallback: { undo(), redo() }` (answer `true` when you did something) plus `resetHistory()` and `undoDepth()` on the control.
|
|
117
117
|
- **JSON is canonical** and keeps the author's bytes: a YAML or Markdown edit rewrites only the changed JSON tokens (`exact-source`), and Undo restores the exact spelling.
|
|
118
118
|
- **YAML and Markdown** are core's browser-safe converters, `@openpresentation/opf/yaml` and `/markdown`. Switching format writes the session's document in the new format. An edit parses back to the document and reaches the session as one `replace` patch, after a short delay (`applyDelay`, default 250 ms) so one burst of typing is one undo step. A syntax error, or an OPF rule the session refuses, is listed with its line and never reaches the document; Undo, Redo and outside edits replace a draft that did not parse.
|
|
119
119
|
- **Nothing is lost silently.** Markdown writes what its dialect has no syntax for (a `design`, region keys, nested groups) as YAML in `opf-slide` and `opf-block` fences; each such part is a warning finding with its JSON Pointer and reason. Comments in YAML cannot survive a rewrite from the document: outside edits drop them and say how many. Format (below) never rewrites from the document, so it keeps them. A document that is not valid OPF cannot be written as YAML or Markdown; the pane stays in JSON and says so.
|
|
120
|
-
- **Completions.** In JSON 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).
|
|
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
|
+
- **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
|
+
- **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`).
|
|
121
123
|
- **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). `pane.getProblems()`, `nextProblem()`, `previousProblem()`, `showProblem(index)` and `triggerCompletion()` expose the same to a host.
|
|
122
124
|
- **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.
|
|
125
|
+
- **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.
|
|
123
126
|
- **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
|
+
- **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.
|
|
124
128
|
- **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.
|
|
125
129
|
- **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.
|
|
126
130
|
- **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/json-editor.d.ts
CHANGED
|
@@ -65,13 +65,18 @@ export interface JsonCodeEditorOptions {
|
|
|
65
65
|
language?: "json" | "yaml" | "markdown";
|
|
66
66
|
readOnly?: boolean;
|
|
67
67
|
/**
|
|
68
|
-
* Live completion list in JSON (default false here; the source pane turns it on): property names from the schema, ranked with the required and the
|
|
68
|
+
* Live completion list in JSON, YAML and Markdown (default false here; the source pane turns it on): in JSON property names from the schema, ranked with the required and the
|
|
69
69
|
* common ones first, and values (enums, booleans, catalog ids, `{}` and `[]` templates with their required members, snippets for slides and the like)
|
|
70
70
|
* at the cursor's JSON path. Typing a quote, a word, `:` or the space after it opens the list; Ctrl+Space (Cmd+I on macOS) opens it anywhere (with
|
|
71
71
|
* the list open it shows or hides the documentation). Tab and Enter (and Shift+Tab, Shift+Enter) accept the selected item, Escape closes the list,
|
|
72
|
-
* Left, Right and Backspace inside the word keep it open. The field menu is on Alt+Shift+O; with `completions` off it is also on Ctrl+Space.
|
|
72
|
+
* Left, Right and Backspace inside the word keep it open. The field menu is on Alt+Shift+O; with `completions` off it is also on Ctrl+Space. YAML offers the
|
|
73
|
+
* same from a line-and-indent reader (block and flow style, after a list dash); Markdown offers the front matter and fence YAML, slide and block
|
|
74
|
+
* options, fence info lines, metric fences, `Note:` and snippets (see the source pane in the README). Enter, Tab, Backspace, pairs and Cmd/Ctrl+/
|
|
75
|
+
* follow the language: YAML indents the way its text is built, Markdown continues lists and quotes, nests a list item with Tab and comments with `<!-- -->`.
|
|
73
76
|
*/
|
|
74
77
|
completions?: boolean;
|
|
78
|
+
/** 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
|
+
smartListEnter?: boolean;
|
|
75
80
|
catalogs?: JsonCatalogContext;
|
|
76
81
|
/** Overrides the built-in menu, allowing an existing host UI to keep its appearance. */
|
|
77
82
|
onOptions?(left?: number, top?: number): void;
|
package/dist/json-editor.js
CHANGED
|
@@ -14,6 +14,7 @@ import {detectIndent,formatJsonSource,whitespaceEdits} from './json-format.js';
|
|
|
14
14
|
import {a11yExtension,createAnnouncer} from './source-a11y.js';
|
|
15
15
|
import { forEachDiagnostic } from "@codemirror/lint";
|
|
16
16
|
import {createDiagnostics,editsTransaction} from './source-diagnostics.js';
|
|
17
|
+
import {languageDataFor,markdownBackspace,markdownEnter,markdownFolding,markdownTab,markdownToggleComment,markdownWrapSelection,yamlEnter} from './source-editing.js';
|
|
17
18
|
const external = Annotation.define();
|
|
18
19
|
const hoverField = StateEffect.define();
|
|
19
20
|
const activeField = StateField.define({ create: () => null, update: (value, tr) => tr.effects.reduce((value2, effect) => effect.is(hoverField) ? effect.value : value2, value) });
|
|
@@ -69,27 +70,43 @@ const markdownLanguage = StreamLanguage.define({
|
|
|
69
70
|
}
|
|
70
71
|
});
|
|
71
72
|
function languageExtension(name, completions) {
|
|
72
|
-
if (name === "yaml") return yaml();
|
|
73
|
-
if (name === "markdown") return markdownLanguage;
|
|
73
|
+
if (name === "yaml") return [yaml(), languageDataFor("yaml"), completions ?? []];
|
|
74
|
+
if (name === "markdown") return [markdownLanguage, languageDataFor("markdown"), markdownWrapSelection, markdownFolding, completions ?? []];
|
|
74
75
|
return [json(), fieldDecorations, completions ?? []];
|
|
75
76
|
}
|
|
77
|
+
// The name the completion engine knows a language by.
|
|
78
|
+
const formatOf = (name) => (name === "markdown" ? "md" : name);
|
|
76
79
|
// Tab or Enter pressed right after a keystroke can arrive before the completion query that keystroke started has answered (with the data
|
|
77
80
|
// loaded it answers within a task or two). Accepting then waits for the answer instead of inserting whitespace or a line over a list that is
|
|
78
81
|
// about to open; when no list comes, `fallback` does what the key does without a list.
|
|
82
|
+
// The wait is cut short by the next key: a key that follows at once (a typed line break, a paste of keystrokes) must find the text the held key made, so
|
|
83
|
+
// the held key runs first, as if no list had been coming.
|
|
84
|
+
const heldKeys = new WeakMap();
|
|
79
85
|
function acceptOrFallBack(isReady, fallback) {
|
|
80
86
|
return (view) => {
|
|
81
87
|
if (acceptCompletion(view)) return true;
|
|
82
88
|
if (completionStatus(view.state) !== "pending" || !isReady()) return fallback(view);
|
|
83
|
-
let tries = 0;
|
|
84
|
-
const
|
|
89
|
+
let tries = 0, done = false, timer = 0;
|
|
90
|
+
const finish = () => {
|
|
91
|
+
if (done) return;
|
|
92
|
+
done = true;
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
heldKeys.delete(view);
|
|
85
95
|
if (!view.dom.isConnected) return;
|
|
86
|
-
if (completionStatus(view.state) === "pending" && ++tries < 50) { setTimeout(settle, 2); return; }
|
|
87
96
|
if (!acceptCompletion(view)) fallback(view);
|
|
88
97
|
};
|
|
89
|
-
|
|
98
|
+
const settle = () => {
|
|
99
|
+
if (done || !view.dom.isConnected) return;
|
|
100
|
+
if (completionStatus(view.state) === "pending" && ++tries < 50) { timer = setTimeout(settle, 2); return; }
|
|
101
|
+
finish();
|
|
102
|
+
};
|
|
103
|
+
heldKeys.set(view, finish);
|
|
104
|
+
timer = setTimeout(settle, 0);
|
|
90
105
|
return true;
|
|
91
106
|
};
|
|
92
107
|
}
|
|
108
|
+
// Runs before every key binding: the key held for the list (see acceptOrFallBack) goes first.
|
|
109
|
+
const releaseHeldKey = EditorView.domEventHandlers({ keydown(_event, view) { heldKeys.get(view)?.(); return false; } });
|
|
93
110
|
// The documentation panel beside the selected item is shown by default (VS Code's `suggest.showInlineDetails`); Ctrl+Space with the list open
|
|
94
111
|
// hides or shows it (`toggleSuggestionDetails`). The choice is the editor's, not the list's, so it stays across lists.
|
|
95
112
|
function toggleDocs(view) {
|
|
@@ -131,19 +148,19 @@ function backspaceInToken(tokenOf) {
|
|
|
131
148
|
// 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)
|
|
132
149
|
// 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
|
|
133
150
|
// a line.
|
|
134
|
-
function completionKeys(isReady, tokenOf, onStep) {
|
|
151
|
+
function completionKeys(isReady, tokenOf, onStep, { newline, tab }) {
|
|
135
152
|
// 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.
|
|
136
153
|
const walk = (forward, fallback) => (view) => {
|
|
137
154
|
if (forward ? hasNextSnippetField(view.state) && nextSnippetField(view) : hasPrevSnippetField(view.state) && prevSnippetField(view)) { onStep(view, forward); return true; }
|
|
138
155
|
return fallback(view);
|
|
139
156
|
};
|
|
140
|
-
const enter =
|
|
141
|
-
return Prec.highest(keymap.of([
|
|
157
|
+
const enter = newline;
|
|
158
|
+
return Prec.highest([releaseHeldKey, keymap.of([
|
|
142
159
|
...completionKeymap.filter((binding) => binding.key !== "Enter" && binding.key !== "Ctrl-Space"),
|
|
143
160
|
{ key: "Ctrl-Space", run: (view) => (listIsOpen(view.state) ? toggleDocs(view) : startCompletion(view)) },
|
|
144
161
|
{ key: "Enter", run: acceptOrFallBack(isReady, enter) },
|
|
145
162
|
{ key: "Shift-Enter", run: acceptOrFallBack(isReady, insertNewlineAndIndent) },
|
|
146
|
-
{ key: "Tab", run: acceptOrFallBack(isReady, walk(true,
|
|
163
|
+
{ key: "Tab", run: acceptOrFallBack(isReady, walk(true, tab)) },
|
|
147
164
|
{ key: "Shift-Tab", run: acceptOrFallBack(isReady, walk(false, indentLess)) },
|
|
148
165
|
{ key: "ArrowLeft", run: moveInToken(false, tokenOf) },
|
|
149
166
|
{ key: "ArrowRight", run: moveInToken(true, tokenOf) },
|
|
@@ -151,7 +168,7 @@ function completionKeys(isReady, tokenOf, onStep) {
|
|
|
151
168
|
{ mac: "Mod-i", run: startCompletion },
|
|
152
169
|
{ mac: "Ctrl-n", run: moveCompletionSelection(true) },
|
|
153
170
|
{ mac: "Ctrl-p", run: moveCompletionSelection(false) },
|
|
154
|
-
]));
|
|
171
|
+
])]);
|
|
155
172
|
}
|
|
156
173
|
const DEFAULT_INDENT = " ";
|
|
157
174
|
const tabWidth = (unit) => (unit === "\t" ? 4 : unit.length);
|
|
@@ -287,21 +304,21 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
287
304
|
// (no promise), and the list stays valid while the author types within the same token (`validFor`), so typing only refilters it.
|
|
288
305
|
let optionsModule = null, optionsLoading = null;
|
|
289
306
|
const loadOptions = () => (optionsLoading ??= import('./source-completions.js').then((module) => (optionsModule = module), (error) => { optionsLoading = null; throw error; }));
|
|
290
|
-
const applyOption = (target, completion, from, to) => optionsModule.applyCompletion(target, completion, from, to, say);
|
|
307
|
+
const applyOption = (target, completion, from, to) => optionsModule.applyCompletion(target, completion, from, to, say, formatOf(languageName));
|
|
291
308
|
// Hover: the schema's words over a key, the record's over a catalog id. A problem under the pointer is the lint tooltip's (message, next, fixes).
|
|
292
309
|
const problemAt = (state, position) => { let hit = false; forEachDiagnostic(state, (_diagnostic, from, to) => { if (position >= from && position <= to) hit = true; }); return hit; };
|
|
293
310
|
const hoverExtension = options.hover === false ? [] : hoverTooltip(async (target, position) => {
|
|
294
|
-
if (disposed ||
|
|
311
|
+
if (disposed || problemAt(target.state, position)) return null;
|
|
295
312
|
if (!optionsModule) { try { await loadOptions(); } catch { return null; } }
|
|
296
|
-
return optionsModule.hoverAt(target.state, position, { catalogs });
|
|
313
|
+
return optionsModule.hoverAt(target.state, position, { catalogs, format: formatOf(languageName) });
|
|
297
314
|
}, { hoverTime: 300 });
|
|
298
315
|
// Cmd/Ctrl+K Cmd/Ctrl+I shows the hover at the caret (VS Code), and says what it holds.
|
|
299
316
|
let chordStartedAt = 0;
|
|
300
317
|
const showHoverAtCaret = (target) => {
|
|
301
|
-
if (
|
|
318
|
+
if (options.hover === false) return false;
|
|
302
319
|
const run = () => {
|
|
303
320
|
const position = target.state.selection.main.head;
|
|
304
|
-
const found = optionsModule.hoverModelAt(target.state, position, { catalogs });
|
|
321
|
+
const found = optionsModule.hoverModelAt(target.state, position, { catalogs, format: formatOf(languageName) });
|
|
305
322
|
if (!found) { say("No hover information here"); return; }
|
|
306
323
|
activateHover(target, found.from, 1, { tooltip: hoverExtension, until: (tr) => tr.docChanged || (tr.selection !== undefined && tr.newSelection.main.head !== position) });
|
|
307
324
|
say(optionsModule.docText(found.model));
|
|
@@ -310,13 +327,13 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
310
327
|
return true;
|
|
311
328
|
};
|
|
312
329
|
function completionResult(context) {
|
|
313
|
-
const found = optionsModule.completionResult(context.state, context.pos, { explicit: context.explicit, catalogs });
|
|
330
|
+
const found = optionsModule.completionResult(context.state, context.pos, { explicit: context.explicit, catalogs, format: formatOf(languageName) });
|
|
314
331
|
if (!found) return null;
|
|
315
332
|
for (const option of found.options) option.apply = applyOption;
|
|
316
333
|
return found;
|
|
317
334
|
}
|
|
318
335
|
function completionSource(context) {
|
|
319
|
-
if (disposed ||
|
|
336
|
+
if (disposed || context.state.readOnly) return null;
|
|
320
337
|
if (optionsModule) return completionResult(context);
|
|
321
338
|
return loadOptions().then(() => (disposed || context.aborted ? null : completionResult(context)), (error) => { options.onError?.(error); return null; });
|
|
322
339
|
}
|
|
@@ -354,7 +371,11 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
354
371
|
} catch (error) { options.onError?.(error); }
|
|
355
372
|
}, 300);
|
|
356
373
|
}
|
|
357
|
-
|
|
374
|
+
// Enter and Tab by language: JSON continues a list of strings, YAML and Markdown indent the way their text is built (source-editing.js).
|
|
375
|
+
const smart = options.smartListEnter !== false;
|
|
376
|
+
const newlineCommand = (target) => (languageName === "json" ? smart && nextStringItem(target) : languageName === "yaml" ? yamlEnter(target, { lists: smart }) : markdownEnter(target, { lists: smart }));
|
|
377
|
+
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;
|
|
358
379
|
const contentAttributes = (invalid = options.invalid, describedBy = options.describedBy) => EditorView.contentAttributes.of({
|
|
359
380
|
"aria-label": options.label,
|
|
360
381
|
"aria-invalid": String(Boolean(invalid)),
|
|
@@ -437,16 +458,22 @@ function mountJsonCodeEditor(parent, options) {
|
|
|
437
458
|
return false;
|
|
438
459
|
} })),
|
|
439
460
|
keymap.of([
|
|
440
|
-
{ key: "Enter", run:
|
|
441
|
-
{ key: '"', run: (view2) => skipDelimiter(view2, '"') },
|
|
442
|
-
{ key: ",", run: (view2) => skipDelimiter(view2, ",") },
|
|
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) },
|
|
443
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
|
|
444
466
|
// it is also on Ctrl+Space, because the list is not there to take that key.
|
|
445
467
|
...(options.completions ? [] : [{ key: "Ctrl-Space", run: () => languageName === "json" && (void openOptions(), true) }, { key: "Mod-Space", run: () => languageName === "json" && (void openOptions(), true) }]),
|
|
446
|
-
{ key: "Tab", run:
|
|
468
|
+
{ key: "Tab", run: (target) => tabCommandFor(target), shift: indentLess },
|
|
447
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.
|
|
448
470
|
// JSON has no comments: say so instead of doing nothing (YAML and Markdown comment their lines out).
|
|
449
|
-
{ key: "Mod-/", run: () => {
|
|
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
|
+
} },
|
|
450
477
|
{ key: "Ctrl-m", mac: "Ctrl-Shift-m", run: toggleTabFocusMode },
|
|
451
478
|
...closeBracketsKeymap,
|
|
452
479
|
...defaultKeymap,
|