@fusion.dev/helix-editor 0.220.0 → 0.222.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,50 +1,151 @@
1
1
  # @fusion.dev/helix-editor
2
2
 
3
- Connects Helix's `Code Editor` to CodeMirror 6, themed from your tokens:
4
- highlighting in your theme's syntax colors, line numbers, folding, bracket
5
- matching and history, diagnostics underlined in their tone with their line
6
- numbers drawn in it, and the editor's own find bar finding and replacing in
7
- CodeMirror.
3
+ Connects the Helix `Editor` frame to ProseMirror: a schema whose blocks each
4
+ keep an id, the editing plugins, and a document drawn in your tokens.
8
5
 
9
6
  ```sh
10
- bun add @fusion.dev/helix @fusion.dev/helix-editor
7
+ bun add @fusion.dev/helix @fusion.dev/helix-editor prosemirror-model prosemirror-state prosemirror-view
11
8
  ```
12
9
 
13
- The element is on the [Code Editor reference](https://fusion.dev/helix/components/code-editor).
14
- On its own it is a text field in the code face; connected, CodeMirror draws it.
15
- For a few lines of code in a form, `Code Input` needs no engine.
10
+ The element is on the [Editor reference](https://fusion.dev/helix/components/editor),
11
+ with its toolbar, its keys, and its parts.
12
+
13
+ ```html
14
+ <helix-editor label="Release notes" placeholder="Write the release notes">
15
+ <div data-prose><h2>Release notes</h2><p>Write documents in your theme.</p></div>
16
+ </helix-editor>
17
+ ```
16
18
 
17
19
  ```js
18
- import '@fusion.dev/helix/code-editor'
19
- import { connectEditor } from '@fusion.dev/helix-editor'
20
+ import '@fusion.dev/helix/editor'
21
+ import { connectEditor, editorPlugins, editorSchema } from '@fusion.dev/helix-editor'
22
+ import { DOMParser } from 'prosemirror-model'
23
+ import { EditorState } from 'prosemirror-state'
24
+ import { EditorView } from 'prosemirror-view'
25
+
26
+ const frame = document.querySelector('helix-editor')
27
+ const content = frame.querySelector('[data-prose]')
28
+ const schema = editorSchema()
20
29
 
21
- const editor = document.querySelector('helix-code-editor')
22
- const disconnect = connectEditor(editor)
30
+ const view = new EditorView(
31
+ { mount: content },
32
+ {
33
+ state: EditorState.create({
34
+ doc: DOMParser.fromSchema(schema).parse(content),
35
+ plugins: editorPlugins(schema),
36
+ }),
37
+ },
38
+ )
39
+ const connection = connectEditor(frame, view)
23
40
 
24
- editor.language = 'ts'
25
- editor.diagnostics = [{ from: 79, to: 83, message: 'Cannot find name totl.' }]
26
- editor.addEventListener('input', () => save(editor.value))
41
+ // Later, when the editor goes away:
42
+ connection.disconnect()
43
+ view.destroy()
27
44
  ```
28
45
 
29
- The value, the form, validation, and the events stay the element's: an edit in
30
- CodeMirror goes through the element's `requestValue`, so a listener refusing
31
- `helix-before-change` refuses the keystroke, and `input` follows each edit.
46
+ **The document is your element, inside the frame.** ProseMirror draws in it,
47
+ so the browser's input methods, spellcheck, and right-to-left text work as they
48
+ do in any field, and a server can send the document's HTML there to be read
49
+ before any script loads. `data-prose` on it has the base layer draw its blocks
50
+ from the first paint. `connectEditor` adds the rules ProseMirror needs to
51
+ the page, so you load no stylesheet of ProseMirror's.
52
+
53
+ **ProseMirror is a peer dependency.** ProseMirror's packages check each
54
+ other's classes, so a page has one copy of `prosemirror-model`,
55
+ `prosemirror-state`, `prosemirror-transform`, and `prosemirror-view`, and this
56
+ package uses yours.
57
+
58
+ ## What it exports
59
+
60
+ | Export | What |
61
+ |---|---|
62
+ | `connectEditor(frame, view, options)` | Connects a frame to a view, and returns a connection with `disconnect()` |
63
+ | `editorSchema(options)` | The schema: Helix's blocks and marks, with your own added through `nodes` and `marks` |
64
+ | `editorPlugins(schema, options)` | The keys, Markdown typed at the start of a line, history, the drop and gap cursors, `blockIds`, `editorFind`, and `editorFrame` |
65
+ | `blockIds(options)` | Keeps every block's id its own |
66
+ | `findBlock(doc, id)` | Where a block stands, by its id |
67
+ | `withBlockId(spec)` | A block of your own that keeps an id as Helix's do |
68
+ | `toolCommands(schema, overrides)` | The command each toolbar tool runs |
69
+ | `formatsOf(state, commands)` | What the toolbar shows for a state |
70
+ | `setLink(schema, href)` | Links the selection, or with an empty address takes its link off |
71
+ | `safeHref(href)` | The address a link may have, or an empty string for one that could run something |
72
+ | `editorFrame()` | The plugin the frame speaks to the view through, which `editorPlugins` includes |
73
+ | `editorFind()` | The find bar's search in the document, which `editorPlugins` includes |
74
+ | `findIn(view, query, direction)`, `replaceIn(view, query, all)`, `endFind(view)` | What the frame's find bar runs: find and select a match, replace, and take the marks away |
75
+ | `matchesIn(doc, query)`, `findResultOf(state)` | Where a query matches in a document, and where an open search stands |
76
+ | `editorNodes`, `editorMarks` | Helix's node and mark specs |
77
+ | `EDITOR_CSS`, `adoptEditorSheet(root)` | The rules `connectEditor` adds to the page |
78
+
79
+ | Option | Of | What it does |
80
+ |---|---|---|
81
+ | `commands` | `connectEditor`, `editorPlugins` | Runs your commands in place of Helix's, by tool, such as undo and redo from y-prosemirror |
82
+ | `history` | `editorPlugins` | `false` leaves the undo history to another plugin |
83
+ | `markdown` | `editorPlugins` | `false` turns off `# `, `- `, `1. `, `> `, and three backticks at the start of a line |
84
+ | `marks`, `nodes` | `editorSchema` | Marks and nodes of your own, or one of Helix's replaced by its name |
85
+ | `generate` | `blockIds` | Makes a new id. Twelve letters and digits by default |
86
+
87
+ ## Blocks and their ids
88
+
89
+ The schema keeps ProseMirror's own names: `paragraph`, `heading`,
90
+ `blockquote`, `code_block`, `horizontal_rule`, `bullet_list`, `ordered_list`,
91
+ `list_item`, and `hard_break`, with the marks `link`, `em`, `strong`, `strike`,
92
+ and `code`. Markdown reads into it and writes out of it through
93
+ `prosemirror-markdown` as it is.
94
+
95
+ Every block carries an `id`, written to `data-block-id` in HTML. `blockIds`
96
+ gives an id to a block that has none, and the second of two blocks sharing one
97
+ a new one, as a split or a paste leaves them. `findBlock(doc, id)` finds a
98
+ block again, for a comment anchored to it or an edit an agent makes to it.
99
+
100
+ A paragraph and a heading are drawn with `dir="auto"`, so each runs in the
101
+ direction of its own first letters, and a code block left to right.
32
102
 
33
- ## Languages
103
+ ## Find
34
104
 
35
- `language` takes `css`, `html`, `js`, `jsx`, `json`, `md`, `py`, `rs`, `sh`,
36
- `ts` and `tsx`, and the names they go by, such as `typescript`, `markdown`,
37
- `python` and `rust`. Each grammar loads the first time an editor asks for it.
38
- Any other name is plain text.
105
+ The frame's find bar searches the document through `connectEditor`: every
106
+ match is marked, the selected one is scrolled into view, and Replace and
107
+ Replace all change the document in one step of the undo history. A match never
108
+ runs from one block into the next. While the bar is open, a change to the
109
+ document finds again, and the bar says the new count. A read-only document is
110
+ searched, and nothing in it is replaced.
39
111
 
40
- ## More of CodeMirror
112
+ ## Collaboration
41
113
 
42
- `connectEditor(editor, { extensions })` adds CodeMirror extensions after
43
- Helix's own, such as completions or a language server client. To connect
44
- CodeMirror under your own control, set the element's `engine` to
45
- `codeMirrorEngine(options)`, or to an engine of your own that answers `mount`.
114
+ The page makes the editor's state, so a binding such as y-prosemirror is
115
+ plugins the page adds. Its own history replaces ProseMirror's:
116
+
117
+ ```js
118
+ import { redoCommand, undoCommand, yCursorPlugin, ySyncPlugin, yUndoPlugin } from 'y-prosemirror'
119
+
120
+ const commands = { redo: redoCommand, undo: undoCommand }
121
+ const state = EditorState.create({
122
+ schema,
123
+ plugins: [
124
+ ySyncPlugin(fragment),
125
+ yCursorPlugin(awareness),
126
+ yUndoPlugin(),
127
+ ...editorPlugins(schema, { commands, history: false }),
128
+ ],
129
+ })
130
+ connectEditor(frame, new EditorView({ mount: content }, { state }), { commands })
131
+ ```
132
+
133
+ **A command is also asked without `dispatch`**, after every change, to know
134
+ whether its button can be pressed, so it has to change nothing then. That is
135
+ why the example takes y-prosemirror's `undoCommand` and `redoCommand`: its
136
+ `undo` and `redo` undo whenever they are called.
137
+
138
+ Block ids are attributes of the blocks, so the binding merges them with the
139
+ rest of the document.
140
+
141
+ ## Working on it
142
+
143
+ ```sh
144
+ bun run --filter @fusion.dev/helix-editor build
145
+ bun run --filter @fusion.dev/helix-editor test
146
+ ```
46
147
 
47
- ## Keyboard
148
+ The specs run ProseMirror's own view, under `bun test` and in a browser with
149
+ `bun run specs:browser editor`, where an input method's composition reaches it.
48
150
 
49
- Tab indents, and Escape then Tab leaves the editor. Command+F, or Control+F on
50
- Windows and Linux, opens the editor's find bar, and Command+Z undoes.
151
+ MIT
@@ -0,0 +1,36 @@
1
+ import type { Node } from 'prosemirror-model';
2
+ import { Plugin, PluginKey } from 'prosemirror-state';
3
+ /**
4
+ * A new block id: twelve letters and digits, from the platform's random
5
+ * source.
6
+ *
7
+ * **`getRandomValues` and not `randomUUID`**, which a page served over plain
8
+ * HTTP from anywhere but `localhost` does not have.
9
+ */
10
+ export declare const newBlockId: () => string;
11
+ /** The key the block id plugin is found under. */
12
+ export declare const blockIdsKey: PluginKey<any>;
13
+ /** What `blockIds` takes. */
14
+ export interface BlockIdsOptions {
15
+ /** Makes a new id. `newBlockId` by default. */
16
+ generate?: (() => string) | undefined;
17
+ }
18
+ /**
19
+ * Keeps every block's id its own: a block with none is given one, and where
20
+ * a split or a paste leaves two blocks sharing an id, the second is given a
21
+ * new one, so the first, the block that was there, keeps it.
22
+ *
23
+ * Ids are given in the same step as the edit that called for them, so undo
24
+ * takes both back together. A document loaded without ids is given them as
25
+ * the view mounts, outside history, so undo never takes them away.
26
+ *
27
+ * **Under collaboration each writer gives ids for its own edits.** Ids are
28
+ * attributes of the blocks, which a binding such as y-prosemirror merges as it
29
+ * merges the rest, so every copy settles on the same id for a block.
30
+ */
31
+ export declare const blockIds: (options?: BlockIdsOptions) => Plugin;
32
+ /** Where a block stands in the document, by its id, or nothing where no block has it. */
33
+ export declare const findBlock: (doc: Node, id: string) => {
34
+ node: Node;
35
+ pos: number;
36
+ } | undefined;
@@ -0,0 +1,66 @@
1
+ import { Plugin, PluginKey } from 'prosemirror-state';
2
+ const ALPHABET = 'abcdefghijklmnopqrstuvwxyz0123456789';
3
+ const LENGTH = 12;
4
+ export const newBlockId = () => {
5
+ const bytes = crypto.getRandomValues(new Uint8Array(LENGTH));
6
+ let id = '';
7
+ for (const byte of bytes) {
8
+ id += ALPHABET[byte % ALPHABET.length];
9
+ }
10
+ return id;
11
+ };
12
+ const keepsId = (node) => 'id' in (node.type.spec.attrs ?? {});
13
+ const fixIds = (state, generate) => {
14
+ const seen = new Set();
15
+ let tr = null;
16
+ state.doc.descendants((node, pos) => {
17
+ if (keepsId(node)) {
18
+ const id = node.attrs.id;
19
+ if (typeof id !== 'string' || id === '' || seen.has(id)) {
20
+ let next = generate();
21
+ while (seen.has(next)) {
22
+ next = generate();
23
+ }
24
+ seen.add(next);
25
+ tr ??= state.tr;
26
+ tr.setNodeAttribute(pos, 'id', next);
27
+ }
28
+ else {
29
+ seen.add(id);
30
+ }
31
+ }
32
+ return !node.isTextblock;
33
+ });
34
+ return tr;
35
+ };
36
+ export const blockIdsKey = new PluginKey('helix-block-ids');
37
+ export const blockIds = (options = {}) => {
38
+ const generate = options.generate ?? newBlockId;
39
+ return new Plugin({
40
+ appendTransaction: (transactions, _before, state) => transactions.some((tr) => tr.docChanged) ? fixIds(state, generate) : null,
41
+ key: blockIdsKey,
42
+ view: (view) => {
43
+ queueMicrotask(() => {
44
+ const tr = view.isDestroyed ? null : fixIds(view.state, generate);
45
+ if (tr) {
46
+ view.dispatch(tr.setMeta('addToHistory', false));
47
+ }
48
+ });
49
+ return {};
50
+ },
51
+ });
52
+ };
53
+ export const findBlock = (doc, id) => {
54
+ let found;
55
+ doc.descendants((node, pos) => {
56
+ if (found) {
57
+ return false;
58
+ }
59
+ if (keepsId(node) && node.attrs.id === id) {
60
+ found = { node, pos };
61
+ return false;
62
+ }
63
+ return !node.isTextblock;
64
+ });
65
+ return found;
66
+ };
@@ -0,0 +1,65 @@
1
+ import { type Command, Plugin, PluginKey } from 'prosemirror-state';
2
+ import { type EditorView } from 'prosemirror-view';
3
+ import { type EditorTool } from '../tools/tools.js';
4
+ /** What the frame tells a renderer, as the frame's settings stand. */
5
+ interface Settings {
6
+ label: string;
7
+ placeholder: string;
8
+ readOnly: boolean;
9
+ }
10
+ /** What the bridge holds: the frame's settings, and whether a frame is connected. */
11
+ interface Bridge extends Settings {
12
+ connected: boolean;
13
+ }
14
+ /** The key the frame's plugin is found under. */
15
+ export declare const editorFrameKey: PluginKey<Bridge>;
16
+ /**
17
+ * The plugin the frame speaks to the view through: it names the document for
18
+ * a screen reader, makes it read-only, shows the placeholder, and hears every
19
+ * update. `editorPlugins` includes it, so connecting changes nothing about
20
+ * the view's plugins.
21
+ *
22
+ * **Included from the start where it can be**, because adding a plugin later
23
+ * remakes every plugin's view, and a collaboration binding draws the whole
24
+ * document again when its view is remade. `connectEditor` adds it only to a
25
+ * view whose plugins were chosen without it.
26
+ *
27
+ * Every document carries `data-prose`, which the base layer draws its blocks
28
+ * by. A document is a `textbox` that takes several lines while a frame is
29
+ * connected, named by the frame's `label`.
30
+ */
31
+ export declare const editorFrame: () => Plugin<Bridge>;
32
+ /** What a connection takes beyond the frame and the view. */
33
+ export interface ConnectEditorOptions {
34
+ /**
35
+ * The commands to run in place of Helix's, by tool, such as undo and redo
36
+ * where y-prosemirror keeps the history. Pass the same to `editorPlugins`,
37
+ * so the keys run them too.
38
+ *
39
+ * Each is also asked with no `dispatch` after every change, to know whether
40
+ * its button can be pressed, and has to change nothing then: y-prosemirror's
41
+ * `undoCommand` and `redoCommand`, and never its `undo` and `redo`, which
42
+ * undo whenever they are called.
43
+ */
44
+ commands?: Partial<Record<EditorTool, Command>> | undefined;
45
+ }
46
+ /** A connection, which is ended rather than left to leak. */
47
+ export interface EditorConnection {
48
+ /** Stops answering the frame, detaches the view from it, and puts back what connecting changed. */
49
+ disconnect(): void;
50
+ }
51
+ /**
52
+ * Connects an Editor frame to a ProseMirror view.
53
+ *
54
+ * The frame's toolbar runs its tools in the view and shows what holds at the
55
+ * selection; the frame's find bar searches the document, and replaces in it
56
+ * unless it is read-only; the frame's `label`, `placeholder`, and `readonly`
57
+ * reach the document; and the editor's rules are added to the page, so the
58
+ * document is drawn in your tokens.
59
+ *
60
+ * The frame is `helix-editor` or `helix-headless-editor`, and the view's
61
+ * schema is `editorSchema()` or one of your own: a tool whose node or mark
62
+ * the schema lacks is left off the toolbar.
63
+ */
64
+ export declare const connectEditor: (frame: HTMLElement, view: EditorView, options?: ConnectEditorOptions) => EditorConnection;
65
+ export {};
@@ -0,0 +1,142 @@
1
+ import { Plugin, PluginKey } from 'prosemirror-state';
2
+ import { Decoration, DecorationSet } from 'prosemirror-view';
3
+ import { editorFind, editorFindKey, endFind, findIn, findResultOf, replaceIn, } from '../find/find.js';
4
+ import { adoptEditorSheet } from '../sheet/editor-sheet.js';
5
+ import { EDITOR_TOOLS, formatsOf, setLink, toolCommands, } from '../tools/tools.js';
6
+ const isFrame = (element) => typeof element.attach === 'function';
7
+ const UNCONNECTED = { connected: false, label: '', placeholder: '', readOnly: false };
8
+ export const editorFrameKey = new PluginKey('helix-editor-frame');
9
+ const listeners = new WeakMap();
10
+ const placeholderOf = (state, text) => {
11
+ const { doc } = state;
12
+ const first = doc.firstChild;
13
+ if (text === '' || doc.childCount !== 1 || !first?.isTextblock || first.content.size > 0) {
14
+ return DecorationSet.empty;
15
+ }
16
+ return DecorationSet.create(doc, [
17
+ Decoration.node(0, first.nodeSize, {
18
+ class: 'helix-editor-placeholder',
19
+ 'data-placeholder': text,
20
+ }),
21
+ ]);
22
+ };
23
+ export const editorFrame = () => new Plugin({
24
+ key: editorFrameKey,
25
+ props: {
26
+ attributes: (state) => {
27
+ const bridge = editorFrameKey.getState(state) ?? UNCONNECTED;
28
+ if (!bridge.connected) {
29
+ return { 'data-prose': '' };
30
+ }
31
+ return {
32
+ ...(bridge.label ? { 'aria-label': bridge.label } : {}),
33
+ 'aria-multiline': 'true',
34
+ ...(bridge.placeholder ? { 'aria-placeholder': bridge.placeholder } : {}),
35
+ ...(bridge.readOnly ? { 'aria-readonly': 'true' } : {}),
36
+ 'data-prose': '',
37
+ role: 'textbox',
38
+ };
39
+ },
40
+ decorations: (state) => {
41
+ const bridge = editorFrameKey.getState(state);
42
+ return bridge?.connected ? placeholderOf(state, bridge.placeholder) : DecorationSet.empty;
43
+ },
44
+ editable: (state) => !editorFrameKey.getState(state)?.readOnly,
45
+ },
46
+ state: {
47
+ apply: (tr, bridge) => tr.getMeta(editorFrameKey) ?? bridge,
48
+ init: () => UNCONNECTED,
49
+ },
50
+ view: () => ({
51
+ update: (view) => listeners.get(view)?.(view.state),
52
+ }),
53
+ });
54
+ export const connectEditor = (frame, view, options = {}) => {
55
+ if (!isFrame(frame)) {
56
+ throw new Error(`connectEditor: <${frame.localName}> is not an editor frame. Import @fusion.dev/helix/editor, and connect once the element is defined.`);
57
+ }
58
+ const { schema } = view.state;
59
+ const commands = toolCommands(schema, options.commands);
60
+ const runs = EDITOR_TOOLS.filter((tool) => tool === 'link' ? schema.marks.link !== undefined : commands[tool] !== undefined);
61
+ const added = [
62
+ ...(editorFrameKey.get(view.state) ? [] : [editorFrame()]),
63
+ ...(editorFindKey.get(view.state) ? [] : [editorFind()]),
64
+ ];
65
+ if (added.length > 0) {
66
+ view.updateState(view.state.reconfigure({ plugins: [...view.state.plugins, ...added] }));
67
+ }
68
+ adoptEditorSheet(view.root);
69
+ let sink;
70
+ let told = '';
71
+ let toldFound = '';
72
+ let answering = false;
73
+ const tell = (state) => {
74
+ const formats = formatsOf(state, commands);
75
+ const said = JSON.stringify(formats);
76
+ if (sink && said !== told) {
77
+ told = said;
78
+ sink.formats(formats);
79
+ }
80
+ const found = findResultOf(state);
81
+ const where = found ? `${found.count} ${found.index}` : '';
82
+ if (sink && found && where !== toldFound && !answering) {
83
+ sink.found(found);
84
+ }
85
+ toldFound = where;
86
+ };
87
+ const answer = (search) => {
88
+ answering = true;
89
+ try {
90
+ return search();
91
+ }
92
+ finally {
93
+ answering = false;
94
+ }
95
+ };
96
+ listeners.set(view, tell);
97
+ sink = frame.attach({
98
+ find: (query, direction) => answer(() => findIn(view, query, direction)),
99
+ findEnd: () => {
100
+ if (!view.isDestroyed) {
101
+ endFind(view);
102
+ }
103
+ },
104
+ focus: () => view.focus(),
105
+ replace: (query, all) => answer(() => replaceIn(view, query, all)),
106
+ run: (tool, value) => {
107
+ if (view.isDestroyed) {
108
+ return false;
109
+ }
110
+ const command = tool === 'link' ? setLink(schema, value ?? '') : commands[tool];
111
+ return command ? command(view.state, view.dispatch, view) : false;
112
+ },
113
+ settings: (settings) => {
114
+ if (!view.isDestroyed) {
115
+ view.dispatch(view.state.tr
116
+ .setMeta(editorFrameKey, { connected: true, ...settings })
117
+ .setMeta('addToHistory', false));
118
+ }
119
+ },
120
+ tools: () => runs,
121
+ });
122
+ tell(view.state);
123
+ return {
124
+ disconnect: () => {
125
+ sink?.detach();
126
+ sink = undefined;
127
+ listeners.delete(view);
128
+ if (view.isDestroyed) {
129
+ return;
130
+ }
131
+ endFind(view);
132
+ if (added.length > 0) {
133
+ view.updateState(view.state.reconfigure({
134
+ plugins: view.state.plugins.filter((plugin) => !added.includes(plugin)),
135
+ }));
136
+ }
137
+ if (editorFrameKey.get(view.state)) {
138
+ view.dispatch(view.state.tr.setMeta(editorFrameKey, UNCONNECTED).setMeta('addToHistory', false));
139
+ }
140
+ },
141
+ };
142
+ };
@@ -0,0 +1,74 @@
1
+ import type { Node } from 'prosemirror-model';
2
+ import { type EditorState, Plugin, PluginKey } from 'prosemirror-state';
3
+ import { type EditorView } from 'prosemirror-view';
4
+ /**
5
+ * What the frame's find bar asks for. The headless tier's `FindQuery`, held
6
+ * to this by a spec, so this package depends on no tier.
7
+ */
8
+ export interface EditorFindQuery {
9
+ /** Whether case has to match, so "Error" does not find "error". */
10
+ caseSensitive: boolean;
11
+ /** Whether `search` is a regular expression, in JavaScript's syntax. */
12
+ regex: boolean;
13
+ /** What Replace puts in place of a match. */
14
+ replace: string;
15
+ /** What the reader typed. */
16
+ search: string;
17
+ /** Whether a match has to be a whole word. */
18
+ wholeWord: boolean;
19
+ }
20
+ /** Where a search stands: how many matches, and which is selected, counting from 0, or -1 for none. */
21
+ export interface EditorFindResult {
22
+ count: number;
23
+ index: number;
24
+ }
25
+ /** Which match a search selects: from the selection, the next one, or the previous one. */
26
+ export type EditorFindDirection = 'first' | 'next' | 'previous';
27
+ /** One match, as positions in the document. */
28
+ interface Match {
29
+ from: number;
30
+ to: number;
31
+ }
32
+ /** What the find plugin holds: the search, every match, and the selected one. */
33
+ interface Found {
34
+ current: number;
35
+ matches: readonly Match[];
36
+ query: EditorFindQuery | undefined;
37
+ }
38
+ /** The key the find plugin is found under. */
39
+ export declare const editorFindKey: PluginKey<Found>;
40
+ /**
41
+ * Every match of `query` in the document, block by block, so a match never
42
+ * runs from one block into the next, nor across a hard break.
43
+ */
44
+ export declare const matchesIn: (doc: Node, query: EditorFindQuery) => Match[];
45
+ /**
46
+ * The find bar's search in the document: every match marked, as
47
+ * `helix-find-match`, and the selected one as `helix-find-current`, which the
48
+ * connector's sheet paints as the base layer paints Find's highlights.
49
+ * `editorPlugins` includes it.
50
+ *
51
+ * While a search is open, a change to the document finds again, staying on
52
+ * the match the selected one became.
53
+ */
54
+ export declare const editorFind: () => Plugin<Found>;
55
+ /** Where the search in a state stands, or nothing while none is open. */
56
+ export declare const findResultOf: (state: EditorState) => EditorFindResult | undefined;
57
+ /**
58
+ * Finds `query` in the view's document, marks every match, and selects one:
59
+ * with `first`, the first from the selection's start, which stays on a
60
+ * selected match while the reader types; with `next`, the first from its
61
+ * end; with `previous`, the last before its start. Each wraps around the
62
+ * ends. The selected match is scrolled into view.
63
+ */
64
+ export declare const findIn: (view: EditorView, query: EditorFindQuery, direction: EditorFindDirection) => EditorFindResult;
65
+ /** Takes the marks a search drew away, and leaves the selection where it is. */
66
+ export declare const endFind: (view: EditorView) => void;
67
+ /**
68
+ * Replaces the selected match, or every match with `all`, in one step of the
69
+ * undo history, and then selects the next match. Where the selection is no
70
+ * match, it selects the next one and replaces nothing. A regular expression's
71
+ * groups reach the replacement as `$1`, as they do in JavaScript.
72
+ */
73
+ export declare const replaceIn: (view: EditorView, query: EditorFindQuery, all: boolean) => EditorFindResult;
74
+ export {};