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
@@ -2,10 +2,11 @@
2
2
  * dsh-vscode-mode host — 文件链接打开工具 + 快捷键的持久化设置。
3
3
  * 依赖守卫:schemastery 动态加载;@deepseek-ai/dsh-settings 仅作 legacy 探测——
4
4
  * rc 线导出 installSettingsSection free function,0.1.2-alpha 起移除(改由 settings
5
- * 服务的 installSection 方法承载,见 runSettingsInstall 三策略)。
5
+ * 服务的 installSection 方法承载),0.1.7 再移除 installSection(设置并入 profile
6
+ * 插件 Config schema,见 runSettingsInstall 四策略:legacy/service/forms/none)。
6
7
  * 缺失/任一策略失败时插件仍可加载(fileOpenTool 降级为配置值,compat 报告可见),
7
8
  * 全程 try/catch,不产生未捕获 rejection。
8
- * 作者 ddj 2026年08月24号 / 2026年08月26号 / 2026年09月02号
9
+ * 作者 ddj 2026年08月24号 / 2026年08月26号 / 2026年09月02号 / 2026年09月22号
9
10
  */
10
11
  import { createRequire } from 'node:module'
11
12
  import { pathToFileURL } from 'node:url'
@@ -15,6 +16,7 @@ import { EDITOR_LIMIT_DEFAULT } from './shared/editorLimit.js'
15
16
  import { INTEGRATION_BASE_DEFAULT } from './shared/integration.js'
16
17
  import { TORTOISE_DIR_DEFAULT } from './shared/svn.js'
17
18
  import type { AiConfigPatch, AiConfigView } from './shared/ai.js'
19
+ import { DEFAULT_NATIVE_CSV } from './shared/nativeOpen.js'
18
20
  import { log } from './log.js'
19
21
 
20
22
  export const FILE_OPEN_SETTINGS_NS = 'dsh-vscode-mode'
@@ -170,11 +172,12 @@ export function loadSettingsDeps(importFn: (specifier: string) => Promise<unknow
170
172
  }
171
173
 
172
174
  /** 设置 section 安装策略(版本适配机制的观测值)。 */
173
- export type SettingsInstallStrategy = 'legacy' | 'service' | 'none' | 'unknown'
175
+ export type SettingsInstallStrategy = 'legacy' | 'service' | 'forms' | 'none' | 'unknown'
174
176
 
175
177
  const INSTALL_UNMOUNTED = '设置 section 尚未装配'
176
178
  const INSTALL_LEGACY = 'rc 线:dsh-settings.installSettingsSection'
177
179
  const INSTALL_SERVICE = '0.1.2-alpha 线:settings 服务 installSection'
180
+ const INSTALL_FORMS = '0.1.7 线:设置并入插件 Config schema(SettingsForms)'
178
181
  const INSTALL_NONE = '两路均不可用:设置持久化降级为配置值'
179
182
 
180
183
  interface ObservedInstall { strategy: SettingsInstallStrategy; note: string }
@@ -201,15 +204,54 @@ export function resetSettingsInstallObserved(): void {
201
204
  observedInstall = { strategy: 'unknown', note: INSTALL_UNMOUNTED }
202
205
  }
203
206
 
207
+ /**
208
+ * 判定设置写冲突错误(0.1.7 SettingsConflictError:code 常量 / 构造器名双通道)。
209
+ * @author ddj 2026年09月22号
210
+ * @param error 捕获到的错误
211
+ * @returns 是否为 revision 冲突
212
+ */
213
+ function isConflictError(error: unknown): boolean {
214
+ if (!error || typeof error !== 'object') return false
215
+ const e = error as { code?: unknown; name?: unknown }
216
+ return e.code === 'SETTINGS_CONFLICT' || e.name === 'SettingsConflictError'
217
+ }
218
+
219
+ /**
220
+ * 0.1.7 forms 策略:订阅 settings/document-updated 事件,目标 namespace 变化时
221
+ * 把 describe 最新值推给 hooks(setSource + onChange),替代旧 installSection 的
222
+ * 变更回调;disposer 经 ctx.effect 随插件 fiber 卸载(热重载不泄漏)。
223
+ * @author ddj 2026年09月22号
224
+ * @param ctx DSH host 上下文
225
+ * @param provider settings 服务(SettingsForms)
226
+ * @param ns 目标设置命名空间
227
+ * @param hooks 设置源绑定与变更回调
228
+ */
229
+ function watchDocUpdates(ctx: Ctx, provider: SettingsProvider, ns: string, hooks: { setSource: (source: () => unknown) => void; onChange: () => void }): void {
230
+ try {
231
+ const bus = ctx as { on?: (name: string, handler: (updatedNs: unknown, revision: unknown) => void) => unknown; effect?: (fn: () => unknown) => unknown }
232
+ if (typeof bus.on !== 'function') return
233
+ const off = bus.on('settings/document-updated', (updatedNs: unknown) => {
234
+ if (updatedNs !== ns) return
235
+ hooks.setSource(() => readSectionValue(provider, ns))
236
+ hooks.onChange()
237
+ })
238
+ if (typeof bus.effect === 'function' && typeof off === 'function') bus.effect(() => off)
239
+ } catch (error) {
240
+ log.warn('settings/document-updated 订阅失败:' + String(error))
241
+ }
242
+ }
243
+
204
244
  /**
205
245
  * 设置 section 版本自适应安装(核心策略分派)。
206
246
  * legacy:dsh-settings 仍导出 installSettingsSection(rc 线)→ 原样调用,行为与旧版一致。
207
247
  * service:该导出已移除(0.1.2-alpha 起)→ 经 ctx.inject(['settings']) 走服务方法
208
248
  * provider.installSection(owner, ns, schema, entry, hooks)(等义封装,含
209
- * base 层与 fiber 卸载回退)。回调内方法缺失再降级记录 none。
249
+ * base 层与 fiber 卸载回退)。
250
+ * forms:0.1.7 起 installSection 移除,设置并入 profile 插件 Config(SettingsForms
251
+ * 的 describe+update 在场即成立)→ 不装 section,改订阅 document-updated。
210
252
  * none:两条路由都不存在 → 仅记录并告警,调用方按配置值运行。
211
253
  * 全程不抛错、不产生未捕获 rejection。
212
- * @author ddj 2026年09月02号
254
+ * @author ddj 2026年09月02号(2026年09月22号 增补 forms 策略)
213
255
  * @param ctx DSH host 上下文(inject 可选探测)
214
256
  * @param ns 设置命名空间
215
257
  * @param schema schemastery schema
@@ -253,6 +295,13 @@ export async function runSettingsInstall(
253
295
  const provider = typeof sc.get === 'function' ? sc.get('settings') : sc.settings
254
296
  const install = (provider as { installSection?: unknown } | undefined)?.installSection
255
297
  if (typeof install !== 'function') {
298
+ // 0.1.7:installSection 已移除;SettingsForms(describe+update)在场走 forms 策略
299
+ const forms = provider as SettingsProvider | undefined
300
+ if (typeof forms?.describe === 'function' && typeof forms?.update === 'function') {
301
+ recordInstall('forms', INSTALL_FORMS)
302
+ watchDocUpdates(ctx, forms, ns, hooks)
303
+ return
304
+ }
256
305
  log.warn('settings 服务无 installSection(DSH 版本 API 变化),section ' + ns + ' 降级为配置值')
257
306
  recordInstall('none', INSTALL_NONE)
258
307
  return
@@ -286,6 +335,101 @@ function keybindingsShape(z: SettingsDeps['z']): Record<string, unknown> {
286
335
  return shape
287
336
  }
288
337
 
338
+ /**
339
+ * 设置节 describe 项的形状校验:value 须为普通对象。
340
+ * ns 在两代语义不同(旧=自装 section 名,0.1.7=profile entry id),同名撞车时
341
+ * 以形状兜底防误命中非设置数据;0.1.7 下本插件 entry 的 Config 值与旧 section
342
+ * 值同为设置键对象,两形状天然兼容。
343
+ * @author ddj 2026年09月22号
344
+ * @param value describe 项的 value 字段
345
+ * @returns 是否具备设置节形状
346
+ */
347
+ function isSectionShaped(value: unknown): boolean {
348
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
349
+ }
350
+
351
+ /** describe 项最小形状(读写工具共用)。 */
352
+ export interface SectionDescriptor { ns?: string; value?: unknown; revision?: number }
353
+
354
+ /**
355
+ * 按 ns + 形状校验查找设置节(host 读路径统一入口)。
356
+ * @author ddj 2026年09月22号
357
+ * @param provider settings 服务(可空)
358
+ * @param ns 设置命名空间
359
+ * @returns 命中的 describe 项;未命中返回 undefined
360
+ */
361
+ export function sectionOf(provider: SettingsProvider | undefined, ns: string): SectionDescriptor | undefined {
362
+ const items = provider?.describe?.({ redactSecrets: true })
363
+ if (!items) return undefined
364
+ return items.find((item) => item.ns === ns && isSectionShaped(item.value))
365
+ }
366
+
367
+ /** 读设置节存储值(未就绪/形状不符返回 undefined)。 */
368
+ function readSectionValue(provider: SettingsProvider | undefined, ns: string): unknown {
369
+ return sectionOf(provider, ns)?.value
370
+ }
371
+
372
+ /** 设置写入结果(ok=false 时带原因;conflict=true 表示经冲突重试后成功)。 */
373
+ export interface SectionWriteResult { ok: boolean; conflict?: boolean; error?: string }
374
+
375
+ /**
376
+ * 带冲突自愈的设置写入:0.1.7 SettingsConflictError(revision 拒写)时重读
377
+ * revision 重试一次;其余错误与服务缺失一律结构化返回,绝不抛未捕获异常。
378
+ * @author ddj 2026年09月22号
379
+ * @param provider settings 服务(可空)
380
+ * @param ns 设置命名空间
381
+ * @param patch 写入字段
382
+ * @param expectedRevision 读侧携带的 revision(缺省用 describe 最新值)
383
+ * @returns 写入结果
384
+ */
385
+ export async function updateSection(provider: SettingsProvider | undefined, ns: string, patch: object, expectedRevision?: number): Promise<SectionWriteResult> {
386
+ if (!provider?.update) return { ok: false, error: 'settings 服务无 update' }
387
+ const first = expectedRevision ?? sectionOf(provider, ns)?.revision
388
+ try {
389
+ await provider.update(ns, patch, first)
390
+ return { ok: true }
391
+ } catch (error) {
392
+ if (!isConflictError(error)) return { ok: false, error: String(error) }
393
+ }
394
+ const fresh = sectionOf(provider, ns)?.revision
395
+ try {
396
+ await provider.update(ns, patch, fresh)
397
+ return { ok: true, conflict: true }
398
+ } catch (error) {
399
+ return { ok: false, conflict: true, error: String(error) }
400
+ }
401
+ }
402
+
403
+ /**
404
+ * 构建设置 section schema(install 路径与插件 Config 声明共用,保证两代形状同源)。
405
+ * 字段集 = fileOpenTool/keybindings/sidebarMinWidth/maxOpenEditors/integrationBaseUrl/
406
+ * aiInline/aiProvider/aiModel/aiEffort/svnPath/tortoisePath/nativeOpenExts,全部带默认值
407
+ * (Config 启动校验在 undefined/空配置下自动填充,rc/alpha 两代 cordis 均通过)。
408
+ * @author ddj 2026年09月22号
409
+ * @param z schemastery 命名空间(静态 import 或动态加载均可)
410
+ * @returns schemastery object schema
411
+ */
412
+ export function buildSettingsSchema(z: SettingsDeps['z']): unknown {
413
+ return z.object({
414
+ fileOpenTool: z.string().default(FILE_OPEN_DEFAULT),
415
+ keybindings: z.object(keybindingsShape(z)).default({ ...KEYBINDING_DEFAULTS }),
416
+ sidebarMinWidth: z.number().default(300),
417
+ // 页签数量上限(0 = 不限制;超限时淘汰最久未使用的页签,固定页签除外)
418
+ maxOpenEditors: z.number().default(EDITOR_LIMIT_DEFAULT),
419
+ integrationBaseUrl: z.string().default(INTEGRATION_BASE_DEFAULT),
420
+ // AI 内联补全(默认关;provider/model 空 = 自动路由;effort 空 = 跟随模型默认)
421
+ aiInline: z.boolean().default(AI_CONFIG_DEFAULT.enabled),
422
+ aiProvider: z.string().default(AI_CONFIG_DEFAULT.provider),
423
+ aiModel: z.string().default(AI_CONFIG_DEFAULT.model),
424
+ aiEffort: z.string().default(AI_CONFIG_DEFAULT.effort),
425
+ // SVN 能力:svn CLI 覆盖(空 = PATH)与 TortoiseSVN 目录(Windows 过渡增强)
426
+ svnPath: z.string().default(''),
427
+ tortoisePath: z.string().default(TORTOISE_DIR_DEFAULT),
428
+ // 原生打开范围(逗号分隔后缀;默认 = 让位清单并集,csv/tsv 决策见 shared/nativeOpen.ts)
429
+ nativeOpenExts: z.string().default(DEFAULT_NATIVE_CSV),
430
+ })
431
+ }
432
+
289
433
  function normalizeValue(value: unknown): string {
290
434
  if (typeof value !== 'string' || value.trim() === '') return FILE_OPEN_DEFAULT
291
435
  return value.trim()
@@ -337,27 +481,12 @@ export async function installOpenSettingsSection(
337
481
  /* 装载失败按缺失处理 */
338
482
  }
339
483
  if (!deps) return false
340
- const schema = deps.z.object({
341
- fileOpenTool: deps.z.string().default(FILE_OPEN_DEFAULT),
342
- keybindings: deps.z.object(keybindingsShape(deps.z)).default({ ...KEYBINDING_DEFAULTS }),
343
- sidebarMinWidth: deps.z.number().default(300),
344
- // 页签数量上限(0 = 不限制;超限时淘汰最久未使用的页签,固定页签除外)
345
- maxOpenEditors: deps.z.number().default(EDITOR_LIMIT_DEFAULT),
346
- integrationBaseUrl: deps.z.string().default(INTEGRATION_BASE_DEFAULT),
347
- // AI 内联补全(默认关;provider/model 空 = 自动路由;effort 空 = 跟随模型默认)
348
- aiInline: deps.z.boolean().default(AI_CONFIG_DEFAULT.enabled),
349
- aiProvider: deps.z.string().default(AI_CONFIG_DEFAULT.provider),
350
- aiModel: deps.z.string().default(AI_CONFIG_DEFAULT.model),
351
- aiEffort: deps.z.string().default(AI_CONFIG_DEFAULT.effort),
352
- // SVN 能力:svn CLI 覆盖(空 = PATH)与 TortoiseSVN 目录(Windows 过渡增强)
353
- svnPath: deps.z.string().default(''),
354
- tortoisePath: deps.z.string().default(TORTOISE_DIR_DEFAULT),
355
- })
484
+ const schema = buildSettingsSchema(deps.z)
356
485
  const strategy = await runSettingsInstall(ctx, ns, schema, entry, {
357
486
  setSource: (source) => hooks.setSource(source as () => FileOpenSettings),
358
487
  onChange: hooks.onChange,
359
488
  }, loader)
360
- return strategy === 'legacy' || strategy === 'service'
489
+ return strategy === 'legacy' || strategy === 'service' || strategy === 'forms'
361
490
  }
362
491
 
363
492
  /** AI 配置脏值读取(settings 未就绪时回退默认)。 */
@@ -385,8 +514,7 @@ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: s
385
514
  let provider: SettingsProvider | undefined
386
515
  const notify = (value: unknown): void => { current = normalizeValue(value); onChange(current) }
387
516
  const syncRevision = (): void => {
388
- const descriptor = provider?.describe?.({ redactSecrets: true })?.find((item: { ns?: string }) => item.ns === FILE_OPEN_SETTINGS_NS)
389
- revision = descriptor?.revision
517
+ revision = sectionOf(provider, FILE_OPEN_SETTINGS_NS)?.revision
390
518
  }
391
519
  const setSource = (source: () => FileOpenSettings): void => notify(source().fileOpenTool)
392
520
  const settingsChange = (): void => syncRevision()
@@ -394,8 +522,7 @@ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: s
394
522
  /** AI 配置当前值(settings 未就绪回退默认)。 */
395
523
  let aiCurrent: AiConfigView = { ...AI_CONFIG_DEFAULT }
396
524
  const aiSync = (): void => {
397
- const descriptor = provider?.describe?.({ redactSecrets: true })?.find((item: { ns?: string }) => item.ns === FILE_OPEN_SETTINGS_NS)
398
- const stored = descriptor?.value
525
+ const stored = readSectionValue(provider, FILE_OPEN_SETTINGS_NS)
399
526
  aiCurrent = stored !== undefined ? aiValueOf(stored) : aiCurrent
400
527
  }
401
528
 
@@ -409,7 +536,7 @@ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: s
409
536
  }
410
537
  /** 读取 settings 存储值(describe 未就绪返回 undefined)。 */
411
538
  const storedValue = (): unknown => {
412
- return provider?.describe?.({ redactSecrets: true })?.find((item: { ns?: string }) => item.ns === FILE_OPEN_SETTINGS_NS)?.value
539
+ return readSectionValue(provider, FILE_OPEN_SETTINGS_NS)
413
540
  }
414
541
 
415
542
  void installOpenSettingsSection(ctx, FILE_OPEN_SETTINGS_NS, {
@@ -438,31 +565,27 @@ export function setupOpenSettings(ctx: Ctx, config: unknown, onChange: (value: s
438
565
  get revision() { return revision },
439
566
  update: async (value: string, expectedRevision?: number): Promise<void> => {
440
567
  const next = normalizeValue(value)
441
- if (!provider?.update) { notify(next); return }
442
- await provider.update(FILE_OPEN_SETTINGS_NS, { fileOpenTool: next }, expectedRevision)
443
- const descriptor = provider.describe?.({ redactSecrets: true })?.find((item: { ns?: string; value?: unknown }) => item.ns === FILE_OPEN_SETTINGS_NS)
444
- const stored = descriptor?.value as { fileOpenTool?: unknown } | undefined
568
+ const result = await updateSection(provider, FILE_OPEN_SETTINGS_NS, { fileOpenTool: next }, expectedRevision)
569
+ if (!result.ok) log.warn('fileOpenTool 设置写入失败:' + (result.error ?? '未知原因'))
570
+ const stored = readSectionValue(provider, FILE_OPEN_SETTINGS_NS) as { fileOpenTool?: unknown } | undefined
445
571
  notify(stored?.fileOpenTool ?? next)
446
572
  syncRevision()
447
573
  },
448
574
  ai: () => aiCurrent,
449
575
  svn: () => svnCurrent,
450
576
  aiUpdate: async (patch: AiConfigPatch, expectedRevision?: number): Promise<AiConfigView> => {
451
- if (!provider?.update) {
452
- // settings 不可用:内存态生效(重启回落默认),保持与 fileOpenTool 的降级语义一致
453
- if (patch.enabled !== undefined) aiCurrent.enabled = patch.enabled
454
- if (patch.provider !== undefined) aiCurrent.provider = patch.provider
455
- if (patch.model !== undefined) aiCurrent.model = patch.model
456
- if (patch.effort !== undefined) aiCurrent.effort = patch.effort
457
- return aiCurrent
458
- }
459
577
  const stored = { ...aiCurrent }
460
578
  const body: Record<string, unknown> = {}
461
579
  if (patch.enabled !== undefined) { stored.enabled = patch.enabled; body.aiInline = patch.enabled }
462
580
  if (patch.provider !== undefined) { stored.provider = patch.provider; body.aiProvider = patch.provider }
463
581
  if (patch.model !== undefined) { stored.model = patch.model; body.aiModel = patch.model }
464
582
  if (patch.effort !== undefined) { stored.effort = patch.effort; body.aiEffort = patch.effort }
465
- await provider.update(FILE_OPEN_SETTINGS_NS, body, expectedRevision)
583
+ const result = await updateSection(provider, FILE_OPEN_SETTINGS_NS, body, expectedRevision)
584
+ if (!result.ok) {
585
+ // settings 不可用/写入失败:内存态生效(重启回落默认),与 fileOpenTool 降级语义一致
586
+ aiCurrent = stored
587
+ return aiCurrent
588
+ }
466
589
  aiSync()
467
590
  return aiCurrent
468
591
  },
package/src/index.ts CHANGED
@@ -14,7 +14,9 @@ import { newSearcher } from './search/orchestrator.js'
14
14
  import { newContentSearcher } from './search/content.js'
15
15
  import { installIsolation } from './mcpIsolation.js'
16
16
  import { cwdOf } from './registry.js'
17
- import { setupOpenSettings } from './fileOpenSettings.js'
17
+ import { setupOpenSettings, buildSettingsSchema } from './fileOpenSettings.js'
18
+ import type { SettingsDeps } from './fileOpenSettings.js'
19
+ import z from '@deepseek-ai/schemastery'
18
20
  import { shellMenuLifecycle } from './integrate.js'
19
21
  import { disposeIndex } from './treeIndex.js'
20
22
  import { sweepTreeCache } from './paths.js'
@@ -36,6 +38,18 @@ import type { Ctx } from './store.js'
36
38
  export const name = "dsh-vscode-mode"
37
39
  export const inject = ['sessions', 'fs', 'webServer', 'loader', 'tools', 'workspaceRegistry', 'agents']
38
40
 
41
+ /**
42
+ * 插件 Config schema(cordis 从 module 导出读 `plugin.Config` 做启动校验;
43
+ * DSH 0.1.7 起设置页按此 schema 自动生成表单,设置值 = profile 插件配置)。
44
+ * 字段集与 installOpenSettingsSection 的 section schema 同源(buildSettingsSchema),
45
+ * 全字段带默认值——undefined/空配置经 schemastery 校验自动填充(rc/alpha 两代
46
+ * cordis 均读该导出,实测 4.0.0-rc.8 与 0.1.7 行为一致,校验必过)。
47
+ * 运行时解析:@deepseek-ai/schemastery 经插件自身 node_modules(link/dev/npm 安装
48
+ * 三形态均在 peer/dev 依赖面内,先于宿主树命中)。
49
+ * @author ddj 2026年09月22号
50
+ */
51
+ export const Config = buildSettingsSchema(z as unknown as SettingsDeps['z'])
52
+
39
53
  /**
40
54
  * 装配插件:挂事件监听、注册路由、安装兼容层。
41
55
  * @author ddj 2026年08月20号
package/src/lsp/client.ts CHANGED
@@ -62,6 +62,9 @@ export function createLspClient(
62
62
  workspaceSymbol: false,
63
63
  hover: false,
64
64
  semanticTokens: false,
65
+ completion: false,
66
+ completionResolve: false,
67
+ signatureHelp: false,
65
68
  semanticTokenTypes: [],
66
69
  semanticTokenModifiers: [],
67
70
  }
@@ -164,6 +167,8 @@ export function createLspClient(
164
167
  documentSymbolProvider?: unknown
165
168
  workspaceSymbolProvider?: unknown
166
169
  hoverProvider?: unknown
170
+ completionProvider?: { resolveProvider?: unknown; triggerCharacters?: unknown }
171
+ signatureHelpProvider?: { triggerCharacters?: unknown; retriggerCharacters?: unknown }
167
172
  semanticTokensProvider?: {
168
173
  legend?: { tokenTypes?: unknown; tokenModifiers?: unknown }
169
174
  }
@@ -185,6 +190,22 @@ export function createLspClient(
185
190
  references: { dynamicRegistration: false },
186
191
  documentSymbol: { dynamicRegistration: false },
187
192
  hover: { dynamicRegistration: false },
193
+ // 补全能力声明:snippetSupport 让服务器回 InsertAsSnippet 片段;
194
+ // resolveSupport.properties 声明我方支持 completionItem/resolve 惰性补全
195
+ // (EmmyLua 据此把 documentation 挪到 resolve 阶段,列表载荷显著变小)。
196
+ completion: {
197
+ dynamicRegistration: false,
198
+ contextSupport: true,
199
+ completionItem: {
200
+ snippetSupport: true,
201
+ documentationFormat: ['markdown', 'plaintext'],
202
+ resolveSupport: { properties: ['documentation', 'detail', 'additionalTextEdits'] },
203
+ },
204
+ },
205
+ signatureHelp: {
206
+ dynamicRegistration: false,
207
+ signatureInformation: { documentationFormat: ['markdown', 'plaintext'] },
208
+ },
188
209
  semanticTokens: {
189
210
  dynamicRegistration: false,
190
211
  requests: { range: false, full: { delta: false } },
@@ -205,6 +226,9 @@ export function createLspClient(
205
226
  workspaceSymbol: Boolean(caps.workspaceSymbolProvider),
206
227
  hover: Boolean(caps.hoverProvider),
207
228
  semanticTokens: Boolean(caps.semanticTokensProvider),
229
+ completion: Boolean(caps.completionProvider),
230
+ completionResolve: Boolean(caps.completionProvider?.resolveProvider),
231
+ signatureHelp: Boolean(caps.signatureHelpProvider),
208
232
  semanticTokenTypes: Array.isArray(legend?.tokenTypes)
209
233
  ? legend.tokenTypes.filter((item): item is string => typeof item === 'string')
210
234
  : [...LSP_SEMANTIC_TOKEN_TYPES],
package/src/lsp/rpc.ts CHANGED
@@ -436,6 +436,63 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
436
436
  }
437
437
  },
438
438
 
439
+ 'edrv.lsp.completion': async (args) => {
440
+ const lang = langOfPath(args.path)
441
+ if (!lang) return { ok: true, completions: undefined }
442
+ const sc = await rootOf(args.sessionId)
443
+ if ('err' in sc) return { ok: false, error: sc.err }
444
+ try {
445
+ const server = serverOf(sc.root, lang)
446
+ if (!server) return { ok: true, completions: undefined }
447
+ const completions = await server.completion(
448
+ wsPath(sc.root, args.path),
449
+ args.position.line,
450
+ args.position.character,
451
+ args.context,
452
+ )
453
+ if (!completions) return { ok: true, completions: undefined }
454
+ const truncated = completions.items.length > 500
455
+ return {
456
+ ok: true,
457
+ completions: truncated ? { ...completions, items: completions.items.slice(0, 500), truncated: true } : completions,
458
+ }
459
+ } catch (error) {
460
+ return { ok: false, error: 'LSP 补全查询失败:' + String(error) }
461
+ }
462
+ },
463
+
464
+ 'edrv.lsp.resolveCompletion': async (args) => {
465
+ const lang = langOfPath(args.path)
466
+ if (!lang) return { ok: true, item: undefined }
467
+ const sc = await rootOf(args.sessionId)
468
+ if ('err' in sc) return { ok: false, error: sc.err }
469
+ try {
470
+ const server = serverOf(sc.root, lang)
471
+ if (!server) return { ok: true, item: args.item }
472
+ const item = await server.resolveCompletion(wsPath(sc.root, args.path), args.item)
473
+ return { ok: true, item: item ?? undefined }
474
+ } catch (error) {
475
+ return { ok: false, error: 'LSP 补全解析失败:' + String(error) }
476
+ }
477
+ },
478
+
479
+ 'edrv.lsp.signatureHelp': async (args) => {
480
+ const lang = langOfPath(args.path)
481
+ if (!lang) return { ok: true, signatureHelp: undefined }
482
+ const sc = await rootOf(args.sessionId)
483
+ if ('err' in sc) return { ok: false, error: sc.err }
484
+ try {
485
+ const server = serverOf(sc.root, lang)
486
+ if (!server) return { ok: true, signatureHelp: undefined }
487
+ return {
488
+ ok: true,
489
+ signatureHelp: (await server.signatureHelp(wsPath(sc.root, args.path), args.position.line, args.position.character)) ?? undefined,
490
+ }
491
+ } catch (error) {
492
+ return { ok: false, error: 'LSP 签名帮助查询失败:' + String(error) }
493
+ }
494
+ },
495
+
439
496
  'edrv.lsp.semanticTokens': async (args) => {
440
497
  const lang = langOfPath(args.path)
441
498
  if (!lang) return { ok: true, tokens: undefined }