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/client.js +260 -179
- package/lib/client.js.map +1 -1
- package/lib/index.js +485 -29
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/addToConversation.ts +32 -19
- package/src/client/sidebar/menuItems.ts +23 -2
- package/src/client/sidebar/panels/FileExplorer.ts +2 -4
- package/src/client/sidebar/types.ts +3 -0
- package/src/client/ui/EditorView.ts +3 -9
- package/src/client/ui/LspSettings.ts +83 -5
- package/src/client/ui/menuPosition.ts +2 -28
- package/src/lsp/config.ts +5 -1
- package/src/lsp/dotnetProvision.ts +224 -0
- package/src/lsp/envRequirements.ts +92 -0
- package/src/lsp/providers.ts +198 -25
- package/src/lsp/rpc.ts +24 -1
- package/src/lsp/server.ts +8 -1
- package/src/shared/lsp.ts +18 -0
- package/src/shared/rpc.ts +5 -1
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
|
-
|
|
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)
|
|
6804
|
-
*
|
|
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
|
-
/**
|
|
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:
|
|
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
|
-
|
|
6874
|
-
|
|
6875
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
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(优先级:手动 > 扩展源 >
|
|
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 (
|
|
7053
|
-
|
|
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
|
-
|
|
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()
|