# 编辑器

[← 返回 README](../README.md) · [文档索引](./index.md)

编辑器整块（表头 + 正文 + 状态行）在同一个容器里；正文是**代码在上、规则面板在下**，均占满宽度。

它有四个「围着 textarea 的视图」：**高亮彩色层**、**校验**、**格式化**、**补全列表**。
本文讲各自的行为；校验单独在 [格式校验](./validation.md)，改属性的界面在 [规则面板](./rule-panel.md)。

## 快捷键与能力一览

| 能力 | 细节 |
| --- | --- |
| 即时生效 | 防抖 400ms 后写回文件并重绘页面 |
| 撤销 / 重做 | 每次连续输入的停顿算一步 |
| 查找 / 替换 | `Ctrl+F` 打开表头查找栏，见下 |
| 缩进 | `Tab` / `Shift+Tab`（补全列表打开时 `Tab` 是「采用建议」） |
| 立即写盘 | `Ctrl/Cmd+S` |
| 格式化 | 「更多操作」里的入口，见下 |
| 面板随编辑器伸缩 | 拖编辑器右下角的 resize 手柄，代码与面板一起变高，不会一大一小 |

## 查找 / 替换

- 查询取**编辑器里选中的那段文字**，`Aa` 控制大小写。
- 命中在彩色层里被圈出来（当前那条更重），`Enter` / `Shift+Enter` 走命中。
- **替换** / **全部替换**都是一次编辑 —— 一次 `Ctrl+Z` 全部撤回。
- 查询是**纯文本，不是正则**。
- 窄窗口下命中计数、按钮文字会依次让位（两级让步：640px 以下隐去命中计数、540px 以下
  `替换` / `全部替换` 换成 ↦ / ⇉），控件不会消失。

## 格式化

一条规则一行、每层缩进两空格、去掉空行。

- 它**只重排行，不改一行里写的东西** —— 缺分号、缺空格留给校验器报。
- 括号不配对的表**原样返回**（宁可不做，也不把一个坏表改得更坏）。
- 已经排好的表上，「格式化」这一项是置灰的。

## 高亮与补全

- 高亮用 DSH 自己的 shiki 配色 token
  （`--shiki-token-keyword/constant/string/comment/punctuation`），所以与 Markdown 代码块同色
  并随浅深主题切换；渲染前**逐段 HTML 转义**，样式表永远不可能变成标记。
- **补全数据来自浏览器本身**：属性名是从 `CSSStyleDeclaration.prototype` 枚举出来的
  （即该引擎真正认识的全部属性，含厂商前缀与新增属性），不依赖手写列表 ——
  手写列表只在没有 DOM 的调用方（如测试）里兜底。
  属性值的枚举集合仍由插件维护，但**每个候选都先过 `CSS.supports(prop, value)`**，
  引擎不认的直接不显示。
- `↑`/`↓` 选择、`Enter` 或 `Tab` 采用、`Esc` 关闭，鼠标按下即采用。
  补全只在规则块内提示属性名（选择器区域不打扰）。
- **只在真正键入时出现**：靠 `InputEvent.inputType` 判断，删除、粘贴、移动光标都不会弹出列表
  （老引擎缺这个字段时回退为「文本变长了才算输入」）。
- **一行里的多条声明只看当前那条**：`color: red; ` 之后（分号紧邻，还没开始写新属性）
  不再弹任何补全 —— 否则同一行的冒号会把光标所在位置误判成「值」。

## 在别的编辑器里改，这里跟着变

- 每 **1.5 秒**（只在页面可见时）问一次宿主「这张表动过没有」，判据是宿主报的**修订号**
  （`size/mtime`），不是文本内容 —— 编辑器把同一份内容重写一遍（改行尾、重新保存）
  不会被误判成冲突，而「文件被 touch 了一下」也不会被漏掉。
- **编辑器干净时直接采用**磁盘那份；**有没保存的改动时不覆盖**，改成页脚问
  「重新载入 / 覆盖它」。
- 宿主另外用 `fs.watch` 盯着样式表**目录**（不是单个文件：VS Code 默认写临时文件再改名覆盖，
  盯着旧文件的监听从此看不到任何东西）。那只是让同步快一点 ——
  起不来的文件系统上功能照旧，走轮询。
- **写盘前会先读一次**，判据相同：文件在别处被改过且编辑器里有没保存的内容，就不写。
  页面从后台切回前台时也会查一次，但那条路径**只发现、不采用** —— 你可能正读到一半。

## 历史版本

**每次覆盖写盘前都留一份快照**：被替换掉的内容存进 `.history/<文件名>/<毫秒戳>.css`。

- **每张表最多 20 份**（按时间从旧的开始剪）。
- 内容与最新一份相同时**不重复留档** —— 否则自动保存的防抖写盘会把有用的版本挤出去。
- 整张表的任何一次覆盖都留档：编辑器的自动保存、导入、以及**恢复本身** —— 所以恢复是可逆的。
- 从「更多操作 → 历史版本」按时间倒序列出版本（本地时间 + 字节数），点一条就恢复进编辑器并落盘。
- 恢复会**清空编辑器的撤销栈** —— `Ctrl+Z` 不该把你刚决定离开的那一版又变回来。
- 一次都没被覆盖过的表显示「还没有历史版本」。

## 问题检测

表头「更多操作 → 问题检测」一页**三节**：前两节问当前文档，第三节只读文本：

| 节 | 问的是什么 | 点一行做什么 |
| --- | --- | --- |
| 命不中的规则 | 这条规则的匹配数是不是 0 | 把那条**整条注释掉** |
| 透明与定位 | 这条规则的样本元素上有没有玻璃 / 定位的坏味道 | **跳到**那条规则（只看，不改） |
| 动效与性能 | 这条规则（或它引用的 `@keyframes`）写得贵不贵 | **跳到**那条规则（只看，不改） |

分开是有意的：命不中的规则通常就该注释掉，而诊断出来的问题不一定能靠删这条解决（有三条要改的是祖先，
不是这一条）；第三类压根不是「坏」，是**代价**。

### 命不中的规则

DSH 换代时，**构建哈希类名会变**（`hHd-Xa_footArea` 这种）。靠它们写的规则不会报错、也不会从表里
消失，只是**永远不再命中** —— 用户看到的是「我明明写了却没生效」。这一节把这件事变成一张可以逐条
处理的清单：

- 列出**当前页面上匹配数为 0** 的规则，每条带行号与选择器原文。
- 点一行 → 把那条**整条注释掉**（原样保留在 `/* … */` 里，随时能改回来）；点「全部注释掉」→ 一次
  改完，**一次 `Ctrl+Z` 可整体撤回**。
- 注释之后列表会自己缩短 —— 它每次都按当前文本重算，不需要再点一次「刷新」。

**哪些不报（这是这张表能不能用的关键）**：

| 跳过 | 原因 |
| --- | --- |
| `:hover` / `:focus` / `:active` 等状态伪类 | 静止页面上本来 0 命中，规则是好的 |
| `.active` / `.selected` / `.open` 等状态类 | 同上：要等交互才命中 |
| `::before` / `::after` 等伪元素 | `querySelectorAll` 从不返回伪元素的盒子 |
| `@media` / `@supports` 里嵌套的规则 | 当前视口或特性不满足时匹配数就是 0 |
| 插件自己那几行控件（`dshCc_*`） | 体检的是「你的规则」，不是插件在测自己 |

第一版把状态样式也算死规则，一次真实运行报了 49 条、绝大多数是悬停样式 —— 那种列表只会教人忽略它。
判据与 `window.dshCss.css.deadSelectors()` 完全一致（同一个函数），区别只是问题检测页还知道**每条在
源码里的位置**，所以能直接注释掉。

### 透明与定位

另外三个「真的会坏掉」的检查，逐元素判定（与预览块里那行提示是同一个函数 `diagnoseElement`）：

| 报什么 | 判据 |
| --- | --- |
| 毛玻璃叠了多层 | 元素自己有 `backdrop-filter`，而某个祖先也有 |
| opacity 废掉玻璃 | 元素自己或某个祖先是半透明（会建立独立的 backdrop root，玻璃等于没开） |
| fixed 被祖先捕获 | 元素是 `position:fixed`，而某个祖先带 `transform` / `filter` / `perspective` —— 它不再相对视口定位（[菜单点不了](./known-issues.md)就是这个） |

样本取**前 3 个命中元素**：命中几百个的规则不该为了体检把页面拖一顿。同一节里每条规则的多个问题按 id
去重，右侧徽标写「⚠ N 处」，鼠标停在上面才展开完整说法。

宁可漏报不可误报，所以这一节同样跳过状态伪类 / 状态类 / 伪元素 / 命不中的规则 —— 静止页面上它们的声明
根本没生效，报出来是噪音。

### 动效与性能

这一节**只读文本**，所以规则命不中也会报（代价写在源码里，成不生效是另一回事）。三条判据：

| 报什么 | 判据 | 为什么 |
| --- | --- | --- |
| `transition: all` | `transition` / `transition-property` 的某个分量就是 `all` | 连不该动的属性一起过渡（宽高、位置都跟着走），行为容易意外 |
| 动画在动布局属性 | `animation` / `animation-name` 引用的 `@keyframes` 里出现 `width` / `height` / `top` / `left` / `margin` / `padding` / `border-radius` / `box-shadow` 等 | 每一帧都要重排或重绘，低端机掉帧 |
| 模糊半径偏大 | `filter` / `backdrop-filter` 里 `blur(≥20px)` | 低端机上是实打实的开销 |

判据都往**漏报**那边偏，因为这一类「不报也不损失什么，报错了就把整张表变成噪音」：

- `transform` / `opacity` / `filter` 动画不报 —— 合成器就能做，报了等于给每个正常动画都标一遍；
- `transition: opacity .2s` 这种明确列出属性、小半径的模糊、`blur(var(--x))` 或没有单位的 `blur(0)`（判不了刻度）都不报；
- `@keyframes` 里的注释先清成空格再抓属性名，否则一段 `/* width: 100%; */` 会被当成真的在动宽度；
- `@media` 里的规则不参与这三节中任何一节 —— 它们属于哪个断点的问题，混进这张表只会误导。

## 相关

- [格式校验](./validation.md) —— 状态行那行红字是什么意思
- [规则面板](./rule-panel.md) —— 点选择器展开的面板
- [控件](./controls.md) —— 表头上那几个按钮与菜单
- [API](./api-host.md) —— 读写与历史快照的接口
