dsh-vscode-mode 0.1.46 → 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.
@@ -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
@@ -154,7 +154,7 @@ export function createLspServer(spec: LspProviderSpec, root: string, languageId:
154
154
  setPhase('starting')
155
155
  try {
156
156
  transport = spawnServer(
157
- { argv: spec.argv, cwd: spec.cwd ?? root },
157
+ { argv: spec.argv, cwd: spec.cwd ?? root, env: spec.env },
158
158
  (message) => { /* 由 client 接管 */ },
159
159
  (line) => log('stderr: ' + line),
160
160
  )
@@ -187,6 +187,13 @@ export function createLspServer(spec: LspProviderSpec, root: string, languageId:
187
187
  })
188
188
  // 让 client 接管 stdout 消息
189
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
+ }
190
197
  capabilities = caps
191
198
  return true
192
199
  } catch (error) {
package/src/shared/lsp.ts CHANGED
@@ -147,6 +147,23 @@ export type LspServerPhase =
147
147
  | 'unavailable' // 连续失败,停止自动重启
148
148
  | 'stopped' // 已停止(禁用/卸载/会话结束)
149
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
+
150
167
  /** 单语言服务器状态(edrv.lsp.status 载荷元素)。 */
151
168
  export interface LspServerStatus {
152
169
  languageId: string
@@ -158,6 +175,7 @@ export interface LspServerStatus {
158
175
  progressMessage?: string
159
176
  root?: string // 绑定工作区根(相对显示)
160
177
  providerName?: string // provider 名称(如 EmmyLua / LuaLS)
178
+ missingEnv?: LspMissingEnv[] // 未满足的环境需求(无则省略)
161
179
  }
162
180
 
163
181
  /**
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, LspSemanticTokens, LspServerStatus, LspSymbol } from './lsp.js'
17
+ import type { LspEnvInstallState, 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'
@@ -145,6 +145,8 @@ export interface RpcRequestMap {
145
145
  'edrv.lsp.hover': { sessionId?: string; path: string; position: LspPosition }
146
146
  'edrv.lsp.semanticTokens': { sessionId?: string; path: string }
147
147
  'edrv.lsp.redetect': { languageId: string }
148
+ 'edrv.lsp.envInstall': { languageId: string; id: string }
149
+ 'edrv.lsp.envState': {}
148
150
  'edrv.lsp.configGet': {}
149
151
  'edrv.lsp.configUpdate': { languageId: string; enabled?: boolean; command?: string; path?: string }
150
152
  'edrv.lsp.ext.list': {}
@@ -209,6 +211,8 @@ export interface RpcOkMap {
209
211
  'edrv.lsp.hover': { hover?: LspHover }
210
212
  'edrv.lsp.semanticTokens': { tokens?: LspSemanticTokens }
211
213
  'edrv.lsp.redetect': { servers: LspServerStatus[] }
214
+ 'edrv.lsp.envInstall': { started: boolean }
215
+ 'edrv.lsp.envState': { states: LspEnvInstallState[] }
212
216
  'edrv.lsp.configGet': { config: Record<string, unknown> }
213
217
  'edrv.lsp.configUpdate': { config: Record<string, unknown>; servers?: LspServerStatus[] }
214
218
  'edrv.lsp.ext.list': { extensions: LspExtInfo[] }