From d5f0b940a2f2f865565ee45fe5086bddb5504127 Mon Sep 17 00:00:00 2001 From: Jonas Date: Wed, 23 Sep 2026 15:53:17 +0200 Subject: [PATCH 1/3] fix(search): find results that span mark boundaries Fixes searching e.g. for "teststring" when "test" has no mark but "string" is bold. Also make sure to sort the search results (with mention matches) so they're processed from top to bottom. Signed-off-by: Jonas Assisted-by: OpenCode:claude-fable-5.1 --- src/plugins/searchDecorations.js | 17 ++++++++++------- src/tests/plugins/searchDecorations.spec.js | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/plugins/searchDecorations.js b/src/plugins/searchDecorations.js index 62bfdff63dc..b610d36e8dc 100644 --- a/src/plugins/searchDecorations.js +++ b/src/plugins/searchDecorations.js @@ -96,18 +96,18 @@ export function runSearch(doc, query, options) { : query.trim().toLowerCase() doc.descendants((node, offset) => { - // Add decorations for text matches - if (node.isText) { - const matches = node.text.matchAll(new RegExp(query, 'gi')) + // Search the whole textblock so matches can sparn mark boundaries. + // Inline leaf nodes take one position each, so map them to one char. + if (node.isTextblock) { + const text = node.textBetween(0, node.content.size, undefined, '\uFFFC') + const matches = text.matchAll(new RegExp(query, 'gi')) for (const match of matches) { results.push({ - from: match.index + offset, - to: match.index + offset + query.length, + from: offset + 1 + match.index, + to: offset + 1 + match.index + match[0].length, }) } - - return } // Add decorations for mention matches @@ -122,6 +122,9 @@ export function runSearch(doc, query, options) { } }) + // Text matches of a block are pushed before its mentions; restore document order + results.sort((a, b) => a.from - b.from) + if (options.matchAll) { return { results, diff --git a/src/tests/plugins/searchDecorations.spec.js b/src/tests/plugins/searchDecorations.spec.js index 772247a2ef9..bea35dc5fd1 100644 --- a/src/tests/plugins/searchDecorations.spec.js +++ b/src/tests/plugins/searchDecorations.spec.js @@ -59,6 +59,27 @@ describe('search plugin', () => { testSearch('

cat dinosaur bird dog cat

', 'cat', expected) }) + it('finds matches spanning different marks', () => { + const expected = { + results: [{ from: 1, to: 11 }], + total: 1, + index: 0, + } + + testSearch('

teststring other

', 'teststring', expected) + }) + + it('finds matches after an inline node', () => { + const doc + = '

aJane Doeteststring

' + + const expected = { + results: [{ from: 3, to: 13 }], + } + + testSearch(doc, 'teststring', expected) + }) + it('finds matches in separate blocks', () => { const doc = '

cat dinosaur bird dog cat

' From 73555da70b7c934c33fd6c830b2cb80fa4c3bcad Mon Sep 17 00:00:00 2001 From: Jonas Date: Wed, 23 Sep 2026 16:09:01 +0200 Subject: [PATCH 2/3] fix(search): escape search string before passing it to regex Signed-off-by: Jonas Assisted-by: OpenCode:claude-fable-5.1 --- src/plugins/searchDecorations.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/plugins/searchDecorations.js b/src/plugins/searchDecorations.js index b610d36e8dc..a7d0ad2b1e1 100644 --- a/src/plugins/searchDecorations.js +++ b/src/plugins/searchDecorations.js @@ -4,6 +4,7 @@ */ import { emit } from '@nextcloud/event-bus' +import { escapeForRegEx } from '@tiptap/core' import { Plugin, PluginKey } from '@tiptap/pm/state' import { Decoration, DecorationSet } from '@tiptap/pm/view' import { searchQueryPluginKey } from './searchQuery.js' @@ -95,14 +96,15 @@ export function runSearch(doc, query, options) { ? query.trim().slice(1).toLowerCase() : query.trim().toLowerCase() + const regex = new RegExp(escapeForRegEx(query), 'gi') + doc.descendants((node, offset) => { // Search the whole textblock so matches can sparn mark boundaries. // Inline leaf nodes take one position each, so map them to one char. if (node.isTextblock) { const text = node.textBetween(0, node.content.size, undefined, '\uFFFC') - const matches = text.matchAll(new RegExp(query, 'gi')) - for (const match of matches) { + for (const match of text.matchAll(regex)) { results.push({ from: offset + 1 + match.index, to: offset + 1 + match.index + match[0].length, From fcf63b2fd0d1ca0735e1809b18c0b7418aa7bdfc Mon Sep 17 00:00:00 2001 From: Jonas Date: Wed, 23 Sep 2026 16:12:15 +0200 Subject: [PATCH 3/3] test(search): match whole searchResults object Signed-off-by: Jonas Assisted-by: OpenCode:claude-fable-5.1 --- src/tests/plugins/searchDecorations.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/plugins/searchDecorations.spec.js b/src/tests/plugins/searchDecorations.spec.js index bea35dc5fd1..9137b388421 100644 --- a/src/tests/plugins/searchDecorations.spec.js +++ b/src/tests/plugins/searchDecorations.spec.js @@ -92,7 +92,7 @@ describe('search plugin', () => { { from: 37, to: 40 }, { from: 55, to: 58 }, ], - total: 5, + total: 4, index: 0, } @@ -128,7 +128,7 @@ function testSearch(content, query, expectedSearchResults) { const editor = createCustomEditor(content, [Mentions]) const doc = editor.state.doc const searched = runSearch(doc, query) - expect(searched).toHaveProperty('results', expectedSearchResults.results) + expect(searched).toMatchObject(expectedSearchResults) expect(highlightResults(doc, searched.results)).toEqual(highlightResults(doc, expectedSearchResults.results)) editor.destroy() }