From 9c8ab791d9b6a8ba830799d4d2f867c71863f866 Mon Sep 17 00:00:00 2001 From: xfy Date: Tue, 7 Jul 2026 14:05:53 +0800 Subject: [PATCH] =?UTF-8?q?fix(editor):=20codeBlock=20=E5=86=85=20Backspac?= =?UTF-8?q?e=20=E5=9C=A8=20lowlight=20decoration=20=E9=87=8D=E5=BB=BA?= =?UTF-8?q?=E5=90=8E=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:CodeBlockLowlight 的 lowlight 插件在 codeBlock 内容变化后重新高亮, 重建 内 text node(包成 )。重建过程导致 DOM selection 错位到
(contentDOM 父元素)而非  内文本,后续 Backspace
删不掉剩余字符,块删空后也无法删除整个 codeBlock。

文档层 ProseMirror selection($anchor)始终正确,只是 DOM 层不同步。用独立
Extension + handleKeyDown plugin prop 接管 codeBlock 内的 Backspace:基于
文档 pos 直接删除字符(不依赖 DOM selection),或在块空时 setNodeMarkup
转段落。handleKeyDown 优先级高于 keymap,能覆盖父类 CodeBlock 的同名 shortcut
(在 CodeBlockLowlight.extend 里 addKeyboardShortcuts 无法覆盖父类,故独立扩展)。

经 Playwright 真实浏览器逐键验证:输入 ab → 删 b → 删 a(此前失败) → 删块,
全链路正常。与 NodeView 无关(禁用 NodeView 同样复现,根因在 lowlight decoration)。
---
 .../src/code-block-backspace-fix.ts           | 61 +++++++++++++++++++
 libs/tiptap-editor/src/index.ts               |  2 +
 2 files changed, 63 insertions(+)
 create mode 100644 libs/tiptap-editor/src/code-block-backspace-fix.ts

diff --git a/libs/tiptap-editor/src/code-block-backspace-fix.ts b/libs/tiptap-editor/src/code-block-backspace-fix.ts
new file mode 100644
index 0000000..85340c5
--- /dev/null
+++ b/libs/tiptap-editor/src/code-block-backspace-fix.ts
@@ -0,0 +1,61 @@
+import { Extension } from '@tiptap/core';
+import { Plugin, PluginKey } from '@tiptap/pm/state';
+
+/**
+ * 修复 CodeBlock 内 Backspace 在 lowlight decoration 重建后失效的问题。
+ *
+ * 背景:CodeBlockLowlight 的 lowlight 插件在 codeBlock 内容变化后重新高亮,重建
+ * `` 内的 text node(包成 ``)。重建过程中 DOM selection
+ * 会错位到 `
`(contentDOM 的父元素)而非 `` 内文本节点,导致后续 Backspace
+ * 无效——删不掉剩余字符,块删空后也无法删除整个 codeBlock。
+ *
+ * 文档层 ProseMirror 的 selection($anchor.parent / parentOffset)始终正确,只是 DOM
+ * 层与文档层不同步。这里用一个高优先级 keymap 插件接管 codeBlock 内的 Backspace:
+ * 当光标在 codeBlock 内(文档层判断)时,直接基于文档 pos 删除前一个字符,或在块空时
+ * clearNodes 转段落,绕过 DOM selection 错位。
+ *
+ * 必须用独立 Extension + addProseMirrorPlugins(而非在 CodeBlockLowlight.extend 里
+ * addKeyboardShortcuts):后者无法覆盖父类已注册的同名 keymap(Tiptap 优先级限制)。
+ */
+const pluginKey = new PluginKey('codeBlockBackspaceFix');
+
+export const CodeBlockBackspaceFix = Extension.create({
+  name: 'codeBlockBackspaceFix',
+
+  addProseMirrorPlugins() {
+    const codeBlockName = 'codeBlock';
+    return [
+      // 高优先级(数字越小越先),抢在父类 CodeBlock keymap 之前
+      new Plugin({
+        key: pluginKey,
+        props: {
+          handleKeyDown(view, event) {
+            if (event.key !== 'Backspace') return false;
+            const { empty, $anchor } = view.state.selection;
+            if (!empty) return false;
+            // 仅处理光标在 codeBlock 内的情况
+            if ($anchor.parent.type.name !== codeBlockName) return false;
+
+            // 在块起始位置(parentOffset 0)
+            if ($anchor.parentOffset === 0) {
+              // 块为空 → 转段落(等价 clearNodes)
+              if ($anchor.parent.textContent.length === 0) {
+                const tr = view.state.tr;
+                const defaultType = view.state.schema.nodes.paragraph;
+                tr.setNodeMarkup($anchor.before(), defaultType);
+                view.dispatch(tr);
+                return true;
+              }
+              return false; // 块非空但在起始,不处理(让光标留在块内)
+            }
+            // 删除前一个字符(基于文档 pos,不依赖 DOM selection)
+            const tr = view.state.tr;
+            tr.delete($anchor.pos - 1, $anchor.pos);
+            view.dispatch(tr);
+            return true;
+          },
+        },
+      }),
+    ];
+  },
+});
diff --git a/libs/tiptap-editor/src/index.ts b/libs/tiptap-editor/src/index.ts
index 8d87df6..010588d 100644
--- a/libs/tiptap-editor/src/index.ts
+++ b/libs/tiptap-editor/src/index.ts
@@ -5,6 +5,7 @@ import { TaskItem, TaskList } from '@tiptap/extension-list';
 import { TableKit } from '@tiptap/extension-table';
 import { Markdown } from '@tiptap/markdown';
 import StarterKit from '@tiptap/starter-kit';
+import { CodeBlockBackspaceFix } from './code-block-backspace-fix';
 import { CodeBlockNodeView, ON_RUN_CODE_STORAGE_KEY } from './code-block-view';
 import { lowlight } from './highlight';
 import { SlashCommand } from './slash-command';
@@ -116,6 +117,7 @@ class TiptapEditorInstance {
             return ({ node, editor }) => new CodeBlockNodeView({ node, editor });
           },
         }),
+        CodeBlockBackspaceFix,
         TableKit,
         UploadImage,
         TaskList,