鸿蒙 Markdown 编辑器表格所见即所得:七个版本的渲染重构(CodeMirror Decoration 实战) 📅 发布时间:2026/9/14 23:12:50 👁 浏览次数: 鸿蒙 Markdown 编辑器表格所见即所得七个版本的渲染重构CodeMirror Decoration 实战摘要本文复盘鸿蒙 Markdown 编辑器表格所见即所得功能的七版迭代v1000037→v1000043。核心问题在于表格视觉是跨行的整体属性逐行渲染在架构上即错因此重构为整块 Widget 渲染随后主战场转向坐标映射通过拆分相对/绝对坐标修复了单元格串表、复制错位等缺陷复制格式经用户实测从 tab 分隔改为 Markdown 源码最终以三通道装机取证闭环验证。全文提炼出三条心得渲染形态跟随视觉所有权边界、坐标命名自解释、装机取证不可替代。上一篇讲到修复流水线这篇是流水线上打得最久的一场仗表格的所见即所得。从整块渲染重构到坐标映射修复构建版本从 v1000037 迭代到 v1000043——七个版本、两轮装机取证、一次架构评估结论不需要重构。这篇按问题拆开讲代码全部来自真实源码。一、问题定义表格所见即所得的四个矛盾Markdown 表格的源码是无定宽的管道符文本而用户期望的表格是有列宽、有边框的表格。要所见即所得必须解决四个矛盾列宽源码里每行长度不同渲染时同列必须等宽、按内容自适应分隔行|---|分隔行只在源码里有意义渲染态不该占一行高度编辑与渲染的切换光标进表格要能编辑源码移出要恢复渲染单元格内的行内格式加粗/斜体/链接在单元格里要正常渲染且选中复制时输出还能再粘贴回表格。第一版方案是逐行渲染每行分别加装饰。上线后立刻暴露问题——列宽跨行无法协调每行独立计算同一列宽度不一致、分隔行始终占位。结论表格的视觉是跨行的整体属性逐行方案在架构上就错了。这就是 v2 重构的起点。二、重构整块 Widget 与列宽网格v2 的核心决策光标不在表格内时把整张表用 Decoration.replace 替换成一个自绘table的 Widget——列宽协调、分隔行零占位、容器级横向滚动全部由这个整体 DOM 承担。列宽算法是纯计算每列取所有单元格内容宽的最大值封顶 20 个中文字符超出换行// editor-build/src/render/tableWidget.ts列宽计算节选// 文本显示宽度估算em全角字符中文/全角标点 1.1// 英文/数字等半角 0.55。列宽自适应与 20 个中文字封顶均按此计量。constMAX_COLUMN_CHARS20;// 3. 列宽每列所有单元格trim 后纯文本长度最大值 → 封顶 20 字符constcolWidths:number[]newArraynumber(colCount).fill(0.5);// ...遍历每行每格按全角/半角折算 unitsif(ccolWidths.lengthunitscolWidths[c]){colWidths[c]Math.min(units,MAX_COLUMN_CHARS);}// ...按 colWidths 输出 col单元格 word-break 换行编辑/渲染切换的规则也简单光标进入表格范围 → 整表回退源码可编辑移出 → 恢复渲染。但这带来一个新问题双栏模式的右栏是只读预览点击落点不是编辑光标不应该进源码态。解法是把表格装饰 field 参数化——同一套逻辑两种形态// editor-build/src/render/renderPlugin.ts节选exportfunctiontableDecorationFieldOf(opts:{mirror:boolean}):StateFieldDecorationSet{returnStateField.defineDecorationSet({create(state){returnbuildTableDecorations(state,opts.mirror);},update(deco,tr){if(tr.docChanged||tr.selection){returnbuildTableDecorations(tr.state,opts.mirror);}returndeco;}});}exportconsttableDecorationFieldtableDecorationFieldOf({mirror:false});// 镜像挂 tableDecorationFieldOf({ mirror: true })——跳过光标判定恒渲染主编辑器挂mirror: false光标判定生效右栏镜像挂mirror: true恒渲染。同一份代码、参数化两种行为比复制两套逻辑好维护得多。三、七版迭代的主战场坐标映射表格渲染态能看之后下一场仗是右栏的单元格交互点击定位、拖选整格高亮、复制。设计思路是把右栏的点击/拖选映射为编辑器的 state 选区字符级文档位置高亮与复制都基于这个选区。而它产出了整个表格战役最典型的一次翻车与修复。现象v1000037/v1000038 两轮装机反馈拖选单元格无高亮、复制内容错位、两个结构相同的表格互相串表在 A 表拖选B 表高亮了。分析定位按取证流程在装机环境挂上[tbl-debug]诊断日志日志实锤了自相矛盾单元格挂载的文档区间是cell20..28而整表的真实区间是88..256——单元格坐标比表格自己的起点还小。根因widget 在 DOM 上挂数据时把相对表格的偏移当成了绝对文档位置cell20是相对表首的偏移。选区相交判定全部拿这个错误坐标去比自然永远不相交、复制放行原生、同结构表光标乱串。修复代码拆分两种坐标各自只用在自己的语义里// editor-build/src/render/tableWidget.ts修复后节选letcontentFrom:numbert.from;// 绝对 doc pos——元数据挂载用letcontentTo:numbert.to;// linkfrom 含 [内容区从 1 起if(t.typelink){contentFromt.from1;contentTot.markerRanges[1].from;}// 标记区相对单元格偏移——渲染输出时删除for(constmoft.markerRanges){consts:numberm.from-tableFrom-cellRelFrom;// 相对坐标tableFrom cellRelFrom 换算conste:numberm.to-tableFrom-cellRelFrom;if(s0ecellText.lengthes){skips.push({start:s,end:e});}}letrelStart:numbercontentFrom-tableFrom-cellRelFrom;// 渲染切片用相对relCellFrom相对表格渲染切片用与contentFrom绝对 doc pos交互元数据用彻底分家。修复后专项 7/7、回归 24/24 全绿。这条修复还顺带救了一个静默缺陷真实文档表格内的行内格式此前因坐标语义错乱而静默丢失。后续两连修坐标修完装机又暴露两个只会在 ArkWeb 出现的问题点击单元格后光标消失CM6 的hasFocus要求activeElement必须是 contentDOM——editable:false把 tabindex 设在了错误元素上焦点回归修复表头高亮被压住z-index 被运行时注入样式压制用!important提权hilog 截图像素 CDP 三通道取证闭环。加上前面的坐标修复正是这七版37 初版 → 38 诊断 → 39 坐标修复 → 40 表头/选区塌缩 → 41 焦点回归 → 42 z-index 提权 → 43 清洁版。四、复制的格式抉择一次被用户实测推翻的决策右栏拖选单元格后复制最初的设计输出单元格内容 tab 分隔电子表格惯例。用户实测粘贴到别处不成表格。决策推翻改为输出 Markdown 表格源码管道符 表头分隔行粘贴到任何 Markdown 编辑器都还是表格// editor-build/src/render/tableSelection.ts节选exportfunctionbuildCopyText(view:EditorView,from:number,to:number):string|null{consttables:TableWrapMeta[]tableWrapsIn(view);lethasIntersectfalse;for(consttoftables){if(fromt.docTotot.docFrom){hasIntersecttrue;break;}}if(!hasIntersect)returnnull;// 选区不与表格相交 → 放行普通复制// ...按行收集相交单元格 → 输出 | a | b | 行 | --- | 分隔行列数表头选中格数}这条教训被总结成一句视觉粒度只服务选中体验复制以语法完整为准——边界格哪怕只选中半个字复制也输出整格源码因为半格源码粘出去是残缺语法。五、验证与效果最终状态整块渲染 jsdom 24/24、表格交互专项 7/7、复制单测 7 场景、十套件回归全绿装机验证走 hilog 实时抓取 CDP 断言 截图像素统计三通道七版之后清洁版闭环。用户确认拖选整格覆盖无缝隙、复制出来是规范 Markdown 表格、光标不再穿透单元格。六、能力边界表事项AI 表现我的结论整块 Widget 架构重构一次成型列宽算法稳定跨行视觉属性用整体 DOM 承担是正确抽象相对/绝对坐标混用两版未发现日志取证实锤跨边界坐标必须显式命名区分rel*/content*/doc*ArkWeb 特有缺陷焦点/z 序Chrome 无法复现装机取证才能定位三通道取证hilogCDP像素是标准动作复制格式决策tab vs Markdown给出利弊用户实测推翻交互语义类决策必须真实目标环境验证架构评估要不要推翻重来给出不需要重构的冷静结论修复期敢于评估架构、更敢于保留架构七、三条心得渲染形态要跟着视觉所有权的边界走表格的视觉是跨行的逐行装饰从架构上就是错的改对抽象后面全顺坐标系统要用命名自解释relCellFrom和contentFrom混用一次代价是三个版本的范围性 bug——跨边界的数据命名里就写清楚参照系模拟器/Chrome 正常不等于完工表格战役的四个缺陷里三个只在 ArkWeb 出现装机取证不是可选项。如果你在做编辑器渲染或者想看 MarkPin 后续关注专栏。