dsh-vscode-mode 0.1.35 → 0.1.42

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/src/lsp/rpc.ts CHANGED
@@ -6,10 +6,11 @@
6
6
  import type { Ctx } from '../store.js'
7
7
  import { searchRoot } from '../search/ripgrep.js'
8
8
  import { sessionOf, cwdOf } from '../registry.js'
9
- import { resolveProviderSpec, langOfPath, configFromPlugin, configFromSettings, LSP_SETTINGS_NS, type LspConfig } from './config.js'
9
+ import { resolveProviderSpec, langOfPath, configFromPlugin, configFromSettings, LSP_LANGUAGES, LSP_SETTINGS_NS, type LspConfig } from './config.js'
10
+ import { clearProviderCache } from './providers.js'
10
11
  import type { LspManager } from './manager.js'
11
12
  import type { RpcHandlerMap } from '../shared/rpc.js'
12
- import type { LspLocation } from '../shared/lsp.js'
13
+ import type { LspLocation, LspServerStatus } from '../shared/lsp.js'
13
14
  import { installFromMarket, installVsixFile, listInstalled, marketGet, marketSearch, uninstall, updateExtension, type ExtInfo } from './extmgr.js'
14
15
 
15
16
  export interface LspRpcDeps {
@@ -85,6 +86,21 @@ function createDocTracker() {
85
86
  sessionDocs.delete(sessionId)
86
87
  return [...toRelease]
87
88
  },
89
+ /** 清除指定语言的文档归属,让当前客户端重新同步时重新 acquire。 */
90
+ resetLanguage(languageId: string): void {
91
+ for (const [docKey, owners] of openDocs) {
92
+ if (!docKey.includes('|' + languageId + '|')) continue
93
+ for (const sessionId of owners) forgetDoc(sessionId, docKey)
94
+ openDocs.delete(docKey)
95
+ }
96
+ for (const key of [...rootLangRefs.keys()]) if (key.endsWith('|' + languageId)) rootLangRefs.delete(key)
97
+ },
98
+ /** 返回指定会话已打开的文档路径,用于重检测后恢复同步。 */
99
+ pathsFor(sessionId: string, languageId: string): string[] {
100
+ const docs = sessionDocs.get(sessionId)
101
+ if (!docs) return []
102
+ return [...docs].filter((key) => key.includes('|' + languageId + '|')).map((key) => key.slice(key.lastIndexOf('|') + 1))
103
+ },
88
104
  }
89
105
  }
90
106
 
@@ -122,6 +138,41 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
122
138
  const rootLocations = (locations: LspLocation[], root: string): LspLocation[] =>
123
139
  locations.map((location) => ({ ...location, root }))
124
140
 
141
+ /** 当前 provider 检测结论 → 设置页 idle 状态(不启动 server)。 */
142
+ const detectedStatus = (languageId: string, root?: string): LspServerStatus => {
143
+ const spec = resolveProviderSpec(ctx, pluginConfig, languageId)
144
+ return {
145
+ languageId,
146
+ source: spec.kind,
147
+ phase: 'idle',
148
+ reason: spec.ready ? undefined : spec.reason,
149
+ version: spec.version,
150
+ providerName: spec.providerName,
151
+ root,
152
+ }
153
+ }
154
+
155
+ /**
156
+ * 清发现缓存并重置旧 server(保留 tracker 文档记录);
157
+ * 下一次模型同步会按最新配置重新 acquire,已打开文档无需重开。
158
+ * 作者 ddj 2026年09月02号
159
+ * @param languageId 语言 id
160
+ * @param sessionId 会话语境(缺省重置全部工作区该语言)
161
+ * @returns 检测后的 idle 状态列表
162
+ */
163
+ const redetectLanguage = async (languageId: string, sessionId?: string): Promise<LspServerStatus[]> => {
164
+ clearProviderCache()
165
+ if (sessionId) {
166
+ const sc = await rootOf(sessionId)
167
+ if ('err' in sc) return []
168
+ manager.reset(sc.root, languageId)
169
+ return [detectedStatus(languageId, sc.root)]
170
+ }
171
+ const roots = [...new Set(manager.statusAll().filter((s) => s.languageId === languageId).map((s) => s.root))]
172
+ manager.resetLanguage(languageId)
173
+ return roots.length ? roots.map((root) => detectedStatus(languageId, root)) : [detectedStatus(languageId)]
174
+ }
175
+
125
176
  const disposeSession = (sessionId: string): void => {
126
177
  for (const rl of tracker.closeSession(sessionId)) {
127
178
  const sep = rl.lastIndexOf('|')
@@ -160,10 +211,12 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
160
211
  const settings = ctx.get('settings')
161
212
  if (!settings?.update) return { ok: false, error: '设置服务不可用' }
162
213
  await settings.update(LSP_SETTINGS_NS, { [lang]: target })
214
+ // 保存/切换后立即重新检测:清缓存 + 重置旧 server,旧状态不残留
215
+ const servers = await redetectLanguage(lang)
163
216
  const descriptor = settings.describe?.({ redactSecrets: true })?.find((item: { ns?: string }) => item.ns === LSP_SETTINGS_NS)
164
217
  const stored = descriptor?.value as Record<string, unknown> | undefined
165
218
  const merged: LspConfig = { ...configFromPlugin(pluginConfig), ...(stored as LspConfig) }
166
- return { ok: true, config: merged as unknown as Record<string, unknown> }
219
+ return { ok: true, config: merged as unknown as Record<string, unknown>, servers }
167
220
  } catch (error) {
168
221
  return { ok: false, error: '配置保存失败:' + String(error) }
169
222
  }
@@ -274,6 +327,31 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
274
327
  }
275
328
  },
276
329
 
330
+ 'edrv.lsp.semanticTokens': async (args) => {
331
+ const lang = langOfPath(args.path)
332
+ if (!lang) return { ok: true, tokens: undefined }
333
+ const sc = await rootOf(args.sessionId)
334
+ if ('err' in sc) return { ok: false, error: sc.err }
335
+ try {
336
+ const server = serverOf(sc.root, lang)
337
+ if (!server) return { ok: true, tokens: undefined }
338
+ return { ok: true, tokens: (await server.semanticTokens(args.path)) ?? undefined }
339
+ } catch (error) {
340
+ return { ok: false, error: 'LSP 语义分色查询失败:' + String(error) }
341
+ }
342
+ },
343
+
344
+ 'edrv.lsp.redetect': async (args) => {
345
+ if (!(LSP_LANGUAGES as readonly string[]).includes(args.languageId)) {
346
+ return { ok: false, error: '不支持的语言:' + args.languageId }
347
+ }
348
+ try {
349
+ return { ok: true, servers: await redetectLanguage(args.languageId) }
350
+ } catch (error) {
351
+ return { ok: false, error: '重新检测失败:' + String(error) }
352
+ }
353
+ },
354
+
277
355
  // --region 扩展管理(edrv.lsp.ext.*:已装/市场/安装/卸载/更新)
278
356
  'edrv.lsp.ext.list': async () => ({ ok: true, extensions: listInstalled() }),
279
357
 
@@ -292,6 +370,7 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
292
370
  if (args.vsixPath) info = await installVsixFile(args.vsixPath)
293
371
  else if (args.namespace && args.name) info = await installFromMarket(args.namespace, args.name, args.version)
294
372
  else return { ok: false, error: '缺少安装源(vsixPath 或 namespace+name)' }
373
+ clearProviderCache() // 新扩展可能成为 Lua provider,立即丢弃旧发现结果
295
374
  return { ok: true, extension: info }
296
375
  } catch (error) {
297
376
  return { ok: false, error: '安装失败:' + String(error) }
@@ -299,7 +378,9 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
299
378
  },
300
379
 
301
380
  'edrv.lsp.ext.uninstall': async (args) => {
302
- return uninstall(args.id) ? { ok: true } : { ok: false, error: '扩展未安装:' + args.id }
381
+ const ok = uninstall(args.id)
382
+ if (ok) clearProviderCache()
383
+ return ok ? { ok: true } : { ok: false, error: '扩展未安装:' + args.id }
303
384
  },
304
385
 
305
386
  'edrv.lsp.ext.update': async (args) => {
@@ -309,6 +390,7 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
309
390
  const current = listInstalled().find((e) => e.id === args.id)
310
391
  return current ? { ok: true, extension: current, updated: false } : { ok: false, error: '扩展未安装:' + args.id }
311
392
  }
393
+ clearProviderCache()
312
394
  return { ok: true, extension: updated, updated: true }
313
395
  } catch (error) {
314
396
  return { ok: false, error: '更新失败:' + String(error) }
package/src/lsp/server.ts CHANGED
@@ -8,8 +8,10 @@ import { spawnServer, type Transport } from './transport.js'
8
8
  import { createLspClient, type LspClient } from './client.js'
9
9
  import { pathToFileUri } from './uri.js'
10
10
  import type { LspProviderSpec } from './providers.js'
11
+ import { LSP_SEMANTIC_TOKEN_MODIFIERS, LSP_SEMANTIC_TOKEN_TYPES } from '../shared/lsp.js'
11
12
  import type {
12
13
  LspLocation,
14
+ LspSemanticTokens,
13
15
  LspServerCapabilities,
14
16
  LspServerPhase,
15
17
  LspServerStatus,
@@ -31,6 +33,7 @@ export interface LspServer {
31
33
  documentSymbol(path: string): Promise<LspSymbol[]>
32
34
  workspaceSymbol(query: string): Promise<LspSymbol[]>
33
35
  hover(path: string, line: number, character: number): Promise<{ contents: string[] } | null>
36
+ semanticTokens(path: string): Promise<LspSemanticTokens | null>
34
37
  dispose(): Promise<void>
35
38
  }
36
39
 
@@ -59,18 +62,39 @@ export function createLspServer(spec: LspProviderSpec, root: string, languageId:
59
62
  let phase: LspServerPhase = 'idle'
60
63
  let transport: Transport | null = null
61
64
  let client: LspClient | null = null
62
- let capabilities: LspServerCapabilities = { definition: false, references: false, documentSymbol: false, workspaceSymbol: false, hover: false }
65
+ let capabilities: LspServerCapabilities = {
66
+ definition: false,
67
+ references: false,
68
+ documentSymbol: false,
69
+ workspaceSymbol: false,
70
+ hover: false,
71
+ semanticTokens: false,
72
+ semanticTokenTypes: [...LSP_SEMANTIC_TOKEN_TYPES],
73
+ semanticTokenModifiers: [...LSP_SEMANTIC_TOKEN_MODIFIERS],
74
+ }
63
75
  let disposed = false
64
76
  let readyWait: Promise<boolean> = Promise.resolve(false)
65
77
  let resolveReady: ((ready: boolean) => void) | null = null
66
78
  let lastProgress: { value?: number; message?: string } = {}
67
- let lastStatus: LspServerStatus = { languageId, source: spec.kind, phase: 'idle', reason: spec.ready ? undefined : spec.reason, root }
79
+ let lastStatus: LspServerStatus = {
80
+ languageId,
81
+ source: spec.kind,
82
+ phase: 'idle',
83
+ reason: spec.ready ? undefined : spec.reason,
84
+ root,
85
+ version: spec.version,
86
+ providerName: spec.providerName ?? (spec.version ? '语言服务器扩展' : undefined),
87
+ }
68
88
 
69
89
  const log = (line: string): void => logger?.('[' + languageId + '@' + root + '] ' + line)
90
+ const providerInfo = {
91
+ version: spec.version,
92
+ providerName: spec.providerName ?? (spec.version ? 'EmmyLua' : undefined),
93
+ }
70
94
 
71
95
  const setPhase = (next: LspServerPhase, reason?: string): void => {
72
96
  phase = next
73
- lastStatus = { languageId, source: spec.kind, phase: next, reason, root, ...progressFields() }
97
+ lastStatus = { languageId, source: spec.kind, phase: next, reason, root, ...providerInfo, ...progressFields() }
74
98
  server.onStateChange?.(lastStatus)
75
99
  }
76
100
 
@@ -254,6 +278,25 @@ export function createLspServer(spec: LspProviderSpec, root: string, languageId:
254
278
  return { contents: stringifyHoverContents(result.contents) }
255
279
  },
256
280
 
281
+ /** 查询全文 semantic tokens,并将服务器 legend 归一到插件固定 legend。 */
282
+ async semanticTokens(path: string): Promise<LspSemanticTokens | null> {
283
+ const doc = docs.get(path)
284
+ if (!doc || !(await readyWait) || !capabilities.semanticTokens) return null
285
+ const result = await ensureClient().request<{ data?: unknown; resultId?: unknown } | null>(
286
+ 'textDocument/semanticTokens/full',
287
+ { textDocument: { uri: doc.uri } },
288
+ )
289
+ if (!result || !Array.isArray(result.data)) return null
290
+ return {
291
+ data: normalizeSemanticData(
292
+ result.data,
293
+ capabilities.semanticTokenTypes ?? [],
294
+ capabilities.semanticTokenModifiers ?? [],
295
+ ),
296
+ resultId: typeof result.resultId === 'string' ? result.resultId : undefined,
297
+ }
298
+ },
299
+
257
300
  async dispose(): Promise<void> {
258
301
  disposed = true
259
302
  resolveReady?.(false)
@@ -344,3 +387,37 @@ function toInt(value: unknown): number {
344
387
  const n = Number(value)
345
388
  return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0
346
389
  }
390
+
391
+ /** 把服务器 legend 下的 5 元组流转换为插件固定 legend 下的 5 元组流。 */
392
+ function normalizeSemanticData(data: unknown[], types: string[], modifiers: string[]): number[] {
393
+ const out: number[] = []
394
+ let previousLine = 0
395
+ let previousCharacter = 0
396
+ let outputLine = 0
397
+ let outputCharacter = 0
398
+ for (let i = 0; i + 4 < data.length; i += 5) {
399
+ const deltaLine = toInt(data[i])
400
+ const deltaCharacter = toInt(data[i + 1])
401
+ const length = toInt(data[i + 2])
402
+ const sourceType = types[toInt(data[i + 3])] ?? 'variable'
403
+ const sourceModifiers = toInt(data[i + 4])
404
+ const line = previousLine + deltaLine
405
+ const character = deltaLine === 0 ? previousCharacter + deltaCharacter : deltaCharacter
406
+ const typeIndex = Math.max(0, LSP_SEMANTIC_TOKEN_TYPES.indexOf(sourceType as never))
407
+ let modifierBits = 0
408
+ for (let bit = 0; bit < modifiers.length; bit++) {
409
+ if ((sourceModifiers & (1 << bit)) === 0) continue
410
+ const modifier = modifiers[bit]
411
+ const targetBit = LSP_SEMANTIC_TOKEN_MODIFIERS.indexOf(modifier as never)
412
+ if (targetBit >= 0) modifierBits |= 1 << targetBit
413
+ }
414
+ const outputDeltaLine = line - outputLine
415
+ const outputDeltaCharacter = outputDeltaLine === 0 ? character - outputCharacter : character
416
+ out.push(outputDeltaLine, outputDeltaCharacter, length, typeIndex, modifierBits)
417
+ previousLine = line
418
+ previousCharacter = character
419
+ outputLine = line
420
+ outputCharacter = character
421
+ }
422
+ return out
423
+ }
package/src/paths.ts CHANGED
@@ -25,6 +25,8 @@ export const DSH_DIR_NAME = '.dsh'
25
25
  export const DSH_HOME_ENV = 'DSH_HOME'
26
26
  /** 树缓存 schema 版本(文件名携带;版本递增即触发旧版清理)。 */
27
27
  export const TREE_CACHE_SCHEMA = 1
28
+ /** LSP provider 发现缓存 schema 版本(格式变更即递增,旧文件自动失效)。 */
29
+ export const LSP_SPEC_CACHE_SCHEMA = 4
28
30
  /** 树缓存保留期:超期文件视为废弃(工作区搬迁/废弃残留)。 */
29
31
  export const TREE_RETENTION_MS = 30 * 24 * 60 * 60 * 1000
30
32
  /** 插件缓存目录总预算:超限按 mtime 从旧到新删文件,防任意增长撑爆磁盘(最后兜底)。 */
@@ -141,6 +143,11 @@ export function userCacheDir(home = dshHome()): string {
141
143
  return join(pluginCacheRoot(home), 'user')
142
144
  }
143
145
 
146
+ /** LSP provider 发现结果缓存文件(用户级:user/lsp-specs.v<schema>.json,跨会话/跨重启共享)。 */
147
+ export function lspSpecCacheFile(home = dshHome(), schema = LSP_SPEC_CACHE_SCHEMA): string {
148
+ return join(userCacheDir(home), 'lsp-specs.v' + schema + '.json')
149
+ }
150
+
144
151
  /** 插件日志根(~/.dsh/dsh-vscode-mode/logs,用户级)。 */
145
152
  export function pluginLogRoot(home = dshHome()): string {
146
153
  return join(home, PLUGIN_ID, 'logs')
package/src/rpc.ts CHANGED
@@ -284,7 +284,10 @@ export function buildHandlers(
284
284
  try {
285
285
  const target = await resolveTarget(ctx, sc.session, args.path)
286
286
  const info = await fs.stat(target)
287
- if (!info || info.type !== 'file') return { ok: false, error: '文件不存在' }
287
+ if (!info || info.type !== 'file') {
288
+ // 带上解析后的真实路径:跳转失败时可直接看出是路径解析错还是目标本身不存在
289
+ return { ok: false, error: '文件不存在', resolvedPath: fs.processPath(target) }
290
+ }
288
291
  if ((info.size ?? 0) > READ_CAP) return { ok: false, error: '文件过大(>8MB),不支持整文件预览' }
289
292
  const content = await fs.readText(target)
290
293
  return { ok: true, content, size: content.length }
package/src/shared/lsp.ts CHANGED
@@ -49,6 +49,26 @@ export interface LspHover {
49
49
  range?: LspRange
50
50
  }
51
51
 
52
+ /** LSP semantic token 标准类型(host 归一化后,client 与 Monaco 共用)。 */
53
+ export const LSP_SEMANTIC_TOKEN_TYPES = [
54
+ 'namespace', 'type', 'class', 'enum', 'interface', 'struct', 'typeParameter',
55
+ 'parameter', 'variable', 'property', 'enumMember', 'event', 'function', 'method',
56
+ 'macro', 'label', 'comment', 'string', 'keyword', 'number', 'regexp', 'operator',
57
+ 'decorator',
58
+ ] as const
59
+
60
+ /** LSP semantic token 标准修饰符(host 归一化后,client 与 Monaco 共用)。 */
61
+ export const LSP_SEMANTIC_TOKEN_MODIFIERS = [
62
+ 'declaration', 'definition', 'readonly', 'static', 'deprecated', 'abstract',
63
+ 'async', 'modification', 'documentation', 'defaultLibrary', 'global', 'local', 'mutable',
64
+ ] as const
65
+
66
+ /** 归一化后的 LSP semanticTokens/full 结果(data 使用 LSP delta 编码)。 */
67
+ export interface LspSemanticTokens {
68
+ data: number[]
69
+ resultId?: string
70
+ }
71
+
52
72
  /** LSP 服务器能力子集:本插件会用到的方法。 */
53
73
  export interface LspServerCapabilities {
54
74
  definition: boolean
@@ -56,6 +76,9 @@ export interface LspServerCapabilities {
56
76
  documentSymbol: boolean
57
77
  workspaceSymbol: boolean
58
78
  hover: boolean
79
+ semanticTokens: boolean
80
+ semanticTokenTypes?: string[]
81
+ semanticTokenModifiers?: string[]
59
82
  }
60
83
 
61
84
  /** 服务器进程/会话状态。 */
@@ -77,6 +100,7 @@ export interface LspServerStatus {
77
100
  progress?: number // LSP 工作区解析进度(0–100,未知时省略)
78
101
  progressMessage?: string
79
102
  root?: string // 绑定工作区根(相对显示)
103
+ providerName?: string // provider 名称(如 EmmyLua / LuaLS)
80
104
  }
81
105
 
82
106
  /**
package/src/shared/rpc.ts CHANGED
@@ -14,7 +14,7 @@ import type {
14
14
  } from './types.js'
15
15
  import type { MpcConfig, MpcProject, MpcProjectSaveInput, MpcServer } from './mcp.js'
16
16
  import type { CompatReport, DevFormInfo } from './compat.js'
17
- import type { LspExtInfo, LspExtUpdate, LspHover, LspLocation, LspMarketItem, LspPosition, LspServerStatus, LspSymbol } from './lsp.js'
17
+ import type { LspExtInfo, LspExtUpdate, LspHover, LspLocation, LspMarketItem, LspPosition, LspSemanticTokens, LspServerStatus, LspSymbol } from './lsp.js'
18
18
 
19
19
  /** webServer 精确路由。 */
20
20
  export const RPC_PATH = '/edrv/rpc'
@@ -143,6 +143,8 @@ export interface RpcRequestMap {
143
143
  'edrv.lsp.documentSymbol': { sessionId?: string; path: string }
144
144
  'edrv.lsp.workspaceSymbol': { sessionId?: string; query: string }
145
145
  'edrv.lsp.hover': { sessionId?: string; path: string; position: LspPosition }
146
+ 'edrv.lsp.semanticTokens': { sessionId?: string; path: string }
147
+ 'edrv.lsp.redetect': { languageId: string }
146
148
  'edrv.lsp.configGet': {}
147
149
  'edrv.lsp.configUpdate': { languageId: string; enabled?: boolean; command?: string; path?: string }
148
150
  'edrv.lsp.ext.list': {}
@@ -205,8 +207,10 @@ export interface RpcOkMap {
205
207
  'edrv.lsp.documentSymbol': { symbols: LspSymbol[] }
206
208
  'edrv.lsp.workspaceSymbol': { symbols: LspSymbol[]; truncated?: boolean }
207
209
  'edrv.lsp.hover': { hover?: LspHover }
210
+ 'edrv.lsp.semanticTokens': { tokens?: LspSemanticTokens }
211
+ 'edrv.lsp.redetect': { servers: LspServerStatus[] }
208
212
  'edrv.lsp.configGet': { config: Record<string, unknown> }
209
- 'edrv.lsp.configUpdate': { config: Record<string, unknown> }
213
+ 'edrv.lsp.configUpdate': { config: Record<string, unknown>; servers?: LspServerStatus[] }
210
214
  'edrv.lsp.ext.list': { extensions: LspExtInfo[] }
211
215
  'edrv.lsp.ext.install': { extension: LspExtInfo; updated?: boolean }
212
216
  'edrv.lsp.ext.uninstall': object