/** * @module @dongasai/dsh-editor/client * * **工作区文件编辑器 —— 浏览器半边**。给右侧栏补上「改」的能力,一共三个入口, * 共用同一份可编辑正文(`EditorSurface`): * * 1. **文档渲染器(默认档,`priority: 'extension'`)**:数据 / 配置 / 纯文本类扩展名。 * 这些文件在官方渲染器里本来只有只读纯文本或代码预览,没有行号跳转等不可替代的能力, * 所以让编辑器直接当默认视图 —— 从文件树点开就能改、就能存。 * 2. **文档渲染器(可选档,`priority: 'builtin'`)**:代码 / 文档类扩展名。官方已有更合适的 * 只读渲染器(Markdown 排版、代码高亮 + 行号定位),编辑器只作为「打开方式」下拉里的 * 一个选项出现,不抢默认。 * 3. **「编辑」按钮 + 编辑器页签**:预览工具栏上的按钮,对**任何文本文件**一键进编辑器 —— * 它用编辑器页签**顶掉**当前那张预览页签(同一个面板、同一个位置,`replaceTab`), * 该文件的编辑器页签已经开着时就聚焦它。这是给 `.md` / `.ts` 这类「默认不进编辑器」的 * 文件准备的快捷入口 —— 不用去「打开方式」下拉里翻。 * * ## 为什么按钮是「换页签」而不是「就地切换渲染器」 * * 预览用哪个渲染器由文档预览自己的 store 决定(`selected = candidates.find(id === * state.rendererId) ?? candidates[0]`),`state.rendererId` 只有它自己的下拉能写,外部插件 * 没有公开入口 —— 所以「原地把预览换成编辑器」做不到。右侧栏的**页签**是公开的: * `ctx.sidebarRightTabs.register` 注册一个 kind,`ctx.sidebarRight.openResource(address, * { kind, replaceTab })` 显式打开它并顶掉当前页签(侧边栏的 `claim(address, kind)` 只查 * `canOpen`、**不查 patterns**),于是编辑器页签既不会跟官方预览抢自动打开的文件,又能 * 一键到达、且不留下两张同文件的页签。 * * ## 数据不丢的三条规矩(改代码时别踩) * * 1. **未保存草稿活过重挂**:切标签 / 关掉页签再打开都会卸载正文组件,草稿放在模块级 * `drafts` 里按 `会话 + 路径` 索引,重挂时恢复(纯派生的 UI 缓存,保存或丢弃后立刻删)。 * 2. **磁盘变化不覆盖草稿**:渲染器模式下文件在磁盘上变了会推进 `content.revision`; * 缓冲区是脏的时候就只提示、不覆盖,横幅里由用户选「加载磁盘版本」还是「继续编辑」。 * 3. **保存带版本闸**:保存时把「缓冲区基线对应的版本」作为 `expectedVersion` 送上去, * 磁盘变过就返回 `stale-version`,横幅里让用户显式点「覆盖保存」。 * * 另外(渲染器模式):读盘后**总是**把读到的磁盘版本回报给 `content.loaded(version)`, * 即使缓冲区是脏的 —— 文档预览的 owner 用这个版本号判断「磁盘变了没有」,若回报旧版本, * 它的自动刷新会一直认为需要重读,形成重读循环。 * * ## 把文件与行号发到对话输入框 * * 编辑器正文的文本框上**右键**有一份菜单:除剪切 / 复制 / 粘贴 / 全选外,最后一项是 * **「发送到 DSH 输入框」** —— 它把「当前文件 + 选区所在行」拼成 DSH 的 `@` 引用 * (`@A.html#L15` / `@A.html#L15-20`,工作区内的路径按相对路径写)插进**这个会话的** * 对话输入框,位置就是输入框里光标上次所在处。 * * 写入面的来源有两条,**两条都不假设侧栏槽位实际传了什么 prop**: * * 1. **就近的槽位 prop**:渲染点给了 `inputActions` 就直接用(作用域最明确)。 * 2. **哨兵注册表兜底**:「文件编辑」之外,本插件还在 `conversation.input.dock` 注册了一个 * 渲染 `null` 的哨兵 —— 挂载时把该会话的 `inputActions` 记进模块级 `inputTargets`, * 卸载即删。那是官方自己(语音输入、目标栏)在用的位置,这个面一定有。 * * **为什么不只信槽位 prop**:slot catalog 列的是**类型并集**,某个渲染点可能只传了子集。 * `sidebar.right.tab.document` 的正文就拿不到 `useSessions`,而正文里直接调用它的后果是 * `TypeError` → React 卸载整棵子树 → **编辑器整块白屏**(真踩过,见 AGENTS.md 约束 19)。 * 所以工作区根也不走 prop:从 `ctx.sessions.list.getSnapshot()` 在事件里现读(比渲染期的 * prop 还新),读不到就退回绝对路径 —— 少一个相对化,绝不让整块正文挂掉。 * * 插进去的是**普通文本而不是芯片**:官方公开面 `InputActions` 只有 * `insertText(text, span)`,没有带结构化引用的插入(带引用的那条路只对内通过 * scoped event 开放);`#L` 行号语法交给模型侧解析,与 Harness 系统提示里规定的 * `@…#L` 引用写法一致。**尾随空格是刻意的** —— `@` 触发检测要求触发 token 紧贴光标, * 空格让刚插进去的引用不会又把 `@` 文件菜单唤出来。 * * 拿不到写入面(会话还没起来、对话区没打开、或输入框正被审批 / 问答占用)或插入被拒 * (正在提交、草稿刚变过)时,只在编辑器里亮一条横幅说明,不静默失败。 * * ## 语法高亮:一层等宽文本 + Custom Highlight API * * 可编辑正文是 `