dsh-vscode-mode 0.1.35 → 0.1.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -485,6 +485,14 @@ function pluginCacheRoot(home = dshHome()) {
485
485
  function workspaceCacheDir(workspaceId, home = dshHome()) {
486
486
  return join(pluginCacheRoot(home), "workspace", workspaceId);
487
487
  }
488
+ /** 用户级缓存目录(跨工作区共享;当前无实例,预留供全局偏好类缓存使用)。 */
489
+ function userCacheDir(home = dshHome()) {
490
+ return join(pluginCacheRoot(home), "user");
491
+ }
492
+ /** LSP provider 发现结果缓存文件(用户级:user/lsp-specs.v<schema>.json,跨会话/跨重启共享)。 */
493
+ function lspSpecCacheFile(home = dshHome(), schema = 4) {
494
+ return join(userCacheDir(home), "lsp-specs.v" + schema + ".json");
495
+ }
488
496
  /** 插件日志根(~/.dsh/dsh-vscode-mode/logs,用户级)。 */
489
497
  function pluginLogRoot(home = dshHome()) {
490
498
  return join(home, PLUGIN_ID, "logs");
@@ -4378,7 +4386,8 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
4378
4386
  const info = await fs.stat(target);
4379
4387
  if (!info || info.type !== "file") return {
4380
4388
  ok: false,
4381
- error: "文件不存在"
4389
+ error: "文件不存在",
4390
+ resolvedPath: fs.processPath(target)
4382
4391
  };
4383
4392
  if ((info.size ?? 0) > 8388608) return {
4384
4393
  ok: false,
@@ -5453,6 +5462,50 @@ function disposeAllServers() {
5453
5462
  running.clear();
5454
5463
  }
5455
5464
  //#endregion
5465
+ //#region src/shared/lsp.ts
5466
+ /** LSP semantic token 标准类型(host 归一化后,client 与 Monaco 共用)。 */
5467
+ const LSP_SEMANTIC_TOKEN_TYPES = [
5468
+ "namespace",
5469
+ "type",
5470
+ "class",
5471
+ "enum",
5472
+ "interface",
5473
+ "struct",
5474
+ "typeParameter",
5475
+ "parameter",
5476
+ "variable",
5477
+ "property",
5478
+ "enumMember",
5479
+ "event",
5480
+ "function",
5481
+ "method",
5482
+ "macro",
5483
+ "label",
5484
+ "comment",
5485
+ "string",
5486
+ "keyword",
5487
+ "number",
5488
+ "regexp",
5489
+ "operator",
5490
+ "decorator"
5491
+ ];
5492
+ /** LSP semantic token 标准修饰符(host 归一化后,client 与 Monaco 共用)。 */
5493
+ const LSP_SEMANTIC_TOKEN_MODIFIERS = [
5494
+ "declaration",
5495
+ "definition",
5496
+ "readonly",
5497
+ "static",
5498
+ "deprecated",
5499
+ "abstract",
5500
+ "async",
5501
+ "modification",
5502
+ "documentation",
5503
+ "defaultLibrary",
5504
+ "global",
5505
+ "local",
5506
+ "mutable"
5507
+ ];
5508
+ //#endregion
5456
5509
  //#region src/lsp/client.ts
5457
5510
  /**
5458
5511
  * dsh-vscode-mode host — LSP 客户端(JSON-RPC over stdio)。
@@ -5484,7 +5537,10 @@ function createLspClient(transport, events = {}) {
5484
5537
  references: false,
5485
5538
  documentSymbol: false,
5486
5539
  workspaceSymbol: false,
5487
- hover: false
5540
+ hover: false,
5541
+ semanticTokens: false,
5542
+ semanticTokenTypes: [],
5543
+ semanticTokenModifiers: []
5488
5544
  };
5489
5545
  const log = (line) => events.onLog?.(line);
5490
5546
  createFrameParser();
@@ -5614,16 +5670,30 @@ function createLspClient(transport, events = {}) {
5614
5670
  definition: { dynamicRegistration: false },
5615
5671
  references: { dynamicRegistration: false },
5616
5672
  documentSymbol: { dynamicRegistration: false },
5617
- hover: { dynamicRegistration: false }
5673
+ hover: { dynamicRegistration: false },
5674
+ semanticTokens: {
5675
+ dynamicRegistration: false,
5676
+ requests: {
5677
+ range: false,
5678
+ full: { delta: false }
5679
+ },
5680
+ tokenTypes: [...LSP_SEMANTIC_TOKEN_TYPES],
5681
+ tokenModifiers: [...LSP_SEMANTIC_TOKEN_MODIFIERS],
5682
+ formats: ["relative"]
5683
+ }
5618
5684
  }
5619
5685
  }
5620
5686
  }))?.capabilities ?? {};
5687
+ const legend = caps.semanticTokensProvider?.legend;
5621
5688
  serverCapabilities = {
5622
5689
  definition: Boolean(caps.definitionProvider),
5623
5690
  references: Boolean(caps.referencesProvider),
5624
5691
  documentSymbol: Boolean(caps.documentSymbolProvider),
5625
5692
  workspaceSymbol: Boolean(caps.workspaceSymbolProvider),
5626
- hover: Boolean(caps.hoverProvider)
5693
+ hover: Boolean(caps.hoverProvider),
5694
+ semanticTokens: Boolean(caps.semanticTokensProvider),
5695
+ semanticTokenTypes: Array.isArray(legend?.tokenTypes) ? legend.tokenTypes.filter((item) => typeof item === "string") : [...LSP_SEMANTIC_TOKEN_TYPES],
5696
+ semanticTokenModifiers: Array.isArray(legend?.tokenModifiers) ? legend.tokenModifiers.filter((item) => typeof item === "string") : [...LSP_SEMANTIC_TOKEN_MODIFIERS]
5627
5697
  };
5628
5698
  client.notify("initialized", {});
5629
5699
  events.onReady?.(serverCapabilities);
@@ -5697,7 +5767,10 @@ function createLspServer(spec, root, languageId, logger) {
5697
5767
  references: false,
5698
5768
  documentSymbol: false,
5699
5769
  workspaceSymbol: false,
5700
- hover: false
5770
+ hover: false,
5771
+ semanticTokens: false,
5772
+ semanticTokenTypes: [...LSP_SEMANTIC_TOKEN_TYPES],
5773
+ semanticTokenModifiers: [...LSP_SEMANTIC_TOKEN_MODIFIERS]
5701
5774
  };
5702
5775
  let disposed = false;
5703
5776
  let readyWait = Promise.resolve(false);
@@ -5708,9 +5781,15 @@ function createLspServer(spec, root, languageId, logger) {
5708
5781
  source: spec.kind,
5709
5782
  phase: "idle",
5710
5783
  reason: spec.ready ? void 0 : spec.reason,
5711
- root
5784
+ root,
5785
+ version: spec.version,
5786
+ providerName: spec.providerName ?? (spec.version ? "语言服务器扩展" : void 0)
5712
5787
  };
5713
5788
  const log = (line) => logger?.("[" + languageId + "@" + root + "] " + line);
5789
+ const providerInfo = {
5790
+ version: spec.version,
5791
+ providerName: spec.providerName ?? (spec.version ? "EmmyLua" : void 0)
5792
+ };
5714
5793
  const setPhase = (next, reason) => {
5715
5794
  phase = next;
5716
5795
  lastStatus = {
@@ -5719,6 +5798,7 @@ function createLspServer(spec, root, languageId, logger) {
5719
5798
  phase: next,
5720
5799
  reason,
5721
5800
  root,
5801
+ ...providerInfo,
5722
5802
  ...progressFields()
5723
5803
  };
5724
5804
  server.onStateChange?.(lastStatus);
@@ -5913,6 +5993,17 @@ function createLspServer(spec, root, languageId, logger) {
5913
5993
  if (!result || !result.contents) return null;
5914
5994
  return { contents: stringifyHoverContents(result.contents) };
5915
5995
  },
5996
+ /** 查询全文 semantic tokens,并将服务器 legend 归一到插件固定 legend。 */
5997
+ async semanticTokens(path) {
5998
+ const doc = docs.get(path);
5999
+ if (!doc || !await readyWait || !capabilities.semanticTokens) return null;
6000
+ const result = await ensureClient().request("textDocument/semanticTokens/full", { textDocument: { uri: doc.uri } });
6001
+ if (!result || !Array.isArray(result.data)) return null;
6002
+ return {
6003
+ data: normalizeSemanticData(result.data, capabilities.semanticTokenTypes ?? [], capabilities.semanticTokenModifiers ?? []),
6004
+ resultId: typeof result.resultId === "string" ? result.resultId : void 0
6005
+ };
6006
+ },
5916
6007
  async dispose() {
5917
6008
  disposed = true;
5918
6009
  resolveReady?.(false);
@@ -6021,6 +6112,39 @@ function toInt(value) {
6021
6112
  const n = Number(value);
6022
6113
  return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
6023
6114
  }
6115
+ /** 把服务器 legend 下的 5 元组流转换为插件固定 legend 下的 5 元组流。 */
6116
+ function normalizeSemanticData(data, types, modifiers) {
6117
+ const out = [];
6118
+ let previousLine = 0;
6119
+ let previousCharacter = 0;
6120
+ let outputLine = 0;
6121
+ let outputCharacter = 0;
6122
+ for (let i = 0; i + 4 < data.length; i += 5) {
6123
+ const deltaLine = toInt(data[i]);
6124
+ const deltaCharacter = toInt(data[i + 1]);
6125
+ const length = toInt(data[i + 2]);
6126
+ const sourceType = types[toInt(data[i + 3])] ?? "variable";
6127
+ const sourceModifiers = toInt(data[i + 4]);
6128
+ const line = previousLine + deltaLine;
6129
+ const character = deltaLine === 0 ? previousCharacter + deltaCharacter : deltaCharacter;
6130
+ const typeIndex = Math.max(0, LSP_SEMANTIC_TOKEN_TYPES.indexOf(sourceType));
6131
+ let modifierBits = 0;
6132
+ for (let bit = 0; bit < modifiers.length; bit++) {
6133
+ if ((sourceModifiers & 1 << bit) === 0) continue;
6134
+ const modifier = modifiers[bit];
6135
+ const targetBit = LSP_SEMANTIC_TOKEN_MODIFIERS.indexOf(modifier);
6136
+ if (targetBit >= 0) modifierBits |= 1 << targetBit;
6137
+ }
6138
+ const outputDeltaLine = line - outputLine;
6139
+ const outputDeltaCharacter = outputDeltaLine === 0 ? character - outputCharacter : character;
6140
+ out.push(outputDeltaLine, outputDeltaCharacter, length, typeIndex, modifierBits);
6141
+ previousLine = line;
6142
+ previousCharacter = character;
6143
+ outputLine = line;
6144
+ outputCharacter = character;
6145
+ }
6146
+ return out;
6147
+ }
6024
6148
  //#endregion
6025
6149
  //#region src/lsp/manager.ts
6026
6150
  /**
@@ -6079,6 +6203,13 @@ function createLspManager(logger) {
6079
6203
  releaseRoot(root) {
6080
6204
  for (const entry of [...entries.values()]) if (entry.server.root === root) dropEntry(entry);
6081
6205
  },
6206
+ reset(root, languageId) {
6207
+ const entry = entries.get(keyOf(root, languageId));
6208
+ if (entry) dropEntry(entry);
6209
+ },
6210
+ resetLanguage(languageId) {
6211
+ for (const entry of [...entries.values()]) if (entry.server.languageId === languageId) dropEntry(entry);
6212
+ },
6082
6213
  statusAll() {
6083
6214
  return [...entries.values()].map((entry) => entry.server.status());
6084
6215
  },
@@ -6388,7 +6519,9 @@ function extensionProviderFor(languageId) {
6388
6519
  kind: "extension",
6389
6520
  argv: p.argv,
6390
6521
  cwd: p.cwd,
6391
- ready: true
6522
+ ready: true,
6523
+ version: p.version,
6524
+ providerName: p.version ? "EmmyLua" : "语言服务器扩展"
6392
6525
  };
6393
6526
  }
6394
6527
  /** PATH 中查找可执行文件(不含扩展名校验,交由 spawn 报错)。 */
@@ -6416,6 +6549,25 @@ function findDotnet() {
6416
6549
  for (const c of ["C:/Program Files/dotnet/dotnet.exe", "C:/Program Files (x86)/dotnet/dotnet.exe"]) if (existsSync(c)) return c;
6417
6550
  return null;
6418
6551
  }
6552
+ /** EmmyLua 自动发现候选(tangzx.emmylua-* / server/emmylua_ls)。 */
6553
+ function candidateEmmyLua(home = dshHome()) {
6554
+ const candidates = [];
6555
+ for (const dir of vscodeExtensionsDirs(home)) for (const extDir of listMatchingDirs(dir, "tangzx.emmylua-")) {
6556
+ const server = join(extDir, "server", "emmylua_ls" + exeSuffix());
6557
+ if (!existsSync(server)) continue;
6558
+ let version;
6559
+ try {
6560
+ const manifest = JSON.parse(readFileSync(join(extDir, "package.json"), "utf8"));
6561
+ if (typeof manifest.version === "string") version = manifest.version;
6562
+ } catch (error) {}
6563
+ candidates.push({
6564
+ path: server,
6565
+ version
6566
+ });
6567
+ }
6568
+ candidates.sort((a, b) => (b.version ?? "").localeCompare(a.version ?? "", void 0, { numeric: true }) || a.path.localeCompare(b.path));
6569
+ return candidates[0] ?? null;
6570
+ }
6419
6571
  /** LuaLS 自动发现候选(VSCode 扩展目录 + extmgr 目录下 sumneko.lua-* 的 server/bin 可执行)。 */
6420
6572
  function candidateLuaServers(home = dshHome()) {
6421
6573
  const plat = platformServerDir();
@@ -6497,30 +6649,41 @@ function resolveLuaProvider(manual, home = dshHome()) {
6497
6649
  cwd: manualResolved.cwd,
6498
6650
  ready: true
6499
6651
  };
6500
- const ext = extensionProviderFor("lua");
6501
- if (ext) return ext;
6502
- const candidates = candidateLuaServers(home);
6503
- if (candidates.length) return {
6504
- ...base,
6505
- kind: "discover",
6506
- argv: [candidates[0]],
6507
- cwd: void 0,
6508
- ready: true
6509
- };
6510
- const inPath = findInPath("lua-language-server" + exeSuffix());
6511
- if (inPath) return {
6512
- ...base,
6513
- kind: "discover",
6514
- argv: [inPath],
6515
- ready: true
6516
- };
6517
- return {
6518
- ...base,
6519
- kind: "none",
6520
- argv: [],
6521
- ready: false,
6522
- reason: manualResolved.reason ?? "未发现 lua-language-server(可安装 LuaLS 或手动指定路径)"
6523
- };
6652
+ return resolveCached("lua", manual, home, (h) => {
6653
+ const ext = extensionProviderFor("lua");
6654
+ if (ext) return ext;
6655
+ const emmy = candidateEmmyLua(h);
6656
+ if (emmy) return {
6657
+ ...base,
6658
+ kind: "extension",
6659
+ argv: [emmy.path],
6660
+ ready: true,
6661
+ version: emmy.version,
6662
+ providerName: "EmmyLua"
6663
+ };
6664
+ const candidates = candidateLuaServers(h);
6665
+ if (candidates.length) return {
6666
+ ...base,
6667
+ kind: "discover",
6668
+ argv: [candidates[0]],
6669
+ cwd: void 0,
6670
+ ready: true
6671
+ };
6672
+ const inPath = findInPath("lua-language-server" + exeSuffix());
6673
+ if (inPath) return {
6674
+ ...base,
6675
+ kind: "discover",
6676
+ argv: [inPath],
6677
+ ready: true
6678
+ };
6679
+ return {
6680
+ ...base,
6681
+ kind: "none",
6682
+ argv: [],
6683
+ ready: false,
6684
+ reason: manualResolved.reason ?? "未发现 lua-language-server(可安装 LuaLS 或手动指定路径)"
6685
+ };
6686
+ });
6524
6687
  }
6525
6688
  /**
6526
6689
  * 解析 C# provider(优先级:手动 > 扩展源 > 自动发现;需 dotnet + Roslyn dll;或 OmniSharp)。
@@ -6539,40 +6702,163 @@ function resolveCSharpProvider(manual, home = dshHome()) {
6539
6702
  cwd: manualResolved.cwd,
6540
6703
  ready: true
6541
6704
  };
6542
- const ext = extensionProviderFor("csharp");
6543
- if (ext) return ext;
6544
- const discovered = candidateCSharpServers(home);
6545
- if (discovered.roslynDll && discovered.dotnet) return {
6546
- ...base,
6547
- kind: "discover",
6548
- argv: [
6549
- discovered.dotnet,
6550
- discovered.roslynDll,
6551
- "--stdio"
6552
- ],
6553
- ready: true
6554
- };
6555
- if (discovered.omnisharpExe) return {
6556
- ...base,
6557
- kind: "discover",
6558
- argv: [discovered.omnisharpExe, "--stdio"],
6559
- ready: true
6560
- };
6561
- const reasons = [];
6562
- if (!discovered.dotnet) reasons.push("未找到 dotnet");
6563
- if (!discovered.roslynDll && !discovered.omnisharpExe) reasons.push("未发现 ms-dotnettools.csharp 扩展");
6564
- return {
6565
- ...base,
6566
- kind: "none",
6567
- argv: [],
6568
- ready: false,
6569
- reason: reasons.join(";") + "(可手动指定 Roslyn/OmniSharp 入口)"
6570
- };
6705
+ return resolveCached("csharp", manual, home, (h) => {
6706
+ const ext = extensionProviderFor("csharp");
6707
+ if (ext) return ext;
6708
+ const discovered = candidateCSharpServers(h);
6709
+ if (discovered.roslynDll && discovered.dotnet) return {
6710
+ ...base,
6711
+ kind: "discover",
6712
+ argv: [
6713
+ discovered.dotnet,
6714
+ discovered.roslynDll,
6715
+ "--stdio"
6716
+ ],
6717
+ ready: true
6718
+ };
6719
+ if (discovered.omnisharpExe) return {
6720
+ ...base,
6721
+ kind: "discover",
6722
+ argv: [discovered.omnisharpExe, "--stdio"],
6723
+ ready: true
6724
+ };
6725
+ const reasons = [];
6726
+ if (!discovered.dotnet) reasons.push("未找到 dotnet");
6727
+ if (!discovered.roslynDll && !discovered.omnisharpExe) reasons.push("未发现 ms-dotnettools.csharp 扩展");
6728
+ return {
6729
+ ...base,
6730
+ kind: "none",
6731
+ argv: [],
6732
+ ready: false,
6733
+ reason: reasons.join(";") + "(可手动指定 Roslyn/OmniSharp 入口)"
6734
+ };
6735
+ });
6571
6736
  }
6572
6737
  const PROVIDER_RESOLVERS = {
6573
6738
  lua: resolveLuaProvider,
6574
6739
  csharp: resolveCSharpProvider
6575
6740
  };
6741
+ /** 进程级缓存(home|languageId → 条目),同一 DSH home 的不同会话共享。 */
6742
+ const specCache = /* @__PURE__ */ new Map();
6743
+ /** 磁盘缓存是否已载入本进程(home → loaded),不同 profile 不串缓存。 */
6744
+ const diskLoaded = /* @__PURE__ */ new Set();
6745
+ /** 自动发现成功结果的复用时长;过期后才检查目录指纹。 */
6746
+ const SPEC_CACHE_TTL_MS = 18e5;
6747
+ /** 未发现结果的短缓存时长,避免缺失服务器时反复扫描 PATH。 */
6748
+ const SPEC_NEGATIVE_CACHE_MS = 6e4;
6749
+ /** 生成带 home 的缓存键,避免不同 profile 的 provider 结果串用。 */
6750
+ function cacheEntryKey(home, languageId) {
6751
+ return home + "|" + languageId;
6752
+ }
6753
+ /**
6754
+ * 构造缓存 key:语言 + 手动配置(manual 结果不缓存,故只参与"是否走缓存"判定)。
6755
+ * @author ddj 2026年08月28号
6756
+ * @param languageId 语言 id
6757
+ * @param manual 手动配置
6758
+ * @returns 缓存 key(manual 有效时返回空串表示跳过缓存)
6759
+ */
6760
+ function cacheKeyOf(languageId, manual) {
6761
+ const pathValue = manual?.path?.trim() ?? "";
6762
+ const commandValue = manual?.command?.trim() ?? "";
6763
+ if (pathValue || commandValue) return "";
6764
+ return languageId;
6765
+ }
6766
+ /**
6767
+ * 环境指纹:扩展目录清单 + 平台。目录增删/换平台即失效(防缓存陈旧指向已删扩展)。
6768
+ * @author ddj 2026年08月28号
6769
+ * @param home DSH home
6770
+ * @returns 指纹串
6771
+ */
6772
+ function fingerprintOf(home) {
6773
+ const marks = vscodeExtensionsDirs(home).map((dir) => {
6774
+ try {
6775
+ return readdirSync(dir).sort().join(",");
6776
+ } catch (error) {
6777
+ return "?";
6778
+ }
6779
+ });
6780
+ return process.platform + "|" + marks.join("|");
6781
+ }
6782
+ /**
6783
+ * 读取磁盘缓存(幂等:每进程只载一次;schema 文件名已隔离版本)。
6784
+ * @author ddj 2026年08月28号
6785
+ * @param home DSH home
6786
+ */
6787
+ function loadDiskCache(home) {
6788
+ if (diskLoaded.has(home)) return;
6789
+ diskLoaded.add(home);
6790
+ const file = lspSpecCacheFile(home);
6791
+ try {
6792
+ const raw = JSON.parse(readFileSync(file, "utf8"));
6793
+ 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);
6794
+ } catch (error) {}
6795
+ }
6796
+ /**
6797
+ * 写回磁盘缓存(best-effort;写失败不影响本次解析结果)。
6798
+ * @author ddj 2026年08月28号
6799
+ * @param home DSH home
6800
+ */
6801
+ function saveDiskCache(home) {
6802
+ const file = lspSpecCacheFile(home);
6803
+ try {
6804
+ mkdirSync(dirname(file), { recursive: true });
6805
+ const out = {};
6806
+ const prefix = home + "|";
6807
+ for (const [key, entry] of specCache) if (key.startsWith(prefix)) out[key.slice(prefix.length)] = entry;
6808
+ writeFileSync(file, JSON.stringify(out), "utf8");
6809
+ } catch (error) {}
6810
+ }
6811
+ /**
6812
+ * 带缓存执行 provider 解析(自动发现路径专用)。
6813
+ * @author ddj 2026年08月28号
6814
+ * @param languageId 语言 id
6815
+ * @param manual 手动配置
6816
+ * @param home DSH home
6817
+ * @param resolve 真解析函数(未命中时调用)
6818
+ * @returns provider 规格
6819
+ */
6820
+ function resolveCached(languageId, manual, home, resolve) {
6821
+ const key = cacheKeyOf(languageId, manual);
6822
+ if (!key) return resolve(home);
6823
+ loadDiskCache(home);
6824
+ const fullKey = cacheEntryKey(home, key);
6825
+ const hit = specCache.get(fullKey);
6826
+ if (hit && isCacheFresh(hit)) return hit.spec;
6827
+ const fingerprint = fingerprintOf(home);
6828
+ if (hit && hit.fingerprint === fingerprint) {
6829
+ hit.at = Date.now();
6830
+ return hit.spec;
6831
+ }
6832
+ const spec = resolve(home);
6833
+ specCache.set(fullKey, {
6834
+ spec,
6835
+ fingerprint,
6836
+ at: Date.now()
6837
+ });
6838
+ saveDiskCache(home);
6839
+ return spec;
6840
+ }
6841
+ /** 判断缓存条目是否仍可直接复用,不触发扩展目录扫描。 */
6842
+ function isCacheFresh(entry) {
6843
+ const age = Date.now() - entry.at;
6844
+ const ttl = entry.spec.kind === "none" ? SPEC_NEGATIVE_CACHE_MS : SPEC_CACHE_TTL_MS;
6845
+ if (age < 0 || age >= ttl) return false;
6846
+ const executable = entry.spec.argv[0];
6847
+ return !entry.spec.ready || !executable || existsSync(executable);
6848
+ }
6849
+ /**
6850
+ * 清空 provider 发现缓存(扩展安装/卸载、配置变更后调用;进程级 + 删磁盘文件)。
6851
+ * @author ddj 2026年08月28号
6852
+ * @param home DSH home(缺省真实)
6853
+ */
6854
+ function clearProviderCache(home = dshHome()) {
6855
+ const prefix = home + "|";
6856
+ for (const key of specCache.keys()) if (key.startsWith(prefix)) specCache.delete(key);
6857
+ diskLoaded.delete(home);
6858
+ try {
6859
+ rmSync(lspSpecCacheFile(home), { force: true });
6860
+ } catch (error) {}
6861
+ }
6576
6862
  //#endregion
6577
6863
  //#region src/lsp/config.ts
6578
6864
  const LSP_SETTINGS_NS = "dsh-vscode-mode.languageServers";
@@ -6736,6 +7022,21 @@ function createDocTracker() {
6736
7022
  }
6737
7023
  sessionDocs.delete(sessionId);
6738
7024
  return [...toRelease];
7025
+ },
7026
+ /** 清除指定语言的文档归属,让当前客户端重新同步时重新 acquire。 */
7027
+ resetLanguage(languageId) {
7028
+ for (const [docKey, owners] of openDocs) {
7029
+ if (!docKey.includes("|" + languageId + "|")) continue;
7030
+ for (const sessionId of owners) forgetDoc(sessionId, docKey);
7031
+ openDocs.delete(docKey);
7032
+ }
7033
+ for (const key of [...rootLangRefs.keys()]) if (key.endsWith("|" + languageId)) rootLangRefs.delete(key);
7034
+ },
7035
+ /** 返回指定会话已打开的文档路径,用于重检测后恢复同步。 */
7036
+ pathsFor(sessionId, languageId) {
7037
+ const docs = sessionDocs.get(sessionId);
7038
+ if (!docs) return [];
7039
+ return [...docs].filter((key) => key.includes("|" + languageId + "|")).map((key) => key.slice(key.lastIndexOf("|") + 1));
6739
7040
  }
6740
7041
  };
6741
7042
  }
@@ -6768,6 +7069,39 @@ function createLspRpc(deps) {
6768
7069
  ...location,
6769
7070
  root
6770
7071
  }));
7072
+ /** 当前 provider 检测结论 → 设置页 idle 状态(不启动 server)。 */
7073
+ const detectedStatus = (languageId, root) => {
7074
+ const spec = resolveProviderSpec(ctx, pluginConfig, languageId);
7075
+ return {
7076
+ languageId,
7077
+ source: spec.kind,
7078
+ phase: "idle",
7079
+ reason: spec.ready ? void 0 : spec.reason,
7080
+ version: spec.version,
7081
+ providerName: spec.providerName,
7082
+ root
7083
+ };
7084
+ };
7085
+ /**
7086
+ * 清发现缓存并重置旧 server(保留 tracker 文档记录);
7087
+ * 下一次模型同步会按最新配置重新 acquire,已打开文档无需重开。
7088
+ * 作者 ddj 2026年09月02号
7089
+ * @param languageId 语言 id
7090
+ * @param sessionId 会话语境(缺省重置全部工作区该语言)
7091
+ * @returns 检测后的 idle 状态列表
7092
+ */
7093
+ const redetectLanguage = async (languageId, sessionId) => {
7094
+ clearProviderCache();
7095
+ if (sessionId) {
7096
+ const sc = await rootOf(sessionId);
7097
+ if ("err" in sc) return [];
7098
+ manager.reset(sc.root, languageId);
7099
+ return [detectedStatus(languageId, sc.root)];
7100
+ }
7101
+ const roots = [...new Set(manager.statusAll().filter((s) => s.languageId === languageId).map((s) => s.root))];
7102
+ manager.resetLanguage(languageId);
7103
+ return roots.length ? roots.map((root) => detectedStatus(languageId, root)) : [detectedStatus(languageId)];
7104
+ };
6771
7105
  const disposeSession = (sessionId) => {
6772
7106
  for (const rl of tracker.closeSession(sessionId)) {
6773
7107
  const sep = rl.lastIndexOf("|");
@@ -6825,13 +7159,15 @@ function createLspRpc(deps) {
6825
7159
  error: "设置服务不可用"
6826
7160
  };
6827
7161
  await settings.update(LSP_SETTINGS_NS, { [lang]: target });
7162
+ const servers = await redetectLanguage(lang);
6828
7163
  const stored = (settings.describe?.({ redactSecrets: true })?.find((item) => item.ns === LSP_SETTINGS_NS))?.value;
6829
7164
  return {
6830
7165
  ok: true,
6831
7166
  config: {
6832
7167
  ...configFromPlugin(pluginConfig),
6833
7168
  ...stored
6834
- }
7169
+ },
7170
+ servers
6835
7171
  };
6836
7172
  } catch (error) {
6837
7173
  return {
@@ -7006,6 +7342,51 @@ function createLspRpc(deps) {
7006
7342
  };
7007
7343
  }
7008
7344
  },
7345
+ "edrv.lsp.semanticTokens": async (args) => {
7346
+ const lang = langOfPath(args.path);
7347
+ if (!lang) return {
7348
+ ok: true,
7349
+ tokens: void 0
7350
+ };
7351
+ const sc = await rootOf(args.sessionId);
7352
+ if ("err" in sc) return {
7353
+ ok: false,
7354
+ error: sc.err
7355
+ };
7356
+ try {
7357
+ const server = serverOf(sc.root, lang);
7358
+ if (!server) return {
7359
+ ok: true,
7360
+ tokens: void 0
7361
+ };
7362
+ return {
7363
+ ok: true,
7364
+ tokens: await server.semanticTokens(args.path) ?? void 0
7365
+ };
7366
+ } catch (error) {
7367
+ return {
7368
+ ok: false,
7369
+ error: "LSP 语义分色查询失败:" + String(error)
7370
+ };
7371
+ }
7372
+ },
7373
+ "edrv.lsp.redetect": async (args) => {
7374
+ if (!LSP_LANGUAGES.includes(args.languageId)) return {
7375
+ ok: false,
7376
+ error: "不支持的语言:" + args.languageId
7377
+ };
7378
+ try {
7379
+ return {
7380
+ ok: true,
7381
+ servers: await redetectLanguage(args.languageId)
7382
+ };
7383
+ } catch (error) {
7384
+ return {
7385
+ ok: false,
7386
+ error: "重新检测失败:" + String(error)
7387
+ };
7388
+ }
7389
+ },
7009
7390
  "edrv.lsp.ext.list": async () => ({
7010
7391
  ok: true,
7011
7392
  extensions: listInstalled()
@@ -7032,6 +7413,7 @@ function createLspRpc(deps) {
7032
7413
  ok: false,
7033
7414
  error: "缺少安装源(vsixPath 或 namespace+name)"
7034
7415
  };
7416
+ clearProviderCache();
7035
7417
  return {
7036
7418
  ok: true,
7037
7419
  extension: info
@@ -7044,7 +7426,9 @@ function createLspRpc(deps) {
7044
7426
  }
7045
7427
  },
7046
7428
  "edrv.lsp.ext.uninstall": async (args) => {
7047
- return uninstall(args.id) ? { ok: true } : {
7429
+ const ok = uninstall(args.id);
7430
+ if (ok) clearProviderCache();
7431
+ return ok ? { ok: true } : {
7048
7432
  ok: false,
7049
7433
  error: "扩展未安装:" + args.id
7050
7434
  };
@@ -7063,6 +7447,7 @@ function createLspRpc(deps) {
7063
7447
  error: "扩展未安装:" + args.id
7064
7448
  };
7065
7449
  }
7450
+ clearProviderCache();
7066
7451
  return {
7067
7452
  ok: true,
7068
7453
  extension: updated,