dsh-vscode-mode 0.5.3 → 0.7.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.
Files changed (77) hide show
  1. package/README.md +72 -2
  2. package/lib/client.js +12082 -6420
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +2750 -147
  5. package/lib/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/client/copyText.ts +28 -0
  8. package/src/client/dap/BpWidget.ts +262 -0
  9. package/src/client/dap/bpMenu.ts +91 -0
  10. package/src/client/dap/bpRowMenu.ts +53 -0
  11. package/src/client/dap/breakpoints.ts +222 -0
  12. package/src/client/dap/decorate.ts +128 -0
  13. package/src/client/dap/hintLine.ts +29 -0
  14. package/src/client/dap/hover.ts +166 -0
  15. package/src/client/dap/hoverMode.ts +159 -0
  16. package/src/client/dap/hoverTree.ts +722 -0
  17. package/src/client/dap/launchInsert.ts +229 -0
  18. package/src/client/dap/launchSnippetProvider.ts +206 -0
  19. package/src/client/dap/modelPath.ts +23 -0
  20. package/src/client/dap/panelSplit.ts +105 -0
  21. package/src/client/dap/pidPick.ts +24 -0
  22. package/src/client/dap/store.ts +571 -0
  23. package/src/client/dap/toolbarDrag.ts +51 -0
  24. package/src/client/dap/trace.ts +22 -0
  25. package/src/client/dap/variableTree.ts +62 -0
  26. package/src/client/fileClipboard.ts +46 -0
  27. package/src/client/index.ts +12 -3
  28. package/src/client/monaco/lsp/index.ts +15 -0
  29. package/src/client/monaco/lsp/providers.ts +2 -0
  30. package/src/client/monaco/theme.ts +15 -0
  31. package/src/client/openWithDialog.ts +66 -0
  32. package/src/client/searchSeed.ts +36 -0
  33. package/src/client/sidebar/contextMenu.ts +51 -10
  34. package/src/client/sidebar/menuItems.ts +473 -50
  35. package/src/client/sidebar/panels/DebugPanel.ts +613 -0
  36. package/src/client/sidebar/panels/SearchPanel.ts +24 -3
  37. package/src/client/sidebar/panels/SvnPanel.ts +167 -23
  38. package/src/client/sidebar/panels/index.ts +19 -0
  39. package/src/client/sidebar/types.ts +8 -0
  40. package/src/client/styles/editor.css +176 -0
  41. package/src/client/svnActions.ts +18 -1
  42. package/src/client/svnLog.ts +3 -3
  43. package/src/client/svnStatus.ts +140 -2
  44. package/src/client/tabActions.ts +22 -10
  45. package/src/client/ui/EditorView.ts +625 -23
  46. package/src/client/ui/OfficialSideTab.ts +1 -0
  47. package/src/client/ui/PromptDialog.ts +63 -0
  48. package/src/client/ui/SideBySideDiff.tsx +214 -29
  49. package/src/client/ui/SideEditorTab.ts +1 -0
  50. package/src/client/ui/SvnDiffPanel.ts +21 -8
  51. package/src/client/ui/SvnLogDialog.ts +44 -2
  52. package/src/client/ui/SvnPatchDialog.ts +63 -0
  53. package/src/client/ui/SvnSumDialog.ts +77 -0
  54. package/src/client/ui/commandCatalog.ts +63 -0
  55. package/src/client/ui/svnExport.ts +71 -0
  56. package/src/dap/configSnippets.ts +269 -0
  57. package/src/dap/discovery.ts +198 -0
  58. package/src/dap/launchConfig.ts +127 -0
  59. package/src/dap/manager.ts +588 -0
  60. package/src/dap/processList.ts +76 -0
  61. package/src/dap/protocol.ts +80 -0
  62. package/src/dap/provider.ts +49 -0
  63. package/src/dap/rpc.ts +192 -0
  64. package/src/dap/sourcePath.ts +82 -0
  65. package/src/index.ts +7 -3
  66. package/src/lsp/providers.ts +3 -2
  67. package/src/reveal.ts +31 -20
  68. package/src/rpc.ts +209 -5
  69. package/src/search/ripgrep.ts +127 -11
  70. package/src/shared/dap.ts +262 -0
  71. package/src/shared/fsNames.ts +115 -0
  72. package/src/shared/keybindings.ts +9 -0
  73. package/src/shared/rpc.ts +52 -2
  74. package/src/shared/svn.ts +147 -0
  75. package/src/shared/svnActions.ts +20 -0
  76. package/src/svn.ts +320 -11
  77. package/src/workspace.ts +0 -75
@@ -1,23 +1,460 @@
1
1
  /**
2
2
  * dsh-vscode-mode client — 文件管理右键菜单内置项。
3
- * 内置项:「在文件浏览器中打开」(文件→OS Explorer 定位选中、目录→打开目录);
4
- * 「添加引用到对话」(文件/文件夹引用注入当前会话对话输入框);
5
- * 「SVN 组」——动作清单与显隐规则来自 shared/svnActions.ts(三入口共用一份,不再各写一遍)。
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
- * @author ddj 2026年09月16号
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
- return svnActionsFor('tree').map((action) => ({
87
- id: 'svn-' + action.id,
88
- label: action.label,
89
- order: action.order,
90
- danger: action.danger === true,
91
- separator: action.separator === true,
92
- visible: (target: TreeMenuTarget, ctx: SidebarCtx) => {
93
- const context = svnContextOf(target, ctx)
94
- return context !== null && svnActionOn(action, context)
95
- },
96
- run: (target: TreeMenuTarget, ctx: SidebarCtx) => {
97
- runSvnAction(action, treeRunCtx(target, ctx))
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
- id: 'reveal-in-explorer',
111
- label: '在文件浏览器中打开',
112
- order: 0,
113
- run: (target, ctx) => {
114
- void revealInExplorer(ctx.sessionId, target.path).then((outcome) => {
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
  }