dsh-vscode-mode 0.1.63 → 0.3.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 +20 -0
- package/lib/client.js +1737 -98
- package/lib/client.js.map +1 -1
- package/lib/index.js +482 -20
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client/commandBridge.ts +85 -0
- package/src/client/commandGlobals.ts +13 -0
- package/src/client/commandPaletteStore.ts +212 -0
- package/src/client/commandRegistry.ts +150 -0
- package/src/client/commandSearch.ts +89 -0
- package/src/client/editorModelState.ts +30 -0
- package/src/client/index.ts +32 -0
- package/src/client/keybindings.ts +71 -17
- package/src/client/monaco/loader.ts +24 -0
- package/src/client/snippets/provider.ts +172 -0
- package/src/client/styles/editor.css +51 -0
- package/src/client/ui/CommandPalette.ts +194 -0
- package/src/client/ui/ConversationDiffDock.ts +17 -9
- package/src/client/ui/EditorView.ts +185 -4
- package/src/client/ui/KeybindingsPanel.ts +1 -1
- package/src/client/ui/QuickOpen.ts +11 -3
- package/src/client/ui/SnippetsPicker.ts +296 -0
- package/src/client/ui/commandCatalog.ts +204 -0
- package/src/rpc.ts +78 -0
- package/src/shared/keybindings.ts +9 -1
- package/src/shared/rpc.ts +11 -0
- package/src/shared/snippets.ts +156 -0
- package/src/snippets.ts +427 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 编辑器指令目录(纯元数据)。
|
|
3
|
+
* 「一条命令 = 一条注册数据」:命令栏、快捷键设置页、快捷键监听与 Monaco 右键菜单
|
|
4
|
+
* 全部从本表读取;新增一条能力只需在 EDITOR_COMMANDS 追加一项(可选 keybinding)。
|
|
5
|
+
* run 只派发 `edrv.command.*` 窗口事件,不直接触碰 React/Monaco(`edrv.command.` 前缀
|
|
6
|
+
* 与既有 `edrv:` 刷新/主题事件分属不同命名空间,互不干扰)。
|
|
7
|
+
* 作者 ddj 2026年09月10号
|
|
8
|
+
*/
|
|
9
|
+
import { hasEditorModel } from '../editorModelState.js'
|
|
10
|
+
|
|
11
|
+
/** 一条编辑器指令(展示 + 执行 + 可用性)。 */
|
|
12
|
+
export interface CommandDef {
|
|
13
|
+
/** 稳定命令 id(`edrv.` 前缀;第三方注册请避让该前缀)。 */
|
|
14
|
+
id: string
|
|
15
|
+
/** 命令栏与设置页展示名。 */
|
|
16
|
+
label: string
|
|
17
|
+
/** 命令栏分组名。 */
|
|
18
|
+
category: string
|
|
19
|
+
/** 组内排序(小者优先)。 */
|
|
20
|
+
order?: number
|
|
21
|
+
/** 默认键位弦(可选;可含 `|` 多候选)。声明后由快捷键设置页展示与录制。 */
|
|
22
|
+
keybinding?: string
|
|
23
|
+
/** 运行体:派发窗口事件或直接执行。 */
|
|
24
|
+
run: () => void
|
|
25
|
+
/** 可用性判定(缺省视为始终可用;返回 false 时命令栏隐藏且 run 被拒)。 */
|
|
26
|
+
available?: () => boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 派发编辑器命令事件(`edrv.command.<action>`)。
|
|
31
|
+
* 无 window 的运行环境(纯 Node 单测)静默跳过,命令本身不因此失败。
|
|
32
|
+
* @author ddj 2026年09月10号
|
|
33
|
+
* @param action 动作名(不含前缀)
|
|
34
|
+
*/
|
|
35
|
+
function emit(action: string): void {
|
|
36
|
+
if (typeof window === 'undefined') return
|
|
37
|
+
window.dispatchEvent(new CustomEvent('edrv.command.' + action))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 编辑器命令事件名前缀(EditorView / QuickOpen / 第三方据此监听)。 */
|
|
41
|
+
export const COMMAND_EVENT_PREFIX = 'edrv.command.'
|
|
42
|
+
|
|
43
|
+
/** 需要活动编辑器模型才可用的命令(命令栏隐藏并拒绝执行)。 */
|
|
44
|
+
function needsModel(): boolean {
|
|
45
|
+
return hasEditorModel()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 面板区显隐切换始终可用(未挂载编辑器时 run 为空操作)。 */
|
|
49
|
+
function alwaysAvailable(): boolean {
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 光标整行下移(编辑行导航)。
|
|
55
|
+
* @author ddj 2026年09月10号
|
|
56
|
+
* @returns 命令定义
|
|
57
|
+
*/
|
|
58
|
+
function nextEditorRowDef(): CommandDef {
|
|
59
|
+
return {
|
|
60
|
+
id: 'edrv.nextEditorRow', label: '下一编辑行(光标整行下移)', category: '导航', order: 50,
|
|
61
|
+
keybinding: 'Ctrl+Alt+ArrowDown', available: needsModel,
|
|
62
|
+
run: () => emit('nextEditorRow'),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 光标整行上移(编辑行导航)。
|
|
68
|
+
* @author ddj 2026年09月10号
|
|
69
|
+
* @returns 命令定义
|
|
70
|
+
*/
|
|
71
|
+
function prevEditorRowDef(): CommandDef {
|
|
72
|
+
return {
|
|
73
|
+
id: 'edrv.prevEditorRow', label: '上一编辑行(光标整行上移)', category: '导航', order: 60,
|
|
74
|
+
keybinding: 'Ctrl+Alt+ArrowUp', available: needsModel,
|
|
75
|
+
run: () => emit('prevEditorRow'),
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 添加选中内容为引用(把当前选区追加进对话输入框;无选区则状态栏提示)。
|
|
81
|
+
* 有活动编辑器模型才可用,保证对话框内按 Ctrl+U 不被本命令吞掉。
|
|
82
|
+
* @author ddj 2026年09月10号
|
|
83
|
+
* @returns 命令定义
|
|
84
|
+
*/
|
|
85
|
+
function addSelectionRefDef(): CommandDef {
|
|
86
|
+
return {
|
|
87
|
+
id: 'edrv.addSelectionRef', label: '添加选中内容为引用', category: '编辑', order: 10,
|
|
88
|
+
keybinding: 'Ctrl+U', available: needsModel,
|
|
89
|
+
run: () => emit('addSelectionRef'),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 编辑器内置指令目录(顺序 = 快捷键设置页展示顺序)。
|
|
95
|
+
* 前置 8 条的键位由 EditorView / QuickOpen 自行 capture 监听(历史实现),
|
|
96
|
+
* 故不进 BRIDGE_COMMANDS,避免同一按键双执行。
|
|
97
|
+
* @author ddj 2026年09月10号
|
|
98
|
+
*/
|
|
99
|
+
export const EDITOR_COMMANDS: readonly CommandDef[] = [
|
|
100
|
+
{
|
|
101
|
+
id: 'edrv.save', label: '保存文件', category: '文件', order: 10,
|
|
102
|
+
keybinding: 'Ctrl+S', available: needsModel,
|
|
103
|
+
run: () => emit('save'),
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'edrv.quickOpen', label: '快速打开文件', category: '文件', order: 20,
|
|
107
|
+
keybinding: 'Ctrl+P', available: alwaysAvailable,
|
|
108
|
+
run: () => emit('quickOpen'),
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'edrv.toggleSidebar', label: '切换侧边栏', category: '视图', order: 10,
|
|
112
|
+
keybinding: 'Ctrl+B', available: alwaysAvailable,
|
|
113
|
+
run: () => emit('toggleSidebar'),
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'edrv.searchInFiles', label: '在工作区中搜索', category: '视图', order: 20,
|
|
117
|
+
keybinding: 'Ctrl+Shift+F', available: alwaysAvailable,
|
|
118
|
+
run: () => emit('searchInFiles'),
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'edrv.navigateBack', label: '后退(导航历史)', category: '导航', order: 10,
|
|
122
|
+
keybinding: 'Alt+ArrowLeft|Ctrl+Alt+-', available: needsModel,
|
|
123
|
+
run: () => emit('navigateBack'),
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'edrv.navigateForward', label: '前进(导航历史)', category: '导航', order: 20,
|
|
127
|
+
keybinding: 'Alt+ArrowRight|Ctrl+Shift+-', available: needsModel,
|
|
128
|
+
run: () => emit('navigateForward'),
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'edrv.nextTab', label: '下一个页签', category: '导航', order: 30,
|
|
132
|
+
keybinding: 'Ctrl+Alt+ArrowRight|Ctrl+PageDown', available: needsModel,
|
|
133
|
+
run: () => emit('nextTab'),
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: 'edrv.prevTab', label: '上一个页签', category: '导航', order: 40,
|
|
137
|
+
keybinding: 'Ctrl+Alt+ArrowLeft|Ctrl+PageUp', available: needsModel,
|
|
138
|
+
run: () => emit('prevTab'),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: 'edrv.goToDefinition', label: '转到定义', category: '语言智能', order: 10,
|
|
142
|
+
available: needsModel,
|
|
143
|
+
run: () => emit('goToDefinition'),
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'edrv.findReferences', label: '查找所有引用', category: '语言智能', order: 20,
|
|
147
|
+
available: needsModel,
|
|
148
|
+
run: () => emit('findReferences'),
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: 'edrv.triggerAi', label: '触发 AI 内联补全', category: '语言智能', order: 30,
|
|
152
|
+
available: needsModel,
|
|
153
|
+
run: () => emit('triggerAi'),
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: 'edrv.openInExplorer', label: '在文件浏览器中打开', category: '文件', order: 30,
|
|
157
|
+
available: needsModel,
|
|
158
|
+
run: () => emit('openInExplorer'),
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'edrv.configureSnippets', label: '代码片段:配置代码片段', category: '代码片段', order: 10,
|
|
162
|
+
available: alwaysAvailable,
|
|
163
|
+
run: () => emit('configureSnippets'),
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 'edrv.insertSnippet', label: '插入代码片段', category: '代码片段', order: 20,
|
|
167
|
+
available: needsModel,
|
|
168
|
+
run: () => emit('insertSnippet'),
|
|
169
|
+
},
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 桥接派发指令(无原生监听,键位由 commandBridge 统一 capture 处理)。
|
|
174
|
+
* 新增「只填目录、不写监听」的编辑器指令一律放这里:键位、命令栏、设置页自动可用。
|
|
175
|
+
* @author ddj 2026年09月10号
|
|
176
|
+
*/
|
|
177
|
+
export const BRIDGE_COMMANDS: readonly CommandDef[] = [
|
|
178
|
+
nextEditorRowDef(),
|
|
179
|
+
prevEditorRowDef(),
|
|
180
|
+
addSelectionRefDef(),
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 命令栏自身的命令(避免循环依赖,由 commandBridge 注入 run 后注册)。
|
|
185
|
+
* @author ddj 2026年09月10号
|
|
186
|
+
* @param run 打开命令栏
|
|
187
|
+
* @returns 命令定义
|
|
188
|
+
*/
|
|
189
|
+
export function showCommandsDef(run: () => void): CommandDef {
|
|
190
|
+
return {
|
|
191
|
+
id: 'edrv.showCommands', label: '显示所有命令', category: '视图', order: 1,
|
|
192
|
+
keybinding: 'Ctrl+Shift+P|F1', available: alwaysAvailable, run,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 需要全局键位派发的指令(桥接类 + 命令栏自身;不含 EditorView/QuickOpen 原生监听的那些)。
|
|
198
|
+
* @author ddj 2026年09月10号
|
|
199
|
+
* @param showCommands 命令栏命令定义
|
|
200
|
+
* @returns 派发集合
|
|
201
|
+
*/
|
|
202
|
+
export function dispatchedCommands(showCommands: CommandDef): readonly CommandDef[] {
|
|
203
|
+
return [...BRIDGE_COMMANDS, showCommands]
|
|
204
|
+
}
|
package/src/rpc.ts
CHANGED
|
@@ -31,6 +31,7 @@ import type { ContentSearcher } from './search/content.js'
|
|
|
31
31
|
import { newContentSearcher } from './search/content.js'
|
|
32
32
|
import { restoreFile, revertCall, revertHunk } from './revert.js'
|
|
33
33
|
import { rulesList, rulesRead, rulesRemove, rulesSave, rulesToggle } from './rules.js'
|
|
34
|
+
import { isSnippetFilePath, snippetsEntries, snippetsList, snippetsRead, snippetsRemove, snippetsSave } from './snippets.js'
|
|
34
35
|
import { listMcp, refreshMcp, removeMcp, saveMcp, toggleMcp } from './mcp.js'
|
|
35
36
|
import { listProjects, projectRefresh, projectRemove, projectSave, projectToggle } from './mcpProject.js'
|
|
36
37
|
import { normalizeFileOpenTool, FILE_OPEN_DEFAULT, FILE_OPEN_SETTINGS_NS } from './fileOpenSettings.js'
|
|
@@ -86,6 +87,24 @@ async function requireSession(ctx: Ctx, sessionId: string | undefined): Promise<
|
|
|
86
87
|
return { session, cwd }
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
/** 片段文件保存时的沙箱策略:用户显式 GUI 写操作,放开到 danger-full-access(镜像 rules.fullPolicy)。 */
|
|
91
|
+
function snippetPolicy(ctx: Ctx): unknown {
|
|
92
|
+
const svc = ctx.get('sandboxPolicy')
|
|
93
|
+
if (!svc || typeof svc.resolve !== 'function') return undefined
|
|
94
|
+
return svc.resolve({ mode: 'danger-full-access' })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 片段文件路径解析:命中全局片段目录(~/.dsh/snippets)返回归一化绝对路径,否则 null。
|
|
99
|
+
* 全局片段位于工作区之外,edrv.read / edrv.save 需绕开 resolveTarget 的 cwd 语义直读直写。
|
|
100
|
+
* @author ddj 2026年09月10号
|
|
101
|
+
* @param path 客户端请求路径(绝对路径)
|
|
102
|
+
* @returns 归一化后的绝对路径或 null
|
|
103
|
+
*/
|
|
104
|
+
function snippetTargetOf(path: string): string | null {
|
|
105
|
+
return isSnippetFilePath(path) ? path.replace(/\\/g, '/') : null
|
|
106
|
+
}
|
|
107
|
+
|
|
89
108
|
/**
|
|
90
109
|
* 手动保存收尾(edrv.save / edrv.saveBinary 共用):目录树失效 + 该路径 pending
|
|
91
110
|
* 文本差异记录标记 superseded 并归档(旧文本 diff 不得再应用到新内容上)。
|
|
@@ -258,6 +277,16 @@ export function buildHandlers(
|
|
|
258
277
|
return { ok: true, results }
|
|
259
278
|
},
|
|
260
279
|
'edrv.read': async (args) => {
|
|
280
|
+
// 全局片段文件(~/.dsh/snippets/*.code-snippets)在工作区之外:直读,不走会话 cwd 解析
|
|
281
|
+
const snippetTarget = snippetTargetOf(args.path)
|
|
282
|
+
if (snippetTarget && args.encoding !== 'base64') {
|
|
283
|
+
try {
|
|
284
|
+
const content = await readFile(snippetTarget, 'utf8')
|
|
285
|
+
return { ok: true, content, size: content.length }
|
|
286
|
+
} catch (error) {
|
|
287
|
+
return { ok: false, error: '读取片段文件失败:' + String(error), resolvedPath: snippetTarget }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
261
290
|
const sc = await requireSession(ctx, args.sessionId)
|
|
262
291
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
263
292
|
const fs = ctx.get('fs')
|
|
@@ -309,6 +338,16 @@ export function buildHandlers(
|
|
|
309
338
|
}
|
|
310
339
|
},
|
|
311
340
|
'edrv.save': async (args) => {
|
|
341
|
+
// 全局片段文件:直写(工作区外),且不进差异审查(片段变更不是 agent 编辑产物)
|
|
342
|
+
const snippetTarget = snippetTargetOf(args.path)
|
|
343
|
+
if (snippetTarget) {
|
|
344
|
+
try {
|
|
345
|
+
await writeFile(snippetTarget, args.content, 'utf8')
|
|
346
|
+
return { ok: true }
|
|
347
|
+
} catch (error) {
|
|
348
|
+
return { ok: false, error: '保存片段文件失败:' + String(error) }
|
|
349
|
+
}
|
|
350
|
+
}
|
|
312
351
|
const sc = await requireSession(ctx, args.sessionId)
|
|
313
352
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
314
353
|
const fs = ctx.get('fs')
|
|
@@ -770,6 +809,45 @@ export function buildHandlers(
|
|
|
770
809
|
return { ok: false, error: '切换规则失败:' + String(error) }
|
|
771
810
|
}
|
|
772
811
|
},
|
|
812
|
+
'snippets.list': async () => {
|
|
813
|
+
try {
|
|
814
|
+
return { ok: true, ...(await snippetsList(ctx)) }
|
|
815
|
+
} catch (error) {
|
|
816
|
+
return { ok: false, error: '读取代码片段失败:' + String(error) }
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
'snippets.read': async (args) => {
|
|
820
|
+
try {
|
|
821
|
+
return { ok: true, content: await snippetsRead(ctx, args) }
|
|
822
|
+
} catch (error) {
|
|
823
|
+
return { ok: false, error: '读取代码片段失败:' + String(error) }
|
|
824
|
+
}
|
|
825
|
+
},
|
|
826
|
+
'snippets.save': async (args) => {
|
|
827
|
+
try {
|
|
828
|
+
return { ok: true, file: await snippetsSave(ctx, args) }
|
|
829
|
+
} catch (error) {
|
|
830
|
+
return { ok: false, error: '保存代码片段失败:' + String(error) }
|
|
831
|
+
}
|
|
832
|
+
},
|
|
833
|
+
'snippets.remove': async (args) => {
|
|
834
|
+
try {
|
|
835
|
+
await snippetsRemove(ctx, args)
|
|
836
|
+
return { ok: true }
|
|
837
|
+
} catch (error) {
|
|
838
|
+
return { ok: false, error: '删除代码片段失败:' + String(error) }
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
'snippets.entries': async (args) => {
|
|
842
|
+
try {
|
|
843
|
+
// 当前会话工作区用于叠加项目片段;会话缺失时仅返回全局片段(不报错,补全仍可用)
|
|
844
|
+
const sc = await requireSession(ctx, args.sessionId)
|
|
845
|
+
const cwd = 'err' in sc ? undefined : sc.cwd
|
|
846
|
+
return { ok: true, ...(await snippetsEntries(ctx, cwd)) }
|
|
847
|
+
} catch (error) {
|
|
848
|
+
return { ok: false, error: '读取代码片段条目失败:' + String(error) }
|
|
849
|
+
}
|
|
850
|
+
},
|
|
773
851
|
}
|
|
774
852
|
}
|
|
775
853
|
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* 纯数据模块:host(settings schema 默认值)与 client(执行匹配/设置页)共用。
|
|
4
4
|
* 键位格式:修饰符 + 主键,`+` 连接,如 `Ctrl+Shift+F`;空串 = 未绑定;
|
|
5
5
|
* `|` 连接多个候选(任一命中即触发),如 `Alt+ArrowLeft|Ctrl+Alt+-`。
|
|
6
|
-
*
|
|
6
|
+
* 约束:本模块被 host 与 client 两半共用,**禁止 import client 侧模块**(React/浏览器 API)。
|
|
7
|
+
* 与 client/ui/commandCatalog 的一致性由 tests/commands.test.ts 断言兜底。
|
|
8
|
+
* 作者 ddj 2026年08月26号 / 2026年09月10号
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
/** 命令 id → 默认键位(可为多候选)。命令目录以此为准,新增命令只需加一项。 */
|
|
@@ -17,6 +19,12 @@ export const KEYBINDING_DEFAULTS: Record<string, string> = {
|
|
|
17
19
|
// 页签循环:主候选避开浏览器保留键(Ctrl+Tab / Ctrl+PgUp/PgDn 会被浏览器截获)
|
|
18
20
|
'edrv.nextTab': 'Ctrl+Alt+ArrowRight|Ctrl+PageDown',
|
|
19
21
|
'edrv.prevTab': 'Ctrl+Alt+ArrowLeft|Ctrl+PageUp',
|
|
22
|
+
// 命令栏(Ctrl+Shift+P 主候选;F1 为 VS Code 同款第二候选)与编辑行导航
|
|
23
|
+
'edrv.showCommands': 'Ctrl+Shift+P|F1',
|
|
24
|
+
'edrv.nextEditorRow': 'Ctrl+Alt+ArrowDown',
|
|
25
|
+
'edrv.prevEditorRow': 'Ctrl+Alt+ArrowUp',
|
|
26
|
+
// 添加选中内容为引用:把当前选区追加进对话输入框(Ctrl+U,VS Code 同款)
|
|
27
|
+
'edrv.addSelectionRef': 'Ctrl+U',
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
/**
|
package/src/shared/rpc.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { MpcConfig, MpcProject, MpcProjectSaveInput, MpcServer } from './mc
|
|
|
16
16
|
import type { CompatReport, DevFormInfo } from './compat.js'
|
|
17
17
|
import type { ShellIntegrationStatus, UnityListPayload, UnityProjectEntry } from './integration.js'
|
|
18
18
|
import type { RuleInfo, RuleProject, RuleRefInput, RuleSaveInput } from './rules.js'
|
|
19
|
+
import type { SnippetEntry, SnippetInfo, SnippetProject, SnippetRefInput, SnippetSaveInput } from './snippets.js'
|
|
19
20
|
import type { LspEnvInstallState, LspExtInfo, LspExtUpdate, LspHover, LspLocation, LspMarketItem, LspPosition, LspSemanticTokens, LspServerStatus, LspSymbol } from './lsp.js'
|
|
20
21
|
import type { AiConfigPatch, AiConfigView, AiDirectoryView, AiInlineRequest, AiInlineResult } from './ai.js'
|
|
21
22
|
|
|
@@ -236,6 +237,11 @@ export interface RpcRequestMap {
|
|
|
236
237
|
'rules.save': RuleSaveInput
|
|
237
238
|
'rules.remove': RuleRefInput
|
|
238
239
|
'rules.toggle': RuleRefInput & { enabled: boolean }
|
|
240
|
+
'snippets.list': {}
|
|
241
|
+
'snippets.read': SnippetRefInput
|
|
242
|
+
'snippets.save': SnippetSaveInput
|
|
243
|
+
'snippets.remove': SnippetRefInput
|
|
244
|
+
'snippets.entries': { sessionId?: string }
|
|
239
245
|
}
|
|
240
246
|
|
|
241
247
|
export type RpcMethod = keyof RpcRequestMap
|
|
@@ -324,6 +330,11 @@ export interface RpcOkMap {
|
|
|
324
330
|
'rules.save': { rule: RuleInfo }
|
|
325
331
|
'rules.remove': object
|
|
326
332
|
'rules.toggle': { rule: RuleInfo }
|
|
333
|
+
'snippets.list': { user: SnippetInfo[]; projects: SnippetProject[] }
|
|
334
|
+
'snippets.read': { content: string }
|
|
335
|
+
'snippets.save': { file: SnippetInfo }
|
|
336
|
+
'snippets.remove': object
|
|
337
|
+
'snippets.entries': { entries: SnippetEntry[] }
|
|
327
338
|
}
|
|
328
339
|
|
|
329
340
|
/** 统一响应:{ok:true, ...payload} 或 {ok:false, error}。 */
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode 代码片段共享数据契约(VS Code 兼容 .code-snippets 文件)。
|
|
3
|
+
* 纯类型模块:禁 node/react 导入(与 shared/rules.ts 同约束)。
|
|
4
|
+
* 文件格式与 VS Code 一致:顶层对象 { "<片段名>": { prefix, body, description?, scope? } },
|
|
5
|
+
* body 可为字符串或字符串数组(数组按行拼接);scope 为语言 id 或语言 id 数组。
|
|
6
|
+
* 作者 ddj 2026年09月10号
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** 片段作用域:全局片段(~/.dsh/snippets)或项目片段(<工作区>/.dsh/snippets)。 */
|
|
10
|
+
export type SnippetScope = 'user' | 'project'
|
|
11
|
+
|
|
12
|
+
/** 一个 .code-snippets 文件的元信息(列表行展示 + 补全索引所需的最小集)。 */
|
|
13
|
+
export interface SnippetInfo {
|
|
14
|
+
scope: SnippetScope
|
|
15
|
+
/** 文件名(含 .code-snippets 后缀,不含路径)。 */
|
|
16
|
+
file: string
|
|
17
|
+
/** 绝对路径(host 解析,供展示与在编辑界面打开)。 */
|
|
18
|
+
absPath: string
|
|
19
|
+
/** 相对提示(用户层 snippets/ 或项目层 .dsh/snippets/)。 */
|
|
20
|
+
relHint: string
|
|
21
|
+
/** 由文件名推导的语言 id(`global`/无法识别为空串 = 全语言生效)。 */
|
|
22
|
+
language: string
|
|
23
|
+
/** 文件内片段条目数(解析失败为 0)。 */
|
|
24
|
+
count: number
|
|
25
|
+
/** 文件字节数(列表排序/超大提示用)。 */
|
|
26
|
+
size: number
|
|
27
|
+
/** 修改时间毫秒。 */
|
|
28
|
+
mtime: number
|
|
29
|
+
/** JSON 解析失败文案(仅 UI 提示,不影响其他文件)。 */
|
|
30
|
+
error?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 一个工作区的项目片段聚合(snippets.list 的 projects 项)。 */
|
|
34
|
+
export interface SnippetProject {
|
|
35
|
+
workspacePath: string
|
|
36
|
+
title: string
|
|
37
|
+
files: SnippetInfo[]
|
|
38
|
+
/** 工作区目录不存在或无片段目录时为 true(UI 显示空态而非报错)。 */
|
|
39
|
+
missingDir?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 片段文件读取/删除入参公共字段。 */
|
|
43
|
+
export interface SnippetRefInput {
|
|
44
|
+
scope: SnippetScope
|
|
45
|
+
/** project 必填:目标工作区绝对路径(须为 DSH 已注册 workspace)。 */
|
|
46
|
+
workspacePath?: string
|
|
47
|
+
file: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 片段文件保存入参:content 为完整 JSON 文本(host 原样写盘)。 */
|
|
51
|
+
export interface SnippetSaveInput {
|
|
52
|
+
scope: SnippetScope
|
|
53
|
+
workspacePath?: string
|
|
54
|
+
file: string
|
|
55
|
+
content: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 一条已展开的片段条目(补全 provider 与插入命令共用)。 */
|
|
59
|
+
export interface SnippetEntry {
|
|
60
|
+
/** 片段名(对象键,补全候选的 label)。 */
|
|
61
|
+
key: string
|
|
62
|
+
/** 触发前缀(VS Code prefix;空串表示仅靠描述/手动插入)。 */
|
|
63
|
+
prefix: string
|
|
64
|
+
/** 展开正文(已按数组形式拼接为含换行的字符串)。 */
|
|
65
|
+
body: string
|
|
66
|
+
/** 片段描述(补全候选的说明,缺省为空串)。 */
|
|
67
|
+
description: string
|
|
68
|
+
scope: SnippetScope
|
|
69
|
+
/** 来源文件名(补全候选 detail 展示)。 */
|
|
70
|
+
file: string
|
|
71
|
+
/** 生效语言 id(空串 = 全语言)。 */
|
|
72
|
+
language: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --region 文件名与模板(纯函数:host 与 client 共用,避免两处漂移)
|
|
76
|
+
/** 全语言生效的文件名(无语言前缀)。 */
|
|
77
|
+
export const SNIPPET_GLOBAL_FILE = 'global.code-snippets'
|
|
78
|
+
/** 片段文件后缀。 */
|
|
79
|
+
export const SNIPPET_EXT = '.code-snippets'
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 语言 id → 默认片段文件名(空语言回退 global)。
|
|
83
|
+
* @author ddj 2026年09月10号
|
|
84
|
+
* @param language 语言 id
|
|
85
|
+
* @returns 文件名(如 `lua.code-snippets`)
|
|
86
|
+
*/
|
|
87
|
+
export function snippetFileNameFor(language: string): string {
|
|
88
|
+
const lang = String(language ?? '').trim().toLowerCase()
|
|
89
|
+
return (lang || 'global') + SNIPPET_EXT
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 归一化新建文件名:用户输入可为裸语言名(补后缀)或完整文件名;空输入为空串。
|
|
94
|
+
* @author ddj 2026年09月10号
|
|
95
|
+
* @param raw 用户输入
|
|
96
|
+
* @returns 归一化后的文件名
|
|
97
|
+
*/
|
|
98
|
+
export function normalizeSnippetFileName(raw: string): string {
|
|
99
|
+
const text = String(raw ?? '').trim()
|
|
100
|
+
if (!text) return ''
|
|
101
|
+
return text.toLowerCase().endsWith(SNIPPET_EXT) ? text : text + SNIPPET_EXT
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 新建片段文件模板(一条示例,改 body 即可用)。
|
|
106
|
+
* @author ddj 2026年09月10号
|
|
107
|
+
* @param language 目标语言 id(空串 = 全局)
|
|
108
|
+
* @returns JSON 文本(带尾换行)
|
|
109
|
+
*/
|
|
110
|
+
export function snippetFileTemplate(language: string): string {
|
|
111
|
+
const lang = String(language ?? '').trim().toLowerCase()
|
|
112
|
+
const sample = (lang || 'global') + ' 示例片段'
|
|
113
|
+
return JSON.stringify({
|
|
114
|
+
[sample]: {
|
|
115
|
+
prefix: 'hello',
|
|
116
|
+
body: ['// ' + (lang || 'global') + ' 片段示例', '$1'],
|
|
117
|
+
description: '示例:输入 hello 后 Tab 展开',
|
|
118
|
+
},
|
|
119
|
+
}, null, 2) + '\n'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 可绑定语言目录(新建片段时的语言下拉来源;与 client/monaco/loader 的 LANG_BY_EXT
|
|
124
|
+
* 取值集合保持一致)。此处冗余一份纯数据,使本模块不依赖浏览器端 Monaco
|
|
125
|
+
* (shared 禁 import client 侧模块);一致性由 tests/snippetLanguage.test.ts 断言兜底。
|
|
126
|
+
*/
|
|
127
|
+
export const SNIPPET_LANGUAGES: readonly string[] = [
|
|
128
|
+
'c', 'cpp', 'csharp', 'css', 'dart', 'dockerfile', 'go', 'html', 'ini', 'java',
|
|
129
|
+
'javascript', 'json', 'jsonc', 'kotlin', 'less', 'lua', 'markdown', 'mdx', 'php',
|
|
130
|
+
'plaintext', 'powershell', 'python', 'ruby', 'rust', 'scss', 'shell', 'sql',
|
|
131
|
+
'swift', 'typescript', 'xml', 'yaml',
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
/** 语言 id → 展示名(仅收录写法与 id 明显不同的;未收录的原样返回)。 */
|
|
135
|
+
const LANGUAGE_LABELS: Record<string, string> = {
|
|
136
|
+
cpp: 'C++',
|
|
137
|
+
csharp: 'C#',
|
|
138
|
+
javascript: 'JavaScript',
|
|
139
|
+
typescript: 'TypeScript',
|
|
140
|
+
jsonc: 'JSON with Comments',
|
|
141
|
+
powershell: 'PowerShell',
|
|
142
|
+
plaintext: '纯文本',
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 语言 id 的人类可读名(用于下拉与「新建 xx 代码片段文件」文案)。
|
|
147
|
+
* @author ddj 2026年09月10号
|
|
148
|
+
* @param id 语言 id(空串表示全语言)
|
|
149
|
+
* @returns 展示名
|
|
150
|
+
*/
|
|
151
|
+
export function languageLabelOf(id: string): string {
|
|
152
|
+
const key = String(id ?? '').trim().toLowerCase()
|
|
153
|
+
if (!key) return '全语言'
|
|
154
|
+
return LANGUAGE_LABELS[key] ?? key
|
|
155
|
+
}
|
|
156
|
+
// --endregion
|