Skip to Content
Search & Replace

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).

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 way

The 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 optionTypeDescription
searchstringThe search string, or regex source. Required.
replacestringReplacement text. $& inserts the match, and with regexp, $1… insert groups. Default "".
caseSensitivebooleanCase-sensitive matching. Default false.
regexpbooleanTreat search as a regular expression. Default false.
wholeWordbooleanOnly match whole words. Default false.
literalbooleanDon’t expand \n/\r/\t in the query. Default false.
skipDeletionsbooleanIgnore text with deletion marks and nodes with deletion track-changes attrs. Default false.
filter(state, result) => booleanResults 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 plugin

Matches carry the class ProseMirror-search-match, and the match under the selection carries ProseMirror-active-search-match.

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 match

Replacing 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 transaction

replaceNext 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

Last updated on