dsh-vscode-mode 0.1.62 → 0.2.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 +8 -0
- package/lib/client.js +1554 -87
- package/lib/client.js.map +1 -1
- package/lib/index.js +9 -2
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client/commandBridge.ts +83 -0
- package/src/client/commandGlobals.ts +13 -0
- package/src/client/commandPaletteStore.ts +165 -0
- package/src/client/commandRegistry.ts +144 -0
- package/src/client/commandSearch.ts +89 -0
- package/src/client/editorModelState.ts +30 -0
- package/src/client/index.ts +55 -4
- package/src/client/keybindings.ts +71 -15
- package/src/client/monaco/loader.ts +3 -3
- package/src/client/monaco/theme.ts +313 -7
- package/src/client/officialSidebar.ts +45 -0
- package/src/client/outline/index.ts +4 -3
- package/src/client/sidebar/SidebarView.ts +16 -1
- package/src/client/sidebar/panels/FileExplorer.ts +58 -4
- package/src/client/sidebar/panels/index.ts +12 -10
- package/src/client/sidebar/types.ts +2 -2
- package/src/client/styles/editor.css +47 -28
- package/src/client/ui/ClaimRouter.ts +96 -0
- package/src/client/ui/CommandPalette.ts +158 -0
- package/src/client/ui/ConversationDiffDock.ts +17 -9
- package/src/client/ui/EditorView.ts +153 -15
- package/src/client/ui/KeybindingsPanel.ts +1 -1
- package/src/client/ui/OfficialSideTab.ts +8 -9
- package/src/client/ui/QuickOpen.ts +11 -3
- package/src/client/ui/commandCatalog.ts +179 -0
- package/src/client-primitives.d.ts +37 -0
- package/src/shared/keybindings.ts +10 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"dsh-plugin",
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 指令执行桥(装配层)。
|
|
3
|
+
* 职责三件:① 把内置指令目录注册进注册表;② 为「桥接派发」类指令挂窗口 capture 键位监听
|
|
4
|
+
* (EditorView 原生监听的 8 条不进这里,避免双执行);③ 注入命令栏执行器。
|
|
5
|
+
* 编辑器动作本身由 EditorView / QuickOpen 监听 `edrv.command.<action>` 窗口事件完成——桥不持有
|
|
6
|
+
* React 状态,因此指令系统可在编辑器挂载前装配、卸载后仍安全。
|
|
7
|
+
* 作者 ddj 2026年09月10号
|
|
8
|
+
*/
|
|
9
|
+
import { BRIDGE_COMMANDS, EDITOR_COMMANDS, dispatchedCommands, showCommandsDef } from './ui/commandCatalog.js'
|
|
10
|
+
import { createCommandRegistry } from './commandRegistry.js'
|
|
11
|
+
import type { CommandRegistry } from './commandRegistry.js'
|
|
12
|
+
import { bindingsOf, matchEvent } from './keybindings.js'
|
|
13
|
+
import { log } from './log.js'
|
|
14
|
+
import { openCommandPalette, setPaletteRunner, setRegistryRef } from './commandPaletteStore.js'
|
|
15
|
+
|
|
16
|
+
const bridgeLog = log.child('commands')
|
|
17
|
+
|
|
18
|
+
/** 桥接装配选项(事件目标可注入,便于单测)。 */
|
|
19
|
+
export interface CommandBridgeOptions {
|
|
20
|
+
/** 键盘事件目标(缺省 window;无 window 的纯 Node 环境自动跳过监听)。 */
|
|
21
|
+
target?: Pick<EventTarget, 'addEventListener' | 'removeEventListener'>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 指令执行桥句柄。 */
|
|
25
|
+
export interface CommandBridge {
|
|
26
|
+
/** 指令注册表(第三方可继续 register)。 */
|
|
27
|
+
registry: CommandRegistry
|
|
28
|
+
/** 卸载全部内置指令并解除键位监听(幂等)。 */
|
|
29
|
+
dispose(): void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 解析事件目标(缺省 window;无 window 返回 null)。 */
|
|
33
|
+
function resolveTarget(options: CommandBridgeOptions): CommandBridgeOptions['target'] | null {
|
|
34
|
+
if (options.target) return options.target
|
|
35
|
+
if (typeof window === 'undefined') return null
|
|
36
|
+
return window
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 创建指令执行桥:注册内置指令 + 桥接类指令的全局键位监听。
|
|
41
|
+
* @author ddj 2026年09月10号
|
|
42
|
+
* @param options 装配选项(事件目标注入)
|
|
43
|
+
* @returns 指令执行桥句柄
|
|
44
|
+
*/
|
|
45
|
+
export function createCommandBridge(options: CommandBridgeOptions = {}): CommandBridge {
|
|
46
|
+
const registry = createCommandRegistry()
|
|
47
|
+
const disposers: Array<() => void> = []
|
|
48
|
+
const palette = showCommandsDef(() => openCommandPalette('command'))
|
|
49
|
+
for (const command of [...EDITOR_COMMANDS, ...BRIDGE_COMMANDS]) {
|
|
50
|
+
disposers.push(registry.register(command))
|
|
51
|
+
}
|
|
52
|
+
disposers.push(registry.register(palette))
|
|
53
|
+
setPaletteRunner((id) => registry.run(id))
|
|
54
|
+
// 命令栏候选来源:模块引用直传(旧实现依赖 window.dsh,DSH 无该命名空间 → 恒空表)
|
|
55
|
+
setRegistryRef(registry)
|
|
56
|
+
|
|
57
|
+
// 全局键位派发集合:桥接类指令 + 命令栏自身(EditorView/QuickOpen 原生监听的 8 条不在内,避免双执行)
|
|
58
|
+
const dispatched = dispatchedCommands(palette)
|
|
59
|
+
const onKey = (event: KeyboardEvent): void => {
|
|
60
|
+
for (const command of dispatched) {
|
|
61
|
+
if (!matchEvent(event, bindingsOf(command.id))) continue
|
|
62
|
+
event.preventDefault()
|
|
63
|
+
event.stopPropagation()
|
|
64
|
+
registry.run(command.id)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const target = resolveTarget(options)
|
|
69
|
+
if (target) target.addEventListener('keydown', onKey as EventListener, true)
|
|
70
|
+
|
|
71
|
+
const dispose = (): void => {
|
|
72
|
+
if (target) target.removeEventListener('keydown', onKey as EventListener, true)
|
|
73
|
+
while (disposers.length) {
|
|
74
|
+
const release = disposers.pop()
|
|
75
|
+
if (release) release()
|
|
76
|
+
}
|
|
77
|
+
setPaletteRunner(null)
|
|
78
|
+
setRegistryRef(null)
|
|
79
|
+
}
|
|
80
|
+
bridgeLog.info('指令注册表已装配(' + registry.list().length + ' 条指令,桥接键位 '
|
|
81
|
+
+ dispatched.length + ' 条)')
|
|
82
|
+
return { registry, dispose }
|
|
83
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 指令系统的全局挂点名约定。
|
|
3
|
+
* 单独成模块:装配层(client/index.ts)与读取方(命令栏/第三方)都要用,
|
|
4
|
+
* 放这里可避免 CommandPalette ↔ index 的循环依赖。
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ 必须挂在 window 自身(双下划线 + edrv 前缀,与既有 __edrvExtPoll 同风格)。
|
|
7
|
+
* 旧实现写 window.dsh.edrvCommands——DSH 从不创建 window.dsh 命名空间(全仓库无赋值),
|
|
8
|
+
* 该路径恒为死代码并导致命令栏读到空表;不要再改回气泡式挂点。
|
|
9
|
+
* 作者 ddj 2026年09月10号
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** 指令注册表的全局挂点名:window[REGISTRY_GLOBAL](第三方注册与调试读取;命令栏首选模块引用)。 */
|
|
13
|
+
export const REGISTRY_GLOBAL = '__edrvCommands__'
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 命令栏开关状态与单实例宿主认领。
|
|
3
|
+
* 命令栏浮层走 createPortal 渲染到 body,但组件必须挂在插件自己的 React 树里:
|
|
4
|
+
* 编辑区有三种形态(官方右侧 Sidebar / better-sidebar / 中央页签),会话切换时旧树先卸载、
|
|
5
|
+
* 新树后挂载,可能出现两个宿主——claimPaletteHost 保证任一时刻只有一个宿主真正渲染浮层。
|
|
6
|
+
* 运行体由 commandBridge 经 setPaletteRunner 注入(避免 store ↔ 注册表循环依赖);
|
|
7
|
+
* 注册表本体经 setRegistryRef 存入模块引用,命令栏据此读取候选——不依赖任何 window 全局
|
|
8
|
+
* (旧实现写 window.dsh.edrvCommands,而 DSH 从不创建 window.dsh,导致命令栏恒为空表)。
|
|
9
|
+
* 作者 ddj 2026年09月10号
|
|
10
|
+
*/
|
|
11
|
+
import type { CommandRegistry } from './commandRegistry.js'
|
|
12
|
+
|
|
13
|
+
/** 命令栏上次的唤起来源(诊断用)。 */
|
|
14
|
+
let openReason = ''
|
|
15
|
+
|
|
16
|
+
/** 装配期存入的指令注册表(命令栏读取候选的唯一真源)。 */
|
|
17
|
+
let registryTable: CommandRegistry | null = null
|
|
18
|
+
|
|
19
|
+
/** 命令栏是否展开。 */
|
|
20
|
+
let opened = false
|
|
21
|
+
|
|
22
|
+
/** 当前真正渲染浮层的宿主令牌(null = 无宿主)。 */
|
|
23
|
+
let hostToken: object | null = null
|
|
24
|
+
|
|
25
|
+
/** 注入的命令执行器(commandBridge 装配时写入)。 */
|
|
26
|
+
let runner: ((id: string) => boolean) | null = null
|
|
27
|
+
|
|
28
|
+
/** 打开命令栏前的焦点元素(关闭后归还焦点,键盘操作不中断)。 */
|
|
29
|
+
let focusBack: HTMLElement | null = null
|
|
30
|
+
|
|
31
|
+
const listeners = new Set<() => void>()
|
|
32
|
+
|
|
33
|
+
/** 通知全部订阅者(回调异常不影响其他订阅)。 */
|
|
34
|
+
function notifyPalette(): void {
|
|
35
|
+
for (const listener of listeners) {
|
|
36
|
+
try {
|
|
37
|
+
listener()
|
|
38
|
+
} catch {
|
|
39
|
+
/* 订阅回调异常忽略 */
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 注入命令执行器(命令栏执行选中命令时调用)。
|
|
46
|
+
* @author ddj 2026年09月10号
|
|
47
|
+
* @param next 执行器(返回是否执行成功)
|
|
48
|
+
*/
|
|
49
|
+
export function setPaletteRunner(next: ((id: string) => boolean) | null): void {
|
|
50
|
+
runner = next
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 执行命令栏选中的命令。
|
|
55
|
+
* @author ddj 2026年09月10号
|
|
56
|
+
* @param id 命令 id
|
|
57
|
+
* @returns 是否执行成功(执行器未注入/命令不可用/抛异常均为 false)
|
|
58
|
+
*/
|
|
59
|
+
export function runPaletteCommand(id: string): boolean {
|
|
60
|
+
if (!runner) return false
|
|
61
|
+
return runner(id) === true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 存入指令注册表(commandBridge 装配期调用;命令栏据此读取候选)。
|
|
66
|
+
* @author ddj 2026年09月10号
|
|
67
|
+
* @param next 指令注册表(null = 卸载)
|
|
68
|
+
*/
|
|
69
|
+
export function setRegistryRef(next: CommandRegistry | null): void {
|
|
70
|
+
registryTable = next
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 读取指令注册表(未装配返回 null;浮层据此回退全局/空表)。
|
|
75
|
+
* @author ddj 2026年09月10号
|
|
76
|
+
* @returns 指令注册表或 null
|
|
77
|
+
*/
|
|
78
|
+
export function registryRef(): CommandRegistry | null {
|
|
79
|
+
return registryTable
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 订阅命令栏开关变化。
|
|
84
|
+
* @author ddj 2026年09月10号
|
|
85
|
+
* @param listener 变化回调
|
|
86
|
+
* @returns 取消订阅函数
|
|
87
|
+
*/
|
|
88
|
+
export function subscribePalette(listener: () => void): () => void {
|
|
89
|
+
listeners.add(listener)
|
|
90
|
+
return () => listeners.delete(listener)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 命令栏是否展开(useSyncExternalStore 快照)。
|
|
95
|
+
* @author ddj 2026年09月10号
|
|
96
|
+
* @returns 是否展开
|
|
97
|
+
*/
|
|
98
|
+
export function isPaletteOpen(): boolean {
|
|
99
|
+
return opened
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 记录打开前的焦点元素(只在真正需要归还时记录)。 */
|
|
103
|
+
function rememberFocus(): void {
|
|
104
|
+
if (typeof document === 'undefined') return
|
|
105
|
+
const active = document.activeElement
|
|
106
|
+
focusBack = active instanceof HTMLElement ? active : null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 打开命令栏(已打开时为空操作;记录当前焦点供关闭后归还)。
|
|
111
|
+
* @author ddj 2026年09月10号
|
|
112
|
+
* @param reason 唤起来源(键盘/命令/外部 API)
|
|
113
|
+
*/
|
|
114
|
+
export function openCommandPalette(reason = 'keybinding'): void {
|
|
115
|
+
if (opened) return
|
|
116
|
+
rememberFocus()
|
|
117
|
+
openReason = reason
|
|
118
|
+
opened = true
|
|
119
|
+
notifyPalette()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 关闭命令栏并归还焦点。
|
|
124
|
+
* @author ddj 2026年09月10号
|
|
125
|
+
*/
|
|
126
|
+
export function closeCommandPalette(): void {
|
|
127
|
+
if (!opened) return
|
|
128
|
+
opened = false
|
|
129
|
+
openReason = ''
|
|
130
|
+
notifyPalette()
|
|
131
|
+
const target = focusBack
|
|
132
|
+
focusBack = null
|
|
133
|
+
if (!target || typeof target.focus !== 'function') return
|
|
134
|
+
try {
|
|
135
|
+
target.focus()
|
|
136
|
+
} catch {
|
|
137
|
+
/* 目标已卸载时忽略 */
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 上次唤起来源(诊断/测试读取)。 */
|
|
142
|
+
export function paletteReason(): string {
|
|
143
|
+
return openReason
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 认领命令栏宿主(返回 true 表示由本宿主渲染浮层)。
|
|
148
|
+
* @author ddj 2026年09月10号
|
|
149
|
+
* @returns 是否为当前宿主
|
|
150
|
+
*/
|
|
151
|
+
export function claimPaletteHost(): boolean {
|
|
152
|
+
if (hostToken !== null) return false
|
|
153
|
+
hostToken = {}
|
|
154
|
+
return true
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 释放宿主认领(仅持有者可释放,卸载顺序错乱不会顶掉新宿主)。
|
|
159
|
+
* @author ddj 2026年09月10号
|
|
160
|
+
* @param token 认领令牌(null 表示本宿主未认领)
|
|
161
|
+
*/
|
|
162
|
+
export function releasePaletteHost(token: object | null): void {
|
|
163
|
+
if (token === null || hostToken !== token) return
|
|
164
|
+
hostToken = null
|
|
165
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 指令注册表(命令栏与快捷键系统的唯一数据源)。
|
|
3
|
+
* 镜像 sidebar/registry.ts 的注册表模式:register 返回注销器、list/subscribe 可订阅;
|
|
4
|
+
* 额外提供 run(id):先做可用性判定再执行,运行体异常一律捕获上报,绝不让命令抛出到事件循环。
|
|
5
|
+
* 重复 id 按「后注册者生效」覆盖,旧注销器带身份校验(不会误删新实例,见 releaseRegistry)。
|
|
6
|
+
* 作者 ddj 2026年09月10号
|
|
7
|
+
*/
|
|
8
|
+
import { filterCommands } from './commandSearch.js'
|
|
9
|
+
import { log } from './log.js'
|
|
10
|
+
import type { CommandDef } from './ui/commandCatalog.js'
|
|
11
|
+
|
|
12
|
+
/** 指令注册表(对插件内与第三方一致)。 */
|
|
13
|
+
export interface CommandRegistry {
|
|
14
|
+
/** 注册命令;返回注销函数(幂等,重复调用无效)。 */
|
|
15
|
+
register(command: CommandDef): () => void
|
|
16
|
+
/** 命令是否存在。 */
|
|
17
|
+
has(id: string): boolean
|
|
18
|
+
/** 读取命令定义。 */
|
|
19
|
+
get(id: string): CommandDef | undefined
|
|
20
|
+
/** 全部命令(按目录序稳定排序)。 */
|
|
21
|
+
list(): CommandDef[]
|
|
22
|
+
/** 可用命令(available 缺省视为可用)。 */
|
|
23
|
+
available(): CommandDef[]
|
|
24
|
+
/** 命令栏过滤(先按可用性过滤,再按相关度排序)。 */
|
|
25
|
+
match(query: string): CommandDef[]
|
|
26
|
+
/** 执行命令;不可用/未注册/抛异常统一返回 false(不抛出)。 */
|
|
27
|
+
run(id: string): boolean
|
|
28
|
+
/** 订阅注册表变化。 */
|
|
29
|
+
subscribe(listener: () => void): () => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const registryLog = log.child('commands')
|
|
33
|
+
|
|
34
|
+
/** 校验命令定义(缺 id/label/run 抛 TypeError,早失败优于静默无效)。 */
|
|
35
|
+
function assertCommand(command: CommandDef): void {
|
|
36
|
+
if (!command || typeof command.id !== 'string' || !command.id) throw new TypeError('命令必须提供 id')
|
|
37
|
+
if (typeof command.label !== 'string' || !command.label) throw new TypeError('命令必须提供 label:' + command.id)
|
|
38
|
+
if (typeof command.run !== 'function') throw new TypeError('命令必须提供 run:' + command.id)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 目录序(order 缺省 100;非有限值同样按 100)。 */
|
|
42
|
+
function orderOf(command: CommandDef): number {
|
|
43
|
+
return typeof command.order === 'number' && Number.isFinite(command.order) ? command.order : 100
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 命令是否可用(available 缺省视为可用;判定异常按不可用处理)。 */
|
|
47
|
+
function isAvailable(command: CommandDef): boolean {
|
|
48
|
+
if (typeof command.available !== 'function') return true
|
|
49
|
+
try {
|
|
50
|
+
return command.available() === true
|
|
51
|
+
} catch (error) {
|
|
52
|
+
registryLog.warn('可用性判定异常(按不可用处理):' + command.id + ' · ' + String(error))
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 注销命令(仅当表中仍是本次注册的定义时移除,避免旧实例误删新实例)。
|
|
59
|
+
* @author ddj 2026年09月10号
|
|
60
|
+
* @param entries 命令表
|
|
61
|
+
* @param notify 变化通知
|
|
62
|
+
* @param command 注册时的定义
|
|
63
|
+
* @returns 注销函数
|
|
64
|
+
*/
|
|
65
|
+
function releaseRegistry(entries: Map<string, CommandDef>, notify: () => void, command: CommandDef): () => void {
|
|
66
|
+
let released = false
|
|
67
|
+
return () => {
|
|
68
|
+
if (released) return
|
|
69
|
+
released = true
|
|
70
|
+
if (entries.get(command.id) !== command) return
|
|
71
|
+
entries.delete(command.id)
|
|
72
|
+
notify()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 执行已注册命令(可用性判定 → 运行体 → 异常上报)。
|
|
78
|
+
* @author ddj 2026年09月10号
|
|
79
|
+
* @param command 命令定义
|
|
80
|
+
* @returns 是否真正执行
|
|
81
|
+
*/
|
|
82
|
+
function runCommand(command: CommandDef): boolean {
|
|
83
|
+
if (!isAvailable(command)) {
|
|
84
|
+
registryLog.debug('命令不可用,已跳过:' + command.id)
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
command.run()
|
|
89
|
+
registryLog.debug('命令已执行:' + command.id)
|
|
90
|
+
return true
|
|
91
|
+
} catch (error) {
|
|
92
|
+
registryLog.error('命令执行失败:' + command.id + ' · ' + String(error))
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 创建指令注册表。
|
|
99
|
+
* @author ddj 2026年09月10号
|
|
100
|
+
* @returns 指令注册表
|
|
101
|
+
*/
|
|
102
|
+
export function createCommandRegistry(): CommandRegistry {
|
|
103
|
+
const entries = new Map<string, CommandDef>()
|
|
104
|
+
const listeners = new Set<() => void>()
|
|
105
|
+
const notify = (): void => {
|
|
106
|
+
for (const listener of listeners) {
|
|
107
|
+
try {
|
|
108
|
+
listener()
|
|
109
|
+
} catch (error) {
|
|
110
|
+
registryLog.warn('注册表订阅回调异常:' + String(error))
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const list = (): CommandDef[] =>
|
|
115
|
+
[...entries.values()].sort((a, b) => orderOf(a) - orderOf(b))
|
|
116
|
+
const available = (): CommandDef[] => list().filter(isAvailable)
|
|
117
|
+
return {
|
|
118
|
+
register(command: CommandDef): () => void {
|
|
119
|
+
assertCommand(command)
|
|
120
|
+
const replaced = entries.get(command.id)
|
|
121
|
+
if (replaced !== undefined) registryLog.warn('命令 id 重复注册(后者生效):' + command.id)
|
|
122
|
+
entries.set(command.id, command)
|
|
123
|
+
notify()
|
|
124
|
+
return releaseRegistry(entries, notify, command)
|
|
125
|
+
},
|
|
126
|
+
has: (id) => entries.has(id),
|
|
127
|
+
get: (id) => entries.get(id),
|
|
128
|
+
list,
|
|
129
|
+
available,
|
|
130
|
+
match: (query) => filterCommands(available(), query),
|
|
131
|
+
run(id: string): boolean {
|
|
132
|
+
const command = entries.get(id)
|
|
133
|
+
if (!command) {
|
|
134
|
+
registryLog.warn('命令未注册:' + id)
|
|
135
|
+
return false
|
|
136
|
+
}
|
|
137
|
+
return runCommand(command)
|
|
138
|
+
},
|
|
139
|
+
subscribe(listener: () => void): () => void {
|
|
140
|
+
listeners.add(listener)
|
|
141
|
+
return () => listeners.delete(listener)
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 命令栏筛选与排序(纯函数,可单测)。
|
|
3
|
+
* 匹配语义:查询与索引文本统一小写,空格分词,全部 token 需在 label/id/category
|
|
4
|
+
* 任一字段中以子串出现(中文子串匹配天然成立)。
|
|
5
|
+
* 排序语义:命中字段权重(label 0 < id 1 < category 2)+ 目录序(order 缺省 100)+ 注册序,
|
|
6
|
+
* 保证同一查询下结果稳定、越相关的越靠前。
|
|
7
|
+
* 作者 ddj 2026年09月10号
|
|
8
|
+
*/
|
|
9
|
+
import type { CommandDef } from './ui/commandCatalog.js'
|
|
10
|
+
|
|
11
|
+
/** 排序用基础序(未声明 order 的命令排在已声明之后)。 */
|
|
12
|
+
const DEFAULT_ORDER = 100
|
|
13
|
+
|
|
14
|
+
/** 命中字段权重(数值越小越优先)。 */
|
|
15
|
+
const FIELD_WEIGHT: Record<string, number> = { label: 0, id: 1, category: 2 }
|
|
16
|
+
|
|
17
|
+
/** 命令栏候选行(命令定义 + 匹配权重)。 */
|
|
18
|
+
export interface CommandHit {
|
|
19
|
+
command: CommandDef
|
|
20
|
+
weight: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 命中字段索引文本(小写)。 */
|
|
24
|
+
interface IndexedFields {
|
|
25
|
+
label: string
|
|
26
|
+
id: string
|
|
27
|
+
category: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 小写化(非字符串按空串处理,容错外部脏数据)。 */
|
|
31
|
+
function lower(text: unknown): string {
|
|
32
|
+
return typeof text === 'string' ? text.toLocaleLowerCase('en-US') : ''
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 建立一条命令的索引文本。 */
|
|
36
|
+
function indexOf(command: CommandDef): IndexedFields {
|
|
37
|
+
return { label: lower(command.label), id: lower(command.id), category: lower(command.category) }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 命令是否命中全部 token;命中时返回最优(最小)字段权重。
|
|
42
|
+
* @author ddj 2026年09月10号
|
|
43
|
+
* @param fields 命令索引文本
|
|
44
|
+
* @param tokens 小写查询 token
|
|
45
|
+
* @returns 字段权重;未命中返回 null
|
|
46
|
+
*/
|
|
47
|
+
function hitWeight(fields: IndexedFields, tokens: readonly string[]): number | null {
|
|
48
|
+
let best: number | null = null
|
|
49
|
+
for (const token of tokens) {
|
|
50
|
+
let tokenWeight: number | null = null
|
|
51
|
+
for (const field of ['label', 'id', 'category'] as const) {
|
|
52
|
+
if (!fields[field].includes(token)) continue
|
|
53
|
+
const weight = FIELD_WEIGHT[field]
|
|
54
|
+
if (tokenWeight === null || weight < tokenWeight) tokenWeight = weight
|
|
55
|
+
if (weight === 0) break
|
|
56
|
+
}
|
|
57
|
+
if (tokenWeight === null) return null
|
|
58
|
+
if (best === null || tokenWeight < best) best = tokenWeight
|
|
59
|
+
}
|
|
60
|
+
return best
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 目录序(order 缺省 100;非有限值同样按 100)。 */
|
|
64
|
+
function orderOf(command: CommandDef): number {
|
|
65
|
+
const order = command.order
|
|
66
|
+
return typeof order === 'number' && Number.isFinite(order) ? order : DEFAULT_ORDER
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 过滤并按相关度排序命令(空查询返回全部,仅按目录序排序)。
|
|
71
|
+
* @author ddj 2026年09月10号
|
|
72
|
+
* @param commands 候选命令(调用方通常已按可用性过滤)
|
|
73
|
+
* @param query 用户输入
|
|
74
|
+
* @returns 命中命令数组
|
|
75
|
+
*/
|
|
76
|
+
export function filterCommands(commands: readonly CommandDef[], query: string): CommandDef[] {
|
|
77
|
+
const tokens = lower(query).split(/\s+/).filter(Boolean)
|
|
78
|
+
const hits: CommandHit[] = []
|
|
79
|
+
for (const command of commands) {
|
|
80
|
+
const weight = tokens.length ? hitWeight(indexOf(command), tokens) : 0
|
|
81
|
+
if (weight !== null) hits.push({ command, weight })
|
|
82
|
+
}
|
|
83
|
+
return hits
|
|
84
|
+
.map((hit, index) => ({ hit, index }))
|
|
85
|
+
.sort((a, b) => a.hit.weight - b.hit.weight
|
|
86
|
+
|| orderOf(a.hit.command) - orderOf(b.hit.command)
|
|
87
|
+
|| a.index - b.index)
|
|
88
|
+
.map((entry) => entry.hit.command)
|
|
89
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 编辑器模型可用性探测(模块级,不依赖 React)。
|
|
3
|
+
* 命令可用性判定(快捷键过滤 / 命令栏隐藏 / 执行前置检查)需要「当前是否有活动编辑器」,
|
|
4
|
+
* 而命令目录是模块级常量、拿不到 EditorView 实例,故统一读 DOM 标记:
|
|
5
|
+
* `[data-edrv-view]` 只在 EditorView 挂载期存在于文档中(浮层 portal 走同一根节点)。
|
|
6
|
+
* 作者 ddj 2026年09月10号
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** 编辑器根节点选择器(EditorView 三种形态共用)。 */
|
|
10
|
+
export const EDITOR_ROOT_SELECTOR = '[data-edrv-view]'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 当前文档是否挂载了编辑器视图(无 document 的运行环境返回 false)。
|
|
14
|
+
* @author ddj 2026年09月10号
|
|
15
|
+
* @returns 是否存在编辑器根节点
|
|
16
|
+
*/
|
|
17
|
+
export function hasEditorView(): boolean {
|
|
18
|
+
if (typeof document === 'undefined') return false
|
|
19
|
+
return document.querySelector(EDITOR_ROOT_SELECTOR) !== null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 当前文档是否已有 Monaco 活动模型(编辑器打开着文件而非空态)。
|
|
24
|
+
* @author ddj 2026年09月10号
|
|
25
|
+
* @returns 是否存在活动模型
|
|
26
|
+
*/
|
|
27
|
+
export function hasEditorModel(): boolean {
|
|
28
|
+
if (typeof document === 'undefined') return false
|
|
29
|
+
return document.querySelector('.monaco-editor textarea.inputarea') !== null
|
|
30
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { ConversationDiffDock } from './ui/ConversationDiffDock.js'
|
|
|
22
22
|
import { McpSettings } from './ui/McpSettings.js'
|
|
23
23
|
import { rpc } from './rpc.js'
|
|
24
24
|
import { loadMonaco } from './monaco/loader.js'
|
|
25
|
+
import { observeScheme, schemeOfSnapshot } from './monaco/theme.js'
|
|
25
26
|
import { createFileOpenerRegistry, scanSidebar, officialSidebarOpener, shouldClaimFiles, type FileOpenContext } from './fileOpeners.js'
|
|
26
27
|
import type { FileOpenerRegistry } from './fileOpeners.js'
|
|
27
28
|
import { installOpenPathRouter, vscodeOpener, autoValue } from './openPathRouter.js'
|
|
@@ -33,6 +34,7 @@ import { detectSidebarService, installSideEditor, setEnsureSideEditor, SIDEBAR_I
|
|
|
33
34
|
import { detectOfficial, installOfficial, registerOfficialFileClaim, OFFICIAL_TAB_KIND, OFFICIAL_TAB_TITLE, isEditorTabActive, restoreEditorTab } from './officialSidebar.js'
|
|
34
35
|
import { SideEditorTab } from './ui/SideEditorTab.js'
|
|
35
36
|
import { OfficialSideTab } from './ui/OfficialSideTab.js'
|
|
37
|
+
import { createClaimRouter } from './ui/ClaimRouter.js'
|
|
36
38
|
import { createAddToConversation } from './addToConversation.js'
|
|
37
39
|
import { createSidebarPanelRegistry } from './sidebar/registry.js'
|
|
38
40
|
import { createFilePanel } from './sidebar/panels/index.js'
|
|
@@ -44,6 +46,8 @@ import { createOutlinePanel } from './outline/index.js'
|
|
|
44
46
|
import { createOutlineSourceRegistry, registerBuiltinOutlineSources } from './outline/sources.js'
|
|
45
47
|
import { createLspOutlineSource } from './outline/lspSource.js'
|
|
46
48
|
import { keybindingsApply } from './keybindings.js'
|
|
49
|
+
import { createCommandBridge } from './commandBridge.js'
|
|
50
|
+
import { REGISTRY_GLOBAL } from './commandGlobals.js'
|
|
47
51
|
import { sidebarMinApply } from './sidebarMin.js'
|
|
48
52
|
import { log } from './log.js'
|
|
49
53
|
import { setupLsp, setSession } from './monaco/lsp/index.js'
|
|
@@ -54,6 +58,33 @@ import type { CompatAdapter } from '../shared/compat.js'
|
|
|
54
58
|
// web boot 直接失败('1 entry did not activate')。兼容层用 ctx.get 运行时探测 + 降级,不靠 inject。
|
|
55
59
|
export const inject = ['slots', 'timer', 'locale', 'connection', 'remote', 'workspaces', 'sessions', 'conversation', 'settingsScope']
|
|
56
60
|
|
|
61
|
+
/** 指令桥装配幂等标记(同一 document 重复 apply 只装配一次,避免重复键位监听)。 */
|
|
62
|
+
let commandsMounted = false
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 装配指令注册表(指令桥 + 命令栏)。
|
|
66
|
+
* 指令桥不依赖已挂载编辑器(run 只派发窗口事件),故可在启动期装配;
|
|
67
|
+
* 命令栏浮层的宿主由 CommandPalette 自己在编辑区树内认领。
|
|
68
|
+
* @author ddj 2026年09月10号
|
|
69
|
+
* @param ctx 客户端根上下文
|
|
70
|
+
* @returns void
|
|
71
|
+
*/
|
|
72
|
+
function setupCommands(ctx: any): void {
|
|
73
|
+
if (commandsMounted) return
|
|
74
|
+
commandsMounted = true
|
|
75
|
+
const bridge = createCommandBridge()
|
|
76
|
+
ctx.provide(REGISTRY_GLOBAL, bridge.registry)
|
|
77
|
+
// 无条件镜像到 window(DSH 不创建 window.dsh,旧条件式赋值是死代码 → 命令栏空表)
|
|
78
|
+
const host = window as unknown as Record<string, unknown>
|
|
79
|
+
host[REGISTRY_GLOBAL] = bridge.registry
|
|
80
|
+
ctx.effect(() => () => {
|
|
81
|
+
bridge.dispose()
|
|
82
|
+
commandsMounted = false
|
|
83
|
+
if (host[REGISTRY_GLOBAL] === bridge.registry) delete host[REGISTRY_GLOBAL]
|
|
84
|
+
}, 'vscode-mode: command registry')
|
|
85
|
+
log.info('指令系统已装配:' + bridge.registry.list().length + ' 条指令,Ctrl+Shift+P 打开命令栏')
|
|
86
|
+
}
|
|
87
|
+
|
|
57
88
|
/**
|
|
58
89
|
* 装配客户端:注册中央编辑区视图与 header 差异角标。
|
|
59
90
|
* @author ddj 2026年08月20号
|
|
@@ -63,6 +94,9 @@ export const inject = ['slots', 'timer', 'locale', 'connection', 'remote', 'work
|
|
|
63
94
|
export function apply(ctx: any): void {
|
|
64
95
|
const schedule = (fn: () => void, ms: number) => ctx.timeout(fn, ms)
|
|
65
96
|
|
|
97
|
+
// 指令系统(命令栏 + 指令注册表)先装配:命令栏在被任何 React slot 渲染前即可唤起
|
|
98
|
+
setupCommands(ctx)
|
|
99
|
+
|
|
66
100
|
const registry: FileOpenerRegistry = createFileOpenerRegistry()
|
|
67
101
|
const workspaces = ctx.get('workspaces')
|
|
68
102
|
const sessions = ctx.get('sessions')
|
|
@@ -148,7 +182,7 @@ export function apply(ctx: any): void {
|
|
|
148
182
|
{ name: '侧边栏打开器(' + SIDEBAR_PLUGIN + ')', active: registry.get(SIDEBAR_PLUGIN) !== undefined, note: registry.get(SIDEBAR_PLUGIN) !== undefined ? '已注册(优先级 80)' : '未检测到侧边栏打开能力' },
|
|
149
183
|
{ name: '侧边栏编辑区(官方 Sidebar)', active: officialService !== undefined, note: officialService !== undefined ? '编辑区=官方右侧 Sidebar Tab(对话+编辑同屏,DSH 0.1.5+)' : '官方侧边栏服务未探测到(DSH < 0.1.5-alpha.1 时属预期)' },
|
|
150
184
|
{ name: '侧边栏编辑区(' + SIDEBAR_PLUGIN + ',归档)', active: sideService !== undefined, note: sideService !== undefined ? '编辑区=侧边栏 Tab(旧版 DSH 回退形态)' : '未检测到;DSH ≥ 0.1.5 优先官方侧边栏,旧版可安装(' + SIDEBAR_INSTALL_CMD + ')' },
|
|
151
|
-
{ name: '文件链接官方认领(dsh-resource://file)', active: claimDisposer !== null, note: claimDisposer !== null ? '
|
|
185
|
+
{ name: '文件链接官方认领(dsh-resource://file)', active: claimDisposer !== null, note: claimDisposer !== null ? '聊天文件链接转发进单一编辑器页签(文件分页归编辑器自带页签栏)' : '链接走官方查看器或旧版路由(未认领)' },
|
|
152
186
|
]
|
|
153
187
|
|
|
154
188
|
ctx.provide('fileOpeners', registry)
|
|
@@ -183,14 +217,16 @@ export function apply(ctx: any): void {
|
|
|
183
217
|
const sidebar = scanSidebar(ctx)
|
|
184
218
|
return sidebar ? registry.register(sidebar) : undefined
|
|
185
219
|
}, 'vscode-mode: sidebar file opener')
|
|
186
|
-
//
|
|
220
|
+
// 官方侧边栏正文组件装配(页类型正文;兼作 file 认领转发失败时的兜底正文)
|
|
187
221
|
const officialRenderTab = (props: Record<string, unknown>) => React.createElement(OfficialSideTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, sessions }))
|
|
188
|
-
/** 官方 file 地址认领同步:自动/VSCodeMode
|
|
222
|
+
/** 官方 file 地址认领同步:自动/VSCodeMode 档认领(转发进单一编辑器页签),其余交官方查看器。 */
|
|
189
223
|
const syncFileClaim = (): void => {
|
|
190
224
|
const official = officialService
|
|
191
225
|
const want = official !== undefined && shouldClaimFiles(selected)
|
|
192
226
|
if (want && claimDisposer === null && official) {
|
|
193
|
-
|
|
227
|
+
// 认领正文只做转发:文件分页收归编辑器自带页签栏,官方侧栏不再按文件分裂编辑器实例
|
|
228
|
+
const renderTab = createClaimRouter({ service: official.service, schedule, fallback: officialRenderTab })
|
|
229
|
+
const disposer = registerOfficialFileClaim({ tabs: official.tabs, slots: ctx.slots, renderTab })
|
|
194
230
|
if (disposer !== null) claimDisposer = ctx.effect(() => disposer, 'vscode-mode: official file claim')
|
|
195
231
|
return
|
|
196
232
|
}
|
|
@@ -265,6 +301,21 @@ export function apply(ctx: any): void {
|
|
|
265
301
|
}
|
|
266
302
|
retryRemoteOpen()
|
|
267
303
|
|
|
304
|
+
// 官方主题跟随(DSH 0.1.5+):ctx.theme 快照事件优先,其次明暗标记观察(observeScheme),
|
|
305
|
+
// 都不可用时 theme.ts 的 detectColorScheme 兜底;统一广播 edrv:theme-change 触发 Monaco 主题重刷。
|
|
306
|
+
const themeService = ctx.get('theme') as { getTheme?: () => unknown } | undefined
|
|
307
|
+
const emitTheme = (): void => {
|
|
308
|
+
const snapshot = typeof themeService?.getTheme === 'function' ? themeService.getTheme() : undefined
|
|
309
|
+
window.dispatchEvent(new CustomEvent('edrv:theme-change', { detail: { scheme: schemeOfSnapshot(snapshot) } }))
|
|
310
|
+
}
|
|
311
|
+
ctx.effect(() => {
|
|
312
|
+
if (!themeService || typeof ctx.on !== 'function') return undefined
|
|
313
|
+
const disposer = ctx.on('theme/change', emitTheme)
|
|
314
|
+
return typeof disposer === 'function' ? disposer : undefined
|
|
315
|
+
}, 'vscode-mode: official theme event')
|
|
316
|
+
ctx.effect(() => observeScheme(emitTheme), 'vscode-mode: theme attribute watch')
|
|
317
|
+
emitTheme()
|
|
318
|
+
|
|
268
319
|
// 中央「文件编辑」页签(旧形态回退):类 VSCode 编辑器;侧边栏形态(官方/better-sidebar)可用时
|
|
269
320
|
// 编辑器住侧边栏 Tab,本页签不注册(避免双实例:Monaco×2 + diff dock 每会话单源抢占)。
|
|
270
321
|
const registerLegacyTab = (): (() => void) | null => {
|