dsh-vscode-mode 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -20
- package/lib/client.js +711 -77
- package/lib/client.js.map +1 -1
- package/lib/index.js +113 -0
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client/events.ts +8 -4
- package/src/client/fileReveal.ts +28 -0
- package/src/client/index.ts +44 -9
- package/src/client/sidebar/contextMenu.ts +99 -0
- package/src/client/sidebar/menuItems.ts +28 -0
- package/src/client/sidebar/panels/FileExplorer.ts +34 -4
- package/src/client/sidebar/types.ts +5 -0
- package/src/client/sidebarBridge.ts +247 -0
- package/src/client/styles/editor.css +13 -1
- package/src/client/ui/ContextMenu.ts +82 -0
- package/src/client/ui/DiffBadge.ts +5 -3
- package/src/client/ui/EditorView.ts +113 -43
- package/src/client/ui/SideEditorTab.ts +48 -0
- package/src/compat.ts +2 -0
- package/src/reveal.ts +64 -0
- package/src/rpc.ts +21 -0
- package/src/shared/rpc.ts +2 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "DSH 上的类 VSCode 编码体验:Monaco
|
|
3
|
+
"version": "0.1.23",
|
|
4
|
+
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 dsh-better-sidebar 侧边栏与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚),状态持久化到工作区旁车",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"dsh-plugin",
|
package/src/client/events.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode client — 跨组件/跨 slot 的窗口事件助手。
|
|
3
|
-
* 迁移自原 src/client/index.ts 的 openEditorView
|
|
3
|
+
* 迁移自原 src/client/index.ts 的 openEditorView 与事件派发,语义不改;
|
|
4
|
+
* 侧边栏形态(sidebarBridge 已注册路由)优先,否则回退 DOM 页签点击。
|
|
4
5
|
* 作者 ddj 2026-08-20
|
|
5
6
|
*/
|
|
7
|
+
import { routeSideEditor } from './sidebarBridge.js'
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
|
-
* 选择中央「文件编辑」页签(DOM 级,无需 store actions
|
|
10
|
+
* 选择中央「文件编辑」页签(DOM 级,无需 store actions)。仅旧页签形态使用。
|
|
9
11
|
* @author ddj 2026年08月26号
|
|
10
12
|
*/
|
|
11
13
|
function selectEditorTab(): void {
|
|
@@ -19,11 +21,12 @@ function selectEditorTab(): void {
|
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
|
-
*
|
|
24
|
+
* 打开「文件编辑」:侧边栏形态路由优先,旧形态回退中央页签。
|
|
23
25
|
* @author ddj 2026年08月26号
|
|
24
26
|
* @param path 要打开的路径(可空)
|
|
25
27
|
*/
|
|
26
28
|
export function openEditorView(path: string | null): void {
|
|
29
|
+
if (routeSideEditor(path ?? null, false)) return
|
|
27
30
|
selectEditorTab()
|
|
28
31
|
const target = path ?? null
|
|
29
32
|
setTimeout(() => {
|
|
@@ -32,11 +35,12 @@ export function openEditorView(path: string | null): void {
|
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
|
-
*
|
|
38
|
+
* 打开文件编辑页并聚焦指定文件的首个差异(侧边栏形态自动展开面板)。
|
|
36
39
|
* @author ddj 2026年08月26号
|
|
37
40
|
* @param path 待聚焦的差异文件路径
|
|
38
41
|
*/
|
|
39
42
|
export function openDiffView(path: string): void {
|
|
43
|
+
if (routeSideEditor(path, true)) return
|
|
40
44
|
selectEditorTab()
|
|
41
45
|
setTimeout(() => {
|
|
42
46
|
window.dispatchEvent(new CustomEvent('edrv:open-editor', {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 「在文件浏览器中打开」RPC 包装(树菜单与 Monaco 右键共用)。
|
|
3
|
+
* 归一化为 { ok, error? },不抛异常;无会话/异常降级返回错误文案。
|
|
4
|
+
* 作者 ddj 2026-08-27
|
|
5
|
+
*/
|
|
6
|
+
import { rpc } from './rpc.js'
|
|
7
|
+
|
|
8
|
+
export interface RevealOutcome {
|
|
9
|
+
ok: boolean
|
|
10
|
+
error?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 请求 host 在 OS 文件浏览器中打开/定位路径。
|
|
15
|
+
* @author ddj 2026年08月27号
|
|
16
|
+
* @param sessionId 会话 id(可空)
|
|
17
|
+
* @param path 工作区相对路径('' = 根目录)
|
|
18
|
+
* @returns 成功或失败原因
|
|
19
|
+
*/
|
|
20
|
+
export async function revealInExplorer(sessionId: string | undefined, path: string): Promise<RevealOutcome> {
|
|
21
|
+
if (!sessionId) return { ok: false, error: '无活动会话' }
|
|
22
|
+
try {
|
|
23
|
+
const res = await rpc('edrv.revealInExplorer', { sessionId, path })
|
|
24
|
+
return res.ok ? { ok: true } : { ok: false, error: res.error }
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return { ok: false, error: '打开异常:' + String(error) }
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode client — 浏览器半入口:slot 注册 + 装配。
|
|
3
|
-
* 挂点:
|
|
3
|
+
* 挂点:betterSidebar「文件编辑」Tab(侧边栏形态,对话+编辑同屏;未装 dsh-better-sidebar 时
|
|
4
|
+
* 回退 conversation.view 中央页签)+ conversation.input.dock 差异条 + header 差异角标。
|
|
4
5
|
* 与 Host 通信:同源 fetch('/edrv/rpc')(shared/rpc 契约)。
|
|
5
6
|
*
|
|
6
7
|
* ⚠️ 跨版本 slot 装配(2026-08-21):新版 DSH 的 slots 系统要求 slot 必须由父 entry
|
|
@@ -25,9 +26,13 @@ import type { FileOpenerRegistry } from './fileOpeners.js'
|
|
|
25
26
|
import { installOpenPathRouter, vscodeOpener, autoValue } from './openPathRouter.js'
|
|
26
27
|
import { SettingsContext } from './settingsContext.js'
|
|
27
28
|
import { SIDEBAR_PLUGIN, pickSettingsBinder, registerSlotSafely } from './compat.js'
|
|
29
|
+
import { detectSidebarService, installSideEditor, setEnsureSideEditor, SIDEBAR_INSTALL_CMD } from './sidebarBridge.js'
|
|
30
|
+
import { SideEditorTab } from './ui/SideEditorTab.js'
|
|
28
31
|
import { createAddToConversation } from './addToConversation.js'
|
|
29
32
|
import { createSidebarPanelRegistry } from './sidebar/registry.js'
|
|
30
33
|
import { createFilePanel } from './sidebar/panels/index.js'
|
|
34
|
+
import { createTreeMenuRegistry } from './sidebar/contextMenu.js'
|
|
35
|
+
import { createDefaultFileMenuItems } from './sidebar/menuItems.js'
|
|
31
36
|
import { createOutlinePanel } from './outline/index.js'
|
|
32
37
|
import { createOutlineSourceRegistry, registerBuiltinOutlineSources } from './outline/sources.js'
|
|
33
38
|
import type { CompatAdapter } from '../shared/compat.js'
|
|
@@ -70,6 +75,8 @@ export function apply(ctx: any): void {
|
|
|
70
75
|
const binder = pickSettingsBinder(ctx)
|
|
71
76
|
const settings = binder.scope
|
|
72
77
|
let selected = autoValue('auto')
|
|
78
|
+
/** 可选探测 betterSidebar 服务(不进 inject:缺失会让插件停靠等待,杀死回退路径)。 */
|
|
79
|
+
const sideService = detectSidebarService(ctx)
|
|
73
80
|
/** 「添加到对话」动作集:编辑区/Tab 右键菜单注入文件引用与代码块(conversation 服务缺失时各动作安全降级)。 */
|
|
74
81
|
const addToConversation = createAddToConversation(ctx)
|
|
75
82
|
|
|
@@ -78,6 +85,7 @@ export function apply(ctx: any): void {
|
|
|
78
85
|
{ name: '设置桥', active: settings !== undefined, note: settings !== undefined ? '使用 ' + binder.service + ' 桥' : '未绑定设置服务(fileOpenTool 持久化不可用)' },
|
|
79
86
|
{ name: '文件打开路由(workspaces.openPath)', active: Boolean(workspaces?.openPath), note: 'vscode 打开器优先,失败回退系统打开' },
|
|
80
87
|
{ name: '侧边栏打开器(' + SIDEBAR_PLUGIN + ')', active: registry.get(SIDEBAR_PLUGIN) !== undefined, note: registry.get(SIDEBAR_PLUGIN) !== undefined ? '已注册(优先级 80)' : '未检测到侧边栏打开能力' },
|
|
88
|
+
{ name: '侧边栏编辑区(' + SIDEBAR_PLUGIN + ')', active: sideService !== undefined, note: sideService !== undefined ? '编辑区=侧边栏 Tab(对话+编辑同屏)' : '未检测到;安装 dsh-better-sidebar 后刷新启用侧边栏形态(' + SIDEBAR_INSTALL_CMD + ')' },
|
|
81
89
|
]
|
|
82
90
|
|
|
83
91
|
ctx.provide('fileOpeners', registry)
|
|
@@ -87,6 +95,12 @@ export function apply(ctx: any): void {
|
|
|
87
95
|
const sidebarPanels = createSidebarPanelRegistry()
|
|
88
96
|
ctx.provide('edrvSidebarPanels', sidebarPanels)
|
|
89
97
|
ctx.effect(() => sidebarPanels.register(createFilePanel()), 'vscode-mode: sidebar panel')
|
|
98
|
+
// 文件右键菜单项注册表(对外 provide,供本插件/第三方注册;内置「在文件浏览器中打开」)
|
|
99
|
+
const fileMenuItems = createTreeMenuRegistry()
|
|
100
|
+
ctx.provide('edrvFileContextMenuItems', fileMenuItems)
|
|
101
|
+
for (const item of createDefaultFileMenuItems()) {
|
|
102
|
+
ctx.effect(() => fileMenuItems.register(item), 'vscode-mode: file context menu item ' + item.id)
|
|
103
|
+
}
|
|
90
104
|
// 大纲源注册表(公开预留口):第三方语言插件(LSP/VSIX 等)注册更高优先级源即可覆盖兜底
|
|
91
105
|
const outlineSources = createOutlineSourceRegistry()
|
|
92
106
|
ctx.provide('edrvOutlineSources', outlineSources)
|
|
@@ -126,14 +140,35 @@ export function apply(ctx: any): void {
|
|
|
126
140
|
}), 'vscode-mode: file link routing')
|
|
127
141
|
}
|
|
128
142
|
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
143
|
+
// 中央「文件编辑」页签(旧形态回退):类 VSCode 编辑器;侧边栏形态可用时编辑器住 betterSidebar Tab,
|
|
144
|
+
// 本页签不注册(避免双实例:Monaco×2 + diff dock 每会话单源抢占)。
|
|
145
|
+
const registerLegacyTab = (): void => {
|
|
146
|
+
registerSlotSafely(ctx, {
|
|
147
|
+
name: 'conversation.view',
|
|
148
|
+
id: 'edrv-editor',
|
|
149
|
+
order: 5,
|
|
150
|
+
label: '文件编辑',
|
|
151
|
+
inject: (sessionId: string) => ({ sessionId }),
|
|
152
|
+
}, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { layout: 'tab', sideHint: SIDEBAR_INSTALL_CMD, schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems })))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (sideService) {
|
|
156
|
+
// 侧边栏形态:注册「文件编辑」Tab(单实例、可按会话持久化、内容打开自动展开面板)
|
|
157
|
+
ctx.effect(() => installSideEditor({
|
|
158
|
+
service: sideService,
|
|
159
|
+
renderTab: (props: Record<string, unknown>) => React.createElement(SideEditorTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems })),
|
|
160
|
+
activeSession: () => {
|
|
161
|
+
const snapshot = sessions?.list?.getSnapshot?.() as { current?: string; byId?: Record<string, { cwd?: string }> } | undefined
|
|
162
|
+
const sessionId = snapshot?.current
|
|
163
|
+
if (!sessionId) return undefined
|
|
164
|
+
return { sessionId, cwd: snapshot?.byId?.[sessionId]?.cwd }
|
|
165
|
+
},
|
|
166
|
+
registerLegacyFallback: registerLegacyTab,
|
|
167
|
+
}), 'vscode-mode: sidebar editor tab')
|
|
168
|
+
} else {
|
|
169
|
+
setEnsureSideEditor(null)
|
|
170
|
+
registerLegacyTab()
|
|
171
|
+
}
|
|
137
172
|
|
|
138
173
|
// 对话输入框上方差异 dock:普通对话显示单文案按钮,文件编辑页由 EditorView 隐藏
|
|
139
174
|
registerSlotSafely(ctx, {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 文件管理右键菜单框架(类型 + 注册表 + 纯构建函数)。
|
|
3
|
+
* 镜像 sidebar/registry.ts 的注册表模式:create + register(返回注销) + list + subscribe,
|
|
4
|
+
* 由 client/index.ts `ctx.provide('edrvFileContextMenuItems', registry)` 对外暴露(第三方可注册)。
|
|
5
|
+
* 新增菜单项只需 register 一条 TreeMenuItem,面板在右键打开时经 buildTreeMenu 过滤/排序。
|
|
6
|
+
* 本模块不触 React/浏览器,可单测。
|
|
7
|
+
* 作者 ddj 2026-08-27
|
|
8
|
+
*/
|
|
9
|
+
import type { TreeEntry } from '../../shared/rpc.js'
|
|
10
|
+
import type { SidebarCtx } from './types.js'
|
|
11
|
+
|
|
12
|
+
/** 右键目标(树行/面板空白区,path 相对工作区根,'' 表示根目录)。 */
|
|
13
|
+
export interface TreeMenuTarget {
|
|
14
|
+
path: string
|
|
15
|
+
type: TreeEntry['type']
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 一条右键菜单项定义。 */
|
|
19
|
+
export interface TreeMenuItem {
|
|
20
|
+
id: string
|
|
21
|
+
label: string
|
|
22
|
+
/** 排序:数值越小越靠前(缺省 100)。 */
|
|
23
|
+
order?: number
|
|
24
|
+
/** 红色警示样式(如删除类操作)。 */
|
|
25
|
+
danger?: boolean
|
|
26
|
+
/** 置灰不可点(如无权限/目标不支持时)。 */
|
|
27
|
+
disabled?: boolean
|
|
28
|
+
/** 前置分隔线。 */
|
|
29
|
+
separator?: boolean
|
|
30
|
+
/** 显隐守卫:返回 false 则不显示。 */
|
|
31
|
+
visible?: (target: TreeMenuTarget, ctx: SidebarCtx) => boolean
|
|
32
|
+
run: (target: TreeMenuTarget, ctx: SidebarCtx) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 右键菜单项注册表(生命周期独立,可注册/注销/订阅)。 */
|
|
36
|
+
export interface TreeMenuRegistry {
|
|
37
|
+
register(item: TreeMenuItem): () => void
|
|
38
|
+
list(): readonly TreeMenuItem[]
|
|
39
|
+
subscribe(listener: () => void): () => void
|
|
40
|
+
get(id: string): TreeMenuItem | undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 校验菜单项并写入注册表(缺 id/run/label 抛 TypeError)。 */
|
|
44
|
+
function itemRegister(entries: Map<string, TreeMenuItem>, notify: () => void, item: TreeMenuItem): void {
|
|
45
|
+
if (!item.id || typeof item.run !== 'function' || !item.label) {
|
|
46
|
+
throw new TypeError('文件右键菜单项必须提供 id、label 和 run')
|
|
47
|
+
}
|
|
48
|
+
entries.set(item.id, item)
|
|
49
|
+
notify()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 创建生命周期独立的右键菜单项注册表。
|
|
54
|
+
* @author ddj 2026年08月27号
|
|
55
|
+
* @returns 菜单项注册表
|
|
56
|
+
*/
|
|
57
|
+
export function createTreeMenuRegistry(): TreeMenuRegistry {
|
|
58
|
+
const entries = new Map<string, TreeMenuItem>()
|
|
59
|
+
const listeners = new Set<() => void>()
|
|
60
|
+
const notify = (): void => {
|
|
61
|
+
for (const listener of listeners) listener()
|
|
62
|
+
}
|
|
63
|
+
const list = (): readonly TreeMenuItem[] =>
|
|
64
|
+
[...entries.values()].sort((a, b) => (a.order ?? 100) - (b.order ?? 100))
|
|
65
|
+
return {
|
|
66
|
+
register(item: TreeMenuItem): () => void {
|
|
67
|
+
itemRegister(entries, notify, item)
|
|
68
|
+
return () => {
|
|
69
|
+
if (entries.get(item.id) !== item) return
|
|
70
|
+
entries.delete(item.id)
|
|
71
|
+
notify()
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
list,
|
|
75
|
+
subscribe(listener: () => void): () => void {
|
|
76
|
+
listeners.add(listener)
|
|
77
|
+
return () => listeners.delete(listener)
|
|
78
|
+
},
|
|
79
|
+
get: (id: string) => entries.get(id),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 按目标构建当前可见菜单项(visible 过滤 + order 排序)。
|
|
85
|
+
* @author ddj 2026年08月27号
|
|
86
|
+
* @param registry 菜单项注册表
|
|
87
|
+
* @param target 右键目标
|
|
88
|
+
* @param ctx 面板共享上下文
|
|
89
|
+
* @returns 可见菜单项(已排序)
|
|
90
|
+
*/
|
|
91
|
+
export function buildTreeMenu(registry: TreeMenuRegistry | undefined, target: TreeMenuTarget, ctx: SidebarCtx): TreeMenuItem[] {
|
|
92
|
+
if (!registry) return []
|
|
93
|
+
const out: TreeMenuItem[] = []
|
|
94
|
+
for (const item of registry.list()) {
|
|
95
|
+
if (typeof item.visible === 'function' && !item.visible(target, ctx)) continue
|
|
96
|
+
out.push(item)
|
|
97
|
+
}
|
|
98
|
+
return out
|
|
99
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 文件管理右键菜单内置项。
|
|
3
|
+
* 首个内置项:「在文件浏览器中打开」(文件→OS Explorer 定位选中、目录→打开目录)。
|
|
4
|
+
* 反馈统一走 ctx.notify(由 EditorView 提供,落到编辑区路径栏状态)。
|
|
5
|
+
* 作者 ddj 2026-08-27
|
|
6
|
+
*/
|
|
7
|
+
import { revealInExplorer } from '../fileReveal.js'
|
|
8
|
+
import type { TreeMenuItem } from './contextMenu.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 构造内置右键菜单项列表(后续内置项直接追加)。
|
|
12
|
+
* @author ddj 2026年08月27号
|
|
13
|
+
* @returns 内置菜单项数组
|
|
14
|
+
*/
|
|
15
|
+
export function createDefaultFileMenuItems(): TreeMenuItem[] {
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
id: 'reveal-in-explorer',
|
|
19
|
+
label: '在文件浏览器中打开',
|
|
20
|
+
order: 0,
|
|
21
|
+
run: (target, ctx) => {
|
|
22
|
+
void revealInExplorer(ctx.sessionId, target.path).then((outcome) => {
|
|
23
|
+
ctx.notify?.(outcome.ok ? '已在文件浏览器中打开' : '打开失败:' + (outcome.error ?? '未知错误'))
|
|
24
|
+
})
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -3,17 +3,21 @@
|
|
|
3
3
|
* dsh-vscode-mode client — 侧边栏「文件管理」面板(懒加载目录树)。
|
|
4
4
|
* 点目录展开/收起(首次经 edrv.listDir 拉取并缓存),点文件经 ctx.openFile 打开;
|
|
5
5
|
* 差异角标取 pendingByPath,活动文件高亮;edrv:refresh 事件与手动刷新重载树。
|
|
6
|
-
*
|
|
6
|
+
* 右键行/空白弹出菜单:项来自 ctx.fileMenuItems 注册表(可拓展),
|
|
7
|
+
* 内置「在文件浏览器中打开」经 edrv.revealInExplorer 定位/打开。
|
|
8
|
+
* 作者 ddj 2026-08-26 / 2026-08-27
|
|
7
9
|
*/
|
|
8
10
|
import React from 'react'
|
|
9
11
|
import { rpc } from '../../rpc.js'
|
|
12
|
+
import { ContextMenu } from '../../ui/ContextMenu.js'
|
|
13
|
+
import { buildTreeMenu } from '../contextMenu.js'
|
|
10
14
|
import type { SidebarCtx } from '../types.js'
|
|
11
15
|
|
|
12
16
|
const DIR_CAP = 4000
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* 目录树面板主体。
|
|
16
|
-
* @param props.ctx 面板共享上下文(sessionId/openFile/activePath/pendingByPath)
|
|
20
|
+
* @param props.ctx 面板共享上下文(sessionId/openFile/activePath/pendingByPath/fileMenuItems/notify)
|
|
17
21
|
*/
|
|
18
22
|
export function FileExplorer(props) {
|
|
19
23
|
const ctx = props?.ctx
|
|
@@ -26,6 +30,7 @@ export function FileExplorer(props) {
|
|
|
26
30
|
const [expanded, setExpanded] = React.useState({})
|
|
27
31
|
const [loading, setLoading] = React.useState({})
|
|
28
32
|
const [error, setError] = React.useState(null)
|
|
33
|
+
const [menu, setMenu] = React.useState(null) // 右键菜单 { x, y, target }
|
|
29
34
|
const tokensRef = React.useRef({})
|
|
30
35
|
// expanded 的 ref 镜像:edrv:refresh 监听用首次渲染闭包,但需读到最新展开态
|
|
31
36
|
const expandedRef = React.useRef({})
|
|
@@ -113,6 +118,11 @@ export function FileExplorer(props) {
|
|
|
113
118
|
title: e.path,
|
|
114
119
|
style: { paddingLeft: 6 + depth * 14 },
|
|
115
120
|
onClick: () => { if (isDir) toggle(e.path); else openFile(e.path) },
|
|
121
|
+
onContextMenu: (ev) => {
|
|
122
|
+
ev.preventDefault()
|
|
123
|
+
ev.stopPropagation()
|
|
124
|
+
setMenu({ x: ev.clientX, y: ev.clientY, target: { path: e.path, type: e.type } })
|
|
125
|
+
},
|
|
116
126
|
},
|
|
117
127
|
React.createElement('span', { className: 'edrv-tree-chev' },
|
|
118
128
|
isDir ? (isOpen ? '▾' : '▸') : ''),
|
|
@@ -145,7 +155,24 @@ export function FileExplorer(props) {
|
|
|
145
155
|
return rows
|
|
146
156
|
}
|
|
147
157
|
|
|
148
|
-
|
|
158
|
+
// 面板区右键(空白处)→ 工作区根目录菜单;行内右键已在 rowEl 阻止冒泡。
|
|
159
|
+
const onPanelContext = (ev) => {
|
|
160
|
+
ev.preventDefault()
|
|
161
|
+
setMenu({ x: ev.clientX, y: ev.clientY, target: { path: '', type: 'directory' } })
|
|
162
|
+
}
|
|
163
|
+
// 当前右键目标的可视菜单项(构建时过滤/排序,registry 变化下次打开生效)。
|
|
164
|
+
const menuEntries = menu
|
|
165
|
+
? buildTreeMenu(ctx?.fileMenuItems, menu.target, ctx).map((item) => ({
|
|
166
|
+
id: item.id,
|
|
167
|
+
label: item.label,
|
|
168
|
+
danger: item.danger,
|
|
169
|
+
disabled: item.disabled,
|
|
170
|
+
separator: item.separator,
|
|
171
|
+
onClick: () => item.run(menu.target, ctx),
|
|
172
|
+
}))
|
|
173
|
+
: []
|
|
174
|
+
|
|
175
|
+
return React.createElement('div', { className: 'edrv-side-panel', onContextMenu: onPanelContext },
|
|
149
176
|
React.createElement('div', { className: 'edrv-side-head' },
|
|
150
177
|
React.createElement('span', { className: 'edrv-side-title' }, '资源管理器'),
|
|
151
178
|
React.createElement('span', { className: 'edrv-side-root', title: root || '' }, rootName),
|
|
@@ -157,5 +184,8 @@ export function FileExplorer(props) {
|
|
|
157
184
|
React.createElement('span', null, String(error)),
|
|
158
185
|
React.createElement('button', { className: 'edrv-side-btn', onClick: () => refresh() }, '重试'))
|
|
159
186
|
: null),
|
|
160
|
-
rowsOf('', 0))
|
|
187
|
+
rowsOf('', 0)),
|
|
188
|
+
(menu && menuEntries.length
|
|
189
|
+
? React.createElement(ContextMenu, { x: menu.x, y: menu.y, entries: menuEntries, onClose: () => setMenu(null) })
|
|
190
|
+
: null))
|
|
161
191
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* 作者 ddj 2026-08-26
|
|
5
5
|
*/
|
|
6
6
|
import type { OutlineSourceRegistry } from '../outline/types.js'
|
|
7
|
+
import type { TreeMenuRegistry } from './contextMenu.js'
|
|
7
8
|
|
|
8
9
|
/** 面板可用的共享上下文(由 SidebarView 从 EditorView 注入,面板不直碰其内部)。 */
|
|
9
10
|
export interface SidebarCtx {
|
|
@@ -20,6 +21,10 @@ export interface SidebarCtx {
|
|
|
20
21
|
editor?: () => unknown | null
|
|
21
22
|
/** 大纲源注册表(公开 provide 为 edrvOutlineSources;大纲面板解析符号用)。 */
|
|
22
23
|
outlineSources?: OutlineSourceRegistry
|
|
24
|
+
/** 文件右键菜单项注册表(公开 provide 为 edrvFileContextMenuItems;文件管理面板构建菜单用)。 */
|
|
25
|
+
fileMenuItems?: TreeMenuRegistry
|
|
26
|
+
/** 面板动作反馈(如右键菜单操作结果 → 编辑区路径栏状态)。 */
|
|
27
|
+
notify?: (message: string) => void
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
/** 单个侧边栏面板定义。 */
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 侧边栏编辑区桥:可选探测 dsh-better-sidebar 的
|
|
3
|
+
* ctx.betterSidebar 服务,并注册「文件编辑」Tab(单实例)。
|
|
4
|
+
* 可选依赖模式:服务缺失时调用方回退到中央页签形态;本模块不注册任何东西。
|
|
5
|
+
* 纯逻辑(探测/初始打开解析/角标计数)可单测,DOM/事件仅存在于 install 路径。
|
|
6
|
+
* 作者 ddj 2026年08月25号
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from 'react'
|
|
10
|
+
|
|
11
|
+
/** betterSidebar 服务名(ctx.get 用;不要加进 inject——缺失会让插件停靠等待)。 */
|
|
12
|
+
export const BETTER_SIDEBAR_SERVICE = 'betterSidebar'
|
|
13
|
+
/** 本插件在侧边栏注册的 Tab 类型 id(亦是 SidebarTab.type)。 */
|
|
14
|
+
export const SIDE_TAB_ID = 'edrv-editor'
|
|
15
|
+
/** 侧边栏 Tab 标题。 */
|
|
16
|
+
export const SIDE_TAB_TITLE = '文件编辑'
|
|
17
|
+
/** 推荐安装命令(提示条/兼容性报告共用)。 */
|
|
18
|
+
export const SIDEBAR_INSTALL_CMD = 'dsh plugin --profile web add dsh-better-sidebar'
|
|
19
|
+
|
|
20
|
+
/** Tab 元数据(openTab 种子随 Tab 持久化,创建路径用它恢复初始打开)。 */
|
|
21
|
+
export interface SideEditorMeta {
|
|
22
|
+
/** 待打开的工作区相对/绝对路径。 */
|
|
23
|
+
openPath?: string
|
|
24
|
+
/** 打开后聚焦首个差异。 */
|
|
25
|
+
focusDiff?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* betterSidebar 服务的最小结构面(结构性探测,不 import 第三方类型)。
|
|
30
|
+
*/
|
|
31
|
+
export interface SidebarServiceLike {
|
|
32
|
+
registerTab: (descriptor: unknown) => () => void
|
|
33
|
+
openTab: (seed: { type: string; title?: string; path?: string; meta?: unknown }, scope?: { sessionId: string; cwd?: string }) => void
|
|
34
|
+
isTabEnabled?: (id: string) => boolean
|
|
35
|
+
features?: readonly string[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 解析 Tab 挂载时的初始打开请求(创建路径读 tab.path/meta,聚焦路径走窗口事件)。
|
|
40
|
+
* @author ddj 2026年08月25号
|
|
41
|
+
* @param tab 侧边栏 Tab(path/meta 为可选字段)
|
|
42
|
+
* @returns 待打开路径与是否聚焦差异
|
|
43
|
+
*/
|
|
44
|
+
export function resolveInitialOpen(tab: { path?: string; meta?: unknown } | null | undefined): { path: string | null; focusDiff: boolean } {
|
|
45
|
+
const meta = (tab?.meta ?? {}) as SideEditorMeta
|
|
46
|
+
const path = typeof tab?.path === 'string' && tab.path ? tab.path : (typeof meta?.openPath === 'string' ? meta.openPath : null)
|
|
47
|
+
return { path: path || null, focusDiff: meta?.focusDiff === true }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 会话级待处理差异文件数缓存(Tab 角标同步读取;EditorView 侧栏形态写入)。 */
|
|
51
|
+
const sidePending = new Map<string, number>()
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 写入会话的待处理差异文件数(0 视为清除,不显示角标)。
|
|
55
|
+
* @author ddj 2026年08月25号
|
|
56
|
+
* @param sessionId 会话 id
|
|
57
|
+
* @param count 待处理差异文件数
|
|
58
|
+
*/
|
|
59
|
+
export function setSidePending(sessionId: string, count: number): void {
|
|
60
|
+
if (!sessionId) return
|
|
61
|
+
const value = Number.isFinite(count) ? Math.max(0, Math.floor(count)) : 0
|
|
62
|
+
if (value > 0) sidePending.set(sessionId, value)
|
|
63
|
+
else sidePending.delete(sessionId)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 读取会话的待处理差异文件数(角标用)。
|
|
68
|
+
* @author ddj 2026年08月25号
|
|
69
|
+
* @param sessionId 会话 id
|
|
70
|
+
* @returns 非负计数
|
|
71
|
+
*/
|
|
72
|
+
export function getSidePending(sessionId?: string): number {
|
|
73
|
+
if (!sessionId) return 0
|
|
74
|
+
return sidePending.get(sessionId) ?? 0
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 模块级侧栏打开路由(events.ts 优先走它;未注册 = 旧页签形态)。 */
|
|
78
|
+
let ensureSideEditor: ((path: string | null, focusDiff: boolean) => boolean) | null = null
|
|
79
|
+
|
|
80
|
+
/** 待消费的初始打开(Tab 组件尚未挂载时暂存;按会话匹配,挂载时取走)。 */
|
|
81
|
+
let pendingOpen: { sessionId: string; path: string | null; focusDiff: boolean } | null = null
|
|
82
|
+
/** Tab 组件当前是否有挂载实例(有则直接投递窗口事件,无则暂存待消费)。 */
|
|
83
|
+
let sideEditorMounted = false
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 注册/撤销侧栏打开路由。
|
|
87
|
+
* @author ddj 2026年08月25号
|
|
88
|
+
* @param fn 路由函数或 null
|
|
89
|
+
*/
|
|
90
|
+
export function setEnsureSideEditor(fn: ((path: string | null, focusDiff: boolean) => boolean) | null): void {
|
|
91
|
+
ensureSideEditor = fn
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 标记侧栏编辑器组件挂载状态(SideEditorTab 挂载/卸载时调用)。
|
|
96
|
+
* @author ddj 2026年08月25号
|
|
97
|
+
* @param mounted 是否已挂载
|
|
98
|
+
*/
|
|
99
|
+
export function setSideEditorMounted(mounted: boolean): void {
|
|
100
|
+
sideEditorMounted = mounted
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 暂存指定会话的待消费初始打开(Tab 组件未挂载时由 install 路径调用)。
|
|
105
|
+
* @author ddj 2026年08月25号
|
|
106
|
+
* @param sessionId 会话 id
|
|
107
|
+
* @param path 待打开路径(可空)
|
|
108
|
+
* @param focusDiff 是否聚焦首个差异
|
|
109
|
+
*/
|
|
110
|
+
export function stagePendingSideOpen(sessionId: string, path: string | null, focusDiff: boolean): void {
|
|
111
|
+
if (!sessionId) return
|
|
112
|
+
pendingOpen = { sessionId, path: path || null, focusDiff: focusDiff === true }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 取走指定会话的待消费初始打开(SideEditorTab 挂载时读取;不存在返回 null)。
|
|
117
|
+
* @author ddj 2026年08月25号
|
|
118
|
+
* @param sessionId 会话 id
|
|
119
|
+
* @returns 待打开请求或 null
|
|
120
|
+
*/
|
|
121
|
+
export function takePendingSideOpen(sessionId?: string): { path: string | null; focusDiff: boolean } | null {
|
|
122
|
+
if (!pendingOpen || !sessionId || pendingOpen.sessionId !== sessionId) return null
|
|
123
|
+
const value = { path: pendingOpen.path, focusDiff: pendingOpen.focusDiff }
|
|
124
|
+
pendingOpen = null
|
|
125
|
+
return value
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 打开/聚焦侧栏编辑器(打开路径可选;返回是否已被侧栏形态接管)。
|
|
130
|
+
* @author ddj 2026年08月25号
|
|
131
|
+
* @param path 待打开路径(可空)
|
|
132
|
+
* @param focusDiff 是否聚焦首个差异
|
|
133
|
+
* @returns 是否路由成功
|
|
134
|
+
*/
|
|
135
|
+
export function routeSideEditor(path: string | null, focusDiff: boolean): boolean {
|
|
136
|
+
if (typeof ensureSideEditor !== 'function') return false
|
|
137
|
+
try {
|
|
138
|
+
return ensureSideEditor(path, focusDiff) === true
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.warn('[dsh-vscode-mode] 侧栏编辑器打开失败(' + String(error) + '),已回退')
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 结构化探测 betterSidebar 服务(可选依赖:缺失/降级返回 undefined)。
|
|
147
|
+
* @author ddj 2026年08月25号
|
|
148
|
+
* @param ctx 客户端服务上下文
|
|
149
|
+
* @returns 服务结构面或 undefined
|
|
150
|
+
*/
|
|
151
|
+
export function detectSidebarService(ctx: { get: (name: string) => unknown }): SidebarServiceLike | undefined {
|
|
152
|
+
try {
|
|
153
|
+
const service = ctx.get(BETTER_SIDEBAR_SERVICE) as SidebarServiceLike | undefined
|
|
154
|
+
if (!service || typeof service.registerTab !== 'function' || typeof service.openTab !== 'function') return undefined
|
|
155
|
+
return service
|
|
156
|
+
} catch {
|
|
157
|
+
return undefined
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 注册侧边栏「文件编辑」Tab 并接管打开路由。
|
|
163
|
+
* Tab 被用户在 better-sidebar 设置里禁用时,打开路由降级为回退回调(中央页签)。
|
|
164
|
+
* @author ddj 2026年08月25号
|
|
165
|
+
* @param options 装配参数(组件依赖 + 活动会话解析 + 回退注册)
|
|
166
|
+
* @returns 卸载器(反注册 Tab + 撤销路由)
|
|
167
|
+
*/
|
|
168
|
+
export function installSideEditor(options: {
|
|
169
|
+
service: SidebarServiceLike
|
|
170
|
+
renderTab: (props: Record<string, unknown>) => unknown
|
|
171
|
+
activeSession: () => { sessionId?: string; cwd?: string } | undefined
|
|
172
|
+
registerLegacyFallback: () => void
|
|
173
|
+
}): () => void {
|
|
174
|
+
const { service, renderTab, activeSession, registerLegacyFallback } = options
|
|
175
|
+
let fallbackInstalled = false
|
|
176
|
+
const open = (path: string | null, focusDiff: boolean): boolean => {
|
|
177
|
+
if (typeof service.isTabEnabled === 'function' && !service.isTabEnabled(SIDE_TAB_ID)) {
|
|
178
|
+
if (!fallbackInstalled) {
|
|
179
|
+
fallbackInstalled = true
|
|
180
|
+
try { registerLegacyFallback() } catch (error) { console.warn('[dsh-vscode-mode] 回退注册失败(' + String(error) + ')') }
|
|
181
|
+
}
|
|
182
|
+
return false
|
|
183
|
+
}
|
|
184
|
+
const scope = activeSession()
|
|
185
|
+
const sessionId = scope?.sessionId
|
|
186
|
+
if (!sessionId) return false
|
|
187
|
+
try {
|
|
188
|
+
service.openTab({
|
|
189
|
+
type: SIDE_TAB_ID,
|
|
190
|
+
title: SIDE_TAB_TITLE,
|
|
191
|
+
...(path ? { path } : {}),
|
|
192
|
+
meta: { openPath: path || undefined, focusDiff },
|
|
193
|
+
}, { sessionId, cwd: scope?.cwd })
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.warn('[dsh-vscode-mode] openTab 失败(' + String(error) + ')')
|
|
196
|
+
return false
|
|
197
|
+
}
|
|
198
|
+
if (sideEditorMounted) {
|
|
199
|
+
// 已挂载:窗口事件直达 EditorView 监听器
|
|
200
|
+
window.dispatchEvent(new CustomEvent('edrv:open-editor', { detail: { path, focusDiff } }))
|
|
201
|
+
} else {
|
|
202
|
+
// 未挂载(面板折叠卸载/首次创建):暂存待消费,挂载时由 SideEditorTab 取走
|
|
203
|
+
stagePendingSideOpen(sessionId, path, focusDiff)
|
|
204
|
+
}
|
|
205
|
+
return true
|
|
206
|
+
}
|
|
207
|
+
let tabDisposer: (() => void) | null = null
|
|
208
|
+
try {
|
|
209
|
+
tabDisposer = service.registerTab({
|
|
210
|
+
id: SIDE_TAB_ID,
|
|
211
|
+
title: SIDE_TAB_TITLE,
|
|
212
|
+
icon: codeIcon,
|
|
213
|
+
single: true,
|
|
214
|
+
badge: (_ctx: unknown, scope: { sessionId?: string }) => getSidePending(scope?.sessionId) || null,
|
|
215
|
+
component: renderTab,
|
|
216
|
+
})
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.warn('[dsh-vscode-mode] 侧边栏 Tab 注册失败(' + String(error) + ')')
|
|
219
|
+
// 注册失败 = 侧栏形态不可用:立即回退中央页签,不接管打开路由
|
|
220
|
+
if (!fallbackInstalled) {
|
|
221
|
+
fallbackInstalled = true
|
|
222
|
+
try { registerLegacyFallback() } catch (inner) { console.warn('[dsh-vscode-mode] 回退注册失败(' + String(inner) + ')') }
|
|
223
|
+
}
|
|
224
|
+
return () => {}
|
|
225
|
+
}
|
|
226
|
+
setEnsureSideEditor(open)
|
|
227
|
+
return () => {
|
|
228
|
+
if (typeof tabDisposer === 'function') tabDisposer()
|
|
229
|
+
setEnsureSideEditor(null)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 侧边栏 Tab 图标(内联 SVG 代码括号,不依赖图标包)。
|
|
235
|
+
* @author ddj 2026年08月25号
|
|
236
|
+
* @param size 边长(px)
|
|
237
|
+
* @returns 图标元素
|
|
238
|
+
*/
|
|
239
|
+
function codeIcon(size: number): unknown {
|
|
240
|
+
return React.createElement('svg', {
|
|
241
|
+
width: size, height: size, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true, focusable: false,
|
|
242
|
+
},
|
|
243
|
+
React.createElement('path', {
|
|
244
|
+
d: 'M5.5 4.5 2.5 8l3 3.5M10.5 4.5 13.5 8l-3 3.5',
|
|
245
|
+
stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
246
|
+
}))
|
|
247
|
+
}
|