Find
The editor has the matcher of a find bar but not the bar. The app draws its own, or uses FindReplace.
import type { EditorFindState } from '@adecore/editor';
import { Button, Input } from '@adecore/ui';
import { formatNumber } from '@adecore/ui/format';
import { useEffect, useState } from 'react';
import { EditorFrame } from '../shared/editor-frame.tsx';
import { SAMPLE, useEditor } from '../shared/editor.ts';
const OPTIONS = { text: SAMPLE, language: 'typescript', theme: 'demo' } as const;
/* A find bar of the host's own, over the editor's matcher. */
export default function FindDemo() {
const { host, editor } = useEditor(OPTIONS);
const [text, setText] = useState('total');
const [state, setState] = useState<EditorFindState>({ count: 0, current: null });
useEffect(() => editor?.onFind(setState), [editor]);
useEffect(() => {
editor?.find(text === '' ? null : { text, caseSensitive: false, wholeWord: false, regex: false });
}, [editor, text]);
return (
<EditorFrame
host={host}
toolbar={
<>
<Input size="sm" aria-label="Find" value={text} onChange={(event) => setText(event.target.value)} className="w-40" />
<span className="text-xs text-text-muted">
{state.current === null ? 'No matches' : `${formatNumber(state.current + 1)} of ${formatNumber(state.count)}`}
</span>
<Button size="xs" onClick={() => editor?.findStep(-1)}>
Previous
</Button>
<Button size="xs" onClick={() => editor?.findStep(1)}>
Next
</Button>
<Button size="xs" onClick={() => editor?.selectFindMatches()}>
Select all
</Button>
</>
}
/>
);
}Find and replace
find(query) marks every match and moves to the first one from the caret. null removes the marks. An EditorFindQuery has text, caseSensitive, wholeWord and regex, and inSelection: true searches only what was selected when it turned on.
const stop = editor.onFind((state) => setCount(state.count));
editor.find({ text: 'total', caseSensitive: false, wholeWord: true, regex: false });
editor.findStep(1);
editor.replace('sum');
editor.endFind();
stop();onFind(listener) reports an EditorFindState whenever the count changes:
count, and the zero-basedcurrentmatch ornull.noSelectionwhen a search in the selection has nothing selected.limited: truewhen more matches exist than the 10,000 marked. Withcurrent: nullthe current match lies past the marked ones, and the view still highlights it.searching,failed(invalid,timeout,unavailableorerror) andlargefor worker searches.replaced,replaceFailedandcaptureFailedafter a worker replacement or capture lookup.
| Method | What it does |
|---|---|
findStep(direction) | Moves to the next (1) or previous (-1) match |
replace(replacement, options?) | Replaces the current match and moves on; false without a match or when read-only |
replaceAll(replacement, options?) | Replaces every match in one undo step and returns how many |
setReplacePreview(replacement) | Draws what a regular expression writes for the current match; null removes it |
selectFindMatches() | Ends the find with a caret on every match and returns how many |
endFind() | Removes the marks and selects the current match |
findFromCursor(query, direction) | Selects the next match from the caret without a find bar |
Matches never touch the selection until endFind. A regular expression expands $1 and the like in the replacement. The EditorReplaceOptions of replace and replaceAll hold preserveCase, which matches the case of the replaced text. An invalid regular expression finds nothing. A search in an empty selection never replaces the whole document.
Regular expressions run only in the search worker. Literal searches run on the page. replaceAllAsync resolves to the number replaced and records one undo step. On overflow or a changed revision it applies nothing and resolves to 0. replaceAll returns its count synchronously when all literal matches are known. Otherwise it starts replaceAllAsync, returns 0 and reports the result through onFind. A regular expression replace returns true once its worker job starts. MAX_REPLACEMENT_PAGE and MAX_REPLACEMENT_CHARACTERS from @adecore/editor are the replacement limits, for a host's status messages.
Search worker
Set searchWorker on createSmartEditorEngine to a factory for a worker entry that calls serveEditorSearch(self). Regular expression searches debounce and run one at a time, with a deadline of 1.5 seconds live and 10 seconds on Enter or a find step. In a document over 8 million UTF-16 units they start only on Enter or a find step. Escape ends the session and releases the worker. Without searchWorker, a regular expression search reports unavailable.
// search.worker.ts
import { serveEditorSearch } from '@adecore/editor/search-worker';
serveEditorSearch(self);const engine = createSmartEditorEngine({
tokenizer,
searchWorker: () => new Worker(new URL('./search.worker.ts', import.meta.url), { type: 'module' })
});@adecore/editor/search-worker supplies serveEditorSearch for the worker entry and createSearchClient for a host that drives the worker itself. Importing it starts nothing. The worker runs the same iterateMatches as editor-core. serveEditorSearch takes an EditorSearchWorkerEndpoint, such as self, and returns a cleanup function.
Driving the worker yourself
createSearchClient(options) returns an EditorSearchClient. Its EditorSearchClientOptions take the workerFactory, a deadline in milliseconds and a schedule(callback, delay) timer, the last two for tests.
import { createSearchClient } from '@adecore/editor/search-worker';
const search = createSearchClient({ workerFactory: () => new Worker(workerUrl, { type: 'module' }) });
const result = await search.search({ revision: 1, text: 'Name: Ada' }, { text: 'Name: (\\w+)', regex: true, caseSensitive: false, wholeWord: false });
search.dispose();Keep one client per document. An EditorSearchSnapshot is the revision and text of a document. The revision identifies the text, and the client resends the text only when the revision changes or the worker restarts.
| Method | Returns |
|---|---|
search(snapshot, query, options?) | An EditorSearchResult: the revision, a transferable Uint32Array of [from, to] pairs (at most 10,000) and limited. EditorSearchOptions take from, to and maxResults |
match(snapshot, query, { from, to }) | An EditorSearchMatch, a FindMatch with captures and groups for one EditorSearchRange, or null, usable for preview and replace with editor-core's revision checks |
next(snapshot, query, offset, options?) | One range or null; EditorSearchNextOptions take backwards, wrap, from and to |
replacements(snapshot, query, replacement, options?) | One page of EditorSearchReplacements: revision, a ranges buffer of [from, to, replacementLength] triples, the joined replacement text and next, the from of the next page or null. Changes nothing |
A replacement page holds up to maxEdits (default 1,000, at most 10,000) and maxCharacters (default and maximum 4 million), set in EditorSearchReplacementOptions. The client refuses a single replacement larger than a page. Collect every page, refuse on any failure or changed revision, then apply all edits at once with expectedRevision, so the result is one undo step and never partial.
The default deadline is 1.5 seconds from posting, startup included. A timeout or a newer request terminates the worker, and the next request starts a fresh one. Of requests made in one microtask only the latest runs. cancel rejects pending work and keeps an idle worker. release() frees the worker and its snapshot and leaves the client usable. dispose() ends it.
Failures reject with an EditorSearchError whose kind, an EditorSearchErrorKind, is timeout, superseded, unavailable, syntax, range, snapshot, error or disposed. A failure before the worker's handshake stays unavailable for that client, so create a new client to retry. The messages between client and worker are EditorSearchInput and EditorSearchOutput.
A worker client of your own
@adecore/editor/worker-client holds the request lifecycle the search client is built on, for other work that belongs in a worker. createWorkerRequestClient(options) returns a WorkerRequestClient. Its WorkerRequestOptions take a workerFactory that returns a WorkerRequestPort (the Worker methods it uses), a deadline (1,500 ms) and hooks to check replies.
It keeps one active and one pending request, and a newer request supersedes both. The deadline includes worker startup, and a timeout terminates the worker. request(inputFactory, deadline?, validate?) sends one request; inputFactory gets the request id. release() frees the worker and keeps the client usable; dispose() is final. A failure rejects with a WorkerRequestError whose kind is a WorkerRequestErrorKind: timeout, superseded, unavailable, error or disposed. Use a separate client for work that cancels independently.