dsh-vscode-mode 0.6.0 → 0.8.0
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 +40 -6
- package/assets/icon.svg +7 -0
- package/cordis.patch.yml +12 -0
- package/lib/client.js +9317 -7494
- package/lib/client.js.map +1 -1
- package/lib/index.js +1295 -173
- package/lib/index.js.map +1 -1
- package/locale/en.json +4 -0
- package/locale/zh.json +4 -0
- package/package.json +3 -1
- package/src/client/compat.ts +134 -4
- package/src/client/copyText.ts +28 -0
- package/src/client/fileClipboard.ts +46 -0
- package/src/client/imagePreview.ts +76 -2
- package/src/client/index.ts +68 -39
- package/src/client/monaco/lsp/lspClient.ts +46 -0
- package/src/client/monaco/lsp/providers.ts +245 -1
- package/src/client/nativeOpenStore.ts +91 -0
- package/src/client/officialSidebar.ts +11 -45
- package/src/client/openWithDialog.ts +66 -0
- package/src/client/outline/index.ts +1 -1
- package/src/client/pdf/pdfPanel.ts +18 -0
- package/src/client/searchSeed.ts +36 -0
- package/src/client/settingsContext.ts +6 -4
- package/src/client/sidebar/contextMenu.ts +51 -10
- package/src/client/sidebar/menuItems.ts +473 -50
- package/src/client/sidebar/panels/FileExplorer.ts +1 -1
- package/src/client/sidebar/panels/SearchPanel.ts +24 -3
- package/src/client/sidebar/panels/SvnPanel.ts +1 -1
- package/src/client/sidebar/panels/index.ts +1 -1
- package/src/client/sidebar/types.ts +6 -0
- package/src/client/styles/editor.css +5 -3
- package/src/client/svnStore.ts +6 -1
- package/src/client/tabActions.ts +22 -10
- package/src/client/ui/EditorView.ts +198 -25
- package/src/client/ui/McpSettings.ts +30 -0
- package/src/client/ui/OfficialSideTab.ts +1 -0
- package/src/client/ui/PerfSettings.ts +4 -1
- package/src/client/ui/PromptDialog.ts +63 -0
- package/src/client/ui/SideEditorTab.ts +1 -0
- package/src/client/ui/commandCatalog.ts +14 -0
- package/src/client/ui/icons.ts +72 -0
- package/src/compat.ts +2 -2
- package/src/dshVersion.ts +2 -1
- package/src/fileOpenSettings.ts +163 -40
- package/src/index.ts +15 -1
- package/src/lsp/client.ts +24 -0
- package/src/lsp/rpc.ts +57 -0
- package/src/lsp/server.ts +272 -2
- package/src/perf.ts +90 -4
- package/src/routes.ts +13 -1
- package/src/rpc.ts +294 -36
- package/src/shared/fsNames.ts +115 -0
- package/src/shared/keybindings.ts +2 -0
- package/src/shared/lsp.ts +79 -0
- package/src/shared/nativeOpen.ts +91 -0
- package/src/shared/rpc.ts +158 -1
|
@@ -18,14 +18,17 @@ export interface TreeMenuTarget {
|
|
|
18
18
|
/** 一条右键菜单项定义。 */
|
|
19
19
|
export interface TreeMenuItem {
|
|
20
20
|
id: string
|
|
21
|
-
|
|
21
|
+
/** 展示文案;目标相关时可传函数(buildTreeMenu 解析为字符串)。 */
|
|
22
|
+
label: string | ((target: TreeMenuTarget, ctx: SidebarCtx) => string)
|
|
22
23
|
/** 排序:数值越小越靠前(缺省 100)。 */
|
|
23
24
|
order?: number
|
|
25
|
+
/** 分组:同组条目相邻成块,组切换处由 buildTreeMenu 动态置前置分隔线(缺省不参与分组)。 */
|
|
26
|
+
group?: number
|
|
24
27
|
/** 红色警示样式(如删除类操作)。 */
|
|
25
28
|
danger?: boolean
|
|
26
|
-
/**
|
|
27
|
-
disabled?: boolean
|
|
28
|
-
/**
|
|
29
|
+
/** 置灰不可点(如无权限/目标不支持时):布尔或按目标动态求值。 */
|
|
30
|
+
disabled?: boolean | ((target: TreeMenuTarget, ctx: SidebarCtx) => boolean)
|
|
31
|
+
/** 前置分隔线(不带 group 的存量/第三方条目沿用静态语义)。 */
|
|
29
32
|
separator?: boolean
|
|
30
33
|
/** 显隐守卫:返回 false 则不显示。 */
|
|
31
34
|
visible?: (target: TreeMenuTarget, ctx: SidebarCtx) => boolean
|
|
@@ -81,19 +84,57 @@ export function createTreeMenuRegistry(): TreeMenuRegistry {
|
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
87
|
+
* 按目标解析动态字段并计算前置分隔线(纯函数)。
|
|
88
|
+
*
|
|
89
|
+
* 分隔线口径:
|
|
90
|
+
* - 带 group 的条目:与前一条可见条目组号不同即出线(首条除外)——组首条被 visible
|
|
91
|
+
* 隐藏时下一条自动顶上分组头,不丢线也不重复出线;
|
|
92
|
+
* - 不带 group 的存量/第三方条目:沿用其静态 separator;
|
|
93
|
+
* - 统一收尾:相邻分隔线合并为一条(动态组头与静态分隔线相邻时不叠双线)。
|
|
94
|
+
* @author ddj 2026年09月22号
|
|
95
|
+
* @param items 可见条目(已按 visible 过滤、按 order 排序)
|
|
96
|
+
* @param target 右键目标(动态 label/disabled 求值用)
|
|
97
|
+
* @param ctx 面板共享上下文
|
|
98
|
+
* @returns 解析后的条目(label/disabled 已定值、separator 已定值)
|
|
99
|
+
*/
|
|
100
|
+
export function markMenuSeparators(
|
|
101
|
+
items: readonly TreeMenuItem[],
|
|
102
|
+
target: TreeMenuTarget,
|
|
103
|
+
ctx: SidebarCtx,
|
|
104
|
+
): TreeMenuItem[] {
|
|
105
|
+
const out: TreeMenuItem[] = []
|
|
106
|
+
let prevGroup: number | null = null
|
|
107
|
+
let sepEmitted = false
|
|
108
|
+
for (const item of items) {
|
|
109
|
+
const grouped = typeof item.group === 'number'
|
|
110
|
+
let sep = grouped ? (out.length > 0 && item.group !== prevGroup) : (item.separator === true && out.length > 0)
|
|
111
|
+
if (grouped) prevGroup = item.group as number
|
|
112
|
+
if (sep && sepEmitted) sep = false
|
|
113
|
+
sepEmitted = sep
|
|
114
|
+
out.push({
|
|
115
|
+
...item,
|
|
116
|
+
label: typeof item.label === 'function' ? item.label(target, ctx) : item.label,
|
|
117
|
+
disabled: typeof item.disabled === 'function' ? item.disabled(target, ctx) === true : item.disabled === true,
|
|
118
|
+
separator: sep,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
return out
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 按目标构建当前可见菜单项(visible 过滤 + order 排序 + 动态字段解析 + 分组分隔线)。
|
|
126
|
+
* @author ddj 2026年08月27号 / 2026年09月22号
|
|
86
127
|
* @param registry 菜单项注册表
|
|
87
128
|
* @param target 右键目标
|
|
88
129
|
* @param ctx 面板共享上下文
|
|
89
|
-
* @returns
|
|
130
|
+
* @returns 可见菜单项(已排序、已解析)
|
|
90
131
|
*/
|
|
91
132
|
export function buildTreeMenu(registry: TreeMenuRegistry | undefined, target: TreeMenuTarget, ctx: SidebarCtx): TreeMenuItem[] {
|
|
92
133
|
if (!registry) return []
|
|
93
|
-
const
|
|
134
|
+
const visible: TreeMenuItem[] = []
|
|
94
135
|
for (const item of registry.list()) {
|
|
95
136
|
if (typeof item.visible === 'function' && !item.visible(target, ctx)) continue
|
|
96
|
-
|
|
137
|
+
visible.push(item)
|
|
97
138
|
}
|
|
98
|
-
return
|
|
139
|
+
return markMenuSeparators(visible, target, ctx)
|
|
99
140
|
}
|
|
@@ -1,23 +1,460 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode client — 文件管理右键菜单内置项。
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* 分组与显隐对齐 CodeBuddy 资源管理器参考图(区分文件/文件夹/根空白区):
|
|
5
|
+
* - 文件:打开方式… | 浏览器打开 | 对话 | 剪切·复制 | 复制路径 | 重命名·删除 | SVN 组
|
|
6
|
+
* - 文件夹:新建两项 | 浏览器打开 | 对话·在文件夹中查找 | 剪切·复制·粘贴 | 复制路径 | 重命名·永久删除 | SVN 组
|
|
7
|
+
* - 根空白区(path === ''):文件夹菜单去掉全部「仅非根」项
|
|
8
|
+
* 参考图中本架构无法实现的条目(在集成终端中打开 / 运行测试 / 调试测试 / Build 等)不显示。
|
|
6
9
|
* 反馈统一走 ctx.notify(由 EditorView 提供,落到编辑区路径栏状态)。
|
|
7
|
-
* 作者 ddj 2026-08-27 / 2026-09-03 / 2026-09-16
|
|
10
|
+
* 作者 ddj 2026-08-27 / 2026-09-03 / 2026-09-16 / 2026年09月22号
|
|
8
11
|
*/
|
|
9
12
|
import { isSvnDiffable } from '../../shared/svn.js'
|
|
10
13
|
import type { SvnChangeEntry } from '../../shared/svn.js'
|
|
11
14
|
import { svnActionOn, svnActionsFor } from '../../shared/svnActions.js'
|
|
12
15
|
import type { SvnActionContext, SvnTargetType } from '../../shared/svnActions.js'
|
|
16
|
+
import { baseNameOf, checkNewName, checkRenameName, joinRelPath } from '../../shared/fsNames.js'
|
|
17
|
+
import type { RpcMethod, RpcRequestMap, RpcResult } from '../../shared/rpc.js'
|
|
13
18
|
import { runSvnAction } from '../svnActions.js'
|
|
14
19
|
import type { SvnActionRunCtx } from '../svnActions.js'
|
|
15
20
|
import { revealInExplorer } from '../fileReveal.js'
|
|
16
21
|
import { statusOfAdd } from '../addToConversation.js'
|
|
22
|
+
import { rpc } from '../rpc.js'
|
|
23
|
+
import { absoluteOf, relativeOf } from '../tabActions.js'
|
|
24
|
+
import { copyText } from '../copyText.js'
|
|
25
|
+
import { clearFileClip, fileClipOf, setFileClip } from '../fileClipboard.js'
|
|
17
26
|
import type { TreeMenuItem } from './contextMenu.js'
|
|
18
27
|
import type { SidebarCtx } from './types.js'
|
|
19
28
|
import type { TreeMenuTarget } from './contextMenu.js'
|
|
20
29
|
|
|
30
|
+
/** 内置条目 order 分段(每段留空隙供后续插入;SVN 组统一 200+,不与内置段交叠)。 */
|
|
31
|
+
const ORDER_OPEN = 5
|
|
32
|
+
const ORDER_NEW_FILE = 10
|
|
33
|
+
const ORDER_NEW_FOLDER = 20
|
|
34
|
+
const ORDER_REVEAL = 30
|
|
35
|
+
const ORDER_ADD_REF = 50
|
|
36
|
+
const ORDER_FIND = 60
|
|
37
|
+
const ORDER_CUT = 70
|
|
38
|
+
const ORDER_COPY = 80
|
|
39
|
+
const ORDER_PASTE = 90
|
|
40
|
+
const ORDER_COPY_PATH = 110
|
|
41
|
+
const ORDER_COPY_REL = 120
|
|
42
|
+
const ORDER_RENAME = 140
|
|
43
|
+
const ORDER_DELETE = 150
|
|
44
|
+
const SVN_ORDER_BASE = 200
|
|
45
|
+
|
|
46
|
+
/** 分组号(组切换处由 buildTreeMenu 出分隔线;SVN 组起始 7,组内分段随共享元数据递增)。 */
|
|
47
|
+
const GROUP_OPEN = 1
|
|
48
|
+
const GROUP_REVEAL = 2
|
|
49
|
+
const GROUP_REF = 3
|
|
50
|
+
const GROUP_CLIP = 4
|
|
51
|
+
const GROUP_COPY_PATH = 5
|
|
52
|
+
const GROUP_EDIT = 6
|
|
53
|
+
const SVN_GROUP_BASE = 7
|
|
54
|
+
|
|
55
|
+
// --region 目标判定与动作收尾
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 目标是否有具体路径(非根空白区;复制路径/剪切/重命名/删除等「仅非根」守卫)。
|
|
59
|
+
* @author ddj 2026年09月22号
|
|
60
|
+
* @param target 右键目标
|
|
61
|
+
* @returns 是否非根
|
|
62
|
+
*/
|
|
63
|
+
function hasPath(target: TreeMenuTarget): boolean {
|
|
64
|
+
return Boolean(target.path)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 目标是否目录(含根空白区)。
|
|
69
|
+
* @author ddj 2026年09月22号
|
|
70
|
+
* @param target 右键目标
|
|
71
|
+
* @returns 是否目录
|
|
72
|
+
*/
|
|
73
|
+
function isDirTarget(target: TreeMenuTarget): boolean {
|
|
74
|
+
return target.type === 'directory'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 目标是否文件(树条目的非目录类型按文件处理)。
|
|
79
|
+
* @author ddj 2026年09月22号
|
|
80
|
+
* @param target 右键目标
|
|
81
|
+
* @returns 是否文件
|
|
82
|
+
*/
|
|
83
|
+
function isFileTarget(target: TreeMenuTarget): boolean {
|
|
84
|
+
return target.type !== 'directory'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 是否有可用工作区目录(复制相对路径需要)。
|
|
89
|
+
* @author ddj 2026年09月22号
|
|
90
|
+
* @param ctx 面板上下文
|
|
91
|
+
* @returns 是否可用
|
|
92
|
+
*/
|
|
93
|
+
function hasCwd(ctx: SidebarCtx): boolean {
|
|
94
|
+
return typeof ctx.cwd === 'string' && ctx.cwd.trim() !== ''
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 派发窗口事件(文件树刷新 / 页签路径改写 / 关签;node 测试环境安全 no-op)。
|
|
99
|
+
* @author ddj 2026年09月22号
|
|
100
|
+
* @param name 事件名
|
|
101
|
+
* @param detail 事件载荷
|
|
102
|
+
*/
|
|
103
|
+
function emit(name: string, detail: Record<string, unknown>): void {
|
|
104
|
+
if (typeof window === 'undefined') return
|
|
105
|
+
window.dispatchEvent(new CustomEvent(name, { detail }))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 文件操作 RPC 兜底执行:网络异常折算为失败结果,菜单动作统一按 ok/error 分支。
|
|
110
|
+
* @author ddj 2026年09月22号
|
|
111
|
+
* @param method RPC 方法名(edrv.fs.*)
|
|
112
|
+
* @param args 请求载荷
|
|
113
|
+
* @returns RPC 结果;异常时为失败结果
|
|
114
|
+
*/
|
|
115
|
+
async function fsCall<M extends RpcMethod>(method: M, args: RpcRequestMap[M]): Promise<RpcResult<M>> {
|
|
116
|
+
try {
|
|
117
|
+
return await rpc(method, args)
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return { ok: false, error: String(error) }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 文件操作成功收尾:反馈 + 目录树刷新 + SVN 变更重查。
|
|
125
|
+
* @author ddj 2026年09月22号
|
|
126
|
+
* @param ctx 面板上下文
|
|
127
|
+
* @param okText 成功反馈文案
|
|
128
|
+
*/
|
|
129
|
+
function fsDone(ctx: SidebarCtx, okText: string): void {
|
|
130
|
+
ctx.notify?.(okText)
|
|
131
|
+
emit('edrv:refresh', {})
|
|
132
|
+
ctx.refreshSvnChanges?.()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 破坏性动作确认(ctx.confirm 缺省回退 window.confirm;无确认通道一律拒绝)。
|
|
137
|
+
* @author ddj 2026年09月22号
|
|
138
|
+
* @param ctx 面板上下文
|
|
139
|
+
* @param message 确认文案
|
|
140
|
+
* @returns 是否确认执行
|
|
141
|
+
*/
|
|
142
|
+
function confirmed(ctx: SidebarCtx, message: string): boolean {
|
|
143
|
+
if (typeof ctx.confirm === 'function') return ctx.confirm(message)
|
|
144
|
+
if (typeof window !== 'undefined' && typeof window.confirm === 'function') return window.confirm(message)
|
|
145
|
+
return false
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --endregion
|
|
149
|
+
|
|
150
|
+
// --region 文件管理动作(新建 / 重命名 / 删除 / 粘贴)
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 新建文件/文件夹:名称弹窗 → shared 校验 → RPC(同名拒绝)→ 文件即打开 + 刷新。
|
|
154
|
+
* @author ddj 2026年09月22号
|
|
155
|
+
* @param target 右键目标(目录)
|
|
156
|
+
* @param ctx 面板上下文
|
|
157
|
+
* @param kind file = 新建文件;directory = 新建文件夹
|
|
158
|
+
*/
|
|
159
|
+
async function createEntry(target: TreeMenuTarget, ctx: SidebarCtx, kind: 'file' | 'directory'): Promise<void> {
|
|
160
|
+
const ask = ctx.prompt
|
|
161
|
+
if (!ask) {
|
|
162
|
+
ctx.notify?.('名称输入不可用')
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
const name = await ask(kind === 'file' ? '新建文件' : '新建文件夹', '')
|
|
166
|
+
if (name === null) return
|
|
167
|
+
const bad = checkNewName(name)
|
|
168
|
+
if (bad) {
|
|
169
|
+
ctx.notify?.(bad)
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
const path = joinRelPath(target.path, name)
|
|
173
|
+
const res = await fsCall(kind === 'file' ? 'edrv.fsCreateFile' : 'edrv.fsCreateDir', { sessionId: ctx.sessionId, path })
|
|
174
|
+
if (!res.ok) {
|
|
175
|
+
ctx.notify?.((kind === 'file' ? '新建文件失败:' : '新建文件夹失败:') + res.error)
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
if (kind === 'file') ctx.openFile(path)
|
|
179
|
+
fsDone(ctx, (kind === 'file' ? '已新建文件 ' : '已新建文件夹 ') + path)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 重命名:名称弹窗(预填原名)→ shared 校验 → RPC → 页签路径改写事件 + 刷新。
|
|
184
|
+
* @author ddj 2026年09月22号
|
|
185
|
+
* @param target 右键目标
|
|
186
|
+
* @param ctx 面板上下文
|
|
187
|
+
*/
|
|
188
|
+
async function renameEntry(target: TreeMenuTarget, ctx: SidebarCtx): Promise<void> {
|
|
189
|
+
const ask = ctx.prompt
|
|
190
|
+
if (!ask) {
|
|
191
|
+
ctx.notify?.('名称输入不可用')
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
const from = target.path
|
|
195
|
+
const name = await ask('重命名', baseNameOf(from))
|
|
196
|
+
if (name === null || name === baseNameOf(from)) return
|
|
197
|
+
const bad = checkRenameName(name)
|
|
198
|
+
if (bad) {
|
|
199
|
+
ctx.notify?.(bad)
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
const res = await fsCall('edrv.fsRename', { sessionId: ctx.sessionId, path: from, newName: name })
|
|
203
|
+
if (!res.ok) {
|
|
204
|
+
ctx.notify?.('重命名失败:' + res.error)
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
if (res.to !== res.from) emit('edrv:path-renamed', { from: res.from, to: res.to })
|
|
208
|
+
fsDone(ctx, '已重命名')
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 删除(文件文案「删除」/ 文件夹文案「永久删除」):确认框把关 → RPC → 关签事件 + 刷新。
|
|
213
|
+
* @author ddj 2026年09月22号
|
|
214
|
+
* @param target 右键目标
|
|
215
|
+
* @param ctx 面板上下文
|
|
216
|
+
*/
|
|
217
|
+
async function deleteEntry(target: TreeMenuTarget, ctx: SidebarCtx): Promise<void> {
|
|
218
|
+
const dirTarget = isDirTarget(target)
|
|
219
|
+
const name = baseNameOf(target.path)
|
|
220
|
+
const message = dirTarget
|
|
221
|
+
? '永久删除文件夹「' + name + '」及其全部内容?该操作不可恢复。'
|
|
222
|
+
: '删除文件「' + name + '」?该操作不可恢复。'
|
|
223
|
+
if (!confirmed(ctx, message)) return
|
|
224
|
+
const res = await fsCall('edrv.fsDelete', { sessionId: ctx.sessionId, path: target.path })
|
|
225
|
+
if (!res.ok) {
|
|
226
|
+
ctx.notify?.('删除失败:' + res.error)
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
emit('edrv:path-deleted', { path: target.path })
|
|
230
|
+
fsDone(ctx, (dirTarget ? '已永久删除「' : '已删除「') + name + '」')
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 粘贴文件剪贴板内容到目标目录(copy → fsCopy;cut → fsMove 成功后清槽)。
|
|
235
|
+
* @author ddj 2026年09月22号
|
|
236
|
+
* @param target 右键目标(目录)
|
|
237
|
+
* @param ctx 面板上下文
|
|
238
|
+
*/
|
|
239
|
+
async function pasteInto(target: TreeMenuTarget, ctx: SidebarCtx): Promise<void> {
|
|
240
|
+
const clip = fileClipOf()
|
|
241
|
+
if (!clip) {
|
|
242
|
+
ctx.notify?.('剪贴板为空')
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
const cut = clip.mode === 'cut'
|
|
246
|
+
const res = await fsCall(cut ? 'edrv.fsMove' : 'edrv.fsCopy', { sessionId: ctx.sessionId, from: clip.path, toDir: target.path })
|
|
247
|
+
if (!res.ok) {
|
|
248
|
+
ctx.notify?.('粘贴失败:' + res.error)
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
if (cut) {
|
|
252
|
+
clearFileClip()
|
|
253
|
+
emit('edrv:path-renamed', { from: res.from, to: res.to })
|
|
254
|
+
}
|
|
255
|
+
fsDone(ctx, (cut ? '已移动到 ' : '已复制到 ') + (target.path || '工作区根目录'))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// --endregion
|
|
259
|
+
|
|
260
|
+
// --region 内置菜单项分组
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* 分组 1:打开方式…(文件专属)+ 新建文件…/新建文件夹(文件夹专属,含根空白区)。
|
|
264
|
+
* @author ddj 2026年09月22号
|
|
265
|
+
* @returns 分组条目
|
|
266
|
+
*/
|
|
267
|
+
function openGroupItems(): TreeMenuItem[] {
|
|
268
|
+
return [
|
|
269
|
+
{
|
|
270
|
+
id: 'open-with',
|
|
271
|
+
label: '打开方式…',
|
|
272
|
+
order: ORDER_OPEN,
|
|
273
|
+
group: GROUP_OPEN,
|
|
274
|
+
visible: (target, ctx) => isFileTarget(target) && hasPath(target) && typeof ctx.openWith === 'function',
|
|
275
|
+
run: (target, ctx) => ctx.openWith?.(target.path),
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
id: 'new-file',
|
|
279
|
+
label: '新建文件…',
|
|
280
|
+
order: ORDER_NEW_FILE,
|
|
281
|
+
group: GROUP_OPEN,
|
|
282
|
+
visible: (target) => isDirTarget(target),
|
|
283
|
+
run: (target, ctx) => { void createEntry(target, ctx, 'file') },
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
id: 'new-folder',
|
|
287
|
+
label: '新建文件夹',
|
|
288
|
+
order: ORDER_NEW_FOLDER,
|
|
289
|
+
group: GROUP_OPEN,
|
|
290
|
+
visible: (target) => isDirTarget(target),
|
|
291
|
+
run: (target, ctx) => { void createEntry(target, ctx, 'directory') },
|
|
292
|
+
},
|
|
293
|
+
]
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 分组 2:在文件资源管理器中显示(文件/文件夹/根空白区共有)。
|
|
298
|
+
* @author ddj 2026年09月22号
|
|
299
|
+
* @returns 分组条目
|
|
300
|
+
*/
|
|
301
|
+
function revealGroupItems(): TreeMenuItem[] {
|
|
302
|
+
return [
|
|
303
|
+
{
|
|
304
|
+
id: 'reveal-in-explorer',
|
|
305
|
+
label: '在文件资源管理器中显示',
|
|
306
|
+
order: ORDER_REVEAL,
|
|
307
|
+
group: GROUP_REVEAL,
|
|
308
|
+
run: (target, ctx) => {
|
|
309
|
+
void revealInExplorer(ctx.sessionId, target.path).then((outcome) => {
|
|
310
|
+
ctx.notify?.(outcome.ok ? '已在文件浏览器中打开' : '打开失败:' + (outcome.error ?? '未知错误'))
|
|
311
|
+
})
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
]
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 分组 3:添加引用到对话(仅非根)+ 在文件夹中查找…(文件夹专属)。
|
|
319
|
+
* @author ddj 2026年09月22号
|
|
320
|
+
* @returns 分组条目
|
|
321
|
+
*/
|
|
322
|
+
function refGroupItems(): TreeMenuItem[] {
|
|
323
|
+
return [
|
|
324
|
+
{
|
|
325
|
+
id: 'add-to-conversation',
|
|
326
|
+
label: '添加引用到对话',
|
|
327
|
+
order: ORDER_ADD_REF,
|
|
328
|
+
group: GROUP_REF,
|
|
329
|
+
// 排除根目录空白区(path==='' 无意义)、无会话、动作集缺失时隐藏
|
|
330
|
+
visible: (target, ctx) => Boolean(target.path && ctx.sessionId && ctx.addToConversation),
|
|
331
|
+
run: (target, ctx) => {
|
|
332
|
+
const isDir = target.type === 'directory'
|
|
333
|
+
const add = ctx.addToConversation
|
|
334
|
+
if (!add) {
|
|
335
|
+
ctx.notify?.('添加到对话不可用')
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
const okText = isDir ? '已添加文件夹引用' : '已添加文件引用'
|
|
339
|
+
void add.appendReference(ctx.sessionId, target.path, undefined, isDir ? 'folder' : 'file').then((outcome) => {
|
|
340
|
+
ctx.notify?.(statusOfAdd(outcome, okText))
|
|
341
|
+
})
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
id: 'find-in-folder',
|
|
346
|
+
label: '在文件夹中查找…',
|
|
347
|
+
order: ORDER_FIND,
|
|
348
|
+
group: GROUP_REF,
|
|
349
|
+
visible: (target, ctx) => isDirTarget(target) && typeof ctx.searchInFolder === 'function',
|
|
350
|
+
run: (target, ctx) => ctx.searchInFolder?.(target.path),
|
|
351
|
+
},
|
|
352
|
+
]
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* 分组 4:剪切/复制(仅非根)+ 粘贴(文件夹专属,剪贴板空时灰显)。
|
|
357
|
+
* @author ddj 2026年09月22号
|
|
358
|
+
* @returns 分组条目
|
|
359
|
+
*/
|
|
360
|
+
function clipGroupItems(): TreeMenuItem[] {
|
|
361
|
+
return [
|
|
362
|
+
{
|
|
363
|
+
id: 'cut',
|
|
364
|
+
label: '剪切',
|
|
365
|
+
order: ORDER_CUT,
|
|
366
|
+
group: GROUP_CLIP,
|
|
367
|
+
visible: hasPath,
|
|
368
|
+
run: (target, ctx) => {
|
|
369
|
+
setFileClip('cut', target.path)
|
|
370
|
+
ctx.notify?.('已剪切「' + baseNameOf(target.path) + '」')
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
id: 'copy',
|
|
375
|
+
label: '复制',
|
|
376
|
+
order: ORDER_COPY,
|
|
377
|
+
group: GROUP_CLIP,
|
|
378
|
+
visible: hasPath,
|
|
379
|
+
run: (target, ctx) => {
|
|
380
|
+
setFileClip('copy', target.path)
|
|
381
|
+
ctx.notify?.('已复制「' + baseNameOf(target.path) + '」')
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
id: 'paste',
|
|
386
|
+
label: '粘贴',
|
|
387
|
+
order: ORDER_PASTE,
|
|
388
|
+
group: GROUP_CLIP,
|
|
389
|
+
visible: isDirTarget,
|
|
390
|
+
disabled: () => fileClipOf() === null,
|
|
391
|
+
run: (target, ctx) => { void pasteInto(target, ctx) },
|
|
392
|
+
},
|
|
393
|
+
]
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* 分组 5:复制路径 / 复制相对路径(仅非根;相对路径还需 cwd)。
|
|
398
|
+
* @author ddj 2026年09月22号
|
|
399
|
+
* @returns 分组条目
|
|
400
|
+
*/
|
|
401
|
+
function copyPathItems(): TreeMenuItem[] {
|
|
402
|
+
return [
|
|
403
|
+
{
|
|
404
|
+
id: 'copy-path',
|
|
405
|
+
label: '复制路径',
|
|
406
|
+
order: ORDER_COPY_PATH,
|
|
407
|
+
group: GROUP_COPY_PATH,
|
|
408
|
+
visible: hasPath,
|
|
409
|
+
run: (target, ctx) => {
|
|
410
|
+
void copyText(absoluteOf(target.path, ctx.cwd), '已复制路径', (message) => ctx.notify?.(message))
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
id: 'copy-relative-path',
|
|
415
|
+
label: '复制相对路径',
|
|
416
|
+
order: ORDER_COPY_REL,
|
|
417
|
+
group: GROUP_COPY_PATH,
|
|
418
|
+
visible: (target, ctx) => hasPath(target) && hasCwd(ctx),
|
|
419
|
+
run: (target, ctx) => {
|
|
420
|
+
void copyText(relativeOf(target.path, ctx.cwd), '已复制相对路径', (message) => ctx.notify?.(message))
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
]
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* 分组 6:重命名…(仅非根)+ 删除/永久删除(仅非根;danger,文件与文件夹文案不同)。
|
|
428
|
+
* @author ddj 2026年09月22号
|
|
429
|
+
* @returns 分组条目
|
|
430
|
+
*/
|
|
431
|
+
function editGroupItems(): TreeMenuItem[] {
|
|
432
|
+
return [
|
|
433
|
+
{
|
|
434
|
+
id: 'rename',
|
|
435
|
+
label: '重命名…',
|
|
436
|
+
order: ORDER_RENAME,
|
|
437
|
+
group: GROUP_EDIT,
|
|
438
|
+
visible: hasPath,
|
|
439
|
+
run: (target, ctx) => { void renameEntry(target, ctx) },
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
id: 'delete',
|
|
443
|
+
// 文案对齐参考图:文件 = 「删除」,文件夹 = 「永久删除」(行为均为确认后的磁盘删除)
|
|
444
|
+
label: (target) => (isDirTarget(target) ? '永久删除' : '删除'),
|
|
445
|
+
order: ORDER_DELETE,
|
|
446
|
+
group: GROUP_EDIT,
|
|
447
|
+
danger: true,
|
|
448
|
+
visible: hasPath,
|
|
449
|
+
run: (target, ctx) => { void deleteEntry(target, ctx) },
|
|
450
|
+
},
|
|
451
|
+
]
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// --endregion
|
|
455
|
+
|
|
456
|
+
// --region SVN 组(动作清单来自 shared/svnActions.ts)
|
|
457
|
+
|
|
21
458
|
/** 目标路径对应的变更条目(不在清单里返回 undefined)。 */
|
|
22
459
|
function svnEntryOf(target: TreeMenuTarget, ctx: SidebarCtx): SvnChangeEntry | undefined {
|
|
23
460
|
if (!target.path) return undefined
|
|
@@ -79,62 +516,48 @@ function treeRunCtx(target: TreeMenuTarget, ctx: SidebarCtx): SvnActionRunCtx {
|
|
|
79
516
|
*
|
|
80
517
|
* 收敛收益:此前每个动作在这里手写一项、页签菜单再手写一遍、命令栏第三遍,
|
|
81
518
|
* 显隐规则三处漂移(P2 的「加入/还原」状态矩阵就重复过两份)。现在只做映射。
|
|
82
|
-
*
|
|
519
|
+
* 共享元数据的 separator 意为「新分段」:映射为分组号 +1,分隔线由 buildTreeMenu 统一出。
|
|
520
|
+
* @author ddj 2026年09月16号 / 2026年09月22号
|
|
83
521
|
* @returns SVN 组条目
|
|
84
522
|
*/
|
|
85
523
|
function svnMenuItems(): TreeMenuItem[] {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
524
|
+
const items: TreeMenuItem[] = []
|
|
525
|
+
let group = SVN_GROUP_BASE
|
|
526
|
+
for (const action of svnActionsFor('tree')) {
|
|
527
|
+
if (action.separator === true) group += 1
|
|
528
|
+
items.push({
|
|
529
|
+
id: 'svn-' + action.id,
|
|
530
|
+
label: action.label,
|
|
531
|
+
order: SVN_ORDER_BASE + action.order,
|
|
532
|
+
group,
|
|
533
|
+
danger: action.danger === true,
|
|
534
|
+
visible: (target: TreeMenuTarget, ctx: SidebarCtx) => {
|
|
535
|
+
const context = svnContextOf(target, ctx)
|
|
536
|
+
return context !== null && svnActionOn(action, context)
|
|
537
|
+
},
|
|
538
|
+
run: (target: TreeMenuTarget, ctx: SidebarCtx) => {
|
|
539
|
+
runSvnAction(action, treeRunCtx(target, ctx))
|
|
540
|
+
},
|
|
541
|
+
})
|
|
542
|
+
}
|
|
543
|
+
return items
|
|
100
544
|
}
|
|
101
545
|
|
|
546
|
+
// --endregion
|
|
547
|
+
|
|
102
548
|
/**
|
|
103
|
-
*
|
|
104
|
-
* @author ddj 2026年08月27号
|
|
549
|
+
* 构造内置右键菜单项列表(后续内置项按分组追加)。
|
|
550
|
+
* @author ddj 2026年08月27号 / 2026年09月22号
|
|
105
551
|
* @returns 内置菜单项数组
|
|
106
552
|
*/
|
|
107
553
|
export function createDefaultFileMenuItems(): TreeMenuItem[] {
|
|
108
554
|
return [
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
ctx.notify?.(outcome.ok ? '已在文件浏览器中打开' : '打开失败:' + (outcome.error ?? '未知错误'))
|
|
116
|
-
})
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
id: 'add-to-conversation',
|
|
121
|
-
label: '添加引用到对话',
|
|
122
|
-
order: 1,
|
|
123
|
-
// 排除根目录空白区(path==='' 无意义)、无会话、动作集缺失时隐藏
|
|
124
|
-
visible: (target, ctx) => Boolean(target.path && ctx.sessionId && ctx.addToConversation),
|
|
125
|
-
run: (target, ctx) => {
|
|
126
|
-
const isDir = target.type === 'directory'
|
|
127
|
-
const add = ctx.addToConversation
|
|
128
|
-
if (!add) {
|
|
129
|
-
ctx.notify?.('添加到对话不可用')
|
|
130
|
-
return
|
|
131
|
-
}
|
|
132
|
-
const okText = isDir ? '已添加文件夹引用' : '已添加文件引用'
|
|
133
|
-
void add.appendReference(ctx.sessionId, target.path, undefined, isDir ? 'folder' : 'file').then((outcome) => {
|
|
134
|
-
ctx.notify?.(statusOfAdd(outcome, okText))
|
|
135
|
-
})
|
|
136
|
-
},
|
|
137
|
-
},
|
|
555
|
+
...openGroupItems(),
|
|
556
|
+
...revealGroupItems(),
|
|
557
|
+
...refGroupItems(),
|
|
558
|
+
...clipGroupItems(),
|
|
559
|
+
...copyPathItems(),
|
|
560
|
+
...editGroupItems(),
|
|
138
561
|
...svnMenuItems(),
|
|
139
562
|
]
|
|
140
563
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* 作者 ddj 2026-08-26 / 2026-08-27 / 2026-08-31 / 2026-09-10 / 2026-09-16
|
|
11
11
|
*/
|
|
12
12
|
import React from 'react'
|
|
13
|
-
import { FileTypeIcon, IconFolderClose16, IconFolderOpen16, IconRefreshOutline16, classifyFileType } from '
|
|
13
|
+
import { FileTypeIcon, IconFolderClose16, IconFolderOpen16, IconRefreshOutline16, classifyFileType } from '../../ui/icons.js'
|
|
14
14
|
import { rpc } from '../../rpc.js'
|
|
15
15
|
import { ContextMenu } from '../../ui/ContextMenu.js'
|
|
16
16
|
import { buildTreeMenu } from '../contextMenu.js'
|