From cd9e5fc40396d438473309d839f27eb60ab6b8af Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sat, 11 Apr 2026 16:37:44 +0200 Subject: [PATCH] fix: guard table resize against stale editor views --- patches/prosemirror-tables@1.8.5.patch | 234 +++++++++++++++++++++++++ pnpm-lock.yaml | 9 +- pnpm-workspace.yaml | 1 + src/components/CommitDialog.tsx | 5 +- src/components/CreateNoteDialog.tsx | 5 +- tests/smoke/table-resize-crash.spec.ts | 122 +++++++++++++ 6 files changed, 371 insertions(+), 5 deletions(-) create mode 100644 patches/prosemirror-tables@1.8.5.patch create mode 100644 tests/smoke/table-resize-crash.spec.ts diff --git a/patches/prosemirror-tables@1.8.5.patch b/patches/prosemirror-tables@1.8.5.patch new file mode 100644 index 00000000..463e2b6b --- /dev/null +++ b/patches/prosemirror-tables@1.8.5.patch @@ -0,0 +1,234 @@ +diff --git a/dist/index.cjs b/dist/index.cjs +index 6c65eb7d57e207a8cd1f2a2ae3bb99507c405cef..c163932957846385623eb2980bf1141eb6631db1 100644 +--- a/dist/index.cjs ++++ b/dist/index.cjs +@@ -2443,6 +2443,21 @@ function handleMouseLeave(view) { + const pluginState = columnResizingPluginKey.getState(view.state); + if (pluginState && pluginState.activeHandle > -1 && !pluginState.dragging) updateHandle(view, -1); + } ++function safeDispatch(view, tr) { ++ try { ++ view.dispatch(tr); ++ return true; ++ } catch (_error) { ++ return false; ++ } ++} ++function safeDomAtPos(view, pos) { ++ try { ++ return view.domAtPos(pos); ++ } catch (_error) { ++ return null; ++ } ++} + function handleMouseDown(view, event, cellMinWidth, defaultCellMinWidth) { + var _view$dom$ownerDocume; + if (!view.editable) return false; +@@ -2451,17 +2466,18 @@ function handleMouseDown(view, event, cellMinWidth, defaultCellMinWidth) { + if (!pluginState || pluginState.activeHandle == -1 || pluginState.dragging) return false; + const cell = view.state.doc.nodeAt(pluginState.activeHandle); + const width = currentColWidth(view, pluginState.activeHandle, cell.attrs); +- view.dispatch(view.state.tr.setMeta(columnResizingPluginKey, { setDragging: { ++ if (width == null) return false; ++ if (!safeDispatch(view, view.state.tr.setMeta(columnResizingPluginKey, { setDragging: { + startX: event.clientX, + startWidth: width +- } })); ++ } }))) return false; + function finish(event$1) { + win.removeEventListener("mouseup", finish); + win.removeEventListener("mousemove", move); + const pluginState$1 = columnResizingPluginKey.getState(view.state); + if (pluginState$1 === null || pluginState$1 === void 0 ? void 0 : pluginState$1.dragging) { + updateColumnWidth(view, pluginState$1.activeHandle, draggedWidth(pluginState$1.dragging, event$1, cellMinWidth)); +- view.dispatch(view.state.tr.setMeta(columnResizingPluginKey, { setDragging: null })); ++ safeDispatch(view, view.state.tr.setMeta(columnResizingPluginKey, { setDragging: null })); + } + } + function move(event$1) { +@@ -2482,15 +2498,18 @@ function handleMouseDown(view, event, cellMinWidth, defaultCellMinWidth) { + function currentColWidth(view, cellPos, { colspan, colwidth }) { + const width = colwidth && colwidth[colwidth.length - 1]; + if (width) return width; +- const dom = view.domAtPos(cellPos); +- let domWidth = dom.node.childNodes[dom.offset].offsetWidth, parts = colspan; ++ const dom = safeDomAtPos(view, cellPos); ++ if (!dom) return null; ++ const cellDom = dom.node && dom.node.childNodes ? dom.node.childNodes[dom.offset] : null; ++ if (!cellDom || typeof cellDom.offsetWidth != "number") return null; ++ let domWidth = cellDom.offsetWidth, parts = colspan; + if (colwidth) { + for (let i = 0; i < colspan; i++) if (colwidth[i]) { + domWidth -= colwidth[i]; + parts--; + } + } +- return domWidth / parts; ++ return parts ? domWidth / parts : null; + } + function domCellAround(target) { + while (target && target.nodeName != "TD" && target.nodeName != "TH") target = target.classList && target.classList.contains("ProseMirror") ? null : target.parentNode; +@@ -2516,10 +2535,15 @@ function draggedWidth(dragging, event, resizeMinWidth) { + return Math.max(resizeMinWidth, dragging.startWidth + offset); + } + function updateHandle(view, value) { +- view.dispatch(view.state.tr.setMeta(columnResizingPluginKey, { setHandle: value })); ++ safeDispatch(view, view.state.tr.setMeta(columnResizingPluginKey, { setHandle: value })); + } + function updateColumnWidth(view, cell, width) { +- const $cell = view.state.doc.resolve(cell); ++ let $cell; ++ try { ++ $cell = view.state.doc.resolve(cell); ++ } catch (_error) { ++ return false; ++ } + const table = $cell.node(-1), map = TableMap.get(table), start = $cell.start(-1); + const col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1; + const tr = view.state.tr; +@@ -2537,16 +2561,24 @@ function updateColumnWidth(view, cell, width) { + colwidth + }); + } +- if (tr.docChanged) view.dispatch(tr); ++ if (tr.docChanged) return safeDispatch(view, tr); ++ return true; + } + function displayColumnWidth(view, cell, width, defaultCellMinWidth) { +- const $cell = view.state.doc.resolve(cell); ++ let $cell; ++ try { ++ $cell = view.state.doc.resolve(cell); ++ } catch (_error) { ++ return false; ++ } + const table = $cell.node(-1), start = $cell.start(-1); + const col = TableMap.get(table).colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1; +- let dom = view.domAtPos($cell.start(-1)).node; ++ const domAtPos = safeDomAtPos(view, $cell.start(-1)); ++ let dom = domAtPos && domAtPos.node; + while (dom && dom.nodeName != "TABLE") dom = dom.parentNode; +- if (!dom) return; ++ if (!dom) return false; + updateColumnsOnResize(table, dom.firstChild, dom, defaultCellMinWidth, col, width); ++ return true; + } + function zeroes(n) { + return Array(n).fill(0); +diff --git a/dist/index.js b/dist/index.js +index 5b4ac25594ba5722409332b1e5c812f108c9bf11..5b811ee70ff2fcb0c7587f838f00bc6fc19e906a 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -2443,6 +2443,21 @@ function handleMouseLeave(view) { + const pluginState = columnResizingPluginKey.getState(view.state); + if (pluginState && pluginState.activeHandle > -1 && !pluginState.dragging) updateHandle(view, -1); + } ++function safeDispatch(view, tr) { ++ try { ++ view.dispatch(tr); ++ return true; ++ } catch (_error) { ++ return false; ++ } ++} ++function safeDomAtPos(view, pos) { ++ try { ++ return view.domAtPos(pos); ++ } catch (_error) { ++ return null; ++ } ++} + function handleMouseDown(view, event, cellMinWidth, defaultCellMinWidth) { + var _view$dom$ownerDocume; + if (!view.editable) return false; +@@ -2451,17 +2466,18 @@ function handleMouseDown(view, event, cellMinWidth, defaultCellMinWidth) { + if (!pluginState || pluginState.activeHandle == -1 || pluginState.dragging) return false; + const cell = view.state.doc.nodeAt(pluginState.activeHandle); + const width = currentColWidth(view, pluginState.activeHandle, cell.attrs); +- view.dispatch(view.state.tr.setMeta(columnResizingPluginKey, { setDragging: { ++ if (width == null) return false; ++ if (!safeDispatch(view, view.state.tr.setMeta(columnResizingPluginKey, { setDragging: { + startX: event.clientX, + startWidth: width +- } })); ++ } }))) return false; + function finish(event$1) { + win.removeEventListener("mouseup", finish); + win.removeEventListener("mousemove", move); + const pluginState$1 = columnResizingPluginKey.getState(view.state); + if (pluginState$1 === null || pluginState$1 === void 0 ? void 0 : pluginState$1.dragging) { + updateColumnWidth(view, pluginState$1.activeHandle, draggedWidth(pluginState$1.dragging, event$1, cellMinWidth)); +- view.dispatch(view.state.tr.setMeta(columnResizingPluginKey, { setDragging: null })); ++ safeDispatch(view, view.state.tr.setMeta(columnResizingPluginKey, { setDragging: null })); + } + } + function move(event$1) { +@@ -2482,15 +2498,18 @@ function handleMouseDown(view, event, cellMinWidth, defaultCellMinWidth) { + function currentColWidth(view, cellPos, { colspan, colwidth }) { + const width = colwidth && colwidth[colwidth.length - 1]; + if (width) return width; +- const dom = view.domAtPos(cellPos); +- let domWidth = dom.node.childNodes[dom.offset].offsetWidth, parts = colspan; ++ const dom = safeDomAtPos(view, cellPos); ++ if (!dom) return null; ++ const cellDom = dom.node && dom.node.childNodes ? dom.node.childNodes[dom.offset] : null; ++ if (!cellDom || typeof cellDom.offsetWidth != "number") return null; ++ let domWidth = cellDom.offsetWidth, parts = colspan; + if (colwidth) { + for (let i = 0; i < colspan; i++) if (colwidth[i]) { + domWidth -= colwidth[i]; + parts--; + } + } +- return domWidth / parts; ++ return parts ? domWidth / parts : null; + } + function domCellAround(target) { + while (target && target.nodeName != "TD" && target.nodeName != "TH") target = target.classList && target.classList.contains("ProseMirror") ? null : target.parentNode; +@@ -2516,10 +2535,15 @@ function draggedWidth(dragging, event, resizeMinWidth) { + return Math.max(resizeMinWidth, dragging.startWidth + offset); + } + function updateHandle(view, value) { +- view.dispatch(view.state.tr.setMeta(columnResizingPluginKey, { setHandle: value })); ++ safeDispatch(view, view.state.tr.setMeta(columnResizingPluginKey, { setHandle: value })); + } + function updateColumnWidth(view, cell, width) { +- const $cell = view.state.doc.resolve(cell); ++ let $cell; ++ try { ++ $cell = view.state.doc.resolve(cell); ++ } catch (_error) { ++ return false; ++ } + const table = $cell.node(-1), map = TableMap.get(table), start = $cell.start(-1); + const col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1; + const tr = view.state.tr; +@@ -2537,16 +2561,24 @@ function updateColumnWidth(view, cell, width) { + colwidth + }); + } +- if (tr.docChanged) view.dispatch(tr); ++ if (tr.docChanged) return safeDispatch(view, tr); ++ return true; + } + function displayColumnWidth(view, cell, width, defaultCellMinWidth) { +- const $cell = view.state.doc.resolve(cell); ++ let $cell; ++ try { ++ $cell = view.state.doc.resolve(cell); ++ } catch (_error) { ++ return false; ++ } + const table = $cell.node(-1), start = $cell.start(-1); + const col = TableMap.get(table).colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1; +- let dom = view.domAtPos($cell.start(-1)).node; ++ const domAtPos = safeDomAtPos(view, $cell.start(-1)); ++ let dom = domAtPos && domAtPos.node; + while (dom && dom.nodeName != "TABLE") dom = dom.parentNode; +- if (!dom) return; ++ if (!dom) return false; + updateColumnsOnResize(table, dom.firstChild, dom, defaultCellMinWidth, col, width); ++ return true; + } + function zeroes(n) { + return Array(n).fill(0); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25406d4f..1a258384 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ patchedDependencies: '@blocknote/core@0.46.2': hash: 9a6d8a53058a8b6127326d40e8188f6b132b9b9e364b87e88e1c9aa3a1867aec path: patches/@blocknote__core@0.46.2.patch + prosemirror-tables@1.8.5: + hash: cd456f0d1d88a3ce11bcf53122a0e0575c0d82810eddd887adae21d6ec570a8b + path: patches/prosemirror-tables@1.8.5.patch importers: @@ -4491,7 +4494,7 @@ snapshots: prosemirror-highlight: 0.13.1(@shikijs/types@3.22.0)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-transform@1.11.0)(prosemirror-view@1.41.6) prosemirror-model: 1.25.4 prosemirror-state: 1.4.4 - prosemirror-tables: 1.8.5 + prosemirror-tables: 1.8.5(patch_hash=cd456f0d1d88a3ce11bcf53122a0e0575c0d82810eddd887adae21d6ec570a8b) prosemirror-transform: 1.11.0 prosemirror-view: 1.41.6 rehype-format: 5.0.1 @@ -6245,7 +6248,7 @@ snapshots: prosemirror-schema-basic: 1.2.4 prosemirror-schema-list: 1.5.1 prosemirror-state: 1.4.4 - prosemirror-tables: 1.8.5 + prosemirror-tables: 1.8.5(patch_hash=cd456f0d1d88a3ce11bcf53122a0e0575c0d82810eddd887adae21d6ec570a8b) prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6) prosemirror-transform: 1.11.0 prosemirror-view: 1.41.6 @@ -8099,7 +8102,7 @@ snapshots: prosemirror-transform: 1.11.0 prosemirror-view: 1.41.6 - prosemirror-tables@1.8.5: + prosemirror-tables@1.8.5(patch_hash=cd456f0d1d88a3ce11bcf53122a0e0575c0d82810eddd887adae21d6ec570a8b): dependencies: prosemirror-keymap: 1.2.3 prosemirror-model: 1.25.4 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9ed3d799..b99d98de 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,3 +6,4 @@ ignoredBuiltDependencies: patchedDependencies: '@blocknote/core@0.46.2': patches/@blocknote__core@0.46.2.patch + prosemirror-tables@1.8.5: patches/prosemirror-tables@1.8.5.patch diff --git a/src/components/CommitDialog.tsx b/src/components/CommitDialog.tsx index 195de9e0..4a07297a 100644 --- a/src/components/CommitDialog.tsx +++ b/src/components/CommitDialog.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useRef } from 'react' -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog' +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' @@ -47,6 +47,9 @@ export function CommitDialog({ open, modifiedCount, suggestedMessage, onCommit, {modifiedCount} file{modifiedCount !== 1 ? 's' : ''} changed + + Review changed files and enter a commit message before committing and pushing. +