Search & Replace
Search is searchPlugin plus a set of commands. The plugin ships in
customPlugins and is on by default; opt out with searchPlugin: false:
import { customPlugins } from "@stesura/core/plugins";
const plugins = [...customPlugins({ field: editorId })]; // with search
const withoutSearch = [...customPlugins({ field: editorId, searchPlugin: false })];The plugin holds the query, an optional range restriction and the match
decorations. While search is open, edits map the existing decorations instead of
rescanning. A full rescan runs 500ms after the last doc change, and immediately
when track-changes visibility toggles: while suggestions are hidden, deleted
text doesn’t match (skipDeletions follows the toggle).
Opening and closing search
import {
enableSearchState,
disableSearchState,
toggleSearchState,
} from "@stesura/core/commands";
enableSearchState(state, dispatch); // open, empty query; false if already open
disableSearchState(state, dispatch); // close and reset; false if already closed
toggleSearchState(state, dispatch); // flip, resetting the query either wayThe core keymap binds Mod-f to enableSearchState, and Escape closes
search (unless the format painter is armed, in which case it disarms that).
Setting a query
setSearchState is a transaction helper, not a command. It stamps meta on a
Transaction and enables search once dispatched:
import { setSearchState } from "@stesura/core/helpers";
import { SearchQuery } from "@stesura/core/plugins";
const query = new SearchQuery({ search: "hello", replace: "world" });
view.dispatch(setSearchState(view.state.tr, query));SearchQuery is adapted from prosemirror-search.
SearchQuery option | Type | Description |
|---|---|---|
search | string | The search string, or regex source. Required. |
replace | string | Replacement text. $& inserts the match, and with regexp, $1… insert groups. Default "". |
caseSensitive | boolean | Case-sensitive matching. Default false. |
regexp | boolean | Treat search as a regular expression. Default false. |
wholeWord | boolean | Only match whole words. Default false. |
literal | boolean | Don’t expand \n/\r/\t in the query. Default false. |
skipDeletions | boolean | Ignore text with deletion marks and nodes with deletion track-changes attrs. Default false. |
filter | (state, result) => boolean | Results it rejects are ignored. |
query.valid is false for an empty string or an invalid regex. Every find and
replace command returns false on an invalid query.
Reading search state
import { getSearchState, getMatchHighlights } from "@stesura/core/helpers";
const searchState = getSearchState(view.state); // undefined without the plugin
// searchState.enabled — boolean
// searchState.query — SearchQuery
// searchState.range — { from, to } | null
// searchState.deco — DecorationSet of matches
const decorations = getMatchHighlights(view.state); // DecorationSet.empty without the pluginMatches carry the class ProseMirror-search-match, and the match under the
selection carries ProseMirror-active-search-match.
Navigating matches
import {
findNext,
findPrev,
findNextNoWrap,
findPrevNoWrap,
} from "@stesura/core/commands";
findNext(state, dispatch); // select the next match, wrapping at the end
findPrev(state, dispatch); // select the previous match, wrapping at the start
findNextNoWrap(state, dispatch); // false once past the last match
findPrevNoWrap(state, dispatch); // false once before the first matchReplacing matches
import {
replaceNext,
replaceNextNoWrap,
replaceCurrent,
replaceAll,
} from "@stesura/core/commands";
replaceNext(state, dispatch); // replace the selected match, then select the next one
replaceCurrent(state, dispatch); // replace the selected match and stay on the result
replaceAll(state, dispatch); // replace every match, in one transactionreplaceNext only replaces when the selection is a match. Otherwise it
selects the next match, so the first call finds and the second replaces.
replaceCurrent returns false unless the selection is a match.
replaceNextNoWrap is replaceNext without wrapping.
Restricting the search range
Pass a range as the third argument to setSearchState:
view.dispatch(setSearchState(view.state.tr, query, { from: 100, to: 500 }));Finding, replacing and the highlights all stay inside the range. It maps through later edits, and if an edit collapses it the range resets to the whole document.
Clearing search state
import { clearSearchState } from "@stesura/core/commands";
clearSearchState()(state, dispatch);Unlike the other commands, clearSearchState is a factory. It empties the
query and range and closes search.
Matches outside the body
Header, footer and note content lives in the main doc but is edited through
sub-editors. classifySearchMatch(state, pos) from @stesura/core/helpers
tells a navigation UI which sub-editor to open. It returns body, footnote,
endnote or headerFooter along with the details it needs.
Comment bodies are not part of the doc. findCommentMatches(threads, config)
searches them with the same string rules (search, caseSensitive,
wholeWord, literal) and returns the first match per comment.
Driving the built-in search UI
editor-react’s ToolBar renders a find-and-replace bar below the ribbon, opened by the Home tab’s search button or Mod-F (also in view mode, where replace is hidden). It drives the commands above and also lists matches in comment bodies. You only need the raw commands if you are building a custom UI or driving search programmatically.
Next steps
- Plugins —
searchPluginandsearchPluginKey. - Customizing the Toolbar — hiding or replacing the search panel.
- UI Extensions — adding your own search-adjacent toolbar controls.