dsh-vscode-mode 0.7.0 → 0.8.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.
Files changed (42) hide show
  1. package/README.md +23 -6
  2. package/assets/icon.svg +7 -0
  3. package/cordis.patch.yml +12 -0
  4. package/lib/client.js +1167 -188
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +845 -96
  7. package/lib/index.js.map +1 -1
  8. package/locale/en.json +4 -0
  9. package/locale/zh.json +4 -0
  10. package/package.json +3 -1
  11. package/src/client/compat.ts +134 -4
  12. package/src/client/imagePreview.ts +76 -2
  13. package/src/client/index.ts +65 -36
  14. package/src/client/monaco/lsp/lspClient.ts +46 -0
  15. package/src/client/monaco/lsp/providers.ts +245 -1
  16. package/src/client/nativeOpenStore.ts +91 -0
  17. package/src/client/officialSidebar.ts +11 -45
  18. package/src/client/outline/index.ts +1 -1
  19. package/src/client/pdf/pdfPanel.ts +18 -0
  20. package/src/client/settingsContext.ts +6 -4
  21. package/src/client/sidebar/panels/FileExplorer.ts +1 -1
  22. package/src/client/sidebar/panels/SvnPanel.ts +1 -1
  23. package/src/client/sidebar/panels/index.ts +1 -1
  24. package/src/client/styles/editor.css +5 -3
  25. package/src/client/svnStore.ts +6 -1
  26. package/src/client/ui/EditorView.ts +117 -18
  27. package/src/client/ui/McpSettings.ts +30 -0
  28. package/src/client/ui/PerfSettings.ts +4 -1
  29. package/src/client/ui/icons.ts +72 -0
  30. package/src/compat.ts +2 -2
  31. package/src/dshVersion.ts +2 -1
  32. package/src/fileOpenSettings.ts +163 -40
  33. package/src/index.ts +15 -1
  34. package/src/lsp/client.ts +24 -0
  35. package/src/lsp/rpc.ts +57 -0
  36. package/src/lsp/server.ts +272 -2
  37. package/src/perf.ts +90 -4
  38. package/src/routes.ts +13 -1
  39. package/src/rpc.ts +96 -34
  40. package/src/shared/lsp.ts +79 -0
  41. package/src/shared/nativeOpen.ts +91 -0
  42. package/src/shared/rpc.ts +146 -1
package/locale/en.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "title": "VSCodeMode",
3
+ "description": "VSCode-like coding on DSH: Monaco editor with tabs and QuickOpen, command palette, agent edit review, SVN integration, and LSP smartness."
4
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "title": "VSCodeMode",
3
+ "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(页签/QuickOpen/状态栏)、命令栏、Agent 编辑差异审查、SVN 集成与 LSP 智能。"
4
+ }
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.7.0",
3
+ "version": "0.8.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
+ "icon": "assets/icon.svg",
5
6
  "keywords": [
6
7
  "dsh",
7
8
  "dsh-plugin",
@@ -35,6 +36,7 @@
35
36
  "files": [
36
37
  "lib",
37
38
  "assets",
39
+ "locale",
38
40
  "skills",
39
41
  "src",
40
42
  "unity",
@@ -10,21 +10,97 @@ export const PLUGIN_NAME = 'dsh-vscode-mode'
10
10
  /** 外部侧栏插件名(dsh-web-ui-all 家族的文件打开能力提供方)。 */
11
11
  export const SIDEBAR_PLUGIN = 'dsh-better-sidebar'
12
12
 
13
- /** 设置 scope 的跨组件上下文形状(与官方 settingsScope 绑定的最小面)。 */
13
+ /** 设置值的已知业务字段(各设置桥透传的 namespace 内容)。 */
14
+ export interface SettingsValueLike {
15
+ fileOpenTool?: unknown
16
+ keybindings?: Record<string, string>
17
+ sidebarMinWidth?: unknown
18
+ maxOpenEditors?: unknown
19
+ /** 原生打开范围(逗号分隔后缀;0.1.7 批次新增,旧版 undefined → 默认集)。 */
20
+ nativeOpenExts?: unknown
21
+ }
22
+
23
+ /** 设置 scope 的跨组件上下文形状(与设置桥绑定的最小面:settingsScope / configForms 适配后同形)。 */
14
24
  export interface SettingsScopeLike {
15
- getSnapshot: () => { status?: string; value?: { fileOpenTool?: unknown; keybindings?: Record<string, string>; sidebarMinWidth?: unknown; maxOpenEditors?: unknown }; writable?: boolean }
25
+ getSnapshot: () => { status?: string; value?: SettingsValueLike; writable?: boolean }
16
26
  subscribe: (listener: () => void) => () => void
17
27
  set: (field: string, value: unknown) => Promise<void>
18
28
  }
19
29
 
30
+ /** DSH 0.1.7+ configForms 服务最小面(0.1.6 无此服务,探测必须可降级)。 */
31
+ interface ConfigFormsService {
32
+ get?: (entryId: string) => unknown
33
+ }
34
+
35
+ /** DSH 0.1.7+ 单个 ConfigForm 最小面(适配 SettingsScopeLike 所需子集)。 */
36
+ interface ConfigFormLike {
37
+ getSnapshot: () => { status?: string; value?: unknown; writable?: boolean }
38
+ subscribe: (listener: () => void) => () => void
39
+ set: (field: string, value: unknown) => Promise<unknown>
40
+ }
41
+
42
+ /**
43
+ * 形状守卫:判断 configForms.get 返回值是否具备适配所需的最小面。
44
+ * @author ddj 2026年09月22号
45
+ * @param form configForms.get(entryId) 的返回值
46
+ * @returns 是否为可用 ConfigForm
47
+ */
48
+ function isConfigForm(form: unknown): form is ConfigFormLike {
49
+ if (!form || typeof form !== 'object') return false
50
+ const candidate = form as Partial<ConfigFormLike>
51
+ return typeof candidate.getSnapshot === 'function'
52
+ && typeof candidate.subscribe === 'function'
53
+ && typeof candidate.set === 'function'
54
+ }
55
+
20
56
  /**
21
- * 设置桥三级优先:webUiSettings(linxin666 兼容桥)→ settingsScope(官方)→ 无。
57
+ * DSH 0.1.7 ConfigForm 适配为既有 SettingsScopeLike(调用方零改动)。
58
+ * status/value/writable 直接透传('ready'|'loading'|'unavailable' 与既有 status 词汇一致);
59
+ * set 丢弃返回的 boolean(避免给未 catch 的既有调用方新增拒绝;form.set 自身 reject 仍传导)。
60
+ * @author ddj 2026年09月22号
61
+ * @param form configForms.get(PLUGIN_NAME) 返回的表单
62
+ * @returns 适配后的设置 scope
63
+ */
64
+ function adaptConfigForm(form: ConfigFormLike): SettingsScopeLike {
65
+ return {
66
+ getSnapshot: () => {
67
+ const snap = form.getSnapshot()
68
+ return { status: snap.status, value: snap.value as SettingsValueLike | undefined, writable: snap.writable }
69
+ },
70
+ subscribe: (listener) => form.subscribe(listener),
71
+ set: async (field, value) => { await form.set(field, value) },
72
+ }
73
+ }
74
+
75
+ /**
76
+ * 探测 DSH 0.1.7+ configForms 官方新设置桥(entryId = PLUGIN_NAME)。
77
+ * 0.1.6 无该服务、ctx.get/表单 get 抛错或形状不符时返回 undefined,交由原两级桥降级。
78
+ * @author ddj 2026年09月22号
79
+ * @param ctx 客户端服务上下文
80
+ * @returns 适配后的设置 scope 或 undefined
81
+ */
82
+ function bindConfigForm(ctx: { get: (name: string) => unknown }): SettingsScopeLike | undefined {
83
+ try {
84
+ const forms = ctx.get('configForms') as ConfigFormsService | undefined
85
+ const form = forms?.get?.(PLUGIN_NAME)
86
+ if (!isConfigForm(form)) return undefined
87
+ return adaptConfigForm(form)
88
+ } catch {
89
+ return undefined
90
+ }
91
+ }
92
+
93
+ /**
94
+ * 设置桥优先序:configForms(DSH 0.1.7+ 官方新桥)→ webUiSettings(linxin666 兼容桥,
95
+ * 旧版现网优先级不变)→ settingsScope(DSH 0.1.6 官方旧桥)→ 无。
22
96
  * 桥绑定抛错时降级到下一级,不让单个桥异常拖垮设置装配。
23
- * @author ddj 2026年08月24号
97
+ * @author ddj 2026年08月24号(2026年09月22 增补 configForms 桥)
24
98
  * @param ctx 客户端服务上下文
25
99
  * @returns 激活的桥名称与绑定 scope
26
100
  */
27
101
  export function pickSettingsBinder(ctx: { get: (name: string) => unknown }): { service: string; scope: SettingsScopeLike | undefined } {
102
+ const configForm = bindConfigForm(ctx)
103
+ if (configForm) return { service: 'configForms', scope: configForm }
28
104
  for (const service of ['webUiSettings', 'settingsScope']) {
29
105
  try {
30
106
  const binder = ctx.get(service) as { bind?: (spec: { namespace: string }) => SettingsScopeLike } | undefined
@@ -37,6 +113,60 @@ export function pickSettingsBinder(ctx: { get: (name: string) => unknown }): { s
37
113
  return { service: 'none', scope: undefined }
38
114
  }
39
115
 
116
+ /** 设置桥形状(探测 + 晚到有界重试 + 就绪通知;调用方按需取用)。 */
117
+ export interface SettingsBridge {
118
+ scope: () => SettingsScopeLike | undefined
119
+ service: () => string
120
+ whenReady: (listener: () => void) => () => void
121
+ }
122
+
123
+ /**
124
+ * 设置桥装配(带晚到有界重试):设置桥服务一律不进 inject(0.1.6 settingsScope /
125
+ * 0.1.7 configForms 互斥,写死任一桥名都会在另一版本停等),因此提供方可能晚于本插件
126
+ * apply 就绪——构造时探测一次,未命中则按 schedule 节奏有界重试,命中即通知 whenReady
127
+ * 订阅者(重挂设置同步 effect);重试用尽保持未就绪(与旧行为一致,不无限轮询)。
128
+ * @author ddj 2026年09月22号
129
+ * @param ctx 客户端服务上下文
130
+ * @param options schedule 注入(缺省不重试=旧行为);attempts/intervalMs 重试节奏
131
+ * @returns 设置桥
132
+ */
133
+ export function settingsBridge(
134
+ ctx: { get: (name: string) => unknown },
135
+ options?: { schedule?: (fn: () => void, ms: number) => void; attempts?: number; intervalMs?: number },
136
+ ): SettingsBridge {
137
+ let hit = pickSettingsBinder(ctx)
138
+ let attemptsLeft = options?.attempts ?? 0
139
+ const interval = options?.intervalMs ?? 2000
140
+ const waiters: Array<() => void> = []
141
+ const notify = (): void => {
142
+ for (const listener of waiters.splice(0)) {
143
+ try { listener() } catch { /* 单个订阅者异常不拖垮其余 */ }
144
+ }
145
+ }
146
+ const retry = (): void => {
147
+ if (hit.scope || attemptsLeft <= 0 || !options?.schedule) return
148
+ attemptsLeft -= 1
149
+ options.schedule(() => {
150
+ hit = pickSettingsBinder(ctx)
151
+ if (hit.scope) { notify(); return }
152
+ retry()
153
+ }, interval)
154
+ }
155
+ retry()
156
+ return {
157
+ scope: () => hit.scope,
158
+ service: () => hit.service,
159
+ whenReady: (listener) => {
160
+ if (hit.scope) { listener(); return () => {} }
161
+ waiters.push(listener)
162
+ return () => {
163
+ const at = waiters.indexOf(listener)
164
+ if (at >= 0) waiters.splice(at, 1)
165
+ }
166
+ },
167
+ }
168
+ }
169
+
40
170
  /** slot 注册描述(对齐 ctx.slots.register 的形状)。 */
41
171
  export interface SlotSpec {
42
172
  name: string
@@ -1,6 +1,7 @@
1
1
  /**
2
- * dsh-vscode-mode client — 图片文件判定与 data URL 组装(编辑区图片预览面板用)。
3
- * 纯函数、无 DOM 依赖,便于单测;MIME 表收敛在 shared/rpc.ts(host base64 响应同源)。
2
+ * dsh-vscode-mode client — 图片文件判定、data URL 组装与缩放纯逻辑(编辑区图片预览面板用)。
3
+ * 纯函数、无 DOM 依赖,便于单测;MIME 表收敛在 shared/rpc.ts(host base64 响应同源);
4
+ * 缩放:步进/clamp 为纯函数,页签记忆为模块级 Map(会话+路径键,EditorView 卸载清空、不持久化)。
4
5
  * @author ddj 2026年09月08号
5
6
  */
6
7
  import { IMAGE_MIME } from '../shared/rpc.js'
@@ -43,3 +44,76 @@ export function isSvgPath(path: string): boolean {
43
44
  export function dataUrlOf(base64: string, mime: string): string {
44
45
  return 'data:' + (mime || 'application/octet-stream') + ';base64,' + base64
45
46
  }
47
+
48
+ /** 图片缩放下限(25%,低于此档不再缩小)。 */
49
+ export const ZOOM_MIN = 0.25
50
+
51
+ /** 图片缩放上限(400%,高于此档不再放大)。 */
52
+ export const ZOOM_MAX = 4
53
+
54
+ /** 图片缩放步进(10%/档,−/+ 按钮每点一次的变化量)。 */
55
+ export const ZOOM_STEP = 0.1
56
+
57
+ /** 页签缩放记忆表:键 = zoomKeyOf(会话, 路径),值 = 显式缩放或 null(适应宽度)。 */
58
+ const zoomMem: Map<string, number | null> = new Map()
59
+
60
+ /**
61
+ * 图片缩放收敛:clamp 到 25%–400% 并按整百分比取整(消除浮点步进噪声)。
62
+ * @author ddj 2026年09月22号
63
+ * @param zoom 目标缩放(非有限数值按 100% 处理)
64
+ * @returns 收敛后的缩放比例
65
+ */
66
+ export function clampZoom(zoom: number): number {
67
+ const value = Number.isFinite(zoom) ? zoom : 1
68
+ return Math.round(Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, value)) * 100) / 100
69
+ }
70
+
71
+ /**
72
+ * 图片缩放步进:按 10%/档变化并 clamp(dir 为负即缩小,其余按放大处理)。
73
+ * @author ddj 2026年09月22号
74
+ * @param base 起始缩放(适应宽度态由调用方实测当前渲染缩放传入)
75
+ * @param dir 方向:< 0 缩小,≥ 0 放大
76
+ * @returns 步进后的缩放比例
77
+ */
78
+ export function zoomStepOf(base: number, dir: number): number {
79
+ return clampZoom(base + (dir < 0 ? -ZOOM_STEP : ZOOM_STEP))
80
+ }
81
+
82
+ /**
83
+ * 页签缩放记忆键(会话 + 路径;NUL 分隔,防止两段拼接撞键)。
84
+ * @author ddj 2026年09月22号
85
+ * @param sessionId 会话 id
86
+ * @param path 图片路径
87
+ * @returns 记忆键
88
+ */
89
+ export function zoomKeyOf(sessionId: string | null | undefined, path: string | null | undefined): string {
90
+ return String(sessionId ?? '') + '\u0000' + String(path ?? '')
91
+ }
92
+
93
+ /**
94
+ * 写入页签缩放记忆(null = 适应宽度;会话级生命周期,不进 localStorage)。
95
+ * @author ddj 2026年09月22号
96
+ * @param key zoomKeyOf 产出的记忆键
97
+ * @param zoom 记忆缩放(null = 适应宽度)
98
+ */
99
+ export function rememberZoom(key: string, zoom: number | null): void {
100
+ zoomMem.set(key, zoom)
101
+ }
102
+
103
+ /**
104
+ * 读取页签缩放记忆(无记忆返回 null,调用方按初始适应宽度处理)。
105
+ * @author ddj 2026年09月22号
106
+ * @param key zoomKeyOf 产出的记忆键
107
+ * @returns 记忆缩放或 null
108
+ */
109
+ export function recallZoom(key: string): number | null {
110
+ return zoomMem.get(key) ?? null
111
+ }
112
+
113
+ /**
114
+ * 清空全部页签缩放记忆(EditorView 卸载时调用)。
115
+ * @author ddj 2026年09月22号
116
+ */
117
+ export function clearZoomMem(): void {
118
+ zoomMem.clear()
119
+ }
@@ -29,9 +29,10 @@ import { installOpenPathRouter, vscodeOpener, autoValue } from './openPathRouter
29
29
  import { patchRemoteOpen, probeRemoteOpen } from './remoteOpenRouter.js'
30
30
  import { setupExtOpen } from './externalOpen.js'
31
31
  import { SettingsContext } from './settingsContext.js'
32
- import { SIDEBAR_PLUGIN, pickSettingsBinder, registerSlotSafely } from './compat.js'
32
+ import { SIDEBAR_PLUGIN, registerSlotSafely, settingsBridge } from './compat.js'
33
33
  import { detectSidebarService, installSideEditor, setEnsureSideEditor, SIDEBAR_INSTALL_CMD } from './sidebarBridge.js'
34
- import { detectOfficial, installOfficial, registerOfficialFileClaim, OFFICIAL_TAB_KIND, OFFICIAL_TAB_TITLE, isEditorTabActive, restoreEditorTab } from './officialSidebar.js'
34
+ import { detectOfficial, installOfficial, registerOfficialFileClaim, OFFICIAL_TAB_KIND, OFFICIAL_TAB_TITLE, isEditorTabActive, restoreEditorTab, buildFileAddress } from './officialSidebar.js'
35
+ import { setNativeCsv, setNativeOpenHandler, resetNativeOpen } from './nativeOpenStore.js'
35
36
  import { SideEditorTab } from './ui/SideEditorTab.js'
36
37
  import { OfficialSideTab } from './ui/OfficialSideTab.js'
37
38
  import { createClaimRouter } from './ui/ClaimRouter.js'
@@ -64,7 +65,9 @@ import type { CompatAdapter } from '../shared/compat.js'
64
65
  // ⚠️ inject 只列必需服务:webUiSettings 是 @linxin666/dsh-client-ui-web-ui-settings 提供的
65
66
  // 可选兼容桥(非官方服务),列入 inject 会让未装该桥的部署启动时 entry 永久 pending,
66
67
  // web boot 直接失败('1 entry did not activate')。兼容层用 ctx.get 运行时探测 + 降级,不靠 inject。
67
- export const inject = ['slots', 'timer', 'locale', 'connection', 'remote', 'workspaces', 'sessions', 'conversation', 'settingsScope']
68
+ // 设置桥服务同理不进 inject(DSH 0.1.7 移除 settingsScope、新增 configForms,0.1.6 反之;
69
+ // 写死任一桥名都会在另一版本停等)——设置桥由 compat.pickSettingsBinder 运行时探测三级降级。
70
+ export const inject = ['slots', 'timer', 'locale', 'connection', 'remote', 'workspaces', 'sessions', 'conversation']
68
71
 
69
72
  /** 指令桥装配幂等标记(同一 document 重复 apply 只装配一次,避免重复键位监听)。 */
70
73
  let commandsMounted = false
@@ -155,8 +158,9 @@ export function apply(ctx: any): void {
155
158
  return subscribeScope(ctx, () => schedule(check, 0))
156
159
  }, 'vscode-mode: editor tab restore')
157
160
  const originalOpenPath = workspaces?.openPath
158
- const binder = pickSettingsBinder(ctx)
159
- const settings = binder.scope
161
+ // 设置桥:服务不进 inject(0.1.6 settingsScope / 0.1.7 configForms 桥名互斥,写死任一
162
+ // 都会在另一版本停等)→ compat.settingsBridge 探测 + 晚到 15×2s 有界重试,命中后重挂同步
163
+ const bridge = settingsBridge(ctx, { schedule, attempts: 15, intervalMs: 2000 })
160
164
  let selected = autoValue('auto')
161
165
  /** 0.1.3+ 会话文件链接路由(remote.session.openWorkspacePath)是否已安装(compatSummary 展示用)。 */
162
166
  let remoteOpenInstalled = false
@@ -175,7 +179,7 @@ export function apply(ctx: any): void {
175
179
 
176
180
  /** 客户端侧兼容摘要(与 host 报告合并展示;惰性求值保证 HMR 后仍新鲜)。 */
177
181
  const compatSummary = (): CompatAdapter[] => [
178
- { name: '设置桥', active: settings !== undefined, note: settings !== undefined ? '使用 ' + binder.service + ' 桥' : '未绑定设置服务(fileOpenTool 持久化不可用)' },
182
+ { name: '设置桥', active: bridge.scope() !== undefined, note: bridge.scope() !== undefined ? '使用 ' + bridge.service() + ' 桥' : '未绑定设置服务(fileOpenTool 持久化不可用,晚到重试窗口内自愈)' },
179
183
  { name: '文件打开路由(workspaces.openPath)', active: Boolean(workspaces?.openPath), note: 'vscode 打开器优先,失败回退系统打开(0.1.2 及更早 DSH 的对话文件链接主路径)' },
180
184
  { name: '会话文件链接路由(remote.session.openWorkspacePath)', active: remoteOpenInstalled, note: '0.1.3+ 对话文件引用/产物打开优先走插件打开器,失败回退系统打开' },
181
185
  { name: '侧边栏打开器(' + SIDEBAR_PLUGIN + ')', active: registry.get(SIDEBAR_PLUGIN) !== undefined, note: registry.get(SIDEBAR_PLUGIN) !== undefined ? '已注册(优先级 80)' : '未检测到侧边栏打开能力' },
@@ -241,35 +245,47 @@ export function apply(ctx: any): void {
241
245
  claimDisposer = null
242
246
  }
243
247
  }
244
- ctx.effect(() => {
245
- if (!settings) return undefined
246
- const sync = (): void => {
247
- const snapshot = settings.getSnapshot()
248
- if (snapshot.status === 'loading') return
249
- selected = autoValue(snapshot.value?.fileOpenTool)
250
- syncFileClaim()
251
- window.dispatchEvent(new CustomEvent('edrv:file-open-tool-change', { detail: { value: selected } }))
252
- }
253
- sync()
254
- return settings.subscribe(sync)
255
- }, 'vscode-mode: file opener setting sync')
256
- // 快捷键配置同步:设置提交后立即刷新键位匹配(编辑器/QuickOpen 按事件时读取)
257
- // 同一订阅里顺带同步侧边栏最小宽度(sidebarMinWidth)→ 派发 edrv:sidebar-min-width 通知编辑器重夹
258
- // 以及页签上限(maxOpenEditors)→ 派发 edrv:max-open-editors 通知编辑器复算淘汰
259
- ctx.effect(() => {
260
- if (!settings) return undefined
261
- const sync = (): void => {
262
- const snapshot = settings.getSnapshot()
263
- if (snapshot.status === 'loading') return
264
- keybindingsApply(snapshot.value?.keybindings)
265
- const minW = sidebarMinApply(snapshot.value?.sidebarMinWidth)
266
- window.dispatchEvent(new CustomEvent('edrv:sidebar-min-width', { detail: { value: minW } }))
267
- const limit = editorLimitApply(snapshot.value?.maxOpenEditors)
268
- window.dispatchEvent(new CustomEvent('edrv:max-open-editors', { detail: { value: limit } }))
269
- }
270
- sync()
271
- return settings.subscribe(sync)
272
- }, 'vscode-mode: keybindings setting sync')
248
+ // 设置同步(fileOpenTool / 快捷键 / 侧宽 / 页签上限):桥可能晚于本插件 apply 就绪
249
+ // (设置桥服务不进 inject),首次命中即挂两条 effect;晚到经 bridge.whenReady 重挂一次
250
+ // (settingsBridge 15×2s 有界重试内命中才会触发,用尽后保持旧行为不轮询)。
251
+ let settingsSyncMounted = false
252
+ const mountSettingsSyncs = (): void => {
253
+ if (settingsSyncMounted) return
254
+ const scope = bridge.scope()
255
+ if (!scope) return
256
+ settingsSyncMounted = true
257
+ ctx.effect(() => {
258
+ const sync = (): void => {
259
+ const snapshot = scope.getSnapshot()
260
+ if (snapshot.status === 'loading') return
261
+ selected = autoValue(snapshot.value?.fileOpenTool)
262
+ syncFileClaim()
263
+ window.dispatchEvent(new CustomEvent('edrv:file-open-tool-change', { detail: { value: selected } }))
264
+ }
265
+ sync()
266
+ return scope.subscribe(sync)
267
+ }, 'vscode-mode: file opener setting sync')
268
+ // 快捷键配置同步:设置提交后立即刷新键位匹配(编辑器/QuickOpen 按事件时读取)
269
+ // 同一订阅里顺带同步侧边栏最小宽度(sidebarMinWidth)→ 派发 edrv:sidebar-min-width 通知编辑器重夹
270
+ // 以及页签上限(maxOpenEditors)→ 派发 edrv:max-open-editors 通知编辑器复算淘汰
271
+ ctx.effect(() => {
272
+ const sync = (): void => {
273
+ const snapshot = scope.getSnapshot()
274
+ if (snapshot.status === 'loading') return
275
+ keybindingsApply(snapshot.value?.keybindings)
276
+ const minW = sidebarMinApply(snapshot.value?.sidebarMinWidth)
277
+ window.dispatchEvent(new CustomEvent('edrv:sidebar-min-width', { detail: { value: minW } }))
278
+ const limit = editorLimitApply(snapshot.value?.maxOpenEditors)
279
+ window.dispatchEvent(new CustomEvent('edrv:max-open-editors', { detail: { value: limit } }))
280
+ // 原生打开范围同拍推送(nativeOpenStore 解析缓存;旧版无该字段 → 保持默认集)
281
+ setNativeCsv(snapshot.value?.nativeOpenExts)
282
+ }
283
+ sync()
284
+ return scope.subscribe(sync)
285
+ }, 'vscode-mode: keybindings setting sync')
286
+ }
287
+ mountSettingsSyncs()
288
+ if (!settingsSyncMounted) ctx.effect(() => bridge.whenReady(mountSettingsSyncs), 'vscode-mode: settings bridge wait')
273
289
  if (workspaces?.openPath) {
274
290
  ctx.effect(() => installOpenPathRouter({
275
291
  workspaces,
@@ -371,6 +387,17 @@ export function apply(ctx: any): void {
371
387
  // 官方打开器(下拉「官方侧边栏」选项,动态出现;openResource 能力缺失时跳过)+ 按当前设置同步 file 地址认领
372
388
  if (typeof official.service.openResource === 'function') {
373
389
  ctx.effect(() => registry.register(officialSidebarOpener(official.service)), 'vscode-mode: official sidebar opener')
390
+ // 原生打开 handler:文件树 openFile 命中 nativeOpenExts 时经 openResource 落官方渲染器
391
+ //(claim 的 canOpen 同步让位该范围,否则地址又被认领转发回本插件);形态撤下时解绑
392
+ ctx.effect(() => {
393
+ setNativeOpenHandler((path) => {
394
+ const scope = readSessionScope(ctx)
395
+ if (!scope.sessionId) return false
396
+ official.service.openResource?.(buildFileAddress(path, scope.sessionId))
397
+ return typeof official.service.openResource === 'function'
398
+ })
399
+ return () => setNativeOpenHandler(null)
400
+ }, 'vscode-mode: native open handler')
374
401
  }
375
402
  syncFileClaim()
376
403
  }
@@ -449,7 +476,7 @@ export function apply(ctx: any): void {
449
476
  id: 'vscode-mode',
450
477
  order: 30,
451
478
  label: 'VSCodeMode',
452
- }, () => React.createElement(SettingsContext.Provider, { value: settings }, React.createElement(McpSettings, { openerRegistry: registry, compatSummary })))
479
+ }, () => React.createElement(SettingsContext.Provider, { value: bridge.scope() }, React.createElement(McpSettings, { openerRegistry: registry, compatSummary })))
453
480
 
454
481
  // 卸载收尾(G4,DSH 0.1.6-alpha.2 起支持运行时卸载/重载):
455
482
  // 注销挂在存活的 window.monaco 上的全部 Monaco provider 并复位模块状态。
@@ -459,5 +486,7 @@ export function apply(ctx: any): void {
459
486
  try { disposeLaunchJson() } catch { /* 同上 */ }
460
487
  try { disposeAiInline() } catch { /* 同上 */ }
461
488
  try { disposeLsp() } catch { /* 同上 */ }
489
+ // 原生打开范围复位回默认集(handler 解绑已由其自身 ctx.effect disposer 承担)
490
+ try { resetNativeOpen() } catch { /* 同上 */ }
462
491
  }, 'vscode-mode: monaco providers teardown')
463
492
  }
@@ -171,6 +171,52 @@ export async function fetchHover(path, text, position) {
171
171
  return res.hover || null
172
172
  }
173
173
 
174
+ /**
175
+ * 查询补全列表(列表阶段不带 documentation,由 resolve 补全)。
176
+ * @author ddj 2026年09月22号
177
+ * @param path 工作区相对路径
178
+ * @param text 文档全文(未保存内容必须进服务器,否则按旧文本解析)
179
+ * @param position Monaco 位置(1-based)
180
+ * @param context LSP CompletionContext(触发类型/触发字符)
181
+ * @returns { items, incomplete };失败或空时 null
182
+ */
183
+ export async function fetchCompletions(path, text, position, context) {
184
+ await syncDoc(path, text, true)
185
+ const args = { path, position: monoToLsp(position.lineNumber, position.column) }
186
+ if (context) args.context = context
187
+ const res = await queryLsp('查询补全', 'edrv.lsp.completion', args)
188
+ if (!res || !res.completions) return null
189
+ return res.completions
190
+ }
191
+
192
+ /**
193
+ * 惰性补全单个候选项(completionItem/resolve)。
194
+ * @author ddj 2026年09月22号
195
+ * @param path 工作区相对路径
196
+ * @param item host 归一化的补全项(含 data)
197
+ * @returns 补全后的条目;失败时回落原条目
198
+ */
199
+ export async function resolveCompletion(path, item) {
200
+ const res = await queryLsp('解析补全', 'edrv.lsp.resolveCompletion', { path, item })
201
+ if (!res || !res.item) return item
202
+ return res.item
203
+ }
204
+
205
+ /**
206
+ * 查询签名帮助。
207
+ * @author ddj 2026年09月22号
208
+ * @param path 工作区相对路径
209
+ * @param text 文档全文
210
+ * @param position Monaco 位置(1-based)
211
+ * @returns 归一化签名帮助;无签名时 null
212
+ */
213
+ export async function fetchSignatureHelp(path, text, position) {
214
+ await syncDoc(path, text, true)
215
+ const res = await queryLsp('查询签名', 'edrv.lsp.signatureHelp', { path, position: monoToLsp(position.lineNumber, position.column) })
216
+ if (!res) return null
217
+ return res.signatureHelp || null
218
+ }
219
+
174
220
  /** 查询当前文档的 semantic tokens(host 已归一化 legend 与 delta data)。 */
175
221
  export async function fetchSemanticTokens(path, text) {
176
222
  await syncDoc(path, text, true)