From 3adb6ea9daf439a88205ec6ea03522440c6c4af6 Mon Sep 17 00:00:00 2001 From: Jonas Date: Wed, 23 Sep 2026 15:53:17 +0200 Subject: [PATCH 1/4] 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 | 25 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/plugins/searchDecorations.js b/src/plugins/searchDecorations.js index b868f58cee5..cf63b5044e4 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, _position) => { - // 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 00b689b558b..ae68fea1e37 100644 --- a/src/tests/plugins/searchDecorations.spec.js +++ b/src/tests/plugins/searchDecorations.spec.js @@ -59,6 +59,31 @@ 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 bc1d0a73bfdc17855b5d2aebdff281edcc5ced65 Mon Sep 17 00:00:00 2001 From: Jonas Date: Wed, 23 Sep 2026 16:09:01 +0200 Subject: [PATCH 2/4] 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 | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/plugins/searchDecorations.js b/src/plugins/searchDecorations.js index cf63b5044e4..3e08192c566 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() - doc.descendants((node, offset, _position) => { + 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 5fd609555ddb9758be83450d0c5384c5c961275e Mon Sep 17 00:00:00 2001 From: Jonas Date: Wed, 23 Sep 2026 16:12:15 +0200 Subject: [PATCH 3/4] 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 ae68fea1e37..78ab4757d88 100644 --- a/src/tests/plugins/searchDecorations.spec.js +++ b/src/tests/plugins/searchDecorations.spec.js @@ -96,7 +96,7 @@ describe('search plugin', () => { { from: 37, to: 40 }, { from: 55, to: 58 }, ], - total: 5, + total: 4, index: 0, } @@ -132,7 +132,7 @@ const 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), ) From 66649f8dba9a51e073a996dff41d90baa74db211 Mon Sep 17 00:00:00 2001 From: Jonas Date: Thu, 24 Sep 2026 07:39:54 +0200 Subject: [PATCH 4/4] chore: add missing JSDoc comments to functions Signed-off-by: Jonas --- playwright/e2e/print.spec.ts | 4 ++++ src/tests/plugins/linkClicking.spec.ts | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/playwright/e2e/print.spec.ts b/playwright/e2e/print.spec.ts index 27219770da7..f9cb884b1dd 100644 --- a/playwright/e2e/print.spec.ts +++ b/playwright/e2e/print.spec.ts @@ -26,6 +26,10 @@ const hasPdftoppm = (() => { } })() +/** + * @param pdf - the pdf file + * @param dir - the target directory + */ function renderPdfPages(pdf: Buffer, dir: string): Buffer[] { const pdfPath = join(dir, 'print.pdf') writeFileSync(pdfPath, pdf) diff --git a/src/tests/plugins/linkClicking.spec.ts b/src/tests/plugins/linkClicking.spec.ts index 7d4d0e5ad85..94f5f413d54 100644 --- a/src/tests/plugins/linkClicking.spec.ts +++ b/src/tests/plugins/linkClicking.spec.ts @@ -8,6 +8,9 @@ import Link from '../../marks/Link.ts' import createCustomEditor from '../testHelpers/createCustomEditor.ts' describe('linkClicking plugin', () => { + /** + * @param content - the editor content + */ function setup(content: string) { const openLink = vi.fn() const editor = createCustomEditor(content, [Link.configure({ openLink })]) @@ -16,6 +19,10 @@ describe('linkClicking plugin', () => { return { editor, openLink, link } } + /** + * @param el - the DOM element + * @param init - mouse event init + */ function click(el: Element, init: MouseEventInit = {}) { el.dispatchEvent( new MouseEvent('click', {