隐藏模式原理:为什么宽度计算如此重要
学完你能做什么
- 理解 OpenCode 隐藏模式的工作原理
- 知道为什么普通格式化工具在隐藏模式下会对齐错位
- 掌握插件的宽度计算算法(三步走)
- 了解
Bun.stringWidth的作用
你现在的困境
你用 OpenCode 写代码,AI 生成了一个漂亮的表格:
| 字段 | 类型 | 说明 |
|--- | --- | ---|
| **name** | string | 用户名 |
| age | number | 年龄 |在源码视图里看着挺整齐。但切到预览模式,表格却错位了:
| 字段 | 类型 | 说明 |
|--- | --- | ---|
| name | string | 用户名 | ← 怎么短了?
| age | number | 年龄 |问题出在哪?隐藏模式。
什么是隐藏模式
OpenCode 默认开启隐藏模式(Concealment Mode),它会在渲染时隐藏 Markdown 语法符号:
| 源码 | 隐藏模式下显示 |
|---|---|
**粗体** | 粗体(4 个字符) |
*斜体* | 斜体(4 个字符) |
~~删除线~~ | 删除线(6 个字符) |
`代码` | 代码(4 个字符 + 背景色) |
隐藏模式的好处
让你专注于内容本身,而不是被一堆 **、* 符号干扰视线。
为什么普通格式化工具会出问题
普通的表格格式化工具计算宽度时,会把 **name** 当作 8 个字符:
** n a m e ** = 8 字符但在隐藏模式下,用户看到的是 name,只有 4 个字符。
结果就是:格式化工具按 8 字符对齐,用户看到的却是 4 字符,表格自然就错位了。
核心思路:计算"显示宽度"而非"字符长度"
这个插件的核心思路是:计算用户实际看到的宽度,而不是源码的字符数。
算法分三步:
第 1 步:保护代码块(代码块里的符号不剥离)
第 2 步:剥离 Markdown 符号(**、*、~~ 等)
第 3 步:用 Bun.stringWidth 计算最终宽度跟我做:理解三步算法
第 1 步:保护代码块
为什么
行内代码(用反引号包裹)里的 Markdown 符号是"字面量",用户会看到 **bold** 这 8 个字符,而不是 bold 这 4 个字符。
所以在剥离 Markdown 符号之前,要先把代码块内容"藏起来"。
源码实现
// 第 1 步:提取并保护行内代码
const codeBlocks: string[] = []
let textWithPlaceholders = text.replace(/`(.+?)`/g, (match, content) => {
codeBlocks.push(content)
return `\x00CODE${codeBlocks.length - 1}\x00`
})工作原理
| 输入 | 处理后 | codeBlocks 数组 |
|---|---|---|
`**bold**` | \x00CODE0\x00 | ["**bold**"] |
`a` and `b` | \x00CODE0\x00 and \x00CODE1\x00 | ["a", "b"] |
用 \x00CODE0\x00 这种特殊占位符替换代码块,后面剥离 Markdown 符号时就不会误伤它们。
第 2 步:剥离 Markdown 符号
为什么
隐藏模式下,**粗体** 显示为 粗体,*斜体* 显示为 斜体。计算宽度时要把这些符号去掉。
源码实现
// 第 2 步:剥离非代码部分的 Markdown 符号
let visualText = textWithPlaceholders
let previousText = ""
while (visualText !== previousText) {
previousText = visualText
visualText = visualText
.replace(/\*\*\*(.+?)\*\*\*/g, "$1") // ***粗斜体*** → 文本
.replace(/\*\*(.+?)\*\*/g, "$1") // **粗体** → 粗体
.replace(/\*(.+?)\*/g, "$1") // *斜体* → 斜体
.replace(/~~(.+?)~~/g, "$1") // ~~删除线~~ → 删除线
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, "$1") //  → alt
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)") // [text](url) → text (url)
}为什么用 while 循环?
处理嵌套语法。比如 ***粗斜体***:
第 1 轮:***粗斜体*** → **粗斜体**(剥离最外层 ***)
第 2 轮:**粗斜体** → *粗斜体*(剥离 **)
第 3 轮:*粗斜体* → 粗斜体(剥离 *)
第 4 轮:粗斜体 = 粗斜体(无变化,退出循环)图片和链接的处理
- 图片
:OpenCode 只显示 alt 文本,所以替换为alt - 链接
[text](url):显示为text (url),保留 URL 信息
第 3 步:恢复代码块 + 计算宽度
为什么
代码块内容要放回去,然后用 Bun.stringWidth 计算最终的显示宽度。
源码实现
// 第 3 步:恢复代码块内容
visualText = visualText.replace(/\x00CODE(\d+)\x00/g, (match, index) => {
return codeBlocks[parseInt(index)]
})
return Bun.stringWidth(visualText)为什么用 Bun.stringWidth?
Bun.stringWidth 能正确计算:
| 字符类型 | 示例 | 字符数 | 显示宽度 |
|---|---|---|---|
| ASCII | abc | 3 | 3 |
| 中文 | 你好 | 2 | 4(每个占 2 格) |
| Emoji | 😀 | 1 | 2(占 2 格) |
| 零宽字符 | a\u200Bb | 3 | 2(零宽字符不占位) |
普通的 text.length 只能数字符个数,无法处理这些特殊情况。
完整示例
假设单元格内容是:**`code`** and *text*
第 1 步:保护代码块
输入:**`code`** and *text*
输出:**\x00CODE0\x00** and *text*
codeBlocks = ["code"]第 2 步:剥离 Markdown 符号
第 1 轮:**\x00CODE0\x00** and *text* → \x00CODE0\x00 and text
第 2 轮:无变化,退出第 3 步:恢复代码块 + 计算宽度
恢复后:code and text
宽度:Bun.stringWidth("code and text") = 13最终,插件按 13 字符的宽度来对齐这个单元格,而不是源码的 22 字符。
检查点
完成本课后,你应该能回答:
- [ ] 隐藏模式会隐藏哪些符号?(答:
**、*、~~等 Markdown 语法符号) - [ ] 为什么要先保护代码块?(答:代码块里的符号是字面量,不应被剥离)
- [ ] 为什么用 while 循环剥离符号?(答:处理嵌套语法,如
***粗斜体***) - [ ]
Bun.stringWidth比text.length好在哪?(答:能正确计算中文、Emoji、零宽字符的显示宽度)
踩坑提醒
常见误解
误解:代码块里的 ** 也会被剥离
事实:不会。插件会先用占位符保护代码块内容,剥离完其他部分的符号后再恢复。
所以 `**bold**` 的宽度是 8(**bold**),不是 4(bold)。
本课小结
| 步骤 | 作用 | 关键代码 |
|---|---|---|
| 保护代码块 | 防止代码块内的符号被误剥离 | text.replace(/\(.+?)`/g, ...)` |
| 剥离 Markdown | 计算隐藏模式下的实际显示内容 | 多轮正则替换 |
| 计算宽度 | 处理中文、Emoji 等特殊字符 | Bun.stringWidth() |
下一课预告
下一课我们学习 表格规范。
你会学到:
- 什么样的表格能被格式化
- 表格验证的 4 条规则
- 如何避免"无效表格"错误