dsh-vscode-mode 0.11.0 → 0.13.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 +158 -111
- package/assets/shell/dsh-open.cs +65 -149
- package/assets/shell/dsh-open.mjs +241 -0
- package/assets/shell/dsh-open.ps1 +28 -47
- package/assets/shell/dsh-open.sh +25 -66
- package/lib/client.js +13362 -11806
- package/lib/client.js.map +1 -1
- package/lib/index.js +4110 -2507
- package/lib/index.js.map +1 -1
- package/package.json +9 -8
- package/skills/dsh-vscodemode-mcp/SKILL.md +94 -132
- package/src/childEnv.ts +19 -0
- package/src/client/activePathStore.ts +28 -0
- package/src/client/addToConversation.ts +4 -2
- package/src/client/ai/inlineProvider.ts +1 -8
- package/src/client/commandBridge.ts +14 -42
- package/src/client/externalOpen.ts +40 -19
- package/src/client/index.ts +101 -18
- package/src/client/keybindings.ts +36 -73
- package/src/client/monaco/loader.ts +3 -1
- package/src/client/openFlow.ts +36 -28
- package/src/client/openReceipt.ts +64 -0
- package/src/client/panelCommands.ts +101 -0
- package/src/client/pdf/pdfLoader.ts +4 -2
- package/src/client/receiptState.ts +120 -0
- package/src/client/rpc.ts +30 -4
- package/src/client/shortcutRecorder.ts +161 -0
- package/src/client/shortcutsOfficial.ts +460 -0
- package/src/client/sidebar/panels/RulesPanel.ts +15 -11
- package/src/client/sidebarBridge.ts +7 -0
- package/src/client/ui/DiffBadge.ts +2 -1
- package/src/client/ui/EditorView.ts +246 -137
- package/src/client/ui/McpSettings.ts +10 -8
- package/src/client/ui/OfficialSideTab.ts +5 -2
- package/src/client/ui/QuickOpen.ts +16 -16
- package/src/client/ui/ShortcutSettings.ts +274 -0
- package/src/client/ui/commandCatalog.ts +39 -43
- package/src/client/useOpenReceipt.ts +128 -0
- package/src/compat.ts +5 -3
- package/src/dap/manager.ts +46 -13
- package/src/devForm.ts +252 -92
- package/src/externalHandoff.ts +93 -52
- package/src/fileOpenSettings.ts +4 -24
- package/src/hostImport.ts +19 -0
- package/src/httpGuard.ts +101 -0
- package/src/index.ts +106 -97
- package/src/integrate.ts +56 -50
- package/src/lsp/transport.ts +84 -127
- package/src/mcp.ts +58 -9
- package/src/mcpIsolation.ts +211 -102
- package/src/mcpProject.ts +220 -133
- package/src/mcpRuntime.ts +343 -0
- package/src/openBridge.ts +69 -0
- package/src/openInbox.ts +207 -0
- package/src/paths.ts +8 -5
- package/src/processTree.ts +63 -0
- package/src/reveal.ts +5 -3
- package/src/routes.ts +156 -154
- package/src/rpc.ts +14 -13
- package/src/rules.ts +100 -44
- package/src/shared/appUrl.ts +19 -0
- package/src/shared/externalOpen.ts +71 -0
- package/src/shared/integration.ts +2 -0
- package/src/shared/keybindings.ts +256 -0
- package/src/shared/mcp.ts +4 -1
- package/src/shared/rpc.ts +7 -4
- package/src/shared/rules.ts +9 -1
- package/src/shared/svn.ts +6 -8
- package/src/svnPlanTool.ts +51 -0
- package/src/unityBridge.ts +26 -3
- package/unity/com.dsh.editor/Editor/DshCodeEditor.cs +99 -203
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs +166 -0
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs.meta +2 -0
- package/unity/com.dsh.editor/README.md +16 -25
- package/unity/com.dsh.editor/package.json +2 -2
- package/src/client/ui/KeybindingsPanel.ts +0 -190
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 官方 shortcuts 服务桥接层。
|
|
3
|
+
* 职责四件:① 运行时探测 ctx.shortcuts(可选服务,有界重试,不进 inject——旧版 DSH 无此服务
|
|
4
|
+
* 时不阻断装配);② safeRegister 逐命令注册(官方 register 对重复 id/保留默认/组合重叠抛错,
|
|
5
|
+
* 冲突时去掉默认键位降级注册,绝不阻断插件加载);③ 官方命令定义构建(commandCatalog → 官方
|
|
6
|
+
* command,resolve 统一走插件注册表 run,可用性为假返回 pass 不吞键);④ 旧 keybindings
|
|
7
|
+
* 设置一次性迁移进官方存储。
|
|
8
|
+
* 作者 ddj 2026年10月
|
|
9
|
+
*/
|
|
10
|
+
import { bindingToChord, chordToBinding, defaultKeybindings, normalizeKeybindings } from '../shared/keybindings.js'
|
|
11
|
+
import { applyKeyAria, matchEvent, parseChords } from './keybindings.js'
|
|
12
|
+
import type { OfficialBinding, ShortcutProfileKey } from '../shared/keybindings.js'
|
|
13
|
+
import { log } from './log.js'
|
|
14
|
+
|
|
15
|
+
const bridgeLog = log.child('shortcuts-official')
|
|
16
|
+
|
|
17
|
+
/** 迁移比对用的默认键位表副本(模块态,避免每次调用重建)。 */
|
|
18
|
+
const KEYBINDING_DEFAULTS_OF: Record<string, string> = defaultKeybindings()
|
|
19
|
+
|
|
20
|
+
//#region 官方服务鸭子类型(结构对齐 @deepseek-ai/dsh-client-shortcuts,不 import 官方包)
|
|
21
|
+
|
|
22
|
+
/** 快照存储(createSnapshotStore 形状)。 */
|
|
23
|
+
export interface SnapshotStoreLike<T> {
|
|
24
|
+
getSnapshot(): T
|
|
25
|
+
subscribe(listener: () => void): () => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 官方目录行(catalog 快照项)。 */
|
|
29
|
+
export interface ShortcutCatalogRow {
|
|
30
|
+
id: string
|
|
31
|
+
label: string
|
|
32
|
+
binding: OfficialBinding | null
|
|
33
|
+
modified: boolean
|
|
34
|
+
issue: string | null
|
|
35
|
+
conflicts: string[]
|
|
36
|
+
keys: string[]
|
|
37
|
+
aria?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 官方配置快照。 */
|
|
41
|
+
export interface ShortcutConfigSnapshot {
|
|
42
|
+
revision: string
|
|
43
|
+
sequence: number
|
|
44
|
+
status: 'loading' | 'ready' | 'unreadable'
|
|
45
|
+
error: string | null
|
|
46
|
+
usingDefaults: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 官方派发上下文。 */
|
|
50
|
+
export interface ShortcutInvokeContext {
|
|
51
|
+
region: string
|
|
52
|
+
modal: string | null
|
|
53
|
+
target?: unknown
|
|
54
|
+
source?: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 官方 resolve 结果。 */
|
|
58
|
+
export type ShortcutResolution =
|
|
59
|
+
| { status: 'handled'; run: () => void }
|
|
60
|
+
| { status: 'pass' }
|
|
61
|
+
| { status: 'blocked'; reason?: string }
|
|
62
|
+
|
|
63
|
+
/** 官方可编辑命令定义(register 入参)。 */
|
|
64
|
+
export interface ShortcutCommandLike {
|
|
65
|
+
id: string
|
|
66
|
+
label: () => string
|
|
67
|
+
aliases?: string[]
|
|
68
|
+
defaults?: Partial<Record<ShortcutProfileKey, OfficialBinding>>
|
|
69
|
+
regions: string[]
|
|
70
|
+
modals: string[]
|
|
71
|
+
resolve: (context: ShortcutInvokeContext) => ShortcutResolution
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 官方偏好编辑操作。 */
|
|
75
|
+
export type ShortcutEditLike =
|
|
76
|
+
| { type: 'set'; id: string; binding: OfficialBinding }
|
|
77
|
+
| { type: 'reset'; id: string }
|
|
78
|
+
| { type: 'reset-all' }
|
|
79
|
+
|
|
80
|
+
/** 官方 shortcuts 服务(ShortcutsService 结构子集,鸭子类型)。 */
|
|
81
|
+
export interface ShortcutsServiceLike {
|
|
82
|
+
register(command: ShortcutCommandLike): () => void
|
|
83
|
+
describeBinding(binding: OfficialBinding | null): { binding: OfficialBinding | null; keys: string[]; issue: string | null; conflicts: string[] }
|
|
84
|
+
edit(edit: ShortcutEditLike, revision: string): Promise<{ status: string; issue?: string | null; conflicts?: string[] }>
|
|
85
|
+
recording(active: boolean): Promise<void>
|
|
86
|
+
catalog: SnapshotStoreLike<ShortcutCatalogRow[]>
|
|
87
|
+
config: SnapshotStoreLike<ShortcutConfigSnapshot>
|
|
88
|
+
fixedCatalog: SnapshotStoreLike<unknown[]>
|
|
89
|
+
platform: string
|
|
90
|
+
runtime: string
|
|
91
|
+
/** ShortcutRegistry 实例(菜单路径 invoke;用于程序化打开官方弹窗)。 */
|
|
92
|
+
registry?: { invoke(id: string, context: ShortcutInvokeContext): unknown }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
//#endregion
|
|
96
|
+
|
|
97
|
+
/** 探测节奏(与插件其他可选服务探测一致:15 次 × 2s)。 */
|
|
98
|
+
export interface ProbeOptions {
|
|
99
|
+
schedule: (fn: () => void, ms: number) => void
|
|
100
|
+
attempts?: number
|
|
101
|
+
intervalMs?: number
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 结构探测:get('shortcuts') 命中且具备核心能力才算服务就绪(设置组件运行时自愈探测复用)。 */
|
|
105
|
+
export function detectShortcuts(ctx: { get: (name: string) => unknown }): ShortcutsServiceLike | null {
|
|
106
|
+
try {
|
|
107
|
+
const service = ctx.get('shortcuts') as ShortcutsServiceLike | undefined
|
|
108
|
+
if (service && typeof service.register === 'function' && typeof service.edit === 'function' && service.catalog && service.config) return service
|
|
109
|
+
} catch {
|
|
110
|
+
/* 服务缺失/构造失败按未命中处理 */
|
|
111
|
+
}
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 探测官方 shortcuts 服务:立即探测一次,未命中按节奏有界重试;命中或用尽均回调一次。
|
|
117
|
+
* @author ddj 2026年10月
|
|
118
|
+
* @param ctx 客户端上下文
|
|
119
|
+
* @param options schedule/重试节奏 + onReady 回调
|
|
120
|
+
* @returns 取消探测的注销函数
|
|
121
|
+
*/
|
|
122
|
+
export function awaitShortcutsService(ctx: { get: (name: string) => unknown }, options: ProbeOptions & { onReady: (service: ShortcutsServiceLike | null) => void }): () => void {
|
|
123
|
+
const maxAttempts = options.attempts ?? 15
|
|
124
|
+
const interval = options.intervalMs ?? 2000
|
|
125
|
+
let attempts = 0
|
|
126
|
+
let settled = false
|
|
127
|
+
const tick = (): void => {
|
|
128
|
+
if (settled) return
|
|
129
|
+
const service = detectShortcuts(ctx)
|
|
130
|
+
if (service !== null) {
|
|
131
|
+
settled = true
|
|
132
|
+
options.onReady(service)
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
if (attempts >= maxAttempts) {
|
|
136
|
+
settled = true
|
|
137
|
+
options.onReady(null)
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
attempts += 1
|
|
141
|
+
options.schedule(tick, interval)
|
|
142
|
+
}
|
|
143
|
+
tick()
|
|
144
|
+
return () => {
|
|
145
|
+
settled = true
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 逐命令安全注册:官方 register 抛错(重复 id/保留默认/组合重叠)时去掉默认键位重试一次,
|
|
151
|
+
* 保证命令仍进官方目录(可被用户手动绑定),注册路径绝不抛错、绝不阻断装配。
|
|
152
|
+
* @author ddj 2026年10月
|
|
153
|
+
* @param service 官方服务
|
|
154
|
+
* @param command 官方命令定义
|
|
155
|
+
* @returns 注销函数(注册失败返回空操作)
|
|
156
|
+
*/
|
|
157
|
+
export function safeRegister(service: ShortcutsServiceLike, command: ShortcutCommandLike): () => void {
|
|
158
|
+
try {
|
|
159
|
+
return service.register(command)
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (!command.defaults || Object.keys(command.defaults).length === 0) {
|
|
162
|
+
bridgeLog.warn('官方快捷键注册失败(' + command.id + '):' + String(error))
|
|
163
|
+
return () => {}
|
|
164
|
+
}
|
|
165
|
+
bridgeLog.warn('官方快捷键默认键位被拒(' + command.id + '),降级为未绑定:' + String(error))
|
|
166
|
+
try {
|
|
167
|
+
return service.register({ ...command, defaults: {} })
|
|
168
|
+
} catch (retryError) {
|
|
169
|
+
bridgeLog.warn('官方快捷键注册失败(' + command.id + '):' + String(retryError))
|
|
170
|
+
return () => {}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 插件指令注册表执行子集(commandRegistry 形状)。 */
|
|
176
|
+
export interface CommandRunner {
|
|
177
|
+
isAvailable(id: string): boolean
|
|
178
|
+
run(id: string): boolean
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 官方服务未就绪时复用旧版窗口键位语义;服务出现后注销,避免双执行。
|
|
183
|
+
* @author ddj 2026年09月28号
|
|
184
|
+
* @param defs 按目录序排列的插件命令
|
|
185
|
+
* @param runner 指令注册表
|
|
186
|
+
* @param chords 当前旧设置键位获取函数
|
|
187
|
+
* @param target 键盘事件目标(缺省 window,测试可注入)
|
|
188
|
+
* @returns 注销监听的函数
|
|
189
|
+
*/
|
|
190
|
+
export function installLegacyKeys(
|
|
191
|
+
defs: ReadonlyArray<{ id: string }>, runner: CommandRunner, chords: () => Record<string, string>,
|
|
192
|
+
target: Pick<EventTarget, 'addEventListener' | 'removeEventListener'> | null = typeof window === 'undefined' ? null : window,
|
|
193
|
+
): () => void {
|
|
194
|
+
if (!target) return () => {}
|
|
195
|
+
const cache = new Map<string, ReturnType<typeof parseChords>>()
|
|
196
|
+
let disposed = false
|
|
197
|
+
const onKey = (event: Event): void => {
|
|
198
|
+
if (disposed) return
|
|
199
|
+
const key = event as KeyboardEvent
|
|
200
|
+
const current = chords()
|
|
201
|
+
for (const def of defs) {
|
|
202
|
+
const chord = current[def.id]
|
|
203
|
+
if (!chord) continue
|
|
204
|
+
let bindings = cache.get(chord)
|
|
205
|
+
if (!bindings) {
|
|
206
|
+
bindings = parseChords(chord)
|
|
207
|
+
cache.set(chord, bindings)
|
|
208
|
+
}
|
|
209
|
+
if (!matchEvent(key, bindings) || !runner.isAvailable(def.id)) continue
|
|
210
|
+
key.preventDefault()
|
|
211
|
+
key.stopPropagation()
|
|
212
|
+
runner.run(def.id)
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
target.addEventListener('keydown', onKey, { capture: true })
|
|
217
|
+
return () => {
|
|
218
|
+
if (disposed) return
|
|
219
|
+
disposed = true
|
|
220
|
+
target.removeEventListener('keydown', onKey, { capture: true })
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 插件命令定义 → 官方命令定义:默认键位取 SHORTCUT_PROFILES(缺省 = 不绑定);
|
|
226
|
+
* resolve 可用性为假返回 pass(不吞键,与旧桥「不可用不执行也不吞键」语义一致),
|
|
227
|
+
* 执行统一走注册表 run(自带可用性复核与异常捕获)。
|
|
228
|
+
* @author ddj 2026年10月
|
|
229
|
+
* @param def 插件命令定义
|
|
230
|
+
* @param runner 插件指令注册表
|
|
231
|
+
* @returns 官方命令定义
|
|
232
|
+
*/
|
|
233
|
+
export function officialCommandOf(def: { id: string; label: string }, runner: CommandRunner): ShortcutCommandLike {
|
|
234
|
+
return {
|
|
235
|
+
id: def.id,
|
|
236
|
+
label: () => def.label,
|
|
237
|
+
// 必须为数组:官方弹窗渲染排名列表时 `...row.aliases` 直接展开,undefined 会让弹窗崩溃
|
|
238
|
+
aliases: [],
|
|
239
|
+
defaults: undefined,
|
|
240
|
+
regions: ['page', 'editable'],
|
|
241
|
+
modals: [],
|
|
242
|
+
resolve: () => {
|
|
243
|
+
if (!runner.isAvailable(def.id)) return { status: 'pass' }
|
|
244
|
+
return {
|
|
245
|
+
status: 'handled',
|
|
246
|
+
run: () => {
|
|
247
|
+
runner.run(def.id)
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 注册全部命令进官方服务(逐命令 safeRegister 隔离,defaults 由调用方按 id 注入 SHORTCUT_PROFILES)。
|
|
256
|
+
* @author ddj 2026年10月
|
|
257
|
+
* @param service 官方服务
|
|
258
|
+
* @param defs 插件命令定义(id/label)
|
|
259
|
+
* @param runner 插件指令注册表
|
|
260
|
+
* @param profiles id → 官方 profile 默认键位
|
|
261
|
+
* @returns 聚合注销函数
|
|
262
|
+
*/
|
|
263
|
+
export function registerOfficialShortcuts(
|
|
264
|
+
service: ShortcutsServiceLike,
|
|
265
|
+
defs: ReadonlyArray<{ id: string; label: string }>,
|
|
266
|
+
runner: CommandRunner,
|
|
267
|
+
profiles: Readonly<Record<string, Partial<Record<ShortcutProfileKey, OfficialBinding>>>>,
|
|
268
|
+
): () => void {
|
|
269
|
+
const disposers: Array<() => void> = []
|
|
270
|
+
for (const def of defs) {
|
|
271
|
+
const command = officialCommandOf(def, runner)
|
|
272
|
+
command.defaults = profiles[def.id] ?? {}
|
|
273
|
+
disposers.push(safeRegister(service, command))
|
|
274
|
+
}
|
|
275
|
+
return () => {
|
|
276
|
+
while (disposers.length) disposers.pop()?.()
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 订阅官方目录 → 重建 edrv.* 弦表(chordOf 数据源);目录变化即通知。
|
|
282
|
+
* 只收录 issue 为空的行(conflict 行保留——用户显式改键后 tooltip 仍显示其选择,
|
|
283
|
+
* 冲突状态由设置页徽标呈现)。
|
|
284
|
+
* @author ddj 2026年10月
|
|
285
|
+
* @param service 官方服务
|
|
286
|
+
* @param onChange 弦表回调(每次全量替换)
|
|
287
|
+
* @returns 注销函数
|
|
288
|
+
*/
|
|
289
|
+
export function bindCatalogChords(service: ShortcutsServiceLike, onChange: (chords: Readonly<Record<string, string>>) => void): () => void {
|
|
290
|
+
const sync = (): void => {
|
|
291
|
+
const chords: Record<string, string> = {}
|
|
292
|
+
const aria: Record<string, string | undefined> = {}
|
|
293
|
+
try {
|
|
294
|
+
for (const row of service.catalog.getSnapshot()) {
|
|
295
|
+
if (!row.id.startsWith('edrv.') || row.binding === null) continue
|
|
296
|
+
if (row.issue !== null) continue
|
|
297
|
+
const chord = row.keys.length ? row.keys.join(' ') : bindingToChord(row.binding)
|
|
298
|
+
if (chord !== null) chords[row.id] = chord
|
|
299
|
+
aria[row.id] = row.aria
|
|
300
|
+
}
|
|
301
|
+
} catch (error) {
|
|
302
|
+
bridgeLog.warn('官方目录弦表重建失败:' + String(error))
|
|
303
|
+
}
|
|
304
|
+
applyKeyAria(aria)
|
|
305
|
+
onChange(chords)
|
|
306
|
+
}
|
|
307
|
+
sync()
|
|
308
|
+
return service.catalog.subscribe(sync)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
//#region 旧设置迁移
|
|
312
|
+
|
|
313
|
+
/** 旧设置 scope 最小形状(compat settingsBridge 绑定产物的最小面,结构与 SettingsScopeLike 兼容)。 */
|
|
314
|
+
export interface LegacySettingsScope {
|
|
315
|
+
getSnapshot(): { status?: string; value?: { keybindings?: unknown } }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** 迁移结果:done=true 表示无需/已完成迁移(含「无差异」),false 表示中止待重试。 */
|
|
319
|
+
export interface MigrationResult {
|
|
320
|
+
imported: number
|
|
321
|
+
skipped: number
|
|
322
|
+
done: boolean
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 官方 edit 单次写入;stale 表示其他窗口已更新,不自动用新 revision 重放。
|
|
327
|
+
* @author ddj 2026年09月28号
|
|
328
|
+
* @param service 官方快捷键服务
|
|
329
|
+
* @param edit 本次操作
|
|
330
|
+
* @returns 是否保存成功
|
|
331
|
+
*/
|
|
332
|
+
async function editOnce(service: ShortcutsServiceLike, edit: ShortcutEditLike): Promise<boolean> {
|
|
333
|
+
try {
|
|
334
|
+
const result = await service.edit(edit, service.config.getSnapshot().revision)
|
|
335
|
+
if (result.status === 'saved') return true
|
|
336
|
+
bridgeLog.warn('官方快捷键写入未成功(' + edit.type + (edit.type === 'set' ? ' ' + edit.id : '') + '):' + result.status)
|
|
337
|
+
} catch (error) {
|
|
338
|
+
bridgeLog.warn('官方快捷键写入异常(' + edit.type + '):' + String(error))
|
|
339
|
+
}
|
|
340
|
+
return false
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* 逐条恢复插件自定义键位;官方 reset-all 会清空所有产品命令的覆盖,不能使用。
|
|
345
|
+
* @author ddj 2026年09月28号
|
|
346
|
+
* @param service 官方快捷键服务
|
|
347
|
+
* @returns 完成数量及首个失败命令(有失败时)
|
|
348
|
+
*/
|
|
349
|
+
export async function resetPluginKeys(service: ShortcutsServiceLike): Promise<{ reset: number; failed?: string }> {
|
|
350
|
+
const ids = service.catalog.getSnapshot()
|
|
351
|
+
.filter((row) => row.id.startsWith('edrv.') && row.modified)
|
|
352
|
+
.map((row) => row.id)
|
|
353
|
+
let reset = 0
|
|
354
|
+
for (const id of ids) {
|
|
355
|
+
if (!await editOnce(service, { type: 'reset', id })) return { reset, failed: id }
|
|
356
|
+
reset += 1
|
|
357
|
+
}
|
|
358
|
+
return { reset }
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const MIGRATION_KEY = 'dsh-vscode-mode.shortcuts-migrated.v1.'
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* 当前快捷键运行平台的迁移标记键;不同 Web/Desktop 和操作系统分别处理。
|
|
365
|
+
* @author ddj 2026年09月28号
|
|
366
|
+
* @param service 官方快捷键服务
|
|
367
|
+
* @returns 本机迁移标记键
|
|
368
|
+
*/
|
|
369
|
+
function migrationKey(service: ShortcutsServiceLike): string {
|
|
370
|
+
return MIGRATION_KEY + service.runtime + '.' + service.platform
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* 检查当前端是否已处理旧设置;存储不可用时继续按目录状态判定。
|
|
375
|
+
* @author ddj 2026年09月28号
|
|
376
|
+
* @param service 官方快捷键服务
|
|
377
|
+
* @returns 是否已处理
|
|
378
|
+
*/
|
|
379
|
+
function hasMigrated(service: ShortcutsServiceLike): boolean {
|
|
380
|
+
try {
|
|
381
|
+
return typeof localStorage !== 'undefined' && localStorage.getItem(migrationKey(service)) === '1'
|
|
382
|
+
} catch {
|
|
383
|
+
return false
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* 标记当前端已处理;旧设置永不清空,供其他平台及人工恢复使用。
|
|
389
|
+
* @author ddj 2026年09月28号
|
|
390
|
+
* @param service 官方快捷键服务
|
|
391
|
+
*/
|
|
392
|
+
function markMigrated(service: ShortcutsServiceLike): void {
|
|
393
|
+
try {
|
|
394
|
+
if (typeof localStorage !== 'undefined') localStorage.setItem(migrationKey(service), '1')
|
|
395
|
+
} catch {
|
|
396
|
+
bridgeLog.warn('无法持久化旧快捷键迁移标记,本次配置仍保留')
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* 旧 keybindings 设置按当前 runtime/platform 一次性导入官方存储:
|
|
402
|
+
* - 仅处理与 KEYBINDING_DEFAULTS 有差异的单候选键位;多候选、空键位、冲突或
|
|
403
|
+
* 官方不可表示项不迁移,完整旧值仍保留(官方每条命令当前只允许一个绑定);
|
|
404
|
+
* - 官方已有自定义值时不覆盖,stale 冲突也不自动重放;
|
|
405
|
+
* - 旧配置供其他平台及人工恢复使用,绝不清空;当前端成功处理后记录本地标记,
|
|
406
|
+
* 防止重启后把已由用户重置的官方键位重新导入。
|
|
407
|
+
* @author ddj 2026年10月
|
|
408
|
+
* @param service 官方服务
|
|
409
|
+
* @param scope 旧设置 scope(settings bridge 绑定产物)
|
|
410
|
+
* @returns 迁移结果
|
|
411
|
+
*/
|
|
412
|
+
export async function migrateLegacyKeybindings(service: ShortcutsServiceLike, scope: LegacySettingsScope): Promise<MigrationResult> {
|
|
413
|
+
const snapshot = scope.getSnapshot()
|
|
414
|
+
if (snapshot.status !== 'ready') return { imported: 0, skipped: 0, done: false }
|
|
415
|
+
if (service.config.getSnapshot().status !== 'ready') return { imported: 0, skipped: 0, done: false }
|
|
416
|
+
if (hasMigrated(service)) return { imported: 0, skipped: 0, done: true }
|
|
417
|
+
const stored = normalizeKeybindings(snapshot.value?.keybindings)
|
|
418
|
+
const overrides = Object.entries(stored).filter(([id, chord]) => chord !== KEYBINDING_DEFAULTS_OF[id])
|
|
419
|
+
if (overrides.length === 0) return { imported: 0, skipped: 0, done: true }
|
|
420
|
+
let imported = 0
|
|
421
|
+
let skipped = 0
|
|
422
|
+
for (const [id, chord] of overrides) {
|
|
423
|
+
if (!chord.trim() || chord.includes('|')) {
|
|
424
|
+
skipped += 1
|
|
425
|
+
bridgeLog.warn('迁移跳过(空键位或多候选,原值保留):' + id)
|
|
426
|
+
continue
|
|
427
|
+
}
|
|
428
|
+
const binding = chordToBinding(chord)
|
|
429
|
+
if (binding === null) {
|
|
430
|
+
skipped += 1
|
|
431
|
+
bridgeLog.warn('迁移跳过(官方不可表示):' + id + ' = ' + chord)
|
|
432
|
+
continue
|
|
433
|
+
}
|
|
434
|
+
if (service.catalog.getSnapshot().some((row) => row.id === id && row.modified)) {
|
|
435
|
+
skipped += 1
|
|
436
|
+
bridgeLog.warn('迁移跳过(官方存储已有自定义值):' + id)
|
|
437
|
+
continue
|
|
438
|
+
}
|
|
439
|
+
let probe: { conflicts: string[]; issue: string | null }
|
|
440
|
+
try {
|
|
441
|
+
probe = service.describeBinding(binding)
|
|
442
|
+
} catch (error) {
|
|
443
|
+
bridgeLog.warn('迁移中止(describeBinding 异常):' + String(error))
|
|
444
|
+
return { imported, skipped, done: false }
|
|
445
|
+
}
|
|
446
|
+
if (probe.conflicts.length > 0 || probe.issue !== null) {
|
|
447
|
+
skipped += 1
|
|
448
|
+
bridgeLog.warn('迁移跳过(与官方键冲突/保留):' + id + ' = ' + chord)
|
|
449
|
+
continue
|
|
450
|
+
}
|
|
451
|
+
const saved = await editOnce(service, { type: 'set', id, binding })
|
|
452
|
+
if (!saved) return { imported, skipped, done: false }
|
|
453
|
+
imported += 1
|
|
454
|
+
}
|
|
455
|
+
markMigrated(service)
|
|
456
|
+
bridgeLog.info('旧快捷键设置已处理(旧值保留):导入 ' + imported + ' 条,跳过 ' + skipped + ' 条')
|
|
457
|
+
return { imported, skipped, done: true }
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
//#endregion
|
|
@@ -152,6 +152,8 @@ function RuleRow(props) {
|
|
|
152
152
|
React.createElement('div', { className: 'edrv-rules-line1' },
|
|
153
153
|
React.createElement('span', { className: 'edrv-rules-file' }, rule.file),
|
|
154
154
|
!props.narrow ? React.createElement('span', { className: 'edrv-rules-path' }, '[' + rule.relHint + rule.file + ']') : null,
|
|
155
|
+
props.narrow && rule.scope === 'user' && rule.origin === 'legacy'
|
|
156
|
+
? React.createElement('span', { className: 'edrv-rules-path', title: rule.absPath }, '旧目录') : null,
|
|
155
157
|
!rule.enabled ? React.createElement('span', { className: 'edrv-rules-offtag' }, '已停用') : null),
|
|
156
158
|
React.createElement('div', { className: 'edrv-rules-sub' },
|
|
157
159
|
React.createElement('span', { className: 'edrv-rules-sublabel' }, '类型:'),
|
|
@@ -288,7 +290,7 @@ export function RulesPanel(props) {
|
|
|
288
290
|
if (scope === 'project' && !cwd) { notify('当前会话没有工作区,无法新建项目规则'); return }
|
|
289
291
|
const parsed = parseRuleFm(NEW_TEMPLATE)
|
|
290
292
|
setForm({
|
|
291
|
-
mode: 'new', scope, workspacePath: scope === 'project' ? cwd : undefined,
|
|
293
|
+
mode: 'new', scope, origin: scope === 'user' ? 'current' : undefined, workspacePath: scope === 'project' ? cwd : undefined,
|
|
292
294
|
relHint: scope === 'project' ? '.dsh/rules/' : 'rules/', file: '', content: NEW_TEMPLATE,
|
|
293
295
|
fmType: parsed.type, desc: parsed.description, globs: parsed.globs.join(', '), error: '', saving: false,
|
|
294
296
|
})
|
|
@@ -299,12 +301,12 @@ export function RulesPanel(props) {
|
|
|
299
301
|
* @param rule 目标规则
|
|
300
302
|
*/
|
|
301
303
|
const openEdit = (rule) => {
|
|
302
|
-
rpc('rules.read', { scope: rule.scope, workspacePath: rule.scope === 'project' ? rule.workspacePath : undefined, file: rule.file })
|
|
304
|
+
rpc('rules.read', { scope: rule.scope, origin: rule.origin, workspacePath: rule.scope === 'project' ? rule.workspacePath : undefined, file: rule.file })
|
|
303
305
|
.then((res) => {
|
|
304
306
|
if (!res || !res.ok) { notify(res?.error ?? '读取规则失败'); return }
|
|
305
307
|
const parsed = parseRuleFm(res.content)
|
|
306
308
|
setForm({
|
|
307
|
-
mode: 'edit', scope: rule.scope, workspacePath: rule.workspacePath, relHint: rule.relHint,
|
|
309
|
+
mode: 'edit', scope: rule.scope, origin: rule.origin, workspacePath: rule.workspacePath, relHint: rule.relHint,
|
|
308
310
|
file: rule.file, content: res.content,
|
|
309
311
|
fmType: parsed.type, desc: parsed.description, globs: parsed.globs.join(', '), error: '', saving: false,
|
|
310
312
|
})
|
|
@@ -342,7 +344,7 @@ export function RulesPanel(props) {
|
|
|
342
344
|
if (!file) { setForm({ ...next, saving: false, error: '文件名不能为空' }); return }
|
|
343
345
|
if (form.fmType === 'auto' && globList(form.globs).length === 0) { setForm({ ...next, saving: false, error: '自动规则需要至少一个 glob(逗号分隔)' }); return }
|
|
344
346
|
setForm(next)
|
|
345
|
-
rpc('rules.save', { scope: form.scope, workspacePath: form.workspacePath, file, content: form.content })
|
|
347
|
+
rpc('rules.save', { scope: form.scope, origin: form.origin, workspacePath: form.workspacePath, file, content: form.content })
|
|
346
348
|
.then((res) => {
|
|
347
349
|
if (res && res.ok) {
|
|
348
350
|
setForm(null)
|
|
@@ -357,8 +359,10 @@ export function RulesPanel(props) {
|
|
|
357
359
|
* @param rule 目标规则
|
|
358
360
|
*/
|
|
359
361
|
const removeRule = (rule) => {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
+
const reveal = rule.scope === 'user' && rule.origin !== 'legacy'
|
|
363
|
+
? '\n若旧目录有同名规则,删除后它将重新生效。' : ''
|
|
364
|
+
if (!window.confirm('删除规则 ' + rule.absPath + '?(不可恢复)' + reveal)) return
|
|
365
|
+
rpc('rules.remove', { scope: rule.scope, origin: rule.origin, workspacePath: rule.workspacePath, file: rule.file })
|
|
362
366
|
.then((res) => {
|
|
363
367
|
if (res && res.ok) { notify('已删除:' + rule.file); load() } else notify(res?.error ?? '删除失败')
|
|
364
368
|
}).catch((e) => notify('删除异常: ' + String(e)))
|
|
@@ -372,13 +376,13 @@ export function RulesPanel(props) {
|
|
|
372
376
|
const enabled = !rule.enabled
|
|
373
377
|
const applyLocal = (on) => setData((prev) => prev ? {
|
|
374
378
|
...prev,
|
|
375
|
-
user: prev.user.map((r) => (
|
|
379
|
+
user: prev.user.map((r) => (rule.scope === 'user' && r.file === rule.file && (r.origin ?? 'current') === (rule.origin ?? 'current') ? { ...r, enabled: on } : r)),
|
|
376
380
|
projects: prev.projects.map((p) => (p.workspacePath === rule.workspacePath
|
|
377
381
|
? { ...p, rules: p.rules.map((r) => (r.file === rule.file ? { ...r, enabled: on } : r)) }
|
|
378
382
|
: p)),
|
|
379
383
|
} : prev)
|
|
380
384
|
applyLocal(enabled)
|
|
381
|
-
rpc('rules.toggle', { scope: rule.scope, workspacePath: rule.workspacePath, file: rule.file, enabled })
|
|
385
|
+
rpc('rules.toggle', { scope: rule.scope, origin: rule.origin, workspacePath: rule.workspacePath, file: rule.file, enabled })
|
|
382
386
|
.then((res) => {
|
|
383
387
|
if (res && res.ok) { applyLocal(res.rule?.enabled === true); notify(res.rule?.enabled === false ? '已停用:' + rule.file : '已启用:' + rule.file) }
|
|
384
388
|
else { applyLocal(!enabled); notify(res?.error ?? '切换失败') }
|
|
@@ -411,7 +415,7 @@ export function RulesPanel(props) {
|
|
|
411
415
|
if (!activeProject()) return '当前工作区未注册为 DSH workspace,暂不能管理其项目规则'
|
|
412
416
|
return '<工作区>/.dsh/rules/ 还没有规则,点击「+ 新建规则」创建'
|
|
413
417
|
}
|
|
414
|
-
return '
|
|
418
|
+
return '还没有用户规则(当前 DSH_HOME/rules/ 或旧目录 ~/.dsh/rules/),点击「+ 新建规则」创建'
|
|
415
419
|
}
|
|
416
420
|
|
|
417
421
|
const renderBody = () => {
|
|
@@ -433,13 +437,13 @@ export function RulesPanel(props) {
|
|
|
433
437
|
onToggle: (rule) => toggleRule({ ...rule, workspacePath: rule.scope === 'project' ? cwd : undefined }),
|
|
434
438
|
}
|
|
435
439
|
return React.createElement('div', { className: 'edrv-rules-list' },
|
|
436
|
-
rows.map((rule) => React.createElement(RuleRow, { key: rule.scope + ':' + rule.file, rule, ...common })))
|
|
440
|
+
rows.map((rule) => React.createElement(RuleRow, { key: rule.scope + ':' + (rule.origin ?? 'project') + ':' + rule.file, rule, ...common })))
|
|
437
441
|
}
|
|
438
442
|
|
|
439
443
|
return React.createElement('div', { className: 'edrv-rules-panel', ref: panelRef },
|
|
440
444
|
renderTabs(),
|
|
441
445
|
renderWsLine(),
|
|
442
446
|
React.createElement('div', { className: 'edrv-rules-hint' },
|
|
443
|
-
tab === 'user' ? '
|
|
447
|
+
tab === 'user' ? '读取 DSH_HOME/rules/ 与 ~/.dsh/rules/;同名时当前目录优先,新建保存到当前目录' : '项目规则仅对所在工作区的会话生效(存放于 <工作区>/.dsh/rules/)'),
|
|
444
448
|
renderBody())
|
|
445
449
|
}
|
|
@@ -133,6 +133,13 @@ export function takePendingSideOpen(sessionId?: string): { path: string | null;
|
|
|
133
133
|
* @param focusDiff 是否聚焦首个差异
|
|
134
134
|
* @returns 是否路由成功
|
|
135
135
|
*/
|
|
136
|
+
/**
|
|
137
|
+
* Check the installed editor route without opening any UI.
|
|
138
|
+
* @author ddj 2026年09月28号
|
|
139
|
+
* @returns Whether a mounted sidebar adapter owns editor navigation.
|
|
140
|
+
*/
|
|
141
|
+
export function hasSideRoute(): boolean { return ensureSideEditor !== null }
|
|
142
|
+
|
|
136
143
|
export function routeSideEditor(path: string | null, focusDiff: boolean): boolean {
|
|
137
144
|
if (typeof ensureSideEditor !== 'function') return false
|
|
138
145
|
try {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* 作者 ddj 2026-08-20
|
|
6
6
|
*/
|
|
7
7
|
import React from 'react'
|
|
8
|
+
import { appUrl } from '../../shared/appUrl.js'
|
|
8
9
|
import { rpc } from '../rpc.js'
|
|
9
10
|
import { openEditorView, emitShowLauncher } from '../events.js'
|
|
10
11
|
import { summarize } from '../state/records.js'
|
|
@@ -48,6 +49,6 @@ export function DiffBadge(props) {
|
|
|
48
49
|
title: n + ' 个文件有差异,点击在编辑区查看',
|
|
49
50
|
style: { position: 'relative', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 30, height: 30, padding: 0, background: 'transparent', border: 'none', borderRadius: 6, cursor: 'pointer' },
|
|
50
51
|
},
|
|
51
|
-
React.createElement('img', { src: '/edrv/assets/compare-select.png', alt: '差异', style: { width: 22, height: 22, display: 'block' } }),
|
|
52
|
+
React.createElement('img', { src: appUrl('/edrv/assets/compare-select.png'), alt: '差异', style: { width: 22, height: 22, display: 'block' } }),
|
|
52
53
|
React.createElement('span', { style: { position: 'absolute', top: 1, right: 0, minWidth: 15, height: 15, padding: '0 3px', borderRadius: 8, background: 'var(--dsw-alias-state-warn-primary,#d97706)', color: '#fff', fontSize: 10, lineHeight: '15px', textAlign: 'center', fontWeight: 700, boxSizing: 'border-box' } }, String(n)))
|
|
53
54
|
}
|