dsh-vscode-mode 0.1.46 → 0.1.48

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/index.js CHANGED
@@ -3,7 +3,7 @@ import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/p
3
3
  import { basename, dirname, extname, join, normalize, resolve, sep } from "node:path";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
- import { homedir } from "node:os";
6
+ import { homedir, tmpdir } from "node:os";
7
7
  import { createHash } from "node:crypto";
8
8
  import { spawn } from "node:child_process";
9
9
  import { inflateRawSync } from "node:zlib";
@@ -6166,7 +6166,8 @@ function createLspServer(spec, root, languageId, logger) {
6166
6166
  try {
6167
6167
  transport = spawnServer({
6168
6168
  argv: spec.argv,
6169
- cwd: spec.cwd ?? root
6169
+ cwd: spec.cwd ?? root,
6170
+ env: spec.env
6170
6171
  }, (message) => {}, (line) => log("stderr: " + line));
6171
6172
  client = createLspClient(transport, {
6172
6173
  onReady: (caps) => {
@@ -6194,7 +6195,9 @@ function createLspServer(spec, root, languageId, logger) {
6194
6195
  },
6195
6196
  onLog: (line) => log("lsp: " + line)
6196
6197
  });
6197
- capabilities = await client.initialize(rootUri, [rootUri]);
6198
+ const caps = await client.initialize(rootUri, [rootUri]);
6199
+ if (languageId === "csharp") client.notify("workspace/didChangeConfiguration", { settings: { dotrush: { roslyn: {} } } });
6200
+ capabilities = caps;
6198
6201
  return true;
6199
6202
  } catch (error) {
6200
6203
  log("start failed: " + String(error));
@@ -6800,8 +6803,9 @@ async function updateExtension(id) {
6800
6803
  /**
6801
6804
  * dsh-vscode-mode host — 语言服务器 provider 解析。
6802
6805
  * 优先级:手动配置 > extmgr 扩展源(Phase 2 挂载) > 自动发现(~/.vscode/extensions、
6803
- * ~/.dsh/dsh-vscode-mode/extensions、PATH)。产物 LspProviderSpec:{ ready, argv, cwd, reason },
6804
- * 供 manager 启动服务器。
6806
+ * ~/.dsh/dsh-vscode-mode/extensions、PATH)。C# 自动发现覆盖 ms-dotnettools.csharp
6807
+ * (Roslyn/OmniSharp)与 DotRush(自带 DotRush.dll 服务器,需对应 .NET 运行时)。
6808
+ * 产物 LspProviderSpec:{ ready, argv, cwd, reason },供 manager 启动服务器。
6805
6809
  * 作者 ddj 2026-08-27
6806
6810
  */
6807
6811
  /** 平台目录名(LuaLS/OmniSharp 的 server/bin/<平台>)。 */
@@ -6825,8 +6829,14 @@ function listMatchingDirs(dir, prefix) {
6825
6829
  }
6826
6830
  return entries.filter((e) => e.isDirectory() && e.name.startsWith(prefix)).map((e) => join(dir, e.name));
6827
6831
  }
6828
- /** VSCode 扩展目录(~/.vscode/extensions、~/.vscode-server/extensions + extmgr 目录;无则空数组)。 */
6832
+ /**
6833
+ * VSCode 扩展目录(~/.vscode/extensions、~/.vscode-server/extensions + extmgr 目录;无则空数组)。
6834
+ * 测试可用 DSH_LSP_EXT_DIRS(平台路径分隔符分隔)完全覆盖扫描列表,避免受真实机器扩展影响。
6835
+ * @author ddj 2026年09月03号
6836
+ */
6829
6837
  function vscodeExtensionsDirs(home = dshHome()) {
6838
+ const override = process.env.DSH_LSP_EXT_DIRS;
6839
+ if (override) return override.split(sep === "\\" ? ";" : ":").map((dir) => dir.trim()).filter((dir) => dir && existsSync(dir));
6830
6840
  const userHome = homedir();
6831
6841
  return [
6832
6842
  join(userHome, ".vscode", "extensions"),
@@ -6847,7 +6857,7 @@ function extensionProviderFor(languageId) {
6847
6857
  cwd: p.cwd,
6848
6858
  ready: true,
6849
6859
  version: p.version,
6850
- providerName: p.version ? "EmmyLua" : "语言服务器扩展"
6860
+ providerName: "语言服务器扩展"
6851
6861
  };
6852
6862
  }
6853
6863
  /** PATH 中查找可执行文件(不含扩展名校验,交由 spawn 报错)。 */
@@ -6868,27 +6878,104 @@ function findInPath(name) {
6868
6878
  }
6869
6879
  return null;
6870
6880
  }
6881
+ /** dotnet 候选(PATH 命中 + 常见安装路径 + 用户级安装位置),仅保留真实存在者。 */
6882
+ function dotnetCandidates() {
6883
+ const localAppData = process.env.LOCALAPPDATA;
6884
+ return [
6885
+ findInPath("dotnet"),
6886
+ "C:/Program Files/dotnet/dotnet.exe",
6887
+ "C:/Program Files (x86)/dotnet/dotnet.exe",
6888
+ localAppData ? join(localAppData, "Microsoft", "dotnet", "dotnet.exe") : null
6889
+ ].filter((p) => !!p && existsSync(p));
6890
+ }
6871
6891
  /** 查找 dotnet(PATH + 常见安装路径)。 */
6872
6892
  function findDotnet() {
6873
- const inPath = findInPath("dotnet");
6874
- if (inPath) return inPath;
6875
- for (const c of ["C:/Program Files/dotnet/dotnet.exe", "C:/Program Files (x86)/dotnet/dotnet.exe"]) if (existsSync(c)) return c;
6893
+ return dotnetCandidates()[0] ?? null;
6894
+ }
6895
+ /**
6896
+ * 查找具备指定 .NET 运行时大版本的 dotnet(按 dotnetCandidates 顺序探测)。
6897
+ * @author ddj 2026年09月03号
6898
+ * @param minMajor 运行时最低大版本(如 10)
6899
+ * @param candidates dotnet 候选路径(缺省 dotnetCandidates();测试可注入)
6900
+ * @returns 满足条件的 dotnet 路径;无满足者返回 null
6901
+ */
6902
+ function dotnetWithRuntime(minMajor, candidates = dotnetCandidates()) {
6903
+ for (const dotnet of candidates) {
6904
+ const sharedDir = join(dirname(dotnet), "shared", "Microsoft.NETCore.App");
6905
+ let versions = [];
6906
+ try {
6907
+ versions = readdirSync(sharedDir);
6908
+ } catch (error) {
6909
+ continue;
6910
+ }
6911
+ if (versions.some((name) => {
6912
+ const major = Number.parseInt(name.split(".")[0] ?? "", 10);
6913
+ return Number.isInteger(major) && major >= minMajor;
6914
+ })) return dotnet;
6915
+ }
6876
6916
  return null;
6877
6917
  }
6918
+ /**
6919
+ * 探测含 .NET SDK 的 dotnet 根目录(MSBuild 工程求值与 restore 需要,runtime-only 不够)。
6920
+ * @author ddj 2026年09月03号
6921
+ * @param candidates dotnet 候选路径(缺省 dotnetCandidates();测试可注入)
6922
+ * @returns 含 SDK 的 dotnet 根目录;无则 null
6923
+ */
6924
+ function findSdkRoot(candidates = dotnetCandidates()) {
6925
+ for (const dotnet of candidates) {
6926
+ const root = dirname(dotnet);
6927
+ if (sdkVersionOf(root)) return root;
6928
+ }
6929
+ return null;
6930
+ }
6931
+ /**
6932
+ * dotnet 根目录下最新 SDK 版本号(无 SDK 返回 null)。
6933
+ * @author ddj 2026年09月03号
6934
+ * @param root dotnet 根目录
6935
+ * @returns SDK 版本号
6936
+ */
6937
+ function sdkVersionOf(root) {
6938
+ try {
6939
+ const versions = readdirSync(join(root, "sdk")).filter((name) => /^\d+\.\d+/.test(name));
6940
+ if (!versions.length) return null;
6941
+ return versions.sort((a, b) => b.localeCompare(a, void 0, { numeric: true }))[0] ?? null;
6942
+ } catch (error) {
6943
+ return null;
6944
+ }
6945
+ }
6946
+ /**
6947
+ * C# 服务器所需的 MSBuild 环境变量(SDK 解析 + restore 的 muxer 定位)。
6948
+ * @author ddj 2026年09月03号
6949
+ * @param sdkRoot 含 SDK 的 dotnet 根目录
6950
+ * @returns 环境变量表
6951
+ */
6952
+ function sdkEnvOf(sdkRoot) {
6953
+ const version = sdkVersionOf(sdkRoot);
6954
+ const env = {
6955
+ DOTNET_ROOT: sdkRoot,
6956
+ DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR: sdkRoot
6957
+ };
6958
+ if (version) env.DOTNET_SDK_PATH = join(sdkRoot, "sdk", version);
6959
+ return env;
6960
+ }
6961
+ /** 读扩展目录 package.json 的 version(读取失败不影响入口使用,返回 undefined)。 */
6962
+ function manifestVersionOf(extDir) {
6963
+ try {
6964
+ const manifest = JSON.parse(readFileSync(join(extDir, "package.json"), "utf8"));
6965
+ return typeof manifest.version === "string" ? manifest.version : void 0;
6966
+ } catch (error) {
6967
+ return;
6968
+ }
6969
+ }
6878
6970
  /** EmmyLua 自动发现候选(tangzx.emmylua-* / server/emmylua_ls)。 */
6879
6971
  function candidateEmmyLua(home = dshHome()) {
6880
6972
  const candidates = [];
6881
6973
  for (const dir of vscodeExtensionsDirs(home)) for (const extDir of listMatchingDirs(dir, "tangzx.emmylua-")) {
6882
6974
  const server = join(extDir, "server", "emmylua_ls" + exeSuffix());
6883
6975
  if (!existsSync(server)) continue;
6884
- let version;
6885
- try {
6886
- const manifest = JSON.parse(readFileSync(join(extDir, "package.json"), "utf8"));
6887
- if (typeof manifest.version === "string") version = manifest.version;
6888
- } catch (error) {}
6889
6976
  candidates.push({
6890
6977
  path: server,
6891
- version
6978
+ version: manifestVersionOf(extDir)
6892
6979
  });
6893
6980
  }
6894
6981
  candidates.sort((a, b) => (b.version ?? "").localeCompare(a.version ?? "", void 0, { numeric: true }) || a.path.localeCompare(b.path));
@@ -6907,20 +6994,73 @@ function candidateLuaServers(home = dshHome()) {
6907
6994
  }
6908
6995
  return out;
6909
6996
  }
6910
- /** C# 自动发现候选:Roslyn dll(需 dotnet)或 OmniSharp 可执行。 */
6997
+ /**
6998
+ * C# 自动发现候选:ms-dotnettools.csharp 的 Roslyn dll(需 dotnet)/ OmniSharp 可执行,
6999
+ * 以及 DotRush 扩展自带服务器(extension/bin/LanguageServer/DotRush.dll,需对应 .NET 运行时)。
7000
+ * @author ddj 2026年09月03号
7001
+ * @param home DSH home(缺省真实;测试可注入临时目录)
7002
+ * @returns 发现结果
7003
+ */
6911
7004
  function candidateCSharpServers(home = dshHome()) {
6912
7005
  const out = {};
6913
- for (const dir of vscodeExtensionsDirs(home)) for (const extDir of listMatchingDirs(dir, "ms-dotnettools.csharp-")) {
6914
- const roslyn = join(extDir, ".roslyn", "Microsoft.CodeAnalysis.LanguageServer.dll");
6915
- if (!out.roslynDll && existsSync(roslyn)) out.roslynDll = roslyn;
6916
- const omni = join(extDir, ".omnisharp", "OmniSharp" + exeSuffix());
6917
- if (!out.omnisharpExe && existsSync(omni)) out.omnisharpExe = omni;
7006
+ const dotrushHits = [];
7007
+ for (const dir of vscodeExtensionsDirs(home)) {
7008
+ for (const extDir of listMatchingDirs(dir, "ms-dotnettools.csharp-")) {
7009
+ const roslyn = join(extDir, ".roslyn", "Microsoft.CodeAnalysis.LanguageServer.dll");
7010
+ if (!out.roslynDll && existsSync(roslyn)) out.roslynDll = roslyn;
7011
+ const omni = join(extDir, ".omnisharp", "OmniSharp" + exeSuffix());
7012
+ if (!out.omnisharpExe && existsSync(omni)) out.omnisharpExe = omni;
7013
+ }
7014
+ for (const extDir of listMatchingDirs(dir, "nromanov.dotrush-")) {
7015
+ const dll = join(extDir, "extension", "bin", "LanguageServer", "DotRush.dll");
7016
+ if (!existsSync(dll)) continue;
7017
+ dotrushHits.push({
7018
+ dll,
7019
+ version: manifestVersionOf(extDir)
7020
+ });
7021
+ }
7022
+ }
7023
+ const best = dotrushHits.sort((a, b) => (b.version ?? "").localeCompare(a.version ?? "", void 0, { numeric: true }) || a.dll.localeCompare(b.dll))[0];
7024
+ if (best) {
7025
+ out.dotrushDll = best.dll;
7026
+ out.dotrushVersion = best.version;
6918
7027
  }
6919
7028
  const dotnet = findDotnet();
6920
7029
  if (dotnet) out.dotnet = dotnet;
6921
7030
  return out;
6922
7031
  }
6923
7032
  /**
7033
+ * 从服务器 dll 同目录的 *.runtimeconfig.json 解析所需 .NET 运行时大版本。
7034
+ * @author ddj 2026年09月03号
7035
+ * @param dllDir 服务器 dll 所在目录
7036
+ * @returns 运行时大版本;无清单或解析失败返回 null
7037
+ */
7038
+ function runtimeMajorOf(dllDir) {
7039
+ try {
7040
+ for (const name of readdirSync(dllDir)) {
7041
+ if (!name.endsWith(".runtimeconfig.json")) continue;
7042
+ const version = JSON.parse(readFileSync(join(dllDir, name), "utf8")).runtimeOptions?.framework?.version;
7043
+ if (typeof version !== "string") continue;
7044
+ const major = Number.parseInt(version.split(".")[0] ?? "", 10);
7045
+ if (Number.isInteger(major)) return major;
7046
+ }
7047
+ } catch (error) {}
7048
+ return null;
7049
+ }
7050
+ /**
7051
+ * 解析 DotRush 启动参数:优先扩展内置 SDK dotnet,否则找满足其运行时大版本的 dotnet。
7052
+ * @author ddj 2026年09月03号
7053
+ * @param dll DotRush.dll 绝对路径
7054
+ * @returns argv 可直接启动;reason 为不可启动的原因
7055
+ */
7056
+ function dotrushLaunch(dll) {
7057
+ const sdkDotnet = join(dirname(dirname(dll)), "Sdk", "dotnet" + exeSuffix());
7058
+ const major = runtimeMajorOf(dirname(dll));
7059
+ const dotnet = existsSync(sdkDotnet) ? sdkDotnet : major !== null ? dotnetWithRuntime(major) : findDotnet();
7060
+ if (dotnet) return { argv: [dotnet, dll] };
7061
+ return { reason: "DotRush 需 .NET " + (major ?? "") + " 运行时,未找到满足的 dotnet" };
7062
+ }
7063
+ /**
6924
7064
  * 手动配置归一化:path 指向可执行文件/目录,command 为可执行名(PATH 解析)。
6925
7065
  * 未配置时返回空 reason(由各 resolve 兜底输出语言相关安装指引文案)。
6926
7066
  * @author ddj 2026年08月27号
@@ -7012,7 +7152,8 @@ function resolveLuaProvider(manual, home = dshHome()) {
7012
7152
  });
7013
7153
  }
7014
7154
  /**
7015
- * 解析 C# provider(优先级:手动 > 扩展源 > 自动发现;需 dotnet + Roslyn dll;或 OmniSharp)。
7155
+ * 解析 C# provider(优先级:手动 > 扩展源 > 自动发现;Roslyn dll 需 dotnet;
7156
+ * DotRush 需满足其运行时的 dotnet;或 OmniSharp)。
7016
7157
  * @author ddj 2026年08月27号
7017
7158
  * @param manual 手动配置
7018
7159
  * @param home DSH home(缺省真实;测试可注入临时目录)
@@ -7032,6 +7173,8 @@ function resolveCSharpProvider(manual, home = dshHome()) {
7032
7173
  const ext = extensionProviderFor("csharp");
7033
7174
  if (ext) return ext;
7034
7175
  const discovered = candidateCSharpServers(h);
7176
+ const sdkRoot = findSdkRoot();
7177
+ const msbuildEnv = sdkRoot ? sdkEnvOf(sdkRoot) : void 0;
7035
7178
  if (discovered.roslynDll && discovered.dotnet) return {
7036
7179
  ...base,
7037
7180
  kind: "discover",
@@ -7040,8 +7183,26 @@ function resolveCSharpProvider(manual, home = dshHome()) {
7040
7183
  discovered.roslynDll,
7041
7184
  "--stdio"
7042
7185
  ],
7186
+ env: msbuildEnv,
7043
7187
  ready: true
7044
7188
  };
7189
+ let dotrushReason = null;
7190
+ let provisionMajor;
7191
+ if (discovered.dotrushDll) {
7192
+ const major = runtimeMajorOf(dirname(discovered.dotrushDll));
7193
+ const launch = dotrushLaunch(discovered.dotrushDll);
7194
+ if ("argv" in launch) return {
7195
+ ...base,
7196
+ kind: "discover",
7197
+ argv: launch.argv,
7198
+ env: msbuildEnv,
7199
+ ready: true,
7200
+ version: discovered.dotrushVersion,
7201
+ providerName: "DotRush"
7202
+ };
7203
+ dotrushReason = launch.reason;
7204
+ provisionMajor = major ?? void 0;
7205
+ }
7045
7206
  if (discovered.omnisharpExe) return {
7046
7207
  ...base,
7047
7208
  kind: "discover",
@@ -7049,13 +7210,17 @@ function resolveCSharpProvider(manual, home = dshHome()) {
7049
7210
  ready: true
7050
7211
  };
7051
7212
  const reasons = [];
7052
- if (!discovered.dotnet) reasons.push("未找到 dotnet");
7053
- if (!discovered.roslynDll && !discovered.omnisharpExe) reasons.push("未发现 ms-dotnettools.csharp 扩展");
7213
+ if (dotrushReason) reasons.push(dotrushReason);
7214
+ else {
7215
+ if (!discovered.dotnet) reasons.push("未找到 dotnet");
7216
+ reasons.push("未发现 ms-dotnettools.csharp / DotRush 扩展");
7217
+ }
7054
7218
  return {
7055
7219
  ...base,
7056
7220
  kind: "none",
7057
7221
  argv: [],
7058
7222
  ready: false,
7223
+ provisionRuntime: provisionMajor,
7059
7224
  reason: reasons.join(";") + "(可手动指定 Roslyn/OmniSharp 入口)"
7060
7225
  };
7061
7226
  });
@@ -7103,7 +7268,7 @@ function fingerprintOf(home) {
7103
7268
  return "?";
7104
7269
  }
7105
7270
  });
7106
- return process.platform + "|" + marks.join("|");
7271
+ return process.platform + "|" + (findSdkRoot() ?? "no-sdk") + "|" + marks.join("|");
7107
7272
  }
7108
7273
  /**
7109
7274
  * 读取磁盘缓存(幂等:每进程只载一次;schema 文件名已隔离版本)。
@@ -7186,6 +7351,196 @@ function clearProviderCache(home = dshHome()) {
7186
7351
  } catch (error) {}
7187
7352
  }
7188
7353
  //#endregion
7354
+ //#region src/lsp/dotnetProvision.ts
7355
+ /**
7356
+ * dsh-vscode-mode host — C# 服务器 .NET 运行时自动配置。
7357
+ * DotRush 等服务器需要 .NET 运行时;缺失时从官方 release 元数据解析下载地址,
7358
+ * sha512 校验后解压到用户级 dotnet 目录(win: %LOCALAPPDATA%\Microsoft\dotnet,
7359
+ * unix: ~/.dotnet),成功后通知监听者重载 provider——用户零手动操作。
7360
+ * 失败静默降级(保留原 reason 文案),10 分钟退避重试。
7361
+ * 安装全程有状态机(envInstallStates)供设置页一键安装与进度轮询使用。
7362
+ * 作者 ddj 2026年09月03号
7363
+ */
7364
+ const RETRY_MS = 6e5;
7365
+ /** 同 major 去重(in-flight)。 */
7366
+ const inFlight = /* @__PURE__ */ new Map();
7367
+ /** 失败时间戳(退避重试)。 */
7368
+ const failedAt = /* @__PURE__ */ new Map();
7369
+ /** 配置成功监听者(重载 provider 缓存与服务器)。 */
7370
+ const listeners = /* @__PURE__ */ new Set();
7371
+ /** 安装进行态(major → phase/message),供设置页轮询展示。 */
7372
+ const installStates = /* @__PURE__ */ new Map();
7373
+ /** 需求 id(与 envRequirements 的 id 一致)。 */
7374
+ function runtimeRequirementId(major) {
7375
+ return "dotnet-runtime:" + major;
7376
+ }
7377
+ /** 当前安装进行态快照(edrv.lsp.envState 载荷)。 */
7378
+ function envInstallStates() {
7379
+ return [...installStates.entries()].map(([major, state]) => ({
7380
+ id: runtimeRequirementId(major),
7381
+ phase: state.phase,
7382
+ message: state.message
7383
+ }));
7384
+ }
7385
+ /** 注册配置成功监听(成功后由 rpc 侧清缓存并重置服务器)。 */
7386
+ function onRuntimeProvisioned(listener) {
7387
+ listeners.add(listener);
7388
+ }
7389
+ /** 平台 → 官方运行时资产文件名。 */
7390
+ function assetNameFor(platform, arch) {
7391
+ const archPart = arch === "arm64" ? "arm64" : "x64";
7392
+ if (platform === "win32") return "dotnet-runtime-win-" + archPart + ".zip";
7393
+ if (platform === "darwin") return "dotnet-runtime-osx-" + archPart + ".tar.gz";
7394
+ return "dotnet-runtime-linux-" + archPart + ".tar.gz";
7395
+ }
7396
+ /** 用户级 dotnet 安装目录(与 providers 的发现候选位置一致)。 */
7397
+ function userDotnetDir() {
7398
+ if (process.platform === "win32") return join(process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "Microsoft", "dotnet");
7399
+ return join(homedir(), ".dotnet");
7400
+ }
7401
+ /** 从 release 元数据 files 里挑当前平台资产(纯函数,测试可注入平台)。 */
7402
+ function pickRuntimeAsset(files, platform = process.platform, arch = process.arch) {
7403
+ const name = assetNameFor(platform, arch);
7404
+ return files.find((f) => f.name === name) ?? null;
7405
+ }
7406
+ /** 是否允许自动配置(测试隔离 + 显式开关)。 */
7407
+ function allowed() {
7408
+ if (process.env.DSH_LSP_AUTO_RUNTIME === "0") return false;
7409
+ if (process.env.DSH_LSP_EXT_DIRS) return false;
7410
+ return true;
7411
+ }
7412
+ function setState(major, phase, message) {
7413
+ installStates.set(major, {
7414
+ phase,
7415
+ message
7416
+ });
7417
+ }
7418
+ function notifyListeners() {
7419
+ for (const listener of listeners) try {
7420
+ listener();
7421
+ } catch {}
7422
+ }
7423
+ /**
7424
+ * 触发运行时自动配置(幂等:同 major 去重;失败 10 分钟退避;已满足则直接成功回调)。
7425
+ * 静默自动路径与设置页一键安装共用;进度经 envInstallStates 暴露。
7426
+ * @author ddj 2026年09月03号
7427
+ * @param major 运行时大版本
7428
+ */
7429
+ function provisionRuntime(major) {
7430
+ if (inFlight.has(major)) return;
7431
+ const failed = failedAt.get(major);
7432
+ if (failed && Date.now() - failed < RETRY_MS) return;
7433
+ const task = runProvision(major).catch(() => false).then((ok) => {
7434
+ inFlight.delete(major);
7435
+ if (ok) {
7436
+ failedAt.delete(major);
7437
+ setState(major, "done");
7438
+ notifyListeners();
7439
+ } else {
7440
+ failedAt.set(major, Date.now());
7441
+ setState(major, "failed");
7442
+ }
7443
+ return ok;
7444
+ });
7445
+ inFlight.set(major, task);
7446
+ }
7447
+ /**
7448
+ * 一键安装入口(设置页按钮):校验允许后触发 provisionRuntime。
7449
+ * @author ddj 2026年09月03号
7450
+ * @param major 运行时大版本
7451
+ * @returns 是否成功启动(已满足/进行中/被禁用时返回 false)
7452
+ */
7453
+ function startEnvInstall(major) {
7454
+ if (dotnetWithRuntime(major)) {
7455
+ setState(major, "done");
7456
+ return false;
7457
+ }
7458
+ if (!allowed() || inFlight.has(major)) return false;
7459
+ const failed = failedAt.get(major);
7460
+ if (failed && Date.now() - failed < RETRY_MS && installStates.get(major)?.phase === "failed") failedAt.delete(major);
7461
+ provisionRuntime(major);
7462
+ return inFlight.has(major);
7463
+ }
7464
+ async function runProvision(major) {
7465
+ if (dotnetWithRuntime(major)) return true;
7466
+ setState(major, "downloading");
7467
+ const files = ((await fetchJson("https://builds.dotnet.microsoft.com/dotnet/release-metadata/" + major + ".0/releases.json"))?.releases?.[0])?.runtime?.files;
7468
+ if (!Array.isArray(files)) return false;
7469
+ const asset = pickRuntimeAsset(files);
7470
+ if (!asset?.url || !asset.hash) return false;
7471
+ const buf = await downloadVerified(asset.url, asset.hash);
7472
+ if (!buf) return false;
7473
+ setState(major, "extracting");
7474
+ const dir = userDotnetDir();
7475
+ const created = !existsSync(dir);
7476
+ try {
7477
+ if (!await extractTo(buf, dir)) throw new Error("解压失败");
7478
+ } catch (error) {
7479
+ if (created) rmSync(dir, {
7480
+ recursive: true,
7481
+ force: true
7482
+ });
7483
+ return false;
7484
+ }
7485
+ return dotnetWithRuntime(major) !== null;
7486
+ }
7487
+ /** 拉取 JSON(30s 超时,失败返回 null)。 */
7488
+ async function fetchJson(url) {
7489
+ try {
7490
+ const res = await fetch(url, { signal: AbortSignal.timeout(3e4) });
7491
+ if (!res.ok) return null;
7492
+ return await res.json();
7493
+ } catch {
7494
+ return null;
7495
+ }
7496
+ }
7497
+ /** 下载并按官方 sha512 校验(5 分钟超时,不匹配返回 null)。 */
7498
+ async function downloadVerified(url, sha512) {
7499
+ try {
7500
+ const res = await fetch(url, { signal: AbortSignal.timeout(3e5) });
7501
+ if (!res.ok) return null;
7502
+ const buf = Buffer.from(await res.arrayBuffer());
7503
+ return createHash("sha512").update(buf).digest("hex").toLowerCase() === sha512.toLowerCase() ? buf : null;
7504
+ } catch {
7505
+ return null;
7506
+ }
7507
+ }
7508
+ /** 解压到目录:win 走 zip(PK 头),unix 落临时包交系统 tar。 */
7509
+ async function extractTo(buf, dir) {
7510
+ mkdirSync(dir, { recursive: true });
7511
+ if (buf.subarray(0, 2).toString("ascii") === "PK") {
7512
+ for (const entry of unzip(buf)) {
7513
+ if (!entry.path || entry.path.startsWith("..")) continue;
7514
+ const parts = entry.path.split(/[\\/]/).filter((p) => p && p !== ".." && p !== ".");
7515
+ if (!parts.length) continue;
7516
+ const target = join(dir, ...parts);
7517
+ if (entry.isDirectory) {
7518
+ mkdirSync(target, { recursive: true });
7519
+ continue;
7520
+ }
7521
+ mkdirSync(dirname(target), { recursive: true });
7522
+ writeFileSync(target, entry.data);
7523
+ }
7524
+ return true;
7525
+ }
7526
+ const tmp = join(tmpdir(), "dsh-dotnet-" + Date.now() + ".tar.gz");
7527
+ try {
7528
+ writeFileSync(tmp, buf);
7529
+ return await new Promise((resolve) => {
7530
+ const child = spawn("tar", [
7531
+ "-xzf",
7532
+ tmp,
7533
+ "-C",
7534
+ dir
7535
+ ], { stdio: "ignore" });
7536
+ child.on("exit", (code) => resolve(code === 0));
7537
+ child.on("error", () => resolve(false));
7538
+ });
7539
+ } finally {
7540
+ rmSync(tmp, { force: true });
7541
+ }
7542
+ }
7543
+ //#endregion
7189
7544
  //#region src/lsp/config.ts
7190
7545
  const LSP_SETTINGS_NS = "dsh-vscode-mode.languageServers";
7191
7546
  const LSP_LANGUAGES = ["lua", "csharp"];
@@ -7263,10 +7618,12 @@ function resolveProviderSpec(ctx, pluginConfig, languageId) {
7263
7618
  ready: false,
7264
7619
  reason: "已在设置中禁用"
7265
7620
  };
7266
- return resolver({
7621
+ const spec = resolver({
7267
7622
  command: lang?.command,
7268
7623
  path: lang?.path
7269
7624
  });
7625
+ if (spec.provisionRuntime) provisionRuntime(spec.provisionRuntime);
7626
+ return spec;
7270
7627
  }
7271
7628
  /** 语言 → 扩展名(供客户端判断是否触发 LSP)。 */
7272
7629
  function langOfPath(path) {
@@ -7281,6 +7638,82 @@ function langOfPath(path) {
7281
7638
  return null;
7282
7639
  }
7283
7640
  //#endregion
7641
+ //#region src/lsp/envRequirements.ts
7642
+ /**
7643
+ * dsh-vscode-mode host — LSP 环境需求检测注册表。
7644
+ * 每个语言注册一个检测器:扫描当前环境,返回未满足的需求清单(含一键安装与官网链接信息)。
7645
+ * 新接入 LSP:在 CHECKERS 加一个检测器条目;需要内置安装器时在 INSTALLERS 注册同前缀安装函数。
7646
+ * 检测在 status/redetect/configUpdate 时执行(不缓存),保证安装完成后状态即时刷新。
7647
+ * 作者 ddj 2026年09月03号
7648
+ */
7649
+ /** dotnet 运行时需求构造(DotRush / ms-Roslyn 共用)。 */
7650
+ function dotnetRuntimeRequirement(major, detail) {
7651
+ return {
7652
+ id: "dotnet-runtime:" + major,
7653
+ label: ".NET 运行时 " + major + ".0",
7654
+ detail,
7655
+ installable: true,
7656
+ manualUrl: "https://dotnet.microsoft.com/download/dotnet/" + major + ".0",
7657
+ manualLabel: "官网下载"
7658
+ };
7659
+ }
7660
+ /** csharp 检测器:DotRush 缺运行时 / ms-Roslyn 缺 dotnet。 */
7661
+ function checkCSharp(home) {
7662
+ const found = candidateCSharpServers(home);
7663
+ const missing = [];
7664
+ if (found.dotrushDll) {
7665
+ const major = runtimeMajorOf(dirname(found.dotrushDll));
7666
+ if (major !== null && dotnetWithRuntime(major) === null) missing.push(dotnetRuntimeRequirement(major, "DotRush 语言服务器运行所需"));
7667
+ return missing;
7668
+ }
7669
+ if (found.roslynDll && !found.dotnet) missing.push(dotnetRuntimeRequirement(8, "Roslyn 语言服务器运行所需"));
7670
+ return missing;
7671
+ }
7672
+ /** lua 检测器:EmmyLua/LuaLS 自带运行时,无外部依赖。 */
7673
+ function checkLua() {
7674
+ return [];
7675
+ }
7676
+ /** 检测器注册表(按 languageId;新 LSP 在此加条目即可)。 */
7677
+ const CHECKERS = {
7678
+ lua: checkLua,
7679
+ csharp: checkCSharp
7680
+ };
7681
+ /** 安装器注册表(按需求 id 前缀匹配;新依赖类型在此加条目)。 */
7682
+ const INSTALLERS = [{
7683
+ prefix: "dotnet-runtime:",
7684
+ run: (id) => {
7685
+ const major = Number.parseInt(id.slice(15), 10);
7686
+ return Number.isInteger(major) ? startEnvInstall(major) : false;
7687
+ }
7688
+ }];
7689
+ /**
7690
+ * 检测某语言未满足的环境需求(仅返回缺失项;检测器异常按无需求处理)。
7691
+ * @author ddj 2026年09月03号
7692
+ * @param languageId 语言 id
7693
+ * @param home DSH home(测试可注入临时目录)
7694
+ * @returns 缺失需求清单
7695
+ */
7696
+ function envRequirementsFor(languageId, home) {
7697
+ const checker = CHECKERS[languageId];
7698
+ if (!checker) return [];
7699
+ try {
7700
+ return checker(home);
7701
+ } catch {
7702
+ return [];
7703
+ }
7704
+ }
7705
+ /**
7706
+ * 尝试一键安装某条需求(存在内置安装器时启动,进行中/已满足返回 false)。
7707
+ * @author ddj 2026年09月03号
7708
+ * @param id 需求标识(如 dotnet-runtime:10)
7709
+ * @returns 是否成功启动安装
7710
+ */
7711
+ function installRequirement(id) {
7712
+ const installer = INSTALLERS.find((it) => id.startsWith(it.prefix));
7713
+ if (!installer) return false;
7714
+ return installer.run(id);
7715
+ }
7716
+ //#endregion
7284
7717
  //#region src/lsp/rpc.ts
7285
7718
  /**
7286
7719
  * 打开文档跟踪:openDocs(docKey→sessionId) + rootLangRefs 计数 + sessionAcquired 归属。
@@ -7375,6 +7808,10 @@ function createDocTracker() {
7375
7808
  function createLspRpc(deps) {
7376
7809
  const { ctx, pluginConfig, manager } = deps;
7377
7810
  const tracker = createDocTracker();
7811
+ onRuntimeProvisioned(() => {
7812
+ clearProviderCache();
7813
+ manager.resetLanguage("csharp");
7814
+ });
7378
7815
  /** 解析会话与工作区根(失败 → null + error)。 */
7379
7816
  const rootOf = async (sessionId) => {
7380
7817
  const session = sessionOf(ctx, sessionId);
@@ -7395,9 +7832,10 @@ function createLspRpc(deps) {
7395
7832
  ...location,
7396
7833
  root
7397
7834
  }));
7398
- /** 当前 provider 检测结论 → 设置页 idle 状态(不启动 server)。 */
7835
+ /** 当前 provider 检测结论 → 设置页 idle 状态(不启动 server);附带未满足的环境需求。 */
7399
7836
  const detectedStatus = (languageId, root) => {
7400
7837
  const spec = resolveProviderSpec(ctx, pluginConfig, languageId);
7838
+ const missingEnv = envRequirementsFor(languageId, dshHome());
7401
7839
  return {
7402
7840
  languageId,
7403
7841
  source: spec.kind,
@@ -7405,7 +7843,8 @@ function createLspRpc(deps) {
7405
7843
  reason: spec.ready ? void 0 : spec.reason,
7406
7844
  version: spec.version,
7407
7845
  providerName: spec.providerName,
7408
- root
7846
+ root,
7847
+ missingEnv: missingEnv.length ? missingEnv : void 0
7409
7848
  };
7410
7849
  };
7411
7850
  /**
@@ -7713,6 +8152,23 @@ function createLspRpc(deps) {
7713
8152
  };
7714
8153
  }
7715
8154
  },
8155
+ "edrv.lsp.envInstall": async (args) => {
8156
+ if (!installRequirement(args.id)) return {
8157
+ ok: false,
8158
+ started: false,
8159
+ error: "该需求不支持一键安装:" + args.id
8160
+ };
8161
+ return {
8162
+ ok: true,
8163
+ started: true
8164
+ };
8165
+ },
8166
+ "edrv.lsp.envState": async () => {
8167
+ return {
8168
+ ok: true,
8169
+ states: envInstallStates()
8170
+ };
8171
+ },
7716
8172
  "edrv.lsp.ext.list": async () => ({
7717
8173
  ok: true,
7718
8174
  extensions: listInstalled()