dsh-vscode-mode 0.1.63 → 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 +919 -43
- package/lib/client.js.map +1 -1
- package/lib/index.js +7 -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 +32 -0
- package/src/client/keybindings.ts +71 -17
- package/src/client/styles/editor.css +14 -0
- package/src/client/ui/CommandPalette.ts +158 -0
- package/src/client/ui/ConversationDiffDock.ts +17 -9
- package/src/client/ui/EditorView.ts +90 -3
- package/src/client/ui/KeybindingsPanel.ts +1 -1
- package/src/client/ui/QuickOpen.ts +11 -3
- package/src/client/ui/commandCatalog.ts +179 -0
- package/src/shared/keybindings.ts +7 -1
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
* dsh-vscode-mode client — 快捷键配置模块(解析/匹配/状态同步)。
|
|
3
3
|
* 纯逻辑(parseChord/parseChords/formatChord/matchEvent/chordFromEvent/normalizeKey)不依赖 DOM,可单测;
|
|
4
4
|
* 模块状态由 settings 订阅驱动(client/index.ts 调 keybindingsApply)。
|
|
5
|
-
* 键位语义:Ctrl 与 Cmd
|
|
6
|
-
*
|
|
5
|
+
* 键位语义:Ctrl 与 Cmd 互认(延续 Ctrl+P/Ctrl+B 捕获行为)。
|
|
6
|
+
* 命令目录(COMMANDS)派生自指令目录 commandCatalog(指令 = 单一数据源,设置页自动跟随);
|
|
7
|
+
* 另支持运行时键位表(第三方命令 register 时声明,不落设置 schema,注销即失效)。
|
|
8
|
+
* 作者 ddj 2026年08月26号 / 2026年09月10号
|
|
7
9
|
*/
|
|
8
10
|
import React from 'react'
|
|
9
11
|
import { KEYBINDING_DEFAULTS, normalizeKeybindings } from '../shared/keybindings.js'
|
|
12
|
+
import { BRIDGE_COMMANDS, EDITOR_COMMANDS, showCommandsDef } from './ui/commandCatalog.js'
|
|
13
|
+
import { log } from './log.js'
|
|
10
14
|
|
|
11
15
|
/** 解析后的键位(修饰符 + 规范化主键)。 */
|
|
12
16
|
export interface Binding {
|
|
@@ -19,24 +23,39 @@ export interface Binding {
|
|
|
19
23
|
|
|
20
24
|
/** 命令目录(设置页展示标签;执行按目录序先匹配先执行,冲突键位确定性)。 */
|
|
21
25
|
export const COMMANDS: Array<{ id: string; label: string }> = [
|
|
22
|
-
{ id:
|
|
23
|
-
{ id: 'edrv.
|
|
24
|
-
{ id:
|
|
25
|
-
{ id: 'edrv.searchInFiles', label: '在工作区中搜索' },
|
|
26
|
-
{ id: 'edrv.navigateBack', label: '后退(导航历史)' },
|
|
27
|
-
{ id: 'edrv.navigateForward', label: '前进(导航历史)' },
|
|
28
|
-
{ id: 'edrv.nextTab', label: '下一个页签' },
|
|
29
|
-
{ id: 'edrv.prevTab', label: '上一个页签' },
|
|
26
|
+
...EDITOR_COMMANDS.map((command) => ({ id: command.id, label: command.label })),
|
|
27
|
+
{ id: 'edrv.showCommands', label: '显示所有命令' },
|
|
28
|
+
...BRIDGE_COMMANDS.map((command) => ({ id: command.id, label: command.label })),
|
|
30
29
|
]
|
|
31
30
|
|
|
31
|
+
/** 命令栏命令定义(键位自检与目录展示共用;run 不参与键位逻辑)。 */
|
|
32
|
+
function paletteCommand(): { id: string; label: string; keybinding?: string } {
|
|
33
|
+
return showCommandsDef(() => {})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 目录与默认键位表一致性自检(仅告警不中断):新指令漏写共享表时第一时间可见。
|
|
38
|
+
* @author ddj 2026年09月10号
|
|
39
|
+
*/
|
|
40
|
+
function checkDefaultsDrift(): void {
|
|
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
|
+
}
|
|
48
|
+
|
|
32
49
|
const MODIFIERS: Record<string, 'ctrl' | 'shift' | 'alt' | 'meta'> = {
|
|
33
50
|
ctrl: 'ctrl', cmd: 'meta', meta: 'meta', shift: 'shift', alt: 'alt',
|
|
34
51
|
}
|
|
35
52
|
|
|
36
53
|
let current: Record<string, string> = { ...KEYBINDING_DEFAULTS }
|
|
37
|
-
|
|
54
|
+
const runtime = new Map<string, Binding[]>()
|
|
38
55
|
const listeners = new Set<() => void>()
|
|
39
56
|
|
|
57
|
+
checkDefaultsDrift()
|
|
58
|
+
|
|
40
59
|
/**
|
|
41
60
|
* 应用设置快照(与默认值合并;未知 id 丢弃;空对象 = 全部默认)。
|
|
42
61
|
* 每个命令可含多候选键位(`|` 分隔),任一命中即触发。
|
|
@@ -45,8 +64,6 @@ const listeners = new Set<() => void>()
|
|
|
45
64
|
*/
|
|
46
65
|
export function keybindingsApply(raw: unknown): void {
|
|
47
66
|
current = { ...KEYBINDING_DEFAULTS, ...normalizeKeybindings(raw) }
|
|
48
|
-
parsed = {}
|
|
49
|
-
for (const id of Object.keys(current)) parsed[id] = parseChords(current[id])
|
|
50
67
|
for (const listener of listeners) {
|
|
51
68
|
try { listener() } catch { /* 监听器异常不影响其他订阅 */ }
|
|
52
69
|
}
|
|
@@ -64,24 +81,61 @@ export function subscribeKeybindings(listener: () => void): () => void {
|
|
|
64
81
|
}
|
|
65
82
|
|
|
66
83
|
/**
|
|
67
|
-
*
|
|
68
|
-
* @author ddj 2026年
|
|
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
|
+
/** 通知键位订阅者(异常隔离)。 */
|
|
107
|
+
function notifyKeybindings(): void {
|
|
108
|
+
for (const listener of listeners) {
|
|
109
|
+
try { listener() } catch { /* 监听器异常不影响其他订阅 */ }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 当前命令的键位弦(运行时键位优先;未绑定/空 → null)。
|
|
115
|
+
* @author ddj 2026年08月26号 / 2026年09月10号
|
|
69
116
|
* @param id 命令 id
|
|
70
117
|
* @returns 键位弦或 null
|
|
71
118
|
*/
|
|
72
119
|
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
|
+
}
|
|
73
124
|
const chord = current[id]
|
|
74
125
|
return typeof chord === 'string' && chord.trim() !== '' ? chord : null
|
|
75
126
|
}
|
|
76
127
|
|
|
77
128
|
/**
|
|
78
129
|
* 当前命令的解析键位集合(未绑定/非法 → 空数组;含多候选)。
|
|
79
|
-
* @author ddj 2026年08月26号
|
|
130
|
+
* @author ddj 2026年08月26号 / 2026年09月10号
|
|
80
131
|
* @param id 命令 id
|
|
81
132
|
* @returns 解析键位数组(可能为空)
|
|
82
133
|
*/
|
|
83
134
|
export function bindingsOf(id: string): Binding[] {
|
|
84
|
-
|
|
135
|
+
const override = runtime.get(id)
|
|
136
|
+
if (override) return override
|
|
137
|
+
const chord = current[id]
|
|
138
|
+
return typeof chord === 'string' ? parseChords(chord) : []
|
|
85
139
|
}
|
|
86
140
|
|
|
87
141
|
/**
|
|
@@ -369,5 +369,19 @@
|
|
|
369
369
|
[data-edrv-view] .edrv-rules-save { height: 24px; padding: 0 14px; border: none; border-radius: 6px; background: var(--dsw-alias-brand-primary, #0f9d58); color: var(--dsw-alias-label-primary-inverted, #fff); font-size: 12px; cursor: pointer; }
|
|
370
370
|
[data-edrv-view] .edrv-rules-save:disabled { opacity: .5; cursor: default; }
|
|
371
371
|
|
|
372
|
+
/* 命令栏(Ctrl+Shift+P / F1):body 级 portal 浮层,故不依赖编辑区几何 */
|
|
373
|
+
[data-edrv-view] .edrv-palette-mask { position: fixed; inset: 0; z-index: 190; background: rgba(0, 0, 0, .28); }
|
|
374
|
+
[data-edrv-view] .edrv-palette { position: fixed; top: 12vh; left: 50%; transform: translateX(-50%); z-index: 191; width: min(580px, calc(100vw - 32px)); max-height: 64vh; display: flex; flex-direction: column; border-radius: 10px; background: var(--dsw-alias-bg-overlay, #ffffff); border: 1px solid var(--dsw-alias-border-l2, #cbd2d9); box-shadow: 0 12px 34px rgba(31, 41, 51, .24); overflow: hidden; }
|
|
375
|
+
[data-edrv-view] .edrv-palette-input { height: 38px; flex: 0 0 auto; padding: 0 12px; border: none; border-bottom: 1px solid var(--dsw-alias-border-l1, #e0e6e8); background: transparent; color: var(--dsw-alias-label-primary, #1f2933); font-size: 13px; outline: none; }
|
|
376
|
+
[data-edrv-view] .edrv-palette-list { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 4px; }
|
|
377
|
+
[data-edrv-view] .edrv-palette-row { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border-radius: 6px; font-size: 12px; color: var(--dsw-alias-label-primary, #1f2933); cursor: pointer; }
|
|
378
|
+
[data-edrv-view] .edrv-palette-row.edrv-palette-sel { background: var(--dsw-alias-interactive-bg-hover, rgba(15, 157, 88, .10)); }
|
|
379
|
+
[data-edrv-view] .edrv-palette-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
380
|
+
[data-edrv-view] .edrv-palette-cat { flex: 0 0 auto; font-size: 10px; padding: 1px 7px; border-radius: 999px; background: var(--dsw-alias-bg-layer-2, #f0f4f4); color: var(--dsw-alias-label-tertiary, #7b8794); }
|
|
381
|
+
[data-edrv-view] .edrv-palette-key { flex: 0 0 auto; font-family: var(--ds-font-family-code, ui-monospace, monospace); font-size: 10px; padding: 1px 6px; border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); border-radius: 5px; background: var(--dsw-alias-bg-layer-1, #f7f9fa); color: var(--dsw-alias-label-secondary, #52606d); }
|
|
382
|
+
[data-edrv-view] .edrv-palette-empty { padding: 14px 10px; font-size: 12px; color: var(--dsw-alias-label-tertiary, #7b8794); text-align: center; }
|
|
383
|
+
[data-edrv-view] .edrv-palette-notice { flex: 0 0 auto; padding: 6px 10px; border-top: 1px solid var(--dsw-alias-border-l1, #e0e6e8); font-size: 11px; color: var(--dsw-alias-state-warn-primary, #b7791f); }
|
|
384
|
+
|
|
385
|
+
|
|
372
386
|
|
|
373
387
|
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 命令栏浮层(Ctrl+Shift+P / F1)。
|
|
3
|
+
* 订阅 commandPaletteStore 的开关状态;候选来自注入的指令注册表(window.dsh.edrvCommands),
|
|
4
|
+
* 过滤/排序复用 commandSearch 纯函数;↑↓ 选择、Enter 执行、Esc 或点击遮罩关闭。
|
|
5
|
+
* 浮层经 createPortal 渲染到 body,故不受编辑区三种形态(官方侧栏/better-sidebar/中央页签)影响;
|
|
6
|
+
* 宿主单实例由 store 的 claimPaletteHost 保证(另一个宿主自动返回 null)。
|
|
7
|
+
* 作者 ddj 2026年09月10号
|
|
8
|
+
*/
|
|
9
|
+
import React from 'react'
|
|
10
|
+
import { createPortal } from 'react-dom'
|
|
11
|
+
import { REGISTRY_GLOBAL } from '../commandGlobals.js'
|
|
12
|
+
import { filterCommands } from '../commandSearch.js'
|
|
13
|
+
import { createCommandRegistry } from '../commandRegistry.js'
|
|
14
|
+
import type { CommandRegistry } from '../commandRegistry.js'
|
|
15
|
+
import { chordOf } from '../keybindings.js'
|
|
16
|
+
import {
|
|
17
|
+
claimPaletteHost, closeCommandPalette, isPaletteOpen, registryRef, releasePaletteHost,
|
|
18
|
+
runPaletteCommand, subscribePalette,
|
|
19
|
+
} from '../commandPaletteStore.js'
|
|
20
|
+
import type { CommandDef } from './commandCatalog.js'
|
|
21
|
+
|
|
22
|
+
/** 空注册表(宿主未注入时的安全降级:命令栏可开但无候选)。 */
|
|
23
|
+
let fallbackRegistry: CommandRegistry | null = null
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 读取指令注册表:① 装配期存入的模块引用(首选,不依赖全局)
|
|
27
|
+
* ② window[REGISTRY_GLOBAL] 镜像(第三方/调试场景)③ 空表降级(仅装配异常时)。
|
|
28
|
+
* @author ddj 2026年09月10号
|
|
29
|
+
* @returns 指令注册表
|
|
30
|
+
*/
|
|
31
|
+
function useRegistry(): CommandRegistry {
|
|
32
|
+
const injected = registryRef()
|
|
33
|
+
if (injected) return injected
|
|
34
|
+
const mirrored = typeof window === 'undefined'
|
|
35
|
+
? undefined
|
|
36
|
+
: (window as unknown as Record<string, CommandRegistry | undefined>)[REGISTRY_GLOBAL]
|
|
37
|
+
if (mirrored) return mirrored
|
|
38
|
+
if (!fallbackRegistry) fallbackRegistry = createCommandRegistry()
|
|
39
|
+
return fallbackRegistry
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 命令栏每一行的展示信息。 */
|
|
43
|
+
interface PaletteRow {
|
|
44
|
+
command: CommandDef
|
|
45
|
+
chord: string | null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 组装候选行(先按可用性过滤,再按查询排序)。
|
|
50
|
+
* @author ddj 2026年09月10号
|
|
51
|
+
* @param registry 指令注册表
|
|
52
|
+
* @param query 用户输入
|
|
53
|
+
* @returns 候选行
|
|
54
|
+
*/
|
|
55
|
+
function rowsOf(registry: CommandRegistry, query: string): PaletteRow[] {
|
|
56
|
+
return filterCommands(registry.available(), query)
|
|
57
|
+
.map((command) => ({ command, chord: chordOf(command.id) }))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 命令栏浮层(未展开/非宿主时渲染 null)。
|
|
62
|
+
* @author ddj 2026年09月10号
|
|
63
|
+
* @returns 浮层 React 元素或 null
|
|
64
|
+
*/
|
|
65
|
+
export function CommandPalette(): React.ReactElement | null {
|
|
66
|
+
const open = React.useSyncExternalStore(subscribePalette, isPaletteOpen)
|
|
67
|
+
const registry = useRegistry()
|
|
68
|
+
const [token] = React.useState(() => (claimPaletteHost() ? {} : null))
|
|
69
|
+
const [query, setQuery] = React.useState('')
|
|
70
|
+
const [selected, setSelected] = React.useState(0)
|
|
71
|
+
const [notice, setNotice] = React.useState('')
|
|
72
|
+
const inputRef = React.useRef<HTMLInputElement | null>(null)
|
|
73
|
+
const host = token !== null
|
|
74
|
+
|
|
75
|
+
React.useEffect(() => () => releasePaletteHost(token), [token])
|
|
76
|
+
React.useEffect(() => {
|
|
77
|
+
if (!open) return undefined
|
|
78
|
+
setQuery('')
|
|
79
|
+
setSelected(0)
|
|
80
|
+
setNotice('')
|
|
81
|
+
const timer = setTimeout(() => inputRef.current?.focus(), 0)
|
|
82
|
+
return () => clearTimeout(timer)
|
|
83
|
+
}, [open])
|
|
84
|
+
|
|
85
|
+
const rows = React.useMemo(
|
|
86
|
+
() => (open && host ? rowsOf(registry, query) : []),
|
|
87
|
+
[open, host, registry, query],
|
|
88
|
+
)
|
|
89
|
+
const active = rows.length ? Math.min(selected, rows.length - 1) : 0
|
|
90
|
+
|
|
91
|
+
React.useEffect(() => {
|
|
92
|
+
if (!open) return
|
|
93
|
+
const node = document.querySelector('.edrv-palette-row.edrv-palette-sel')
|
|
94
|
+
const scroll = (node as HTMLElement | null)?.scrollIntoView
|
|
95
|
+
if (typeof scroll === 'function') scroll.call(node, { block: 'nearest' })
|
|
96
|
+
}, [open, active, rows.length])
|
|
97
|
+
|
|
98
|
+
/** 执行候选行(执行器内部会做可用性校验与异常上报)。 */
|
|
99
|
+
const execute = (row: PaletteRow | undefined): void => {
|
|
100
|
+
if (!row) return
|
|
101
|
+
closeCommandPalette()
|
|
102
|
+
if (!runPaletteCommand(row.command.id)) setNotice('命令未执行:' + row.command.label)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
|
|
106
|
+
event.stopPropagation()
|
|
107
|
+
if (event.key === 'Escape') {
|
|
108
|
+
event.preventDefault()
|
|
109
|
+
closeCommandPalette()
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
113
|
+
event.preventDefault()
|
|
114
|
+
if (!rows.length) return
|
|
115
|
+
const step = event.key === 'ArrowDown' ? 1 : rows.length - 1
|
|
116
|
+
setSelected((value) => (Math.min(value, rows.length - 1) + step) % rows.length)
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
if (event.key === 'Enter') {
|
|
120
|
+
event.preventDefault()
|
|
121
|
+
execute(rows[active])
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!open || !host || typeof document === 'undefined') return null
|
|
126
|
+
const list = rows.length
|
|
127
|
+
? rows.map((row, index) => React.createElement('div', {
|
|
128
|
+
key: row.command.id,
|
|
129
|
+
className: 'edrv-palette-row' + (index === active ? ' edrv-palette-sel' : ''),
|
|
130
|
+
onMouseEnter: () => setSelected(index),
|
|
131
|
+
onClick: () => execute(row),
|
|
132
|
+
},
|
|
133
|
+
React.createElement('span', { className: 'edrv-palette-label' }, row.command.label),
|
|
134
|
+
React.createElement('span', { className: 'edrv-palette-cat' }, row.command.category),
|
|
135
|
+
row.chord ? React.createElement('kbd', { className: 'edrv-palette-key' }, row.chord) : null))
|
|
136
|
+
: React.createElement('div', { className: 'edrv-palette-empty' }, '无匹配命令')
|
|
137
|
+
|
|
138
|
+
const panel = React.createElement('div', { className: 'edrv-palette', 'data-edrv-palette': '1', onKeyDown },
|
|
139
|
+
React.createElement('input', {
|
|
140
|
+
ref: inputRef,
|
|
141
|
+
className: 'edrv-palette-input',
|
|
142
|
+
value: query,
|
|
143
|
+
placeholder: '输入命令名称…(↑↓ 选择 · Enter 执行 · Esc 关闭)',
|
|
144
|
+
spellCheck: false,
|
|
145
|
+
onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
146
|
+
setQuery(event.target.value)
|
|
147
|
+
setSelected(0)
|
|
148
|
+
setNotice('')
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
React.createElement('div', { className: 'edrv-palette-list' }, list),
|
|
152
|
+
notice ? React.createElement('div', { className: 'edrv-palette-notice' }, notice) : null)
|
|
153
|
+
|
|
154
|
+
const overlay = React.createElement('div', { 'data-edrv-view': '1' },
|
|
155
|
+
React.createElement('div', { className: 'edrv-palette-mask', onClick: () => closeCommandPalette() }),
|
|
156
|
+
panel)
|
|
157
|
+
return createPortal(overlay, document.body)
|
|
158
|
+
}
|
|
@@ -11,6 +11,7 @@ import { summarize } from '../state/records.js'
|
|
|
11
11
|
import { nextDiffPath } from '../diffDock.js'
|
|
12
12
|
import { readDiffDock, subscribeDiffDock } from '../diffDockStore.js'
|
|
13
13
|
import { DiffBox } from './DiffBox.js'
|
|
14
|
+
import { CommandPalette } from './CommandPalette.js'
|
|
14
15
|
|
|
15
16
|
const nextIndexBySession = new Map()
|
|
16
17
|
|
|
@@ -60,12 +61,17 @@ export function ConversationDiffDock(props) {
|
|
|
60
61
|
}, content)
|
|
61
62
|
|
|
62
63
|
const editorSnapshot = readDiffDock(sessionId)
|
|
64
|
+
// 命令栏浮层的兜底宿主:本 dock 是每会话常驻 slot(编辑区未挂载时仍能唤起命令栏);
|
|
65
|
+
// 与 EditorView 同时挂载时由 commandPaletteStore 的单实例认领保证只渲染一份。
|
|
66
|
+
const palette = React.createElement(CommandPalette, { key: 'edrv-palette', sessionId })
|
|
63
67
|
if (editorSnapshot) {
|
|
64
|
-
if (editorSnapshot.mode === 'editor-empty' && !editorSnapshot.fileTotal) return
|
|
65
|
-
return
|
|
68
|
+
if (editorSnapshot.mode === 'editor-empty' && !editorSnapshot.fileTotal) return palette
|
|
69
|
+
return React.createElement(React.Fragment, null,
|
|
70
|
+
renderDock(React.createElement(DiffBox, Object.assign({}, editorSnapshot, { dock: true })), sessionId),
|
|
71
|
+
palette)
|
|
66
72
|
}
|
|
67
73
|
|
|
68
|
-
if (!sessionId || !summary?.pendingFiles?.length) return
|
|
74
|
+
if (!sessionId || !summary?.pendingFiles?.length) return palette
|
|
69
75
|
|
|
70
76
|
const paths = summary.pendingFiles.map((file) => file.path)
|
|
71
77
|
const click = () => {
|
|
@@ -75,10 +81,12 @@ export function ConversationDiffDock(props) {
|
|
|
75
81
|
if (next.path) openDiffView(next.path)
|
|
76
82
|
}
|
|
77
83
|
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
return React.createElement(React.Fragment, null,
|
|
85
|
+
renderDock(React.createElement(DiffBox, {
|
|
86
|
+
mode: 'chat',
|
|
87
|
+
dock: true,
|
|
88
|
+
fileTotal: paths.length,
|
|
89
|
+
onOpenNextFile: click,
|
|
90
|
+
}), sessionId || 'chat'),
|
|
91
|
+
palette)
|
|
84
92
|
}
|
|
@@ -19,6 +19,7 @@ import { createDiffRenderer } from '../monaco/diffRender.js'
|
|
|
19
19
|
import { ST, callIdAttr, noopHunk, summarize } from '../state/records.js'
|
|
20
20
|
import { diffRegions } from '../state/regions.js'
|
|
21
21
|
import { QuickOpen } from './QuickOpen.js'
|
|
22
|
+
import { CommandPalette } from './CommandPalette.js'
|
|
22
23
|
import { DiffLauncher } from './DiffLauncher.js'
|
|
23
24
|
import { SidebarView } from '../sidebar/SidebarView.js'
|
|
24
25
|
import { clearDiffDock, publishDiffDock } from '../diffDockStore.js'
|
|
@@ -131,6 +132,10 @@ export function EditorView(props) {
|
|
|
131
132
|
const navBackRef = React.useRef(null) // 后退动作最新闭包(窗口级键盘监听读取)
|
|
132
133
|
const navForwardRef = React.useRef(null) // 前进动作最新闭包(窗口级键盘监听读取)
|
|
133
134
|
const cycleTabRef = React.useRef(null) // 页签循环动作最新闭包(Ctrl+Alt+←/→、Ctrl+PgUp/PgDn)
|
|
135
|
+
const rowNavColRef = React.useRef(null) // 整行上下移动的期望列(连续移动保持列位)
|
|
136
|
+
const rowNavMoveRef = React.useRef(false) // 本次光标变化是否由整行移动触发(否则清空期望列)
|
|
137
|
+
const activeRef = React.useRef(null) // 当前活动文件的最新值(空依赖闭包/指令回调读取)
|
|
138
|
+
activeRef.current = active
|
|
134
139
|
const tabsHostRef = React.useRef(null) // 页签栏容器(切换后把当前页签滚入可见区)
|
|
135
140
|
const [navTick, setNavTick] = React.useState(0) // 历史可用性版本(按钮 disabled 重渲染)
|
|
136
141
|
const hoverRegionsRef = React.useRef([]) // 当前 pending 区域镜像(稳定回调读取)
|
|
@@ -799,6 +804,66 @@ export function EditorView(props) {
|
|
|
799
804
|
return () => window.removeEventListener('keydown', onKey, true)
|
|
800
805
|
}, [])
|
|
801
806
|
|
|
807
|
+
// 整行上下移动的实现见下方「指令系统接线」effect(moveRow 单点定义,命令栏与键位共用)。
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* 指令系统接线:命令栏/键位/第三方派发的 `edrv.command.*` 事件落到编辑器动作。
|
|
811
|
+
* 全部动作经最新闭包执行(与窗口级键位监听同源);事件名逐条字面书写,
|
|
812
|
+
* 便于与指令目录(ui/commandCatalog)静态对照(tests/commands.test.ts 有断言)。
|
|
813
|
+
* @author ddj 2026年09月10号
|
|
814
|
+
*/
|
|
815
|
+
React.useEffect(() => {
|
|
816
|
+
/** 整行上下移动:Monaco 内置 cursorUp/Down 会丢列位,故按期望列自行定位(连续移动保持同列)。 */
|
|
817
|
+
const moveRow = (step) => {
|
|
818
|
+
const ed = editorRef.current
|
|
819
|
+
const model = ed?.getModel?.()
|
|
820
|
+
const pos = ed?.getPosition?.()
|
|
821
|
+
if (!ed || !model || !pos) return
|
|
822
|
+
const col = rowNavColRef.current ?? pos.column
|
|
823
|
+
const line = Math.max(1, Math.min(model.getLineCount(), pos.lineNumber + step))
|
|
824
|
+
if (line === pos.lineNumber) return
|
|
825
|
+
const maxCol = model.getLineMaxColumn(line)
|
|
826
|
+
rowNavColRef.current = col
|
|
827
|
+
rowNavMoveRef.current = true
|
|
828
|
+
ed.setPosition({ lineNumber: line, column: Math.max(1, Math.min(col, maxCol)) })
|
|
829
|
+
ed.revealLineInCenterIfOutsideViewport?.(line)
|
|
830
|
+
}
|
|
831
|
+
const handlers = [
|
|
832
|
+
['edrv.command.save', () => { if (editorRef.current?.getModel?.()) { flushSave(); doSaveRef.current?.(false) } }],
|
|
833
|
+
// 快速打开由 QuickOpen 自己接该事件(它持有搜索框 ref),此处不重复实现
|
|
834
|
+
['edrv.command.toggleSidebar', () => setSidebarOn((v) => !v)],
|
|
835
|
+
['edrv.command.searchInFiles', () => {
|
|
836
|
+
setSidebarOn(true)
|
|
837
|
+
setActivePanel('search')
|
|
838
|
+
setTimeout(() => window.dispatchEvent(new CustomEvent('edrv:search-focus')), 0)
|
|
839
|
+
}],
|
|
840
|
+
['edrv.command.navigateBack', () => navBackRef.current?.()],
|
|
841
|
+
['edrv.command.navigateForward', () => navForwardRef.current?.()],
|
|
842
|
+
['edrv.command.nextTab', () => cycleTabRef.current?.(1)],
|
|
843
|
+
['edrv.command.prevTab', () => cycleTabRef.current?.(-1)],
|
|
844
|
+
['edrv.command.nextEditorRow', () => moveRow(1)],
|
|
845
|
+
['edrv.command.prevEditorRow', () => moveRow(-1)],
|
|
846
|
+
['edrv.command.goToDefinition', () => { const ed = editorRef.current; if (ed) void runGoToDefinition(ed) }],
|
|
847
|
+
['edrv.command.findReferences', () => { const ed = editorRef.current; if (ed) void runFindReferences(ed) }],
|
|
848
|
+
['edrv.command.triggerAi', () => editorRef.current?.trigger?.('edrv-ai', 'editor.action.inlineSuggest.trigger', null)],
|
|
849
|
+
['edrv.command.openInExplorer', () => {
|
|
850
|
+
const path = activeRef.current
|
|
851
|
+
if (!path) { setStatus('无活动文件'); return }
|
|
852
|
+
menuHandlersRef.current?.openInExplorer?.(path)
|
|
853
|
+
}],
|
|
854
|
+
]
|
|
855
|
+
const byName = new Map(handlers)
|
|
856
|
+
const onCommand = (event) => {
|
|
857
|
+
const action = byName.get(event.type)
|
|
858
|
+
if (!action) return
|
|
859
|
+
try { action() } catch (error) { dbg(sessionId, '指令 ' + event.type + ' 失败:' + String(error)) }
|
|
860
|
+
}
|
|
861
|
+
for (const [eventName] of handlers) window.addEventListener(eventName, onCommand)
|
|
862
|
+
return () => {
|
|
863
|
+
for (const [eventName] of handlers) window.removeEventListener(eventName, onCommand)
|
|
864
|
+
}
|
|
865
|
+
}, [sessionId])
|
|
866
|
+
|
|
802
867
|
// 活动页签滚动可见(页签栏溢出时键盘切换/打开文件后把当前页签带回视野):
|
|
803
868
|
// 只调整页签栏自身 scrollLeft,不触发页面滚动。
|
|
804
869
|
React.useEffect(() => {
|
|
@@ -1065,6 +1130,9 @@ export function EditorView(props) {
|
|
|
1065
1130
|
})
|
|
1066
1131
|
ed.onDidChangeCursorPosition((e) => {
|
|
1067
1132
|
setCursor('Ln ' + e.position.lineNumber + ', Col ' + e.position.column)
|
|
1133
|
+
// 整行移动(↓↑)刚定位时保留期望列;其余光标移动(点击/打字/方向键)清空期望列
|
|
1134
|
+
if (rowNavMoveRef.current) rowNavMoveRef.current = false
|
|
1135
|
+
else rowNavColRef.current = null
|
|
1068
1136
|
// 导航历史:同文件内光标移动防抖记录(程序化恢复触发的位置与栈顶去重,无副作用)
|
|
1069
1137
|
if (navCursorTimerRef.current) clearTimeout(navCursorTimerRef.current)
|
|
1070
1138
|
navCursorTimerRef.current = setTimeout(() => {
|
|
@@ -1126,6 +1194,20 @@ export function EditorView(props) {
|
|
|
1126
1194
|
run: (edx) => { void runFindReferences(edx) },
|
|
1127
1195
|
})
|
|
1128
1196
|
ed.addCommand(m.KeyCode.F12, () => { void runGoToDefinition(ed) })
|
|
1197
|
+
// 整行上下移动 + 命令栏:只登记 Monaco 右键菜单入口(不绑键位)。
|
|
1198
|
+
// 键位由指令桥统一 capture 派发 `edrv.command.*`,此处再绑一次会双执行。
|
|
1199
|
+
ed.addAction({
|
|
1200
|
+
id: 'edrv.prevEditorRow', label: '上一编辑行', contextMenuGroupId: '1_edrv',
|
|
1201
|
+
run: () => window.dispatchEvent(new CustomEvent('edrv.command.prevEditorRow')),
|
|
1202
|
+
})
|
|
1203
|
+
ed.addAction({
|
|
1204
|
+
id: 'edrv.nextEditorRow', label: '下一编辑行', contextMenuGroupId: '1_edrv',
|
|
1205
|
+
run: () => window.dispatchEvent(new CustomEvent('edrv.command.nextEditorRow')),
|
|
1206
|
+
})
|
|
1207
|
+
ed.addAction({
|
|
1208
|
+
id: 'edrv.showCommands', label: '显示所有命令', contextMenuGroupId: '1_edrv',
|
|
1209
|
+
run: () => window.dispatchEvent(new CustomEvent('edrv.command.showCommands')),
|
|
1210
|
+
})
|
|
1129
1211
|
bindLspEditor(ed)
|
|
1130
1212
|
bindLspUnderline(ed, m)
|
|
1131
1213
|
// AI 补全:编辑器实例登记(差异静默判定用)+ Alt+\ 手动触发 ghost text
|
|
@@ -1652,7 +1734,7 @@ export function EditorView(props) {
|
|
|
1652
1734
|
} else if (!active) {
|
|
1653
1735
|
body = React.createElement('div', { className: 'edrv-empty' },
|
|
1654
1736
|
React.createElement('div', null, '暂无打开的文件'),
|
|
1655
|
-
React.createElement('div', { style: { fontSize: 12 } }, '使用右上搜索框 (' + (chordOf('edrv.quickOpen') ?? 'Ctrl+P') + ') 打开工作区文件;agent 修改文件后顶部会出现差异角标'))
|
|
1737
|
+
React.createElement('div', { style: { fontSize: 12 } }, '使用右上搜索框 (' + (chordOf('edrv.quickOpen') ?? 'Ctrl+P') + ') 打开工作区文件;' + (chordOf('edrv.showCommands') ?? 'Ctrl+Shift+P') + ' 打开命令栏;agent 修改文件后顶部会出现差异角标'))
|
|
1656
1738
|
} else if (content === null && loadError) {
|
|
1657
1739
|
body = loadingBody('文件加载失败:' + loadError, 0, () => {
|
|
1658
1740
|
setLoadError(null)
|
|
@@ -1831,14 +1913,19 @@ export function EditorView(props) {
|
|
|
1831
1913
|
mainCol)
|
|
1832
1914
|
|
|
1833
1915
|
const baseStyle = { minHeight: 0, display: 'flex', flexDirection: 'column', background: 'var(--dsw-alias-bg-base,transparent)', overflow: 'hidden' }
|
|
1916
|
+
// 命令栏浮层(Ctrl+Shift+P / F1):portal 到 body,但需挂在插件自己的 React 树里;
|
|
1917
|
+
// 三种布局形态都渲染 EditorView,故这里挂载即可,多宿主由 store 单实例认领兜底。
|
|
1918
|
+
const paletteEl = React.createElement(CommandPalette, { key: 'edrv-palette', sessionId })
|
|
1834
1919
|
const rootEl = layout === 'side'
|
|
1835
1920
|
? React.createElement('div', { ref: viewRootRef, 'data-edrv-view': '1', 'data-edrv-layout': 'side', className: 'edrv-view-side', style: Object.assign({}, baseStyle, { height: '100%' }) },
|
|
1836
1921
|
editorRow,
|
|
1837
1922
|
menuBackdrop,
|
|
1838
|
-
tabMenuEl
|
|
1923
|
+
tabMenuEl,
|
|
1924
|
+
paletteEl)
|
|
1839
1925
|
: React.createElement('div', { ref: viewRootRef, 'data-edrv-view': '1', style: Object.assign({}, baseStyle, { height: 'var(--edrv-editor-height, 100%)', maxHeight: 'var(--edrv-editor-height, 100%)' }) },
|
|
1840
1926
|
editorRow,
|
|
1841
1927
|
menuBackdrop,
|
|
1842
|
-
tabMenuEl
|
|
1928
|
+
tabMenuEl,
|
|
1929
|
+
paletteEl)
|
|
1843
1930
|
return rootEl
|
|
1844
1931
|
}
|
|
@@ -174,7 +174,7 @@ export function KeybindingsPanel() {
|
|
|
174
174
|
|
|
175
175
|
return React.createElement('section', { className: 'vsm-general-page' },
|
|
176
176
|
React.createElement('h2', null, '快捷键'),
|
|
177
|
-
React.createElement('p', null, '
|
|
177
|
+
React.createElement('p', null, '配置编辑器视图的快捷键(命令栏 / 保存 / 快速打开 / 侧边栏 / 搜索 / 后退前进 / 页签与编辑行导航)。修改即时写入配置,重新聚焦编辑器后生效;命令栏(默认 Ctrl+Shift+P 或 F1)可搜索并执行下列全部命令。'),
|
|
178
178
|
unavailable && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, '设置服务暂不可用,当前使用默认键位。'),
|
|
179
179
|
error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
|
|
180
180
|
message && React.createElement('div', { className: 'vsm-kb-message' }, message),
|
|
@@ -49,17 +49,25 @@ export function QuickOpen(props) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
React.useEffect(() => {
|
|
52
|
+
const openBox = () => {
|
|
53
|
+
inputRef.current?.focus?.()
|
|
54
|
+
setOpen(true)
|
|
55
|
+
}
|
|
52
56
|
const onKey = (e) => {
|
|
53
57
|
if (matchEvent(e, bindingsOf('edrv.quickOpen'))) {
|
|
54
58
|
e.preventDefault(); e.stopPropagation()
|
|
55
|
-
|
|
56
|
-
setOpen(true)
|
|
59
|
+
openBox()
|
|
57
60
|
} else if (e.key === 'Escape') {
|
|
58
61
|
setOpen(false); inputRef.current?.blur?.()
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
64
|
window.addEventListener('keydown', onKey, true)
|
|
62
|
-
|
|
65
|
+
// 指令系统入口(命令栏「快速打开文件」):与键位复用同一动作
|
|
66
|
+
window.addEventListener('edrv.command.quickOpen', openBox)
|
|
67
|
+
return () => {
|
|
68
|
+
window.removeEventListener('keydown', onKey, true)
|
|
69
|
+
window.removeEventListener('edrv.command.quickOpen', openBox)
|
|
70
|
+
}
|
|
63
71
|
}, [])
|
|
64
72
|
|
|
65
73
|
const dirText = (p) => {
|