用户追加三条需求,全部落地: 1) 打开方式 = 宿主探测应用 + 插件自研 open-with(并存) - 逐字恢复 src/client/open-with.ts / open-with-settings.tsx / plugin-settings.ts / src/open-external.ts / open.external 路由 / api.openExternal / editor descriptor 的 settings.render,以及 20 个 openWith* 词条(19 份词典从 main 取回原文) - 恢复 menu-flip.ts + layout.css 子菜单翻转规则(子菜单回来了) - FileTree 菜单段(无标题行):宿主行 → 分隔线 → 插件 pinned + 「在应用中打开 >」 子菜单(图钉 / SSH 后缀原样)→ 分隔线 → 宿主 reveal;宿主可用时过滤插件 explorer 目标,宿主不可用时插件行照旧(SSH 远端场景保持可用) - 死键清理:菜单去掉标题行后 openInApp 词条无引用,已从 21 份词典删除 2) 文件页多选右键「压缩并打包下载」 - 新增 src/zip.ts(自研 ZIP:方法 0/8 按收益二选一、UTF-8 bit 11、目录条目、 自实现 CRC32、无第三方依赖)+ src/archive-route.ts(名净化 + 递归收集)+ GET /sidebar/archive(同一道 workspace fence、目录递归跳过符号链接、 10000 条目 / 256 MiB 上限)+ api.archiveUrl - FileTree:多选 ≥2 项或单选目录时出现;fetch → blob → objectURL 下载, 路由错误(超限 / fence / 非 GET)以 zipFailed 显示在错误条 - 修掉真实缺陷:central directory 只写 MS-DOS 位而 version-made-by 声称 Unix, Info-ZIP 会把每个文件解成 mode 000;改为 Unix 语义(0o100644 * 0x10000, << 16 会溢出 int32)。系统 unzip 复验:mode 644/755、unzip -t 无错 3) 文件变动页层级树 + 视觉打磨 - 新增 src/client/changes/change-tree.ts:单子目录链压缩成一行、目录先于文件、 大小写不敏感排序、文件带 GitFileStatus、目录带下级变更数 - GitLens 两组各自渲染树:折叠状态按「组+路径」独立、默认展开;分组头吸顶 + 计数胶囊;Stage all/Unstage all 改为带 aria-label 的 IconButton;选中 = interactive-bg-active + 2px 内嵌强调条;12px/级缩进 + l1 参考线; 历史行 hash 徽标 / ref 胶囊 / 作者·时间右对齐 验证:typecheck / lint / test(134 文件 1422 通过)/ build 全绿; DSH_CMD='npx -y --package @deepseek-ai/dsh@0.1.7-rc.1 dsh' pnpm test:mount 7/7 通过; 真实宿主截图确认三条需求(多选+双源菜单+zip 项、变动页树形层级)
99 lines
3.2 KiB
TypeScript
99 lines
3.2 KiB
TypeScript
/**
|
|
* Client/host routing for the file tree's external-open action. Remote
|
|
* VSCode-family SSH URLs must launch on the browser machine, while local
|
|
* editor URLs and reveal actions keep using the DSH host opener.
|
|
*/
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { api } from '../src/client/api.ts'
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
function hostOk(): ReturnType<typeof vi.fn> {
|
|
return vi.fn(async () => ({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ ok: true, value: { started: true } }),
|
|
}))
|
|
}
|
|
|
|
describe('api.openExternal', () => {
|
|
it('launches an SSH remote-editor URL on the client without calling the host', async () => {
|
|
const assign = vi.fn()
|
|
const fetchMock = hostOk()
|
|
vi.stubGlobal('window', { location: { assign } })
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
const url = 'vscode://vscode-remote/ssh-remote+dev/home/u/f.ts'
|
|
|
|
await expect(api.openExternal({ action: 'url', url })).resolves.toEqual({ started: true })
|
|
expect(assign).toHaveBeenCalledOnce()
|
|
expect(assign).toHaveBeenCalledWith(url)
|
|
expect(fetchMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('launches Cursor/custom VSCode-family SSH URLs on the client too', async () => {
|
|
const assign = vi.fn()
|
|
const fetchMock = hostOk()
|
|
vi.stubGlobal('window', { location: { assign } })
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
await api.openExternal({
|
|
action: 'url',
|
|
url: 'cursor://vscode-remote/ssh-remote+dev/home/u/f.ts',
|
|
})
|
|
await api.openExternal({
|
|
action: 'url',
|
|
url: 'myfork://vscode-remote/ssh-remote+dev/home/u/f.ts',
|
|
})
|
|
|
|
expect(assign).toHaveBeenCalledTimes(2)
|
|
expect(fetchMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('keeps local editor URLs on the host opener', async () => {
|
|
const assign = vi.fn()
|
|
const fetchMock = hostOk()
|
|
vi.stubGlobal('window', { location: { assign } })
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
await expect(api.openExternal({
|
|
action: 'url',
|
|
url: 'vscode://file//tmp/a.ts',
|
|
})).resolves.toEqual({ started: true })
|
|
|
|
expect(assign).not.toHaveBeenCalled()
|
|
expect(fetchMock).toHaveBeenCalledOnce()
|
|
expect(fetchMock.mock.calls[0]?.[0]).toBe('/sidebar/api/open.external')
|
|
})
|
|
|
|
it('keeps reveal actions and http(s) lookalikes on the host opener', async () => {
|
|
const assign = vi.fn()
|
|
const fetchMock = hostOk()
|
|
vi.stubGlobal('window', { location: { assign } })
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
await api.openExternal({ action: 'reveal', path: '/tmp/a.ts' })
|
|
await api.openExternal({
|
|
action: 'url',
|
|
url: 'https://vscode-remote/ssh-remote+dev/home/u/f.ts',
|
|
})
|
|
|
|
expect(assign).not.toHaveBeenCalled()
|
|
expect(fetchMock).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('returns a rejected promise when client navigation throws', async () => {
|
|
const error = new Error('navigation failed')
|
|
const fetchMock = hostOk()
|
|
vi.stubGlobal('window', { location: { assign: vi.fn(() => { throw error }) } })
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
await expect(api.openExternal({
|
|
action: 'url',
|
|
url: 'vscode://vscode-remote/ssh-remote+dev/home/u/f.ts',
|
|
})).rejects.toBe(error)
|
|
expect(fetchMock).not.toHaveBeenCalled()
|
|
})
|
|
})
|