dsh-vscode-mode 0.1.45 → 0.1.47

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.
@@ -0,0 +1,92 @@
1
+ /**
2
+ * dsh-vscode-mode host — LSP 环境需求检测注册表。
3
+ * 每个语言注册一个检测器:扫描当前环境,返回未满足的需求清单(含一键安装与官网链接信息)。
4
+ * 新接入 LSP:在 CHECKERS 加一个检测器条目;需要内置安装器时在 INSTALLERS 注册同前缀安装函数。
5
+ * 检测在 status/redetect/configUpdate 时执行(不缓存),保证安装完成后状态即时刷新。
6
+ * 作者 ddj 2026年09月03号
7
+ */
8
+ import { dirname } from 'node:path'
9
+ import { candidateCSharpServers, dotnetWithRuntime, runtimeMajorOf } from './providers.js'
10
+ import { startEnvInstall } from './dotnetProvision.js'
11
+ import type { LspMissingEnv } from '../shared/lsp.js'
12
+
13
+ /** dotnet 运行时需求构造(DotRush / ms-Roslyn 共用)。 */
14
+ function dotnetRuntimeRequirement(major: number, detail: string): LspMissingEnv {
15
+ return {
16
+ id: 'dotnet-runtime:' + major,
17
+ label: '.NET 运行时 ' + major + '.0',
18
+ detail,
19
+ installable: true,
20
+ manualUrl: 'https://dotnet.microsoft.com/download/dotnet/' + major + '.0',
21
+ manualLabel: '官网下载',
22
+ }
23
+ }
24
+
25
+ /** csharp 检测器:DotRush 缺运行时 / ms-Roslyn 缺 dotnet。 */
26
+ function checkCSharp(home: string): LspMissingEnv[] {
27
+ const found = candidateCSharpServers(home)
28
+ const missing: LspMissingEnv[] = []
29
+ if (found.dotrushDll) {
30
+ const major = runtimeMajorOf(dirname(found.dotrushDll))
31
+ if (major !== null && dotnetWithRuntime(major) === null) {
32
+ missing.push(dotnetRuntimeRequirement(major, 'DotRush 语言服务器运行所需'))
33
+ }
34
+ return missing
35
+ }
36
+ if (found.roslynDll && !found.dotnet) {
37
+ // 官方 Roslyn 服务器分支未实测,保守按 LTS 8 基线提示
38
+ missing.push(dotnetRuntimeRequirement(8, 'Roslyn 语言服务器运行所需'))
39
+ }
40
+ return missing
41
+ }
42
+
43
+ /** lua 检测器:EmmyLua/LuaLS 自带运行时,无外部依赖。 */
44
+ function checkLua(): LspMissingEnv[] {
45
+ return []
46
+ }
47
+
48
+ /** 检测器注册表(按 languageId;新 LSP 在此加条目即可)。 */
49
+ const CHECKERS: Record<string, (home: string) => LspMissingEnv[]> = {
50
+ lua: checkLua,
51
+ csharp: checkCSharp,
52
+ }
53
+
54
+ /** 安装器注册表(按需求 id 前缀匹配;新依赖类型在此加条目)。 */
55
+ const INSTALLERS: { prefix: string; run: (id: string) => boolean }[] = [
56
+ {
57
+ prefix: 'dotnet-runtime:',
58
+ run: (id) => {
59
+ const major = Number.parseInt(id.slice('dotnet-runtime:'.length), 10)
60
+ return Number.isInteger(major) ? startEnvInstall(major) : false
61
+ },
62
+ },
63
+ ]
64
+
65
+ /**
66
+ * 检测某语言未满足的环境需求(仅返回缺失项;检测器异常按无需求处理)。
67
+ * @author ddj 2026年09月03号
68
+ * @param languageId 语言 id
69
+ * @param home DSH home(测试可注入临时目录)
70
+ * @returns 缺失需求清单
71
+ */
72
+ export function envRequirementsFor(languageId: string, home: string): LspMissingEnv[] {
73
+ const checker = CHECKERS[languageId]
74
+ if (!checker) return []
75
+ try {
76
+ return checker(home)
77
+ } catch {
78
+ return []
79
+ }
80
+ }
81
+
82
+ /**
83
+ * 尝试一键安装某条需求(存在内置安装器时启动,进行中/已满足返回 false)。
84
+ * @author ddj 2026年09月03号
85
+ * @param id 需求标识(如 dotnet-runtime:10)
86
+ * @returns 是否成功启动安装
87
+ */
88
+ export function installRequirement(id: string): boolean {
89
+ const installer = INSTALLERS.find((it) => id.startsWith(it.prefix))
90
+ if (!installer) return false
91
+ return installer.run(id)
92
+ }
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * dsh-vscode-mode host — 语言服务器 provider 解析。
3
3
  * 优先级:手动配置 > extmgr 扩展源(Phase 2 挂载) > 自动发现(~/.vscode/extensions、
4
- * ~/.dsh/dsh-vscode-mode/extensions、PATH)。产物 LspProviderSpec:{ ready, argv, cwd, reason },
5
- * 供 manager 启动服务器。
4
+ * ~/.dsh/dsh-vscode-mode/extensions、PATH)。C# 自动发现覆盖 ms-dotnettools.csharp
5
+ * (Roslyn/OmniSharp)与 DotRush(自带 DotRush.dll 服务器,需对应 .NET 运行时)。
6
+ * 产物 LspProviderSpec:{ ready, argv, cwd, reason },供 manager 启动服务器。
6
7
  * 作者 ddj 2026-08-27
7
8
  */
8
9
  import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, rmSync, type Dirent } from 'node:fs'
@@ -20,6 +21,9 @@ export interface LspProviderSpec {
20
21
  kind: LspProviderKind
21
22
  argv: string[]
22
23
  cwd?: string
24
+ env?: Record<string, string>
25
+ /** 需自动配置的 .NET 运行时大版本(发现 DotRush 但缺运行时时设置,见 dotnetProvision.ts)。 */
26
+ provisionRuntime?: number
23
27
  ready: boolean
24
28
  reason?: string
25
29
  version?: string
@@ -56,8 +60,16 @@ export function listMatchingDirs(dir: string, prefix: string): string[] {
56
60
  return entries.filter((e) => e.isDirectory() && e.name.startsWith(prefix)).map((e) => join(dir, e.name))
57
61
  }
58
62
 
59
- /** VSCode 扩展目录(~/.vscode/extensions、~/.vscode-server/extensions + extmgr 目录;无则空数组)。 */
63
+ /**
64
+ * VSCode 扩展目录(~/.vscode/extensions、~/.vscode-server/extensions + extmgr 目录;无则空数组)。
65
+ * 测试可用 DSH_LSP_EXT_DIRS(平台路径分隔符分隔)完全覆盖扫描列表,避免受真实机器扩展影响。
66
+ * @author ddj 2026年09月03号
67
+ */
60
68
  export function vscodeExtensionsDirs(home = dshHome()): string[] {
69
+ const override = process.env.DSH_LSP_EXT_DIRS
70
+ if (override) {
71
+ return override.split(sep === '\\' ? ';' : ':').map((dir) => dir.trim()).filter((dir) => dir && existsSync(dir))
72
+ }
61
73
  const userHome = homedir()
62
74
  return [
63
75
  join(userHome, '.vscode', 'extensions'),
@@ -90,7 +102,7 @@ export function registerExtensionProvider(languageId: string, argv: string[], cw
90
102
  export function extensionProviderFor(languageId: string): LspProviderSpec | null {
91
103
  const p = extProviders.get(languageId)
92
104
  if (!p || !p.argv.length) return null
93
- return { languageId, kind: 'extension', argv: p.argv, cwd: p.cwd, ready: true, version: p.version, providerName: p.version ? 'EmmyLua' : '语言服务器扩展' }
105
+ return { languageId, kind: 'extension', argv: p.argv, cwd: p.cwd, ready: true, version: p.version, providerName: '语言服务器扩展' }
94
106
  }
95
107
 
96
108
  /** 清空全部扩展源注册(插件卸载/扩展目录重建时)。 */
@@ -113,18 +125,104 @@ export function findInPath(name: string): string | null {
113
125
  return null
114
126
  }
115
127
 
116
- /** 查找 dotnet(PATH + 常见安装路径)。 */
117
- export function findDotnet(): string | null {
118
- const inPath = findInPath('dotnet')
119
- if (inPath) return inPath
120
- const candidates = [
128
+ /** dotnet 候选(PATH 命中 + 常见安装路径 + 用户级安装位置),仅保留真实存在者。 */
129
+ export function dotnetCandidates(): string[] {
130
+ const localAppData = process.env.LOCALAPPDATA
131
+ const out = [
132
+ findInPath('dotnet'),
121
133
  'C:/Program Files/dotnet/dotnet.exe',
122
134
  'C:/Program Files (x86)/dotnet/dotnet.exe',
135
+ localAppData ? join(localAppData, 'Microsoft', 'dotnet', 'dotnet.exe') : null,
123
136
  ]
124
- for (const c of candidates) if (existsSync(c)) return c
137
+ return out.filter((p): p is string => !!p && existsSync(p))
138
+ }
139
+
140
+ /** 查找 dotnet(PATH + 常见安装路径)。 */
141
+ export function findDotnet(): string | null {
142
+ return dotnetCandidates()[0] ?? null
143
+ }
144
+
145
+ /**
146
+ * 查找具备指定 .NET 运行时大版本的 dotnet(按 dotnetCandidates 顺序探测)。
147
+ * @author ddj 2026年09月03号
148
+ * @param minMajor 运行时最低大版本(如 10)
149
+ * @param candidates dotnet 候选路径(缺省 dotnetCandidates();测试可注入)
150
+ * @returns 满足条件的 dotnet 路径;无满足者返回 null
151
+ */
152
+ export function dotnetWithRuntime(minMajor: number, candidates: string[] = dotnetCandidates()): string | null {
153
+ for (const dotnet of candidates) {
154
+ const sharedDir = join(dirname(dotnet), 'shared', 'Microsoft.NETCore.App')
155
+ let versions: string[] = []
156
+ try {
157
+ versions = readdirSync(sharedDir)
158
+ } catch (error) {
159
+ continue
160
+ }
161
+ const found = versions.some((name) => {
162
+ const major = Number.parseInt(name.split('.')[0] ?? '', 10)
163
+ return Number.isInteger(major) && major >= minMajor
164
+ })
165
+ if (found) return dotnet
166
+ }
167
+ return null
168
+ }
169
+
170
+ /**
171
+ * 探测含 .NET SDK 的 dotnet 根目录(MSBuild 工程求值与 restore 需要,runtime-only 不够)。
172
+ * @author ddj 2026年09月03号
173
+ * @param candidates dotnet 候选路径(缺省 dotnetCandidates();测试可注入)
174
+ * @returns 含 SDK 的 dotnet 根目录;无则 null
175
+ */
176
+ export function findSdkRoot(candidates: string[] = dotnetCandidates()): string | null {
177
+ for (const dotnet of candidates) {
178
+ const root = dirname(dotnet)
179
+ if (sdkVersionOf(root)) return root
180
+ }
125
181
  return null
126
182
  }
127
183
 
184
+ /**
185
+ * dotnet 根目录下最新 SDK 版本号(无 SDK 返回 null)。
186
+ * @author ddj 2026年09月03号
187
+ * @param root dotnet 根目录
188
+ * @returns SDK 版本号
189
+ */
190
+ export function sdkVersionOf(root: string): string | null {
191
+ try {
192
+ const versions = readdirSync(join(root, 'sdk')).filter((name) => /^\d+\.\d+/.test(name))
193
+ if (!versions.length) return null
194
+ return versions.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }))[0] ?? null
195
+ } catch (error) {
196
+ return null
197
+ }
198
+ }
199
+
200
+ /**
201
+ * C# 服务器所需的 MSBuild 环境变量(SDK 解析 + restore 的 muxer 定位)。
202
+ * @author ddj 2026年09月03号
203
+ * @param sdkRoot 含 SDK 的 dotnet 根目录
204
+ * @returns 环境变量表
205
+ */
206
+ export function sdkEnvOf(sdkRoot: string): Record<string, string> {
207
+ const version = sdkVersionOf(sdkRoot)
208
+ const env: Record<string, string> = {
209
+ DOTNET_ROOT: sdkRoot,
210
+ DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR: sdkRoot,
211
+ }
212
+ if (version) env.DOTNET_SDK_PATH = join(sdkRoot, 'sdk', version)
213
+ return env
214
+ }
215
+
216
+ /** 读扩展目录 package.json 的 version(读取失败不影响入口使用,返回 undefined)。 */
217
+ function manifestVersionOf(extDir: string): string | undefined {
218
+ try {
219
+ const manifest = JSON.parse(readFileSync(join(extDir, 'package.json'), 'utf8')) as { version?: unknown }
220
+ return typeof manifest.version === 'string' ? manifest.version : undefined
221
+ } catch (error) {
222
+ return undefined
223
+ }
224
+ }
225
+
128
226
  /** EmmyLua 自动发现候选(tangzx.emmylua-* / server/emmylua_ls)。 */
129
227
  export function candidateEmmyLua(home = dshHome()): { path: string; version?: string } | null {
130
228
  const candidates: { path: string; version?: string }[] = []
@@ -132,12 +230,7 @@ export function candidateEmmyLua(home = dshHome()): { path: string; version?: st
132
230
  for (const extDir of listMatchingDirs(dir, 'tangzx.emmylua-')) {
133
231
  const server = join(extDir, 'server', 'emmylua_ls' + exeSuffix())
134
232
  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 })
233
+ candidates.push({ path: server, version: manifestVersionOf(extDir) })
141
234
  }
142
235
  }
143
236
  candidates.sort((a, b) => (b.version ?? '').localeCompare(a.version ?? '', undefined, { numeric: true }) || a.path.localeCompare(b.path))
@@ -161,9 +254,25 @@ export function candidateLuaServers(home = dshHome()): string[] {
161
254
  return out
162
255
  }
163
256
 
164
- /** C# 自动发现候选:Roslyn dll(需 dotnet)或 OmniSharp 可执行。 */
165
- export function candidateCSharpServers(home = dshHome()): { roslynDll?: string; omnisharpExe?: string; dotnet?: string } {
166
- const out: { roslynDll?: string; omnisharpExe?: string; dotnet?: string } = {}
257
+ /** C# 服务器发现结果(ms-Roslyn / DotRush / OmniSharp + 系统 dotnet)。 */
258
+ export interface CSharpCandidates {
259
+ roslynDll?: string
260
+ dotrushDll?: string
261
+ dotrushVersion?: string
262
+ omnisharpExe?: string
263
+ dotnet?: string
264
+ }
265
+
266
+ /**
267
+ * C# 自动发现候选:ms-dotnettools.csharp 的 Roslyn dll(需 dotnet)/ OmniSharp 可执行,
268
+ * 以及 DotRush 扩展自带服务器(extension/bin/LanguageServer/DotRush.dll,需对应 .NET 运行时)。
269
+ * @author ddj 2026年09月03号
270
+ * @param home DSH home(缺省真实;测试可注入临时目录)
271
+ * @returns 发现结果
272
+ */
273
+ export function candidateCSharpServers(home = dshHome()): CSharpCandidates {
274
+ const out: CSharpCandidates = {}
275
+ const dotrushHits: { dll: string; version?: string }[] = []
167
276
  for (const dir of vscodeExtensionsDirs(home)) {
168
277
  for (const extDir of listMatchingDirs(dir, 'ms-dotnettools.csharp-')) {
169
278
  const roslyn = join(extDir, '.roslyn', 'Microsoft.CodeAnalysis.LanguageServer.dll')
@@ -171,12 +280,57 @@ export function candidateCSharpServers(home = dshHome()): { roslynDll?: string;
171
280
  const omni = join(extDir, '.omnisharp', 'OmniSharp' + exeSuffix())
172
281
  if (!out.omnisharpExe && existsSync(omni)) out.omnisharpExe = omni
173
282
  }
283
+ for (const extDir of listMatchingDirs(dir, 'nromanov.dotrush-')) {
284
+ const dll = join(extDir, 'extension', 'bin', 'LanguageServer', 'DotRush.dll')
285
+ if (!existsSync(dll)) continue
286
+ dotrushHits.push({ dll, version: manifestVersionOf(extDir) })
287
+ }
288
+ }
289
+ // 多版本并存时取清单版本最高者(与 EmmyLua 发现的排序策略一致)
290
+ const best = dotrushHits.sort((a, b) => (b.version ?? '').localeCompare(a.version ?? '', undefined, { numeric: true }) || a.dll.localeCompare(b.dll))[0]
291
+ if (best) {
292
+ out.dotrushDll = best.dll
293
+ out.dotrushVersion = best.version
174
294
  }
175
295
  const dotnet = findDotnet()
176
296
  if (dotnet) out.dotnet = dotnet
177
297
  return out
178
298
  }
179
299
 
300
+ /**
301
+ * 从服务器 dll 同目录的 *.runtimeconfig.json 解析所需 .NET 运行时大版本。
302
+ * @author ddj 2026年09月03号
303
+ * @param dllDir 服务器 dll 所在目录
304
+ * @returns 运行时大版本;无清单或解析失败返回 null
305
+ */
306
+ export function runtimeMajorOf(dllDir: string): number | null {
307
+ try {
308
+ for (const name of readdirSync(dllDir)) {
309
+ if (!name.endsWith('.runtimeconfig.json')) continue
310
+ const config = JSON.parse(readFileSync(join(dllDir, name), 'utf8')) as { runtimeOptions?: { framework?: { version?: unknown } } }
311
+ const version = config.runtimeOptions?.framework?.version
312
+ if (typeof version !== 'string') continue
313
+ const major = Number.parseInt(version.split('.')[0] ?? '', 10)
314
+ if (Number.isInteger(major)) return major
315
+ }
316
+ } catch (error) { /* 清单读取失败按无约束处理 */ }
317
+ return null
318
+ }
319
+
320
+ /**
321
+ * 解析 DotRush 启动参数:优先扩展内置 SDK dotnet,否则找满足其运行时大版本的 dotnet。
322
+ * @author ddj 2026年09月03号
323
+ * @param dll DotRush.dll 绝对路径
324
+ * @returns argv 可直接启动;reason 为不可启动的原因
325
+ */
326
+ function dotrushLaunch(dll: string): { argv: string[] } | { reason: string } {
327
+ const sdkDotnet = join(dirname(dirname(dll)), 'Sdk', 'dotnet' + exeSuffix())
328
+ const major = runtimeMajorOf(dirname(dll))
329
+ const dotnet = existsSync(sdkDotnet) ? sdkDotnet : major !== null ? dotnetWithRuntime(major) : findDotnet()
330
+ if (dotnet) return { argv: [dotnet, dll] }
331
+ return { reason: 'DotRush 需 .NET ' + (major ?? '') + ' 运行时,未找到满足的 dotnet' }
332
+ }
333
+
180
334
  /**
181
335
  * 手动配置归一化:path 指向可执行文件/目录,command 为可执行名(PATH 解析)。
182
336
  * 未配置时返回空 reason(由各 resolve 兜底输出语言相关安装指引文案)。
@@ -221,7 +375,8 @@ export function resolveLuaProvider(manual?: { command?: string; path?: string },
221
375
  }
222
376
 
223
377
  /**
224
- * 解析 C# provider(优先级:手动 > 扩展源 > 自动发现;需 dotnet + Roslyn dll;或 OmniSharp)。
378
+ * 解析 C# provider(优先级:手动 > 扩展源 > 自动发现;Roslyn dll 需 dotnet;
379
+ * DotRush 需满足其运行时的 dotnet;或 OmniSharp)。
225
380
  * @author ddj 2026年08月27号
226
381
  * @param manual 手动配置
227
382
  * @param home DSH home(缺省真实;测试可注入临时目录)
@@ -235,16 +390,34 @@ export function resolveCSharpProvider(manual?: { command?: string; path?: string
235
390
  const ext = extensionProviderFor('csharp')
236
391
  if (ext) return ext
237
392
  const discovered = candidateCSharpServers(h)
393
+ // SDK 环境注入:runtime-only 的 dotnet 会导致 MSBuild 求值/restore 退化
394
+ const sdkRoot = findSdkRoot()
395
+ const msbuildEnv = sdkRoot ? sdkEnvOf(sdkRoot) : undefined
238
396
  if (discovered.roslynDll && discovered.dotnet) {
239
- return { ...base, kind: 'discover', argv: [discovered.dotnet, discovered.roslynDll, '--stdio'], ready: true }
397
+ return { ...base, kind: 'discover', argv: [discovered.dotnet, discovered.roslynDll, '--stdio'], env: msbuildEnv, ready: true }
398
+ }
399
+ let dotrushReason: string | null = null
400
+ let provisionMajor: number | undefined
401
+ if (discovered.dotrushDll) {
402
+ const major = runtimeMajorOf(dirname(discovered.dotrushDll))
403
+ const launch = dotrushLaunch(discovered.dotrushDll)
404
+ if ('argv' in launch) {
405
+ return { ...base, kind: 'discover', argv: launch.argv, env: msbuildEnv, ready: true, version: discovered.dotrushVersion, providerName: 'DotRush' }
406
+ }
407
+ dotrushReason = launch.reason
408
+ provisionMajor = major ?? undefined
240
409
  }
241
410
  if (discovered.omnisharpExe) {
242
411
  return { ...base, kind: 'discover', argv: [discovered.omnisharpExe, '--stdio'], ready: true }
243
412
  }
244
413
  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 入口)' }
414
+ if (dotrushReason) {
415
+ reasons.push(dotrushReason)
416
+ } else {
417
+ if (!discovered.dotnet) reasons.push('未找到 dotnet')
418
+ reasons.push('未发现 ms-dotnettools.csharp / DotRush 扩展')
419
+ }
420
+ return { ...base, kind: 'none', argv: [], ready: false, provisionRuntime: provisionMajor, reason: reasons.join(';') + '(可手动指定 Roslyn/OmniSharp 入口)' }
248
421
  })
249
422
  }
250
423
 
@@ -319,7 +492,7 @@ function fingerprintOf(home: string): string {
319
492
  return '?'
320
493
  }
321
494
  })
322
- return process.platform + '|' + marks.join('|')
495
+ return process.platform + '|' + (findSdkRoot() ?? 'no-sdk') + '|' + marks.join('|')
323
496
  }
324
497
 
325
498
  /**
package/src/lsp/rpc.ts CHANGED
@@ -8,10 +8,13 @@ import { searchRoot } from '../search/ripgrep.js'
8
8
  import { sessionOf, cwdOf } from '../registry.js'
9
9
  import { resolveProviderSpec, langOfPath, configFromPlugin, configFromSettings, LSP_LANGUAGES, LSP_SETTINGS_NS, type LspConfig } from './config.js'
10
10
  import { clearProviderCache } from './providers.js'
11
+ import { onRuntimeProvisioned, envInstallStates } from './dotnetProvision.js'
12
+ import { envRequirementsFor, installRequirement } from './envRequirements.js'
11
13
  import type { LspManager } from './manager.js'
12
14
  import type { RpcHandlerMap } from '../shared/rpc.js'
13
15
  import type { LspLocation, LspServerStatus } from '../shared/lsp.js'
14
16
  import { installFromMarket, installVsixFile, listInstalled, marketGet, marketSearch, uninstall, updateExtension, type ExtInfo } from './extmgr.js'
17
+ import { dshHome } from '../paths.js'
15
18
 
16
19
  export interface LspRpcDeps {
17
20
  ctx: Ctx
@@ -114,6 +117,12 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
114
117
  const { ctx, pluginConfig, manager } = deps
115
118
  const tracker = createDocTracker()
116
119
 
120
+ // 运行时自动配置成功:清 provider 缓存并重置 C# 服务器,下一次文档同步即用新运行时重启
121
+ onRuntimeProvisioned(() => {
122
+ clearProviderCache()
123
+ manager.resetLanguage('csharp')
124
+ })
125
+
117
126
  /** 解析会话与工作区根(失败 → null + error)。 */
118
127
  const rootOf = async (sessionId: string | undefined): Promise<{ root: string; sessionId: string } | { err: string }> => {
119
128
  const session = sessionOf(ctx, sessionId)
@@ -138,9 +147,10 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
138
147
  const rootLocations = (locations: LspLocation[], root: string): LspLocation[] =>
139
148
  locations.map((location) => ({ ...location, root }))
140
149
 
141
- /** 当前 provider 检测结论 → 设置页 idle 状态(不启动 server)。 */
150
+ /** 当前 provider 检测结论 → 设置页 idle 状态(不启动 server);附带未满足的环境需求。 */
142
151
  const detectedStatus = (languageId: string, root?: string): LspServerStatus => {
143
152
  const spec = resolveProviderSpec(ctx, pluginConfig, languageId)
153
+ const missingEnv = envRequirementsFor(languageId, dshHome())
144
154
  return {
145
155
  languageId,
146
156
  source: spec.kind,
@@ -149,6 +159,7 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
149
159
  version: spec.version,
150
160
  providerName: spec.providerName,
151
161
  root,
162
+ missingEnv: missingEnv.length ? missingEnv : undefined,
152
163
  }
153
164
  }
154
165
 
@@ -352,6 +363,18 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
352
363
  }
353
364
  },
354
365
 
366
+ // 环境需求:一键安装(内置安装器)与安装进度查询
367
+ 'edrv.lsp.envInstall': async (args) => {
368
+ if (!installRequirement(args.id)) {
369
+ return { ok: false, started: false, error: '该需求不支持一键安装:' + args.id }
370
+ }
371
+ return { ok: true, started: true }
372
+ },
373
+
374
+ 'edrv.lsp.envState': async () => {
375
+ return { ok: true, states: envInstallStates() }
376
+ },
377
+
355
378
  // --region 扩展管理(edrv.lsp.ext.*:已装/市场/安装/卸载/更新)
356
379
  'edrv.lsp.ext.list': async () => ({ ok: true, extensions: listInstalled() }),
357
380
 
package/src/lsp/server.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  import { spawnServer, type Transport } from './transport.js'
8
8
  import { createLspClient, type LspClient } from './client.js'
9
9
  import { pathToFileUri } from './uri.js'
10
+ import { deriveDefinitionFromLocations } from './derive.js'
10
11
  import type { LspProviderSpec } from './providers.js'
11
12
  import { LSP_SEMANTIC_TOKEN_MODIFIERS, LSP_SEMANTIC_TOKEN_TYPES } from '../shared/lsp.js'
12
13
  import type {
@@ -64,6 +65,7 @@ export function createLspServer(spec: LspProviderSpec, root: string, languageId:
64
65
  let client: LspClient | null = null
65
66
  let capabilities: LspServerCapabilities = {
66
67
  definition: false,
68
+ declaration: false,
67
69
  references: false,
68
70
  documentSymbol: false,
69
71
  workspaceSymbol: false,
@@ -152,7 +154,7 @@ export function createLspServer(spec: LspProviderSpec, root: string, languageId:
152
154
  setPhase('starting')
153
155
  try {
154
156
  transport = spawnServer(
155
- { argv: spec.argv, cwd: spec.cwd ?? root },
157
+ { argv: spec.argv, cwd: spec.cwd ?? root, env: spec.env },
156
158
  (message) => { /* 由 client 接管 */ },
157
159
  (line) => log('stderr: ' + line),
158
160
  )
@@ -185,6 +187,13 @@ export function createLspServer(spec: LspProviderSpec, root: string, languageId:
185
187
  })
186
188
  // 让 client 接管 stdout 消息
187
189
  const caps = await client.initialize(rootUri, [rootUri])
190
+ // DotRush 服务器阻塞等待带 dotrush 段的配置通知才继续初始化(加载工作区),
191
+ // 不发则所有文档类查询返回空;官方 VSCode 扩展经 configurationSection 自动发送。
192
+ // 按 csharp 语言统一发送:手动配置的 DotRush 与其它 C# 服务器(OmniSharp 等)
193
+ // 对不认识的配置段按协议忽略,均安全
194
+ if (languageId === 'csharp') {
195
+ client.notify('workspace/didChangeConfiguration', { settings: { dotrush: { roslyn: {} } } })
196
+ }
188
197
  capabilities = caps
189
198
  return true
190
199
  } catch (error) {
@@ -231,11 +240,55 @@ export function createLspServer(spec: LspProviderSpec, root: string, languageId:
231
240
  async definition(path: string, line: number, character: number): Promise<LspLocation[]> {
232
241
  const doc = docs.get(path)
233
242
  if (!doc || !(await readyWait)) return []
234
- const result = await ensureClient().request<unknown>(
235
- 'textDocument/definition',
236
- { textDocument: { uri: doc.uri }, position: { line, character } },
243
+ const client = ensureClient()
244
+ const position = { line, character }
245
+ // ① 标准定义(方法/跨文件命名空间等)
246
+ const direct = normalizeLocations(
247
+ await client.request<unknown>(
248
+ 'textDocument/definition',
249
+ { textDocument: { uri: doc.uri }, position },
250
+ ),
237
251
  )
238
- return normalizeLocations(result)
252
+ if (direct.length) return direct
253
+ // ② 声明(服务器能力支持时):EmmyLua 对局部变量/参数/表字段 definition 常返回空,declaration 更准
254
+ if (capabilities.declaration) {
255
+ try {
256
+ const decl = normalizeLocations(
257
+ await client.request<unknown>(
258
+ 'textDocument/declaration',
259
+ { textDocument: { uri: doc.uri }, position },
260
+ ),
261
+ )
262
+ if (decl.length) {
263
+ log('definition fallback → declaration (' + path + ':' + line + ':' + character + ')')
264
+ return decl
265
+ }
266
+ } catch (error) {
267
+ log('declaration request failed: ' + String(error))
268
+ }
269
+ }
270
+ // ③ 引用推导:definition/declaration 均空时,从 references(includeDeclaration) 推导声明
271
+ // (EmmyLua 局部/参数把声明排在引用首位;词文本不一致则拒绝,宁可空不误跳)
272
+ try {
273
+ const refs = normalizeLocations(
274
+ await client.request<unknown>(
275
+ 'textDocument/references',
276
+ {
277
+ textDocument: { uri: doc.uri },
278
+ position,
279
+ context: { includeDeclaration: true },
280
+ },
281
+ ),
282
+ )
283
+ const derived = deriveDefinitionFromLocations(position, refs, doc.text, doc.uri)
284
+ if (derived) {
285
+ log('definition fallback → references-derived (' + path + ':' + line + ':' + character + ')')
286
+ return [derived]
287
+ }
288
+ } catch (error) {
289
+ log('references fallback failed: ' + String(error))
290
+ }
291
+ return []
239
292
  },
240
293
 
241
294
  async references(path: string, line: number, character: number, includeDeclaration: boolean): Promise<LspLocation[]> {
package/src/shared/lsp.ts CHANGED
@@ -69,9 +69,66 @@ export interface LspSemanticTokens {
69
69
  resultId?: string
70
70
  }
71
71
 
72
+ /** 语义 token 解码后的绝对范围(LSP 0-based;type 为插件固定 legend 与 type 名)。 */
73
+ export interface LspSemanticTokenRange {
74
+ start: LspPosition
75
+ end: LspPosition
76
+ type: string
77
+ modifiers: number
78
+ }
79
+
80
+ /** 语义 token 中可作为"可导航标识符"的类型(ctrl+hover 下划线判定用)。 */
81
+ export const LSP_NAVIGABLE_TOKEN_TYPES: ReadonlySet<string> = new Set([
82
+ 'variable', 'parameter', 'property', 'function', 'method', 'class', 'enum',
83
+ 'struct', 'interface', 'namespace', 'typeParameter', 'enumMember', 'event', 'type',
84
+ ])
85
+
86
+ /**
87
+ * 语义 token 是否属于可导航标识符。
88
+ * @author ddj 2026年09月02号
89
+ * @param type 语义 token 类型名(插件固定 legend)
90
+ * @returns 是否可导航
91
+ */
92
+ export function isNavigableTokenType(type: string): boolean {
93
+ return LSP_NAVIGABLE_TOKEN_TYPES.has(type)
94
+ }
95
+
96
+ /**
97
+ * 解码 LSP delta 编码的语义 token 流为绝对范围数组。
98
+ * @author ddj 2026年09月02号
99
+ * @param data 5 元组 delta 流(host normalizeSemanticData 产物,类型索引对应固定 legend)
100
+ * @param types 固定 legend 类型表(缺省 LSP_SEMANTIC_TOKEN_TYPES)
101
+ * @returns 绝对范围列表(保持文档顺序)
102
+ */
103
+ export function decodeSemanticTokens(
104
+ data: number[],
105
+ types: readonly string[] = LSP_SEMANTIC_TOKEN_TYPES,
106
+ ): LspSemanticTokenRange[] {
107
+ const out: LspSemanticTokenRange[] = []
108
+ let line = 0
109
+ let character = 0
110
+ for (let i = 0; i + 4 < data.length; i += 5) {
111
+ const deltaLine = data[i] ?? 0
112
+ const deltaCharacter = data[i + 1] ?? 0
113
+ const length = Math.max(0, data[i + 2] ?? 0)
114
+ const typeIndex = data[i + 3] ?? 0
115
+ const modifiers = data[i + 4] ?? 0
116
+ line += deltaLine
117
+ character = deltaLine === 0 ? character + deltaCharacter : deltaCharacter
118
+ out.push({
119
+ start: { line, character },
120
+ end: { line, character: character + length },
121
+ type: types[typeIndex] ?? 'variable',
122
+ modifiers,
123
+ })
124
+ }
125
+ return out
126
+ }
127
+
72
128
  /** LSP 服务器能力子集:本插件会用到的方法。 */
73
129
  export interface LspServerCapabilities {
74
130
  definition: boolean
131
+ declaration: boolean
75
132
  references: boolean
76
133
  documentSymbol: boolean
77
134
  workspaceSymbol: boolean
@@ -90,6 +147,23 @@ export type LspServerPhase =
90
147
  | 'unavailable' // 连续失败,停止自动重启
91
148
  | 'stopped' // 已停止(禁用/卸载/会话结束)
92
149
 
150
+ /** 单条缺失环境需求(edrv.lsp.status.missingEnv 元素,仅含未满足项)。 */
151
+ export interface LspMissingEnv {
152
+ id: string // 需求标识(如 dotnet-runtime:10,同时是安装器键)
153
+ label: string // 显示名(如 '.NET 运行时 10.0')
154
+ detail?: string // 说明(哪个服务器需要)
155
+ installable: boolean // 支持一键安装
156
+ manualUrl?: string // 官网手动下载地址
157
+ manualLabel?: string
158
+ }
159
+
160
+ /** 环境安装进行态(edrv.lsp.envState 载荷元素)。 */
161
+ export interface LspEnvInstallState {
162
+ id: string
163
+ phase: 'idle' | 'downloading' | 'extracting' | 'done' | 'failed'
164
+ message?: string
165
+ }
166
+
93
167
  /** 单语言服务器状态(edrv.lsp.status 载荷元素)。 */
94
168
  export interface LspServerStatus {
95
169
  languageId: string
@@ -101,6 +175,7 @@ export interface LspServerStatus {
101
175
  progressMessage?: string
102
176
  root?: string // 绑定工作区根(相对显示)
103
177
  providerName?: string // provider 名称(如 EmmyLua / LuaLS)
178
+ missingEnv?: LspMissingEnv[] // 未满足的环境需求(无则省略)
104
179
  }
105
180
 
106
181
  /**