From ea2e93d8f191aad7bbff05cc61f29179df11ebb8 Mon Sep 17 00:00:00 2001 From: Jonas Date: Thu, 25 Jul 2024 13:01:55 +0200 Subject: [PATCH] fix(headingAnchor): Rebuild decorationSet in case of replacements When the whole document gets replaced by the same content, `DecorationSet.map()` returns an empty decorationSet. So only use it for updates where no decorations get removed. Signed-off-by: Jonas --- cypress/e2e/conflict.spec.js | 4 ++++ src/plugins/headingAnchor.js | 30 ++++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/cypress/e2e/conflict.spec.js b/cypress/e2e/conflict.spec.js index b688824ece4..b4c7aa47f8c 100644 --- a/cypress/e2e/conflict.spec.js +++ b/cypress/e2e/conflict.spec.js @@ -108,6 +108,10 @@ variants.forEach(function({ fixture, mime }) { }) }) +/** + * @param {string} fileName - filename + * @param {string} mime - mimetype + */ function createConflict(fileName, mime) { cy.visit('/apps/files') cy.openFile(fileName) diff --git a/src/plugins/headingAnchor.js b/src/plugins/headingAnchor.js index 3080173e93f..000b4c4ad20 100644 --- a/src/plugins/headingAnchor.js +++ b/src/plugins/headingAnchor.js @@ -22,6 +22,7 @@ export default function headingAnchor() { state: { init(_, { doc }) { + console.debug('headingAnchor init') const headings = extractHeadings(doc) return { headings, @@ -33,9 +34,7 @@ export default function headingAnchor() { return value } const headings = extractHeadings(newState.doc) - const decorations = headingsChanged(headings, value.headings) - ? anchorDecorations(newState.doc, headings) - : value.decorations.map(tr.mapping, tr.doc) + const decorations = mapDecorations(value, tr, headings) || anchorDecorations(newState.doc, headings) return { headings, decorations } }, }, @@ -48,11 +47,34 @@ export default function headingAnchor() { }) } +/** + * Map the previous decorations to current document state. + * + * Return false if headings changed or decorations would get removed. + * The latter prevents lost decorations in case of replacements. + * + * @param {object} value - previous plugin state + * @param {object} tr - current transaction + * @param {Array} headings - array of headings + * + * @return {false|DecorationSet} + */ +function mapDecorations(value, tr, headings) { + if (headingsChanged(headings, value.headings)) { + return false + } + let removedDecorations = false + const decorations = value.decorations.map(tr.mapping, tr.doc, { onRemove: () => { removedDecorations = true } }) + return removedDecorations + ? false + : decorations +} + /** * Check if the headings provided are equivalent. * * @param {Array} current - array of headings - * @param {Array} other - headings to compare against + * @param {Array} prev - headings to compare against * * @return {boolean} */