dsh-ide
v0.3.3
Published
Right-Sidebar code review + GitLens plugin for DeepSeek Harness: an embedded CodeMirror editor with a per-line blame gutter.
Maintainers
Readme
dsh-ide — 给 DSH 侧边栏换上一个真正的代码编辑器
这是什么
dsh-ide 是 DeepSeek Harness(DSH)的一个插件。DSH Web 界面右侧边栏里打开代码文件时,默认给你的是一块纯文本查看面板;dsh-ide 把它换成真正的编辑器(CodeMirror 6),并在编辑器上补齐阅读和修改代码时最需要的那些上下文:逐行 Git blame、光标行的内嵌 Git 标注、面板内的 Git 历史与提交差异、大纲树、Ctrl/Cmd+点击跳转到定义、以及不依赖外部工具的静态检查。
它解决的问题很具体:在 DSH 侧边栏里看代码或改代码时,一块纯文本面板回答不了「这一行是谁什么时候写的」「这个文件最近改了什么」「文件结构长什么样」「这个符号定义在哪」;想回答就得离开 DSH,切到别的编辑器。dsh-ide 让你不离开 DSH 就拿到接近 IDE 的这些信息,并且改完可以直接 Ctrl/Cmd+S 存回磁盘。
功能一览
编辑器
| 能力 | 说明 |
|---|---|
| CodeMirror 6 编辑器 | 语法高亮、行号、撤销/重做、多选区与多光标、矩形(列)选区、拖放光标、输入法(IME)输入、Tab 缩进、不可见特殊字符可视化 |
| 保存 | Ctrl/Cmd+S 写回磁盘(可改绑);状态条显示 已同步 / 未保存 / 已保存,保存失败时显示真实原因 |
| 文件内查找 / 替换 | Ctrl/Cmd+F 打开的是插件自己的面板(中文、和工作区查找同一套控件与样式):查询框 + Aa / .* / \b + 匹配计数 + ↑/↓(Enter / Shift+Enter)+ 替换 / 全部替换。匹配用琥珀色底 + 下划线标出(自己控制配色,不再依赖编辑器默认的低对比高亮);选中一个词时同文件内的相同词也会高亮 |
| Ctrl/Cmd + 悬停显示可跳转 | 按住 Ctrl/Cmd 把鼠标移到标识符上,它会渲染成带虚线下划线的链接(背景微微泛蓝、光标变成手型)——点击就是跳转到定义,先给反馈再执行 |
| 工作区查找 / 替换 | Ctrl/Cmd+Shift+F 打开:在整个会话工作区里查找(大小写 / 正则 / 全词三种模式,结果按文件分组,点一行跳到那里),并把「替换为… + 全部替换」做在同一个面板里。替换只作用于面板里列出的那些行,且宿主写入前会逐行核对那行是否仍然是搜索时看到的内容——期间被外部改过的行会被跳过并在结果里报出来 |
| 主题 | 编辑器与悬停提示都走 DSH 的 CSS 令牌(--dsw-alias-*),跟随皮肤;令牌缺失时回退到内置深色配色 |
Git
| 能力 | 说明 |
|---|---|
| 逐行 blame gutter | 每一行左侧显示 短sha · 相对时间(如 3f9a1c2 · 2 天前);悬停给出该次提交的完整信息;工作区里还没提交的行显示「未提交」。点击某一行打开这次提交的 Git 卡片。整列可以在设置里关掉,或用快捷键 Ctrl+Alt+H 切换 |
| 代码变动标记 | 两处都标:每行左侧一根彩条(绿=新增、蓝=修改、红=这一行之后删除了代码,三种都是同样粗细的竖条),以及编辑器右边缘的概览标尺(整篇改动分布,点一下跳到对应位置——IDE 的那条)。基线是 HEAD(工作区相对上次提交的全部改动,已暂存与未暂存都算);未纳入版本控制的文件整篇按「新增」标绿。两处一起用 Ctrl+Alt+C 或设置里的开关切换 |
| 变动视图 | 状态条「变动」按钮打开:按行号顺序列出这个文件相对 HEAD 的每一处改动(+ / ~ / − + 行号 + 该行代码预览),点一行就跳到那行;状态条同时给出 +n −m,diff 被上限截断时写明「变动不完整」 |
| 光标行的内嵌 Git 标注 | 光标所在行的行尾跟着显示同一份 短sha · 相对时间(悬停给出作者与提交标题),点它直接打开该提交。可以在设置里关掉,也可以用快捷键切换 |
| 面板内的 Git 视图 | 点状态条「历史」,或点某一行 blame 进入。包含:该文件的历史列表(git log --follow,默认 50 条)、提交卡片(短 sha / 作者 / 邮箱 / 相对时间 / summary / 正文 / +n −n)、以及这次提交对本文件的 diff 分块(+ / - / 上下文三色);每行的新文件行号可点击,跳回编辑器对应行并关闭视图 |
| 刷新 Git | 外部提交过、或文件被外部改过之后,重新读取每一行的提交归属,重画 gutter 与内嵌标注 |
| 不完整提示 | blame 输出被上限截断时,状态条显示「blame 不完整」,而不是把部分归属当成全部 |
大纲与跳转
| 能力 | 说明 |
|---|---|
| 大纲树 | 面板左侧的大纲按缩进/标题层级叠成树:Python 的 def 挂在所属 class 下、JS/TS 的方法挂在类下、TOML/INI/PO 的键挂在所属节下、YAML/JSON 按缩进挂。每个节点按种类配字形与颜色(类 / 接口 / 函数 / 方法 / 类型 / 枚举 / 常量 / 标题 / 键 / 节 / 宏 / 对象 / 词条),可单个节点折叠,面板头部有「展开」「折叠」全部;点节点名跳行,点折叠箭头只折叠不跳行 |
| 跳转到定义 | Ctrl/Cmd+点击一个标识符:先在当前缓冲区里找声明行(瞬时),找不到再让宿主在会话工作区内搜索(仓库里用 git grep,非仓库退回有上限的目录遍历)。找不到时状态条直说「没有找到 X 的定义」,不会静默不动 |
| 返回 / 前进 | 导航历史是整个会话共享的(上限 100 条),不是每个文件各一份——从文件树打开另一个文件后按「← 返回」,会回到上一个文件和你离开时的行。记录来源:Ctrl/Cmd+点击、大纲点击、Git diff 行号、工作区查找命中、带行号的侧边栏导航;离开一个文件时会记住光标所在行。没得回退时状态条直说,不会静默无反应 |
静态检查
| 能力 | 说明 |
|---|---|
| 导入解析(零依赖) | 宿主自己解析:TS/JS/Vue/Svelte 的相对/绝对导入(含 /index.* 与 NodeNext 的 ./x.js → x.ts)、沿 node_modules 向上找裸包、Node 内置模块白名单、Python 的相对导入。找不到的用 @codemirror/lint 画波浪线,悬停显示文字 |
| ruff(可选) | 机器上有 ruff 时,Python 文件额外跑 ruff check --output-format=json(当前缓冲区经 stdin 检查,不落临时文件),规则按族归类为 error / warning / info。没有 ruff 就只做导入检查,状态条写「仅导入检查」 |
| 触发方式 | 编辑停下 400ms 后自动跑一次;也可以点状态条「重新检查」立刻重跑。状态条给出「N 错误 · M 提示」,检查被上限截断时显示「检查不完整」 |
字体与设置
| 能力 | 说明 |
|---|---|
| 字体族与字号 | 代码字体(CSS family 列表,按需加引号并保证等宽兜底)与代码字号 8–32px |
| 检测本机字体 | Chromium 的 Local Font Access API(需要授权),按 mono/code/fira/jetbrains/cascadia 等关键词筛出候选等宽字体,点击即应用;浏览器不支持时提示手动填写字体名 |
| 导入字体文件 | .ttf / .otf / .woff / .woff2 上传到宿主保存(页面无法写文件),页面每次加载时由宿主重新注册;设置面板列出已保存字体,可一键应用或删除 |
| 可配置快捷键 | 文件内查找、工作区查找、代码变动标记、左侧 blame 列、内嵌 Git 信息、返回、前进、大纲、刷新 Git、保存,十项都能改绑 |
它认领哪些文件
认领(在这些文件上接管编辑器):
| 类别 | 扩展名 |
|---|---|
| Python | py pyi |
| JS / TS 系 | js jsx mjs cjs ts tsx vue svelte |
| 数据与配置 | json jsonc yaml yml toml ini cfg cnf conf properties |
| SQL | sql |
| 脚本 | sh bash zsh ps1 bat |
| 其它语言 | go rs java kt cs rb php c h cc cpp hpp hh cxx |
| gettext | po pot |
刻意不认领的两类(这是有意的取舍,不是遗漏):
md/markdown/mdx:留给 better-sidebar 的 Markdown 预览器,它有预览/编辑切换、Mermaid 渲染和目录大纲,而这些在纯编辑器里没有。.env:那套渲染器会对凭据形态的内容做遮罩,纯编辑器不会。
代价是这两类文件在这个插件里没有 blame、没有导入检查。HTML、图片、PDF 等也照旧由各自的预览器处理。另外,个别被认领的扩展名(如 rb、php、ps1、bat)目前没有高亮语法,会以纯文本显示。
安装
发布到 npm 之后,两条路都只需要一条命令——优先走这条:
# Web / CLI(profile 名换成你要用的那个)
npx @deepseek-ai/dsh plugin --profile web add [email protected]桌面版:打开应用的插件管理窗口,添加 [email protected](桌面版的插件管理器只接受 npm registry 的包名/精确版本,见下面「桌面版」一节)。
包名 dsh-ide、access: public,publishConfig.registry 已写死指向 registry.npmjs.org,所以即使本机 registry 是镜像源,npm publish 也发到正确的地址。
发布到 npm(维护者)
npm publish --dry-run # 先看会发出去什么(24 个文件,约 425 kB)
npm login --registry=https://registry.npmjs.org # 本机默认 registry 是 npmmirror(只读镜像),登录必须显式指向 npmjs
npm publish # publishConfig 已指向 npmjs,不必再加 --registry每次发布都要递增 version(npm 不允许覆盖已发布的版本号);发布后用 npm view dsh-ide version 确认。
如果发布返回 403 ... Two-factor authentication or granular access token with bypass 2fa enabled is required:账号的 2FA 模式是「授权 + 写入」,~/.npmrc 里的经典 token 不能绕过它。二选一:
npm publish --otp=123456 # 现取一个 30 秒验证码
# 或者:npmjs.com → Access Tokens → 生成 Granular Access Token,
# 勾选 "Allow this token to bypass two-factor authentication",
# 再 npm config set //registry.npmjs.org/:_authToken=<新 token>,然后 npm publish从源码安装(开发用;未发布也能装)
1. 构建
在插件目录里:
npm install
npm run buildnpm run build 依次做三件事:编译宿主半片(tsc,输出 lib/index.js、lib/host/*.js 和 lib/types/**)、对浏览器半片做类型检查、再用 esbuild 把 src/client/index.tsx 连同 CodeMirror 打成 lib/client.js(CodeMirror 是内联进 bundle 的,不需要 CDN 或额外安装)。
2. 装进一个 DSH profile
npx @deepseek-ai/dsh plugin --profile web add C:\path\to\dsh-ide把路径换成这个目录的绝对路径。如果 dsh 已经在 PATH 上,可以把 npx @deepseek-ai/dsh 简写成 dsh。
这条命令做两件事:在 profile 目录里调用 pnpm,把这个目录写成一条 link: 依赖(例如 "dsh-ide": "link:C:/path/to/dsh-ide"),然后把 dsh-ide 追加进该 profile package.json 的 dsh.profile.bundles 列表。插件自己带的 cordis.patch.yml 提供这一层的声明,所以不需要手改 profile 的 patch 文件。
因为是 link: 而不是拷贝,之后改代码只需要重新 npm run build,不需要重新安装。
3. 重启 DSH,再刷新页面
dsh web为什么必须重启:profile 的 bundle 列表和宿主半片注册的路由,都是在 DSH 进程启动时读取的。为什么还要刷新页面:浏览器半片(lib/client.js)是页面加载时取回的。所以——
- 只改了浏览器半片:重新
npm run build,刷新页面即可; - 动了宿主半片(
src/index.ts、src/host/**)或增删了 bundle 行:需要重启 DSH 进程,再刷新页面。
4. 卸载
npx @deepseek-ai/dsh plugin --profile web remove dsh-idedsh plugin 把后面的参数原样转给 profile 目录里的 pnpm(remove 是 pnpm 的 uninstall 别名),随后按已安装状态重新对齐 dsh.profile.bundles:依赖没了,对应的 bundle 行也会被移除。同样要重启 DSH 并刷新页面才生效。
它需要一个什么样的侧边栏
dsh-ide 自己不带侧边栏,它需要宿主的侧边栏提供「文件查看器注册表」。
- 装了
dsh-better-sidebar:dsh-ide 注册一个优先级高于内置code查看器的 viewer,代码文件在这个编辑器里打开,其它文件继续用原来的预览器。本条是在本机验证过的路径(本机 profile 里是dsh-better-sidebar0.19.1)。 - 没装:插件退回用 DSH Core 右侧栏自带的 tab 注册表来认领同一批扩展名。这条兜底路径的注册与渲染现在有测试覆盖了(
scripts/verify-apply.mjs用假 context 真的跑一遍apply():断言 tab 类型的 id/kind/优先级、整地址 glob、canOpen只接受带会话的地址、座位名、两种地址各自渲染出编辑器还是「无会话」提示,以及两种模式不会同时注册);在浏览器里真的出现在右侧栏这一层仍未在本机验证(本机装了 better-sidebar,走的必定是上面那条),所以不承诺它在所有组合下都可用。它只接受带会话的地址(也就是从文件树打开的地址);不带会话的地址会提示「这个地址不带会话,无法在此编辑(请从文件树打开)」。
桌面版(Electron)
桌面版用的是同一套机制:一个 profile、一条依赖、dsh.profile.bundles 里的一行。区别是那个 profile 归 Electron 所有,规则更严:
dsh plugin --profile desktop ...会被启动器直接拒绝(profile "desktop" is managed exclusively by the Electron application),CLI 不能装、也不能改这个 profile。- 桌面版自己的插件管理器只接受 npm registry 的包名(可带精确版本或 tag):它读的
packageNameFromSpec()明确拒绝file:、路径、://和带空格的 spec,然后跑pnpm add <spec> --save-exact --ignore-scripts。所以没有发布到 registry 的本地插件,走不了那个界面。 - 组合时它还有一条围栏:profile 里每个启用的 bundle,其真实路径必须落在 profile 目录之内(
desktop-host的allowLinkedPackages关掉时)。也就是说 Web profile 那种link:C:/.../dsh-ide的写法在桌面版会启动失败——除非你跑的是工作区开发模式(pnpm run dev:desktop/start:desktop,它会传--allow-linked-profile)。
本机(打包版桌面应用 + $DSH_HOME/profiles/desktop)实际验证过的安装方式,一行命令:
npm run build # 改了 src/ 之后
npm run install:desktop # 打包 → 装进 profile → 补 lockfile → 记录 lockHash
npm run verify:desktop # 自查,不用重启应用就知道会不会启动失败scripts/install-desktop.mjs 做的正是「那个插件管理器会留下的状态」:把 tarball 放进 profile 并解到 node_modules/dsh-ide(桌面版是 hoisted 布局,必须是真目录而不是软链)、在 package.json 里加 "dsh-ide": "file:dsh-ide-0.3.3.tgz" 与 dsh.profile.bundles 的一行、按 pnpm 的写法补 pnpm-lock.yaml 的 importers/packages/snapshots 三处(integrity = sha512-<base64(sha512(tgz))>)、最后把 desktop-runtime-state.json 的 lockHash 更新成新 lockfile 的 sha256(这个值一致,应用就走「已准备」路径,不会重新准备 profile)。重复执行是安全的。
装完完全退出再启动桌面版才生效。
⚠️ 本机装的桌面版还有一个「第三方插件管理器」(@linxin666/dsh-client-ui-plugin-manager + dshmarket,来自 @linxin666/dsh-web-all),它才是那个 profile 的实际管理者:本机实测它会在自己的事务里重写 package.json/pnpm-lock.yaml(改前留下 package.json.bak-dsh-ide),并丢掉不是它自己装的条目——我第一次手工装的那份就是这样在第二天被清掉的。它自己的安装框(「安装目标」)其实接受 file: / link: / git 源(sourceKindOf() 把 link:/file:/git:/github: 归为非 registry 来源,只拦 shell 元字符),所以在那之后:
- 想让它管着这个插件:在那个界面的安装框里填
file:C:/Users/xiechunlong/person_data/codes/AIprojects/dsh-ide/dist/dsh-ide-0.3.3.tgz(用file:而不是link:,因为link:指到 profile 之外会让打包版桌面应用启动失败)。 - 或者等它发布到 npm 之后,在那里填
[email protected]—— 这是唯一完全受支持的路径。 - 在那之前,别在那个管理器里对 dsh-ide 做增删,否则会把我装的那份清掉(再跑一次
npm run install:desktop即可恢复)。
npm run verify:desktop(scripts/verify-desktop-install.mjs)逐条复核桌面版自己的规则:bundle 是否都在 profile 目录内、有没有软链、cordis.patch.yml / 宿主入口 / 客户端 bundle 是否齐全、dsh.client.platform 是否为 web、host 包是否声明成 peer 而不是 dependency、lockHash 是否与 lockfile 一致。
重启桌面版(完全退出再启动)后,插件与 Web 版行为一致:同一份 lib/client.js、同一批 $DSH_HOME/dsh-ide/ 下的字体与偏好设置。
本机的一个既有阻塞:这个桌面 profile 的 lockfile 里有一个包的发布时间落在 pnpm 的
minimumReleaseAge窗口内([email protected]),所以任何由 pnpm 驱动的安装/改动(包括桌面版插件管理器自己的pnpm add)都会以ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION失败。等这个包"变老"或者调整该策略后才会恢复;上面的手工流程不受影响(只有校验那一步需要--config.minimum-release-age=0)。
使用
一次典型的阅读/修改过程
- 在侧边栏的文件树里点开一个代码文件,面板里打开的就是这个 IDE 视图(编辑器 + 左侧 blame gutter)。
- 读归属:gutter 每行是
短sha · 相对时间;光标停在哪一行,行尾就跟着显示同一份信息。点 gutter 那一行或行尾标注,打开这次提交的卡片与差异。 - 跳转:
Ctrl/Cmd+点击一个标识符跳到它的定义;用「← 返回」回到跳转之前的位置,再「前进 →」走回去。 - 看历史:点状态条「历史」,上方是该文件的提交列表,点一条看它对这个文件改了什么;点 diff 里的行号回到编辑器对应行。
- 看结构:点「大纲」,在树里点节点跳行。
- 改代码:停下约 400ms 后会自动跑一次静态检查,波浪线处悬停看说明;改完按
Ctrl/Cmd+S保存。
状态条按钮
| 按钮 | 作用 | |---|---| | 保存 | 把当前缓冲区写回磁盘;没有未保存改动时不可点。在 better-sidebar 模式下,保存按钮由面板头部渲染(侧边栏根据插件上报的状态绘制,带未保存标记),状态条里就不重复出现 | | 重新检查 | 立刻重跑一次静态检查(平时的自动检查在编辑停下 400ms 后触发) | | ← 返回 | 回到上一个跳转位置;没有可回退的历史时不可点 | | 前进 → | 返回之后再往前走一步 | | 大纲 / 隐藏大纲 | 显示或隐藏左侧大纲树(隐藏时不再重算) | | 变动 / 收起变动 | 打开或收起「代码变动」视图:按行列出相对 HEAD 的增/改/删,点一行跳到那行 | | 全局查找 / 收起查找 | 打开或收起「工作区查找 / 替换」面板 | | 历史 | 打开面板内的 Git 视图:该文件历史 + 提交卡片 + 提交对本文件的差异 | | 字体/快捷键 / 收起设置 | 打开或收起设置浮层(字体、字号、两个 Git 开关、快捷键表) | | 刷新 Git | 重新读取每一行的提交归属(外部提交或外部改过文件之后点它) |
状态条上还会出现这些文字:已同步 / 未保存 / 已保存、Ctrl/Cmd+S 保存、未纳入版本控制、blame 不完整、保存失败:<原因>、检查中…、N 错误 · M 提示、检查不完整、仅导入检查 / 无问题,以及跳转提示(例如 符号 → 相对路径:行号)。Git 视图里另有 读取提交…、没有提交历史(文件可能未纳入版本控制) 和 该提交没有改动这个文件(可能只是重命名,或是合并提交)。
设置面板
设置面板有两个入口,渲染的是同一个组件、同一份存储:
- 状态条右侧的「字体/快捷键」按钮(两种集成模式都有,这是常驻入口);
- 装了 better-sidebar 时,侧边栏设置卡里属于本 viewer 的那一项。
所以不存在「某一项设置只有一个入口才能改」的情况。面板内容:代码字体、代码字号(8–32px,失焦时才提交,避免逐键夹取)、检测本机字体、导入字体文件、已保存字体列表、「光标行末尾显示内嵌 Git 信息」开关,以及快捷键表。
快捷键
默认值如下,按本机平台显示(Windows/Linux 上是 Ctrl,macOS 上是 Cmd)。
| 命令 | 默认 | 说明 |
|---|---|---|
| 文件内查找 / 替换 | Ctrl+F | 打开当前文件的查找与替换面板 |
| 工作区查找 / 替换 | Ctrl+Shift+F | 在整个工作区查找,并可批量替换列出的那些行 |
| 展开/折叠代码变动标记 | Ctrl+Alt+C | 切换相对 HEAD 的增/改/删彩色条 |
| 展开/折叠左侧 blame 列 | Ctrl+Alt+H | 切换每行 短sha · 相对时间 那一列 |
| 展开/折叠内嵌 Git 信息 | Ctrl+Alt+B | 切换光标行末尾的作者/时间标注 |
| 返回上一个位置 | Ctrl+Shift+← 或 Alt+← | 回到跳转之前的位置 |
| 前进 | Ctrl+Shift+→ 或 Alt+→ | 返回之后再往前走一步 |
| 显示/隐藏大纲 | Ctrl+Alt+O | 切换左侧大纲树 |
| 刷新 Git 信息 | Ctrl+Alt+G | 重新读取每行的提交归属 |
| 保存 | Ctrl+S | 写回磁盘 |
返回/前进各给了两个备选:Ctrl+Shift+←/→ 与 Alt+←/→。原因是没有任何一个组合在所有宿主里都能到达页面——Alt+←/→ 同时是浏览器的前进/后退,浏览器会自己执行历史跳转,而页面无法取消它(preventDefault 拦不住),实测「点按钮能回退、按快捷键没反应」就是它;Ctrl+Alt+←/→ 则是 Intel 显卡驱动的屏幕旋转热键。一个绑定可以写多个备选(用 , 分隔),命中任意一个即触发;面板里显示成 Ctrl+Shift+← / Alt+←。
怎么改绑:打开设置面板,点那一行的输入框,然后直接按下想用的组合键——不用手打字符串(手打一个永远匹配不上的绑定是这类功能最糟的失败模式)。按 Esc 清除绑定,右边的 × 也清除,清除后显示「未绑定」。不带真实修饰键的组合(例如单个字母)会被拒绝,否则编辑区里的普通输入会被这个绑定吃掉。按键别名是等价的(Left 与 ArrowLeft 是同一个键)。面板里显示的是本机真实按键名(Ctrl+Alt+H、Alt+←),存储形式是等价的 mod+alt+h。
改过的键不会挡住改进后的默认值:偏好里除了快捷键表,还记录「哪几个命令是你亲手改的」。没有这份记录时(旧版本写下的表),一个等于旧默认值的条目会被当作「当时没改过」而跟随新默认——否则新默认永远到不了已有安装;你亲手改过的(例如 alt+b)会被保留。这条记录在首次加载时由旧表推断一次并写回。
快捷键在编辑器有焦点时由编辑器处理;焦点停在面板里的按钮上时也不会失效(面板容器兜住这一层)。
关于「返回 / 前进」的几条规则(每一条都是按实机反馈修出来的):
- 只记录跳转,不记录滚动。没有可回退的位置时状态条会写「没有可返回的位置(历史只记录跳转)」,而不是静默无反应。
- 光标所在的那一行才算「我刚才在哪」。这条位置是随光标实时记录的,不是「写进历史时那一行」——从文件树点开另一个文件离开当前文件时,记录的是你正在读的那一行,所以返回会回到你真正看的位置,而不是回到文件开头。
- 同一个文件内部的跳转也进历史(点大纲、点变动列表、点查找结果、本文件内的跳转定义)。以前「同一个文件就更新当前条目、不新增」的规则会把这类跳转整个吃掉,于是既回不去也前进不了。
- 返回之后的「前进」一定还在:打开一个文件不算一次跳转。以前只要返回跨越了文件,打开目标文件时就会往历史里追加一条,把刚走过的那条前进支路截断——表现就是「明明返回成功了,前进却说没有可前进的内容」。
- 跳转 / 返回 / 前进之后,光标会落在目标行并被滚动到中间,键盘焦点也直接交给文件:不用再点一下文件才能用快捷键或直接打字(列表、大纲、搜索结果这些点击本来就把焦点留在了按钮上)。
数据存放位置
| 内容 | 位置 |
|---|---|
| 导入的字体文件 | $DSH_HOME/dsh-ide/fonts/<文件名> |
| 编辑器偏好:字体、字号、左侧 blame 列与内嵌 Git 开关、快捷键表,以及「亲手改过哪几个键」的记录 | $DSH_HOME/dsh-ide/prefs.json |
$DSH_HOME 没有设置时回退到 ~/.dsh。
这些都在宿主侧,既不属于某个浏览器,也不属于某个工作区,所以刷新页面、换一个浏览器、换一个工作区之后,设置和导入过的字体都还在(导入的字体在每次页面加载时由宿主重新注册回页面)。偏好文件以「写临时文件再改名」的方式更新,避免崩溃时留下半截文件被读成「没有设置」。早期版本存在浏览器 localStorage 里的字体选择,会在宿主还是空的时候被迁移一次。
已知限制
- 没有语言服务器:「跳转到定义」和「大纲」都是文本/正则级的,宏、动态导入、别名等复杂情况会找不到或落到不精确的行。
- 导入检查不解析别名:
tsconfig的路径别名、包的exports/imports映射都不求值,注释和字符串里的from '...'也不会剔除;别名与工作区包只报为提示,不报为错误。Python 的绝对导入被有意跳过(目标可能在 site-packages),只检查相对导入。 - ruff 是可选的,但项目自带的就够:查找顺序是
<工作区>/.venv、venv、env(Windows 上是Scripts\ruff.exe/ruff.cmd,.cmd/.bat会经cmd.exe执行),最后才是PATH上的ruff。所以只要你的项目里装了 ruff(uv / poetry / pdm / pip 建的 venv 都算),Python 文件就是全量 ruff 规则检查,不需要全局安装;前三者都找不到时才退回「仅导入检查」,状态条会写出来。跑 ruff 时cwd是会话工作区根,所以项目自己的pyproject.toml/ruff.toml配置同样生效。TS/JS 目前没有类似的外部检查器(只有导入解析),这一项是后续可以补的。 - 文件在「嵌套仓库 / worktree」里也认得出:每条按文件的 git 命令(blame、历史、提交差异、工作区变动)都在该文件所在目录运行,而不是会话根。工作区里可能有被外层仓库
.gitignore掉的嵌套仓库或git worktree(本机就是worktrees/ref-tape),从会话根看这种路径对 git 是不存在的——那会让「改动」看起来是干净的、blame 什么都答不出来。 - 工作区查找的边界:优先
git grep(仓库里快、且尊重.gitignore),非仓库退回有上限的目录遍历(800 文件 / 单文件 512 KB / 2 秒),此时读取任何扩展名的文本文件、以「含 NUL 字节即视为二进制」跳过;结果上限 200 处、查询最长 200 字符。正则模式在仓库里走的是git grep -E(POSIX ERE),与 JavaScript 正则语法不完全一致(例如\d),所以某些模式在仓库与非仓库下的行集可能略有差别;每行的列号与出现次数始终由 JavaScript 匹配器计算,显示是一致的。替换一次最多 50 个文件 / 2000 处 / 替换文本 1000 字符。 - 有大小上限:单次请求体上限 32 MiB,更大的文件保存会失败(客户端在上传前就用同一个上限拒绝,并直接说出文件大小,不会先传几十兆再报错);大于 256 KiB 的文件只扫描前 256 KiB 的导入,说明符最多 200 条,超限时状态条显示「检查不完整」。
md/markdown/mdx和.env不由本插件接管(原因见上),因此它们没有 blame 和导入检查。- 主题与语法配色是尽力映射 DSH 的 CSS 令牌,令牌缺失时回退到内置深色配色;界面文案目前是硬编码中文。
完整版(每一项的根因、实测数字、被推翻的早期结论,以及复现命令)见
docs/审查与修复记录.md。
开发
npm run build # 构建:宿主 tsc + 客户端类型检查 + esbuild 打包 lib/client.js
npm run verify # 类型检查 + 八套检查:
# verify-host(宿主行为:认证围栏、分发、工作区限定、读写、偏好、冷会话)
# verify-imports(导入解析,真实临时目录树)
# verify-outline(大纲树、地址解析、字体、快捷键语法)
# verify-nav(返回/前进历史:同文件跳转也是条目、跳转后前进仍在、离开时记的是当前光标行)
# verify-git(真实仓库上的 log/show 解析)
# verify-bundle(客户端 bundle 契约:注册 id、工厂、导出面)
# verify-apply(挂载测试:真的跑 apply(),覆盖模式 A / 模式 B 的注册与渲染)
# verify-fresh(产物新鲜度:重建并与 lib/ 逐字节比对)
npm run selftest # 宿主解析器夹具(blame / log / ruff 输出形态与归桶边界);
# 在 git 仓库目录内运行会追加真实 git 检查,否则只跑夹具源码位置:宿主入口是 src/index.ts,宿主实现都在 src/host/**(路由、git、导入解析、ruff、字体、偏好);浏览器半片在 src/client/**(编辑器、大纲、快捷键、偏好、传输层)。lib/ 是构建产物,profile 实际加载的就是它——改完源码必须重新构建,否则页面跑的还是旧产物。
更完整的实现说明、集成模式与三轮审查记录见 docs/审查与修复记录.md。
