@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.
- package/LICENSE.md +21 -0
- package/README.md +18 -0
- package/dist/index.cjs +950 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +230 -0
- package/dist/index.d.ts +230 -0
- package/dist/index.js +915 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
- package/src/constants/constants.ts +2 -0
- package/src/find-and-replace.ts +434 -0
- package/src/index.ts +8 -0
- package/src/plugin/plugin-state.ts +27 -0
- package/src/plugin/plugin.ts +60 -0
- package/src/search/matches.ts +26 -0
- package/src/search/regex.ts +67 -0
- package/src/search/search.ts +70 -0
- package/src/search/text-segments.ts +63 -0
- package/src/search/textblock-search.ts +31 -0
- package/src/search/types.ts +17 -0
- package/src/state/debounced-search-state.ts +39 -0
- package/src/style/create-style-tag.ts +23 -0
- package/src/style/style.ts +10 -0
- package/src/types.ts +96 -0
- package/src/utils/createDecoration.ts +14 -0
- package/src/utils/createDecorations.ts +16 -0
- package/src/utils/createState.ts +23 -0
- package/src/utils/findDecorations.ts +27 -0
- package/src/utils/getChangedTextblocks.ts +29 -0
- package/src/utils/getFallbackCurrentIndex.ts +8 -0
- package/src/utils/getMappedCurrentIndex.ts +32 -0
- package/src/utils/getMetaCurrentIndex.ts +19 -0
- package/src/utils/getPreviousCurrentResult.ts +10 -0
- package/src/utils/getResultsToRefresh.ts +22 -0
- package/src/utils/getSearchCurrentIndex.ts +24 -0
- package/src/utils/getTextNodeResult.ts +21 -0
- package/src/utils/groupResults.ts +68 -0
- package/src/utils/hasCurrentIndex.ts +5 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/isCurrentResult.ts +7 -0
- package/src/utils/isNewSearch.ts +5 -0
- package/src/utils/mapResult.ts +10 -0
- package/src/utils/mapResults.ts +19 -0
- package/src/utils/mergeResults.ts +18 -0
- package/src/utils/normalizeCurrentIndex.ts +10 -0
- package/src/utils/refreshCurrentIndexState.ts +30 -0
- package/src/utils/refreshDecorations.ts +28 -0
- package/src/utils/refreshDocumentState.ts +47 -0
- package/src/utils/refreshState.ts +27 -0
- package/src/utils/replaceAllResults.ts +18 -0
- package/src/utils/replaceGroup.ts +54 -0
- package/src/utils/resolveCurrentIndex.ts +27 -0
- package/src/utils/resultInTextblocks.ts +30 -0
- package/src/utils/sameResult.ts +5 -0
- package/src/utils/shouldRefreshResult.ts +18 -0
- package/src/utils/touchesSearch.ts +7 -0
- package/src/utils/types.ts +11 -0
- package/src/utils/updateState.ts +33 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Mapping } from '@tiptap/pm/transform'
|
|
2
|
+
|
|
3
|
+
import type { FindAndReplacePluginState } from '../plugin/plugin-state.js'
|
|
4
|
+
import type { SearchResult } from '../search/search.js'
|
|
5
|
+
import { findNextIndex } from '../search/search.js'
|
|
6
|
+
|
|
7
|
+
import { getPreviousCurrentResult } from './getPreviousCurrentResult.js'
|
|
8
|
+
|
|
9
|
+
export function getMappedCurrentIndex(
|
|
10
|
+
previousState: FindAndReplacePluginState,
|
|
11
|
+
results: SearchResult[],
|
|
12
|
+
docChanged: boolean,
|
|
13
|
+
mapping: Mapping | undefined,
|
|
14
|
+
): number | null | undefined {
|
|
15
|
+
if (!docChanged) {
|
|
16
|
+
return undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!mapping) {
|
|
20
|
+
return undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const previousResult = getPreviousCurrentResult(previousState)
|
|
24
|
+
|
|
25
|
+
if (!previousResult) {
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const mappedFrom = mapping.map(previousResult.from, 1)
|
|
30
|
+
|
|
31
|
+
return findNextIndex(results, mappedFrom)
|
|
32
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FindAndReplaceMeta } from '../plugin/plugin-state.js'
|
|
2
|
+
|
|
3
|
+
import { hasCurrentIndex } from './hasCurrentIndex.js'
|
|
4
|
+
import { normalizeCurrentIndex } from './normalizeCurrentIndex.js'
|
|
5
|
+
|
|
6
|
+
export function getMetaCurrentIndex(
|
|
7
|
+
meta: FindAndReplaceMeta | undefined,
|
|
8
|
+
length: number,
|
|
9
|
+
): number | null | undefined {
|
|
10
|
+
if (!meta) {
|
|
11
|
+
return undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!hasCurrentIndex(meta)) {
|
|
15
|
+
return undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return normalizeCurrentIndex(meta.currentIndex, length)
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FindAndReplacePluginState } from '../plugin/plugin-state.js'
|
|
2
|
+
import type { SearchResult } from '../search/search.js'
|
|
3
|
+
|
|
4
|
+
export function getPreviousCurrentResult(
|
|
5
|
+
previousState: FindAndReplacePluginState,
|
|
6
|
+
): SearchResult | undefined {
|
|
7
|
+
return previousState.currentIndex === null
|
|
8
|
+
? undefined
|
|
9
|
+
: previousState.results[previousState.currentIndex]
|
|
10
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SearchResult } from '../search/search.js'
|
|
2
|
+
|
|
3
|
+
import { shouldRefreshResult } from './shouldRefreshResult.js'
|
|
4
|
+
import type { IndexedResult, TextblockRange } from './types.js'
|
|
5
|
+
|
|
6
|
+
export function getResultsToRefresh(
|
|
7
|
+
results: SearchResult[],
|
|
8
|
+
textblocks: readonly TextblockRange[],
|
|
9
|
+
previousCurrent: SearchResult | null,
|
|
10
|
+
currentIndex: number | null,
|
|
11
|
+
): IndexedResult[] {
|
|
12
|
+
const current = currentIndex === null ? null : (results[currentIndex] ?? null)
|
|
13
|
+
const refreshedResults: IndexedResult[] = []
|
|
14
|
+
|
|
15
|
+
results.forEach((result, index) => {
|
|
16
|
+
if (shouldRefreshResult(result, textblocks, previousCurrent, current)) {
|
|
17
|
+
refreshedResults.push({ result, index })
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return refreshedResults
|
|
22
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Mapping } from '@tiptap/pm/transform'
|
|
2
|
+
|
|
3
|
+
import type { FindAndReplaceMeta, FindAndReplacePluginState } from '../plugin/plugin-state.js'
|
|
4
|
+
import type { SearchResult } from '../search/search.js'
|
|
5
|
+
|
|
6
|
+
import { getFallbackCurrentIndex } from './getFallbackCurrentIndex.js'
|
|
7
|
+
import { getMappedCurrentIndex } from './getMappedCurrentIndex.js'
|
|
8
|
+
import { isNewSearch } from './isNewSearch.js'
|
|
9
|
+
|
|
10
|
+
export function getSearchCurrentIndex(
|
|
11
|
+
previousState: FindAndReplacePluginState,
|
|
12
|
+
results: SearchResult[],
|
|
13
|
+
meta: FindAndReplaceMeta | undefined,
|
|
14
|
+
docChanged: boolean,
|
|
15
|
+
mapping: Mapping | undefined,
|
|
16
|
+
): number {
|
|
17
|
+
if (isNewSearch(meta)) {
|
|
18
|
+
return 0
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const mappedIndex = getMappedCurrentIndex(previousState, results, docChanged, mapping)
|
|
22
|
+
|
|
23
|
+
return mappedIndex ?? getFallbackCurrentIndex(previousState, results.length)
|
|
24
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EditorState } from '@tiptap/pm/state'
|
|
2
|
+
|
|
3
|
+
import type { SearchResult } from '../search/search.js'
|
|
4
|
+
|
|
5
|
+
export interface TextNodeResult {
|
|
6
|
+
from: number
|
|
7
|
+
text: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getTextNodeResult(state: EditorState, result: SearchResult): TextNodeResult | null {
|
|
11
|
+
const $from = state.doc.resolve(result.from)
|
|
12
|
+
const node = $from.parent.child($from.index())
|
|
13
|
+
|
|
14
|
+
if (!node.isText || !node.text) {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const from = result.from - $from.textOffset
|
|
19
|
+
|
|
20
|
+
return result.to <= from + node.nodeSize ? { from, text: node.text } : null
|
|
21
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { EditorState } from '@tiptap/pm/state'
|
|
2
|
+
|
|
3
|
+
import type { SearchResult } from '../search/search.js'
|
|
4
|
+
import type { TextNodeResult } from './getTextNodeResult.js'
|
|
5
|
+
import { getTextNodeResult } from './getTextNodeResult.js'
|
|
6
|
+
|
|
7
|
+
export interface ResultGroup {
|
|
8
|
+
from: number
|
|
9
|
+
text: string | null
|
|
10
|
+
results: SearchResult[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function shouldMergeResult(
|
|
14
|
+
group: ResultGroup | undefined,
|
|
15
|
+
textNode: TextNodeResult | null,
|
|
16
|
+
): boolean {
|
|
17
|
+
if (!group || !textNode) {
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return group.from === textNode.from && group.text !== null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createTextResultGroup(result: SearchResult, textNode: TextNodeResult): ResultGroup {
|
|
25
|
+
return {
|
|
26
|
+
from: textNode.from,
|
|
27
|
+
text: textNode.text,
|
|
28
|
+
results: [result],
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createResultGroup(result: SearchResult): ResultGroup {
|
|
33
|
+
return {
|
|
34
|
+
from: result.from,
|
|
35
|
+
text: null,
|
|
36
|
+
results: [result],
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function appendResult(
|
|
41
|
+
groups: ResultGroup[],
|
|
42
|
+
result: SearchResult,
|
|
43
|
+
textNode: TextNodeResult | null,
|
|
44
|
+
): void {
|
|
45
|
+
const group = groups.at(-1)
|
|
46
|
+
|
|
47
|
+
if (shouldMergeResult(group, textNode)) {
|
|
48
|
+
group!.results.push(result)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (textNode) {
|
|
53
|
+
groups.push(createTextResultGroup(result, textNode))
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
groups.push(createResultGroup(result))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function groupResults(state: EditorState, results: SearchResult[]): ResultGroup[] {
|
|
61
|
+
const groups: ResultGroup[] = []
|
|
62
|
+
|
|
63
|
+
for (const result of results) {
|
|
64
|
+
appendResult(groups, result, getTextNodeResult(state, result))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return groups
|
|
68
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Mapping } from '@tiptap/pm/transform'
|
|
2
|
+
|
|
3
|
+
import type { SearchResult } from '../search/search.js'
|
|
4
|
+
|
|
5
|
+
export function mapResult(result: SearchResult, mapping: Mapping): SearchResult | null {
|
|
6
|
+
const from = mapping.map(result.from, 1)
|
|
7
|
+
const to = mapping.map(result.to, -1)
|
|
8
|
+
|
|
9
|
+
return from < to ? { from, to } : null
|
|
10
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Mapping } from '@tiptap/pm/transform'
|
|
2
|
+
|
|
3
|
+
import type { SearchResult } from '../search/search.js'
|
|
4
|
+
|
|
5
|
+
import { mapResult } from './mapResult.js'
|
|
6
|
+
|
|
7
|
+
export function mapResults(results: SearchResult[], mapping: Mapping): SearchResult[] {
|
|
8
|
+
const mappedResults: SearchResult[] = []
|
|
9
|
+
|
|
10
|
+
results.forEach(result => {
|
|
11
|
+
const mapped = mapResult(result, mapping)
|
|
12
|
+
|
|
13
|
+
if (mapped) {
|
|
14
|
+
mappedResults.push(mapped)
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
return mappedResults
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SearchResult } from '../search/search.js'
|
|
2
|
+
|
|
3
|
+
export function mergeResults(first: SearchResult[], second: SearchResult[]): SearchResult[] {
|
|
4
|
+
const results: SearchResult[] = []
|
|
5
|
+
let firstIndex = 0
|
|
6
|
+
let secondIndex = 0
|
|
7
|
+
|
|
8
|
+
while (firstIndex < first.length && secondIndex < second.length) {
|
|
9
|
+
if (first[firstIndex].from < second[secondIndex].from) {
|
|
10
|
+
results.push(first[firstIndex++])
|
|
11
|
+
continue
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
results.push(second[secondIndex++])
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return results.concat(first.slice(firstIndex), second.slice(secondIndex))
|
|
18
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function normalizeCurrentIndex(
|
|
2
|
+
currentIndex: number | null | undefined,
|
|
3
|
+
length: number,
|
|
4
|
+
): number | null {
|
|
5
|
+
if (currentIndex === undefined || currentIndex === null || length === 0) {
|
|
6
|
+
return null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return Math.max(0, Math.min(currentIndex, length - 1))
|
|
10
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Node } from '@tiptap/pm/model'
|
|
2
|
+
|
|
3
|
+
import type { FindAndReplaceMeta, FindAndReplacePluginState } from '../plugin/plugin-state.js'
|
|
4
|
+
|
|
5
|
+
import { refreshDecorations } from './refreshDecorations.js'
|
|
6
|
+
import { resolveCurrentIndex } from './resolveCurrentIndex.js'
|
|
7
|
+
|
|
8
|
+
export function refreshCurrentIndexState(
|
|
9
|
+
previousState: FindAndReplacePluginState,
|
|
10
|
+
state: FindAndReplacePluginState,
|
|
11
|
+
doc: Node,
|
|
12
|
+
meta: FindAndReplaceMeta | undefined,
|
|
13
|
+
): FindAndReplacePluginState {
|
|
14
|
+
const currentIndex = resolveCurrentIndex(previousState, state.results, meta, false, undefined)
|
|
15
|
+
const previousCurrent =
|
|
16
|
+
previousState.currentIndex === null ? null : previousState.results[previousState.currentIndex]
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
...state,
|
|
20
|
+
currentIndex,
|
|
21
|
+
decorations: refreshDecorations(
|
|
22
|
+
previousState.decorations,
|
|
23
|
+
doc,
|
|
24
|
+
state.results,
|
|
25
|
+
currentIndex,
|
|
26
|
+
[],
|
|
27
|
+
previousCurrent,
|
|
28
|
+
),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Node } from '@tiptap/pm/model'
|
|
2
|
+
import { DecorationSet } from '@tiptap/pm/view'
|
|
3
|
+
|
|
4
|
+
import type { SearchResult } from '../search/search.js'
|
|
5
|
+
|
|
6
|
+
import { createDecoration } from './createDecoration.js'
|
|
7
|
+
import { findDecorations } from './findDecorations.js'
|
|
8
|
+
import { getResultsToRefresh } from './getResultsToRefresh.js'
|
|
9
|
+
import type { TextblockRange } from './types.js'
|
|
10
|
+
|
|
11
|
+
export function refreshDecorations(
|
|
12
|
+
decorations: DecorationSet,
|
|
13
|
+
doc: Node,
|
|
14
|
+
results: SearchResult[],
|
|
15
|
+
currentIndex: number | null,
|
|
16
|
+
textblocks: readonly TextblockRange[],
|
|
17
|
+
previousCurrent: SearchResult | null,
|
|
18
|
+
): DecorationSet {
|
|
19
|
+
const resultsToRefresh = getResultsToRefresh(results, textblocks, previousCurrent, currentIndex)
|
|
20
|
+
const refreshedResults = resultsToRefresh.map(({ result }) => result)
|
|
21
|
+
const staleResults = previousCurrent ? [...refreshedResults, previousCurrent] : refreshedResults
|
|
22
|
+
const decorationsToRemove = findDecorations(decorations, staleResults, textblocks)
|
|
23
|
+
const decorationsToAdd = resultsToRefresh.map(({ result, index }) => {
|
|
24
|
+
return createDecoration(result, currentIndex, index)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
return decorations.remove(decorationsToRemove).add(doc, decorationsToAdd)
|
|
28
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Node } from '@tiptap/pm/model'
|
|
2
|
+
import type { Mapping } from '@tiptap/pm/transform'
|
|
3
|
+
|
|
4
|
+
import type { FindAndReplaceMeta, FindAndReplacePluginState } from '../plugin/plugin-state.js'
|
|
5
|
+
import { searchTextblocks } from '../search/search.js'
|
|
6
|
+
|
|
7
|
+
import { getChangedTextblocks } from './getChangedTextblocks.js'
|
|
8
|
+
import { mapResult } from './mapResult.js'
|
|
9
|
+
import { mapResults } from './mapResults.js'
|
|
10
|
+
import { mergeResults } from './mergeResults.js'
|
|
11
|
+
import { refreshDecorations } from './refreshDecorations.js'
|
|
12
|
+
import { resolveCurrentIndex } from './resolveCurrentIndex.js'
|
|
13
|
+
import { resultInTextblocks } from './resultInTextblocks.js'
|
|
14
|
+
|
|
15
|
+
export function refreshDocumentState(
|
|
16
|
+
previousState: FindAndReplacePluginState,
|
|
17
|
+
state: FindAndReplacePluginState,
|
|
18
|
+
doc: Node,
|
|
19
|
+
meta: FindAndReplaceMeta | undefined,
|
|
20
|
+
mapping: Mapping,
|
|
21
|
+
): FindAndReplacePluginState {
|
|
22
|
+
const textblocks = getChangedTextblocks(doc, mapping)
|
|
23
|
+
const mappedResults = mapResults(previousState.results, mapping)
|
|
24
|
+
const unaffectedResults = mappedResults.filter(result => !resultInTextblocks(result, textblocks))
|
|
25
|
+
const changedResults = searchTextblocks(textblocks, state.searchTerm, state)
|
|
26
|
+
const results = mergeResults(unaffectedResults, changedResults)
|
|
27
|
+
const currentIndex = resolveCurrentIndex(previousState, results, meta, true, mapping)
|
|
28
|
+
const previousCurrent =
|
|
29
|
+
previousState.currentIndex === null
|
|
30
|
+
? null
|
|
31
|
+
: mapResult(previousState.results[previousState.currentIndex], mapping)
|
|
32
|
+
const decorations = previousState.decorations.map(mapping, doc)
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
...state,
|
|
36
|
+
results,
|
|
37
|
+
currentIndex,
|
|
38
|
+
decorations: refreshDecorations(
|
|
39
|
+
decorations,
|
|
40
|
+
doc,
|
|
41
|
+
results,
|
|
42
|
+
currentIndex,
|
|
43
|
+
textblocks,
|
|
44
|
+
previousCurrent,
|
|
45
|
+
),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Node } from '@tiptap/pm/model'
|
|
2
|
+
import type { Mapping } from '@tiptap/pm/transform'
|
|
3
|
+
|
|
4
|
+
import type { FindAndReplaceMeta, FindAndReplacePluginState } from '../plugin/plugin-state.js'
|
|
5
|
+
import { searchDocument } from '../search/search.js'
|
|
6
|
+
|
|
7
|
+
import { createDecorations } from './createDecorations.js'
|
|
8
|
+
import { resolveCurrentIndex } from './resolveCurrentIndex.js'
|
|
9
|
+
|
|
10
|
+
export function refreshState(
|
|
11
|
+
previousState: FindAndReplacePluginState,
|
|
12
|
+
state: FindAndReplacePluginState,
|
|
13
|
+
doc: Node,
|
|
14
|
+
meta: FindAndReplaceMeta | undefined,
|
|
15
|
+
docChanged: boolean,
|
|
16
|
+
mapping: Mapping | undefined,
|
|
17
|
+
): FindAndReplacePluginState {
|
|
18
|
+
const results = searchDocument(doc, state.searchTerm, state)
|
|
19
|
+
const currentIndex = resolveCurrentIndex(previousState, results, meta, docChanged, mapping)
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
...state,
|
|
23
|
+
results,
|
|
24
|
+
currentIndex,
|
|
25
|
+
decorations: createDecorations(doc, results, currentIndex),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { EditorState, Transaction } from '@tiptap/pm/state'
|
|
2
|
+
|
|
3
|
+
import type { SearchResult } from '../search/search.js'
|
|
4
|
+
import { groupResults } from './groupResults.js'
|
|
5
|
+
import { replaceGroup } from './replaceGroup.js'
|
|
6
|
+
|
|
7
|
+
export function replaceAllResults(
|
|
8
|
+
tr: Transaction,
|
|
9
|
+
state: EditorState,
|
|
10
|
+
results: SearchResult[],
|
|
11
|
+
replaceTerm: string,
|
|
12
|
+
): void {
|
|
13
|
+
const groups = groupResults(state, results)
|
|
14
|
+
|
|
15
|
+
for (const group of groups.reverse()) {
|
|
16
|
+
replaceGroup(tr, group, replaceTerm)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Transaction } from '@tiptap/pm/state'
|
|
2
|
+
|
|
3
|
+
import type { SearchResult } from '../search/search.js'
|
|
4
|
+
import type { ResultGroup } from './groupResults.js'
|
|
5
|
+
|
|
6
|
+
function replaceSeparateResults(
|
|
7
|
+
tr: Transaction,
|
|
8
|
+
results: SearchResult[],
|
|
9
|
+
replaceTerm: string,
|
|
10
|
+
): void {
|
|
11
|
+
for (const result of [...results].reverse()) {
|
|
12
|
+
tr.insertText(replaceTerm, result.from, result.to)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createReplacement(
|
|
17
|
+
group: ResultGroup,
|
|
18
|
+
text: string,
|
|
19
|
+
firstResult: SearchResult,
|
|
20
|
+
replaceTerm: string,
|
|
21
|
+
): string {
|
|
22
|
+
let offset = firstResult.from - group.from
|
|
23
|
+
const replacement: string[] = []
|
|
24
|
+
|
|
25
|
+
for (const result of group.results) {
|
|
26
|
+
const from = result.from - group.from
|
|
27
|
+
const to = result.to - group.from
|
|
28
|
+
|
|
29
|
+
replacement.push(text.slice(offset, from), replaceTerm)
|
|
30
|
+
offset = to
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return replacement.join('')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function replaceGroup(tr: Transaction, group: ResultGroup, replaceTerm: string): void {
|
|
37
|
+
const firstResult = group.results[0]
|
|
38
|
+
const lastResult = group.results.at(-1)
|
|
39
|
+
|
|
40
|
+
if (!lastResult) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (group.text === null) {
|
|
45
|
+
replaceSeparateResults(tr, group.results, replaceTerm)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
tr.insertText(
|
|
50
|
+
createReplacement(group, group.text, firstResult, replaceTerm),
|
|
51
|
+
firstResult.from,
|
|
52
|
+
lastResult.to,
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Mapping } from '@tiptap/pm/transform'
|
|
2
|
+
|
|
3
|
+
import type { FindAndReplaceMeta, FindAndReplacePluginState } from '../plugin/plugin-state.js'
|
|
4
|
+
import type { SearchResult } from '../search/search.js'
|
|
5
|
+
|
|
6
|
+
import { getMetaCurrentIndex } from './getMetaCurrentIndex.js'
|
|
7
|
+
import { getSearchCurrentIndex } from './getSearchCurrentIndex.js'
|
|
8
|
+
|
|
9
|
+
export function resolveCurrentIndex(
|
|
10
|
+
previousState: FindAndReplacePluginState,
|
|
11
|
+
results: SearchResult[],
|
|
12
|
+
meta: FindAndReplaceMeta | undefined,
|
|
13
|
+
docChanged: boolean,
|
|
14
|
+
mapping: Mapping | undefined,
|
|
15
|
+
): number | null {
|
|
16
|
+
if (results.length === 0) {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const metaIndex = getMetaCurrentIndex(meta, results.length)
|
|
21
|
+
|
|
22
|
+
if (metaIndex !== undefined) {
|
|
23
|
+
return metaIndex
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return getSearchCurrentIndex(previousState, results, meta, docChanged, mapping)
|
|
27
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SearchResult } from '../search/search.js'
|
|
2
|
+
|
|
3
|
+
import type { TextblockRange } from './types.js'
|
|
4
|
+
|
|
5
|
+
export function resultInTextblocks(
|
|
6
|
+
result: SearchResult,
|
|
7
|
+
textblocks: readonly TextblockRange[],
|
|
8
|
+
): boolean {
|
|
9
|
+
let start = 0
|
|
10
|
+
let end = textblocks.length - 1
|
|
11
|
+
|
|
12
|
+
while (start <= end) {
|
|
13
|
+
const index = Math.floor((start + end) / 2)
|
|
14
|
+
const textblock = textblocks[index]
|
|
15
|
+
|
|
16
|
+
if (result.from < textblock.from) {
|
|
17
|
+
end = index - 1
|
|
18
|
+
continue
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (result.to > textblock.to) {
|
|
22
|
+
start = index + 1
|
|
23
|
+
continue
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SearchResult } from '../search/search.js'
|
|
2
|
+
|
|
3
|
+
import { isCurrentResult } from './isCurrentResult.js'
|
|
4
|
+
import { resultInTextblocks } from './resultInTextblocks.js'
|
|
5
|
+
import type { TextblockRange } from './types.js'
|
|
6
|
+
|
|
7
|
+
export function shouldRefreshResult(
|
|
8
|
+
result: SearchResult,
|
|
9
|
+
textblocks: readonly TextblockRange[],
|
|
10
|
+
previousCurrent: SearchResult | null,
|
|
11
|
+
current: SearchResult | null,
|
|
12
|
+
): boolean {
|
|
13
|
+
return (
|
|
14
|
+
resultInTextblocks(result, textblocks) ||
|
|
15
|
+
isCurrentResult(result, previousCurrent) ||
|
|
16
|
+
isCurrentResult(result, current)
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FindAndReplaceMeta } from '../plugin/plugin-state.js'
|
|
2
|
+
|
|
3
|
+
const searchKeys = ['searchTerm', 'caseSensitive', 'useRegex', 'wholeWord'] as const
|
|
4
|
+
|
|
5
|
+
export function touchesSearch(meta: FindAndReplaceMeta | undefined): boolean {
|
|
6
|
+
return !!meta && searchKeys.some(key => key in meta)
|
|
7
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TextblockSearchTarget } from '../search/search.js'
|
|
2
|
+
|
|
3
|
+
export interface TextblockRange extends TextblockSearchTarget {
|
|
4
|
+
from: number
|
|
5
|
+
to: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface IndexedResult {
|
|
9
|
+
result: import('../search/search.js').SearchResult
|
|
10
|
+
index: number
|
|
11
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Node } from '@tiptap/pm/model'
|
|
2
|
+
import type { Mapping } from '@tiptap/pm/transform'
|
|
3
|
+
|
|
4
|
+
import type { FindAndReplaceMeta, FindAndReplacePluginState } from '../plugin/plugin-state.js'
|
|
5
|
+
|
|
6
|
+
import { hasCurrentIndex } from './hasCurrentIndex.js'
|
|
7
|
+
import { refreshCurrentIndexState } from './refreshCurrentIndexState.js'
|
|
8
|
+
import { refreshDocumentState } from './refreshDocumentState.js'
|
|
9
|
+
import { refreshState } from './refreshState.js'
|
|
10
|
+
import { touchesSearch } from './touchesSearch.js'
|
|
11
|
+
|
|
12
|
+
export function updateState(
|
|
13
|
+
previousState: FindAndReplacePluginState,
|
|
14
|
+
state: FindAndReplacePluginState,
|
|
15
|
+
doc: Node,
|
|
16
|
+
meta: FindAndReplaceMeta | undefined,
|
|
17
|
+
docChanged: boolean,
|
|
18
|
+
mapping: Mapping,
|
|
19
|
+
): FindAndReplacePluginState {
|
|
20
|
+
if (touchesSearch(meta)) {
|
|
21
|
+
return refreshState(previousState, state, doc, meta, docChanged, mapping)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (docChanged) {
|
|
25
|
+
return refreshDocumentState(previousState, state, doc, meta, mapping)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (hasCurrentIndex(meta)) {
|
|
29
|
+
return refreshCurrentIndexState(previousState, state, doc, meta)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return state
|
|
33
|
+
}
|