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
package/src/client/index.ts
CHANGED
|
@@ -32,7 +32,7 @@ import { openEditorView } from './events.js'
|
|
|
32
32
|
import { setupExtOpen } from './externalOpen.js'
|
|
33
33
|
import { SettingsContext } from './settingsContext.js'
|
|
34
34
|
import { SIDEBAR_PLUGIN, registerSlotSafely, settingsBridge } from './compat.js'
|
|
35
|
-
import { detectSidebarService, installSideEditor, setEnsureSideEditor, SIDEBAR_INSTALL_CMD } from './sidebarBridge.js'
|
|
35
|
+
import { detectSidebarService, hasSideRoute, installSideEditor, routeSideEditor, setEnsureSideEditor, SIDEBAR_INSTALL_CMD } from './sidebarBridge.js'
|
|
36
36
|
import { detectOfficial, installOfficial, registerOfficialFileClaim, OFFICIAL_TAB_KIND, OFFICIAL_TAB_TITLE, isEditorTabActive, restoreEditorTab, buildFileAddress } from './officialSidebar.js'
|
|
37
37
|
import { setNativeCsv, setNativeOpenHandler, resetNativeOpen } from './nativeOpenStore.js'
|
|
38
38
|
import { SideEditorTab } from './ui/SideEditorTab.js'
|
|
@@ -50,9 +50,13 @@ import { createDefaultFileMenuItems } from './sidebar/menuItems.js'
|
|
|
50
50
|
import { createOutlinePanel } from './outline/index.js'
|
|
51
51
|
import { createOutlineSourceRegistry, registerBuiltinOutlineSources } from './outline/sources.js'
|
|
52
52
|
import { createLspOutlineSource } from './outline/lspSource.js'
|
|
53
|
-
import {
|
|
53
|
+
import { applyOfficialChords } from './keybindings.js'
|
|
54
54
|
import { createCommandBridge } from './commandBridge.js'
|
|
55
|
+
import type { CommandBridge } from './commandBridge.js'
|
|
55
56
|
import { REGISTRY_GLOBAL } from './commandGlobals.js'
|
|
57
|
+
import { awaitShortcutsService, bindCatalogChords, detectShortcuts, installLegacyKeys, migrateLegacyKeybindings, registerOfficialShortcuts } from './shortcutsOfficial.js'
|
|
58
|
+
import { SHORTCUT_PROFILES, defaultKeybindings, normalizeKeybindings } from '../shared/keybindings.js'
|
|
59
|
+
import { ShortcutSettings } from './ui/ShortcutSettings.js'
|
|
56
60
|
import { sidebarMinApply } from './sidebarMin.js'
|
|
57
61
|
import { editorLimitApply } from './editorLimit.js'
|
|
58
62
|
import { log } from './log.js'
|
|
@@ -71,31 +75,38 @@ import type { CompatAdapter } from '../shared/compat.js'
|
|
|
71
75
|
// 写死任一桥名都会在另一版本停等)——设置桥由 compat.pickSettingsBinder 运行时探测三级降级。
|
|
72
76
|
export const inject = ['slots', 'timer', 'locale', 'connection', 'remote', 'workspaces', 'sessions', 'conversation']
|
|
73
77
|
|
|
74
|
-
/**
|
|
75
|
-
let
|
|
78
|
+
/** 指令桥单例(同一 document 重复 apply 只装配一次;HMR 重挂不重复注册)。 */
|
|
79
|
+
let commandsBridge: CommandBridge | null = null
|
|
76
80
|
|
|
77
81
|
/**
|
|
78
82
|
* 装配指令注册表(指令桥 + 命令栏)。
|
|
79
83
|
* 指令桥不依赖已挂载编辑器(run 只派发窗口事件),故可在启动期装配;
|
|
80
84
|
* 命令栏浮层的宿主由 CommandPalette 自己在编辑区树内认领。
|
|
81
|
-
* @author ddj 2026年09月10号
|
|
85
|
+
* @author ddj 2026年09月10号 / 2026年10月
|
|
82
86
|
* @param ctx 客户端根上下文
|
|
83
|
-
* @returns
|
|
87
|
+
* @returns 指令桥(官方 shortcuts 注册的执行器来源)
|
|
84
88
|
*/
|
|
85
|
-
function setupCommands(ctx: any):
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
+
function setupCommands(ctx: any): CommandBridge {
|
|
90
|
+
if (commandsBridge) return commandsBridge
|
|
91
|
+
/** @author ddj 2026年09月28号 @returns Current session/workspace for command ownership. */
|
|
92
|
+
const readScope = () => readSessionScope(ctx)
|
|
93
|
+
/** @author ddj 2026年09月28号 @returns Whether an installed sidebar can mount the editor. */
|
|
94
|
+
const canOpen = () => hasSideRoute()
|
|
95
|
+
/** @author ddj 2026年09月28号 @returns Whether the current editor route accepted the request. */
|
|
96
|
+
const open = () => routeSideEditor(null, false)
|
|
97
|
+
const bridge = createCommandBridge({ readScope, canOpen, open })
|
|
98
|
+
commandsBridge = bridge
|
|
89
99
|
ctx.provide(REGISTRY_GLOBAL, bridge.registry)
|
|
90
100
|
// 无条件镜像到 window(DSH 不创建 window.dsh,旧条件式赋值是死代码 → 命令栏空表)
|
|
91
101
|
const host = window as unknown as Record<string, unknown>
|
|
92
102
|
host[REGISTRY_GLOBAL] = bridge.registry
|
|
93
103
|
ctx.effect(() => () => {
|
|
94
104
|
bridge.dispose()
|
|
95
|
-
|
|
105
|
+
commandsBridge = null
|
|
96
106
|
if (host[REGISTRY_GLOBAL] === bridge.registry) delete host[REGISTRY_GLOBAL]
|
|
97
107
|
}, 'vscode-mode: command registry')
|
|
98
108
|
log.info('指令系统已装配:' + bridge.registry.list().length + ' 条指令,Ctrl+Shift+P 打开命令栏')
|
|
109
|
+
return bridge
|
|
99
110
|
}
|
|
100
111
|
|
|
101
112
|
/**
|
|
@@ -131,14 +142,14 @@ export function apply(ctx: any): void {
|
|
|
131
142
|
}, 'vscode-mode: settings document editor route')
|
|
132
143
|
|
|
133
144
|
// 指令系统(命令栏 + 指令注册表)先装配:命令栏在被任何 React slot 渲染前即可唤起
|
|
134
|
-
setupCommands(ctx)
|
|
145
|
+
const commands = setupCommands(ctx)
|
|
135
146
|
|
|
136
147
|
const registry: FileOpenerRegistry = createFileOpenerRegistry()
|
|
137
148
|
const workspaces = ctx.get('workspaces')
|
|
138
149
|
const sessions = ctx.get('sessions')
|
|
139
150
|
|
|
140
151
|
// 外部深链落地:Windows 右键菜单 / Unity 外部编辑器 → 浏览器 URL 参数 → 打开规则路由
|
|
141
|
-
setupExtOpen(ctx)
|
|
152
|
+
ctx.effect(() => setupExtOpen(ctx), 'vscode-mode: external open')
|
|
142
153
|
|
|
143
154
|
// Monaco 加载时机:不再 DSH 启动即预热,改为进入会话界面(当前会话出现)后再后台加载,
|
|
144
155
|
// 用户点开「文件编辑」页签即用;空闲时仍由 EditorView 挂载兜底加载。
|
|
@@ -253,7 +264,7 @@ export function apply(ctx: any): void {
|
|
|
253
264
|
return sidebar ? registry.register(sidebar) : undefined
|
|
254
265
|
}, 'vscode-mode: sidebar file opener')
|
|
255
266
|
// 官方侧边栏正文组件装配(页类型正文;兼作 file 认领转发失败时的兜底正文)
|
|
256
|
-
const officialRenderTab = (props: Record<string, unknown>) => React.createElement(OfficialSideTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, fileOpeners: registry, sessions }))
|
|
267
|
+
const officialRenderTab = (props: Record<string, unknown>) => React.createElement(OfficialSideTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, fileOpeners: registry, sessions, readSessionScope: () => readSessionScope(ctx) }))
|
|
257
268
|
/** 官方 file 地址认领同步:自动/VSCodeMode 档认领(转发进单一编辑器页签),其余交官方查看器。 */
|
|
258
269
|
const syncFileClaim = (): void => {
|
|
259
270
|
const official = officialService
|
|
@@ -270,6 +281,17 @@ export function apply(ctx: any): void {
|
|
|
270
281
|
claimDisposer = null
|
|
271
282
|
}
|
|
272
283
|
}
|
|
284
|
+
const legacyDefaults = defaultKeybindings()
|
|
285
|
+
let officialReady = false
|
|
286
|
+
/**
|
|
287
|
+
* 获取旧版快捷键覆盖;设置桥尚未就绪时回退默认键位。
|
|
288
|
+
* @author ddj 2026年09月28号
|
|
289
|
+
* @returns 旧版键位弦表
|
|
290
|
+
*/
|
|
291
|
+
const legacyChords = (): Record<string, string> => ({
|
|
292
|
+
...legacyDefaults, ...normalizeKeybindings(bridge.scope()?.getSnapshot().value?.keybindings),
|
|
293
|
+
})
|
|
294
|
+
applyOfficialChords(legacyChords())
|
|
273
295
|
// 设置同步(fileOpenTool / 快捷键 / 侧宽 / 页签上限):桥可能晚于本插件 apply 就绪
|
|
274
296
|
// (设置桥服务不进 inject),首次命中即挂两条 effect;晚到经 bridge.whenReady 重挂一次
|
|
275
297
|
// (settingsBridge 15×2s 有界重试内命中才会触发,用尽后保持旧行为不轮询)。
|
|
@@ -290,14 +312,14 @@ export function apply(ctx: any): void {
|
|
|
290
312
|
sync()
|
|
291
313
|
return scope.subscribe(sync)
|
|
292
314
|
}, 'vscode-mode: file opener setting sync')
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
315
|
+
// 界面偏好同步:侧边栏最小宽度(sidebarMinWidth)→ 派发 edrv:sidebar-min-width 通知编辑器重夹
|
|
316
|
+
// 页签上限(maxOpenEditors)→ 派发 edrv:max-open-editors 通知编辑器复算淘汰
|
|
317
|
+
// (快捷键配置已归官方 shortcuts 机制,目录弦表同步见 'vscode-mode: official shortcuts' effect)
|
|
296
318
|
ctx.effect(() => {
|
|
297
319
|
const sync = (): void => {
|
|
298
320
|
const snapshot = scope.getSnapshot()
|
|
299
321
|
if (snapshot.status === 'loading') return
|
|
300
|
-
|
|
322
|
+
if (!officialReady) applyOfficialChords(legacyChords())
|
|
301
323
|
const minW = sidebarMinApply(snapshot.value?.sidebarMinWidth)
|
|
302
324
|
window.dispatchEvent(new CustomEvent('edrv:sidebar-min-width', { detail: { value: minW } }))
|
|
303
325
|
const limit = editorLimitApply(snapshot.value?.maxOpenEditors)
|
|
@@ -311,6 +333,58 @@ export function apply(ctx: any): void {
|
|
|
311
333
|
}
|
|
312
334
|
mountSettingsSyncs()
|
|
313
335
|
if (!settingsSyncMounted) ctx.effect(() => bridge.whenReady(mountSettingsSyncs), 'vscode-mode: settings bridge wait')
|
|
336
|
+
|
|
337
|
+
// 官方 shortcuts 机制装配(可选服务,有界重试;服务缺失仅降级告警,不阻断装配):
|
|
338
|
+
// ① 全部指令注册进官方目录(含无默认键位命令,官方 UI 可自定义绑定;冲突默认自动降级未绑定);
|
|
339
|
+
// ② 订阅官方目录 → 弦表同步(chordOf/bindingsOf 数据源,tooltip/命令栏提示跟随);
|
|
340
|
+
// ③ 旧 keybindings 设置一次性迁移进官方存储(幂等,与设置桥双就绪后有界重试)。
|
|
341
|
+
ctx.effect(() => {
|
|
342
|
+
let cancelled = false
|
|
343
|
+
let disposeRegistration: (() => void) | null = null
|
|
344
|
+
let disposeCatalog: (() => void) | null = null
|
|
345
|
+
const disposeLegacy = installLegacyKeys(commands.registry.list(), commands.registry, legacyChords)
|
|
346
|
+
const cancelProbe = awaitShortcutsService(ctx, {
|
|
347
|
+
schedule,
|
|
348
|
+
onReady: (service) => {
|
|
349
|
+
if (cancelled) return
|
|
350
|
+
if (service === null) {
|
|
351
|
+
log.warn('官方 shortcuts 服务不可用(DSH 版本过旧?),继续使用旧版快捷键派发')
|
|
352
|
+
return
|
|
353
|
+
}
|
|
354
|
+
disposeLegacy()
|
|
355
|
+
officialReady = true
|
|
356
|
+
disposeRegistration = registerOfficialShortcuts(service, commands.registry.list(), commands.registry, SHORTCUT_PROFILES)
|
|
357
|
+
disposeCatalog = bindCatalogChords(service, applyOfficialChords)
|
|
358
|
+
let attempts = 0
|
|
359
|
+
const attemptMigration = (): void => {
|
|
360
|
+
if (cancelled) return
|
|
361
|
+
const scope = bridge.scope()
|
|
362
|
+
if (!scope) {
|
|
363
|
+
if (attempts < 15) {
|
|
364
|
+
attempts += 1
|
|
365
|
+
schedule(attemptMigration, 2000)
|
|
366
|
+
}
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
void migrateLegacyKeybindings(service, scope).then((result) => {
|
|
370
|
+
if (cancelled || result.done) return
|
|
371
|
+
if (attempts < 15) {
|
|
372
|
+
attempts += 1
|
|
373
|
+
schedule(attemptMigration, 2000)
|
|
374
|
+
}
|
|
375
|
+
})
|
|
376
|
+
}
|
|
377
|
+
attemptMigration()
|
|
378
|
+
},
|
|
379
|
+
})
|
|
380
|
+
return () => {
|
|
381
|
+
cancelled = true
|
|
382
|
+
cancelProbe()
|
|
383
|
+
disposeLegacy()
|
|
384
|
+
disposeRegistration?.()
|
|
385
|
+
disposeCatalog?.()
|
|
386
|
+
}
|
|
387
|
+
}, 'vscode-mode: official shortcuts')
|
|
314
388
|
if (workspaces?.openPath) {
|
|
315
389
|
ctx.effect(() => installOpenPathRouter({
|
|
316
390
|
workspaces,
|
|
@@ -503,6 +577,15 @@ export function apply(ctx: any): void {
|
|
|
503
577
|
label: 'VSCodeMode',
|
|
504
578
|
}, () => React.createElement(SettingsContext.Provider, { value: bridge.scope() }, React.createElement(McpSettings, { openerRegistry: registry, compatSummary })))
|
|
505
579
|
|
|
580
|
+
// 独立「快捷键」设置区(与 VSCodeMode 平级):官方快捷键子页 + 插件快捷键子页。
|
|
581
|
+
// 服务经 getShortcuts 运行时探测(自愈),组件内部自带就绪重试。
|
|
582
|
+
registerSlotSafely(ctx, {
|
|
583
|
+
name: 'settings.section',
|
|
584
|
+
id: 'vscode-mode-shortcuts',
|
|
585
|
+
order: 31,
|
|
586
|
+
label: '快捷键',
|
|
587
|
+
}, () => React.createElement(ShortcutSettings, { getShortcuts: () => detectShortcuts(ctx) }))
|
|
588
|
+
|
|
506
589
|
// 卸载收尾(G4,DSH 0.1.6-alpha.2 起支持运行时卸载/重载):
|
|
507
590
|
// 注销挂在存活的 window.monaco 上的全部 Monaco provider 并复位模块状态。
|
|
508
591
|
// 不做会导致重载后重复注册(补全/跳转/hover 各翻倍)与陈旧开关残留。
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-vscode-mode client —
|
|
2
|
+
* dsh-vscode-mode client — 快捷键配置模块(解析/匹配/官方目录同步)。
|
|
3
3
|
* 纯逻辑(parseChord/parseChords/formatChord/matchEvent/chordFromEvent/normalizeKey)不依赖 DOM,可单测;
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* 作者 ddj 2026年08月26号 / 2026年09月10号
|
|
4
|
+
* 弦表(current)由官方 shortcuts 目录驱动:client/index.ts 经 shortcutsOfficial.bindCatalogChords
|
|
5
|
+
* 订阅官方 catalog 后调 applyOfficialChords(键位持久化/冲突校验/派发全部归官方机制,本模块不再自派发)。
|
|
6
|
+
* 键位语义:Ctrl 与 Cmd 互认(延续历史捕获行为)。
|
|
7
|
+
* 命令目录(COMMANDS)派生自指令目录 commandCatalog(指令 = 单一数据源)。
|
|
8
|
+
* 作者 ddj 2026年08月26号 / 2026年09月10号 / 2026年10月
|
|
9
9
|
*/
|
|
10
10
|
import React from 'react'
|
|
11
|
-
import {
|
|
12
|
-
import { BRIDGE_COMMANDS, EDITOR_COMMANDS, showCommandsDef } from './ui/commandCatalog.js'
|
|
13
|
-
import { log } from './log.js'
|
|
11
|
+
import { BRIDGE_COMMANDS, EDITOR_COMMANDS } from './ui/commandCatalog.js'
|
|
14
12
|
|
|
15
13
|
/** 解析后的键位(修饰符 + 规范化主键)。 */
|
|
16
14
|
export interface Binding {
|
|
@@ -21,52 +19,45 @@ export interface Binding {
|
|
|
21
19
|
key: string
|
|
22
20
|
}
|
|
23
21
|
|
|
24
|
-
/**
|
|
22
|
+
/** 命令目录(设置页展示标签;命令栏候选与键位提示共用)。 */
|
|
25
23
|
export const COMMANDS: Array<{ id: string; label: string }> = [
|
|
26
24
|
...EDITOR_COMMANDS.map((command) => ({ id: command.id, label: command.label })),
|
|
27
25
|
{ id: 'edrv.showCommands', label: '显示所有命令' },
|
|
28
26
|
...BRIDGE_COMMANDS.map((command) => ({ id: command.id, label: command.label })),
|
|
29
27
|
]
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return showCommandsDef(() => {})
|
|
29
|
+
const MODIFIERS: Record<string, 'ctrl' | 'shift' | 'alt' | 'meta'> = {
|
|
30
|
+
ctrl: 'ctrl', cmd: 'meta', meta: 'meta', shift: 'shift', alt: 'alt',
|
|
34
31
|
}
|
|
35
32
|
|
|
33
|
+
/** 弦表(官方 catalog 派生:id → 显示键位弦)。官方目录就绪前为空(tooltip 走各自的兜底文案)。 */
|
|
34
|
+
let current: Record<string, string> = {}
|
|
35
|
+
let currentAria: Record<string, string | undefined> = {}
|
|
36
|
+
|
|
36
37
|
/**
|
|
37
|
-
*
|
|
38
|
-
* @author ddj 2026年09月
|
|
38
|
+
* Read official accessibility metadata without deriving it from visual keycaps.
|
|
39
|
+
* @author ddj 2026年09月28号
|
|
40
|
+
* @param id Command identifier.
|
|
41
|
+
* @returns Official single-key ARIA shortcut, absent for two-key chords.
|
|
39
42
|
*/
|
|
40
|
-
function
|
|
41
|
-
for (const command of [...EDITOR_COMMANDS, paletteCommand()]) {
|
|
42
|
-
if (!command.keybinding) continue
|
|
43
|
-
if (KEYBINDING_DEFAULTS[command.id] === command.keybinding) continue
|
|
44
|
-
log.warn('指令默认键位与共享表不一致:' + command.id
|
|
45
|
-
+ ' 目录=' + command.keybinding + ' 共享表=' + String(KEYBINDING_DEFAULTS[command.id]))
|
|
46
|
-
}
|
|
47
|
-
}
|
|
43
|
+
export function ariaOf(id: string): string | undefined { return currentAria[id] }
|
|
48
44
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Replace accessibility metadata from the official catalog.
|
|
47
|
+
* @author ddj 2026年09月28号
|
|
48
|
+
* @param aria Complete catalog ARIA map, applied before display notification.
|
|
49
|
+
*/
|
|
50
|
+
export function applyKeyAria(aria: Record<string, string | undefined>): void { currentAria = { ...aria } }
|
|
55
51
|
const listeners = new Set<() => void>()
|
|
56
52
|
|
|
57
|
-
checkDefaultsDrift()
|
|
58
|
-
|
|
59
53
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* @
|
|
63
|
-
* @param raw 设置 scope 的 keybindings 字段
|
|
54
|
+
* 应用官方目录弦表(id → 显示键位弦;由 shortcutsOfficial.bindCatalogChords 全量推送)。
|
|
55
|
+
* @author ddj 2026年10月
|
|
56
|
+
* @param chords 弦表(官方 catalog 派生,含用户覆盖;issue 行不收录)
|
|
64
57
|
*/
|
|
65
|
-
export function
|
|
66
|
-
current = { ...
|
|
67
|
-
|
|
68
|
-
try { listener() } catch { /* 监听器异常不影响其他订阅 */ }
|
|
69
|
-
}
|
|
58
|
+
export function applyOfficialChords(chords: Readonly<Record<string, string>>): void {
|
|
59
|
+
current = { ...chords }
|
|
60
|
+
notifyKeybindings()
|
|
70
61
|
}
|
|
71
62
|
|
|
72
63
|
/**
|
|
@@ -80,29 +71,6 @@ export function subscribeKeybindings(listener: () => void): () => void {
|
|
|
80
71
|
return () => listeners.delete(listener)
|
|
81
72
|
}
|
|
82
73
|
|
|
83
|
-
/**
|
|
84
|
-
* 注册运行时键位(第三方命令;优先于设置值,注销即失效)。
|
|
85
|
-
* @author ddj 2026年09月10号
|
|
86
|
-
* @param id 命令 id
|
|
87
|
-
* @param chord 键位弦(空/非法按未绑定处理)
|
|
88
|
-
* @returns 注销函数(幂等)
|
|
89
|
-
*/
|
|
90
|
-
export function addRuntimeKeybinding(id: string, chord: string): () => void {
|
|
91
|
-
runtime.set(id, parseChords(chord))
|
|
92
|
-
notifyKeybindings()
|
|
93
|
-
return () => removeRuntimeKeybinding(id)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* 移除运行时键位。
|
|
98
|
-
* @author ddj 2026年09月10号
|
|
99
|
-
* @param id 命令 id
|
|
100
|
-
*/
|
|
101
|
-
export function removeRuntimeKeybinding(id: string): void {
|
|
102
|
-
if (!runtime.delete(id)) return
|
|
103
|
-
notifyKeybindings()
|
|
104
|
-
}
|
|
105
|
-
|
|
106
74
|
/** 通知键位订阅者(异常隔离)。 */
|
|
107
75
|
function notifyKeybindings(): void {
|
|
108
76
|
for (const listener of listeners) {
|
|
@@ -111,29 +79,23 @@ function notifyKeybindings(): void {
|
|
|
111
79
|
}
|
|
112
80
|
|
|
113
81
|
/**
|
|
114
|
-
*
|
|
115
|
-
* @author ddj 2026年08月26号 / 2026年
|
|
82
|
+
* 当前命令的键位弦(官方目录派生;未绑定/空 → null)。
|
|
83
|
+
* @author ddj 2026年08月26号 / 2026年10月
|
|
116
84
|
* @param id 命令 id
|
|
117
85
|
* @returns 键位弦或 null
|
|
118
86
|
*/
|
|
119
87
|
export function chordOf(id: string): string | null {
|
|
120
|
-
if (runtime.has(id)) {
|
|
121
|
-
const chords = runtime.get(id) ?? []
|
|
122
|
-
return chords.length ? chords.map(formatChord).join('|') : null
|
|
123
|
-
}
|
|
124
88
|
const chord = current[id]
|
|
125
89
|
return typeof chord === 'string' && chord.trim() !== '' ? chord : null
|
|
126
90
|
}
|
|
127
91
|
|
|
128
92
|
/**
|
|
129
93
|
* 当前命令的解析键位集合(未绑定/非法 → 空数组;含多候选)。
|
|
130
|
-
* @author ddj 2026年08月26号 / 2026年
|
|
94
|
+
* @author ddj 2026年08月26号 / 2026年10月
|
|
131
95
|
* @param id 命令 id
|
|
132
96
|
* @returns 解析键位数组(可能为空)
|
|
133
97
|
*/
|
|
134
98
|
export function bindingsOf(id: string): Binding[] {
|
|
135
|
-
const override = runtime.get(id)
|
|
136
|
-
if (override) return override
|
|
137
99
|
const chord = current[id]
|
|
138
100
|
return typeof chord === 'string' ? parseChords(chord) : []
|
|
139
101
|
}
|
|
@@ -216,7 +178,8 @@ export function formatChord(binding: Binding): string {
|
|
|
216
178
|
* @param bindings 解析键位或键位数组(null/空 = 未绑定,永不命中)
|
|
217
179
|
* @returns 是否命中
|
|
218
180
|
*/
|
|
219
|
-
export function matchEvent(e: { ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean; altKey?: boolean; key?: string }, bindings: Binding | Binding[] | null): boolean {
|
|
181
|
+
export function matchEvent(e: { ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean; altKey?: boolean; key?: string; isComposing?: boolean; keyCode?: number; defaultPrevented?: boolean; getModifierState?: (key: string) => boolean }, bindings: Binding | Binding[] | null): boolean {
|
|
182
|
+
if (e.defaultPrevented || e.isComposing || e.keyCode === 229 || e.key === 'Dead' || e.getModifierState?.('AltGraph')) return false
|
|
220
183
|
const list = Array.isArray(bindings) ? bindings : bindings ? [bindings] : []
|
|
221
184
|
for (const binding of list) {
|
|
222
185
|
if (Boolean(e.ctrlKey || e.metaKey) !== (binding.ctrl || binding.meta)) continue
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { applyOfficial, registerThemes } from './theme.js'
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
import { appUrl } from '../../shared/appUrl.js'
|
|
10
|
+
|
|
11
|
+
export const MONACO_BASE = appUrl('/edrv/vendor/monaco/vs')
|
|
10
12
|
let monacoPromise = null
|
|
11
13
|
let monacoStage = { phase: 'idle', progress: 0, message: '准备加载 Monaco…' }
|
|
12
14
|
const stageListeners = new Set()
|
package/src/client/openFlow.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { rpc } from './rpc.js'
|
|
10
10
|
import { chooseWorkspace } from './chooseDialog.js'
|
|
11
|
-
import {
|
|
11
|
+
import { requestOpen } from './openReceipt.js'
|
|
12
12
|
import { toastDom } from './externalOpen.js'
|
|
13
13
|
import { createAddToConversation } from './addToConversation.js'
|
|
14
14
|
import type { AddOutcome, CtxLike } from './addToConversation.js'
|
|
@@ -47,7 +47,7 @@ export interface OpenDeps {
|
|
|
47
47
|
openSession(id: string): void
|
|
48
48
|
reference(sessionId: string, path: string, appearance: 'file' | 'folder'): Promise<boolean>
|
|
49
49
|
choose(title: string, folder: string): Promise<'recent' | 'create' | null>
|
|
50
|
-
openEditor(path: string | null, line?: number, column?: number):
|
|
50
|
+
openEditor(path: string | null, line?: number, column?: number, sessionId?: string): Promise<boolean>
|
|
51
51
|
schedule(fn: () => void, ms: number): void
|
|
52
52
|
notify(text: string): void
|
|
53
53
|
}
|
|
@@ -57,8 +57,6 @@ const READY_TIMEOUT_MS = 15000
|
|
|
57
57
|
const READY_TICK_MS = 300
|
|
58
58
|
const REF_RETRY_MAX = 20
|
|
59
59
|
const REF_RETRY_MS = 300
|
|
60
|
-
const REST_STEP_MS = 150
|
|
61
|
-
const LINE_AT_DELAY_MS = 200
|
|
62
60
|
|
|
63
61
|
/**
|
|
64
62
|
* 路径规范化:`\→/`、小写、去尾分隔符(Windows 大小写不敏感前缀匹配用)。
|
|
@@ -124,11 +122,11 @@ export function pickRule(kind: PathKind, matched: boolean, hasSession: boolean):
|
|
|
124
122
|
* @param params 深链参数(首路径驱动规则)
|
|
125
123
|
* @param overrides 依赖覆盖(测试注入)
|
|
126
124
|
*/
|
|
127
|
-
export async function openDeepLink(ctx: unknown, params: OpenParams, overrides: Partial<OpenDeps> = {}): Promise<
|
|
125
|
+
export async function openDeepLink(ctx: unknown, params: OpenParams, overrides: Partial<OpenDeps> = {}): Promise<boolean> {
|
|
128
126
|
const deps = realDeps(ctx, overrides)
|
|
129
127
|
if (!(await waitListsReady(deps))) {
|
|
130
128
|
deps.notify('DSH 会话列表未就绪,请稍后重试')
|
|
131
|
-
return
|
|
129
|
+
return false
|
|
132
130
|
}
|
|
133
131
|
const primary = params.paths[0]
|
|
134
132
|
const kind = await deps.stat(primary).catch(() => 'missing' as PathKind)
|
|
@@ -137,14 +135,16 @@ export async function openDeepLink(ctx: unknown, params: OpenParams, overrides:
|
|
|
137
135
|
try {
|
|
138
136
|
if (rule === 'abort') {
|
|
139
137
|
deps.notify('路径不存在:' + primary)
|
|
140
|
-
return
|
|
138
|
+
return false
|
|
141
139
|
}
|
|
142
|
-
if (rule === 'choose')
|
|
143
|
-
if (rule === 'folderNew')
|
|
144
|
-
if (rule === 'fileRecent')
|
|
145
|
-
|
|
140
|
+
if (rule === 'choose') await runChoose(deps, primary, params)
|
|
141
|
+
else if (rule === 'folderNew') await runFolderNew(deps, primary, params)
|
|
142
|
+
else if (rule === 'fileRecent') await runFileRecent(deps, primary, params)
|
|
143
|
+
else await runFileBootstrap(deps, primary, params)
|
|
144
|
+
return true
|
|
146
145
|
} catch (error) {
|
|
147
146
|
deps.notify('打开失败:' + String((error as Error)?.message ?? error))
|
|
147
|
+
return false
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -162,12 +162,12 @@ async function runChoose(deps: OpenDeps, folder: string, params: OpenParams): Pr
|
|
|
162
162
|
const matches = matchWorkspaces(deps.workspaces().items, folder)
|
|
163
163
|
const best = bestMatch(deps, matches)
|
|
164
164
|
const choice = await deps.choose(best?.title ?? '工作区', folder)
|
|
165
|
-
if (choice === null)
|
|
165
|
+
if (choice === null) throw new Error('用户取消打开')
|
|
166
166
|
if (choice === 'create') return runFolderNew(deps, folder, params)
|
|
167
167
|
const ws = best ?? deps.workspaces().items[0]
|
|
168
168
|
const sessionId = await spawnInWorkspace(deps, ws.workspaceId, undefined)
|
|
169
169
|
await settleRef(deps, sessionId, folder, 'folder')
|
|
170
|
-
deps
|
|
170
|
+
await openChecked(deps, sessionId, null)
|
|
171
171
|
await openRest(deps, sessionId, params, 1)
|
|
172
172
|
}
|
|
173
173
|
|
|
@@ -178,21 +178,21 @@ async function runFileRecent(deps: OpenDeps, file: string, params: OpenParams):
|
|
|
178
178
|
const anchor = recentSessionIn(deps, ws)
|
|
179
179
|
if (anchor) {
|
|
180
180
|
deps.openSession(anchor.id)
|
|
181
|
-
deps.
|
|
181
|
+
await openChecked(deps, anchor.id, file, params.line, params.column)
|
|
182
182
|
await openRest(deps, anchor.id, params, 1)
|
|
183
183
|
return
|
|
184
184
|
}
|
|
185
185
|
// 文件所属工作区存在但无会话 → 在该工作区新建对话 + 文件引用
|
|
186
186
|
const sessionId = await spawnInWorkspace(deps, ws.workspaceId, undefined)
|
|
187
187
|
await settleRef(deps, sessionId, file, 'file')
|
|
188
|
-
deps
|
|
188
|
+
await openChecked(deps, sessionId, file, params.line, params.column)
|
|
189
189
|
await openRest(deps, sessionId, params, 1)
|
|
190
190
|
return
|
|
191
191
|
}
|
|
192
192
|
const anchor = recentSession(deps.sessions().byId, deps.sessions().ids)
|
|
193
193
|
if (!anchor) return runFileBootstrap(deps, file, params)
|
|
194
194
|
deps.openSession(anchor.id)
|
|
195
|
-
deps.
|
|
195
|
+
await openChecked(deps, anchor.id, file, params.line, params.column)
|
|
196
196
|
await openRest(deps, anchor.id, params, 1)
|
|
197
197
|
}
|
|
198
198
|
|
|
@@ -228,7 +228,7 @@ async function runFileBootstrap(deps: OpenDeps, file: string, params: OpenParams
|
|
|
228
228
|
const ws = await deps.createWorkspace(dirnameOf(file))
|
|
229
229
|
const sessionId = await spawnInWorkspace(deps, ws.workspaceId, undefined)
|
|
230
230
|
await settleRef(deps, sessionId, file, 'file')
|
|
231
|
-
deps
|
|
231
|
+
await openChecked(deps, sessionId, file, params.line, params.column)
|
|
232
232
|
await openRest(deps, sessionId, params, 1)
|
|
233
233
|
}
|
|
234
234
|
|
|
@@ -237,7 +237,7 @@ async function runFolderNew(deps: OpenDeps, folder: string, params: OpenParams):
|
|
|
237
237
|
const ws = await deps.createWorkspace(folder)
|
|
238
238
|
const sessionId = await spawnInWorkspace(deps, ws.workspaceId, undefined)
|
|
239
239
|
await settleRef(deps, sessionId, folder, 'folder')
|
|
240
|
-
deps
|
|
240
|
+
await openChecked(deps, sessionId, null)
|
|
241
241
|
await openRest(deps, sessionId, params, 1)
|
|
242
242
|
}
|
|
243
243
|
|
|
@@ -264,17 +264,20 @@ async function settleRef(deps: OpenDeps, sessionId: string, path: string, appear
|
|
|
264
264
|
if (await deps.reference(sessionId, path, appearance)) return
|
|
265
265
|
await sleep(deps, REF_RETRY_MS)
|
|
266
266
|
}
|
|
267
|
+
throw new Error('引用插入未完成')
|
|
267
268
|
}
|
|
268
269
|
|
|
269
270
|
/** 其余路径:文件进编辑器(步进),文件夹补文件夹引用。 */
|
|
270
271
|
async function openRest(deps: OpenDeps, sessionId: string | null, params: OpenParams, from: number): Promise<void> {
|
|
271
272
|
const rest = params.paths.slice(from)
|
|
272
|
-
for (const
|
|
273
|
+
for (const path of rest) {
|
|
273
274
|
const kind = await deps.stat(path).catch(() => 'missing' as PathKind)
|
|
274
275
|
if (kind === 'file') {
|
|
275
|
-
|
|
276
|
+
await openChecked(deps, sessionId ?? undefined, path)
|
|
276
277
|
} else if (kind === 'directory' && sessionId) {
|
|
277
278
|
await settleRef(deps, sessionId, path, 'folder')
|
|
279
|
+
} else {
|
|
280
|
+
throw new Error('路径不存在:' + path)
|
|
278
281
|
}
|
|
279
282
|
}
|
|
280
283
|
}
|
|
@@ -316,16 +319,21 @@ function realDeps(ctx: unknown, overrides: Partial<OpenDeps> = {}): OpenDeps {
|
|
|
316
319
|
return outcome === 'ok' || outcome === 'busy'
|
|
317
320
|
},
|
|
318
321
|
choose: (title, folder) => chooseWorkspace(title, folder),
|
|
319
|
-
openEditor:
|
|
320
|
-
if (path == null) {
|
|
321
|
-
openEditorView(null)
|
|
322
|
-
return
|
|
323
|
-
}
|
|
324
|
-
openEditorView(path)
|
|
325
|
-
if (line != null) setTimeout(() => emitOpenAt(path, line, column), LINE_AT_DELAY_MS)
|
|
326
|
-
},
|
|
322
|
+
openEditor: requestOpen,
|
|
327
323
|
schedule: (fn, ms) => setTimeout(fn, ms),
|
|
328
324
|
notify: (text) => toastDom(text),
|
|
329
325
|
}
|
|
330
326
|
return { ...base, ...overrides }
|
|
331
327
|
}
|
|
328
|
+
|
|
329
|
+
/** @private @author ddj 2026年09月28号
|
|
330
|
+
* Await actual editor completion and propagate failure to the request result.
|
|
331
|
+
* @param deps Open dependencies.
|
|
332
|
+
* @param sessionId Target session.
|
|
333
|
+
* @param path Target file or editor mount.
|
|
334
|
+
* @param line Optional line.
|
|
335
|
+
* @param column Optional column.
|
|
336
|
+
*/
|
|
337
|
+
async function openChecked(deps: OpenDeps, sessionId: string | undefined, path: string | null, line?: number, column?: number): Promise<void> {
|
|
338
|
+
if (!(await deps.openEditor(path, line, column, sessionId))) throw new Error('编辑器打开未完成:' + (path ?? '工作区'))
|
|
339
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { openEditorView } from './events.js'
|
|
2
|
+
|
|
3
|
+
export interface EditorRequest { requestId: string; sessionId: string; path: string | null; line?: number; column?: number }
|
|
4
|
+
interface Waiter { request: EditorRequest; finish: (success: boolean) => void; claimed?: boolean }
|
|
5
|
+
const waiters = new Map<string, Waiter>()
|
|
6
|
+
|
|
7
|
+
/** @public @author ddj 2026年09月28号
|
|
8
|
+
* Atomically claim the pending request for this editor's session on mount.
|
|
9
|
+
* @param sessionId Owning session.
|
|
10
|
+
* @returns Unclaimed request, if any.
|
|
11
|
+
*/
|
|
12
|
+
export function peekOpen(sessionId: string): EditorRequest | undefined {
|
|
13
|
+
const row = [...waiters.values()].find((item) => item.request.sessionId === sessionId && !item.claimed)
|
|
14
|
+
if (!row) return undefined
|
|
15
|
+
row.claimed = true
|
|
16
|
+
return row.request
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @public @author ddj 2026年09月28号
|
|
20
|
+
* Complete an actual editor open by its unguessable local request ID.
|
|
21
|
+
* @param requestId Claimed request identity.
|
|
22
|
+
* @param success Whether content and navigation are ready.
|
|
23
|
+
* @param error Optional diagnostic for the caller.
|
|
24
|
+
*/
|
|
25
|
+
export function finishOpen(requestId: string, success: boolean, error?: string): void {
|
|
26
|
+
const row = waiters.get(requestId)
|
|
27
|
+
if (row?.claimed) row.finish(success === true)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @public @author ddj 2026年09月28号
|
|
31
|
+
* Route an editor open and await a correlated model-ready completion.
|
|
32
|
+
* @param path File path, or null to mount the editor.
|
|
33
|
+
* @param line Optional target line.
|
|
34
|
+
* @param column Optional target column.
|
|
35
|
+
* @param sessionId Owning session, required for routing.
|
|
36
|
+
* @returns Actual completion; timeout and disposal are false.
|
|
37
|
+
*/
|
|
38
|
+
export function requestOpen(path: string | null, line?: number, column?: number, sessionId = ''): Promise<boolean> {
|
|
39
|
+
if (typeof window === 'undefined' || !sessionId) return Promise.resolve(false)
|
|
40
|
+
const request: EditorRequest = { requestId: crypto.randomUUID(), sessionId, path, line, column }
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
let timer: ReturnType<typeof setTimeout>
|
|
43
|
+
/** @private @author ddj 2026年09月28号 Complete once and release the deadline. */
|
|
44
|
+
function finish(success: boolean): void {
|
|
45
|
+
clearTimeout(timer)
|
|
46
|
+
waiters.delete(request.requestId)
|
|
47
|
+
resolve(success)
|
|
48
|
+
}
|
|
49
|
+
waiters.set(request.requestId, { request, finish })
|
|
50
|
+
timer = setTimeout(() => finish(false), 20_000)
|
|
51
|
+
openEditorView(null)
|
|
52
|
+
window.dispatchEvent(new CustomEvent('edrv:open-request', { detail: request }))
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @public @author ddj 2026年09月28号
|
|
57
|
+
* Check that an editor receipt is still awaited after timeout or disposal.
|
|
58
|
+
* @param requestId Local request identity.
|
|
59
|
+
* @returns Whether this request may still be completed.
|
|
60
|
+
*/
|
|
61
|
+
export function hasOpen(requestId: string): boolean { return waiters.has(requestId) }
|
|
62
|
+
|
|
63
|
+
/** @public @author ddj 2026年09月28号 Cancel outstanding receipts when the client plugin unloads. */
|
|
64
|
+
export function disposeOpenWait(): void { for (const waiter of [...waiters.values()]) waiter.finish(false) }
|