dsh-vscode-mode 0.1.36 → 0.1.43
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/lib/client.js +730 -29
- package/lib/client.js.map +1 -1
- package/lib/index.js +699 -88
- package/lib/index.js.map +1 -1
- package/package.json +1 -5
- package/src/client/monaco/loader.ts +12 -1
- package/src/client/monaco/lsp/lspClient.ts +65 -11
- package/src/client/monaco/lsp/providers.ts +34 -1
- package/src/client/monaco/theme.ts +229 -0
- package/src/client/ui/EditorView.ts +21 -2
- package/src/client/ui/LspSettings.ts +39 -7
- package/src/client/ui/McpSettings.ts +5 -1
- package/src/compat.ts +46 -5
- package/src/dshVersion.ts +121 -0
- package/src/fileOpenSettings.ts +143 -24
- package/src/host-deps.d.ts +5 -5
- package/src/index.ts +2 -1
- package/src/lsp/client.ts +29 -2
- package/src/lsp/manager.ts +15 -0
- package/src/lsp/providers.ts +203 -24
- package/src/lsp/rpc.ts +86 -4
- package/src/lsp/server.ts +80 -3
- package/src/lsp/settings.ts +12 -6
- package/src/paths.ts +7 -0
- package/src/rpc.ts +4 -1
- package/src/shared/compat.ts +9 -3
- package/src/shared/lsp.ts +24 -0
- package/src/shared/rpc.ts +6 -2
package/src/lsp/providers.ts
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* 供 manager 启动服务器。
|
|
6
6
|
* 作者 ddj 2026-08-27
|
|
7
7
|
*/
|
|
8
|
-
import { existsSync, readdirSync, type Dirent } from 'node:fs'
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, rmSync, type Dirent } from 'node:fs'
|
|
9
9
|
import { homedir } from 'node:os'
|
|
10
|
-
import { join, sep } from 'node:path'
|
|
10
|
+
import { dirname, join, sep } from 'node:path'
|
|
11
11
|
import { isAbsolute } from 'node:path'
|
|
12
12
|
import { extensionsRoot } from './extmgr.js'
|
|
13
|
-
import { dshHome } from '../paths.js'
|
|
13
|
+
import { dshHome, lspSpecCacheFile } from '../paths.js'
|
|
14
14
|
|
|
15
15
|
/** provider 来源标记(与 shared/lsp.ts 的 status.source 一致)。 */
|
|
16
16
|
export type LspProviderKind = 'extension' | 'discover' | 'manual' | 'none'
|
|
@@ -22,6 +22,8 @@ export interface LspProviderSpec {
|
|
|
22
22
|
cwd?: string
|
|
23
23
|
ready: boolean
|
|
24
24
|
reason?: string
|
|
25
|
+
version?: string
|
|
26
|
+
providerName?: string
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/** 语言 → 扩展名(用于判断哪些文件触发该语言服务器)。 */
|
|
@@ -88,7 +90,7 @@ export function registerExtensionProvider(languageId: string, argv: string[], cw
|
|
|
88
90
|
export function extensionProviderFor(languageId: string): LspProviderSpec | null {
|
|
89
91
|
const p = extProviders.get(languageId)
|
|
90
92
|
if (!p || !p.argv.length) return null
|
|
91
|
-
return { languageId, kind: 'extension', argv: p.argv, cwd: p.cwd, ready: true }
|
|
93
|
+
return { languageId, kind: 'extension', argv: p.argv, cwd: p.cwd, ready: true, version: p.version, providerName: p.version ? 'EmmyLua' : '语言服务器扩展' }
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
/** 清空全部扩展源注册(插件卸载/扩展目录重建时)。 */
|
|
@@ -123,6 +125,25 @@ export function findDotnet(): string | null {
|
|
|
123
125
|
return null
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
/** EmmyLua 自动发现候选(tangzx.emmylua-* / server/emmylua_ls)。 */
|
|
129
|
+
export function candidateEmmyLua(home = dshHome()): { path: string; version?: string } | null {
|
|
130
|
+
const candidates: { path: string; version?: string }[] = []
|
|
131
|
+
for (const dir of vscodeExtensionsDirs(home)) {
|
|
132
|
+
for (const extDir of listMatchingDirs(dir, 'tangzx.emmylua-')) {
|
|
133
|
+
const server = join(extDir, 'server', 'emmylua_ls' + exeSuffix())
|
|
134
|
+
if (!existsSync(server)) continue
|
|
135
|
+
let version: string | undefined
|
|
136
|
+
try {
|
|
137
|
+
const manifest = JSON.parse(readFileSync(join(extDir, 'package.json'), 'utf8')) as { version?: unknown }
|
|
138
|
+
if (typeof manifest.version === 'string') version = manifest.version
|
|
139
|
+
} catch (error) { /* 清单读取失败不影响入口使用 */ }
|
|
140
|
+
candidates.push({ path: server, version })
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
candidates.sort((a, b) => (b.version ?? '').localeCompare(a.version ?? '', undefined, { numeric: true }) || a.path.localeCompare(b.path))
|
|
144
|
+
return candidates[0] ?? null
|
|
145
|
+
}
|
|
146
|
+
|
|
126
147
|
/** LuaLS 自动发现候选(VSCode 扩展目录 + extmgr 目录下 sumneko.lua-* 的 server/bin 可执行)。 */
|
|
127
148
|
export function candidateLuaServers(home = dshHome()): string[] {
|
|
128
149
|
const plat = platformServerDir()
|
|
@@ -186,13 +207,17 @@ export function resolveLuaProvider(manual?: { command?: string; path?: string },
|
|
|
186
207
|
const base = { languageId: 'lua' }
|
|
187
208
|
const manualResolved = manualSpec(manual)
|
|
188
209
|
if (manualResolved.ready) return { ...base, kind: 'manual', argv: manualResolved.argv, cwd: manualResolved.cwd, ready: true }
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
210
|
+
return resolveCached('lua', manual, home, (h) => {
|
|
211
|
+
const ext = extensionProviderFor('lua')
|
|
212
|
+
if (ext) return ext
|
|
213
|
+
const emmy = candidateEmmyLua(h)
|
|
214
|
+
if (emmy) return { ...base, kind: 'extension', argv: [emmy.path], ready: true, version: emmy.version, providerName: 'EmmyLua' }
|
|
215
|
+
const candidates = candidateLuaServers(h)
|
|
216
|
+
if (candidates.length) return { ...base, kind: 'discover', argv: [candidates[0]], cwd: undefined, ready: true }
|
|
217
|
+
const inPath = findInPath('lua-language-server' + exeSuffix())
|
|
218
|
+
if (inPath) return { ...base, kind: 'discover', argv: [inPath], ready: true }
|
|
219
|
+
return { ...base, kind: 'none', argv: [], ready: false, reason: manualResolved.reason ?? '未发现 lua-language-server(可安装 LuaLS 或手动指定路径)' }
|
|
220
|
+
})
|
|
196
221
|
}
|
|
197
222
|
|
|
198
223
|
/**
|
|
@@ -206,19 +231,21 @@ export function resolveCSharpProvider(manual?: { command?: string; path?: string
|
|
|
206
231
|
const base = { languageId: 'csharp' }
|
|
207
232
|
const manualResolved = manualSpec(manual)
|
|
208
233
|
if (manualResolved.ready) return { ...base, kind: 'manual', argv: manualResolved.argv, cwd: manualResolved.cwd, ready: true }
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
234
|
+
return resolveCached('csharp', manual, home, (h) => {
|
|
235
|
+
const ext = extensionProviderFor('csharp')
|
|
236
|
+
if (ext) return ext
|
|
237
|
+
const discovered = candidateCSharpServers(h)
|
|
238
|
+
if (discovered.roslynDll && discovered.dotnet) {
|
|
239
|
+
return { ...base, kind: 'discover', argv: [discovered.dotnet, discovered.roslynDll, '--stdio'], ready: true }
|
|
240
|
+
}
|
|
241
|
+
if (discovered.omnisharpExe) {
|
|
242
|
+
return { ...base, kind: 'discover', argv: [discovered.omnisharpExe, '--stdio'], ready: true }
|
|
243
|
+
}
|
|
244
|
+
const reasons: string[] = []
|
|
245
|
+
if (!discovered.dotnet) reasons.push('未找到 dotnet')
|
|
246
|
+
if (!discovered.roslynDll && !discovered.omnisharpExe) reasons.push('未发现 ms-dotnettools.csharp 扩展')
|
|
247
|
+
return { ...base, kind: 'none', argv: [], ready: false, reason: reasons.join(';') + '(可手动指定 Roslyn/OmniSharp 入口)' }
|
|
248
|
+
})
|
|
222
249
|
}
|
|
223
250
|
|
|
224
251
|
/** 语言 → provider 解析器(可扩展;Phase 2 由 extmgr 注册扩展源 provider)。 */
|
|
@@ -233,3 +260,155 @@ export const PROVIDER_RESOLVERS: Record<string, ProviderResolver> = {
|
|
|
233
260
|
export function looksAbsolute(value: string): boolean {
|
|
234
261
|
return isAbsolute(value)
|
|
235
262
|
}
|
|
263
|
+
|
|
264
|
+
// --region provider 发现结果缓存(进程级 + 磁盘持久化)
|
|
265
|
+
/**
|
|
266
|
+
* 发现结果缓存:只缓存"自动发现/扩展源"结论,不缓存 manual(用户改配置要立即生效)。
|
|
267
|
+
* 进程内 Map 命中优先;未命中读磁盘缓存(跨会话/跨重启共享);仍无才真扫描。
|
|
268
|
+
* 缓存条目按"配置签名 + 扩展目录指纹"校验,任一变化即失效重扫。
|
|
269
|
+
* 作者 ddj 2026年08月28号
|
|
270
|
+
*/
|
|
271
|
+
|
|
272
|
+
/** 缓存条目:spec + 生成时的环境指纹。 */
|
|
273
|
+
interface SpecCacheEntry {
|
|
274
|
+
spec: LspProviderSpec
|
|
275
|
+
fingerprint: string
|
|
276
|
+
at: number
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** 进程级缓存(home|languageId → 条目),同一 DSH home 的不同会话共享。 */
|
|
280
|
+
const specCache = new Map<string, SpecCacheEntry>()
|
|
281
|
+
/** 磁盘缓存是否已载入本进程(home → loaded),不同 profile 不串缓存。 */
|
|
282
|
+
const diskLoaded = new Set<string>()
|
|
283
|
+
/** 自动发现成功结果的复用时长;过期后才检查目录指纹。 */
|
|
284
|
+
const SPEC_CACHE_TTL_MS = 30 * 60 * 1000
|
|
285
|
+
/** 未发现结果的短缓存时长,避免缺失服务器时反复扫描 PATH。 */
|
|
286
|
+
const SPEC_NEGATIVE_CACHE_MS = 60 * 1000
|
|
287
|
+
|
|
288
|
+
/** 生成带 home 的缓存键,避免不同 profile 的 provider 结果串用。 */
|
|
289
|
+
function cacheEntryKey(home: string, languageId: string): string {
|
|
290
|
+
return home + '|' + languageId
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 构造缓存 key:语言 + 手动配置(manual 结果不缓存,故只参与"是否走缓存"判定)。
|
|
295
|
+
* @author ddj 2026年08月28号
|
|
296
|
+
* @param languageId 语言 id
|
|
297
|
+
* @param manual 手动配置
|
|
298
|
+
* @returns 缓存 key(manual 有效时返回空串表示跳过缓存)
|
|
299
|
+
*/
|
|
300
|
+
function cacheKeyOf(languageId: string, manual?: { command?: string; path?: string }): string {
|
|
301
|
+
const pathValue = manual?.path?.trim() ?? ''
|
|
302
|
+
const commandValue = manual?.command?.trim() ?? ''
|
|
303
|
+
if (pathValue || commandValue) return '' // 手动配置:不缓存,立即生效
|
|
304
|
+
return languageId
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 环境指纹:扩展目录清单 + 平台。目录增删/换平台即失效(防缓存陈旧指向已删扩展)。
|
|
309
|
+
* @author ddj 2026年08月28号
|
|
310
|
+
* @param home DSH home
|
|
311
|
+
* @returns 指纹串
|
|
312
|
+
*/
|
|
313
|
+
function fingerprintOf(home: string): string {
|
|
314
|
+
const dirs = vscodeExtensionsDirs(home)
|
|
315
|
+
const marks = dirs.map((dir) => {
|
|
316
|
+
try {
|
|
317
|
+
return readdirSync(dir).sort().join(',')
|
|
318
|
+
} catch (error) {
|
|
319
|
+
return '?'
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
return process.platform + '|' + marks.join('|')
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 读取磁盘缓存(幂等:每进程只载一次;schema 文件名已隔离版本)。
|
|
327
|
+
* @author ddj 2026年08月28号
|
|
328
|
+
* @param home DSH home
|
|
329
|
+
*/
|
|
330
|
+
function loadDiskCache(home: string): void {
|
|
331
|
+
if (diskLoaded.has(home)) return
|
|
332
|
+
diskLoaded.add(home)
|
|
333
|
+
const file = lspSpecCacheFile(home)
|
|
334
|
+
try {
|
|
335
|
+
const raw = JSON.parse(readFileSync(file, 'utf8')) as Record<string, SpecCacheEntry>
|
|
336
|
+
for (const [languageId, entry] of Object.entries(raw)) {
|
|
337
|
+
if (entry && typeof entry === 'object' && entry.spec && typeof entry.fingerprint === 'string') {
|
|
338
|
+
specCache.set(cacheEntryKey(home, languageId), entry)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
} catch (error) { /* 首次运行/损坏:静默降级为真扫描 */ }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* 写回磁盘缓存(best-effort;写失败不影响本次解析结果)。
|
|
346
|
+
* @author ddj 2026年08月28号
|
|
347
|
+
* @param home DSH home
|
|
348
|
+
*/
|
|
349
|
+
function saveDiskCache(home: string): void {
|
|
350
|
+
const file = lspSpecCacheFile(home)
|
|
351
|
+
try {
|
|
352
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
353
|
+
const out: Record<string, SpecCacheEntry> = {}
|
|
354
|
+
const prefix = home + '|'
|
|
355
|
+
for (const [key, entry] of specCache) if (key.startsWith(prefix)) out[key.slice(prefix.length)] = entry
|
|
356
|
+
writeFileSync(file, JSON.stringify(out), 'utf8')
|
|
357
|
+
} catch (error) { /* 写入失败静默 */ }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* 带缓存执行 provider 解析(自动发现路径专用)。
|
|
362
|
+
* @author ddj 2026年08月28号
|
|
363
|
+
* @param languageId 语言 id
|
|
364
|
+
* @param manual 手动配置
|
|
365
|
+
* @param home DSH home
|
|
366
|
+
* @param resolve 真解析函数(未命中时调用)
|
|
367
|
+
* @returns provider 规格
|
|
368
|
+
*/
|
|
369
|
+
function resolveCached(
|
|
370
|
+
languageId: string,
|
|
371
|
+
manual: { command?: string; path?: string } | undefined,
|
|
372
|
+
home: string,
|
|
373
|
+
resolve: (h: string) => LspProviderSpec,
|
|
374
|
+
): LspProviderSpec {
|
|
375
|
+
const key = cacheKeyOf(languageId, manual)
|
|
376
|
+
if (!key) return resolve(home) // 手动配置:不走缓存
|
|
377
|
+
loadDiskCache(home)
|
|
378
|
+
const fullKey = cacheEntryKey(home, key)
|
|
379
|
+
const hit = specCache.get(fullKey)
|
|
380
|
+
if (hit && isCacheFresh(hit)) return hit.spec
|
|
381
|
+
const fingerprint = fingerprintOf(home)
|
|
382
|
+
if (hit && hit.fingerprint === fingerprint) {
|
|
383
|
+
hit.at = Date.now()
|
|
384
|
+
return hit.spec
|
|
385
|
+
}
|
|
386
|
+
const spec = resolve(home)
|
|
387
|
+
specCache.set(fullKey, { spec, fingerprint, at: Date.now() })
|
|
388
|
+
saveDiskCache(home)
|
|
389
|
+
return spec
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** 判断缓存条目是否仍可直接复用,不触发扩展目录扫描。 */
|
|
393
|
+
function isCacheFresh(entry: SpecCacheEntry): boolean {
|
|
394
|
+
const age = Date.now() - entry.at
|
|
395
|
+
const ttl = entry.spec.kind === 'none' ? SPEC_NEGATIVE_CACHE_MS : SPEC_CACHE_TTL_MS
|
|
396
|
+
if (age < 0 || age >= ttl) return false
|
|
397
|
+
const executable = entry.spec.argv[0]
|
|
398
|
+
return !entry.spec.ready || !executable || existsSync(executable)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* 清空 provider 发现缓存(扩展安装/卸载、配置变更后调用;进程级 + 删磁盘文件)。
|
|
403
|
+
* @author ddj 2026年08月28号
|
|
404
|
+
* @param home DSH home(缺省真实)
|
|
405
|
+
*/
|
|
406
|
+
export function clearProviderCache(home = dshHome()): void {
|
|
407
|
+
const prefix = home + '|'
|
|
408
|
+
for (const key of specCache.keys()) if (key.startsWith(prefix)) specCache.delete(key)
|
|
409
|
+
diskLoaded.delete(home)
|
|
410
|
+
try {
|
|
411
|
+
rmSync(lspSpecCacheFile(home), { force: true })
|
|
412
|
+
} catch (error) { /* 删除失败静默 */ }
|
|
413
|
+
}
|
|
414
|
+
// --endregion
|
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
|
-
|
|
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 = {
|
|
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 = {
|
|
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/lsp/settings.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-vscode-mode host — LSP 配置设置 section
|
|
2
|
+
* dsh-vscode-mode host — LSP 配置设置 section 安装(版本自适应;缺失/失败不抛错)。
|
|
3
3
|
* ns:dsh-vscode-mode.languageServers;每语言 { enabled, command, path }。
|
|
4
|
-
*
|
|
4
|
+
* 安装策略分派见 fileOpenSettings.runSettingsInstall(rc 线 legacy / alpha 线服务方法)。
|
|
5
|
+
* 作者 ddj 2026-08-27 / 2026-09-02
|
|
5
6
|
*/
|
|
6
7
|
import type { Ctx } from '../store.js'
|
|
7
8
|
import { LSP_LANGUAGES, LSP_SETTINGS_NS, type LspConfig } from './config.js'
|
|
8
|
-
import { loadSettingsDeps, type SettingsDeps } from '../fileOpenSettings.js'
|
|
9
|
+
import { loadSettingsDeps, runSettingsInstall, type SettingsDeps } from '../fileOpenSettings.js'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* 安装 LSP 设置 section。
|
|
@@ -15,7 +16,12 @@ import { loadSettingsDeps, type SettingsDeps } from '../fileOpenSettings.js'
|
|
|
15
16
|
* @returns 是否成功安装
|
|
16
17
|
*/
|
|
17
18
|
export async function installLspSettingsSection(ctx: Ctx, entry: LspConfig): Promise<boolean> {
|
|
18
|
-
|
|
19
|
+
let deps: SettingsDeps | null = null
|
|
20
|
+
try {
|
|
21
|
+
deps = await loadSettingsDeps()
|
|
22
|
+
} catch {
|
|
23
|
+
/* 装载失败按缺失处理 */
|
|
24
|
+
}
|
|
19
25
|
if (!deps) return false
|
|
20
26
|
try {
|
|
21
27
|
// schemastery 无 .optional(),对象字段默认即可选;.default 必给避免 undefined 覆盖
|
|
@@ -29,11 +35,11 @@ export async function installLspSettingsSection(ctx: Ctx, entry: LspConfig): Pro
|
|
|
29
35
|
}).default({})
|
|
30
36
|
}
|
|
31
37
|
const schema = z.object(langShape).default({})
|
|
32
|
-
|
|
38
|
+
const strategy = await runSettingsInstall(ctx, LSP_SETTINGS_NS, schema, entry, {
|
|
33
39
|
setSource: () => {},
|
|
34
40
|
onChange: () => {},
|
|
35
41
|
})
|
|
36
|
-
return
|
|
42
|
+
return strategy === 'legacy' || strategy === 'service'
|
|
37
43
|
} catch (error) {
|
|
38
44
|
// 设置 section 安装失败不致命:降级为无设置 UI,LSP 核心功能不受影响
|
|
39
45
|
console.error('[dsh-vscode-mode] LSP 设置 section 安装失败:' + String(error))
|
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')
|
|
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/compat.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode 兼容层双面契约 — 与其他插件 / DSH 版本的探测与自诊断报告。
|
|
3
3
|
* Purity rule: no node/react imports (same as types.ts / rpc.ts)。
|
|
4
|
-
* 作者 ddj 2026-08-24
|
|
4
|
+
* 作者 ddj 2026-08-24 / 2026-09-02
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/** 单项适配状态:外部插件/服务、护栏或版本适配项,active 表示健康/生效。 */
|
|
8
8
|
export interface CompatAdapter {
|
|
9
9
|
name: string
|
|
10
10
|
active: boolean
|
|
@@ -17,11 +17,17 @@ export interface DevFormInfo {
|
|
|
17
17
|
path?: string
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* 兼容性报告:host 侧构建(RPC edrv.compat 返回),client 侧补充本地适配项后展示。
|
|
22
|
+
* dshVersion = 运行中 DSH 核心版本(未探测到为空串);adapters = 版本适配机制状态行
|
|
23
|
+
* (旧客户端忽略未知字段,安全前向兼容)。
|
|
24
|
+
*/
|
|
21
25
|
export interface CompatReport {
|
|
22
26
|
pluginVersion: string
|
|
23
27
|
external: CompatAdapter[]
|
|
24
28
|
guards: CompatAdapter[]
|
|
25
29
|
warnings: string[]
|
|
26
30
|
devForm?: DevFormInfo
|
|
31
|
+
dshVersion?: string
|
|
32
|
+
adapters?: CompatAdapter[]
|
|
27
33
|
}
|