dsh-vscode-mode 0.6.0 → 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.
- package/README.md +19 -2
- package/lib/client.js +8302 -7458
- package/lib/client.js.map +1 -1
- package/lib/index.js +373 -0
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/copyText.ts +28 -0
- package/src/client/fileClipboard.ts +46 -0
- package/src/client/index.ts +3 -3
- package/src/client/openWithDialog.ts +66 -0
- package/src/client/searchSeed.ts +36 -0
- package/src/client/sidebar/contextMenu.ts +51 -10
- package/src/client/sidebar/menuItems.ts +473 -50
- package/src/client/sidebar/panels/SearchPanel.ts +24 -3
- package/src/client/sidebar/types.ts +6 -0
- package/src/client/tabActions.ts +22 -10
- package/src/client/ui/EditorView.ts +81 -7
- package/src/client/ui/OfficialSideTab.ts +1 -0
- 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/rpc.ts +198 -2
- package/src/shared/fsNames.ts +115 -0
- package/src/shared/keybindings.ts +2 -0
- package/src/shared/rpc.ts +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ SVN 集成(侧栏变更面板/日志弹窗/并排差异)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 剪贴板写入 + 反馈(页签菜单与文件树菜单共用)。
|
|
3
|
+
* 提取自 EditorView 的 copyText 闭包:无剪贴板 API 或写入失败时降级提示、不抛错。
|
|
4
|
+
* 作者 ddj 2026年09月22号
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 写入文本到系统剪贴板并给出反馈。
|
|
9
|
+
* @author ddj 2026年09月22号
|
|
10
|
+
* @param value 待复制文本
|
|
11
|
+
* @param okText 成功反馈文案
|
|
12
|
+
* @param notify 反馈通道(缺省不反馈)
|
|
13
|
+
* @returns 是否复制成功
|
|
14
|
+
*/
|
|
15
|
+
export async function copyText(value: string, okText: string, notify?: (message: string) => void): Promise<boolean> {
|
|
16
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard?.writeText) {
|
|
17
|
+
notify?.('剪贴板不可用')
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
await navigator.clipboard.writeText(value)
|
|
22
|
+
notify?.(okText)
|
|
23
|
+
return true
|
|
24
|
+
} catch {
|
|
25
|
+
notify?.('剪贴板写入失败')
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 文件剪贴板(剪切/复制/粘贴的应用内单槽)。
|
|
3
|
+
*
|
|
4
|
+
* 浏览器拿不到系统文件剪贴板,按 VS Code 语义做应用内文件剪贴板:
|
|
5
|
+
* 「剪切/复制」只登记源路径,「粘贴」时才走 edrv.fsMove / edrv.fsCopy 真正落盘。
|
|
6
|
+
* 模块级单槽(同 searchSeed 一次性槽模式),不依赖 React/DOM,可 node 单测。
|
|
7
|
+
* 作者 ddj 2026年09月22号
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** 文件剪贴板内容。 */
|
|
11
|
+
export interface FileClip {
|
|
12
|
+
/** copy = 复制;cut = 剪切(粘贴后源被移走)。 */
|
|
13
|
+
mode: 'copy' | 'cut'
|
|
14
|
+
/** 源路径(工作区相对)。 */
|
|
15
|
+
path: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 单槽剪贴板(null = 空)。 */
|
|
19
|
+
let clip: FileClip | null = null
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 写入文件剪贴板(重复以最后一次为准)。
|
|
23
|
+
* @author ddj 2026年09月22号
|
|
24
|
+
* @param mode copy = 复制;cut = 剪切
|
|
25
|
+
* @param path 源路径(工作区相对)
|
|
26
|
+
*/
|
|
27
|
+
export function setFileClip(mode: FileClip['mode'], path: string): void {
|
|
28
|
+
clip = { mode, path }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 读取当前剪贴板(不清空;粘贴执行与「粘贴」灰显判定共用)。
|
|
33
|
+
* @author ddj 2026年09月22号
|
|
34
|
+
* @returns 剪贴板内容;空槽返回 null
|
|
35
|
+
*/
|
|
36
|
+
export function fileClipOf(): FileClip | null {
|
|
37
|
+
return clip
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 清空文件剪贴板(剪切粘贴成功后;测试隔离用)。
|
|
42
|
+
* @author ddj 2026年09月22号
|
|
43
|
+
*/
|
|
44
|
+
export function clearFileClip(): void {
|
|
45
|
+
clip = null
|
|
46
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -224,7 +224,7 @@ export function apply(ctx: any): void {
|
|
|
224
224
|
return sidebar ? registry.register(sidebar) : undefined
|
|
225
225
|
}, 'vscode-mode: sidebar file opener')
|
|
226
226
|
// 官方侧边栏正文组件装配(页类型正文;兼作 file 认领转发失败时的兜底正文)
|
|
227
|
-
const officialRenderTab = (props: Record<string, unknown>) => React.createElement(OfficialSideTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, sessions }))
|
|
227
|
+
const officialRenderTab = (props: Record<string, unknown>) => React.createElement(OfficialSideTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, fileOpeners: registry, sessions }))
|
|
228
228
|
/** 官方 file 地址认领同步:自动/VSCodeMode 档认领(转发进单一编辑器页签),其余交官方查看器。 */
|
|
229
229
|
const syncFileClaim = (): void => {
|
|
230
230
|
const official = officialService
|
|
@@ -334,7 +334,7 @@ export function apply(ctx: any): void {
|
|
|
334
334
|
order: 5,
|
|
335
335
|
label: '文件编辑',
|
|
336
336
|
inject: (sessionId: string) => ({ sessionId }),
|
|
337
|
-
}, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { layout: 'tab', sideHint: SIDEBAR_INSTALL_CMD, schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, sessions })))
|
|
337
|
+
}, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { layout: 'tab', sideHint: SIDEBAR_INSTALL_CMD, schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, fileOpeners: registry, sessions })))
|
|
338
338
|
}
|
|
339
339
|
|
|
340
340
|
// 侧边栏三形态互斥切换:官方(DSH 0.1.5+)> better-sidebar(归档,仅旧版回退)> 中央页签。
|
|
@@ -385,7 +385,7 @@ export function apply(ctx: any): void {
|
|
|
385
385
|
if (legacyDisposer !== null) { legacyDisposer(); legacyDisposer = null }
|
|
386
386
|
sideDisposer = ctx.effect(() => installSideEditor({
|
|
387
387
|
service: sideService as NonNullable<typeof sideService>,
|
|
388
|
-
renderTab: (props: Record<string, unknown>) => React.createElement(SideEditorTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, sessions })),
|
|
388
|
+
renderTab: (props: Record<string, unknown>) => React.createElement(SideEditorTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, fileOpeners: registry, sessions })),
|
|
389
389
|
activeSession: () => {
|
|
390
390
|
const scope = readSessionScope(ctx)
|
|
391
391
|
if (!scope.sessionId) return undefined
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* dsh-vscode-mode client — 「打开方式…」选择弹窗(资源管理器右键、文件目标专属)。
|
|
4
|
+
* 列出 fileOpeners 注册表中当前可用的打开器(本插件/官方侧栏/系统等),
|
|
5
|
+
* 选中即调用该打开器 open();复用 ModalShell 视觉,Esc/遮罩/取消 = 不打开。
|
|
6
|
+
* 作者 ddj 2026年09月22号
|
|
7
|
+
*/
|
|
8
|
+
import React from 'react'
|
|
9
|
+
import { createRoot } from 'react-dom/client'
|
|
10
|
+
import { availableOpeners, isAvailable } from './fileOpeners.js'
|
|
11
|
+
import { ModalShell } from './ui/ModalShell.js'
|
|
12
|
+
|
|
13
|
+
/** 打开器列表行样式(列表为一次性选择器,不进 editor.css 的常驻样式表)。 */
|
|
14
|
+
const ITEM_STYLE = {
|
|
15
|
+
display: 'flex', flexDirection: 'column', gap: 2, width: '100%', textAlign: 'left',
|
|
16
|
+
padding: '8px 10px', marginBottom: 6, borderRadius: 6, cursor: 'pointer',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 打开器选择卡片(ModalShell 内容)。 */
|
|
20
|
+
function OpenWithCard({ openers, onPick, onCancel }) {
|
|
21
|
+
return React.createElement(React.Fragment, null,
|
|
22
|
+
React.createElement('h3', null, '打开方式…'),
|
|
23
|
+
openers.map((opener) => React.createElement('button', {
|
|
24
|
+
key: opener.id,
|
|
25
|
+
style: ITEM_STYLE,
|
|
26
|
+
title: opener.description || opener.label,
|
|
27
|
+
onClick: () => onPick(opener),
|
|
28
|
+
},
|
|
29
|
+
React.createElement('span', null, opener.label),
|
|
30
|
+
opener.description ? React.createElement('span', { style: { fontSize: 12, opacity: 0.7 } }, opener.description) : null)),
|
|
31
|
+
React.createElement('div', { className: 'vsm-mcp-dialog-actions' },
|
|
32
|
+
React.createElement('button', { onClick: onCancel }, '取消')))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 弹出打开器选择弹窗;选中后立即以该打开器打开目标文件。
|
|
37
|
+
* @author ddj 2026年09月22号
|
|
38
|
+
* @param registry 文件打开器注册表
|
|
39
|
+
* @param context 打开上下文(会话/工作区)
|
|
40
|
+
* @param path 目标文件(工作区相对)
|
|
41
|
+
* @param onDone 结果回调(opened = 实际使用的打开器;null = 取消/未打开),反馈文案由调用方接线
|
|
42
|
+
*/
|
|
43
|
+
export function openWithDialog(registry, context, path, onDone) {
|
|
44
|
+
const openers = availableOpeners(registry)
|
|
45
|
+
if (!openers.length) {
|
|
46
|
+
onDone?.(null)
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
const host = document.createElement('div')
|
|
50
|
+
document.body.appendChild(host)
|
|
51
|
+
const root = createRoot(host)
|
|
52
|
+
const close = () => {
|
|
53
|
+
root.unmount()
|
|
54
|
+
host.remove()
|
|
55
|
+
}
|
|
56
|
+
const pick = (opener) => {
|
|
57
|
+
close()
|
|
58
|
+
if (!isAvailable(opener)) {
|
|
59
|
+
onDone?.(null)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
Promise.resolve(opener.open(path, context)).then(() => onDone?.(opener)).catch(() => onDone?.(null))
|
|
63
|
+
}
|
|
64
|
+
root.render(React.createElement(ModalShell, { onClose: () => { close(); onDone?.(null) } },
|
|
65
|
+
React.createElement(OpenWithCard, { openers, onPick: pick, onCancel: () => { close(); onDone?.(null) } })))
|
|
66
|
+
}
|
package/src/client/searchSeed.ts
CHANGED
|
@@ -62,3 +62,39 @@ export function takeSearchSeed(): string {
|
|
|
62
62
|
export function clearSearchSeed(): void {
|
|
63
63
|
pendingSeed = null
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
// --region 目录过滤种子(「在文件夹中查找…」:SearchPanel 消费后置 include 过滤)
|
|
67
|
+
|
|
68
|
+
/** 待消费的目录过滤种子('' = 无)。 */
|
|
69
|
+
let pendingScope: string | null = null
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 写入待消费的目录过滤种子(空目录等价于清除)。
|
|
73
|
+
* @author ddj 2026年09月22号
|
|
74
|
+
* @param dir 目标目录(工作区相对;空 = 根 = 不限定目录)
|
|
75
|
+
*/
|
|
76
|
+
export function setSearchScope(dir: unknown): void {
|
|
77
|
+
const text = String(dir ?? '').trim().replace(/\\/g, '/').replace(/\/+$/, '')
|
|
78
|
+
pendingScope = text ? text : null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 取走待消费的目录过滤种子(取后即清空,与 query 种子同一一次性消费语义)。
|
|
83
|
+
* @author ddj 2026年09月22号
|
|
84
|
+
* @returns 目录相对路径;无待消费种子返回空串
|
|
85
|
+
*/
|
|
86
|
+
export function takeSearchScope(): string {
|
|
87
|
+
const scope = pendingScope
|
|
88
|
+
pendingScope = null
|
|
89
|
+
return scope ?? ''
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 清空待消费的目录过滤种子(测试隔离用)。
|
|
94
|
+
* @author ddj 2026年09月22号
|
|
95
|
+
*/
|
|
96
|
+
export function clearSearchScope(): void {
|
|
97
|
+
pendingScope = null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --endregion
|
|
@@ -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
|
}
|