dsh-vscode-mode 0.1.36 → 0.1.43
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 +730 -29
- package/lib/client.js.map +1 -1
- package/lib/index.js +699 -88
- package/lib/index.js.map +1 -1
- package/package.json +1 -5
- package/src/client/monaco/loader.ts +12 -1
- package/src/client/monaco/lsp/lspClient.ts +65 -11
- package/src/client/monaco/lsp/providers.ts +34 -1
- package/src/client/monaco/theme.ts +229 -0
- package/src/client/ui/EditorView.ts +21 -2
- package/src/client/ui/LspSettings.ts +39 -7
- package/src/client/ui/McpSettings.ts +5 -1
- package/src/compat.ts +46 -5
- package/src/dshVersion.ts +121 -0
- package/src/fileOpenSettings.ts +143 -24
- package/src/host-deps.d.ts +5 -5
- package/src/index.ts +2 -1
- package/src/lsp/client.ts +29 -2
- package/src/lsp/manager.ts +15 -0
- package/src/lsp/providers.ts +203 -24
- package/src/lsp/rpc.ts +86 -4
- package/src/lsp/server.ts +80 -3
- package/src/lsp/settings.ts +12 -6
- package/src/paths.ts +7 -0
- package/src/rpc.ts +4 -1
- package/src/shared/compat.ts +9 -3
- package/src/shared/lsp.ts +24 -0
- package/src/shared/rpc.ts +6 -2
package/lib/index.js
CHANGED
|
@@ -128,18 +128,9 @@ const FILE_OPEN_SETTINGS_NS = "dsh-vscode-mode";
|
|
|
128
128
|
const FILE_OPEN_DEFAULT = "auto";
|
|
129
129
|
let depsPromise;
|
|
130
130
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* @returns keybindings 字段形状
|
|
135
|
-
*/
|
|
136
|
-
function keybindingsShape(z) {
|
|
137
|
-
const shape = {};
|
|
138
|
-
for (const [id, chord] of Object.entries(KEYBINDING_DEFAULTS)) shape[id] = z.string().default(chord);
|
|
139
|
-
return shape;
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* 动态加载设置依赖(模块级缓存;任一缺失返回 null 而非抛错)。
|
|
131
|
+
* 动态加载设置依赖(模块级缓存;任一缺失/失败返回 null 而非抛错)。
|
|
132
|
+
* installSettingsSection 仅在 rc 线 dsh-settings 中存在时提供(alpha 起移除,
|
|
133
|
+
* 属性探测得到 undefined,不抛错)。
|
|
143
134
|
* @author ddj 2026年08月24号
|
|
144
135
|
* @returns 设置依赖或 null
|
|
145
136
|
*/
|
|
@@ -150,6 +141,101 @@ function loadSettingsDeps() {
|
|
|
150
141
|
})).catch(() => null);
|
|
151
142
|
return depsPromise;
|
|
152
143
|
}
|
|
144
|
+
const INSTALL_UNMOUNTED = "设置 section 尚未装配";
|
|
145
|
+
const INSTALL_LEGACY = "rc 线:dsh-settings.installSettingsSection";
|
|
146
|
+
const INSTALL_SERVICE = "0.1.2-alpha 线:settings 服务 installSection";
|
|
147
|
+
const INSTALL_NONE = "两路均不可用:设置持久化降级为配置值";
|
|
148
|
+
let observedInstall = {
|
|
149
|
+
strategy: "unknown",
|
|
150
|
+
note: INSTALL_UNMOUNTED
|
|
151
|
+
};
|
|
152
|
+
/** 记录最近一次安装策略(供兼容报告展示;失败路径也记录,便于诊断)。 */
|
|
153
|
+
function recordInstall(strategy, note) {
|
|
154
|
+
observedInstall = {
|
|
155
|
+
strategy,
|
|
156
|
+
note
|
|
157
|
+
};
|
|
158
|
+
return strategy;
|
|
159
|
+
}
|
|
160
|
+
/** 读取观测到的安装策略(供报告;测试可用 reset 复位)。 */
|
|
161
|
+
function settingsInstallStrategy() {
|
|
162
|
+
return observedInstall.strategy;
|
|
163
|
+
}
|
|
164
|
+
/** 读取观测到的安装策略说明文案。 */
|
|
165
|
+
function settingsInstallNote() {
|
|
166
|
+
return observedInstall.note;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* 设置 section 版本自适应安装(核心策略分派)。
|
|
170
|
+
* legacy:dsh-settings 仍导出 installSettingsSection(rc 线)→ 原样调用,行为与旧版一致。
|
|
171
|
+
* service:该导出已移除(0.1.2-alpha 起)→ 经 ctx.inject(['settings']) 走服务方法
|
|
172
|
+
* provider.installSection(owner, ns, schema, entry, hooks)(等义封装,含
|
|
173
|
+
* base 层与 fiber 卸载回退)。回调内方法缺失再降级记录 none。
|
|
174
|
+
* none:两条路由都不存在 → 仅记录并告警,调用方按配置值运行。
|
|
175
|
+
* 全程不抛错、不产生未捕获 rejection。
|
|
176
|
+
* @author ddj 2026年09月02号
|
|
177
|
+
* @param ctx DSH host 上下文(inject 可选探测)
|
|
178
|
+
* @param ns 设置命名空间
|
|
179
|
+
* @param schema schemastery schema
|
|
180
|
+
* @param entry 初始设置值(作为 base 层)
|
|
181
|
+
* @param hooks 设置源绑定与变更回调
|
|
182
|
+
* @param loader 依赖装载器(测试注入)
|
|
183
|
+
* @returns 采用/观测到的策略
|
|
184
|
+
*/
|
|
185
|
+
async function runSettingsInstall(ctx, ns, schema, entry, hooks, loader = loadSettingsDeps) {
|
|
186
|
+
let deps = null;
|
|
187
|
+
try {
|
|
188
|
+
deps = await loader();
|
|
189
|
+
} catch {}
|
|
190
|
+
if (!deps) {
|
|
191
|
+
console.warn("[dsh-vscode-mode] 设置依赖不可用,section " + ns + " 未安装(配置回退)");
|
|
192
|
+
return recordInstall("none", INSTALL_NONE);
|
|
193
|
+
}
|
|
194
|
+
const legacy = deps.installSettingsSection;
|
|
195
|
+
if (typeof legacy === "function") try {
|
|
196
|
+
legacy(ctx, ns, schema, entry, hooks);
|
|
197
|
+
return recordInstall("legacy", INSTALL_LEGACY);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.warn("[dsh-vscode-mode] legacy 设置安装失败,尝试服务路由:" + String(error));
|
|
200
|
+
}
|
|
201
|
+
const ctxInject = ctx.inject;
|
|
202
|
+
if (typeof ctxInject !== "function") return recordInstall("none", INSTALL_NONE);
|
|
203
|
+
try {
|
|
204
|
+
ctxInject(["settings"], (sctx) => {
|
|
205
|
+
const sc = sctx;
|
|
206
|
+
const provider = typeof sc.get === "function" ? sc.get("settings") : sc.settings;
|
|
207
|
+
const install = provider?.installSection;
|
|
208
|
+
if (typeof install !== "function") {
|
|
209
|
+
console.warn("[dsh-vscode-mode] settings 服务无 installSection(DSH 版本 API 变化),section " + ns + " 降级为配置值");
|
|
210
|
+
recordInstall("none", INSTALL_NONE);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
install.call(provider, ctx, ns, schema, entry, hooks);
|
|
215
|
+
recordInstall("service", INSTALL_SERVICE);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.warn("[dsh-vscode-mode] settings.installSection 安装失败(" + String(error) + "),section " + ns + " 降级为配置值");
|
|
218
|
+
recordInstall("none", INSTALL_NONE);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.warn("[dsh-vscode-mode] settings 服务路由不可用(" + String(error) + ")");
|
|
223
|
+
return recordInstall("none", INSTALL_NONE);
|
|
224
|
+
}
|
|
225
|
+
if (observedInstall.strategy !== "unknown") return observedInstall.strategy;
|
|
226
|
+
return recordInstall("service", "已调度 settings 服务 installSection(等待 settings 就绪)");
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* 按命令目录构建 keybindings 显式键形状(每键独立默认值)。
|
|
230
|
+
* @author ddj 2026年08月26号
|
|
231
|
+
* @param z schemastery 命名空间
|
|
232
|
+
* @returns keybindings 字段形状
|
|
233
|
+
*/
|
|
234
|
+
function keybindingsShape(z) {
|
|
235
|
+
const shape = {};
|
|
236
|
+
for (const [id, chord] of Object.entries(KEYBINDING_DEFAULTS)) shape[id] = z.string().default(chord);
|
|
237
|
+
return shape;
|
|
238
|
+
}
|
|
153
239
|
function normalizeValue(value) {
|
|
154
240
|
if (typeof value !== "string" || value.trim() === "") return FILE_OPEN_DEFAULT;
|
|
155
241
|
return value.trim();
|
|
@@ -158,8 +244,8 @@ function configValue(config) {
|
|
|
158
244
|
return normalizeValue(config?.fileOpenTool);
|
|
159
245
|
}
|
|
160
246
|
/**
|
|
161
|
-
* 安装设置 section
|
|
162
|
-
* @author ddj 2026年08月24号
|
|
247
|
+
* 安装设置 section(版本自适应;任一策略不可用返回 false 不抛错)。
|
|
248
|
+
* @author ddj 2026年08月24号 / 2026年09月02号
|
|
163
249
|
* @param ctx DSH host 上下文
|
|
164
250
|
* @param ns 设置命名空间
|
|
165
251
|
* @param entry 初始设置值
|
|
@@ -168,14 +254,19 @@ function configValue(config) {
|
|
|
168
254
|
* @returns 是否成功安装
|
|
169
255
|
*/
|
|
170
256
|
async function installOpenSettingsSection(ctx, ns, entry, hooks, loader = loadSettingsDeps) {
|
|
171
|
-
|
|
257
|
+
let deps = null;
|
|
258
|
+
try {
|
|
259
|
+
deps = await loader();
|
|
260
|
+
} catch {}
|
|
172
261
|
if (!deps) return false;
|
|
173
|
-
const
|
|
262
|
+
const strategy = await runSettingsInstall(ctx, ns, deps.z.object({
|
|
174
263
|
fileOpenTool: deps.z.string().default(FILE_OPEN_DEFAULT),
|
|
175
264
|
keybindings: deps.z.object(keybindingsShape(deps.z)).default({ ...KEYBINDING_DEFAULTS })
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
|
|
265
|
+
}), entry, {
|
|
266
|
+
setSource: (source) => hooks.setSource(source),
|
|
267
|
+
onChange: hooks.onChange
|
|
268
|
+
}, loader);
|
|
269
|
+
return strategy === "legacy" || strategy === "service";
|
|
179
270
|
}
|
|
180
271
|
/**
|
|
181
272
|
* 注册设置命名空间,并提供 host 侧的读取与更新状态。
|
|
@@ -370,6 +461,101 @@ async function runPnpmInstall(ctx, profileDir) {
|
|
|
370
461
|
}
|
|
371
462
|
}
|
|
372
463
|
//#endregion
|
|
464
|
+
//#region src/dshVersion.ts
|
|
465
|
+
/**
|
|
466
|
+
* dsh-vscode-mode host — DSH 核心版本探测与区间匹配(版本适配机制的地基)。
|
|
467
|
+
* 约定:以运行时可解析的 @deepseek-ai/dsh-settings 包版本代表 DSH 核心版本——
|
|
468
|
+
* 核心包发布锁步同版(rc.2 / alpha.2 全树同版实证),且该包是插件动态依赖面、
|
|
469
|
+
* exports 已放行 ./package.json。解析失败按候选降级(dsh-web-app / dsh-base / dsh),
|
|
470
|
+
* 全部失败返回空串(报告显示"未探测",功能按能力探测运行)。
|
|
471
|
+
* 纯函数(parse / compare / inRange / familyLabel)可在 node 环境单测。
|
|
472
|
+
* 作者 ddj 2026-09-02
|
|
473
|
+
*/
|
|
474
|
+
const VERSION_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
475
|
+
/** 解析版本串;非法返回 null(不抛错)。 */
|
|
476
|
+
function parseDshVersion(input) {
|
|
477
|
+
const match = VERSION_RE.exec(input.trim());
|
|
478
|
+
if (!match) return null;
|
|
479
|
+
return {
|
|
480
|
+
major: Number(match[1]),
|
|
481
|
+
minor: Number(match[2]),
|
|
482
|
+
patch: Number(match[3]),
|
|
483
|
+
pre: match[4] ? match[4].split(".") : []
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
/** 预发布标识比较:纯数字按值;数字段 < 字母段;字母按字典序(semver 规则)。 */
|
|
487
|
+
function comparePreId(a, b) {
|
|
488
|
+
const numericA = /^\d+$/.test(a);
|
|
489
|
+
const numericB = /^\d+$/.test(b);
|
|
490
|
+
if (numericA && numericB) return Number(a) - Number(b);
|
|
491
|
+
if (numericA !== numericB) return numericA ? -1 : 1;
|
|
492
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
493
|
+
}
|
|
494
|
+
/** 比较两个已解析版本:a<b → -1;相等 → 0;a>b → 1。 */
|
|
495
|
+
function compareDshVersions(a, b) {
|
|
496
|
+
for (const key of [
|
|
497
|
+
"major",
|
|
498
|
+
"minor",
|
|
499
|
+
"patch"
|
|
500
|
+
]) if (a[key] !== b[key]) return a[key] < b[key] ? -1 : 1;
|
|
501
|
+
if (a.pre.length === b.pre.length && a.pre.every((part, index) => part === b.pre[index])) return 0;
|
|
502
|
+
if (a.pre.length === 0) return 1;
|
|
503
|
+
if (b.pre.length === 0) return -1;
|
|
504
|
+
const length = Math.max(a.pre.length, b.pre.length);
|
|
505
|
+
for (let i = 0; i < length; i++) {
|
|
506
|
+
const left = a.pre[i];
|
|
507
|
+
const right = b.pre[i];
|
|
508
|
+
if (left === void 0) return -1;
|
|
509
|
+
if (right === void 0) return 1;
|
|
510
|
+
const cmp = comparePreId(left, right);
|
|
511
|
+
if (cmp !== 0) return cmp;
|
|
512
|
+
}
|
|
513
|
+
return 0;
|
|
514
|
+
}
|
|
515
|
+
/** 版本是否落在区间内(入参解析失败一律不命中)。 */
|
|
516
|
+
function inDshRange(version, range) {
|
|
517
|
+
if (!version) return false;
|
|
518
|
+
if (range.from) {
|
|
519
|
+
const lower = parseDshVersion(range.from);
|
|
520
|
+
if (!lower || compareDshVersions(version, lower) < 0) return false;
|
|
521
|
+
}
|
|
522
|
+
if (range.to) {
|
|
523
|
+
const upper = parseDshVersion(range.to);
|
|
524
|
+
if (!upper || compareDshVersions(version, upper) > 0) return false;
|
|
525
|
+
}
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
/** 版本线标签:报告与文档展示版本归属;不可解析返回 '未知'。 */
|
|
529
|
+
function familyLabel(input) {
|
|
530
|
+
const version = parseDshVersion(input);
|
|
531
|
+
if (!version) return "未知";
|
|
532
|
+
if (inDshRange(version, { from: "0.1.2-alpha.1" })) return "0.1.2-alpha 及更新(设置 API=settings.installSection)";
|
|
533
|
+
return "0.1.0/0.1.1 rc 线(设置 API=installSettingsSection)";
|
|
534
|
+
}
|
|
535
|
+
/** 探测候选:按与插件运行环境相关性排序,逐个尝试解析 package.json。 */
|
|
536
|
+
const VERSION_CANDIDATES = [
|
|
537
|
+
"@deepseek-ai/dsh-settings/package.json",
|
|
538
|
+
"@deepseek-ai/dsh-web-app/package.json",
|
|
539
|
+
"@deepseek-ai/dsh-base/package.json",
|
|
540
|
+
"@deepseek-ai/dsh/package.json"
|
|
541
|
+
];
|
|
542
|
+
let detectedVersion;
|
|
543
|
+
/** 探测运行中 DSH 核心版本(模块级缓存;失败空串,不抛错)。 */
|
|
544
|
+
function detectDshVersion() {
|
|
545
|
+
if (detectedVersion !== void 0) return detectedVersion;
|
|
546
|
+
detectedVersion = "";
|
|
547
|
+
const require = createRequire(import.meta.url);
|
|
548
|
+
for (const specifier of VERSION_CANDIDATES) try {
|
|
549
|
+
const file = require.resolve(specifier);
|
|
550
|
+
const pkg = JSON.parse(readFileSync(file, "utf8"));
|
|
551
|
+
if (typeof pkg.version === "string" && parseDshVersion(pkg.version) !== null) {
|
|
552
|
+
detectedVersion = pkg.version;
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
} catch {}
|
|
556
|
+
return detectedVersion;
|
|
557
|
+
}
|
|
558
|
+
//#endregion
|
|
373
559
|
//#region src/shared/rpc.ts
|
|
374
560
|
/** webServer 精确路由。 */
|
|
375
561
|
const RPC_PATH = "/edrv/rpc";
|
|
@@ -485,6 +671,14 @@ function pluginCacheRoot(home = dshHome()) {
|
|
|
485
671
|
function workspaceCacheDir(workspaceId, home = dshHome()) {
|
|
486
672
|
return join(pluginCacheRoot(home), "workspace", workspaceId);
|
|
487
673
|
}
|
|
674
|
+
/** 用户级缓存目录(跨工作区共享;当前无实例,预留供全局偏好类缓存使用)。 */
|
|
675
|
+
function userCacheDir(home = dshHome()) {
|
|
676
|
+
return join(pluginCacheRoot(home), "user");
|
|
677
|
+
}
|
|
678
|
+
/** LSP provider 发现结果缓存文件(用户级:user/lsp-specs.v<schema>.json,跨会话/跨重启共享)。 */
|
|
679
|
+
function lspSpecCacheFile(home = dshHome(), schema = 4) {
|
|
680
|
+
return join(userCacheDir(home), "lsp-specs.v" + schema + ".json");
|
|
681
|
+
}
|
|
488
682
|
/** 插件日志根(~/.dsh/dsh-vscode-mode/logs,用户级)。 */
|
|
489
683
|
function pluginLogRoot(home = dshHome()) {
|
|
490
684
|
return join(home, PLUGIN_ID, "logs");
|
|
@@ -737,26 +931,63 @@ function detectGuards(ctx) {
|
|
|
737
931
|
note: duplicates[0] ?? "唯一装配"
|
|
738
932
|
}];
|
|
739
933
|
}
|
|
934
|
+
/** 已实测覆盖的最高 DSH 版本(适配矩阵上界,超过则提示,见 buildReport)。 */
|
|
935
|
+
const TESTED_DSH_MAX = "0.1.2-alpha.4";
|
|
936
|
+
/** 版本适配机制状态行:DSH 版本探测 + 设置 section 安装策略。 */
|
|
937
|
+
function versionAdapters(dshVersion) {
|
|
938
|
+
const adapters = [];
|
|
939
|
+
const parsed = parseDshVersion(dshVersion);
|
|
940
|
+
if (dshVersion === "") adapters.push({
|
|
941
|
+
name: "DSH 版本",
|
|
942
|
+
active: false,
|
|
943
|
+
note: "未探测到版本号(能力探测模式运行)"
|
|
944
|
+
});
|
|
945
|
+
else if (!parsed) adapters.push({
|
|
946
|
+
name: "DSH 版本",
|
|
947
|
+
active: false,
|
|
948
|
+
note: "无法解析版本号 " + dshVersion + "(能力探测模式运行)"
|
|
949
|
+
});
|
|
950
|
+
else adapters.push({
|
|
951
|
+
name: "DSH 版本",
|
|
952
|
+
active: true,
|
|
953
|
+
note: dshVersion + " · " + familyLabel(dshVersion)
|
|
954
|
+
});
|
|
955
|
+
const strategy = settingsInstallStrategy();
|
|
956
|
+
adapters.push({
|
|
957
|
+
name: "设置 section 安装(版本适配)",
|
|
958
|
+
active: strategy === "legacy" || strategy === "service",
|
|
959
|
+
note: settingsInstallNote()
|
|
960
|
+
});
|
|
961
|
+
return adapters;
|
|
962
|
+
}
|
|
740
963
|
/**
|
|
741
964
|
* 构建完整兼容性报告(RPC 与启动日志共用)。
|
|
742
|
-
* @author ddj 2026年08月24号
|
|
965
|
+
* @author ddj 2026年08月24号 / 2026年09月02号
|
|
743
966
|
* @param ctx DSH host 上下文
|
|
744
|
-
* @param options 测试注入:depsAvailable 跳过动态导入、version
|
|
967
|
+
* @param options 测试注入:depsAvailable 跳过动态导入、version 固定插件版本、dshVersion 固定 DSH 版本
|
|
745
968
|
* @returns 兼容性报告
|
|
746
969
|
*/
|
|
747
970
|
async function buildReport(ctx, options) {
|
|
748
971
|
const deps = options?.depsAvailable ?? await loadSettingsDeps() !== null;
|
|
972
|
+
const dshVersion = options?.dshVersion ?? detectDshVersion();
|
|
749
973
|
const external = detectExternal(ctx, deps);
|
|
750
974
|
const guards = detectGuards(ctx);
|
|
975
|
+
const adapters = versionAdapters(dshVersion);
|
|
751
976
|
const warnings = [];
|
|
752
977
|
if (!deps) warnings.push("未安装 @deepseek-ai/dsh-settings:文件打开工具设置持久化不可用(降级为配置值)");
|
|
978
|
+
if (settingsInstallStrategy() === "none") warnings.push("设置 section 安装不可用:" + settingsInstallNote());
|
|
979
|
+
const parsed = parseDshVersion(dshVersion);
|
|
980
|
+
const testedMax = parseDshVersion(TESTED_DSH_MAX);
|
|
981
|
+
if (parsed && testedMax && compareDshVersions(parsed, testedMax) > 0) warnings.push("DSH " + dshVersion + " 高于已实测版本(0.1.2-alpha.4):设置 API 按能力探测运行,异常时请回报适配矩阵");
|
|
753
982
|
for (const guard of guards) if (!guard.active && guard.note) warnings.push(guard.note);
|
|
754
983
|
return {
|
|
755
984
|
pluginVersion: options?.version ?? pluginVersionOf(),
|
|
756
985
|
external,
|
|
757
986
|
guards,
|
|
987
|
+
adapters,
|
|
758
988
|
warnings,
|
|
759
|
-
devForm: readDevForm()
|
|
989
|
+
devForm: readDevForm(),
|
|
990
|
+
dshVersion
|
|
760
991
|
};
|
|
761
992
|
}
|
|
762
993
|
//#endregion
|
|
@@ -4378,7 +4609,8 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
4378
4609
|
const info = await fs.stat(target);
|
|
4379
4610
|
if (!info || info.type !== "file") return {
|
|
4380
4611
|
ok: false,
|
|
4381
|
-
error: "文件不存在"
|
|
4612
|
+
error: "文件不存在",
|
|
4613
|
+
resolvedPath: fs.processPath(target)
|
|
4382
4614
|
};
|
|
4383
4615
|
if ((info.size ?? 0) > 8388608) return {
|
|
4384
4616
|
ok: false,
|
|
@@ -5453,6 +5685,50 @@ function disposeAllServers() {
|
|
|
5453
5685
|
running.clear();
|
|
5454
5686
|
}
|
|
5455
5687
|
//#endregion
|
|
5688
|
+
//#region src/shared/lsp.ts
|
|
5689
|
+
/** LSP semantic token 标准类型(host 归一化后,client 与 Monaco 共用)。 */
|
|
5690
|
+
const LSP_SEMANTIC_TOKEN_TYPES = [
|
|
5691
|
+
"namespace",
|
|
5692
|
+
"type",
|
|
5693
|
+
"class",
|
|
5694
|
+
"enum",
|
|
5695
|
+
"interface",
|
|
5696
|
+
"struct",
|
|
5697
|
+
"typeParameter",
|
|
5698
|
+
"parameter",
|
|
5699
|
+
"variable",
|
|
5700
|
+
"property",
|
|
5701
|
+
"enumMember",
|
|
5702
|
+
"event",
|
|
5703
|
+
"function",
|
|
5704
|
+
"method",
|
|
5705
|
+
"macro",
|
|
5706
|
+
"label",
|
|
5707
|
+
"comment",
|
|
5708
|
+
"string",
|
|
5709
|
+
"keyword",
|
|
5710
|
+
"number",
|
|
5711
|
+
"regexp",
|
|
5712
|
+
"operator",
|
|
5713
|
+
"decorator"
|
|
5714
|
+
];
|
|
5715
|
+
/** LSP semantic token 标准修饰符(host 归一化后,client 与 Monaco 共用)。 */
|
|
5716
|
+
const LSP_SEMANTIC_TOKEN_MODIFIERS = [
|
|
5717
|
+
"declaration",
|
|
5718
|
+
"definition",
|
|
5719
|
+
"readonly",
|
|
5720
|
+
"static",
|
|
5721
|
+
"deprecated",
|
|
5722
|
+
"abstract",
|
|
5723
|
+
"async",
|
|
5724
|
+
"modification",
|
|
5725
|
+
"documentation",
|
|
5726
|
+
"defaultLibrary",
|
|
5727
|
+
"global",
|
|
5728
|
+
"local",
|
|
5729
|
+
"mutable"
|
|
5730
|
+
];
|
|
5731
|
+
//#endregion
|
|
5456
5732
|
//#region src/lsp/client.ts
|
|
5457
5733
|
/**
|
|
5458
5734
|
* dsh-vscode-mode host — LSP 客户端(JSON-RPC over stdio)。
|
|
@@ -5484,7 +5760,10 @@ function createLspClient(transport, events = {}) {
|
|
|
5484
5760
|
references: false,
|
|
5485
5761
|
documentSymbol: false,
|
|
5486
5762
|
workspaceSymbol: false,
|
|
5487
|
-
hover: false
|
|
5763
|
+
hover: false,
|
|
5764
|
+
semanticTokens: false,
|
|
5765
|
+
semanticTokenTypes: [],
|
|
5766
|
+
semanticTokenModifiers: []
|
|
5488
5767
|
};
|
|
5489
5768
|
const log = (line) => events.onLog?.(line);
|
|
5490
5769
|
createFrameParser();
|
|
@@ -5614,16 +5893,30 @@ function createLspClient(transport, events = {}) {
|
|
|
5614
5893
|
definition: { dynamicRegistration: false },
|
|
5615
5894
|
references: { dynamicRegistration: false },
|
|
5616
5895
|
documentSymbol: { dynamicRegistration: false },
|
|
5617
|
-
hover: { dynamicRegistration: false }
|
|
5896
|
+
hover: { dynamicRegistration: false },
|
|
5897
|
+
semanticTokens: {
|
|
5898
|
+
dynamicRegistration: false,
|
|
5899
|
+
requests: {
|
|
5900
|
+
range: false,
|
|
5901
|
+
full: { delta: false }
|
|
5902
|
+
},
|
|
5903
|
+
tokenTypes: [...LSP_SEMANTIC_TOKEN_TYPES],
|
|
5904
|
+
tokenModifiers: [...LSP_SEMANTIC_TOKEN_MODIFIERS],
|
|
5905
|
+
formats: ["relative"]
|
|
5906
|
+
}
|
|
5618
5907
|
}
|
|
5619
5908
|
}
|
|
5620
5909
|
}))?.capabilities ?? {};
|
|
5910
|
+
const legend = caps.semanticTokensProvider?.legend;
|
|
5621
5911
|
serverCapabilities = {
|
|
5622
5912
|
definition: Boolean(caps.definitionProvider),
|
|
5623
5913
|
references: Boolean(caps.referencesProvider),
|
|
5624
5914
|
documentSymbol: Boolean(caps.documentSymbolProvider),
|
|
5625
5915
|
workspaceSymbol: Boolean(caps.workspaceSymbolProvider),
|
|
5626
|
-
hover: Boolean(caps.hoverProvider)
|
|
5916
|
+
hover: Boolean(caps.hoverProvider),
|
|
5917
|
+
semanticTokens: Boolean(caps.semanticTokensProvider),
|
|
5918
|
+
semanticTokenTypes: Array.isArray(legend?.tokenTypes) ? legend.tokenTypes.filter((item) => typeof item === "string") : [...LSP_SEMANTIC_TOKEN_TYPES],
|
|
5919
|
+
semanticTokenModifiers: Array.isArray(legend?.tokenModifiers) ? legend.tokenModifiers.filter((item) => typeof item === "string") : [...LSP_SEMANTIC_TOKEN_MODIFIERS]
|
|
5627
5920
|
};
|
|
5628
5921
|
client.notify("initialized", {});
|
|
5629
5922
|
events.onReady?.(serverCapabilities);
|
|
@@ -5697,7 +5990,10 @@ function createLspServer(spec, root, languageId, logger) {
|
|
|
5697
5990
|
references: false,
|
|
5698
5991
|
documentSymbol: false,
|
|
5699
5992
|
workspaceSymbol: false,
|
|
5700
|
-
hover: false
|
|
5993
|
+
hover: false,
|
|
5994
|
+
semanticTokens: false,
|
|
5995
|
+
semanticTokenTypes: [...LSP_SEMANTIC_TOKEN_TYPES],
|
|
5996
|
+
semanticTokenModifiers: [...LSP_SEMANTIC_TOKEN_MODIFIERS]
|
|
5701
5997
|
};
|
|
5702
5998
|
let disposed = false;
|
|
5703
5999
|
let readyWait = Promise.resolve(false);
|
|
@@ -5708,9 +6004,15 @@ function createLspServer(spec, root, languageId, logger) {
|
|
|
5708
6004
|
source: spec.kind,
|
|
5709
6005
|
phase: "idle",
|
|
5710
6006
|
reason: spec.ready ? void 0 : spec.reason,
|
|
5711
|
-
root
|
|
6007
|
+
root,
|
|
6008
|
+
version: spec.version,
|
|
6009
|
+
providerName: spec.providerName ?? (spec.version ? "语言服务器扩展" : void 0)
|
|
5712
6010
|
};
|
|
5713
6011
|
const log = (line) => logger?.("[" + languageId + "@" + root + "] " + line);
|
|
6012
|
+
const providerInfo = {
|
|
6013
|
+
version: spec.version,
|
|
6014
|
+
providerName: spec.providerName ?? (spec.version ? "EmmyLua" : void 0)
|
|
6015
|
+
};
|
|
5714
6016
|
const setPhase = (next, reason) => {
|
|
5715
6017
|
phase = next;
|
|
5716
6018
|
lastStatus = {
|
|
@@ -5719,6 +6021,7 @@ function createLspServer(spec, root, languageId, logger) {
|
|
|
5719
6021
|
phase: next,
|
|
5720
6022
|
reason,
|
|
5721
6023
|
root,
|
|
6024
|
+
...providerInfo,
|
|
5722
6025
|
...progressFields()
|
|
5723
6026
|
};
|
|
5724
6027
|
server.onStateChange?.(lastStatus);
|
|
@@ -5913,6 +6216,17 @@ function createLspServer(spec, root, languageId, logger) {
|
|
|
5913
6216
|
if (!result || !result.contents) return null;
|
|
5914
6217
|
return { contents: stringifyHoverContents(result.contents) };
|
|
5915
6218
|
},
|
|
6219
|
+
/** 查询全文 semantic tokens,并将服务器 legend 归一到插件固定 legend。 */
|
|
6220
|
+
async semanticTokens(path) {
|
|
6221
|
+
const doc = docs.get(path);
|
|
6222
|
+
if (!doc || !await readyWait || !capabilities.semanticTokens) return null;
|
|
6223
|
+
const result = await ensureClient().request("textDocument/semanticTokens/full", { textDocument: { uri: doc.uri } });
|
|
6224
|
+
if (!result || !Array.isArray(result.data)) return null;
|
|
6225
|
+
return {
|
|
6226
|
+
data: normalizeSemanticData(result.data, capabilities.semanticTokenTypes ?? [], capabilities.semanticTokenModifiers ?? []),
|
|
6227
|
+
resultId: typeof result.resultId === "string" ? result.resultId : void 0
|
|
6228
|
+
};
|
|
6229
|
+
},
|
|
5916
6230
|
async dispose() {
|
|
5917
6231
|
disposed = true;
|
|
5918
6232
|
resolveReady?.(false);
|
|
@@ -6021,6 +6335,39 @@ function toInt(value) {
|
|
|
6021
6335
|
const n = Number(value);
|
|
6022
6336
|
return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
|
|
6023
6337
|
}
|
|
6338
|
+
/** 把服务器 legend 下的 5 元组流转换为插件固定 legend 下的 5 元组流。 */
|
|
6339
|
+
function normalizeSemanticData(data, types, modifiers) {
|
|
6340
|
+
const out = [];
|
|
6341
|
+
let previousLine = 0;
|
|
6342
|
+
let previousCharacter = 0;
|
|
6343
|
+
let outputLine = 0;
|
|
6344
|
+
let outputCharacter = 0;
|
|
6345
|
+
for (let i = 0; i + 4 < data.length; i += 5) {
|
|
6346
|
+
const deltaLine = toInt(data[i]);
|
|
6347
|
+
const deltaCharacter = toInt(data[i + 1]);
|
|
6348
|
+
const length = toInt(data[i + 2]);
|
|
6349
|
+
const sourceType = types[toInt(data[i + 3])] ?? "variable";
|
|
6350
|
+
const sourceModifiers = toInt(data[i + 4]);
|
|
6351
|
+
const line = previousLine + deltaLine;
|
|
6352
|
+
const character = deltaLine === 0 ? previousCharacter + deltaCharacter : deltaCharacter;
|
|
6353
|
+
const typeIndex = Math.max(0, LSP_SEMANTIC_TOKEN_TYPES.indexOf(sourceType));
|
|
6354
|
+
let modifierBits = 0;
|
|
6355
|
+
for (let bit = 0; bit < modifiers.length; bit++) {
|
|
6356
|
+
if ((sourceModifiers & 1 << bit) === 0) continue;
|
|
6357
|
+
const modifier = modifiers[bit];
|
|
6358
|
+
const targetBit = LSP_SEMANTIC_TOKEN_MODIFIERS.indexOf(modifier);
|
|
6359
|
+
if (targetBit >= 0) modifierBits |= 1 << targetBit;
|
|
6360
|
+
}
|
|
6361
|
+
const outputDeltaLine = line - outputLine;
|
|
6362
|
+
const outputDeltaCharacter = outputDeltaLine === 0 ? character - outputCharacter : character;
|
|
6363
|
+
out.push(outputDeltaLine, outputDeltaCharacter, length, typeIndex, modifierBits);
|
|
6364
|
+
previousLine = line;
|
|
6365
|
+
previousCharacter = character;
|
|
6366
|
+
outputLine = line;
|
|
6367
|
+
outputCharacter = character;
|
|
6368
|
+
}
|
|
6369
|
+
return out;
|
|
6370
|
+
}
|
|
6024
6371
|
//#endregion
|
|
6025
6372
|
//#region src/lsp/manager.ts
|
|
6026
6373
|
/**
|
|
@@ -6079,6 +6426,13 @@ function createLspManager(logger) {
|
|
|
6079
6426
|
releaseRoot(root) {
|
|
6080
6427
|
for (const entry of [...entries.values()]) if (entry.server.root === root) dropEntry(entry);
|
|
6081
6428
|
},
|
|
6429
|
+
reset(root, languageId) {
|
|
6430
|
+
const entry = entries.get(keyOf(root, languageId));
|
|
6431
|
+
if (entry) dropEntry(entry);
|
|
6432
|
+
},
|
|
6433
|
+
resetLanguage(languageId) {
|
|
6434
|
+
for (const entry of [...entries.values()]) if (entry.server.languageId === languageId) dropEntry(entry);
|
|
6435
|
+
},
|
|
6082
6436
|
statusAll() {
|
|
6083
6437
|
return [...entries.values()].map((entry) => entry.server.status());
|
|
6084
6438
|
},
|
|
@@ -6388,7 +6742,9 @@ function extensionProviderFor(languageId) {
|
|
|
6388
6742
|
kind: "extension",
|
|
6389
6743
|
argv: p.argv,
|
|
6390
6744
|
cwd: p.cwd,
|
|
6391
|
-
ready: true
|
|
6745
|
+
ready: true,
|
|
6746
|
+
version: p.version,
|
|
6747
|
+
providerName: p.version ? "EmmyLua" : "语言服务器扩展"
|
|
6392
6748
|
};
|
|
6393
6749
|
}
|
|
6394
6750
|
/** PATH 中查找可执行文件(不含扩展名校验,交由 spawn 报错)。 */
|
|
@@ -6416,6 +6772,25 @@ function findDotnet() {
|
|
|
6416
6772
|
for (const c of ["C:/Program Files/dotnet/dotnet.exe", "C:/Program Files (x86)/dotnet/dotnet.exe"]) if (existsSync(c)) return c;
|
|
6417
6773
|
return null;
|
|
6418
6774
|
}
|
|
6775
|
+
/** EmmyLua 自动发现候选(tangzx.emmylua-* / server/emmylua_ls)。 */
|
|
6776
|
+
function candidateEmmyLua(home = dshHome()) {
|
|
6777
|
+
const candidates = [];
|
|
6778
|
+
for (const dir of vscodeExtensionsDirs(home)) for (const extDir of listMatchingDirs(dir, "tangzx.emmylua-")) {
|
|
6779
|
+
const server = join(extDir, "server", "emmylua_ls" + exeSuffix());
|
|
6780
|
+
if (!existsSync(server)) continue;
|
|
6781
|
+
let version;
|
|
6782
|
+
try {
|
|
6783
|
+
const manifest = JSON.parse(readFileSync(join(extDir, "package.json"), "utf8"));
|
|
6784
|
+
if (typeof manifest.version === "string") version = manifest.version;
|
|
6785
|
+
} catch (error) {}
|
|
6786
|
+
candidates.push({
|
|
6787
|
+
path: server,
|
|
6788
|
+
version
|
|
6789
|
+
});
|
|
6790
|
+
}
|
|
6791
|
+
candidates.sort((a, b) => (b.version ?? "").localeCompare(a.version ?? "", void 0, { numeric: true }) || a.path.localeCompare(b.path));
|
|
6792
|
+
return candidates[0] ?? null;
|
|
6793
|
+
}
|
|
6419
6794
|
/** LuaLS 自动发现候选(VSCode 扩展目录 + extmgr 目录下 sumneko.lua-* 的 server/bin 可执行)。 */
|
|
6420
6795
|
function candidateLuaServers(home = dshHome()) {
|
|
6421
6796
|
const plat = platformServerDir();
|
|
@@ -6497,30 +6872,41 @@ function resolveLuaProvider(manual, home = dshHome()) {
|
|
|
6497
6872
|
cwd: manualResolved.cwd,
|
|
6498
6873
|
ready: true
|
|
6499
6874
|
};
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6875
|
+
return resolveCached("lua", manual, home, (h) => {
|
|
6876
|
+
const ext = extensionProviderFor("lua");
|
|
6877
|
+
if (ext) return ext;
|
|
6878
|
+
const emmy = candidateEmmyLua(h);
|
|
6879
|
+
if (emmy) return {
|
|
6880
|
+
...base,
|
|
6881
|
+
kind: "extension",
|
|
6882
|
+
argv: [emmy.path],
|
|
6883
|
+
ready: true,
|
|
6884
|
+
version: emmy.version,
|
|
6885
|
+
providerName: "EmmyLua"
|
|
6886
|
+
};
|
|
6887
|
+
const candidates = candidateLuaServers(h);
|
|
6888
|
+
if (candidates.length) return {
|
|
6889
|
+
...base,
|
|
6890
|
+
kind: "discover",
|
|
6891
|
+
argv: [candidates[0]],
|
|
6892
|
+
cwd: void 0,
|
|
6893
|
+
ready: true
|
|
6894
|
+
};
|
|
6895
|
+
const inPath = findInPath("lua-language-server" + exeSuffix());
|
|
6896
|
+
if (inPath) return {
|
|
6897
|
+
...base,
|
|
6898
|
+
kind: "discover",
|
|
6899
|
+
argv: [inPath],
|
|
6900
|
+
ready: true
|
|
6901
|
+
};
|
|
6902
|
+
return {
|
|
6903
|
+
...base,
|
|
6904
|
+
kind: "none",
|
|
6905
|
+
argv: [],
|
|
6906
|
+
ready: false,
|
|
6907
|
+
reason: manualResolved.reason ?? "未发现 lua-language-server(可安装 LuaLS 或手动指定路径)"
|
|
6908
|
+
};
|
|
6909
|
+
});
|
|
6524
6910
|
}
|
|
6525
6911
|
/**
|
|
6526
6912
|
* 解析 C# provider(优先级:手动 > 扩展源 > 自动发现;需 dotnet + Roslyn dll;或 OmniSharp)。
|
|
@@ -6539,40 +6925,163 @@ function resolveCSharpProvider(manual, home = dshHome()) {
|
|
|
6539
6925
|
cwd: manualResolved.cwd,
|
|
6540
6926
|
ready: true
|
|
6541
6927
|
};
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
|
|
6928
|
+
return resolveCached("csharp", manual, home, (h) => {
|
|
6929
|
+
const ext = extensionProviderFor("csharp");
|
|
6930
|
+
if (ext) return ext;
|
|
6931
|
+
const discovered = candidateCSharpServers(h);
|
|
6932
|
+
if (discovered.roslynDll && discovered.dotnet) return {
|
|
6933
|
+
...base,
|
|
6934
|
+
kind: "discover",
|
|
6935
|
+
argv: [
|
|
6936
|
+
discovered.dotnet,
|
|
6937
|
+
discovered.roslynDll,
|
|
6938
|
+
"--stdio"
|
|
6939
|
+
],
|
|
6940
|
+
ready: true
|
|
6941
|
+
};
|
|
6942
|
+
if (discovered.omnisharpExe) return {
|
|
6943
|
+
...base,
|
|
6944
|
+
kind: "discover",
|
|
6945
|
+
argv: [discovered.omnisharpExe, "--stdio"],
|
|
6946
|
+
ready: true
|
|
6947
|
+
};
|
|
6948
|
+
const reasons = [];
|
|
6949
|
+
if (!discovered.dotnet) reasons.push("未找到 dotnet");
|
|
6950
|
+
if (!discovered.roslynDll && !discovered.omnisharpExe) reasons.push("未发现 ms-dotnettools.csharp 扩展");
|
|
6951
|
+
return {
|
|
6952
|
+
...base,
|
|
6953
|
+
kind: "none",
|
|
6954
|
+
argv: [],
|
|
6955
|
+
ready: false,
|
|
6956
|
+
reason: reasons.join(";") + "(可手动指定 Roslyn/OmniSharp 入口)"
|
|
6957
|
+
};
|
|
6958
|
+
});
|
|
6571
6959
|
}
|
|
6572
6960
|
const PROVIDER_RESOLVERS = {
|
|
6573
6961
|
lua: resolveLuaProvider,
|
|
6574
6962
|
csharp: resolveCSharpProvider
|
|
6575
6963
|
};
|
|
6964
|
+
/** 进程级缓存(home|languageId → 条目),同一 DSH home 的不同会话共享。 */
|
|
6965
|
+
const specCache = /* @__PURE__ */ new Map();
|
|
6966
|
+
/** 磁盘缓存是否已载入本进程(home → loaded),不同 profile 不串缓存。 */
|
|
6967
|
+
const diskLoaded = /* @__PURE__ */ new Set();
|
|
6968
|
+
/** 自动发现成功结果的复用时长;过期后才检查目录指纹。 */
|
|
6969
|
+
const SPEC_CACHE_TTL_MS = 18e5;
|
|
6970
|
+
/** 未发现结果的短缓存时长,避免缺失服务器时反复扫描 PATH。 */
|
|
6971
|
+
const SPEC_NEGATIVE_CACHE_MS = 6e4;
|
|
6972
|
+
/** 生成带 home 的缓存键,避免不同 profile 的 provider 结果串用。 */
|
|
6973
|
+
function cacheEntryKey(home, languageId) {
|
|
6974
|
+
return home + "|" + languageId;
|
|
6975
|
+
}
|
|
6976
|
+
/**
|
|
6977
|
+
* 构造缓存 key:语言 + 手动配置(manual 结果不缓存,故只参与"是否走缓存"判定)。
|
|
6978
|
+
* @author ddj 2026年08月28号
|
|
6979
|
+
* @param languageId 语言 id
|
|
6980
|
+
* @param manual 手动配置
|
|
6981
|
+
* @returns 缓存 key(manual 有效时返回空串表示跳过缓存)
|
|
6982
|
+
*/
|
|
6983
|
+
function cacheKeyOf(languageId, manual) {
|
|
6984
|
+
const pathValue = manual?.path?.trim() ?? "";
|
|
6985
|
+
const commandValue = manual?.command?.trim() ?? "";
|
|
6986
|
+
if (pathValue || commandValue) return "";
|
|
6987
|
+
return languageId;
|
|
6988
|
+
}
|
|
6989
|
+
/**
|
|
6990
|
+
* 环境指纹:扩展目录清单 + 平台。目录增删/换平台即失效(防缓存陈旧指向已删扩展)。
|
|
6991
|
+
* @author ddj 2026年08月28号
|
|
6992
|
+
* @param home DSH home
|
|
6993
|
+
* @returns 指纹串
|
|
6994
|
+
*/
|
|
6995
|
+
function fingerprintOf(home) {
|
|
6996
|
+
const marks = vscodeExtensionsDirs(home).map((dir) => {
|
|
6997
|
+
try {
|
|
6998
|
+
return readdirSync(dir).sort().join(",");
|
|
6999
|
+
} catch (error) {
|
|
7000
|
+
return "?";
|
|
7001
|
+
}
|
|
7002
|
+
});
|
|
7003
|
+
return process.platform + "|" + marks.join("|");
|
|
7004
|
+
}
|
|
7005
|
+
/**
|
|
7006
|
+
* 读取磁盘缓存(幂等:每进程只载一次;schema 文件名已隔离版本)。
|
|
7007
|
+
* @author ddj 2026年08月28号
|
|
7008
|
+
* @param home DSH home
|
|
7009
|
+
*/
|
|
7010
|
+
function loadDiskCache(home) {
|
|
7011
|
+
if (diskLoaded.has(home)) return;
|
|
7012
|
+
diskLoaded.add(home);
|
|
7013
|
+
const file = lspSpecCacheFile(home);
|
|
7014
|
+
try {
|
|
7015
|
+
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
7016
|
+
for (const [languageId, entry] of Object.entries(raw)) if (entry && typeof entry === "object" && entry.spec && typeof entry.fingerprint === "string") specCache.set(cacheEntryKey(home, languageId), entry);
|
|
7017
|
+
} catch (error) {}
|
|
7018
|
+
}
|
|
7019
|
+
/**
|
|
7020
|
+
* 写回磁盘缓存(best-effort;写失败不影响本次解析结果)。
|
|
7021
|
+
* @author ddj 2026年08月28号
|
|
7022
|
+
* @param home DSH home
|
|
7023
|
+
*/
|
|
7024
|
+
function saveDiskCache(home) {
|
|
7025
|
+
const file = lspSpecCacheFile(home);
|
|
7026
|
+
try {
|
|
7027
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
7028
|
+
const out = {};
|
|
7029
|
+
const prefix = home + "|";
|
|
7030
|
+
for (const [key, entry] of specCache) if (key.startsWith(prefix)) out[key.slice(prefix.length)] = entry;
|
|
7031
|
+
writeFileSync(file, JSON.stringify(out), "utf8");
|
|
7032
|
+
} catch (error) {}
|
|
7033
|
+
}
|
|
7034
|
+
/**
|
|
7035
|
+
* 带缓存执行 provider 解析(自动发现路径专用)。
|
|
7036
|
+
* @author ddj 2026年08月28号
|
|
7037
|
+
* @param languageId 语言 id
|
|
7038
|
+
* @param manual 手动配置
|
|
7039
|
+
* @param home DSH home
|
|
7040
|
+
* @param resolve 真解析函数(未命中时调用)
|
|
7041
|
+
* @returns provider 规格
|
|
7042
|
+
*/
|
|
7043
|
+
function resolveCached(languageId, manual, home, resolve) {
|
|
7044
|
+
const key = cacheKeyOf(languageId, manual);
|
|
7045
|
+
if (!key) return resolve(home);
|
|
7046
|
+
loadDiskCache(home);
|
|
7047
|
+
const fullKey = cacheEntryKey(home, key);
|
|
7048
|
+
const hit = specCache.get(fullKey);
|
|
7049
|
+
if (hit && isCacheFresh(hit)) return hit.spec;
|
|
7050
|
+
const fingerprint = fingerprintOf(home);
|
|
7051
|
+
if (hit && hit.fingerprint === fingerprint) {
|
|
7052
|
+
hit.at = Date.now();
|
|
7053
|
+
return hit.spec;
|
|
7054
|
+
}
|
|
7055
|
+
const spec = resolve(home);
|
|
7056
|
+
specCache.set(fullKey, {
|
|
7057
|
+
spec,
|
|
7058
|
+
fingerprint,
|
|
7059
|
+
at: Date.now()
|
|
7060
|
+
});
|
|
7061
|
+
saveDiskCache(home);
|
|
7062
|
+
return spec;
|
|
7063
|
+
}
|
|
7064
|
+
/** 判断缓存条目是否仍可直接复用,不触发扩展目录扫描。 */
|
|
7065
|
+
function isCacheFresh(entry) {
|
|
7066
|
+
const age = Date.now() - entry.at;
|
|
7067
|
+
const ttl = entry.spec.kind === "none" ? SPEC_NEGATIVE_CACHE_MS : SPEC_CACHE_TTL_MS;
|
|
7068
|
+
if (age < 0 || age >= ttl) return false;
|
|
7069
|
+
const executable = entry.spec.argv[0];
|
|
7070
|
+
return !entry.spec.ready || !executable || existsSync(executable);
|
|
7071
|
+
}
|
|
7072
|
+
/**
|
|
7073
|
+
* 清空 provider 发现缓存(扩展安装/卸载、配置变更后调用;进程级 + 删磁盘文件)。
|
|
7074
|
+
* @author ddj 2026年08月28号
|
|
7075
|
+
* @param home DSH home(缺省真实)
|
|
7076
|
+
*/
|
|
7077
|
+
function clearProviderCache(home = dshHome()) {
|
|
7078
|
+
const prefix = home + "|";
|
|
7079
|
+
for (const key of specCache.keys()) if (key.startsWith(prefix)) specCache.delete(key);
|
|
7080
|
+
diskLoaded.delete(home);
|
|
7081
|
+
try {
|
|
7082
|
+
rmSync(lspSpecCacheFile(home), { force: true });
|
|
7083
|
+
} catch (error) {}
|
|
7084
|
+
}
|
|
6576
7085
|
//#endregion
|
|
6577
7086
|
//#region src/lsp/config.ts
|
|
6578
7087
|
const LSP_SETTINGS_NS = "dsh-vscode-mode.languageServers";
|
|
@@ -6736,6 +7245,21 @@ function createDocTracker() {
|
|
|
6736
7245
|
}
|
|
6737
7246
|
sessionDocs.delete(sessionId);
|
|
6738
7247
|
return [...toRelease];
|
|
7248
|
+
},
|
|
7249
|
+
/** 清除指定语言的文档归属,让当前客户端重新同步时重新 acquire。 */
|
|
7250
|
+
resetLanguage(languageId) {
|
|
7251
|
+
for (const [docKey, owners] of openDocs) {
|
|
7252
|
+
if (!docKey.includes("|" + languageId + "|")) continue;
|
|
7253
|
+
for (const sessionId of owners) forgetDoc(sessionId, docKey);
|
|
7254
|
+
openDocs.delete(docKey);
|
|
7255
|
+
}
|
|
7256
|
+
for (const key of [...rootLangRefs.keys()]) if (key.endsWith("|" + languageId)) rootLangRefs.delete(key);
|
|
7257
|
+
},
|
|
7258
|
+
/** 返回指定会话已打开的文档路径,用于重检测后恢复同步。 */
|
|
7259
|
+
pathsFor(sessionId, languageId) {
|
|
7260
|
+
const docs = sessionDocs.get(sessionId);
|
|
7261
|
+
if (!docs) return [];
|
|
7262
|
+
return [...docs].filter((key) => key.includes("|" + languageId + "|")).map((key) => key.slice(key.lastIndexOf("|") + 1));
|
|
6739
7263
|
}
|
|
6740
7264
|
};
|
|
6741
7265
|
}
|
|
@@ -6768,6 +7292,39 @@ function createLspRpc(deps) {
|
|
|
6768
7292
|
...location,
|
|
6769
7293
|
root
|
|
6770
7294
|
}));
|
|
7295
|
+
/** 当前 provider 检测结论 → 设置页 idle 状态(不启动 server)。 */
|
|
7296
|
+
const detectedStatus = (languageId, root) => {
|
|
7297
|
+
const spec = resolveProviderSpec(ctx, pluginConfig, languageId);
|
|
7298
|
+
return {
|
|
7299
|
+
languageId,
|
|
7300
|
+
source: spec.kind,
|
|
7301
|
+
phase: "idle",
|
|
7302
|
+
reason: spec.ready ? void 0 : spec.reason,
|
|
7303
|
+
version: spec.version,
|
|
7304
|
+
providerName: spec.providerName,
|
|
7305
|
+
root
|
|
7306
|
+
};
|
|
7307
|
+
};
|
|
7308
|
+
/**
|
|
7309
|
+
* 清发现缓存并重置旧 server(保留 tracker 文档记录);
|
|
7310
|
+
* 下一次模型同步会按最新配置重新 acquire,已打开文档无需重开。
|
|
7311
|
+
* 作者 ddj 2026年09月02号
|
|
7312
|
+
* @param languageId 语言 id
|
|
7313
|
+
* @param sessionId 会话语境(缺省重置全部工作区该语言)
|
|
7314
|
+
* @returns 检测后的 idle 状态列表
|
|
7315
|
+
*/
|
|
7316
|
+
const redetectLanguage = async (languageId, sessionId) => {
|
|
7317
|
+
clearProviderCache();
|
|
7318
|
+
if (sessionId) {
|
|
7319
|
+
const sc = await rootOf(sessionId);
|
|
7320
|
+
if ("err" in sc) return [];
|
|
7321
|
+
manager.reset(sc.root, languageId);
|
|
7322
|
+
return [detectedStatus(languageId, sc.root)];
|
|
7323
|
+
}
|
|
7324
|
+
const roots = [...new Set(manager.statusAll().filter((s) => s.languageId === languageId).map((s) => s.root))];
|
|
7325
|
+
manager.resetLanguage(languageId);
|
|
7326
|
+
return roots.length ? roots.map((root) => detectedStatus(languageId, root)) : [detectedStatus(languageId)];
|
|
7327
|
+
};
|
|
6771
7328
|
const disposeSession = (sessionId) => {
|
|
6772
7329
|
for (const rl of tracker.closeSession(sessionId)) {
|
|
6773
7330
|
const sep = rl.lastIndexOf("|");
|
|
@@ -6825,13 +7382,15 @@ function createLspRpc(deps) {
|
|
|
6825
7382
|
error: "设置服务不可用"
|
|
6826
7383
|
};
|
|
6827
7384
|
await settings.update(LSP_SETTINGS_NS, { [lang]: target });
|
|
7385
|
+
const servers = await redetectLanguage(lang);
|
|
6828
7386
|
const stored = (settings.describe?.({ redactSecrets: true })?.find((item) => item.ns === LSP_SETTINGS_NS))?.value;
|
|
6829
7387
|
return {
|
|
6830
7388
|
ok: true,
|
|
6831
7389
|
config: {
|
|
6832
7390
|
...configFromPlugin(pluginConfig),
|
|
6833
7391
|
...stored
|
|
6834
|
-
}
|
|
7392
|
+
},
|
|
7393
|
+
servers
|
|
6835
7394
|
};
|
|
6836
7395
|
} catch (error) {
|
|
6837
7396
|
return {
|
|
@@ -7006,6 +7565,51 @@ function createLspRpc(deps) {
|
|
|
7006
7565
|
};
|
|
7007
7566
|
}
|
|
7008
7567
|
},
|
|
7568
|
+
"edrv.lsp.semanticTokens": async (args) => {
|
|
7569
|
+
const lang = langOfPath(args.path);
|
|
7570
|
+
if (!lang) return {
|
|
7571
|
+
ok: true,
|
|
7572
|
+
tokens: void 0
|
|
7573
|
+
};
|
|
7574
|
+
const sc = await rootOf(args.sessionId);
|
|
7575
|
+
if ("err" in sc) return {
|
|
7576
|
+
ok: false,
|
|
7577
|
+
error: sc.err
|
|
7578
|
+
};
|
|
7579
|
+
try {
|
|
7580
|
+
const server = serverOf(sc.root, lang);
|
|
7581
|
+
if (!server) return {
|
|
7582
|
+
ok: true,
|
|
7583
|
+
tokens: void 0
|
|
7584
|
+
};
|
|
7585
|
+
return {
|
|
7586
|
+
ok: true,
|
|
7587
|
+
tokens: await server.semanticTokens(args.path) ?? void 0
|
|
7588
|
+
};
|
|
7589
|
+
} catch (error) {
|
|
7590
|
+
return {
|
|
7591
|
+
ok: false,
|
|
7592
|
+
error: "LSP 语义分色查询失败:" + String(error)
|
|
7593
|
+
};
|
|
7594
|
+
}
|
|
7595
|
+
},
|
|
7596
|
+
"edrv.lsp.redetect": async (args) => {
|
|
7597
|
+
if (!LSP_LANGUAGES.includes(args.languageId)) return {
|
|
7598
|
+
ok: false,
|
|
7599
|
+
error: "不支持的语言:" + args.languageId
|
|
7600
|
+
};
|
|
7601
|
+
try {
|
|
7602
|
+
return {
|
|
7603
|
+
ok: true,
|
|
7604
|
+
servers: await redetectLanguage(args.languageId)
|
|
7605
|
+
};
|
|
7606
|
+
} catch (error) {
|
|
7607
|
+
return {
|
|
7608
|
+
ok: false,
|
|
7609
|
+
error: "重新检测失败:" + String(error)
|
|
7610
|
+
};
|
|
7611
|
+
}
|
|
7612
|
+
},
|
|
7009
7613
|
"edrv.lsp.ext.list": async () => ({
|
|
7010
7614
|
ok: true,
|
|
7011
7615
|
extensions: listInstalled()
|
|
@@ -7032,6 +7636,7 @@ function createLspRpc(deps) {
|
|
|
7032
7636
|
ok: false,
|
|
7033
7637
|
error: "缺少安装源(vsixPath 或 namespace+name)"
|
|
7034
7638
|
};
|
|
7639
|
+
clearProviderCache();
|
|
7035
7640
|
return {
|
|
7036
7641
|
ok: true,
|
|
7037
7642
|
extension: info
|
|
@@ -7044,7 +7649,9 @@ function createLspRpc(deps) {
|
|
|
7044
7649
|
}
|
|
7045
7650
|
},
|
|
7046
7651
|
"edrv.lsp.ext.uninstall": async (args) => {
|
|
7047
|
-
|
|
7652
|
+
const ok = uninstall(args.id);
|
|
7653
|
+
if (ok) clearProviderCache();
|
|
7654
|
+
return ok ? { ok: true } : {
|
|
7048
7655
|
ok: false,
|
|
7049
7656
|
error: "扩展未安装:" + args.id
|
|
7050
7657
|
};
|
|
@@ -7063,6 +7670,7 @@ function createLspRpc(deps) {
|
|
|
7063
7670
|
error: "扩展未安装:" + args.id
|
|
7064
7671
|
};
|
|
7065
7672
|
}
|
|
7673
|
+
clearProviderCache();
|
|
7066
7674
|
return {
|
|
7067
7675
|
ok: true,
|
|
7068
7676
|
extension: updated,
|
|
@@ -7113,7 +7721,10 @@ function createLspRpc(deps) {
|
|
|
7113
7721
|
* @returns 是否成功安装
|
|
7114
7722
|
*/
|
|
7115
7723
|
async function installLspSettingsSection(ctx, entry) {
|
|
7116
|
-
|
|
7724
|
+
let deps = null;
|
|
7725
|
+
try {
|
|
7726
|
+
deps = await loadSettingsDeps();
|
|
7727
|
+
} catch {}
|
|
7117
7728
|
if (!deps) return false;
|
|
7118
7729
|
try {
|
|
7119
7730
|
const z = deps.z;
|
|
@@ -7124,11 +7735,11 @@ async function installLspSettingsSection(ctx, entry) {
|
|
|
7124
7735
|
path: z.string()
|
|
7125
7736
|
}).default({});
|
|
7126
7737
|
const schema = z.object(langShape).default({});
|
|
7127
|
-
|
|
7738
|
+
const strategy = await runSettingsInstall(ctx, LSP_SETTINGS_NS, schema, entry, {
|
|
7128
7739
|
setSource: () => {},
|
|
7129
7740
|
onChange: () => {}
|
|
7130
7741
|
});
|
|
7131
|
-
return
|
|
7742
|
+
return strategy === "legacy" || strategy === "service";
|
|
7132
7743
|
} catch (error) {
|
|
7133
7744
|
console.error("[dsh-vscode-mode] LSP 设置 section 安装失败:" + String(error));
|
|
7134
7745
|
return false;
|
|
@@ -7214,7 +7825,7 @@ async function logCompatSummary(ctx, warnings) {
|
|
|
7214
7825
|
for (const warning of warnings) report.warnings.push(warning);
|
|
7215
7826
|
const logger = ctx.logger;
|
|
7216
7827
|
if (!logger?.info) return;
|
|
7217
|
-
const head = "[dsh-vscode-mode] 兼容性:" + report.external.length + " 项外部适配 / " + report.guards.length + " 项护栏";
|
|
7828
|
+
const head = "[dsh-vscode-mode] 兼容性:" + (report.dshVersion ? "DSH " + report.dshVersion : "DSH 未探测") + " · " + report.external.length + " 项外部适配 / " + report.guards.length + " 项护栏 / " + (report.adapters?.length ?? 0) + " 项版本适配";
|
|
7218
7829
|
if (report.warnings.length) logger.warn?.(head + ",警告 " + report.warnings.length + " 条:" + report.warnings.join(";"));
|
|
7219
7830
|
else logger.info?.(head + ",无警告");
|
|
7220
7831
|
} catch (error) {
|