This repository has been archived on 2026-10-09. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
new-api-key-bench/docs/JSON预览器代码折叠调研.md
skycurtain b3ad8a5a8e feat(view): JSON 折叠纯函数——括号栈扫描与可见性投影
- scanJsonFolds:括号栈单遍扫描(行尾 {/[ 开块、行首 }/] 收块),end=闭括号行(闭括号随块隐藏;LSP 文档的「闭括号前一行」是 0-based 记法的转述陷阱,注释已记录),单行块不产出,非法结构健壮跳过
- buildVisibleProjection:可见性投影,隐藏语义同 VS Code hidesLine(start < line ≤ end,折叠头可见),嵌套折叠区间覆盖天然正确
- findVisibleIndex:model 行号 → 可视索引反查
- clampLineToVisible:选区端点钳制到折叠头行(Monaco adjustSelections 语义)
- 新增 20 个纯函数单测(扫描规则/嵌套/非法结构/投影边界/钳制)
- 附调研文档 docs/JSON预览器代码折叠调研.md(VS Code/CM6 源码考证 + 交互惯例)
2026-09-09 14:16:51 +08:00

8.9 KiB
Raw Permalink Blame History

JSON 预览器代码折叠调研

如何在 bench 的虚拟滚动 JSON 预览器里实现 VS Code 式的代码块折叠——主流实现机制考证 + 对现有架构的嫁接方案。

调研日期:2026-09-09 · 方法:VS Code / CodeMirror 6 源码级考证 + JSON 查看器生态惯例 + 现有代码精读


1. 背景与结论

/api/log/token 的响应 pretty-print 后 2~4MB、约两万行,折叠对象/数组能显著降低阅读成本。现有预览器(json-text-view.vue + json-text-processor.ts)架构:

  • 纯函数 processor 切行 + 正则浅切词高亮(仅可视窗口行执行)
  • 等高行(20px)虚拟滚动:spacer 撑高 + translateY 窗口偏移 + overscan + trailing 节流
  • model 层行选区状态机(锚点行号,DOM Selection 只是同步目标)

结论:完全可行,且现有架构条件优越。 主流方案的本质是同一个模式——折叠范围(model 行号对)+ 折叠位集合 + 可见性投影(隐藏行从视图流移除)。现有代码"选区存 model 行号、渲染走投影"已经是 model/view 分离,加折叠只是把"目前可视坐标 = model 坐标的 1:1 映射"变成"折叠时跳过隐藏行的映射"。计算方式用括号栈行级扫描(VS Code 官方 JSON 语言服务同款),2 万行 O(n) 扫描不到 1ms。

2. 主流实现机制

2.1 VS Code / Monaco(源码考证)

关注点 机制 出处
数据结构 FoldingRange = (startLine, endLine) 行号对;FoldingRegions 平行数组(Uint32Array ×2)+ BitField 折叠位 foldingRanges.ts
核心语义 hidesLine(line) = start < line ≤ end——折叠头行(start)永远可见,隐藏 start+1 .. end(闭括号行也在隐藏区) 同上
视图投影 HiddenRangeModel 派生隐藏区间,view model 把隐藏行从视图流移除(非 CSS 遮盖);视图行号重排,gutter 显示 model 行号(跳号显示) hiddenRangeModel.ts
视觉呈现 chevron 画在首行 gutter;首行行尾追加 … 摘要(afterContentClassName: 'inline-folded') foldingDecorations.ts
gutter 交互 点击 chevron 图标本身才折叠/展开;控件默认 mouseover(悬停显示,已折叠区常显) folding.ts
滚动稳定 StableEditorScrollState:折叠前记录视口顶部锚点(model 行号 + 视口内像素偏移),折叠后反推新 scrollTop——折叠视口上方的块时视口不跳变 stableEditorScroll.ts

2.2 CodeMirror 6(源码 + 官方文档)

  • 折叠态是 editor state 的一部分:foldState = StateField<DecorationSet>,foldEffect/unfoldEffect(doc offset 区间)事务驱动
  • 折叠 = Decoration.replace({widget}) 把区间收成一个占位 widget(默认 …,可自定义「N lines」);点击占位符展开
  • 与自身 viewport 虚拟渲染天然兼容:替换后可视行数减少,viewport 按行块计算自然适配
  • 选区保护:foldState.update 末尾检查选区头——落进折叠区时该折叠自动清除(光标移入即展开)

2.3 JSON 查看器生态(Chrome DevTools / Firefox JSONView / JSON Hero)

  • 折叠锚点都在键名行,摘要写在折叠行行尾(▸ {…} / N keys / N items)
  • 默认全展开;树形工具无行号槽、无快捷键;编辑器系才有快捷键(Ctrl+K Ctrl+[ 系)
  • 无 hover 展开预览的先例(摘要数字已够)

3. 折叠范围计算:括号栈(选型定论)

路线 原理 JSON pretty-print 适用性
缩进推导(VS Code 通用 fallback) 逐行缩进层级栈式配对 需处理空行归属、offSide 语义,JSON 上是绕路
括号栈行级扫描(VS Code JSON 语言服务同款) 行尾 {/[ 开块压栈;行首 }/] 收栈 零误判且更便宜(官方 jsonFolding.ts 同款)

选括号栈。pretty-print 后两条铁律:每行至多一个括号、缩进即结构(JSON.stringify 产物中字符串值不会跨行,行尾判定安全)。规则:

  • 开块行:行尾为 { 或 [ → 记录 start,压栈
  • 闭块行:行首 token 为 } 或 ] → 弹栈,产出 {start, end: 闭括号行 − 1}
  • end = 闭括号前一行(与 VS Code LSP FoldingRange、jsonFolding 惯例一致)
  • 单行块不产出折叠(closeLine > start + 1 才产出,同 jsonFolding 的 line > startLine + 1)
  • 顺带记录 depth(顶层块 = 1)与 kind(object/array),供摘要与「折叠全部」用

4. 嫁接到现有架构:改动点映射

现有代码的隐藏前提是可视坐标 = model 坐标。加折叠 = 显式建立映射。逐点:

现有代码 现状 折叠后
lineList model 全量行数组 不变(model 层真相)
computeVisibleRowWindow(lineList.length, …) 输入 model 行数 输入 visibleModel.length;输出为可视索引窗口
visibleLineList 行号 index + 1 model 行号直取 visibleModel[visIdx] 反查 model 行号;gutter 显示 model 行号(跳号,VS Code 惯例)
totalHeightPx lineList.length × 20 visibleModel.length × 20
gutterWidth model 总行数 仍用 model 总行数(跳号显示最大行号不变)
resolveLineFromPointer 几何换算直出 model 行号 先出可视索引,反查 model 行号(选区保持 model 坐标;几何反查天然不会命中隐藏行)
边缘自动滚动 lineAtEdge model 行号 同上反查
v-for :key model 行号 不变(Vue 复用稳定)
isLineSelected model 行号判断 不变(隐藏行不渲染,高亮自动正确)

新增 model 层状态:

  1. folds: JsonFoldRange[]——扫描产物,按 start 升序天然嵌套
  2. collapsedSet: Set<startLine>——已折叠集合(isCollapsed O(1))
  3. visibleModel: number[]——投影数组:未被隐藏的 model 行号按序排列(2 万行 ≈ 80KB;全量重建 <1ms,先全量,不做局部增量)

5. 三个关键难点与解法

5.1 scrollTop 跳变补偿

VS Code StableEditorScrollState 的等高行等价实现:

折叠事务前:anchorModel = visibleModel[floor(scrollTop / 20)]
            deltaPx = scrollTop − visIdx × 20        (scrollTop = 0 时跳过)
折叠事务中:改 collapsedSet → 全量重建 visibleModel
折叠事务后:newScrollTop = findVisibleIndex(anchorModel) × 20 + deltaPx

锚点必须是 model 行号——折叠只改变 model 行的可视索引,不改变行本身。

5.2 选区与隐藏行冲突

采用 Monaco 语义(收缩) 而非 CM6 语义(自动展开)——查看器无编辑,收缩更符合阅读直觉:

  • 选区端点落入隐藏区(start < line ≤ end)→ 钳制到折叠头行 start
  • 复制语义不受损:选区提取按 model 行号在 lineList 纯数据操作,折叠区内内容依然完整可复制
  • syncDomSelection 需改为:选区端点先钳制到可见,再转可视索引,与可视窗口求交集后索引 DOM(原实现 model/可视坐标混用,折叠后会错位)

5.3 非 JSON 响应降级与状态重置

  • props.json === null(非 JSON 降级为纯文本)→ 不扫描、无 chevron、无摘要:raw 文本无可靠括号结构(HTML 里的 { 会误判),与现有降级链路一致
  • 切换响应(props.json 变化)时必须重置 collapsedSet——新数据的 folds 是新扫描结果,旧行号语义已失效

6. 交互清单(按生态惯例取舍)

项 决策 依据
gutter chevron(hover 显 / 已折叠常显) ✅ VS Code mouseover 控件惯例
行尾摘要 {…} N 行,点击展开 ✅ 编辑器系 … + JSON 工具系行数计数的并存;行数可预判展开长度,适合日志大 JSON
全部折叠(收 depth=1 顶层)/ 全部展开,放响应 meta 行 ✅ 2 万行场景最高频操作
默认全展开 ✅ 生态一致,不自作主张
快捷键 Ctrl+K Ctrl+[ 系 ❌ 暂缓 查看器无键盘编辑流
hover 展开预览 ❌ 生态无先例
分层折叠(折叠到 depth N) 🕐 后议 可用「折叠到第 N 层」一键替代
折叠态持久化 ❌ bench「无持久化、刷新即重置」哲学

7. 实施拆分

  1. 纯函数先行(json-text-processor.ts + 单测):scanJsonFolds / buildVisibleProjection / findVisibleIndex / clampLineToVisible
  2. 视图接线(json-text-view.vue):投影层改造上表 8 接入点 + chevron + 行尾摘要 + scrollTop 补偿事务 + defineExpose(collapseAll/expandAll/hasFolds)
  3. meta 行按钮(endpoint-panel.vue):折叠全部 / 展开全部
  4. 回归:全量测试 + 新增折叠用例(重点:折叠后滚动、嵌套折叠不重复计数、补偿无跳变)