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.
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";
@@ -5760,6 +5760,7 @@ function createLspClient(transport, events = {}) {
5760
5760
  const pending = /* @__PURE__ */ new Map();
5761
5761
  let serverCapabilities = {
5762
5762
  definition: false,
5763
+ declaration: false,
5763
5764
  references: false,
5764
5765
  documentSymbol: false,
5765
5766
  workspaceSymbol: false,
@@ -5894,6 +5895,7 @@ function createLspClient(transport, events = {}) {
5894
5895
  didSave: true
5895
5896
  },
5896
5897
  definition: { dynamicRegistration: false },
5898
+ declaration: { dynamicRegistration: false },
5897
5899
  references: { dynamicRegistration: false },
5898
5900
  documentSymbol: { dynamicRegistration: false },
5899
5901
  hover: { dynamicRegistration: false },
@@ -5913,6 +5915,7 @@ function createLspClient(transport, events = {}) {
5913
5915
  const legend = caps.semanticTokensProvider?.legend;
5914
5916
  serverCapabilities = {
5915
5917
  definition: Boolean(caps.definitionProvider),
5918
+ declaration: Boolean(caps.declarationProvider),
5916
5919
  references: Boolean(caps.referencesProvider),
5917
5920
  documentSymbol: Boolean(caps.documentSymbolProvider),
5918
5921
  workspaceSymbol: Boolean(caps.workspaceSymbolProvider),
@@ -5966,6 +5969,73 @@ function pathToFileUri(absPath) {
5966
5969
  return pathToFileURL(normalized).toString();
5967
5970
  }
5968
5971
  //#endregion
5972
+ //#region src/lsp/derive.ts
5973
+ /** file:// URI(或裸路径)→ 归一化绝对路径(小写、/ 分隔)。 */
5974
+ function normAbs(uri) {
5975
+ const path = String(uri ?? "").replace(/^file:\/\/\/?/, "");
5976
+ return decodeURIComponent(path).replace(/\\/g, "/").replace(/^\/([A-Za-z]:)/, "$1").toLowerCase();
5977
+ }
5978
+ /** 取文档 text 在 LSP range(0-based)内的切片。 */
5979
+ function sliceRange(text, range) {
5980
+ const lines = text.split(/\r\n|\r|\n/);
5981
+ const start = range.start;
5982
+ const end = range.end;
5983
+ const startLine = Math.max(0, start.line);
5984
+ const endLine = Math.max(startLine, end.line);
5985
+ if (startLine >= lines.length || endLine >= lines.length) return "";
5986
+ const startChar = Math.max(0, start.character);
5987
+ if (startLine === endLine) return lines[startLine].slice(startChar, Math.max(startChar, end.character));
5988
+ const first = lines[startLine].slice(startChar);
5989
+ const middle = lines.slice(startLine + 1, endLine);
5990
+ const last = lines[endLine].slice(0, Math.max(0, end.character));
5991
+ return [
5992
+ first,
5993
+ ...middle,
5994
+ last
5995
+ ].join("\n");
5996
+ }
5997
+ /** LSP position 是否落在 range 内(start 包含、end 排除;零宽按点匹配)。 */
5998
+ function posInLspRange(pos, range) {
5999
+ const start = range.start;
6000
+ const end = range.end ?? start;
6001
+ if (start.line === end.line && start.character === end.character) return pos.line === start.line && pos.character === start.character;
6002
+ if (pos.line < start.line || pos.line > end.line) return false;
6003
+ if (pos.line === start.line && pos.character < start.character) return false;
6004
+ if (pos.line === end.line && pos.character >= end.character) return false;
6005
+ return true;
6006
+ }
6007
+ /** 两个 range 是否完全一致。 */
6008
+ function sameRange(a, b) {
6009
+ return a.start.line === b.start.line && a.start.character === b.start.character && a.end.line === b.end.line && a.end.character === b.end.character;
6010
+ }
6011
+ /** range 长度(行/列均按 0-based 差估算,仅用于取"最小包裹"排序)。 */
6012
+ function rangeSize(r) {
6013
+ return (r.end.line - r.start.line) * 1e6 + (r.end.line === r.start.line ? r.end.character - r.start.character : r.end.character);
6014
+ }
6015
+ /**
6016
+ * 从引用列表推导定义位置。
6017
+ * @author ddj 2026年09月02号
6018
+ * @param queryPos 查询位置(LSP 0-based)
6019
+ * @param locations references(includeDeclaration) 结果
6020
+ * @param docText 当前文档全文(词文本一致校验)
6021
+ * @param docAbsPath 当前文档绝对路径(root+相对路径,与 loc.uri 归一化后比较)
6022
+ * @returns 推导出的定义位置;无法确定返回 null
6023
+ */
6024
+ function deriveDefinitionFromLocations(queryPos, locations, docText, docAbsPath) {
6025
+ if (!locations.length) return null;
6026
+ const docAbs = normAbs(docAbsPath);
6027
+ const containing = locations.filter((loc) => posInLspRange(queryPos, loc.range)).sort((a, b) => rangeSize(a.range) - rangeSize(b.range))[0];
6028
+ if (!containing) return null;
6029
+ const queryWord = sliceRange(docText, containing.range);
6030
+ if (!queryWord) return null;
6031
+ const candidates = locations.filter((loc) => {
6032
+ return normAbs(loc.uri) === docAbs && sliceRange(docText, loc.range) === queryWord;
6033
+ });
6034
+ if (!candidates.length) return null;
6035
+ const first = candidates[0];
6036
+ return sameRange(first.range, containing.range) ? containing : first;
6037
+ }
6038
+ //#endregion
5969
6039
  //#region src/lsp/server.ts
5970
6040
  /**
5971
6041
  * dsh-vscode-mode host — 单语言服务器会话(绑定 工作区根 + 语言)。
@@ -5990,6 +6060,7 @@ function createLspServer(spec, root, languageId, logger) {
5990
6060
  let client = null;
5991
6061
  let capabilities = {
5992
6062
  definition: false,
6063
+ declaration: false,
5993
6064
  references: false,
5994
6065
  documentSymbol: false,
5995
6066
  workspaceSymbol: false,
@@ -6095,7 +6166,8 @@ function createLspServer(spec, root, languageId, logger) {
6095
6166
  try {
6096
6167
  transport = spawnServer({
6097
6168
  argv: spec.argv,
6098
- cwd: spec.cwd ?? root
6169
+ cwd: spec.cwd ?? root,
6170
+ env: spec.env
6099
6171
  }, (message) => {}, (line) => log("stderr: " + line));
6100
6172
  client = createLspClient(transport, {
6101
6173
  onReady: (caps) => {
@@ -6123,7 +6195,9 @@ function createLspServer(spec, root, languageId, logger) {
6123
6195
  },
6124
6196
  onLog: (line) => log("lsp: " + line)
6125
6197
  });
6126
- 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;
6127
6201
  return true;
6128
6202
  } catch (error) {
6129
6203
  log("start failed: " + String(error));
@@ -6173,13 +6247,42 @@ function createLspServer(spec, root, languageId, logger) {
6173
6247
  async definition(path, line, character) {
6174
6248
  const doc = docs.get(path);
6175
6249
  if (!doc || !await readyWait) return [];
6176
- return normalizeLocations(await ensureClient().request("textDocument/definition", {
6250
+ const client = ensureClient();
6251
+ const position = {
6252
+ line,
6253
+ character
6254
+ };
6255
+ const direct = normalizeLocations(await client.request("textDocument/definition", {
6177
6256
  textDocument: { uri: doc.uri },
6178
- position: {
6179
- line,
6180
- character
6181
- }
6257
+ position
6182
6258
  }));
6259
+ if (direct.length) return direct;
6260
+ if (capabilities.declaration) try {
6261
+ const decl = normalizeLocations(await client.request("textDocument/declaration", {
6262
+ textDocument: { uri: doc.uri },
6263
+ position
6264
+ }));
6265
+ if (decl.length) {
6266
+ log("definition fallback → declaration (" + path + ":" + line + ":" + character + ")");
6267
+ return decl;
6268
+ }
6269
+ } catch (error) {
6270
+ log("declaration request failed: " + String(error));
6271
+ }
6272
+ try {
6273
+ const derived = deriveDefinitionFromLocations(position, normalizeLocations(await client.request("textDocument/references", {
6274
+ textDocument: { uri: doc.uri },
6275
+ position,
6276
+ context: { includeDeclaration: true }
6277
+ })), doc.text, doc.uri);
6278
+ if (derived) {
6279
+ log("definition fallback → references-derived (" + path + ":" + line + ":" + character + ")");
6280
+ return [derived];
6281
+ }
6282
+ } catch (error) {
6283
+ log("references fallback failed: " + String(error));
6284
+ }
6285
+ return [];
6183
6286
  },
6184
6287
  async references(path, line, character, includeDeclaration) {
6185
6288
  const doc = docs.get(path);
@@ -6700,8 +6803,9 @@ async function updateExtension(id) {
6700
6803
  /**
6701
6804
  * dsh-vscode-mode host — 语言服务器 provider 解析。
6702
6805
  * 优先级:手动配置 > extmgr 扩展源(Phase 2 挂载) > 自动发现(~/.vscode/extensions、
6703
- * ~/.dsh/dsh-vscode-mode/extensions、PATH)。产物 LspProviderSpec:{ ready, argv, cwd, reason },
6704
- * 供 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 启动服务器。
6705
6809
  * 作者 ddj 2026-08-27
6706
6810
  */
6707
6811
  /** 平台目录名(LuaLS/OmniSharp 的 server/bin/<平台>)。 */
@@ -6725,8 +6829,14 @@ function listMatchingDirs(dir, prefix) {
6725
6829
  }
6726
6830
  return entries.filter((e) => e.isDirectory() && e.name.startsWith(prefix)).map((e) => join(dir, e.name));
6727
6831
  }
6728
- /** 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
+ */
6729
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));
6730
6840
  const userHome = homedir();
6731
6841
  return [
6732
6842
  join(userHome, ".vscode", "extensions"),
@@ -6747,7 +6857,7 @@ function extensionProviderFor(languageId) {
6747
6857
  cwd: p.cwd,
6748
6858
  ready: true,
6749
6859
  version: p.version,
6750
- providerName: p.version ? "EmmyLua" : "语言服务器扩展"
6860
+ providerName: "语言服务器扩展"
6751
6861
  };
6752
6862
  }
6753
6863
  /** PATH 中查找可执行文件(不含扩展名校验,交由 spawn 报错)。 */
@@ -6768,27 +6878,104 @@ function findInPath(name) {
6768
6878
  }
6769
6879
  return null;
6770
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
+ }
6771
6891
  /** 查找 dotnet(PATH + 常见安装路径)。 */
6772
6892
  function findDotnet() {
6773
- const inPath = findInPath("dotnet");
6774
- if (inPath) return inPath;
6775
- 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
+ }
6916
+ return null;
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
+ }
6776
6929
  return null;
6777
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
+ }
6778
6970
  /** EmmyLua 自动发现候选(tangzx.emmylua-* / server/emmylua_ls)。 */
6779
6971
  function candidateEmmyLua(home = dshHome()) {
6780
6972
  const candidates = [];
6781
6973
  for (const dir of vscodeExtensionsDirs(home)) for (const extDir of listMatchingDirs(dir, "tangzx.emmylua-")) {
6782
6974
  const server = join(extDir, "server", "emmylua_ls" + exeSuffix());
6783
6975
  if (!existsSync(server)) continue;
6784
- let version;
6785
- try {
6786
- const manifest = JSON.parse(readFileSync(join(extDir, "package.json"), "utf8"));
6787
- if (typeof manifest.version === "string") version = manifest.version;
6788
- } catch (error) {}
6789
6976
  candidates.push({
6790
6977
  path: server,
6791
- version
6978
+ version: manifestVersionOf(extDir)
6792
6979
  });
6793
6980
  }
6794
6981
  candidates.sort((a, b) => (b.version ?? "").localeCompare(a.version ?? "", void 0, { numeric: true }) || a.path.localeCompare(b.path));
@@ -6807,20 +6994,73 @@ function candidateLuaServers(home = dshHome()) {
6807
6994
  }
6808
6995
  return out;
6809
6996
  }
6810
- /** 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
+ */
6811
7004
  function candidateCSharpServers(home = dshHome()) {
6812
7005
  const out = {};
6813
- for (const dir of vscodeExtensionsDirs(home)) for (const extDir of listMatchingDirs(dir, "ms-dotnettools.csharp-")) {
6814
- const roslyn = join(extDir, ".roslyn", "Microsoft.CodeAnalysis.LanguageServer.dll");
6815
- if (!out.roslynDll && existsSync(roslyn)) out.roslynDll = roslyn;
6816
- const omni = join(extDir, ".omnisharp", "OmniSharp" + exeSuffix());
6817
- 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;
6818
7027
  }
6819
7028
  const dotnet = findDotnet();
6820
7029
  if (dotnet) out.dotnet = dotnet;
6821
7030
  return out;
6822
7031
  }
6823
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
+ /**
6824
7064
  * 手动配置归一化:path 指向可执行文件/目录,command 为可执行名(PATH 解析)。
6825
7065
  * 未配置时返回空 reason(由各 resolve 兜底输出语言相关安装指引文案)。
6826
7066
  * @author ddj 2026年08月27号
@@ -6912,7 +7152,8 @@ function resolveLuaProvider(manual, home = dshHome()) {
6912
7152
  });
6913
7153
  }
6914
7154
  /**
6915
- * 解析 C# provider(优先级:手动 > 扩展源 > 自动发现;需 dotnet + Roslyn dll;或 OmniSharp)。
7155
+ * 解析 C# provider(优先级:手动 > 扩展源 > 自动发现;Roslyn dll 需 dotnet;
7156
+ * DotRush 需满足其运行时的 dotnet;或 OmniSharp)。
6916
7157
  * @author ddj 2026年08月27号
6917
7158
  * @param manual 手动配置
6918
7159
  * @param home DSH home(缺省真实;测试可注入临时目录)
@@ -6932,6 +7173,8 @@ function resolveCSharpProvider(manual, home = dshHome()) {
6932
7173
  const ext = extensionProviderFor("csharp");
6933
7174
  if (ext) return ext;
6934
7175
  const discovered = candidateCSharpServers(h);
7176
+ const sdkRoot = findSdkRoot();
7177
+ const msbuildEnv = sdkRoot ? sdkEnvOf(sdkRoot) : void 0;
6935
7178
  if (discovered.roslynDll && discovered.dotnet) return {
6936
7179
  ...base,
6937
7180
  kind: "discover",
@@ -6940,8 +7183,26 @@ function resolveCSharpProvider(manual, home = dshHome()) {
6940
7183
  discovered.roslynDll,
6941
7184
  "--stdio"
6942
7185
  ],
7186
+ env: msbuildEnv,
6943
7187
  ready: true
6944
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
+ }
6945
7206
  if (discovered.omnisharpExe) return {
6946
7207
  ...base,
6947
7208
  kind: "discover",
@@ -6949,13 +7210,17 @@ function resolveCSharpProvider(manual, home = dshHome()) {
6949
7210
  ready: true
6950
7211
  };
6951
7212
  const reasons = [];
6952
- if (!discovered.dotnet) reasons.push("未找到 dotnet");
6953
- 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
+ }
6954
7218
  return {
6955
7219
  ...base,
6956
7220
  kind: "none",
6957
7221
  argv: [],
6958
7222
  ready: false,
7223
+ provisionRuntime: provisionMajor,
6959
7224
  reason: reasons.join(";") + "(可手动指定 Roslyn/OmniSharp 入口)"
6960
7225
  };
6961
7226
  });
@@ -7003,7 +7268,7 @@ function fingerprintOf(home) {
7003
7268
  return "?";
7004
7269
  }
7005
7270
  });
7006
- return process.platform + "|" + marks.join("|");
7271
+ return process.platform + "|" + (findSdkRoot() ?? "no-sdk") + "|" + marks.join("|");
7007
7272
  }
7008
7273
  /**
7009
7274
  * 读取磁盘缓存(幂等:每进程只载一次;schema 文件名已隔离版本)。
@@ -7086,6 +7351,196 @@ function clearProviderCache(home = dshHome()) {
7086
7351
  } catch (error) {}
7087
7352
  }
7088
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
7089
7544
  //#region src/lsp/config.ts
7090
7545
  const LSP_SETTINGS_NS = "dsh-vscode-mode.languageServers";
7091
7546
  const LSP_LANGUAGES = ["lua", "csharp"];
@@ -7163,10 +7618,12 @@ function resolveProviderSpec(ctx, pluginConfig, languageId) {
7163
7618
  ready: false,
7164
7619
  reason: "已在设置中禁用"
7165
7620
  };
7166
- return resolver({
7621
+ const spec = resolver({
7167
7622
  command: lang?.command,
7168
7623
  path: lang?.path
7169
7624
  });
7625
+ if (spec.provisionRuntime) provisionRuntime(spec.provisionRuntime);
7626
+ return spec;
7170
7627
  }
7171
7628
  /** 语言 → 扩展名(供客户端判断是否触发 LSP)。 */
7172
7629
  function langOfPath(path) {
@@ -7181,6 +7638,82 @@ function langOfPath(path) {
7181
7638
  return null;
7182
7639
  }
7183
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
7184
7717
  //#region src/lsp/rpc.ts
7185
7718
  /**
7186
7719
  * 打开文档跟踪:openDocs(docKey→sessionId) + rootLangRefs 计数 + sessionAcquired 归属。
@@ -7275,6 +7808,10 @@ function createDocTracker() {
7275
7808
  function createLspRpc(deps) {
7276
7809
  const { ctx, pluginConfig, manager } = deps;
7277
7810
  const tracker = createDocTracker();
7811
+ onRuntimeProvisioned(() => {
7812
+ clearProviderCache();
7813
+ manager.resetLanguage("csharp");
7814
+ });
7278
7815
  /** 解析会话与工作区根(失败 → null + error)。 */
7279
7816
  const rootOf = async (sessionId) => {
7280
7817
  const session = sessionOf(ctx, sessionId);
@@ -7295,9 +7832,10 @@ function createLspRpc(deps) {
7295
7832
  ...location,
7296
7833
  root
7297
7834
  }));
7298
- /** 当前 provider 检测结论 → 设置页 idle 状态(不启动 server)。 */
7835
+ /** 当前 provider 检测结论 → 设置页 idle 状态(不启动 server);附带未满足的环境需求。 */
7299
7836
  const detectedStatus = (languageId, root) => {
7300
7837
  const spec = resolveProviderSpec(ctx, pluginConfig, languageId);
7838
+ const missingEnv = envRequirementsFor(languageId, dshHome());
7301
7839
  return {
7302
7840
  languageId,
7303
7841
  source: spec.kind,
@@ -7305,7 +7843,8 @@ function createLspRpc(deps) {
7305
7843
  reason: spec.ready ? void 0 : spec.reason,
7306
7844
  version: spec.version,
7307
7845
  providerName: spec.providerName,
7308
- root
7846
+ root,
7847
+ missingEnv: missingEnv.length ? missingEnv : void 0
7309
7848
  };
7310
7849
  };
7311
7850
  /**
@@ -7613,6 +8152,23 @@ function createLspRpc(deps) {
7613
8152
  };
7614
8153
  }
7615
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
+ },
7616
8172
  "edrv.lsp.ext.list": async () => ({
7617
8173
  ok: true,
7618
8174
  extensions: listInstalled()