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/README.md +4 -0
- package/lib/client.js +256 -8
- package/lib/client.js.map +1 -1
- package/lib/index.js +590 -34
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/monaco/lsp/providers.ts +10 -1
- package/src/client/monaco/lsp/underline.ts +101 -0
- package/src/client/styles/editor.css +2 -0
- package/src/client/ui/EditorView.ts +2 -0
- package/src/client/ui/LspSettings.ts +83 -5
- package/src/lsp/client.ts +4 -0
- package/src/lsp/config.ts +5 -1
- package/src/lsp/derive.ts +96 -0
- 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 +58 -5
- package/src/shared/lsp.ts +75 -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";
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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)
|
|
6704
|
-
*
|
|
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
|
-
/**
|
|
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:
|
|
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
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
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(优先级:手动 > 扩展源 >
|
|
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 (
|
|
6953
|
-
|
|
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
|
-
|
|
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()
|