dsh-vscode-mode 0.7.0 → 0.9.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 (60) hide show
  1. package/README.md +37 -6
  2. package/assets/icon.svg +7 -0
  3. package/cordis.patch.yml +16 -0
  4. package/lib/client.js +2804 -300
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +1975 -191
  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 +4 -2
  11. package/src/ai/inline.ts +19 -2
  12. package/src/ai/svnTriage.ts +150 -0
  13. package/src/capture.ts +225 -51
  14. package/src/client/addToConversation.ts +203 -1
  15. package/src/client/compat.ts +134 -4
  16. package/src/client/imagePreview.ts +76 -2
  17. package/src/client/index.ts +65 -36
  18. package/src/client/monaco/lsp/lspClient.ts +46 -0
  19. package/src/client/monaco/lsp/providers.ts +245 -1
  20. package/src/client/nativeOpenStore.ts +91 -0
  21. package/src/client/officialSidebar.ts +11 -45
  22. package/src/client/outline/index.ts +1 -1
  23. package/src/client/pdf/pdfPanel.ts +18 -0
  24. package/src/client/pendingNav.ts +141 -0
  25. package/src/client/settingsContext.ts +6 -4
  26. package/src/client/sidebar/panels/FileExplorer.ts +2 -15
  27. package/src/client/sidebar/panels/SvnPanel.ts +682 -72
  28. package/src/client/sidebar/panels/index.ts +1 -1
  29. package/src/client/sidebar/panels/svnListBudget.ts +58 -0
  30. package/src/client/state/records.ts +7 -1
  31. package/src/client/styles/editor.css +47 -5
  32. package/src/client/svnStatus.ts +206 -1
  33. package/src/client/svnStore.ts +6 -1
  34. package/src/client/ui/AiSettings.ts +47 -14
  35. package/src/client/ui/DiffBox.ts +1 -1
  36. package/src/client/ui/DiffLauncher.ts +6 -4
  37. package/src/client/ui/EditorView.ts +149 -22
  38. package/src/client/ui/McpSettings.ts +30 -0
  39. package/src/client/ui/PerfSettings.ts +4 -1
  40. package/src/client/ui/SvnAiPlanDialog.ts +293 -0
  41. package/src/client/ui/icons.ts +91 -0
  42. package/src/compat.ts +26 -5
  43. package/src/dshVersion.ts +2 -1
  44. package/src/fileOpenSettings.ts +274 -49
  45. package/src/index.ts +18 -1
  46. package/src/lsp/client.ts +24 -0
  47. package/src/lsp/rpc.ts +57 -0
  48. package/src/lsp/server.ts +272 -2
  49. package/src/model.ts +2 -1
  50. package/src/perf.ts +90 -4
  51. package/src/remoteWorkspace.ts +177 -0
  52. package/src/routes.ts +13 -1
  53. package/src/rpc.ts +115 -39
  54. package/src/shared/ai.ts +12 -0
  55. package/src/shared/lsp.ts +79 -0
  56. package/src/shared/nativeOpen.ts +91 -0
  57. package/src/shared/rpc.ts +162 -2
  58. package/src/shared/svn.ts +296 -10
  59. package/src/shared/types.ts +5 -1
  60. package/src/svn.ts +235 -13
package/src/rpc.ts CHANGED
@@ -34,7 +34,7 @@ import { rulesList, rulesRead, rulesRemove, rulesSave, rulesToggle } from './rul
34
34
  import { isSnippetFilePath, snippetsEntries, snippetsList, snippetsRead, snippetsRemove, snippetsSave } from './snippets.js'
35
35
  import { listMcp, refreshMcp, removeMcp, saveMcp, toggleMcp } from './mcp.js'
36
36
  import { listProjects, projectRefresh, projectRemove, projectSave, projectToggle } from './mcpProject.js'
37
- import { normalizeFileOpenTool, FILE_OPEN_DEFAULT, FILE_OPEN_SETTINGS_NS } from './fileOpenSettings.js'
37
+ import { normalizeFileOpenTool, FILE_OPEN_DEFAULT, FILE_OPEN_SETTINGS_NS, sectionOf, updateSection } from './fileOpenSettings.js'
38
38
  import { INTEGRATION_BASE_DEFAULT } from './shared/integration.js'
39
39
  import { shellMenuRegister, shellMenuRemove, shellMenuStatus } from './integrate.js'
40
40
  import { unityAdd, unityInstall, unityList, unityRemove } from './unityBridge.js'
@@ -44,10 +44,11 @@ import { findProfileDir, readDevForm, setDevForm } from './devForm.js'
44
44
  import { normalizeRel } from './tree.js'
45
45
  import { baseNameOf, checkNewName, checkRenameName, isSubPath, joinRelPath, parentRelOf } from './shared/fsNames.js'
46
46
  import { invalidateIndex, listDirCached } from './treeIndex.js'
47
+ import { findRemoteWs, remoteDisplayOf, type RemoteWs } from './remoteWorkspace.js'
47
48
  import { revealInExplorer } from './reveal.js'
48
49
  import { dshHome, debugLogFile, pluginLogRoot } from './paths.js'
49
50
  import { clearDebugLog, debugRecord, isDebugLogName, listDebugLogs, readDebugLog } from './debugLog.js'
50
- import { markActiveSessions, moveOutSessions, planMoveOut, purgeArchive, restoreSession, scanSessionInventory, sessionsArchiveRoot, sessionSizeOf, sidecarSummaryOf } from './perf.js'
51
+ import { markActiveSessions, markOfficialFlags, moveOutSessions, planMoveOut, purgeArchive, restoreSession, scanSessionInventory, sessionsArchiveRoot, sessionSizeOf, sidecarSummaryOf, unarchiveOfficial } from './perf.js'
51
52
  import { patchHasPerfConfig, patchInsertPerfConfig, patchRemovePerfConfig, perfConfigBlock } from './perfPatch.js'
52
53
  import type { FileVersions } from './fileVersions.js'
53
54
 
@@ -56,8 +57,8 @@ const staleCheckedAt = new Map<string, number>()
56
57
  /** stale 自动清理最小间隔。 */
57
58
  const STALE_CHECK_MIN_MS = 10_000
58
59
 
59
- /** 记录 → 客户端视图(不含 before 全文,仅长度)。 */
60
- function recView(record: DiffRecord): RecordView {
60
+ /** 记录 → 客户端视图(不含 before 全文,仅长度);远程工作区附远端展示路径(issue #7)。 */
61
+ function recView(record: DiffRecord, ws: RemoteWs | null = null): RecordView {
61
62
  return {
62
63
  callId: record.callId,
63
64
  toolName: record.toolName,
@@ -75,6 +76,7 @@ function recView(record: DiffRecord): RecordView {
75
76
  conflict: record.conflict === true,
76
77
  legacy: record.legacy === true,
77
78
  at: record.at,
79
+ ...(ws ? { displayPath: remoteDisplayOf(record.path, ws) ?? undefined } : {}),
78
80
  }
79
81
  }
80
82
 
@@ -107,6 +109,57 @@ function snippetTargetOf(path: string): string | null {
107
109
  return isSnippetFilePath(path) ? path.replace(/\\/g, '/') : null
108
110
  }
109
111
 
112
+ /** readTargetOf 成功形态:解析后的目标路径、stat 信息与 fs 服务句柄。 */
113
+ interface ReadTargetOk {
114
+ target: string
115
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
116
+ fs: any
117
+ info: { version?: unknown; size?: number }
118
+ }
119
+
120
+ /**
121
+ * 二进制/文本读取共用前置(edrv.read 与 edrv.readBinary):解析路径 + debugRecord +
122
+ * stat + 文件类型校验 + 32MB 二进制上限,保证两条读通道的错误口径完全一致。
123
+ * @author ddj 2026年09月22号
124
+ * @param ctx DSH 上下文
125
+ * @param sc requireSession 成功结果(session + cwd)
126
+ * @param path 客户端请求路径
127
+ * @returns 成功返回解析目标与 stat;失败返回错误文案(文件不存在时附 resolvedPath)
128
+ */
129
+ async function readTargetOf(
130
+ ctx: Ctx,
131
+ sc: { session: Session; cwd: string },
132
+ path: string,
133
+ ): Promise<ReadTargetOk | { err: string; resolvedPath?: string }> {
134
+ const fs = ctx.get('fs')
135
+ if (!fs) return { err: '缺少 fs' }
136
+ try {
137
+ const target = await resolveTarget(ctx, sc.session, path)
138
+ debugRecord(ctx, sc.cwd, '[DEBUG path.resolve] input=' + String(path ?? '') + ' resolved=' + fs.processPath(target), 'debug')
139
+ const info = await fs.stat(target)
140
+ if (!info || info.type !== 'file') {
141
+ // 带上解析后的真实路径:跳转失败时可直接看出是路径解析错还是目标本身不存在
142
+ return { err: '文件不存在', resolvedPath: fs.processPath(target) }
143
+ }
144
+ if ((info.size ?? 0) > BINARY_READ_CAP) return { err: '文件过大(>32MB),不支持整文件预览' }
145
+ return { target, fs, info }
146
+ } catch (error) {
147
+ return { err: '读取失败:' + String(error) }
148
+ }
149
+ }
150
+
151
+ /**
152
+ * readTargetOf 失败结果 → RPC 错误载荷(文件不存在时保留 resolvedPath 供诊断)。
153
+ * @author ddj 2026年09月22号
154
+ * @param prep readTargetOf 的失败分支
155
+ * @returns { ok:false } 形态载荷
156
+ */
157
+ function targetErrOf(prep: { err: string; resolvedPath?: string }): { ok: false; error: string; resolvedPath?: string } {
158
+ return prep.resolvedPath !== undefined
159
+ ? { ok: false, error: prep.err, resolvedPath: prep.resolvedPath }
160
+ : { ok: false, error: prep.err }
161
+ }
162
+
110
163
  /**
111
164
  * 本地文件系统版本令牌(mtime+size):与 ctx.fs 的不透明版本串用途相同,
112
165
  * 仅供全局片段文件(走 node:fs 直读、不经 ctx.fs)在 edrv.read 时回带。
@@ -181,6 +234,7 @@ async function applyDecisions(
181
234
  ): Promise<DecideResult[]> {
182
235
  const results: DecideResult[] = []
183
236
  const resolved: DiffRecord[] = []
237
+ const ws = findRemoteWs(cwd)
184
238
  let changed = false
185
239
  for (const item of items) {
186
240
  const record = bucket.get(item.callId)
@@ -204,7 +258,7 @@ async function applyDecisions(
204
258
  markDecision(record, item.scope ?? 'call', item.hunkIndex, item.decision)
205
259
  changed = true
206
260
  if (!wasResolved && recordResolved(record)) resolved.push(record)
207
- const result: DecideResult = { callId: item.callId, ok: true, record: recView(record) }
261
+ const result: DecideResult = { callId: item.callId, ok: true, record: recView(record, ws) }
208
262
  if (revertedStale) result.stale = true
209
263
  results.push(result)
210
264
  }
@@ -241,8 +295,7 @@ async function latestPatchBackup(patchPath: string): Promise<string | undefined>
241
295
  /** 读取设置中的深链基址(缺省/非法回退默认 3080)。 */
242
296
  async function integrationBaseUrlOf(ctx: Ctx): Promise<string> {
243
297
  const settings = ctx.get('settings')
244
- const descriptor = settings?.describe?.({ redactSecrets: true })?.find((item: { ns?: string }) => item.ns === FILE_OPEN_SETTINGS_NS)
245
- const value = descriptor?.value as { integrationBaseUrl?: unknown } | undefined
298
+ const value = sectionOf(settings, FILE_OPEN_SETTINGS_NS)?.value as { integrationBaseUrl?: unknown } | undefined
246
299
  const raw = typeof value?.integrationBaseUrl === 'string' ? value.integrationBaseUrl.trim() : ''
247
300
  return raw || INTEGRATION_BASE_DEFAULT
248
301
  }
@@ -384,11 +437,12 @@ export function buildHandlers(
384
437
  }
385
438
  }
386
439
  const out: RecordView[] = []
440
+ const ws = findRemoteWs(sc.cwd)
387
441
  for (const rec of bucket.values()) {
388
442
  // 面板全量查询过滤已归档;聊天条按 callId 查询保留(状态徽章仍需正确显示)
389
443
  if (!want && rec.archived) continue
390
444
  if (want && !want.has(rec.callId)) continue
391
- out.push(recView(rec))
445
+ out.push(recView(rec, ws))
392
446
  }
393
447
  out.sort((a, b) => (a.at < b.at ? -1 : 1))
394
448
  return { ok: true, records: out }
@@ -433,26 +487,36 @@ export function buildHandlers(
433
487
  }
434
488
  const sc = await requireSession(ctx, args.sessionId)
435
489
  if ('err' in sc) return { ok: false, error: sc.err }
436
- const fs = ctx.get('fs')
437
- if (!fs) return { ok: false, error: '缺少 fs' }
490
+ const prep = await readTargetOf(ctx, sc, args.path)
491
+ if ('err' in prep) return targetErrOf(prep)
438
492
  try {
439
- const target = await resolveTarget(ctx, sc.session, args.path)
440
- debugRecord(ctx, sc.cwd, '[DEBUG path.resolve] input=' + String(args.path ?? '') + ' resolved=' + fs.processPath(target), 'debug')
441
- const info = await fs.stat(target)
442
- if (!info || info.type !== 'file') {
443
- // 带上解析后的真实路径:跳转失败时可直接看出是路径解析错还是目标本身不存在
444
- return { ok: false, error: '文件不存在', resolvedPath: fs.processPath(target) }
445
- }
446
- if ((info.size ?? 0) > BINARY_READ_CAP) return { ok: false, error: '文件过大(>32MB),不支持整文件预览' }
447
493
  if (args.encoding === 'base64') {
448
- // 图片/PDF 等二进制预览:readBytes 无解码、无二进制拒绝;超上限已由上方 stat 拦截
449
- const bytes = await fs.readBytes(target, undefined, BINARY_READ_CAP)
494
+ // 图片/PDF 等二进制预览:readBytes 无解码、无二进制拒绝;超上限已由 readTargetOf 拦截
495
+ const bytes = await prep.fs.readBytes(prep.target, undefined, BINARY_READ_CAP)
450
496
  const content = Buffer.from(bytes).toString('base64')
451
- return { ok: true, content, size: bytes.byteLength, encoding: 'base64', mime: binaryMimeOf(args.path), version: String(info.version ?? '') }
497
+ return { ok: true, content, size: bytes.byteLength, encoding: 'base64', mime: binaryMimeOf(args.path), version: String(prep.info.version ?? '') }
498
+ }
499
+ if ((prep.info.size ?? 0) > READ_CAP) return { ok: false, error: '文件过大(>8MB),不支持整文件预览' }
500
+ const content = await prep.fs.readText(prep.target)
501
+ return { ok: true, content, size: content.length, version: String(prep.info.version ?? '') }
502
+ } catch (error) {
503
+ return { ok: false, error: '读取失败:' + String(error) }
504
+ }
505
+ },
506
+ 'edrv.readBinary': async (args) => {
507
+ // 二进制直读通道(图片/PDF 预览):与 edrv.read base64 分支共用 readTargetOf 前置,
508
+ // 返回 Uint8Array 信封;routes 接线后以 octet-stream + x-edrv-* 头直出,
509
+ // 未接线时信封被 JSON 化,由 client 探测到后自动回退 base64 路径(见 shared/rpc readBinaryPreview)。
510
+ const sc = await requireSession(ctx, args.sessionId)
511
+ if ('err' in sc) return { ok: false, error: sc.err }
512
+ const prep = await readTargetOf(ctx, sc, args.path)
513
+ if ('err' in prep) return targetErrOf(prep)
514
+ try {
515
+ const bytes = await prep.fs.readBytes(prep.target, undefined, BINARY_READ_CAP)
516
+ return {
517
+ ok: true,
518
+ binary: { bytes, mime: binaryMimeOf(args.path), size: bytes.byteLength, version: String(prep.info.version ?? '') },
452
519
  }
453
- if ((info.size ?? 0) > READ_CAP) return { ok: false, error: '文件过大(>8MB),不支持整文件预览' }
454
- const content = await fs.readText(target)
455
- return { ok: true, content, size: content.length, version: String(info.version ?? '') }
456
520
  } catch (error) {
457
521
  return { ok: false, error: '读取失败:' + String(error) }
458
522
  }
@@ -550,6 +614,7 @@ export function buildHandlers(
550
614
  const sc = await requireSession(ctx, args.sessionId)
551
615
  if ('err' in sc) return { ok: false, error: sc.err }
552
616
  const batches = parseArchive(await readArchiveText(ctx, sc.cwd)).filter((b) => b.cwd === sc.cwd)
617
+ const ws = findRemoteWs(sc.cwd)
553
618
  const entries = batches.map((b) => {
554
619
  const recs = Array.isArray(b.records) ? b.records : []
555
620
  const sum = recs.reduce((s, r) => {
@@ -560,7 +625,16 @@ export function buildHandlers(
560
625
  if (sm.superseded) s.superseded++
561
626
  return s
562
627
  }, { accepted: 0, rejected: 0, pending: 0, superseded: 0 })
563
- return { at: b.at, lastAt: b.lastAt || b.at, path: b.path, batch: b.batch ?? null, reason: b.reason ?? null, nRecords: recs.length, summary: sum }
628
+ return {
629
+ at: b.at,
630
+ lastAt: b.lastAt || b.at,
631
+ path: b.path,
632
+ ...(ws ? { displayPath: remoteDisplayOf(b.path, ws) ?? undefined } : {}),
633
+ batch: b.batch ?? null,
634
+ reason: b.reason ?? null,
635
+ nRecords: recs.length,
636
+ summary: sum,
637
+ }
564
638
  })
565
639
  entries.sort((a, b) => (Number(b.batch ?? -1) - Number(a.batch ?? -1)) || (a.at < b.at ? 1 : -1))
566
640
  return { ok: true, entries }
@@ -847,22 +921,20 @@ export function buildHandlers(
847
921
  },
848
922
  'vscode.fileOpenSettingsGet': async () => {
849
923
  const settings = ctx.get('settings')
850
- const descriptor = settings?.describe?.({ redactSecrets: true })?.find((item: { ns?: string }) => item.ns === FILE_OPEN_SETTINGS_NS)
851
- const value = descriptor?.value as { fileOpenTool?: unknown } | undefined
852
- return { ok: true, fileOpenTool: normalizeFileOpenTool(value?.fileOpenTool ?? FILE_OPEN_DEFAULT), integrationBaseUrl: await integrationBaseUrlOf(ctx), revision: descriptor?.revision }
924
+ const section = sectionOf(settings, FILE_OPEN_SETTINGS_NS)
925
+ const value = section?.value as { fileOpenTool?: unknown } | undefined
926
+ return { ok: true, fileOpenTool: normalizeFileOpenTool(value?.fileOpenTool ?? FILE_OPEN_DEFAULT), integrationBaseUrl: await integrationBaseUrlOf(ctx), revision: section?.revision }
853
927
  },
854
928
  'vscode.fileOpenSettingsUpdate': async (args) => {
855
929
  const settings = ctx.get('settings')
856
- if (!settings?.update) return { ok: false, error: '设置服务不可用' }
857
- try {
858
- const patch: Record<string, unknown> = { fileOpenTool: normalizeFileOpenTool(args.fileOpenTool) }
859
- if (typeof args.integrationBaseUrl === 'string' && args.integrationBaseUrl.trim()) patch.integrationBaseUrl = args.integrationBaseUrl.trim()
860
- await settings.update(FILE_OPEN_SETTINGS_NS, patch, args.expectedRevision)
861
- const descriptor = settings.describe?.({ redactSecrets: true })?.find((item: { ns?: string }) => item.ns === FILE_OPEN_SETTINGS_NS)
862
- const value = descriptor?.value as { fileOpenTool?: unknown } | undefined
863
- return { ok: true, fileOpenTool: normalizeFileOpenTool(value?.fileOpenTool), integrationBaseUrl: await integrationBaseUrlOf(ctx), revision: descriptor?.revision }
864
- } catch (error) { return { ok: false, error: String(error) }
865
- }
930
+ const patch: Record<string, unknown> = { fileOpenTool: normalizeFileOpenTool(args.fileOpenTool) }
931
+ if (typeof args.integrationBaseUrl === 'string' && args.integrationBaseUrl.trim()) patch.integrationBaseUrl = args.integrationBaseUrl.trim()
932
+ // 冲突自愈(0.1.7 SettingsConflictError 重读重试一次)与形状校验读取统一走 fileOpenSettings 工具
933
+ const result = await updateSection(settings, FILE_OPEN_SETTINGS_NS, patch, args.expectedRevision)
934
+ if (!result.ok) return { ok: false, error: result.error ?? '设置服务不可用' }
935
+ const section = sectionOf(settings, FILE_OPEN_SETTINGS_NS)
936
+ const value = section?.value as { fileOpenTool?: unknown } | undefined
937
+ return { ok: true, fileOpenTool: normalizeFileOpenTool(value?.fileOpenTool), integrationBaseUrl: await integrationBaseUrlOf(ctx), revision: section?.revision }
866
938
  },
867
939
  'compat': async () => ({ ok: true, report: await buildReport(ctx) }),
868
940
  'vscode.devFormGet': async () => ({ ok: true, devForm: readDevForm() }),
@@ -953,6 +1025,7 @@ export function buildHandlers(
953
1025
  const inventory = await scanSessionInventory(home, sessionsArchiveRoot(home))
954
1026
  const active = activeSessionIds(ctx)
955
1027
  markActiveSessions(inventory.sessions, active)
1028
+ markOfficialFlags(inventory.sessions, ctx)
956
1029
  return { ok: true, ...inventory, activeIds: [...active] }
957
1030
  } catch (error) {
958
1031
  return { ok: false, error: '盘点失败:' + String(error) }
@@ -990,7 +1063,10 @@ export function buildHandlers(
990
1063
  'edrv.perf.restore': async (args) => {
991
1064
  try {
992
1065
  const result = await restoreSession(dshHome(), sessionsArchiveRoot(dshHome()), args.workspaceKey, args.sessionId)
993
- return result.ok ? { ok: true, restored: true } : { ok: false, error: result.error ?? '恢复失败' }
1066
+ if (!result.ok) return { ok: false, error: result.error ?? '恢复失败' }
1067
+ // 目录搬回后对齐官方归档标志(best-effort:服务缺失/调用失败不影响恢复结果,见 perf.unarchiveOfficial)
1068
+ await unarchiveOfficial(ctx, args.sessionId)
1069
+ return { ok: true, restored: true }
994
1070
  } catch (error) {
995
1071
  return { ok: false, error: '恢复失败:' + String(error) }
996
1072
  }
package/src/shared/ai.ts CHANGED
@@ -66,6 +66,12 @@ export interface AiConfigView {
66
66
  model: string
67
67
  /** 空 = 跟随模型默认(请求不携带 reasoningEffort)。 */
68
68
  effort: string
69
+ /** AI 任务模型(非补全场景):空/缺省 = 跟随补全配置。 */
70
+ taskProvider?: string
71
+ /** AI 任务模型 id(与 taskProvider 成对;空/缺省 = 跟随补全配置)。 */
72
+ taskModel?: string
73
+ /** AI 任务模型思考档位(空/缺省 = 跟随补全配置的 effort)。 */
74
+ taskEffort?: string
69
75
  }
70
76
 
71
77
  /** AI 补全配置更新载荷(edrv.ai.configUpdate)。 */
@@ -75,6 +81,12 @@ export interface AiConfigPatch {
75
81
  model?: string
76
82
  /** 空 = 清除(跟随默认)。 */
77
83
  effort?: string
84
+ /** AI 任务模型(非补全场景):空 = 清除(跟随补全配置)。 */
85
+ taskProvider?: string
86
+ /** AI 任务模型 id(与 taskProvider 成对更新)。 */
87
+ taskModel?: string
88
+ /** AI 任务模型思考档位:空 = 清除(跟随补全配置的 effort)。 */
89
+ taskEffort?: string
78
90
  }
79
91
 
80
92
  /**
package/src/shared/lsp.ts CHANGED
@@ -125,6 +125,82 @@ export function decodeSemanticTokens(
125
125
  return out
126
126
  }
127
127
 
128
+ /**
129
+ * LSP CompletionItemKind(协议 1-based)→ Monaco CompletionItemKind 枚举名(0-based)。
130
+ *
131
+ * 为什么按**名称**而不是编号直传:两侧枚举编号并不一致 —— LSP 2=Method,而 Monaco
132
+ * 0=Method。若把协议编号当 Monaco 编号塞回去,草稿纸上的「只差一位」会变成全表错位
133
+ * (属性显示成方法图标等)。用名称映射让两侧各自的编号在各自类型系统里解析,杜绝硬编码。
134
+ * 索引 i 对应 LSP kind i(索引 0 占位,协议从 1 起)。
135
+ * @author ddj 2026年09月22号
136
+ */
137
+ export const LSP_COMPLETION_KIND_NAMES = [
138
+ '', 'Text', 'Method', 'Function', 'Constructor', 'Field', 'Variable', 'Class',
139
+ 'Interface', 'Module', 'Property', 'Unit', 'Value', 'Enum', 'Keyword', 'Snippet',
140
+ 'Color', 'File', 'Reference', 'Folder', 'EnumMember', 'Constant', 'Struct',
141
+ 'Event', 'Operator', 'TypeParameter',
142
+ ] as const
143
+
144
+ /** LSP InsertTextFormat:1=PlainText,2=Snippet。 */
145
+ export const LSP_INSERT_TEXT_FORMAT_SNIPPET = 2
146
+
147
+ /**
148
+ * LSP 补全项文本编辑范围(单 range 或 insert/replace 双 range 形态)。
149
+ * 两种形态由服务器任选,必须都能解析:只认一种会让另一家服务器的补全落点错位。
150
+ */
151
+ export interface LspCompletionTextEdit {
152
+ range?: LspRange
153
+ insert?: LspRange
154
+ replace?: LspRange
155
+ newText: string
156
+ }
157
+
158
+ /** 归一化后的补全项(host 归一化;client 映射为 Monaco suggestion)。 */
159
+ export interface LspCompletionItem {
160
+ label: string
161
+ kind?: number // LSP CompletionItemKind 原始编号(client 经名称表转 Monaco)
162
+ detail?: string
163
+ documentation?: string
164
+ insertText?: string
165
+ insertTextFormat?: number
166
+ textEdit?: LspCompletionTextEdit
167
+ additionalTextEdits?: Array<{ range: LspRange; newText: string }>
168
+ sortText?: string
169
+ filterText?: string
170
+ preselect?: boolean
171
+ commitCharacters?: string[]
172
+ deprecated?: boolean
173
+ data?: unknown // 原样透传,completionItem/resolve 需回传
174
+ }
175
+
176
+ /** 归一化后的补全列表(incomplete:服务器要求按已输入内容重查)。 */
177
+ export interface LspCompletionList {
178
+ items: LspCompletionItem[]
179
+ incomplete: boolean
180
+ truncated?: boolean
181
+ }
182
+
183
+ /** 签名帮助中的单个参数(label 可为字符串或 [start, end] 元组,原样透传)。 */
184
+ export interface LspParameterInformation {
185
+ label: string | [number, number]
186
+ documentation?: string
187
+ }
188
+
189
+ /** 归一化后的单个签名。 */
190
+ export interface LspSignatureInformation {
191
+ label: string
192
+ documentation?: string
193
+ parameters: LspParameterInformation[]
194
+ activeParameter?: number
195
+ }
196
+
197
+ /** 归一化后的签名帮助结果。 */
198
+ export interface LspSignatureHelp {
199
+ signatures: LspSignatureInformation[]
200
+ activeSignature: number
201
+ activeParameter: number
202
+ }
203
+
128
204
  /** LSP 服务器能力子集:本插件会用到的方法。 */
129
205
  export interface LspServerCapabilities {
130
206
  definition: boolean
@@ -134,6 +210,9 @@ export interface LspServerCapabilities {
134
210
  workspaceSymbol: boolean
135
211
  hover: boolean
136
212
  semanticTokens: boolean
213
+ completion: boolean
214
+ completionResolve: boolean
215
+ signatureHelp: boolean
137
216
  semanticTokenTypes?: string[]
138
217
  semanticTokenModifiers?: string[]
139
218
  }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * dsh-vscode-mode shared — 原生打开范围判定(host/client 双面契约)。
3
+ * 背景:DSH 文件预览双形态——本插件认领 `dsh-resource://file/**` 进 Monaco 编辑,
4
+ * 官方渲染器(Office 预览/不可预览提示/表格预览)看认领让位。本模块承载「原生打开」
5
+ * 范围的单一事实源:默认集 = 让位清单(插件本来就不认领的后缀),用户可经设置
6
+ * `nativeOpenExts`(逗号分隔)增删;host 的 Config schema 默认值与 client 的
7
+ * 文件树点击判定 / claim 认领判定共用,保证三处永不同源漂移。
8
+ * 决策沿袭(不因本模块迁移而变):CSV/TSV **不在**默认集——官方 0.1.7 有表格预览,
9
+ * 但编辑器可编辑性优先,仅用户显式加入 nativeOpenExts 才让位(同 avif 例外先例)。
10
+ * 作者 ddj 2026年09月22号
11
+ */
12
+
13
+ /**
14
+ * Office 文档后缀:官方 `dsh-client-ui-sidebar-documentpreview` 的 Office 渲染器
15
+ * 声明 `doc/docx/xls/xlsx/ppt/pptx`(0.1.6 起侧栏 Office 预览;0.1.7 扩展表格 CSV/TSV
16
+ * 只读预览——本清单刻意不含 csv/tsv,见文件头决策)。本插件让位官方(不认领),
17
+ * 否则会把这些文件路由进 Monaco 而丢掉官方预览。
18
+ */
19
+ export const OFFICE_EXT: readonly string[] = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp', 'pages', 'numbers']
20
+
21
+ /**
22
+ * 官方「不可预览二进制容器」清单(`UNVIEWABLE_BINARY_EXTENSIONS`,逐项取自
23
+ * `dsh-client-ui-sidebar-documentpreview/lib/client.js`)。这些后缀官方会给出
24
+ * 「无法预览」提示;本插件让位官方,避免把二进制当文本读成乱码。
25
+ *
26
+ * ⚠️ 刻意例外:官方清单含 `avif`,但本插件图片预览支持 avif(浏览器原生解码),
27
+ * 故从本表**移除** `avif` —— 保留本插件的图片预览优于官方的「不可预览」。
28
+ * @author ddj 2026年09月18号
29
+ */
30
+ export const BLIND_EXT: readonly string[] = [
31
+ // 媒体
32
+ 'mp4', 'mov', 'avi', 'mkv', 'webm', 'flv', 'wmv', 'm4v', 'mp3', 'wav', 'flac', 'ogg', 'm4a', 'aac', 'wma', 'opus',
33
+ // 归档
34
+ 'zip', 'gz', 'tgz', 'bz2', 'xz', 'zst', '7z', 'rar', 'tar', 'jar',
35
+ // 可执行与库
36
+ 'exe', 'dll', 'so', 'dylib', 'bin', 'o', 'class', 'pyc', 'wasm',
37
+ // 字体
38
+ 'ttf', 'otf', 'woff', 'woff2', 'eot',
39
+ // 磁盘映像
40
+ 'dmg', 'iso', 'img',
41
+ // 数据库与设计文件
42
+ 'sqlite', 'db', 'psd', 'ai', 'sketch', 'tiff', 'tif', 'heic', 'heif',
43
+ ]
44
+
45
+ /** 原生打开默认后缀集 = 让位清单并集(两清单元素天然互斥,直接拼接)。 */
46
+ export const DEFAULT_NATIVE_EXT: readonly string[] = [...OFFICE_EXT, ...BLIND_EXT]
47
+
48
+ /** 默认范围的设置序列(逗号分隔;Config schema 默认值与设置页回显同源)。 */
49
+ export const DEFAULT_NATIVE_CSV: string = DEFAULT_NATIVE_EXT.join(',')
50
+
51
+ /**
52
+ * 取 path 的 basename 后缀(小写、无点);无后缀/隐藏文件返回 ''。
53
+ * @author ddj 2026年09月22号
54
+ * @param path 文件路径(`/` 或 `\` 分隔均可)
55
+ * @returns 小写后缀或 ''
56
+ */
57
+ export function suffixOf(path: unknown): string {
58
+ const base = String(path ?? '').replace(/\\/g, '/').split('/').pop() ?? ''
59
+ const dot = base.lastIndexOf('.')
60
+ return dot > 0 ? base.slice(dot + 1).toLowerCase() : ''
61
+ }
62
+
63
+ /**
64
+ * 解析设置串为后缀集合:逗号分隔、去空白、去前导点、小写;空段丢弃。
65
+ * 解析失败/空串 → 空集合(= 不追加任何原生打开范围,让位仍走 deferToOfficial)。
66
+ * @author ddj 2026年09月22号
67
+ * @param csv 逗号分隔后缀串
68
+ * @returns 后缀集合
69
+ */
70
+ export function parseExtCsv(csv: unknown): Set<string> {
71
+ const out = new Set<string>()
72
+ if (typeof csv !== 'string') return out
73
+ for (const raw of csv.split(',')) {
74
+ const item = raw.trim().replace(/^\./, '').toLowerCase()
75
+ if (item) out.add(item)
76
+ }
77
+ return out
78
+ }
79
+
80
+ /**
81
+ * 该路径是否命中「原生打开」范围(仅看用户配置集合;默认集语义由调用方
82
+ * 与 deferToOfficial 取并,保持两判定各司其职)。
83
+ * @author ddj 2026年09月22号
84
+ * @param path 文件路径
85
+ * @param exts 已解析的后缀集合(parseExtCsv 产物)
86
+ * @returns 是否命中
87
+ */
88
+ export function inNativeOpen(path: unknown, exts: ReadonlySet<string>): boolean {
89
+ const ext = suffixOf(path)
90
+ return ext !== '' && exts.has(ext)
91
+ }