ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CodeMirror 6 代码块渲染踩坑:一个空行毒死全文档(鸿蒙编辑器卡片化/折叠/点击进源码)

CodeMirror 6 代码块渲染踩坑:一个空行毒死全文档(鸿蒙编辑器卡片化/折叠/点击进源码) CodeMirror 6 代码块渲染踩坑一个空行毒死全文档鸿蒙编辑器卡片化/折叠/点击进源码摘要本文复盘 MarkPin 编辑器代码块渲染态改造中踩过的四个坑① 一个空首行因装饰排序违反 CodeMirrorstartSide约束异常兜底清空全文档装饰导致一个空行毒死全文档② 折叠态两个高度陷阱0.5px 行高偏差、行框 strut 撑住卡片不收缩③ 点击进源码态光标系统性偏一行最终绕开不可信的posAtCoords用确定性映射接管点击④ 卡片底色 z 序盖住选区高亮迁移到显式 layer 体系解决。每个坑都按「现象 → 分析定位 → 修复代码 → 验证」展开并沉淀出三条心得兜底要有传染半径意识、排序约束要在数据结构层解决、所见即所得里精确是硬指标。所见即所得编辑器里代码块是最复杂的渲染对象既要卡片化的视觉圆角、隔离边距、行号、语言标签又要折叠和一键复制还要支持点击进入源码态编辑。这篇讲 MarkPin 代码块渲染态改造里的四个代表性问题——每个都按「现象 → 分析定位 → 修复代码 → 验证」展开其中一个空代码块毒死全文档渲染的案例堪称本系列最经典的一行排序引发的血案。一、背景与目标改造前的代码块是逐行铺背景相邻两个代码块没有空行隔开时视觉上连成一片。改造目标四件事卡片化圆角卡片、与相邻块有隔离边距、带行号与语言标签折叠点击折叠为「▸ 已折叠 N 行」且不影响行数/字数统计复制悬停出现复制按钮复制块内纯代码不含围栏与语言标记点击进源码态预览模式点击渲染态代码块切换为可编辑源码光标精确落在点击处。实现机制延续系列一贯的做法装饰Decoration只改视觉源码零改动——折叠、卡片、行号全部是装饰层的事文档字节永远不动所以行数/字数/大纲天然不受影响。二、坑一一个空首行毒死全文档现象用户报告某个代码块首行是空的时不只是这个块渲染残缺全文档所有代码块的渲染全坏。分析定位这不是渲染逻辑错了是装饰构建抛了异常。代码块的装饰有严格的添加顺序要求CodeMirror 的 RangeSetBuilder 要求同一位置的装饰按 startSide 升序添加行装饰 → side 负数 widget → side 正数 widget。旧代码构建装饰数组后只按位置排序from → to稳定排序保持插入顺序——当首内容行是空行时语言标签工具区side 为正位置相同恰好被排到了行装饰前面顺序违反约束builder 直接抛异常。而渲染插件有个兜底构建异常时回退为无装饰——于是一个块的异常传染成全文档裸奔。修复代码给装饰加秩rank对齐 CodeMirror 的 startSide 语义排序比较器从两维变三维// editor-build/src/render/renderPlugin.ts真实代码节选// §3.20.2 排序秩RangeSetBuilder 要求同 from 位置按 startSide 升序添加// line deco widget side -1 side 1/replace。// 旧排序只看 from/to首内容行为空行时 push 在前的 toolbar 稳定排序后// 仍排行装饰前 → builder 抛异常 → 兜底清空全文档装饰一个空首行毒死全篇。constRANK_LINE0;constRANK_SIDE_NEG12;constRANK_SIDE_POS13;// 排序from → rankstartSide 升序→ todecorations.sort((a,b)a.from-b.from||a.rank-b.rank||a.to-b.to);constbuildernewRangeSetBuilderDecoration();for(constdofdecorations)builder.add(d.from,d.to,d.deco);验证空代码块、首行空代码块、多块共存三个场景全绿console 零错误顺带补齐了空代码块的完整卡片渲染此前空块走的是裸条降级路径。三、坑二折叠的两个高度陷阱折叠的原理第 2…N 内容行逐行替换为空Decoration.replace插一个「▸ 已折叠 N 行」标记决定行高。但落地时踩了两个高度陷阱陷阱一0.5px渲染态头部行比编辑态高 0.5px。根因是 CodeMirror 会在行内 widget 相邻处自动插入一个cm-widgetBuffer元素带 vertical-align恰好撑高行框。修复是在渲染态作用域内把它display:none——等高战争以 22.4px 22.4px 告终陷阱二折叠后卡片不收缩折叠后内容消失了卡片区域大小却不变。根因是行级 CSS 只压掉了 span 内容没压掉行框自身的 strut行距撑起的最小高度——每行残留约 21px。修复是行级类直接把字号与行高压到零// editor-build/src/render/renderPlugin.ts折叠分支节选真实代码for(letlnfirstContent;lnlastContent;ln){constlinedoc.line(ln);// §3.20.4 教训延续行级 font-size/line-height 0 压掉行框 strutdecorations.push({from:line.from,to:line.from,deco:Decoration.line({class:mk-code-line mk-code-fold-zero}),rank:RANK_LINE});// 再 replace 行内容不含行末换行符避免跨行约束}配套 CSS 一行.mk-code-fold-zero { font-size: 0; line-height: 0 }。修复后折叠态卡片高度精确等于首行 标记行展开还原零残留。教训沉淀压高度要压行框而不是只压内容——CSS 行盒模型的 strut 会替你保留最小高度。四、坑三点击进源码态光标总偏一行现象预览模式点击渲染态代码块进入源码态编辑光标落点系统性偏移一行点空行落下一行。这个现象在普通文本行也存在原生posAtCoords的既有偏差但代码块区域因为有零高围栏行偏移更严重。分析定位排查结论渲染态布局下 CodeMirror 原生坐标换算posAtCoords的落点解析不可信。既然不可信就不修它、绕开它——预览模式主编辑器的所有点击统一接管用确定性映射计算光标位置行命中用几何区间判定字符位置用行内文本节点遍历测量TreeWalker getClientRects只有视口边缘等极端情况放行原生兜底。修复代码源码态本身用 StateField 管理点击命中代码块 → 派发 effect 加入源码态集合光标离开块范围自动恢复渲染态Obsidian 式且集合随文档编辑自动映射位置// editor-build/src/render/renderPlugin.ts节选真实代码exportconstcodeSourceEditFieldStateField.defineCodeFoldRange[]({update(value,tr){letnextvalue.map(r({// 文档变更时映射区间from:tr.changes.mapPos(r.from,-1),to:tr.changes.mapPos(r.to,1)}));for(consteoftr.effects){if(e.is(setCodeSourceEdit)!next.some(rr.frome.value.fromr.toe.value.to)){nextnext.concat([e.value]);}}// 光标离开块范围 → 恢复渲染态主规则if(tr.selection!undefined){constheadtr.state.selection.main.head;nextnext.filter(rheadr.fromheadr.to);}returnnext;}});// 点击入口判定pos 归属哪个代码块 tokenexportfunctionfindCodeBlockTokenAt(state:EditorState,pos:number):CodeFoldRange|undefined{for(consttofgetTokens(state)){if(t.typecode_blockpost.frompost.to){return{from:t.from,to:t.to};}}returnundefined;}进入源码态时还要做一次视口稳定补偿点击瞬间围栏行从零高变整行高页面会跳——进入/退出各做一次帧末补偿实测位移从半行级压到 1px 量级。五、坑四选区高亮被卡片底色吃掉卡片底色最初画在行级伪元素上z 序在选区层之上代码块内拖选文字看不到高亮。修复是把卡片底色迁移到 CodeMirror 的 layer 体系新建一层专画卡片矩形z 序排到选区层之下-3 -2选区高亮自然盖在卡片上层容器pointer-events:none不拦截点击。同一套 layer 后面又承担了头部行、底边框的绘制——视觉层与交互层分离后谁画在哪层变成显式声明。顺带把高亮语言也升了级highlight.js 从手动注册 51 种改为全量 193 种语言注册别名arkts/ets/ts/c 等内置识别语言标签显示官方名ts→ TypeScript配色对齐 VS Code 官方色板并适配浅色/深色/护眼三主题。六、验证与效果验证组合jsdom 套件装饰结构断言 Chrome 行为验证点击映射、折叠往返、复制内容 装机 CDP 断言真 ArkWeb 内 JS 断言 系统剪贴板闭环复制后真按 CtrlV文档增量恰为纯代码 像素级 diff悬停按钮显隐的截屏对比中全屏唯一差异簇就是按钮显现区。最终用户确认四目标全部达成且源码模式零影响。七、能力边界表事项AI 表现我的结论卡片化/折叠/复制的机制设计一次成型装饰方案干净装饰只改视觉、源码零改动是正确的第一性原则排序约束startSide首版遗漏异常后快速定位兜底逻辑异常回退空集会放大局部错误设计时要考虑传染半径CSS 行盒模型strut两次踩坑0.5px/不收缩压高度必须压行框widget 缓冲元素是隐形变量坐标换算可信度识别出 posAtCoords 系统性偏差并绕开不可信的基元就别修它用确定性映射接管入口层序管理迁移到显式 layer 体系视觉分层显式化后遮挡类 bug 消失八、三条心得渲染插件的兜底要有传染半径意识异常回退全文档清空装饰等于让一个块的 bug 变成全文档的 bug——兜底要降级到最小作用域统一排序约束要在数据结构层解决排序秩不要靠注意插入顺序的约定——约定必然被打破所见即所得的交互闭环里精确是硬指标光标偏一行这种像素级问题在编辑器里就是功能性 bug值得动用接管确定性映射的重手段。如果你在做编辑器渲染或者想看 MarkPin 后续关注专栏。
返回列表