@tiptap/extension-find-and-replace 3.29.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.
Files changed (58) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +18 -0
  3. package/dist/index.cjs +950 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +230 -0
  6. package/dist/index.d.ts +230 -0
  7. package/dist/index.js +915 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +55 -0
  10. package/src/constants/constants.ts +2 -0
  11. package/src/find-and-replace.ts +434 -0
  12. package/src/index.ts +8 -0
  13. package/src/plugin/plugin-state.ts +27 -0
  14. package/src/plugin/plugin.ts +60 -0
  15. package/src/search/matches.ts +26 -0
  16. package/src/search/regex.ts +67 -0
  17. package/src/search/search.ts +70 -0
  18. package/src/search/text-segments.ts +63 -0
  19. package/src/search/textblock-search.ts +31 -0
  20. package/src/search/types.ts +17 -0
  21. package/src/state/debounced-search-state.ts +39 -0
  22. package/src/style/create-style-tag.ts +23 -0
  23. package/src/style/style.ts +10 -0
  24. package/src/types.ts +96 -0
  25. package/src/utils/createDecoration.ts +14 -0
  26. package/src/utils/createDecorations.ts +16 -0
  27. package/src/utils/createState.ts +23 -0
  28. package/src/utils/findDecorations.ts +27 -0
  29. package/src/utils/getChangedTextblocks.ts +29 -0
  30. package/src/utils/getFallbackCurrentIndex.ts +8 -0
  31. package/src/utils/getMappedCurrentIndex.ts +32 -0
  32. package/src/utils/getMetaCurrentIndex.ts +19 -0
  33. package/src/utils/getPreviousCurrentResult.ts +10 -0
  34. package/src/utils/getResultsToRefresh.ts +22 -0
  35. package/src/utils/getSearchCurrentIndex.ts +24 -0
  36. package/src/utils/getTextNodeResult.ts +21 -0
  37. package/src/utils/groupResults.ts +68 -0
  38. package/src/utils/hasCurrentIndex.ts +5 -0
  39. package/src/utils/index.ts +2 -0
  40. package/src/utils/isCurrentResult.ts +7 -0
  41. package/src/utils/isNewSearch.ts +5 -0
  42. package/src/utils/mapResult.ts +10 -0
  43. package/src/utils/mapResults.ts +19 -0
  44. package/src/utils/mergeResults.ts +18 -0
  45. package/src/utils/normalizeCurrentIndex.ts +10 -0
  46. package/src/utils/refreshCurrentIndexState.ts +30 -0
  47. package/src/utils/refreshDecorations.ts +28 -0
  48. package/src/utils/refreshDocumentState.ts +47 -0
  49. package/src/utils/refreshState.ts +27 -0
  50. package/src/utils/replaceAllResults.ts +18 -0
  51. package/src/utils/replaceGroup.ts +54 -0
  52. package/src/utils/resolveCurrentIndex.ts +27 -0
  53. package/src/utils/resultInTextblocks.ts +30 -0
  54. package/src/utils/sameResult.ts +5 -0
  55. package/src/utils/shouldRefreshResult.ts +18 -0
  56. package/src/utils/touchesSearch.ts +7 -0
  57. package/src/utils/types.ts +11 -0
  58. package/src/utils/updateState.ts +33 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/find-and-replace.ts","../src/state/debounced-search-state.ts","../src/plugin/plugin.ts","../src/constants/constants.ts","../src/style/create-style-tag.ts","../src/style/style.ts","../src/search/regex.ts","../src/search/matches.ts","../src/search/text-segments.ts","../src/search/textblock-search.ts","../src/search/search.ts","../src/utils/createDecorations.ts","../src/utils/createDecoration.ts","../src/utils/createState.ts","../src/utils/hasCurrentIndex.ts","../src/utils/findDecorations.ts","../src/utils/sameResult.ts","../src/utils/isCurrentResult.ts","../src/utils/resultInTextblocks.ts","../src/utils/shouldRefreshResult.ts","../src/utils/getResultsToRefresh.ts","../src/utils/refreshDecorations.ts","../src/utils/normalizeCurrentIndex.ts","../src/utils/getMetaCurrentIndex.ts","../src/utils/getFallbackCurrentIndex.ts","../src/utils/getPreviousCurrentResult.ts","../src/utils/getMappedCurrentIndex.ts","../src/utils/isNewSearch.ts","../src/utils/getSearchCurrentIndex.ts","../src/utils/resolveCurrentIndex.ts","../src/utils/refreshCurrentIndexState.ts","../src/utils/getChangedTextblocks.ts","../src/utils/mapResult.ts","../src/utils/mapResults.ts","../src/utils/mergeResults.ts","../src/utils/refreshDocumentState.ts","../src/utils/refreshState.ts","../src/utils/touchesSearch.ts","../src/utils/updateState.ts","../src/utils/getTextNodeResult.ts","../src/utils/groupResults.ts","../src/utils/replaceGroup.ts","../src/utils/replaceAllResults.ts","../src/index.ts"],"sourcesContent":["import type { Command, Editor } from '@tiptap/core'\nimport { Extension } from '@tiptap/core'\nimport type { EditorState, Transaction } from '@tiptap/pm/state'\nimport { TextSelection } from '@tiptap/pm/state'\n\nimport {\n clearDebouncedSearchState,\n getDebouncedSearchState,\n} from './state/debounced-search-state.js'\nimport type { FindAndReplaceMeta, FindAndReplacePluginState } from './plugin/plugin.js'\nimport { FindAndReplacePlugin, FindAndReplacePluginKey } from './plugin/plugin.js'\nimport { findNextIndex, searchDocument } from './search/search.js'\nimport type { SearchResult } from './search/search.js'\nimport type { FindAndReplaceOptions, FindAndReplaceStorage } from './types.js'\nimport { replaceAllResults } from './utils/replaceAllResults.js'\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n findAndReplace: {\n /**\n * Set the search term and highlight all matches in the document.\n * @param term The search term, treated as regex source when regex mode is enabled.\n * @example editor.commands.setSearchTerm('Hello')\n */\n setSearchTerm: (term: string) => ReturnType\n\n /**\n * Set the replace term used by the replace commands.\n * @param term The replacement text.\n * @example editor.commands.setReplaceTerm('World')\n */\n setReplaceTerm: (term: string) => ReturnType\n\n /**\n * Set whether the search is case sensitive.\n * @param caseSensitive The new case sensitivity.\n * @example editor.commands.setCaseSensitive(true)\n */\n setCaseSensitive: (caseSensitive: boolean) => ReturnType\n\n /**\n * Set whether the search term is treated as an RE2-compatible regular expression.\n * Lookarounds and backreferences are not supported.\n * @param useRegex The new regex mode.\n * @example editor.commands.setUseRegex(true)\n */\n setUseRegex: (useRegex: boolean) => ReturnType\n\n /**\n * Set whether to match whole words only. Ignored when regex mode is enabled.\n * @param wholeWord The new whole word mode.\n * @example editor.commands.setWholeWord(true)\n */\n setWholeWord: (wholeWord: boolean) => ReturnType\n\n /**\n * Replace the current search result and jump to the next one.\n * @example editor.commands.replace()\n */\n replace: () => ReturnType\n\n /**\n * Replace all search results at once.\n * @example editor.commands.replaceAll()\n */\n replaceAll: () => ReturnType\n\n /**\n * Jump to the next search result, wrapping around at the end.\n * @example editor.commands.goToNextResult()\n */\n goToNextResult: () => ReturnType\n\n /**\n * Jump to the previous search result, wrapping around at the start.\n * @example editor.commands.goToPreviousResult()\n */\n goToPreviousResult: () => ReturnType\n\n /**\n * Clear the search term and remove all search result highlights.\n * @example editor.commands.clearSearch()\n */\n clearSearch: () => ReturnType\n }\n }\n}\n\nfunction mergePluginMeta(tr: Transaction, meta: FindAndReplaceMeta): void {\n const currentMeta = tr.getMeta(FindAndReplacePluginKey) as FindAndReplaceMeta | undefined\n\n tr.setMeta(FindAndReplacePluginKey, { ...currentMeta, ...meta })\n}\n\nfunction setPluginMeta(meta: FindAndReplaceMeta): Command {\n return ({ tr, dispatch }) => {\n if (dispatch) {\n mergePluginMeta(tr, meta)\n }\n\n return true\n }\n}\n\nfunction applySearchTerm(editor: Editor, term: string): void {\n const tr = editor.state.tr\n\n mergePluginMeta(tr, { searchTerm: term })\n editor.view.dispatch(tr)\n}\n\nfunction consumePendingSearch(editor: Editor): string | null {\n const state = getDebouncedSearchState(editor)\n\n if (state.pendingTerm === null) {\n return null\n }\n\n const pendingTerm = state.pendingTerm\n\n state.pendingTerm = null\n\n if (state.timeout !== null) {\n clearTimeout(state.timeout)\n state.timeout = null\n }\n\n return pendingTerm\n}\n\nfunction getPendingAwarePluginState(\n state: EditorState,\n pendingTerm: string | null,\n): FindAndReplacePluginState | null {\n const pluginState = FindAndReplacePluginKey.getState(state)\n\n if (!pluginState) {\n return null\n }\n\n if (pendingTerm === null) {\n return pluginState.results.length === 0 ? null : pluginState\n }\n\n const results = searchDocument(state.doc, pendingTerm, {\n caseSensitive: pluginState.caseSensitive,\n useRegex: pluginState.useRegex,\n wholeWord: pluginState.wholeWord,\n })\n\n if (results.length === 0) {\n return null\n }\n\n return {\n ...pluginState,\n searchTerm: pendingTerm,\n results,\n currentIndex: 0,\n }\n}\n\nfunction currentResult(pluginState: FindAndReplacePluginState): SearchResult | undefined {\n return pluginState.results[pluginState.currentIndex ?? 0]\n}\n\nfunction shiftIndex(currentIndex: number | null, length: number, direction: 1 | -1): number {\n if (currentIndex === null) {\n return direction === 1 ? 0 : length - 1\n }\n\n return (currentIndex + direction + length) % length\n}\n\nfunction selectResult(tr: Transaction, result: SearchResult, index: number): void {\n tr.setSelection(TextSelection.create(tr.doc, result.from, result.to))\n mergePluginMeta(tr, { currentIndex: index })\n tr.scrollIntoView()\n}\n\nfunction replaceResult(\n tr: Transaction,\n pluginState: FindAndReplacePluginState,\n result: SearchResult,\n): void {\n const { searchTerm, replaceTerm, caseSensitive, useRegex, wholeWord } = pluginState\n\n tr.insertText(replaceTerm, result.from, result.to)\n\n // Jump to the first result behind the inserted text, so a replacement\n // that still matches the search (e.g. \"foo\" -> \"foobar\") is skipped.\n const results = searchDocument(tr.doc, searchTerm, { caseSensitive, useRegex, wholeWord })\n const nextIndex = findNextIndex(results, result.from + replaceTerm.length)\n\n if (nextIndex === null) {\n mergePluginMeta(tr, { currentIndex: null })\n return\n }\n\n selectResult(tr, results[nextIndex], nextIndex)\n}\n\nexport const FindAndReplace = Extension.create<FindAndReplaceOptions, FindAndReplaceStorage>({\n name: 'findAndReplace',\n\n addOptions() {\n return {\n searchTerm: '',\n replaceTerm: '',\n caseSensitive: false,\n useRegex: false,\n wholeWord: false,\n searchDebounceMs: 250,\n injectCSS: true,\n injectNonce: undefined,\n }\n },\n\n addStorage() {\n return {\n searchTerm: this.options.searchTerm,\n replaceTerm: this.options.replaceTerm,\n caseSensitive: this.options.caseSensitive,\n useRegex: this.options.useRegex,\n wholeWord: this.options.wholeWord,\n results: [],\n currentIndex: null,\n }\n },\n\n addProseMirrorPlugins() {\n return [\n FindAndReplacePlugin(this.options, pluginState => {\n this.storage.results = pluginState.results\n this.storage.currentIndex = pluginState.currentIndex\n }),\n ]\n },\n\n onTransaction() {\n const pluginState = FindAndReplacePluginKey.getState(this.editor.state)\n\n if (!pluginState) {\n return\n }\n\n this.storage.searchTerm = pluginState.searchTerm\n this.storage.replaceTerm = pluginState.replaceTerm\n this.storage.caseSensitive = pluginState.caseSensitive\n this.storage.useRegex = pluginState.useRegex\n this.storage.wholeWord = pluginState.wholeWord\n this.storage.results = pluginState.results\n this.storage.currentIndex = pluginState.currentIndex\n },\n\n addCommands() {\n const scheduleSearchTerm = (term: string): boolean => {\n const state = getDebouncedSearchState(this.editor)\n\n if (this.options.searchDebounceMs <= 0) {\n if (state.timeout !== null) {\n clearTimeout(state.timeout)\n state.timeout = null\n }\n\n state.pendingTerm = null\n\n applySearchTerm(this.editor, term)\n\n return true\n }\n\n if (state.timeout !== null) {\n clearTimeout(state.timeout)\n }\n\n state.pendingTerm = term\n state.timeout = setTimeout(() => {\n const debouncedState = getDebouncedSearchState(this.editor)\n\n debouncedState.timeout = null\n\n if (debouncedState.pendingTerm === null) {\n return\n }\n\n const pendingTerm = debouncedState.pendingTerm\n\n debouncedState.pendingTerm = null\n applySearchTerm(this.editor, pendingTerm)\n }, this.options.searchDebounceMs)\n\n return true\n }\n\n return {\n setSearchTerm:\n term =>\n ({ dispatch }) => {\n if (!dispatch) {\n return true\n }\n\n return scheduleSearchTerm(term)\n },\n setReplaceTerm: term => setPluginMeta({ replaceTerm: term }),\n setCaseSensitive: caseSensitive => setPluginMeta({ caseSensitive }),\n setUseRegex: useRegex => setPluginMeta({ useRegex }),\n setWholeWord: wholeWord => setPluginMeta({ wholeWord }),\n clearSearch:\n () =>\n ({ tr, dispatch }) => {\n const state = getDebouncedSearchState(this.editor)\n\n if (state.timeout !== null) {\n clearTimeout(state.timeout)\n state.timeout = null\n }\n\n state.pendingTerm = null\n\n if (dispatch) {\n mergePluginMeta(tr, { searchTerm: '' })\n }\n\n return true\n },\n\n replace:\n () =>\n ({ tr, state, dispatch }) => {\n const pendingTerm = dispatch ? consumePendingSearch(this.editor) : null\n\n if (dispatch && pendingTerm !== null) {\n mergePluginMeta(tr, { searchTerm: pendingTerm })\n }\n\n const pluginState = getPendingAwarePluginState(state, pendingTerm)\n\n if (!pluginState) {\n return false\n }\n\n const result = currentResult(pluginState)\n\n if (!result) {\n return false\n }\n\n if (dispatch) {\n replaceResult(tr, pluginState, result)\n }\n\n return true\n },\n\n replaceAll:\n () =>\n ({ tr, state, dispatch }) => {\n const pendingTerm = dispatch ? consumePendingSearch(this.editor) : null\n\n if (dispatch && pendingTerm !== null) {\n mergePluginMeta(tr, { searchTerm: pendingTerm })\n }\n\n const pluginState = getPendingAwarePluginState(state, pendingTerm)\n\n if (!pluginState) {\n return false\n }\n\n if (dispatch) {\n replaceAllResults(tr, state, pluginState.results, pluginState.replaceTerm)\n }\n\n return true\n },\n\n goToNextResult:\n () =>\n ({ tr, state, dispatch }) => {\n const pendingTerm = dispatch ? consumePendingSearch(this.editor) : null\n\n if (dispatch && pendingTerm !== null) {\n mergePluginMeta(tr, { searchTerm: pendingTerm })\n }\n\n const pluginState = getPendingAwarePluginState(state, pendingTerm)\n\n if (!pluginState) {\n return false\n }\n\n const nextIndex = shiftIndex(pluginState.currentIndex, pluginState.results.length, 1)\n\n if (dispatch) {\n selectResult(tr, pluginState.results[nextIndex], nextIndex)\n }\n\n return true\n },\n\n goToPreviousResult:\n () =>\n ({ tr, state, dispatch }) => {\n const pendingTerm = dispatch ? consumePendingSearch(this.editor) : null\n\n if (dispatch && pendingTerm !== null) {\n mergePluginMeta(tr, { searchTerm: pendingTerm })\n }\n\n const pluginState = getPendingAwarePluginState(state, pendingTerm)\n\n if (!pluginState) {\n return false\n }\n\n const previousIndex = shiftIndex(pluginState.currentIndex, pluginState.results.length, -1)\n\n if (dispatch) {\n selectResult(tr, pluginState.results[previousIndex], previousIndex)\n }\n\n return true\n },\n }\n },\n\n onDestroy() {\n clearDebouncedSearchState(this.editor)\n },\n})\n\nexport default FindAndReplace\n","import type { Editor } from '@tiptap/core'\n\nexport interface DebouncedSearchState {\n timeout: ReturnType<typeof setTimeout> | null\n pendingTerm: string | null\n}\n\nconst debouncedSearchState = new WeakMap<Editor, DebouncedSearchState>()\n\nexport function getDebouncedSearchState(editor: Editor): DebouncedSearchState {\n const existingState = debouncedSearchState.get(editor)\n\n if (existingState) {\n return existingState\n }\n\n const nextState: DebouncedSearchState = {\n timeout: null,\n pendingTerm: null,\n }\n\n debouncedSearchState.set(editor, nextState)\n\n return nextState\n}\n\nexport function clearDebouncedSearchState(editor: Editor): void {\n const state = debouncedSearchState.get(editor)\n\n if (!state) {\n return\n }\n\n if (state.timeout !== null) {\n clearTimeout(state.timeout)\n }\n\n debouncedSearchState.delete(editor)\n}\n","import { Plugin, PluginKey } from '@tiptap/pm/state'\n\nimport { currentResultClass, resultClass } from '../constants/constants.js'\nimport { createStyleTag } from '../style/create-style-tag.js'\nimport type { FindAndReplaceMeta, FindAndReplacePluginState } from './plugin-state.js'\nimport { style } from '../style/style.js'\nimport type { FindAndReplaceOptions } from '../types.js'\nimport { createState, updateState } from '../utils/index.js'\n\nexport { currentResultClass, resultClass }\nexport type { FindAndReplaceMeta, FindAndReplacePluginState }\n\nexport const FindAndReplacePluginKey = new PluginKey<FindAndReplacePluginState>('findAndReplace')\n\nexport const FindAndReplacePlugin = (\n options: FindAndReplaceOptions,\n onCreate: (pluginState: FindAndReplacePluginState) => void,\n) => {\n return new Plugin<FindAndReplacePluginState>({\n key: FindAndReplacePluginKey,\n\n state: {\n init: (_config, state) => {\n if (options.injectCSS && typeof document !== 'undefined') {\n createStyleTag(style, options.injectNonce)\n }\n\n const pluginState = createState(state.doc, options)\n onCreate(pluginState)\n\n return pluginState\n },\n\n apply: (tr, state, _oldState, newState) => {\n const meta = tr.getMeta(FindAndReplacePluginKey) as FindAndReplaceMeta | undefined\n\n if (!meta && !tr.docChanged) {\n return state\n }\n\n return updateState(\n state,\n { ...state, ...meta },\n newState.doc,\n meta,\n tr.docChanged,\n tr.mapping,\n )\n },\n },\n\n props: {\n decorations(state) {\n return FindAndReplacePluginKey.getState(state)?.decorations\n },\n },\n })\n}\n\nexport default FindAndReplacePlugin\n","export const resultClass = 'find-and-replace-result'\nexport const currentResultClass = 'find-and-replace-result-current'\n","export const createStyleTag = (style: string, nonce?: string): HTMLStyleElement => {\n const existing = document.querySelector<HTMLStyleElement>(\n 'style[data-tiptap-extension-find-and-replace-style]',\n )\n\n if (existing !== null) {\n return existing\n }\n\n const styleNode = document.createElement('style')\n\n if (nonce) {\n styleNode.setAttribute('nonce', nonce)\n }\n\n styleNode.setAttribute('data-tiptap-extension-find-and-replace-style', '')\n styleNode.textContent = style\n document.getElementsByTagName('head')[0].appendChild(styleNode)\n\n return styleNode\n}\n\nexport default createStyleTag\n","export const style = `.find-and-replace-result {\n background-color: rgb(255 225 0 / 0.4);\n}\n\n.find-and-replace-result-current {\n background-color: rgb(255 165 0 / 0.55);\n}\n`\n\nexport default style\n","import { RE2JS } from 're2js'\n\nimport type { SearchOptions } from './types.js'\n\nexport type SearchRegex = RegExp | RE2JS\n\nconst escapeRegExp = (value: string): string => {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/** Unicode word characters like letters, accents, numbers, and underscore-like punctuation. */\nconst unicodeWordCharacter = '[\\\\p{L}\\\\p{M}\\\\p{N}\\\\p{Pc}]'\n\nfunction compileNativeRegex(source: string, caseSensitive: boolean): RegExp | null {\n try {\n return new RegExp(source, caseSensitive ? 'gu' : 'giu')\n } catch {\n return null\n }\n}\n\nfunction compileSafeRegex(source: string, caseSensitive: boolean): RE2JS | null {\n try {\n const flags = caseSensitive ? 0 : RE2JS.CASE_INSENSITIVE\n\n return RE2JS.compile(source, flags)\n } catch {\n return null\n }\n}\n\nfunction compileRegex(\n source: string,\n options: Pick<SearchOptions, 'caseSensitive' | 'useRegex'>,\n): SearchRegex | null {\n return options.useRegex\n ? compileSafeRegex(source, options.caseSensitive)\n : compileNativeRegex(source, options.caseSensitive)\n}\n\n/**\n * Creates a search matcher for a term.\n * Regex mode uses RE2-compatible syntax to avoid catastrophic backtracking.\n * @param term The search term, treated as regex source when `useRegex` is enabled.\n * @param options Case sensitivity, regex mode, and whole word matching.\n * @returns A compiled matcher, or `null` when the term is invalid or unsupported.\n */\nexport function createSearchRegex(term: string, options: SearchOptions): SearchRegex | null {\n if (!term) {\n return null\n }\n\n let source: string\n\n if (options.useRegex) {\n source = term\n } else {\n const escaped = escapeRegExp(term)\n\n // Whole-word searches reject terms next to another Unicode word character.\n source = options.wholeWord\n ? `(?<!${unicodeWordCharacter})${escaped}(?!${unicodeWordCharacter})`\n : escaped\n }\n\n return compileRegex(source, options)\n}\n","import { RE2JS } from 're2js'\n\nimport type { SearchRegex } from './regex.js'\n\ninterface TextMatch {\n index: number\n value: string\n}\n\nfunction* findNativeMatches(regex: RegExp, text: string): Generator<TextMatch> {\n for (const match of text.matchAll(regex)) {\n yield { index: match.index, value: match[0] }\n }\n}\n\nfunction* findSafeMatches(regex: RE2JS, text: string): Generator<TextMatch> {\n const matcher = regex.matcher(text)\n\n while (matcher.find()) {\n yield { index: matcher.start(), value: matcher.group() ?? '' }\n }\n}\n\nexport function findMatches(regex: SearchRegex, text: string): Generator<TextMatch> {\n return regex instanceof RegExp ? findNativeMatches(regex, text) : findSafeMatches(regex, text)\n}\n","import type { Node } from '@tiptap/pm/model'\n\ninterface TextSegment {\n isText: boolean\n pos: number\n length: number\n text: string\n textOffset: number\n}\n\n// Non-text inline nodes (hard break, mention, ...) contribute a placeholder\n// so matches never silently span across them.\nexport function getTextSegments(textblock: Node, pos: number): TextSegment[] {\n const segments: TextSegment[] = []\n let textOffset = 0\n\n textblock.forEach((child, offset) => {\n const text = child.isText ? (child.text ?? '') : '\\n'\n\n segments.push({\n isText: child.isText,\n pos: pos + 1 + offset,\n length: child.nodeSize,\n text,\n textOffset,\n })\n textOffset += text.length\n })\n\n return segments\n}\n\nfunction findOffsetSegment(segments: TextSegment[], offset: number): TextSegment | undefined {\n let low = 0\n let high = segments.length\n\n while (low < high) {\n const middle = Math.floor((low + high) / 2)\n const segment = segments[middle]\n const segmentEnd = segment.textOffset + segment.text.length\n\n if (offset < segmentEnd) {\n high = middle\n } else {\n low = middle + 1\n }\n }\n\n return segments[low] ?? segments.at(-1)\n}\n\nexport function offsetToPos(segments: TextSegment[], offset: number): number {\n const segment = findOffsetSegment(segments, offset)\n\n return segment ? segment.pos + Math.min(offset - segment.textOffset, segment.length) : 0\n}\n\nexport function overlapsNonTextSegment(segments: TextSegment[], from: number, to: number): boolean {\n return segments.some(\n segment =>\n !segment.isText && from < segment.textOffset + segment.text.length && to > segment.textOffset,\n )\n}\n","import type { Node } from '@tiptap/pm/model'\n\nimport { findMatches } from './matches.js'\nimport type { SearchRegex } from './regex.js'\nimport { getTextSegments, offsetToPos, overlapsNonTextSegment } from './text-segments.js'\nimport type { SearchResult } from './types.js'\n\nexport function searchTextblock(regex: SearchRegex, textblock: Node, pos: number): SearchResult[] {\n const segments = getTextSegments(textblock, pos)\n const text = segments.map(segment => segment.text).join('')\n const results: SearchResult[] = []\n\n for (const match of findMatches(regex, text)) {\n if (match.value.length === 0) {\n continue\n }\n\n const matchEnd = match.index + match.value.length\n\n if (overlapsNonTextSegment(segments, match.index, matchEnd)) {\n continue\n }\n\n results.push({\n from: offsetToPos(segments, match.index),\n to: offsetToPos(segments, matchEnd),\n })\n }\n\n return results\n}\n","import type { Node } from '@tiptap/pm/model'\n\nimport { createSearchRegex } from './regex.js'\nimport { searchTextblock } from './textblock-search.js'\nimport type { SearchOptions, SearchResult, TextblockSearchTarget } from './types.js'\n\nexport type { SearchOptions, SearchResult, TextblockSearchTarget } from './types.js'\nexport { createSearchRegex } from './regex.js'\n\n/**\n * Searches textblocks for a term and returns their matched ranges.\n * @param textblocks The textblocks and their document positions.\n * @param term The search term, treated as regex source when `useRegex` is enabled.\n * @param options Case sensitivity, regex mode, and whole word matching.\n * @returns The list of matched ranges in textblock order.\n */\nexport function searchTextblocks(\n textblocks: readonly TextblockSearchTarget[],\n term: string,\n options: SearchOptions,\n): SearchResult[] {\n const regex = createSearchRegex(term, options)\n\n if (!regex) {\n return []\n }\n\n return textblocks.flatMap(({ node, pos }) => searchTextblock(regex, node, pos))\n}\n\n/**\n * Searches all textblocks of a document for a term and returns the matched ranges.\n * Matches may span multiple text nodes (e.g. across marks), but never leave a textblock.\n * @param doc The document to search in.\n * @param term The search term, treated as regex source when `useRegex` is enabled.\n * @param options Case sensitivity, regex mode, and whole word matching.\n * @returns The list of matched ranges in document order.\n */\nexport function searchDocument(doc: Node, term: string, options: SearchOptions): SearchResult[] {\n const textblocks: TextblockSearchTarget[] = []\n\n doc.descendants((node, pos) => {\n if (!node.isTextblock) {\n return true\n }\n\n textblocks.push({ node, pos })\n\n return false\n })\n\n return searchTextblocks(textblocks, term, options)\n}\n\n/**\n * Finds the index of the first result at or after a position, wrapping around\n * to the first result when the position is behind the last result.\n * @param results The current search results.\n * @param from The position to search from.\n * @returns The result index, or `null` when there are no results.\n */\nexport function findNextIndex(results: SearchResult[], from: number): number | null {\n if (results.length === 0) {\n return null\n }\n\n const index = results.findIndex(result => result.from >= from)\n\n return index === -1 ? 0 : index\n}\n","import type { Node } from '@tiptap/pm/model'\nimport { DecorationSet } from '@tiptap/pm/view'\n\nimport type { SearchResult } from '../search/search.js'\n\nimport { createDecoration } from './createDecoration.js'\n\nexport function createDecorations(\n doc: Node,\n results: SearchResult[],\n currentIndex: number | null,\n): DecorationSet {\n const decorations = results.map((result, index) => createDecoration(result, currentIndex, index))\n\n return DecorationSet.create(doc, decorations)\n}\n","import { Decoration } from '@tiptap/pm/view'\n\nimport { currentResultClass, resultClass } from '../constants/constants.js'\nimport type { SearchResult } from '../search/search.js'\n\nexport function createDecoration(\n result: SearchResult,\n currentIndex: number | null,\n index: number,\n): Decoration {\n const className = index === currentIndex ? `${resultClass} ${currentResultClass}` : resultClass\n\n return Decoration.inline(result.from, result.to, { class: className })\n}\n","import type { Node } from '@tiptap/pm/model'\n\nimport type { FindAndReplaceOptions } from '../types.js'\nimport type { FindAndReplacePluginState } from '../plugin/plugin-state.js'\nimport { searchDocument } from '../search/search.js'\n\nimport { createDecorations } from './createDecorations.js'\n\nexport function createState(doc: Node, options: FindAndReplaceOptions): FindAndReplacePluginState {\n const results = searchDocument(doc, options.searchTerm, options)\n const currentIndex = results.length > 0 ? 0 : null\n\n return {\n searchTerm: options.searchTerm,\n replaceTerm: options.replaceTerm,\n caseSensitive: options.caseSensitive,\n useRegex: options.useRegex,\n wholeWord: options.wholeWord,\n results,\n currentIndex,\n decorations: createDecorations(doc, results, currentIndex),\n }\n}\n","import type { FindAndReplaceMeta } from '../plugin/plugin-state.js'\n\nexport function hasCurrentIndex(meta: FindAndReplaceMeta | undefined): boolean {\n return !!meta && 'currentIndex' in meta\n}\n","import { Decoration, DecorationSet } from '@tiptap/pm/view'\n\nimport type { SearchResult } from '../search/search.js'\n\nimport type { TextblockRange } from './types.js'\n\nexport function findDecorations(\n decorations: DecorationSet,\n results: readonly SearchResult[],\n textblocks: readonly TextblockRange[],\n): Decoration[] {\n const found = new Set<Decoration>()\n\n textblocks.forEach(textblock => {\n decorations.find(textblock.from, textblock.to).forEach(decoration => found.add(decoration))\n })\n\n results.forEach(result => {\n decorations\n .find(result.from, result.to, decoration => {\n return decoration.from === result.from && decoration.to === result.to\n })\n .forEach(decoration => found.add(decoration))\n })\n\n return [...found]\n}\n","import type { SearchResult } from '../search/search.js'\n\nexport function sameResult(first: SearchResult, second: SearchResult): boolean {\n return first.from === second.from && first.to === second.to\n}\n","import type { SearchResult } from '../search/search.js'\n\nimport { sameResult } from './sameResult.js'\n\nexport function isCurrentResult(result: SearchResult, current: SearchResult | null): boolean {\n return current !== null && sameResult(result, current)\n}\n","import type { SearchResult } from '../search/search.js'\n\nimport type { TextblockRange } from './types.js'\n\nexport function resultInTextblocks(\n result: SearchResult,\n textblocks: readonly TextblockRange[],\n): boolean {\n let start = 0\n let end = textblocks.length - 1\n\n while (start <= end) {\n const index = Math.floor((start + end) / 2)\n const textblock = textblocks[index]\n\n if (result.from < textblock.from) {\n end = index - 1\n continue\n }\n\n if (result.to > textblock.to) {\n start = index + 1\n continue\n }\n\n return true\n }\n\n return false\n}\n","import type { SearchResult } from '../search/search.js'\n\nimport { isCurrentResult } from './isCurrentResult.js'\nimport { resultInTextblocks } from './resultInTextblocks.js'\nimport type { TextblockRange } from './types.js'\n\nexport function shouldRefreshResult(\n result: SearchResult,\n textblocks: readonly TextblockRange[],\n previousCurrent: SearchResult | null,\n current: SearchResult | null,\n): boolean {\n return (\n resultInTextblocks(result, textblocks) ||\n isCurrentResult(result, previousCurrent) ||\n isCurrentResult(result, current)\n )\n}\n","import type { SearchResult } from '../search/search.js'\n\nimport { shouldRefreshResult } from './shouldRefreshResult.js'\nimport type { IndexedResult, TextblockRange } from './types.js'\n\nexport function getResultsToRefresh(\n results: SearchResult[],\n textblocks: readonly TextblockRange[],\n previousCurrent: SearchResult | null,\n currentIndex: number | null,\n): IndexedResult[] {\n const current = currentIndex === null ? null : (results[currentIndex] ?? null)\n const refreshedResults: IndexedResult[] = []\n\n results.forEach((result, index) => {\n if (shouldRefreshResult(result, textblocks, previousCurrent, current)) {\n refreshedResults.push({ result, index })\n }\n })\n\n return refreshedResults\n}\n","import type { Node } from '@tiptap/pm/model'\nimport { DecorationSet } from '@tiptap/pm/view'\n\nimport type { SearchResult } from '../search/search.js'\n\nimport { createDecoration } from './createDecoration.js'\nimport { findDecorations } from './findDecorations.js'\nimport { getResultsToRefresh } from './getResultsToRefresh.js'\nimport type { TextblockRange } from './types.js'\n\nexport function refreshDecorations(\n decorations: DecorationSet,\n doc: Node,\n results: SearchResult[],\n currentIndex: number | null,\n textblocks: readonly TextblockRange[],\n previousCurrent: SearchResult | null,\n): DecorationSet {\n const resultsToRefresh = getResultsToRefresh(results, textblocks, previousCurrent, currentIndex)\n const refreshedResults = resultsToRefresh.map(({ result }) => result)\n const staleResults = previousCurrent ? [...refreshedResults, previousCurrent] : refreshedResults\n const decorationsToRemove = findDecorations(decorations, staleResults, textblocks)\n const decorationsToAdd = resultsToRefresh.map(({ result, index }) => {\n return createDecoration(result, currentIndex, index)\n })\n\n return decorations.remove(decorationsToRemove).add(doc, decorationsToAdd)\n}\n","export function normalizeCurrentIndex(\n currentIndex: number | null | undefined,\n length: number,\n): number | null {\n if (currentIndex === undefined || currentIndex === null || length === 0) {\n return null\n }\n\n return Math.max(0, Math.min(currentIndex, length - 1))\n}\n","import type { FindAndReplaceMeta } from '../plugin/plugin-state.js'\n\nimport { hasCurrentIndex } from './hasCurrentIndex.js'\nimport { normalizeCurrentIndex } from './normalizeCurrentIndex.js'\n\nexport function getMetaCurrentIndex(\n meta: FindAndReplaceMeta | undefined,\n length: number,\n): number | null | undefined {\n if (!meta) {\n return undefined\n }\n\n if (!hasCurrentIndex(meta)) {\n return undefined\n }\n\n return normalizeCurrentIndex(meta.currentIndex, length)\n}\n","import type { FindAndReplacePluginState } from '../plugin/plugin-state.js'\n\nexport function getFallbackCurrentIndex(\n previousState: FindAndReplacePluginState,\n length: number,\n): number {\n return previousState.currentIndex === null ? 0 : Math.min(previousState.currentIndex, length - 1)\n}\n","import type { FindAndReplacePluginState } from '../plugin/plugin-state.js'\nimport type { SearchResult } from '../search/search.js'\n\nexport function getPreviousCurrentResult(\n previousState: FindAndReplacePluginState,\n): SearchResult | undefined {\n return previousState.currentIndex === null\n ? undefined\n : previousState.results[previousState.currentIndex]\n}\n","import type { Mapping } from '@tiptap/pm/transform'\n\nimport type { FindAndReplacePluginState } from '../plugin/plugin-state.js'\nimport type { SearchResult } from '../search/search.js'\nimport { findNextIndex } from '../search/search.js'\n\nimport { getPreviousCurrentResult } from './getPreviousCurrentResult.js'\n\nexport function getMappedCurrentIndex(\n previousState: FindAndReplacePluginState,\n results: SearchResult[],\n docChanged: boolean,\n mapping: Mapping | undefined,\n): number | null | undefined {\n if (!docChanged) {\n return undefined\n }\n\n if (!mapping) {\n return undefined\n }\n\n const previousResult = getPreviousCurrentResult(previousState)\n\n if (!previousResult) {\n return undefined\n }\n\n const mappedFrom = mapping.map(previousResult.from, 1)\n\n return findNextIndex(results, mappedFrom)\n}\n","import type { FindAndReplaceMeta } from '../plugin/plugin-state.js'\n\nexport function isNewSearch(meta: FindAndReplaceMeta | undefined): boolean {\n return !!meta && 'searchTerm' in meta\n}\n","import type { Mapping } from '@tiptap/pm/transform'\n\nimport type { FindAndReplaceMeta, FindAndReplacePluginState } from '../plugin/plugin-state.js'\nimport type { SearchResult } from '../search/search.js'\n\nimport { getFallbackCurrentIndex } from './getFallbackCurrentIndex.js'\nimport { getMappedCurrentIndex } from './getMappedCurrentIndex.js'\nimport { isNewSearch } from './isNewSearch.js'\n\nexport function getSearchCurrentIndex(\n previousState: FindAndReplacePluginState,\n results: SearchResult[],\n meta: FindAndReplaceMeta | undefined,\n docChanged: boolean,\n mapping: Mapping | undefined,\n): number {\n if (isNewSearch(meta)) {\n return 0\n }\n\n const mappedIndex = getMappedCurrentIndex(previousState, results, docChanged, mapping)\n\n return mappedIndex ?? getFallbackCurrentIndex(previousState, results.length)\n}\n","import type { Mapping } from '@tiptap/pm/transform'\n\nimport type { FindAndReplaceMeta, FindAndReplacePluginState } from '../plugin/plugin-state.js'\nimport type { SearchResult } from '../search/search.js'\n\nimport { getMetaCurrentIndex } from './getMetaCurrentIndex.js'\nimport { getSearchCurrentIndex } from './getSearchCurrentIndex.js'\n\nexport function resolveCurrentIndex(\n previousState: FindAndReplacePluginState,\n results: SearchResult[],\n meta: FindAndReplaceMeta | undefined,\n docChanged: boolean,\n mapping: Mapping | undefined,\n): number | null {\n if (results.length === 0) {\n return null\n }\n\n const metaIndex = getMetaCurrentIndex(meta, results.length)\n\n if (metaIndex !== undefined) {\n return metaIndex\n }\n\n return getSearchCurrentIndex(previousState, results, meta, docChanged, mapping)\n}\n","import type { Node } from '@tiptap/pm/model'\n\nimport type { FindAndReplaceMeta, FindAndReplacePluginState } from '../plugin/plugin-state.js'\n\nimport { refreshDecorations } from './refreshDecorations.js'\nimport { resolveCurrentIndex } from './resolveCurrentIndex.js'\n\nexport function refreshCurrentIndexState(\n previousState: FindAndReplacePluginState,\n state: FindAndReplacePluginState,\n doc: Node,\n meta: FindAndReplaceMeta | undefined,\n): FindAndReplacePluginState {\n const currentIndex = resolveCurrentIndex(previousState, state.results, meta, false, undefined)\n const previousCurrent =\n previousState.currentIndex === null ? null : previousState.results[previousState.currentIndex]\n\n return {\n ...state,\n currentIndex,\n decorations: refreshDecorations(\n previousState.decorations,\n doc,\n state.results,\n currentIndex,\n [],\n previousCurrent,\n ),\n }\n}\n","import type { Node } from '@tiptap/pm/model'\nimport type { Mapping } from '@tiptap/pm/transform'\n\nimport type { TextblockRange } from './types.js'\n\nexport function getChangedTextblocks(doc: Node, mapping: Mapping): TextblockRange[] {\n const textblocks = new Map<number, TextblockRange>()\n\n mapping.maps.forEach((stepMap, index) => {\n const remaining = mapping.slice(index + 1)\n\n stepMap.forEach((_oldStart, _oldEnd, newStart, newEnd) => {\n const from = Math.max(0, remaining.map(newStart, -1) - 1)\n const to = Math.min(doc.content.size, remaining.map(newEnd, 1) + 1)\n\n doc.nodesBetween(from, to, (node, pos) => {\n if (!node.isTextblock) {\n return true\n }\n\n textblocks.set(pos, { node, pos, from: pos, to: pos + node.nodeSize })\n\n return false\n })\n })\n })\n\n return [...textblocks.values()].sort((first, second) => first.pos - second.pos)\n}\n","import type { Mapping } from '@tiptap/pm/transform'\n\nimport type { SearchResult } from '../search/search.js'\n\nexport function mapResult(result: SearchResult, mapping: Mapping): SearchResult | null {\n const from = mapping.map(result.from, 1)\n const to = mapping.map(result.to, -1)\n\n return from < to ? { from, to } : null\n}\n","import type { Mapping } from '@tiptap/pm/transform'\n\nimport type { SearchResult } from '../search/search.js'\n\nimport { mapResult } from './mapResult.js'\n\nexport function mapResults(results: SearchResult[], mapping: Mapping): SearchResult[] {\n const mappedResults: SearchResult[] = []\n\n results.forEach(result => {\n const mapped = mapResult(result, mapping)\n\n if (mapped) {\n mappedResults.push(mapped)\n }\n })\n\n return mappedResults\n}\n","import type { SearchResult } from '../search/search.js'\n\nexport function mergeResults(first: SearchResult[], second: SearchResult[]): SearchResult[] {\n const results: SearchResult[] = []\n let firstIndex = 0\n let secondIndex = 0\n\n while (firstIndex < first.length && secondIndex < second.length) {\n if (first[firstIndex].from < second[secondIndex].from) {\n results.push(first[firstIndex++])\n continue\n }\n\n results.push(second[secondIndex++])\n }\n\n return results.concat(first.slice(firstIndex), second.slice(secondIndex))\n}\n","import type { Node } from '@tiptap/pm/model'\nimport type { Mapping } from '@tiptap/pm/transform'\n\nimport type { FindAndReplaceMeta, FindAndReplacePluginState } from '../plugin/plugin-state.js'\nimport { searchTextblocks } from '../search/search.js'\n\nimport { getChangedTextblocks } from './getChangedTextblocks.js'\nimport { mapResult } from './mapResult.js'\nimport { mapResults } from './mapResults.js'\nimport { mergeResults } from './mergeResults.js'\nimport { refreshDecorations } from './refreshDecorations.js'\nimport { resolveCurrentIndex } from './resolveCurrentIndex.js'\nimport { resultInTextblocks } from './resultInTextblocks.js'\n\nexport function refreshDocumentState(\n previousState: FindAndReplacePluginState,\n state: FindAndReplacePluginState,\n doc: Node,\n meta: FindAndReplaceMeta | undefined,\n mapping: Mapping,\n): FindAndReplacePluginState {\n const textblocks = getChangedTextblocks(doc, mapping)\n const mappedResults = mapResults(previousState.results, mapping)\n const unaffectedResults = mappedResults.filter(result => !resultInTextblocks(result, textblocks))\n const changedResults = searchTextblocks(textblocks, state.searchTerm, state)\n const results = mergeResults(unaffectedResults, changedResults)\n const currentIndex = resolveCurrentIndex(previousState, results, meta, true, mapping)\n const previousCurrent =\n previousState.currentIndex === null\n ? null\n : mapResult(previousState.results[previousState.currentIndex], mapping)\n const decorations = previousState.decorations.map(mapping, doc)\n\n return {\n ...state,\n results,\n currentIndex,\n decorations: refreshDecorations(\n decorations,\n doc,\n results,\n currentIndex,\n textblocks,\n previousCurrent,\n ),\n }\n}\n","import type { Node } from '@tiptap/pm/model'\nimport type { Mapping } from '@tiptap/pm/transform'\n\nimport type { FindAndReplaceMeta, FindAndReplacePluginState } from '../plugin/plugin-state.js'\nimport { searchDocument } from '../search/search.js'\n\nimport { createDecorations } from './createDecorations.js'\nimport { resolveCurrentIndex } from './resolveCurrentIndex.js'\n\nexport function refreshState(\n previousState: FindAndReplacePluginState,\n state: FindAndReplacePluginState,\n doc: Node,\n meta: FindAndReplaceMeta | undefined,\n docChanged: boolean,\n mapping: Mapping | undefined,\n): FindAndReplacePluginState {\n const results = searchDocument(doc, state.searchTerm, state)\n const currentIndex = resolveCurrentIndex(previousState, results, meta, docChanged, mapping)\n\n return {\n ...state,\n results,\n currentIndex,\n decorations: createDecorations(doc, results, currentIndex),\n }\n}\n","import type { FindAndReplaceMeta } from '../plugin/plugin-state.js'\n\nconst searchKeys = ['searchTerm', 'caseSensitive', 'useRegex', 'wholeWord'] as const\n\nexport function touchesSearch(meta: FindAndReplaceMeta | undefined): boolean {\n return !!meta && searchKeys.some(key => key in meta)\n}\n","import type { Node } from '@tiptap/pm/model'\nimport type { Mapping } from '@tiptap/pm/transform'\n\nimport type { FindAndReplaceMeta, FindAndReplacePluginState } from '../plugin/plugin-state.js'\n\nimport { hasCurrentIndex } from './hasCurrentIndex.js'\nimport { refreshCurrentIndexState } from './refreshCurrentIndexState.js'\nimport { refreshDocumentState } from './refreshDocumentState.js'\nimport { refreshState } from './refreshState.js'\nimport { touchesSearch } from './touchesSearch.js'\n\nexport function updateState(\n previousState: FindAndReplacePluginState,\n state: FindAndReplacePluginState,\n doc: Node,\n meta: FindAndReplaceMeta | undefined,\n docChanged: boolean,\n mapping: Mapping,\n): FindAndReplacePluginState {\n if (touchesSearch(meta)) {\n return refreshState(previousState, state, doc, meta, docChanged, mapping)\n }\n\n if (docChanged) {\n return refreshDocumentState(previousState, state, doc, meta, mapping)\n }\n\n if (hasCurrentIndex(meta)) {\n return refreshCurrentIndexState(previousState, state, doc, meta)\n }\n\n return state\n}\n","import type { EditorState } from '@tiptap/pm/state'\n\nimport type { SearchResult } from '../search/search.js'\n\nexport interface TextNodeResult {\n from: number\n text: string\n}\n\nexport function getTextNodeResult(state: EditorState, result: SearchResult): TextNodeResult | null {\n const $from = state.doc.resolve(result.from)\n const node = $from.parent.child($from.index())\n\n if (!node.isText || !node.text) {\n return null\n }\n\n const from = result.from - $from.textOffset\n\n return result.to <= from + node.nodeSize ? { from, text: node.text } : null\n}\n","import type { EditorState } from '@tiptap/pm/state'\n\nimport type { SearchResult } from '../search/search.js'\nimport type { TextNodeResult } from './getTextNodeResult.js'\nimport { getTextNodeResult } from './getTextNodeResult.js'\n\nexport interface ResultGroup {\n from: number\n text: string | null\n results: SearchResult[]\n}\n\nfunction shouldMergeResult(\n group: ResultGroup | undefined,\n textNode: TextNodeResult | null,\n): boolean {\n if (!group || !textNode) {\n return false\n }\n\n return group.from === textNode.from && group.text !== null\n}\n\nfunction createTextResultGroup(result: SearchResult, textNode: TextNodeResult): ResultGroup {\n return {\n from: textNode.from,\n text: textNode.text,\n results: [result],\n }\n}\n\nfunction createResultGroup(result: SearchResult): ResultGroup {\n return {\n from: result.from,\n text: null,\n results: [result],\n }\n}\n\nfunction appendResult(\n groups: ResultGroup[],\n result: SearchResult,\n textNode: TextNodeResult | null,\n): void {\n const group = groups.at(-1)\n\n if (shouldMergeResult(group, textNode)) {\n group!.results.push(result)\n return\n }\n\n if (textNode) {\n groups.push(createTextResultGroup(result, textNode))\n return\n }\n\n groups.push(createResultGroup(result))\n}\n\nexport function groupResults(state: EditorState, results: SearchResult[]): ResultGroup[] {\n const groups: ResultGroup[] = []\n\n for (const result of results) {\n appendResult(groups, result, getTextNodeResult(state, result))\n }\n\n return groups\n}\n","import type { Transaction } from '@tiptap/pm/state'\n\nimport type { SearchResult } from '../search/search.js'\nimport type { ResultGroup } from './groupResults.js'\n\nfunction replaceSeparateResults(\n tr: Transaction,\n results: SearchResult[],\n replaceTerm: string,\n): void {\n for (const result of [...results].reverse()) {\n tr.insertText(replaceTerm, result.from, result.to)\n }\n}\n\nfunction createReplacement(\n group: ResultGroup,\n text: string,\n firstResult: SearchResult,\n replaceTerm: string,\n): string {\n let offset = firstResult.from - group.from\n const replacement: string[] = []\n\n for (const result of group.results) {\n const from = result.from - group.from\n const to = result.to - group.from\n\n replacement.push(text.slice(offset, from), replaceTerm)\n offset = to\n }\n\n return replacement.join('')\n}\n\nexport function replaceGroup(tr: Transaction, group: ResultGroup, replaceTerm: string): void {\n const firstResult = group.results[0]\n const lastResult = group.results.at(-1)\n\n if (!lastResult) {\n return\n }\n\n if (group.text === null) {\n replaceSeparateResults(tr, group.results, replaceTerm)\n return\n }\n\n tr.insertText(\n createReplacement(group, group.text, firstResult, replaceTerm),\n firstResult.from,\n lastResult.to,\n )\n}\n","import type { EditorState, Transaction } from '@tiptap/pm/state'\n\nimport type { SearchResult } from '../search/search.js'\nimport { groupResults } from './groupResults.js'\nimport { replaceGroup } from './replaceGroup.js'\n\nexport function replaceAllResults(\n tr: Transaction,\n state: EditorState,\n results: SearchResult[],\n replaceTerm: string,\n): void {\n const groups = groupResults(state, results)\n\n for (const group of groups.reverse()) {\n replaceGroup(tr, group, replaceTerm)\n }\n}\n","import { FindAndReplace } from './find-and-replace.js'\n\nexport * from './find-and-replace.js'\nexport * from './plugin/plugin.js'\nexport * from './search/search.js'\nexport * from './types.js'\n\nexport default FindAndReplace\n"],"mappings":"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","names":["style"]}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@tiptap/extension-find-and-replace",
3
+ "version": "3.29.0",
4
+ "description": "find and replace extension for tiptap",
5
+ "keywords": [
6
+ "tiptap",
7
+ "tiptap extension"
8
+ ],
9
+ "homepage": "https://tiptap.dev",
10
+ "bugs": {
11
+ "url": "https://github.com/ueberdosis/tiptap/issues"
12
+ },
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/ueberdosis/tiptap",
17
+ "directory": "packages/extension-find-and-replace"
18
+ },
19
+ "funding": {
20
+ "type": "github",
21
+ "url": "https://github.com/sponsors/ueberdosis"
22
+ },
23
+ "files": [
24
+ "src",
25
+ "dist"
26
+ ],
27
+ "type": "module",
28
+ "main": "dist/index.cjs",
29
+ "module": "dist/index.js",
30
+ "types": "dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": {
34
+ "import": "./dist/index.d.ts",
35
+ "require": "./dist/index.d.cts"
36
+ },
37
+ "import": "./dist/index.js",
38
+ "require": "./dist/index.cjs"
39
+ }
40
+ },
41
+ "dependencies": {
42
+ "re2js": "^2.8.6"
43
+ },
44
+ "devDependencies": {
45
+ "@tiptap/core": "^3.29.0",
46
+ "@tiptap/pm": "^3.29.0"
47
+ },
48
+ "peerDependencies": {
49
+ "@tiptap/core": "3.29.0",
50
+ "@tiptap/pm": "3.29.0"
51
+ },
52
+ "scripts": {
53
+ "build": "tsup"
54
+ }
55
+ }
@@ -0,0 +1,2 @@
1
+ export const resultClass = 'find-and-replace-result'
2
+ export const currentResultClass = 'find-and-replace-result-current'
@@ -0,0 +1,434 @@
1
+ import type { Command, Editor } from '@tiptap/core'
2
+ import { Extension } from '@tiptap/core'
3
+ import type { EditorState, Transaction } from '@tiptap/pm/state'
4
+ import { TextSelection } from '@tiptap/pm/state'
5
+
6
+ import {
7
+ clearDebouncedSearchState,
8
+ getDebouncedSearchState,
9
+ } from './state/debounced-search-state.js'
10
+ import type { FindAndReplaceMeta, FindAndReplacePluginState } from './plugin/plugin.js'
11
+ import { FindAndReplacePlugin, FindAndReplacePluginKey } from './plugin/plugin.js'
12
+ import { findNextIndex, searchDocument } from './search/search.js'
13
+ import type { SearchResult } from './search/search.js'
14
+ import type { FindAndReplaceOptions, FindAndReplaceStorage } from './types.js'
15
+ import { replaceAllResults } from './utils/replaceAllResults.js'
16
+
17
+ declare module '@tiptap/core' {
18
+ interface Commands<ReturnType> {
19
+ findAndReplace: {
20
+ /**
21
+ * Set the search term and highlight all matches in the document.
22
+ * @param term The search term, treated as regex source when regex mode is enabled.
23
+ * @example editor.commands.setSearchTerm('Hello')
24
+ */
25
+ setSearchTerm: (term: string) => ReturnType
26
+
27
+ /**
28
+ * Set the replace term used by the replace commands.
29
+ * @param term The replacement text.
30
+ * @example editor.commands.setReplaceTerm('World')
31
+ */
32
+ setReplaceTerm: (term: string) => ReturnType
33
+
34
+ /**
35
+ * Set whether the search is case sensitive.
36
+ * @param caseSensitive The new case sensitivity.
37
+ * @example editor.commands.setCaseSensitive(true)
38
+ */
39
+ setCaseSensitive: (caseSensitive: boolean) => ReturnType
40
+
41
+ /**
42
+ * Set whether the search term is treated as an RE2-compatible regular expression.
43
+ * Lookarounds and backreferences are not supported.
44
+ * @param useRegex The new regex mode.
45
+ * @example editor.commands.setUseRegex(true)
46
+ */
47
+ setUseRegex: (useRegex: boolean) => ReturnType
48
+
49
+ /**
50
+ * Set whether to match whole words only. Ignored when regex mode is enabled.
51
+ * @param wholeWord The new whole word mode.
52
+ * @example editor.commands.setWholeWord(true)
53
+ */
54
+ setWholeWord: (wholeWord: boolean) => ReturnType
55
+
56
+ /**
57
+ * Replace the current search result and jump to the next one.
58
+ * @example editor.commands.replace()
59
+ */
60
+ replace: () => ReturnType
61
+
62
+ /**
63
+ * Replace all search results at once.
64
+ * @example editor.commands.replaceAll()
65
+ */
66
+ replaceAll: () => ReturnType
67
+
68
+ /**
69
+ * Jump to the next search result, wrapping around at the end.
70
+ * @example editor.commands.goToNextResult()
71
+ */
72
+ goToNextResult: () => ReturnType
73
+
74
+ /**
75
+ * Jump to the previous search result, wrapping around at the start.
76
+ * @example editor.commands.goToPreviousResult()
77
+ */
78
+ goToPreviousResult: () => ReturnType
79
+
80
+ /**
81
+ * Clear the search term and remove all search result highlights.
82
+ * @example editor.commands.clearSearch()
83
+ */
84
+ clearSearch: () => ReturnType
85
+ }
86
+ }
87
+ }
88
+
89
+ function mergePluginMeta(tr: Transaction, meta: FindAndReplaceMeta): void {
90
+ const currentMeta = tr.getMeta(FindAndReplacePluginKey) as FindAndReplaceMeta | undefined
91
+
92
+ tr.setMeta(FindAndReplacePluginKey, { ...currentMeta, ...meta })
93
+ }
94
+
95
+ function setPluginMeta(meta: FindAndReplaceMeta): Command {
96
+ return ({ tr, dispatch }) => {
97
+ if (dispatch) {
98
+ mergePluginMeta(tr, meta)
99
+ }
100
+
101
+ return true
102
+ }
103
+ }
104
+
105
+ function applySearchTerm(editor: Editor, term: string): void {
106
+ const tr = editor.state.tr
107
+
108
+ mergePluginMeta(tr, { searchTerm: term })
109
+ editor.view.dispatch(tr)
110
+ }
111
+
112
+ function consumePendingSearch(editor: Editor): string | null {
113
+ const state = getDebouncedSearchState(editor)
114
+
115
+ if (state.pendingTerm === null) {
116
+ return null
117
+ }
118
+
119
+ const pendingTerm = state.pendingTerm
120
+
121
+ state.pendingTerm = null
122
+
123
+ if (state.timeout !== null) {
124
+ clearTimeout(state.timeout)
125
+ state.timeout = null
126
+ }
127
+
128
+ return pendingTerm
129
+ }
130
+
131
+ function getPendingAwarePluginState(
132
+ state: EditorState,
133
+ pendingTerm: string | null,
134
+ ): FindAndReplacePluginState | null {
135
+ const pluginState = FindAndReplacePluginKey.getState(state)
136
+
137
+ if (!pluginState) {
138
+ return null
139
+ }
140
+
141
+ if (pendingTerm === null) {
142
+ return pluginState.results.length === 0 ? null : pluginState
143
+ }
144
+
145
+ const results = searchDocument(state.doc, pendingTerm, {
146
+ caseSensitive: pluginState.caseSensitive,
147
+ useRegex: pluginState.useRegex,
148
+ wholeWord: pluginState.wholeWord,
149
+ })
150
+
151
+ if (results.length === 0) {
152
+ return null
153
+ }
154
+
155
+ return {
156
+ ...pluginState,
157
+ searchTerm: pendingTerm,
158
+ results,
159
+ currentIndex: 0,
160
+ }
161
+ }
162
+
163
+ function currentResult(pluginState: FindAndReplacePluginState): SearchResult | undefined {
164
+ return pluginState.results[pluginState.currentIndex ?? 0]
165
+ }
166
+
167
+ function shiftIndex(currentIndex: number | null, length: number, direction: 1 | -1): number {
168
+ if (currentIndex === null) {
169
+ return direction === 1 ? 0 : length - 1
170
+ }
171
+
172
+ return (currentIndex + direction + length) % length
173
+ }
174
+
175
+ function selectResult(tr: Transaction, result: SearchResult, index: number): void {
176
+ tr.setSelection(TextSelection.create(tr.doc, result.from, result.to))
177
+ mergePluginMeta(tr, { currentIndex: index })
178
+ tr.scrollIntoView()
179
+ }
180
+
181
+ function replaceResult(
182
+ tr: Transaction,
183
+ pluginState: FindAndReplacePluginState,
184
+ result: SearchResult,
185
+ ): void {
186
+ const { searchTerm, replaceTerm, caseSensitive, useRegex, wholeWord } = pluginState
187
+
188
+ tr.insertText(replaceTerm, result.from, result.to)
189
+
190
+ // Jump to the first result behind the inserted text, so a replacement
191
+ // that still matches the search (e.g. "foo" -> "foobar") is skipped.
192
+ const results = searchDocument(tr.doc, searchTerm, { caseSensitive, useRegex, wholeWord })
193
+ const nextIndex = findNextIndex(results, result.from + replaceTerm.length)
194
+
195
+ if (nextIndex === null) {
196
+ mergePluginMeta(tr, { currentIndex: null })
197
+ return
198
+ }
199
+
200
+ selectResult(tr, results[nextIndex], nextIndex)
201
+ }
202
+
203
+ export const FindAndReplace = Extension.create<FindAndReplaceOptions, FindAndReplaceStorage>({
204
+ name: 'findAndReplace',
205
+
206
+ addOptions() {
207
+ return {
208
+ searchTerm: '',
209
+ replaceTerm: '',
210
+ caseSensitive: false,
211
+ useRegex: false,
212
+ wholeWord: false,
213
+ searchDebounceMs: 250,
214
+ injectCSS: true,
215
+ injectNonce: undefined,
216
+ }
217
+ },
218
+
219
+ addStorage() {
220
+ return {
221
+ searchTerm: this.options.searchTerm,
222
+ replaceTerm: this.options.replaceTerm,
223
+ caseSensitive: this.options.caseSensitive,
224
+ useRegex: this.options.useRegex,
225
+ wholeWord: this.options.wholeWord,
226
+ results: [],
227
+ currentIndex: null,
228
+ }
229
+ },
230
+
231
+ addProseMirrorPlugins() {
232
+ return [
233
+ FindAndReplacePlugin(this.options, pluginState => {
234
+ this.storage.results = pluginState.results
235
+ this.storage.currentIndex = pluginState.currentIndex
236
+ }),
237
+ ]
238
+ },
239
+
240
+ onTransaction() {
241
+ const pluginState = FindAndReplacePluginKey.getState(this.editor.state)
242
+
243
+ if (!pluginState) {
244
+ return
245
+ }
246
+
247
+ this.storage.searchTerm = pluginState.searchTerm
248
+ this.storage.replaceTerm = pluginState.replaceTerm
249
+ this.storage.caseSensitive = pluginState.caseSensitive
250
+ this.storage.useRegex = pluginState.useRegex
251
+ this.storage.wholeWord = pluginState.wholeWord
252
+ this.storage.results = pluginState.results
253
+ this.storage.currentIndex = pluginState.currentIndex
254
+ },
255
+
256
+ addCommands() {
257
+ const scheduleSearchTerm = (term: string): boolean => {
258
+ const state = getDebouncedSearchState(this.editor)
259
+
260
+ if (this.options.searchDebounceMs <= 0) {
261
+ if (state.timeout !== null) {
262
+ clearTimeout(state.timeout)
263
+ state.timeout = null
264
+ }
265
+
266
+ state.pendingTerm = null
267
+
268
+ applySearchTerm(this.editor, term)
269
+
270
+ return true
271
+ }
272
+
273
+ if (state.timeout !== null) {
274
+ clearTimeout(state.timeout)
275
+ }
276
+
277
+ state.pendingTerm = term
278
+ state.timeout = setTimeout(() => {
279
+ const debouncedState = getDebouncedSearchState(this.editor)
280
+
281
+ debouncedState.timeout = null
282
+
283
+ if (debouncedState.pendingTerm === null) {
284
+ return
285
+ }
286
+
287
+ const pendingTerm = debouncedState.pendingTerm
288
+
289
+ debouncedState.pendingTerm = null
290
+ applySearchTerm(this.editor, pendingTerm)
291
+ }, this.options.searchDebounceMs)
292
+
293
+ return true
294
+ }
295
+
296
+ return {
297
+ setSearchTerm:
298
+ term =>
299
+ ({ dispatch }) => {
300
+ if (!dispatch) {
301
+ return true
302
+ }
303
+
304
+ return scheduleSearchTerm(term)
305
+ },
306
+ setReplaceTerm: term => setPluginMeta({ replaceTerm: term }),
307
+ setCaseSensitive: caseSensitive => setPluginMeta({ caseSensitive }),
308
+ setUseRegex: useRegex => setPluginMeta({ useRegex }),
309
+ setWholeWord: wholeWord => setPluginMeta({ wholeWord }),
310
+ clearSearch:
311
+ () =>
312
+ ({ tr, dispatch }) => {
313
+ const state = getDebouncedSearchState(this.editor)
314
+
315
+ if (state.timeout !== null) {
316
+ clearTimeout(state.timeout)
317
+ state.timeout = null
318
+ }
319
+
320
+ state.pendingTerm = null
321
+
322
+ if (dispatch) {
323
+ mergePluginMeta(tr, { searchTerm: '' })
324
+ }
325
+
326
+ return true
327
+ },
328
+
329
+ replace:
330
+ () =>
331
+ ({ tr, state, dispatch }) => {
332
+ const pendingTerm = dispatch ? consumePendingSearch(this.editor) : null
333
+
334
+ if (dispatch && pendingTerm !== null) {
335
+ mergePluginMeta(tr, { searchTerm: pendingTerm })
336
+ }
337
+
338
+ const pluginState = getPendingAwarePluginState(state, pendingTerm)
339
+
340
+ if (!pluginState) {
341
+ return false
342
+ }
343
+
344
+ const result = currentResult(pluginState)
345
+
346
+ if (!result) {
347
+ return false
348
+ }
349
+
350
+ if (dispatch) {
351
+ replaceResult(tr, pluginState, result)
352
+ }
353
+
354
+ return true
355
+ },
356
+
357
+ replaceAll:
358
+ () =>
359
+ ({ tr, state, dispatch }) => {
360
+ const pendingTerm = dispatch ? consumePendingSearch(this.editor) : null
361
+
362
+ if (dispatch && pendingTerm !== null) {
363
+ mergePluginMeta(tr, { searchTerm: pendingTerm })
364
+ }
365
+
366
+ const pluginState = getPendingAwarePluginState(state, pendingTerm)
367
+
368
+ if (!pluginState) {
369
+ return false
370
+ }
371
+
372
+ if (dispatch) {
373
+ replaceAllResults(tr, state, pluginState.results, pluginState.replaceTerm)
374
+ }
375
+
376
+ return true
377
+ },
378
+
379
+ goToNextResult:
380
+ () =>
381
+ ({ tr, state, dispatch }) => {
382
+ const pendingTerm = dispatch ? consumePendingSearch(this.editor) : null
383
+
384
+ if (dispatch && pendingTerm !== null) {
385
+ mergePluginMeta(tr, { searchTerm: pendingTerm })
386
+ }
387
+
388
+ const pluginState = getPendingAwarePluginState(state, pendingTerm)
389
+
390
+ if (!pluginState) {
391
+ return false
392
+ }
393
+
394
+ const nextIndex = shiftIndex(pluginState.currentIndex, pluginState.results.length, 1)
395
+
396
+ if (dispatch) {
397
+ selectResult(tr, pluginState.results[nextIndex], nextIndex)
398
+ }
399
+
400
+ return true
401
+ },
402
+
403
+ goToPreviousResult:
404
+ () =>
405
+ ({ tr, state, dispatch }) => {
406
+ const pendingTerm = dispatch ? consumePendingSearch(this.editor) : null
407
+
408
+ if (dispatch && pendingTerm !== null) {
409
+ mergePluginMeta(tr, { searchTerm: pendingTerm })
410
+ }
411
+
412
+ const pluginState = getPendingAwarePluginState(state, pendingTerm)
413
+
414
+ if (!pluginState) {
415
+ return false
416
+ }
417
+
418
+ const previousIndex = shiftIndex(pluginState.currentIndex, pluginState.results.length, -1)
419
+
420
+ if (dispatch) {
421
+ selectResult(tr, pluginState.results[previousIndex], previousIndex)
422
+ }
423
+
424
+ return true
425
+ },
426
+ }
427
+ },
428
+
429
+ onDestroy() {
430
+ clearDebouncedSearchState(this.editor)
431
+ },
432
+ })
433
+
434
+ export default FindAndReplace
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { FindAndReplace } from './find-and-replace.js'
2
+
3
+ export * from './find-and-replace.js'
4
+ export * from './plugin/plugin.js'
5
+ export * from './search/search.js'
6
+ export * from './types.js'
7
+
8
+ export default FindAndReplace