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

129 lines
8.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. **回归**:全量测试 + 新增折叠用例(重点:折叠后滚动、嵌套折叠不重复计数、补偿无跳变)