dsh-vscode-mode 0.10.0 → 0.12.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 +62 -11
- package/lib/client.js +2879 -1030
- package/lib/client.js.map +1 -1
- package/lib/index.js +2227 -1056
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/activePathStore.ts +28 -0
- package/src/client/commandBridge.ts +9 -41
- package/src/client/dap/hover.ts +2 -7
- package/src/client/index.ts +116 -14
- package/src/client/keybindings.ts +19 -73
- package/src/client/luaReservedWords.ts +10 -0
- package/src/client/monaco/lsp/keywords.ts +121 -0
- package/src/client/monaco/lsp/lspClient.ts +68 -6
- package/src/client/monaco/lsp/memberScope.ts +90 -0
- package/src/client/monaco/lsp/providers.ts +14 -4
- package/src/client/settingsDocRouter.ts +52 -0
- package/src/client/shortcutsOfficial.ts +451 -0
- package/src/client/sidebar/menuItems.ts +1 -0
- package/src/client/sidebar/panels/RulesPanel.ts +15 -11
- package/src/client/sidebar/types.ts +2 -0
- package/src/client/styles/editor.css +143 -37
- package/src/client/svnActions.ts +4 -4
- package/src/client/ui/ContextMenu.ts +4 -2
- package/src/client/ui/EditorView.ts +142 -135
- package/src/client/ui/McpSettings.ts +4 -5
- package/src/client/ui/QuickOpen.ts +10 -11
- package/src/client/ui/ShortcutSettings.ts +294 -0
- package/src/client/ui/SvnLogDialog.ts +200 -69
- package/src/client/ui/SvnUpdateDialog.ts +472 -0
- package/src/client/ui/commandCatalog.ts +36 -41
- package/src/client/ui/svnUpdateModel.ts +135 -0
- package/src/fileOpenSettings.ts +3 -0
- package/src/index.ts +2 -0
- package/src/lsp/completionCap.ts +80 -0
- package/src/lsp/manager.ts +42 -10
- package/src/lsp/rpc.ts +8 -5
- package/src/lsp/server.ts +18 -4
- package/src/rpc.ts +74 -2
- package/src/rules.ts +100 -44
- package/src/settingsDocPath.ts +30 -0
- package/src/shared/keybindings.ts +253 -0
- package/src/shared/rpc.ts +12 -2
- package/src/shared/rules.ts +9 -1
- package/src/shared/svn.ts +38 -0
- package/src/svn.ts +102 -7
- package/src/svnText.ts +22 -13
- package/src/svnUpdateJob.ts +588 -0
- package/src/client/ui/KeybindingsPanel.ts +0 -190
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ SVN 集成(侧栏变更面板/日志弹窗/并排差异)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
5
5
|
"icon": "assets/icon.svg",
|
|
6
6
|
"keywords": [
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 活动编辑器文件路径单点(模块级,不依赖 React)。
|
|
3
|
+
* 命令目录(ui/commandCatalog)是模块级常量、拿不到 EditorView 实例;其可用性判定
|
|
4
|
+
* 需要「当前活动文件是什么」(如 Markdown 预览切换仅 .md 活动文件吞键),故由
|
|
5
|
+
* EditorView 把 active 状态镜像到本 store,目录按需读取。
|
|
6
|
+
* 作者 ddj 2026年10月
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** 当前活动文件路径(null = 无活动文件/编辑器未挂载)。 */
|
|
10
|
+
let activePath: string | null = null
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 镜像活动文件路径(EditorView 渲染同步点调用;卸载时回写 null)。
|
|
14
|
+
* @author ddj 2026年10月
|
|
15
|
+
* @param path 活动文件路径或 null
|
|
16
|
+
*/
|
|
17
|
+
export function setActiveEditorPath(path: string | null): void {
|
|
18
|
+
activePath = path
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 读取当前活动文件路径。
|
|
23
|
+
* @author ddj 2026年10月
|
|
24
|
+
* @returns 活动文件路径或 null
|
|
25
|
+
*/
|
|
26
|
+
export function activeEditorPath(): string | null {
|
|
27
|
+
return activePath
|
|
28
|
+
}
|
|
@@ -1,48 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode client — 指令执行桥(装配层)。
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* 职责两件:① 把内置指令目录注册进注册表(命令栏候选源);② 注入命令栏执行器。
|
|
4
|
+
* 键位派发全部归官方 shortcuts 机制(shortcutsOfficial.registerOfficialShortcuts 注册官方命令,
|
|
5
|
+
* resolve 走注册表 run)——本桥不再挂窗口键位监听,避免与官方派发双执行。
|
|
5
6
|
* 编辑器动作本身由 EditorView / QuickOpen 监听 `edrv.command.<action>` 窗口事件完成——桥不持有
|
|
6
7
|
* React 状态,因此指令系统可在编辑器挂载前装配、卸载后仍安全。
|
|
7
|
-
* 作者 ddj 2026年09月10号
|
|
8
|
+
* 作者 ddj 2026年09月10号 / 2026年10月
|
|
8
9
|
*/
|
|
9
|
-
import { BRIDGE_COMMANDS, EDITOR_COMMANDS,
|
|
10
|
+
import { BRIDGE_COMMANDS, EDITOR_COMMANDS, showCommandsDef } from './ui/commandCatalog.js'
|
|
10
11
|
import { createCommandRegistry } from './commandRegistry.js'
|
|
11
12
|
import type { CommandRegistry } from './commandRegistry.js'
|
|
12
|
-
import { bindingsOf, matchEvent } from './keybindings.js'
|
|
13
13
|
import { log } from './log.js'
|
|
14
14
|
import { openCommandPalette, setPaletteRunner, setRegistryRef } from './commandPaletteStore.js'
|
|
15
15
|
|
|
16
16
|
const bridgeLog = log.child('commands')
|
|
17
17
|
|
|
18
|
-
/** 桥接装配选项(事件目标可注入,便于单测)。 */
|
|
19
|
-
export interface CommandBridgeOptions {
|
|
20
|
-
/** 键盘事件目标(缺省 window;无 window 的纯 Node 环境自动跳过监听)。 */
|
|
21
|
-
target?: Pick<EventTarget, 'addEventListener' | 'removeEventListener'>
|
|
22
|
-
}
|
|
23
|
-
|
|
24
18
|
/** 指令执行桥句柄。 */
|
|
25
19
|
export interface CommandBridge {
|
|
26
20
|
/** 指令注册表(第三方可继续 register)。 */
|
|
27
21
|
registry: CommandRegistry
|
|
28
|
-
/**
|
|
22
|
+
/** 卸载全部内置指令(幂等)。 */
|
|
29
23
|
dispose(): void
|
|
30
24
|
}
|
|
31
25
|
|
|
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
26
|
/**
|
|
40
|
-
* 创建指令执行桥:注册内置指令 +
|
|
27
|
+
* 创建指令执行桥:注册内置指令 + 命令栏自身,注入命令栏执行器。
|
|
41
28
|
* @author ddj 2026年09月10号
|
|
42
|
-
* @param options 装配选项(事件目标注入)
|
|
43
29
|
* @returns 指令执行桥句柄
|
|
44
30
|
*/
|
|
45
|
-
export function createCommandBridge(
|
|
31
|
+
export function createCommandBridge(): CommandBridge {
|
|
46
32
|
const registry = createCommandRegistry()
|
|
47
33
|
const disposers: Array<() => void> = []
|
|
48
34
|
const palette = showCommandsDef(() => openCommandPalette('command'))
|
|
@@ -54,24 +40,7 @@ export function createCommandBridge(options: CommandBridgeOptions = {}): Command
|
|
|
54
40
|
// 命令栏候选来源:模块引用直传(旧实现依赖 window.dsh,DSH 无该命名空间 → 恒空表)
|
|
55
41
|
setRegistryRef(registry)
|
|
56
42
|
|
|
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
|
-
// 可用性前置:命令不可用时既不执行也不吞键(否则无编辑器时 Ctrl+U 会被吃掉)
|
|
63
|
-
if (!registry.isAvailable(command.id)) continue
|
|
64
|
-
event.preventDefault()
|
|
65
|
-
event.stopPropagation()
|
|
66
|
-
registry.run(command.id)
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
const target = resolveTarget(options)
|
|
71
|
-
if (target) target.addEventListener('keydown', onKey as EventListener, true)
|
|
72
|
-
|
|
73
43
|
const dispose = (): void => {
|
|
74
|
-
if (target) target.removeEventListener('keydown', onKey as EventListener, true)
|
|
75
44
|
while (disposers.length) {
|
|
76
45
|
const release = disposers.pop()
|
|
77
46
|
if (release) release()
|
|
@@ -79,7 +48,6 @@ export function createCommandBridge(options: CommandBridgeOptions = {}): Command
|
|
|
79
48
|
setPaletteRunner(null)
|
|
80
49
|
setRegistryRef(null)
|
|
81
50
|
}
|
|
82
|
-
bridgeLog.info('指令注册表已装配(' + registry.list().length + '
|
|
83
|
-
+ dispatched.length + ' 条)')
|
|
51
|
+
bridgeLog.info('指令注册表已装配(' + registry.list().length + ' 条指令;键位派发已归官方 shortcuts 机制)')
|
|
84
52
|
return { registry, dispose }
|
|
85
53
|
}
|
package/src/client/dap/hover.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { dapStore } from './store.js'
|
|
9
9
|
import { pathOfModel } from '../monaco/lsp/lspClient.js'
|
|
10
|
+
import { LUA_RESERVED_WORD_SET } from '../luaReservedWords.js'
|
|
10
11
|
import { BLOCK_ATTR, EQ_ATTR, EXPR_ATTR, HEAD_ATTR, TYPE_ATTR, VAL_ATTR, createTreeState, escapeHtml, initialHtml } from './hoverTree.js'
|
|
11
12
|
|
|
12
13
|
const GLOBAL_KEY = '__edrvDapHoverProvider__'
|
|
@@ -35,12 +36,6 @@ function treeKeyOf(target: { expression: string; lineNumber: number; startColumn
|
|
|
35
36
|
return target.expression + '@' + path + ':' + target.lineNumber + ':' + target.startColumn + '-' + target.endColumn
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
/** Lua 保留字集合(保留字不是可求值表达式,避免 adapter 返回 syntax err 噪声)。 */
|
|
39
|
-
const LUA_KEYWORDS = new Set([
|
|
40
|
-
'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function', 'goto',
|
|
41
|
-
'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true', 'until', 'while',
|
|
42
|
-
])
|
|
43
|
-
|
|
44
39
|
/** 可求值路径表达式:标识符链,成员用 . 或 : 连接(注释里的中文文本等一律不匹配)。 */
|
|
45
40
|
const EVAL_EXPR = /^[A-Za-z_][A-Za-z0-9_]*(?:[.:][A-Za-z_][A-Za-z0-9_]*)*$/
|
|
46
41
|
|
|
@@ -51,7 +46,7 @@ const EVAL_EXPR = /^[A-Za-z_][A-Za-z0-9_]*(?:[.:][A-Za-z_][A-Za-z0-9_]*)*$/
|
|
|
51
46
|
* @returns 是否可求值
|
|
52
47
|
*/
|
|
53
48
|
function isEvalExpr(text: string): boolean {
|
|
54
|
-
return EVAL_EXPR.test(text) && !
|
|
49
|
+
return EVAL_EXPR.test(text) && !LUA_RESERVED_WORD_SET.has(text)
|
|
55
50
|
}
|
|
56
51
|
|
|
57
52
|
/**
|
package/src/client/index.ts
CHANGED
|
@@ -27,6 +27,8 @@ import { createFileOpenerRegistry, scanSidebar, officialSidebarOpener, shouldCla
|
|
|
27
27
|
import type { FileOpenerRegistry } from './fileOpeners.js'
|
|
28
28
|
import { installOpenPathRouter, vscodeOpener, autoValue } from './openPathRouter.js'
|
|
29
29
|
import { patchRemoteOpen, probeRemoteOpen } from './remoteOpenRouter.js'
|
|
30
|
+
import { patchSettingsDoc, probeSettingsDoc } from './settingsDocRouter.js'
|
|
31
|
+
import { openEditorView } from './events.js'
|
|
30
32
|
import { setupExtOpen } from './externalOpen.js'
|
|
31
33
|
import { SettingsContext } from './settingsContext.js'
|
|
32
34
|
import { SIDEBAR_PLUGIN, registerSlotSafely, settingsBridge } from './compat.js'
|
|
@@ -48,9 +50,13 @@ import { createDefaultFileMenuItems } from './sidebar/menuItems.js'
|
|
|
48
50
|
import { createOutlinePanel } from './outline/index.js'
|
|
49
51
|
import { createOutlineSourceRegistry, registerBuiltinOutlineSources } from './outline/sources.js'
|
|
50
52
|
import { createLspOutlineSource } from './outline/lspSource.js'
|
|
51
|
-
import {
|
|
53
|
+
import { applyOfficialChords } from './keybindings.js'
|
|
52
54
|
import { createCommandBridge } from './commandBridge.js'
|
|
55
|
+
import type { CommandBridge } from './commandBridge.js'
|
|
53
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'
|
|
54
60
|
import { sidebarMinApply } from './sidebarMin.js'
|
|
55
61
|
import { editorLimitApply } from './editorLimit.js'
|
|
56
62
|
import { log } from './log.js'
|
|
@@ -69,31 +75,32 @@ import type { CompatAdapter } from '../shared/compat.js'
|
|
|
69
75
|
// 写死任一桥名都会在另一版本停等)——设置桥由 compat.pickSettingsBinder 运行时探测三级降级。
|
|
70
76
|
export const inject = ['slots', 'timer', 'locale', 'connection', 'remote', 'workspaces', 'sessions', 'conversation']
|
|
71
77
|
|
|
72
|
-
/**
|
|
73
|
-
let
|
|
78
|
+
/** 指令桥单例(同一 document 重复 apply 只装配一次;HMR 重挂不重复注册)。 */
|
|
79
|
+
let commandsBridge: CommandBridge | null = null
|
|
74
80
|
|
|
75
81
|
/**
|
|
76
82
|
* 装配指令注册表(指令桥 + 命令栏)。
|
|
77
83
|
* 指令桥不依赖已挂载编辑器(run 只派发窗口事件),故可在启动期装配;
|
|
78
84
|
* 命令栏浮层的宿主由 CommandPalette 自己在编辑区树内认领。
|
|
79
|
-
* @author ddj 2026年09月10号
|
|
85
|
+
* @author ddj 2026年09月10号 / 2026年10月
|
|
80
86
|
* @param ctx 客户端根上下文
|
|
81
|
-
* @returns
|
|
87
|
+
* @returns 指令桥(官方 shortcuts 注册的执行器来源)
|
|
82
88
|
*/
|
|
83
|
-
function setupCommands(ctx: any):
|
|
84
|
-
if (
|
|
85
|
-
commandsMounted = true
|
|
89
|
+
function setupCommands(ctx: any): CommandBridge {
|
|
90
|
+
if (commandsBridge) return commandsBridge
|
|
86
91
|
const bridge = createCommandBridge()
|
|
92
|
+
commandsBridge = bridge
|
|
87
93
|
ctx.provide(REGISTRY_GLOBAL, bridge.registry)
|
|
88
94
|
// 无条件镜像到 window(DSH 不创建 window.dsh,旧条件式赋值是死代码 → 命令栏空表)
|
|
89
95
|
const host = window as unknown as Record<string, unknown>
|
|
90
96
|
host[REGISTRY_GLOBAL] = bridge.registry
|
|
91
97
|
ctx.effect(() => () => {
|
|
92
98
|
bridge.dispose()
|
|
93
|
-
|
|
99
|
+
commandsBridge = null
|
|
94
100
|
if (host[REGISTRY_GLOBAL] === bridge.registry) delete host[REGISTRY_GLOBAL]
|
|
95
101
|
}, 'vscode-mode: command registry')
|
|
96
102
|
log.info('指令系统已装配:' + bridge.registry.list().length + ' 条指令,Ctrl+Shift+P 打开命令栏')
|
|
103
|
+
return bridge
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
/**
|
|
@@ -104,9 +111,32 @@ function setupCommands(ctx: any): void {
|
|
|
104
111
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
105
112
|
export function apply(ctx: any): void {
|
|
106
113
|
const schedule = (fn: () => void, ms: number) => ctx.timeout(fn, ms)
|
|
114
|
+
ctx.effect(() => {
|
|
115
|
+
let disposed = false
|
|
116
|
+
let attempts = 0
|
|
117
|
+
let restore: (() => void) | null = null
|
|
118
|
+
const install = (): void => {
|
|
119
|
+
if (disposed || restore || attempts >= 15) return
|
|
120
|
+
attempts += 1
|
|
121
|
+
const service = probeSettingsDoc(ctx)
|
|
122
|
+
if (service) {
|
|
123
|
+
restore = patchSettingsDoc(service, async () => {
|
|
124
|
+
const result = await rpc('vscode.settingsDocumentPath', {})
|
|
125
|
+
return result.ok ? { ok: true, path: result.path } : { ok: false, error: result.error }
|
|
126
|
+
}, openEditorView, (message) => log.warn(message))
|
|
127
|
+
if (restore) return
|
|
128
|
+
}
|
|
129
|
+
if (attempts < 15) schedule(install, 2000)
|
|
130
|
+
}
|
|
131
|
+
install()
|
|
132
|
+
return () => {
|
|
133
|
+
disposed = true
|
|
134
|
+
restore?.()
|
|
135
|
+
}
|
|
136
|
+
}, 'vscode-mode: settings document editor route')
|
|
107
137
|
|
|
108
138
|
// 指令系统(命令栏 + 指令注册表)先装配:命令栏在被任何 React slot 渲染前即可唤起
|
|
109
|
-
setupCommands(ctx)
|
|
139
|
+
const commands = setupCommands(ctx)
|
|
110
140
|
|
|
111
141
|
const registry: FileOpenerRegistry = createFileOpenerRegistry()
|
|
112
142
|
const workspaces = ctx.get('workspaces')
|
|
@@ -245,6 +275,17 @@ export function apply(ctx: any): void {
|
|
|
245
275
|
claimDisposer = null
|
|
246
276
|
}
|
|
247
277
|
}
|
|
278
|
+
const legacyDefaults = defaultKeybindings()
|
|
279
|
+
let officialReady = false
|
|
280
|
+
/**
|
|
281
|
+
* 获取旧版快捷键覆盖;设置桥尚未就绪时回退默认键位。
|
|
282
|
+
* @author ddj 2026年09月28号
|
|
283
|
+
* @returns 旧版键位弦表
|
|
284
|
+
*/
|
|
285
|
+
const legacyChords = (): Record<string, string> => ({
|
|
286
|
+
...legacyDefaults, ...normalizeKeybindings(bridge.scope()?.getSnapshot().value?.keybindings),
|
|
287
|
+
})
|
|
288
|
+
applyOfficialChords(legacyChords())
|
|
248
289
|
// 设置同步(fileOpenTool / 快捷键 / 侧宽 / 页签上限):桥可能晚于本插件 apply 就绪
|
|
249
290
|
// (设置桥服务不进 inject),首次命中即挂两条 effect;晚到经 bridge.whenReady 重挂一次
|
|
250
291
|
// (settingsBridge 15×2s 有界重试内命中才会触发,用尽后保持旧行为不轮询)。
|
|
@@ -265,14 +306,14 @@ export function apply(ctx: any): void {
|
|
|
265
306
|
sync()
|
|
266
307
|
return scope.subscribe(sync)
|
|
267
308
|
}, 'vscode-mode: file opener setting sync')
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
309
|
+
// 界面偏好同步:侧边栏最小宽度(sidebarMinWidth)→ 派发 edrv:sidebar-min-width 通知编辑器重夹
|
|
310
|
+
// 页签上限(maxOpenEditors)→ 派发 edrv:max-open-editors 通知编辑器复算淘汰
|
|
311
|
+
// (快捷键配置已归官方 shortcuts 机制,目录弦表同步见 'vscode-mode: official shortcuts' effect)
|
|
271
312
|
ctx.effect(() => {
|
|
272
313
|
const sync = (): void => {
|
|
273
314
|
const snapshot = scope.getSnapshot()
|
|
274
315
|
if (snapshot.status === 'loading') return
|
|
275
|
-
|
|
316
|
+
if (!officialReady) applyOfficialChords(legacyChords())
|
|
276
317
|
const minW = sidebarMinApply(snapshot.value?.sidebarMinWidth)
|
|
277
318
|
window.dispatchEvent(new CustomEvent('edrv:sidebar-min-width', { detail: { value: minW } }))
|
|
278
319
|
const limit = editorLimitApply(snapshot.value?.maxOpenEditors)
|
|
@@ -286,6 +327,58 @@ export function apply(ctx: any): void {
|
|
|
286
327
|
}
|
|
287
328
|
mountSettingsSyncs()
|
|
288
329
|
if (!settingsSyncMounted) ctx.effect(() => bridge.whenReady(mountSettingsSyncs), 'vscode-mode: settings bridge wait')
|
|
330
|
+
|
|
331
|
+
// 官方 shortcuts 机制装配(可选服务,有界重试;服务缺失仅降级告警,不阻断装配):
|
|
332
|
+
// ① 全部指令注册进官方目录(含无默认键位命令,官方 UI 可自定义绑定;冲突默认自动降级未绑定);
|
|
333
|
+
// ② 订阅官方目录 → 弦表同步(chordOf/bindingsOf 数据源,tooltip/命令栏提示跟随);
|
|
334
|
+
// ③ 旧 keybindings 设置一次性迁移进官方存储(幂等,与设置桥双就绪后有界重试)。
|
|
335
|
+
ctx.effect(() => {
|
|
336
|
+
let cancelled = false
|
|
337
|
+
let disposeRegistration: (() => void) | null = null
|
|
338
|
+
let disposeCatalog: (() => void) | null = null
|
|
339
|
+
const disposeLegacy = installLegacyKeys(commands.registry.list(), commands.registry, legacyChords)
|
|
340
|
+
const cancelProbe = awaitShortcutsService(ctx, {
|
|
341
|
+
schedule,
|
|
342
|
+
onReady: (service) => {
|
|
343
|
+
if (cancelled) return
|
|
344
|
+
if (service === null) {
|
|
345
|
+
log.warn('官方 shortcuts 服务不可用(DSH 版本过旧?),继续使用旧版快捷键派发')
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
disposeLegacy()
|
|
349
|
+
officialReady = true
|
|
350
|
+
disposeRegistration = registerOfficialShortcuts(service, commands.registry.list(), commands.registry, SHORTCUT_PROFILES)
|
|
351
|
+
disposeCatalog = bindCatalogChords(service, applyOfficialChords)
|
|
352
|
+
let attempts = 0
|
|
353
|
+
const attemptMigration = (): void => {
|
|
354
|
+
if (cancelled) return
|
|
355
|
+
const scope = bridge.scope()
|
|
356
|
+
if (!scope) {
|
|
357
|
+
if (attempts < 15) {
|
|
358
|
+
attempts += 1
|
|
359
|
+
schedule(attemptMigration, 2000)
|
|
360
|
+
}
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
void migrateLegacyKeybindings(service, scope).then((result) => {
|
|
364
|
+
if (cancelled || result.done) return
|
|
365
|
+
if (attempts < 15) {
|
|
366
|
+
attempts += 1
|
|
367
|
+
schedule(attemptMigration, 2000)
|
|
368
|
+
}
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
attemptMigration()
|
|
372
|
+
},
|
|
373
|
+
})
|
|
374
|
+
return () => {
|
|
375
|
+
cancelled = true
|
|
376
|
+
cancelProbe()
|
|
377
|
+
disposeLegacy()
|
|
378
|
+
disposeRegistration?.()
|
|
379
|
+
disposeCatalog?.()
|
|
380
|
+
}
|
|
381
|
+
}, 'vscode-mode: official shortcuts')
|
|
289
382
|
if (workspaces?.openPath) {
|
|
290
383
|
ctx.effect(() => installOpenPathRouter({
|
|
291
384
|
workspaces,
|
|
@@ -478,6 +571,15 @@ export function apply(ctx: any): void {
|
|
|
478
571
|
label: 'VSCodeMode',
|
|
479
572
|
}, () => React.createElement(SettingsContext.Provider, { value: bridge.scope() }, React.createElement(McpSettings, { openerRegistry: registry, compatSummary })))
|
|
480
573
|
|
|
574
|
+
// 独立「快捷键」设置区(与 VSCodeMode 平级):官方快捷键子页 + 插件快捷键子页。
|
|
575
|
+
// 服务经 getShortcuts 运行时探测(自愈),组件内部自带就绪重试。
|
|
576
|
+
registerSlotSafely(ctx, {
|
|
577
|
+
name: 'settings.section',
|
|
578
|
+
id: 'vscode-mode-shortcuts',
|
|
579
|
+
order: 31,
|
|
580
|
+
label: '快捷键',
|
|
581
|
+
}, () => React.createElement(ShortcutSettings, { getShortcuts: () => detectShortcuts(ctx) }))
|
|
582
|
+
|
|
481
583
|
// 卸载收尾(G4,DSH 0.1.6-alpha.2 起支持运行时卸载/重载):
|
|
482
584
|
// 注销挂在存活的 window.monaco 上的全部 Monaco provider 并复位模块状态。
|
|
483
585
|
// 不做会导致重载后重复注册(补全/跳转/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,29 @@ 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
|
-
/** 命令栏命令定义(键位自检与目录展示共用;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
|
-
|
|
49
29
|
const MODIFIERS: Record<string, 'ctrl' | 'shift' | 'alt' | 'meta'> = {
|
|
50
30
|
ctrl: 'ctrl', cmd: 'meta', meta: 'meta', shift: 'shift', alt: 'alt',
|
|
51
31
|
}
|
|
52
32
|
|
|
53
|
-
|
|
54
|
-
|
|
33
|
+
/** 弦表(官方 catalog 派生:id → 显示键位弦)。官方目录就绪前为空(tooltip 走各自的兜底文案)。 */
|
|
34
|
+
let current: Record<string, string> = {}
|
|
55
35
|
const listeners = new Set<() => void>()
|
|
56
36
|
|
|
57
|
-
checkDefaultsDrift()
|
|
58
|
-
|
|
59
37
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* @
|
|
63
|
-
* @param raw 设置 scope 的 keybindings 字段
|
|
38
|
+
* 应用官方目录弦表(id → 显示键位弦;由 shortcutsOfficial.bindCatalogChords 全量推送)。
|
|
39
|
+
* @author ddj 2026年10月
|
|
40
|
+
* @param chords 弦表(官方 catalog 派生,含用户覆盖;issue 行不收录)
|
|
64
41
|
*/
|
|
65
|
-
export function
|
|
66
|
-
current = { ...
|
|
67
|
-
|
|
68
|
-
try { listener() } catch { /* 监听器异常不影响其他订阅 */ }
|
|
69
|
-
}
|
|
42
|
+
export function applyOfficialChords(chords: Readonly<Record<string, string>>): void {
|
|
43
|
+
current = { ...chords }
|
|
44
|
+
notifyKeybindings()
|
|
70
45
|
}
|
|
71
46
|
|
|
72
47
|
/**
|
|
@@ -80,29 +55,6 @@ export function subscribeKeybindings(listener: () => void): () => void {
|
|
|
80
55
|
return () => listeners.delete(listener)
|
|
81
56
|
}
|
|
82
57
|
|
|
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
58
|
/** 通知键位订阅者(异常隔离)。 */
|
|
107
59
|
function notifyKeybindings(): void {
|
|
108
60
|
for (const listener of listeners) {
|
|
@@ -111,29 +63,23 @@ function notifyKeybindings(): void {
|
|
|
111
63
|
}
|
|
112
64
|
|
|
113
65
|
/**
|
|
114
|
-
*
|
|
115
|
-
* @author ddj 2026年08月26号 / 2026年
|
|
66
|
+
* 当前命令的键位弦(官方目录派生;未绑定/空 → null)。
|
|
67
|
+
* @author ddj 2026年08月26号 / 2026年10月
|
|
116
68
|
* @param id 命令 id
|
|
117
69
|
* @returns 键位弦或 null
|
|
118
70
|
*/
|
|
119
71
|
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
72
|
const chord = current[id]
|
|
125
73
|
return typeof chord === 'string' && chord.trim() !== '' ? chord : null
|
|
126
74
|
}
|
|
127
75
|
|
|
128
76
|
/**
|
|
129
77
|
* 当前命令的解析键位集合(未绑定/非法 → 空数组;含多候选)。
|
|
130
|
-
* @author ddj 2026年08月26号 / 2026年
|
|
78
|
+
* @author ddj 2026年08月26号 / 2026年10月
|
|
131
79
|
* @param id 命令 id
|
|
132
80
|
* @returns 解析键位数组(可能为空)
|
|
133
81
|
*/
|
|
134
82
|
export function bindingsOf(id: string): Binding[] {
|
|
135
|
-
const override = runtime.get(id)
|
|
136
|
-
if (override) return override
|
|
137
83
|
const chord = current[id]
|
|
138
84
|
return typeof chord === 'string' ? parseChords(chord) : []
|
|
139
85
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lua 5.3 保留字,供编辑器补全与调试悬停共同使用。
|
|
3
|
+
* 作者 ddj 2026年09月24号
|
|
4
|
+
*/
|
|
5
|
+
export const LUA_RESERVED_WORDS = [
|
|
6
|
+
'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function', 'goto',
|
|
7
|
+
'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true', 'until', 'while',
|
|
8
|
+
] as const
|
|
9
|
+
|
|
10
|
+
export const LUA_RESERVED_WORD_SET: ReadonlySet<string> = new Set(LUA_RESERVED_WORDS)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 为 Lua 的 LSP 补全补充服务器缺失的保留字与注解基础类型候选。
|
|
3
|
+
* 作者 ddj 2026年09月24号
|
|
4
|
+
*/
|
|
5
|
+
import { LUA_RESERVED_WORDS } from '../../luaReservedWords.js'
|
|
6
|
+
|
|
7
|
+
const LUA_BASE_TYPES = [
|
|
8
|
+
'any', 'boolean', 'integer', 'nil', 'number', 'string', 'table', 'thread', 'userdata',
|
|
9
|
+
] as const
|
|
10
|
+
const TYPE_START = /^\s*---@(?:(?:type|return)\s+|param\s+\S+\s+|field\s+(?:(?:public|private|protected|package)\s+)?\S+\s+)(.*)$/
|
|
11
|
+
|
|
12
|
+
interface KeywordRange {
|
|
13
|
+
startLineNumber: number
|
|
14
|
+
endLineNumber: number
|
|
15
|
+
startColumn: number
|
|
16
|
+
endColumn: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface KeywordItem {
|
|
20
|
+
label: string
|
|
21
|
+
kind: number
|
|
22
|
+
insertText: string
|
|
23
|
+
range: KeywordRange
|
|
24
|
+
detail: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 构造轻量本地候选,保留服务器同名项的优先权交给 mergeKeywords。
|
|
29
|
+
* @author ddj 2026年09月24号
|
|
30
|
+
* @param words 候选文本
|
|
31
|
+
* @param kind Monaco 候选类型
|
|
32
|
+
* @param range 当前单词范围
|
|
33
|
+
* @param detail 候选说明
|
|
34
|
+
* @returns 可直接交给 Monaco 的候选
|
|
35
|
+
*/
|
|
36
|
+
function keywordItems(words: readonly string[], kind: number, range: KeywordRange, detail: string): KeywordItem[] {
|
|
37
|
+
return words.map((label) => ({ label, kind, insertText: label, range, detail }))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 判断光标是否在 EmmyLua 注解的类型槽;普通注释和描述文字均不匹配。
|
|
42
|
+
* @author ddj 2026年09月24号
|
|
43
|
+
* @param model 当前文档
|
|
44
|
+
* @param position 光标所在行
|
|
45
|
+
* @param range 当前词范围
|
|
46
|
+
* @returns 是否应提示基础类型
|
|
47
|
+
*/
|
|
48
|
+
export function isLuaTypeSlot(
|
|
49
|
+
model: { getLineContent(lineNumber: number): string },
|
|
50
|
+
position: { lineNumber: number },
|
|
51
|
+
range: Pick<KeywordRange, 'startColumn'>,
|
|
52
|
+
): boolean {
|
|
53
|
+
const beforeWord = model.getLineContent(position.lineNumber).slice(0, range.startColumn - 1)
|
|
54
|
+
const match = TYPE_START.exec(beforeWord)
|
|
55
|
+
if (!match) return false
|
|
56
|
+
const context = match[1] ?? ''
|
|
57
|
+
const typePrefix = context.trimEnd()
|
|
58
|
+
if (/[.:]$/.test(typePrefix)) return false
|
|
59
|
+
return !(/\s$/.test(context) && typePrefix && !/[|&,<]$/.test(typePrefix))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 在普通代码位置生成关键字;成员名和类型注解位置只交给对应候选。
|
|
64
|
+
* @author ddj 2026年09月24号
|
|
65
|
+
* @param monaco Monaco 语言枚举
|
|
66
|
+
* @param model 当前文档
|
|
67
|
+
* @param position 光标所在行
|
|
68
|
+
* @param range 当前单词的替换范围
|
|
69
|
+
* @returns 当前上下文可用的 Lua 关键字候选
|
|
70
|
+
*/
|
|
71
|
+
export function luaKeywordsAt(
|
|
72
|
+
monaco: { languages: { CompletionItemKind: { Keyword: number } } },
|
|
73
|
+
model: { getLineContent(lineNumber: number): string },
|
|
74
|
+
position: { lineNumber: number },
|
|
75
|
+
range: KeywordRange,
|
|
76
|
+
): KeywordItem[] {
|
|
77
|
+
const line = model.getLineContent(position.lineNumber)
|
|
78
|
+
const beforeWord = line.charAt(range.startColumn - 2)
|
|
79
|
+
if (beforeWord === '.' || beforeWord === ':' || /^\s*---@/.test(line)) return []
|
|
80
|
+
return keywordItems(LUA_RESERVED_WORDS, monaco.languages.CompletionItemKind.Keyword, range, 'Lua 关键字')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 仅在注解类型槽补最基本类型,项目类型和服务器自带类型仍由 LSP 提供。
|
|
85
|
+
* @author ddj 2026年09月24号
|
|
86
|
+
* @param monaco Monaco 语言枚举
|
|
87
|
+
* @param model 当前文档
|
|
88
|
+
* @param position 光标所在行
|
|
89
|
+
* @param range 当前单词的替换范围
|
|
90
|
+
* @returns 类型候选;非注解返回空数组
|
|
91
|
+
*/
|
|
92
|
+
export function luaTypesAt(
|
|
93
|
+
monaco: { languages: { CompletionItemKind: { Keyword: number } } },
|
|
94
|
+
model: { getLineContent(lineNumber: number): string },
|
|
95
|
+
position: { lineNumber: number },
|
|
96
|
+
range: KeywordRange,
|
|
97
|
+
): KeywordItem[] {
|
|
98
|
+
if (!isLuaTypeSlot(model, position, range)) return []
|
|
99
|
+
return keywordItems(LUA_BASE_TYPES, monaco.languages.CompletionItemKind.Keyword, range, 'Lua 基础类型')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 合并 LSP 与本地关键字,服务器同名项优先并保持原顺序。
|
|
104
|
+
* @author ddj 2026年09月24号
|
|
105
|
+
* @param server 服务器候选
|
|
106
|
+
* @param keywords 本地关键字候选
|
|
107
|
+
* @returns 去重后的补全列表
|
|
108
|
+
*/
|
|
109
|
+
export function mergeKeywords<T extends { label: string }, U extends { label: string }>(
|
|
110
|
+
server: T[],
|
|
111
|
+
keywords: U[],
|
|
112
|
+
): Array<T | U> {
|
|
113
|
+
if (keywords.length === 0) return server
|
|
114
|
+
const labels = new Set(server.map((item) => item.label))
|
|
115
|
+
const merged: Array<T | U> = [...server]
|
|
116
|
+
for (const keyword of keywords) {
|
|
117
|
+
if (labels.has(keyword.label)) continue
|
|
118
|
+
merged.push(keyword)
|
|
119
|
+
}
|
|
120
|
+
return merged
|
|
121
|
+
}
|