dsh-vscode-mode 0.1.32 → 0.1.35

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
@@ -1,10 +1,12 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, extname, join, normalize, resolve, sep } from "node:path";
4
- import { existsSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
5
- import { fileURLToPath } from "node:url";
4
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { homedir } from "node:os";
7
7
  import { createHash } from "node:crypto";
8
+ import { spawn } from "node:child_process";
9
+ import { inflateRawSync } from "node:zlib";
8
10
  //#region src/mcp.ts
9
11
  /**
10
12
  * dsh-vscode-mode host — MCP 运行时管理适配层。
@@ -2178,7 +2180,7 @@ function pathMatch(value, query) {
2178
2180
  * 作者 ddj 2026年08月24号
2179
2181
  */
2180
2182
  const STDOUT_CAP$1 = 4 << 20;
2181
- const STDERR_CAP$1 = 65536;
2183
+ const STDERR_CAP$2 = 65536;
2182
2184
  const GRACE_MS$1 = 2e4;
2183
2185
  /** 默认排除目录(文件/内容搜索共用,导出供 provider 复用)。 */
2184
2186
  const EXCLUDES = [
@@ -2359,7 +2361,7 @@ async function searchRipgrep(input) {
2359
2361
  stdio: {
2360
2362
  stdin: "ignore",
2361
2363
  stdout: { maxBytes: STDOUT_CAP$1 },
2362
- stderr: { maxBytes: STDERR_CAP$1 }
2364
+ stderr: { maxBytes: STDERR_CAP$2 }
2363
2365
  },
2364
2366
  graceMs: GRACE_MS$1,
2365
2367
  signal: input.signal
@@ -2824,7 +2826,7 @@ function newSearcher(ctx) {
2824
2826
  //#endregion
2825
2827
  //#region src/search/content.ts
2826
2828
  const STDOUT_CAP = 16 << 20;
2827
- const STDERR_CAP = 65536;
2829
+ const STDERR_CAP$1 = 65536;
2828
2830
  const GRACE_MS = 2e4;
2829
2831
  const DEFAULT_MAX_MATCHES = 500;
2830
2832
  const DEFAULT_MAX_FILES = 100;
@@ -2986,7 +2988,7 @@ async function searchRipgrepContent(input) {
2986
2988
  stdio: {
2987
2989
  stdin: "ignore",
2988
2990
  stdout: { maxBytes: STDOUT_CAP },
2989
- stderr: { maxBytes: STDERR_CAP }
2991
+ stderr: { maxBytes: STDERR_CAP$1 }
2990
2992
  },
2991
2993
  graceMs: GRACE_MS,
2992
2994
  signal: input.signal
@@ -4274,8 +4276,9 @@ async function latestPatchBackup(patchPath) {
4274
4276
  return (await readdir(dir).catch(() => [])).filter((n) => n.startsWith(base + ".bak-")).sort().reverse()[0];
4275
4277
  }
4276
4278
  /** 各方法 handler 表(类型由 shared/rpc 的 RpcHandlerMap 约束)。 */
4277
- function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx)) {
4279
+ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx), lspHandlers) {
4278
4280
  return {
4281
+ ...lspHandlers ?? {},
4279
4282
  "edrv.list": async (args) => {
4280
4283
  const sc = await requireSession(ctx, args.sessionId);
4281
4284
  if ("err" in sc) return {
@@ -5100,8 +5103,8 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
5100
5103
  * 统一入口:按方法分发到 handler 表。
5101
5104
  * @author ddj 2026年08月20号 / 2026年08月26号
5102
5105
  */
5103
- async function handleRpc(ctx, registry, method, args, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx)) {
5104
- const handler = buildHandlers(ctx, registry, searcher, contentSearcher)[method];
5106
+ async function handleRpc(ctx, registry, method, args, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx), lspHandlers) {
5107
+ const handler = buildHandlers(ctx, registry, searcher, contentSearcher, lspHandlers)[method];
5105
5108
  if (!handler) return {
5106
5109
  ok: false,
5107
5110
  error: "未知方法: " + String(method)
@@ -5244,56 +5247,1960 @@ function dropFileIndex(cwd) {
5244
5247
  fileIndex.delete(cwd);
5245
5248
  }
5246
5249
  //#endregion
5247
- //#region src/index.ts
5250
+ //#region src/lsp/jsonrpc.ts
5248
5251
  /**
5249
- * @dsh-external 生态 → dsh-vscode-mode:DSH 上的类 VSCode 编码体验(Host 半入口)。
5250
- * 职责:装配 capture(tools/result 捕获 edit/write 差异)、RPC 分发(edrv.* / mcp.* / vscode.* / compat)、
5251
- * webServer 路由(/edrv/rpc、/edrv/assets/*、/edrv/vendor/*)、工作区旁车持久化、
5252
- * 兼容层装配(依赖守卫 + 重复装配自诊断 + 启动日志兼容性报告)。
5253
- * 结构:shared/(双面契约) + model/store/capture/workspace/revert/registry/rpc/routes/compat(host 模块)。
5254
- * 作者 ddj 2026-08-20
5252
+ * dsh-vscode-mode host — LSP JSON-RPC 帧编解码(纯函数,node 可测)。
5253
+ * LSP over stdio 用 Content-Length 帧:
5254
+ * Content-Length: <n>\r\n\r\n<恰好 n 字节 JSON>
5255
+ * 导出:encodeMessage(对象→帧 Buffer)、createFrameParser(流式粘包/拆包)、parseFrame(单帧)。
5256
+ * 作者 ddj 2026-08-27
5255
5257
  */
5256
- const name = "dsh-vscode-mode";
5257
- const inject = [
5258
- "sessions",
5259
- "fs",
5260
- "webServer",
5261
- "loader",
5262
- "tools",
5263
- "workspaceRegistry",
5264
- "agents"
5265
- ];
5258
+ const HEADER_END = "\r\n\r\n";
5259
+ /** 消息头正则(仅取 Content-Length;其它头忽略)。 */
5260
+ const HEADER_RE = /^Content-Length:\s*(\d+)\s*$/im;
5261
+ /** 将对象编码为带 Content-Length 头的帧。 */
5262
+ function encodeMessage(message) {
5263
+ const body = JSON.stringify(message);
5264
+ const head = "Content-Length: " + Buffer.byteLength(body, "utf8") + "\r\n\r\n";
5265
+ return Buffer.from(head + body, "utf8");
5266
+ }
5266
5267
  /**
5267
- * 装配插件:挂事件监听、注册路由、安装兼容层。
5268
- * @author ddj 2026年08月20号
5269
- * @param ctx DSH 上下文(sessions/fs/webServer 由 inject 提供;sandboxPolicy/subprocess 惰性获取)
5270
- * @param config 插件配置(可选 imageDir 覆盖图标目录)
5268
+ * 尝试从 buffer 解析单个帧;成功返回 { body, rest },数据不足返回 null。
5269
+ * @author ddj 2026年08月27号
5270
+ * @param buffer 已累积的字节
5271
+ * @returns 解析结果(rest 为剩余未消费字节)
5271
5272
  */
5272
- function apply(ctx, config) {
5273
- const registry = /* @__PURE__ */ new Map();
5274
- const searcher = newSearcher(ctx);
5275
- const contentSearcher = newContentSearcher(ctx);
5276
- /** 兼容性警告收集(route 护栏等写入,启动日志一并输出)。 */
5277
- const warnings = [];
5278
- setupOpenSettings(ctx, config, () => {});
5279
- ctx.on("tools/result", (exec, result) => {
5280
- captureToolResult(ctx, registry, exec, result);
5273
+ function parseFrame(buffer) {
5274
+ const headEnd = buffer.indexOf(HEADER_END);
5275
+ if (headEnd < 0) return null;
5276
+ const header = buffer.subarray(0, headEnd).toString("utf8");
5277
+ const m = HEADER_RE.exec(header);
5278
+ if (!m) {
5279
+ const next = buffer.indexOf("Content-Length:");
5280
+ return next >= 0 ? parseFrame(buffer.subarray(next)) : null;
5281
+ }
5282
+ const length = Number(m[1]);
5283
+ const bodyStart = headEnd + 4;
5284
+ if (buffer.length < bodyStart + length) return null;
5285
+ return {
5286
+ body: buffer.subarray(bodyStart, bodyStart + length).toString("utf8"),
5287
+ rest: buffer.subarray(bodyStart + length)
5288
+ };
5289
+ }
5290
+ /** 帧解析器:push 数据块,吐出完整消息(可能拆包后重组、也可能一包多帧)。 */
5291
+ function createFrameParser() {
5292
+ let buffer = Buffer.alloc(0);
5293
+ return {
5294
+ push(chunk) {
5295
+ if (chunk.length) buffer = Buffer.concat([buffer, chunk]);
5296
+ const out = [];
5297
+ for (;;) {
5298
+ const parsed = parseFrame(buffer);
5299
+ if (!parsed) break;
5300
+ buffer = parsed.rest;
5301
+ try {
5302
+ out.push(JSON.parse(parsed.body));
5303
+ } catch (error) {
5304
+ out.push({
5305
+ jsonrpc: "2.0",
5306
+ error: {
5307
+ code: -32700,
5308
+ message: "Parse error: " + String(error)
5309
+ },
5310
+ id: null
5311
+ });
5312
+ }
5313
+ }
5314
+ return out;
5315
+ },
5316
+ clear() {
5317
+ buffer = Buffer.alloc(0);
5318
+ }
5319
+ };
5320
+ }
5321
+ /** 构造标准 JSON-RPC 请求消息。 */
5322
+ function requestMessage(id, method, params) {
5323
+ return {
5324
+ jsonrpc: "2.0",
5325
+ id,
5326
+ method,
5327
+ params
5328
+ };
5329
+ }
5330
+ /** 构造标准 JSON-RPC 通知消息(无 id,fire-and-forget)。 */
5331
+ function notifyMessage(method, params) {
5332
+ return {
5333
+ jsonrpc: "2.0",
5334
+ method,
5335
+ params
5336
+ };
5337
+ }
5338
+ //#endregion
5339
+ //#region src/lsp/transport.ts
5340
+ /**
5341
+ * dsh-vscode-mode host — LSP 语言服务器进程传输层。
5342
+ * 基于 node:child_process.spawn(stdio pipe 双向流)。刻意不用 DSH ctx.subprocess
5343
+ * 抽象:LSP 需要持续写 stdin + 流式读 stdout,而该抽象 stdin 不可写、输出为一次性收集。
5344
+ * 作者 ddj 2026-08-27
5345
+ */
5346
+ const STDERR_CAP = 262144;
5347
+ const running = /* @__PURE__ */ new Set();
5348
+ /**
5349
+ * 启动语言服务器进程。
5350
+ * @author ddj 2026年08月27号
5351
+ * @param spec 启动参数(argv 数组,不经 shell,Windows 引号安全)
5352
+ * @param onMessage stdout 消息回调
5353
+ * @param logger 诊断日志(可选)
5354
+ * @returns 传输句柄
5355
+ */
5356
+ function spawnServer(spec, onMessage, logger) {
5357
+ const { argv, cwd, env } = spec;
5358
+ const child = spawn(argv[0], argv.slice(1), {
5359
+ cwd,
5360
+ env: {
5361
+ ...process.env,
5362
+ ...env
5363
+ },
5364
+ stdio: [
5365
+ "pipe",
5366
+ "pipe",
5367
+ "pipe"
5368
+ ],
5369
+ windowsHide: true
5281
5370
  });
5282
- ctx.on("session/disposed", (session) => {
5283
- const cwd = cwdOf(session);
5284
- if (cwd) {
5285
- registry.delete(cwd);
5286
- dropFileIndex(cwd);
5287
- searcher.dispose(cwd);
5288
- contentSearcher.dispose(cwd);
5289
- disposeIndex(cwd);
5371
+ running.add(child);
5372
+ let stderrBuf = Buffer.alloc(0);
5373
+ let alive = true;
5374
+ const parser = createFrameParser();
5375
+ child.stderr.on("data", (chunk) => {
5376
+ if (stderrBuf.length < STDERR_CAP) stderrBuf = Buffer.concat([stderrBuf, chunk], Math.min(stderrBuf.length + chunk.length, STDERR_CAP));
5377
+ logger?.(chunk.toString("utf8"));
5378
+ });
5379
+ let exited = false;
5380
+ const emitExit = (code, signal) => {
5381
+ if (exited) return;
5382
+ exited = true;
5383
+ alive = false;
5384
+ running.delete(child);
5385
+ transport.onExit?.(code, signal);
5386
+ };
5387
+ child.on("error", (error) => {
5388
+ logger?.("spawn error: " + String(error));
5389
+ emitExit(null, null);
5390
+ });
5391
+ child.on("exit", (code, signal) => emitExit(code, signal));
5392
+ child.on("close", (code, signal) => emitExit(code, signal));
5393
+ const transport = {
5394
+ write(chunk) {
5395
+ if (!alive || !child.stdin.writable) return false;
5396
+ child.stdin.write(chunk);
5397
+ return true;
5398
+ },
5399
+ onMessage,
5400
+ stderr() {
5401
+ return stderrBuf.toString("utf8");
5402
+ },
5403
+ onExit: null,
5404
+ dispose() {
5405
+ if (!alive) return;
5406
+ child.kill();
5407
+ setTimeout(() => {
5408
+ if (!alive) return;
5409
+ alive = false;
5410
+ running.delete(child);
5411
+ try {
5412
+ if (process.platform === "win32") spawn("taskkill", [
5413
+ "/PID",
5414
+ String(child.pid),
5415
+ "/T",
5416
+ "/F"
5417
+ ], {
5418
+ windowsHide: true,
5419
+ stdio: "ignore"
5420
+ });
5421
+ else child.kill("SIGKILL");
5422
+ } catch (error) {
5423
+ logger?.("force kill error: " + String(error));
5424
+ }
5425
+ }, 1500).unref?.();
5426
+ },
5427
+ get alive() {
5428
+ return alive;
5429
+ },
5430
+ get pid() {
5431
+ return child.pid;
5290
5432
  }
5433
+ };
5434
+ child.stdout.on("data", (chunk) => {
5435
+ for (const message of parser.push(chunk)) transport.onMessage?.(message);
5291
5436
  });
5292
- registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher), (warning) => warnings.push(warning));
5293
- installIsolation(ctx);
5294
- sweepTreeCache();
5295
- logCompatSummary(ctx, warnings);
5296
- ctx.logger?.info?.("[dsh-vscode-mode] 编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用)");
5437
+ return transport;
5438
+ }
5439
+ /** 强杀全部运行中的 LSP 子进程(插件卸载/退出用)。 */
5440
+ function disposeAllServers() {
5441
+ for (const child of running) try {
5442
+ if (process.platform === "win32" && child.pid) spawn("taskkill", [
5443
+ "/PID",
5444
+ String(child.pid),
5445
+ "/T",
5446
+ "/F"
5447
+ ], {
5448
+ windowsHide: true,
5449
+ stdio: "ignore"
5450
+ });
5451
+ else child.kill("SIGKILL");
5452
+ } catch (error) {}
5453
+ running.clear();
5454
+ }
5455
+ //#endregion
5456
+ //#region src/lsp/client.ts
5457
+ /**
5458
+ * dsh-vscode-mode host — LSP 客户端(JSON-RPC over stdio)。
5459
+ * 职责:请求/通知/握手/超时/服务器端请求处理/生命周期。transport 无关性:
5460
+ * 只依赖 write/onMessage 契约,便于单测时注入假传输。
5461
+ * 作者 ddj 2026-08-27
5462
+ */
5463
+ const DEFAULT_TIMEOUT_MS = 2e4;
5464
+ var LspError = class extends Error {
5465
+ code;
5466
+ constructor(code, message) {
5467
+ super(message);
5468
+ this.code = code;
5469
+ }
5470
+ };
5471
+ /**
5472
+ * 创建 LSP 客户端。
5473
+ * @author ddj 2026年08月27号
5474
+ * @param transport 传输句柄(write/onMessage/alive/dispose)
5475
+ * @param events 事件回调
5476
+ * @returns LSP 客户端
5477
+ */
5478
+ function createLspClient(transport, events = {}) {
5479
+ let nextId = 1;
5480
+ let alive = transport.alive;
5481
+ const pending = /* @__PURE__ */ new Map();
5482
+ let serverCapabilities = {
5483
+ definition: false,
5484
+ references: false,
5485
+ documentSymbol: false,
5486
+ workspaceSymbol: false,
5487
+ hover: false
5488
+ };
5489
+ const log = (line) => events.onLog?.(line);
5490
+ createFrameParser();
5491
+ transport.onMessage = (message) => {
5492
+ const msg = message;
5493
+ if (typeof msg.method === "string") {
5494
+ if (msg.method === "$/progress") events.onProgress?.(msg.params);
5495
+ handleServerRequest(msg);
5496
+ return;
5497
+ }
5498
+ if (typeof msg.id === "number" || typeof msg.id === "string") {
5499
+ const entry = pending.get(Number(msg.id));
5500
+ if (!entry) return;
5501
+ pending.delete(Number(msg.id));
5502
+ clearTimeout(entry.timer);
5503
+ if (msg.error) {
5504
+ const err = msg.error;
5505
+ entry.reject(new LspError(err.code ?? -32e3, err.message ?? "LSP error"));
5506
+ } else entry.resolve(msg.result);
5507
+ }
5508
+ };
5509
+ /** 服务器端请求:消息/日志类打日志,配置/能力类回 result:null,其余回 method not found。 */
5510
+ const handleServerRequest = async (msg) => {
5511
+ const { method, params, id } = msg;
5512
+ const respond = (payload) => {
5513
+ transport.write(encodeMessage(payload));
5514
+ };
5515
+ try {
5516
+ if (method === "window/logMessage" || method === "window/showMessage" || method === "window/showMessageRequest") {
5517
+ const p = params;
5518
+ log("[server " + (p?.type ?? "") + "] " + String(p?.message ?? ""));
5519
+ if (id !== void 0) respond({
5520
+ jsonrpc: "2.0",
5521
+ id,
5522
+ result: null
5523
+ });
5524
+ return;
5525
+ }
5526
+ if (method === "workspace/configuration") {
5527
+ const p = params;
5528
+ const items = Array.isArray(p?.items) ? p.items.map(() => null) : [];
5529
+ if (id !== void 0) respond({
5530
+ jsonrpc: "2.0",
5531
+ id,
5532
+ result: items
5533
+ });
5534
+ return;
5535
+ }
5536
+ if (method === "window/workDoneProgress/create" || method === "client/registerCapability" || method === "workspace/workspaceFolders") {
5537
+ if (id !== void 0) respond({
5538
+ jsonrpc: "2.0",
5539
+ id,
5540
+ result: null
5541
+ });
5542
+ return;
5543
+ }
5544
+ if (id !== void 0) respond({
5545
+ jsonrpc: "2.0",
5546
+ id,
5547
+ error: {
5548
+ code: -32601,
5549
+ message: "Method not found: " + method
5550
+ }
5551
+ });
5552
+ } catch (error) {
5553
+ log("server request handler error: " + String(error));
5554
+ if (id !== void 0) respond({
5555
+ jsonrpc: "2.0",
5556
+ id,
5557
+ error: {
5558
+ code: -32603,
5559
+ message: String(error)
5560
+ }
5561
+ });
5562
+ }
5563
+ };
5564
+ const transportExit = transport.onExit;
5565
+ transport.onExit = (code, signal) => {
5566
+ alive = false;
5567
+ for (const entry of pending.values()) {
5568
+ clearTimeout(entry.timer);
5569
+ entry.reject(/* @__PURE__ */ new Error("LSP 服务器已退出(" + String(code) + "/" + String(signal) + ")"));
5570
+ }
5571
+ pending.clear();
5572
+ events.onExit?.(code, signal);
5573
+ transportExit?.(code, signal);
5574
+ };
5575
+ const client = {
5576
+ async request(method, params, timeoutMs = DEFAULT_TIMEOUT_MS) {
5577
+ if (!alive) throw new Error("LSP 服务器未运行");
5578
+ const id = nextId++;
5579
+ return new Promise((resolve, reject) => {
5580
+ const timer = setTimeout(() => {
5581
+ pending.delete(id);
5582
+ reject(/* @__PURE__ */ new Error("LSP 请求超时:" + method + "(" + timeoutMs + "ms)"));
5583
+ }, timeoutMs);
5584
+ pending.set(id, {
5585
+ resolve,
5586
+ reject,
5587
+ timer
5588
+ });
5589
+ transport.write(encodeMessage(requestMessage(id, method, params)));
5590
+ });
5591
+ },
5592
+ notify(method, params) {
5593
+ if (!alive) return;
5594
+ transport.write(encodeMessage(notifyMessage(method, params)));
5595
+ },
5596
+ async initialize(rootUri, workspaceFolders) {
5597
+ const caps = (await client.request("initialize", {
5598
+ processId: process.pid,
5599
+ rootUri: rootUri || null,
5600
+ rootPath: null,
5601
+ workspaceFolders: workspaceFolders.length ? workspaceFolders.map((uri) => ({
5602
+ uri,
5603
+ name: uri
5604
+ })) : null,
5605
+ capabilities: {
5606
+ window: { workDoneProgress: true },
5607
+ workspace: { configuration: true },
5608
+ textDocument: {
5609
+ synchronization: {
5610
+ dynamicRegistration: false,
5611
+ willSave: false,
5612
+ didSave: true
5613
+ },
5614
+ definition: { dynamicRegistration: false },
5615
+ references: { dynamicRegistration: false },
5616
+ documentSymbol: { dynamicRegistration: false },
5617
+ hover: { dynamicRegistration: false }
5618
+ }
5619
+ }
5620
+ }))?.capabilities ?? {};
5621
+ serverCapabilities = {
5622
+ definition: Boolean(caps.definitionProvider),
5623
+ references: Boolean(caps.referencesProvider),
5624
+ documentSymbol: Boolean(caps.documentSymbolProvider),
5625
+ workspaceSymbol: Boolean(caps.workspaceSymbolProvider),
5626
+ hover: Boolean(caps.hoverProvider)
5627
+ };
5628
+ client.notify("initialized", {});
5629
+ events.onReady?.(serverCapabilities);
5630
+ return serverCapabilities;
5631
+ },
5632
+ async shutdown() {
5633
+ try {
5634
+ await client.request("shutdown", null, 3e3);
5635
+ } catch (error) {
5636
+ log("shutdown: " + String(error));
5637
+ }
5638
+ try {
5639
+ client.notify("exit", null);
5640
+ } catch (error) {
5641
+ log("exit: " + String(error));
5642
+ }
5643
+ },
5644
+ get alive() {
5645
+ return alive;
5646
+ }
5647
+ };
5648
+ return client;
5649
+ }
5650
+ //#endregion
5651
+ //#region src/lsp/uri.ts
5652
+ /**
5653
+ * dsh-vscode-mode host — 路径 ↔ file:// URI 转换(纯函数,node 可测)。
5654
+ * LSP 的 uri 字段必须是 file:// 绝对 URI;客户端 Monaco 用 edrv:///<encodeURI(path)>。
5655
+ * 作者 ddj 2026-08-27
5656
+ */
5657
+ /**
5658
+ * 绝对路径 → file:// URI(Windows 盘符保留:D:/x → file:///D:/x)。
5659
+ * @author ddj 2026年08月27号
5660
+ * @param absPath 绝对路径(正/反斜杠均可)
5661
+ * @returns file:// URI
5662
+ */
5663
+ function pathToFileUri(absPath) {
5664
+ const normalized = absPath.replace(/\\/g, "/");
5665
+ const drive = /^([A-Za-z]:)(?:\/(.*))?$/.exec(normalized);
5666
+ if (drive) {
5667
+ const tail = drive[2] ? "/" + drive[2].split("/").map(encodeURIComponent).join("/") : "";
5668
+ return "file:///" + drive[1] + tail;
5669
+ }
5670
+ return pathToFileURL(normalized).toString();
5671
+ }
5672
+ //#endregion
5673
+ //#region src/lsp/server.ts
5674
+ /**
5675
+ * dsh-vscode-mode host — 单语言服务器会话(绑定 工作区根 + 语言)。
5676
+ * 负责:启动传输 + LSP 客户端握手、文档同步(didOpen/didChange/didSave/didClose)、
5677
+ * 请求封装(definition/references/documentSymbol/workspaceSymbol/hover)、状态上报。
5678
+ * 作者 ddj 2026-08-27
5679
+ */
5680
+ /**
5681
+ * 创建语言服务器会话。
5682
+ * @author ddj 2026年08月27号
5683
+ * @param spec provider 规格(argv/cwd)
5684
+ * @param root 工作区根(绝对路径)
5685
+ * @param languageId 语言 id
5686
+ * @param logger 诊断日志
5687
+ * @returns 会话句柄
5688
+ */
5689
+ function createLspServer(spec, root, languageId, logger) {
5690
+ const rootUri = pathToFileUri(root);
5691
+ const docs = /* @__PURE__ */ new Map();
5692
+ let phase = "idle";
5693
+ let transport = null;
5694
+ let client = null;
5695
+ let capabilities = {
5696
+ definition: false,
5697
+ references: false,
5698
+ documentSymbol: false,
5699
+ workspaceSymbol: false,
5700
+ hover: false
5701
+ };
5702
+ let disposed = false;
5703
+ let readyWait = Promise.resolve(false);
5704
+ let resolveReady = null;
5705
+ let lastProgress = {};
5706
+ let lastStatus = {
5707
+ languageId,
5708
+ source: spec.kind,
5709
+ phase: "idle",
5710
+ reason: spec.ready ? void 0 : spec.reason,
5711
+ root
5712
+ };
5713
+ const log = (line) => logger?.("[" + languageId + "@" + root + "] " + line);
5714
+ const setPhase = (next, reason) => {
5715
+ phase = next;
5716
+ lastStatus = {
5717
+ languageId,
5718
+ source: spec.kind,
5719
+ phase: next,
5720
+ reason,
5721
+ root,
5722
+ ...progressFields()
5723
+ };
5724
+ server.onStateChange?.(lastStatus);
5725
+ };
5726
+ /** 返回当前进度字段,避免空进度污染状态载荷。 */
5727
+ const progressFields = () => ({
5728
+ progress: lastProgress.value,
5729
+ progressMessage: lastProgress.message
5730
+ });
5731
+ /** 处理 LSP $/progress 通知并映射为编辑器状态。 */
5732
+ const applyProgress = (params) => {
5733
+ const report = params?.value;
5734
+ if (report?.kind === "begin") {
5735
+ lastProgress = {
5736
+ value: 0,
5737
+ message: typeof report.message === "string" ? report.message : "正在解析工作区"
5738
+ };
5739
+ if (phase === "ready") phase = "indexing";
5740
+ } else if (report?.kind === "end") {
5741
+ lastProgress = {
5742
+ value: 100,
5743
+ message: typeof report.message === "string" ? report.message : "解析完成"
5744
+ };
5745
+ if (phase === "indexing") phase = "ready";
5746
+ } else if (typeof report?.percentage === "number") {
5747
+ lastProgress = {
5748
+ value: Math.max(0, Math.min(100, report.percentage)),
5749
+ message: typeof report.message === "string" ? report.message : lastProgress.message
5750
+ };
5751
+ if (phase === "ready") phase = "indexing";
5752
+ } else if (typeof report?.message === "string") lastProgress = {
5753
+ ...lastProgress,
5754
+ message: report.message
5755
+ };
5756
+ lastStatus = {
5757
+ ...lastStatus,
5758
+ phase,
5759
+ ...progressFields()
5760
+ };
5761
+ server.onStateChange?.(lastStatus);
5762
+ };
5763
+ const ensureClient = () => {
5764
+ if (!client) throw new Error("LSP 服务器未启动");
5765
+ return client;
5766
+ };
5767
+ const server = {
5768
+ languageId,
5769
+ root,
5770
+ get phase() {
5771
+ return phase;
5772
+ },
5773
+ get capabilities() {
5774
+ return capabilities;
5775
+ },
5776
+ status() {
5777
+ return lastStatus;
5778
+ },
5779
+ onStateChange: null,
5780
+ async start() {
5781
+ if (disposed) return false;
5782
+ if (phase === "starting") return readyWait;
5783
+ if (phase === "ready" || phase === "indexing") return true;
5784
+ readyWait = new Promise((resolve) => {
5785
+ resolveReady = resolve;
5786
+ });
5787
+ lastProgress = {};
5788
+ setPhase("starting");
5789
+ try {
5790
+ transport = spawnServer({
5791
+ argv: spec.argv,
5792
+ cwd: spec.cwd ?? root
5793
+ }, (message) => {}, (line) => log("stderr: " + line));
5794
+ client = createLspClient(transport, {
5795
+ onReady: (caps) => {
5796
+ capabilities = caps;
5797
+ for (const doc of docs.values()) client?.notify("textDocument/didOpen", { textDocument: {
5798
+ uri: doc.uri,
5799
+ languageId,
5800
+ version: doc.version,
5801
+ text: doc.text
5802
+ } });
5803
+ setPhase("ready");
5804
+ resolveReady?.(true);
5805
+ resolveReady = null;
5806
+ log("ready: definition=" + caps.definition + " references=" + caps.references + " symbols=" + caps.documentSymbol);
5807
+ },
5808
+ onProgress: (params) => applyProgress(params),
5809
+ onExit: (code, signal) => {
5810
+ if (disposed) return;
5811
+ resolveReady?.(false);
5812
+ resolveReady = null;
5813
+ log("server exited (" + String(code) + "/" + String(signal) + ")");
5814
+ client = null;
5815
+ transport = null;
5816
+ setPhase("idle", "服务器已退出(" + String(code) + "/" + String(signal) + "),等待重启");
5817
+ },
5818
+ onLog: (line) => log("lsp: " + line)
5819
+ });
5820
+ capabilities = await client.initialize(rootUri, [rootUri]);
5821
+ return true;
5822
+ } catch (error) {
5823
+ log("start failed: " + String(error));
5824
+ setPhase("unavailable", "启动失败:" + String(error));
5825
+ resolveReady?.(false);
5826
+ resolveReady = null;
5827
+ try {
5828
+ await server.dispose();
5829
+ } catch {}
5830
+ return false;
5831
+ }
5832
+ },
5833
+ sync(path, text, version) {
5834
+ const existing = docs.get(path);
5835
+ const uri = existing?.uri ?? pathToFileUri(root + "/" + path.split("/").join("/"));
5836
+ if (existing) {
5837
+ existing.version = version;
5838
+ existing.text = text;
5839
+ if (phase === "ready" || phase === "indexing") client?.notify("textDocument/didChange", {
5840
+ textDocument: {
5841
+ uri,
5842
+ version
5843
+ },
5844
+ contentChanges: [{ text }]
5845
+ });
5846
+ return;
5847
+ }
5848
+ docs.set(path, {
5849
+ path,
5850
+ uri,
5851
+ version,
5852
+ text
5853
+ });
5854
+ if (phase === "ready" || phase === "indexing") client?.notify("textDocument/didOpen", { textDocument: {
5855
+ uri,
5856
+ languageId,
5857
+ version,
5858
+ text
5859
+ } });
5860
+ },
5861
+ close(path) {
5862
+ const doc = docs.get(path);
5863
+ if (!doc) return;
5864
+ docs.delete(path);
5865
+ if (phase === "ready" || phase === "indexing") client?.notify("textDocument/didClose", { textDocument: { uri: doc.uri } });
5866
+ },
5867
+ async definition(path, line, character) {
5868
+ const doc = docs.get(path);
5869
+ if (!doc || !await readyWait) return [];
5870
+ return normalizeLocations(await ensureClient().request("textDocument/definition", {
5871
+ textDocument: { uri: doc.uri },
5872
+ position: {
5873
+ line,
5874
+ character
5875
+ }
5876
+ }));
5877
+ },
5878
+ async references(path, line, character, includeDeclaration) {
5879
+ const doc = docs.get(path);
5880
+ if (!doc || !await readyWait) return [];
5881
+ return normalizeLocations(await ensureClient().request("textDocument/references", {
5882
+ textDocument: { uri: doc.uri },
5883
+ position: {
5884
+ line,
5885
+ character
5886
+ },
5887
+ context: { includeDeclaration }
5888
+ }));
5889
+ },
5890
+ async documentSymbol(path) {
5891
+ const doc = docs.get(path);
5892
+ if (!doc || !await readyWait) return [];
5893
+ const result = await ensureClient().request("textDocument/documentSymbol", { textDocument: { uri: doc.uri } });
5894
+ if (!Array.isArray(result)) return [];
5895
+ return result.map(normalizeSymbol).filter((s) => s !== null);
5896
+ },
5897
+ async workspaceSymbol(query) {
5898
+ if (!await readyWait) return [];
5899
+ const result = await ensureClient().request("workspace/symbol", { query });
5900
+ if (!Array.isArray(result)) return [];
5901
+ return result.map(normalizeSymbol).filter((s) => s !== null);
5902
+ },
5903
+ async hover(path, line, character) {
5904
+ const doc = docs.get(path);
5905
+ if (!doc || !await readyWait) return null;
5906
+ const result = await ensureClient().request("textDocument/hover", {
5907
+ textDocument: { uri: doc.uri },
5908
+ position: {
5909
+ line,
5910
+ character
5911
+ }
5912
+ });
5913
+ if (!result || !result.contents) return null;
5914
+ return { contents: stringifyHoverContents(result.contents) };
5915
+ },
5916
+ async dispose() {
5917
+ disposed = true;
5918
+ resolveReady?.(false);
5919
+ resolveReady = null;
5920
+ if (client) {
5921
+ await client.shutdown().catch(() => {});
5922
+ client = null;
5923
+ }
5924
+ if (transport) {
5925
+ transport.dispose();
5926
+ transport = null;
5927
+ }
5928
+ setPhase("stopped");
5929
+ }
5930
+ };
5931
+ return server;
5932
+ }
5933
+ /** 归一化 definition/references 结果(Location | Location[] | LocationLink[])。 */
5934
+ function normalizeLocations(result) {
5935
+ if (!Array.isArray(result)) return [];
5936
+ const out = [];
5937
+ for (const item of result) {
5938
+ if (!item || typeof item !== "object") continue;
5939
+ const obj = item;
5940
+ const uri = typeof obj.targetUri === "string" ? obj.targetUri : typeof obj.uri === "string" ? obj.uri : "";
5941
+ const range = obj.targetRange ?? obj.range;
5942
+ if (!uri || !range || !range.start || !range.end) continue;
5943
+ out.push({
5944
+ uri,
5945
+ range: {
5946
+ start: {
5947
+ line: toInt(range.start.line),
5948
+ character: toInt(range.start.character)
5949
+ },
5950
+ end: {
5951
+ line: toInt(range.end.line),
5952
+ character: toInt(range.end.character)
5953
+ }
5954
+ }
5955
+ });
5956
+ }
5957
+ return out;
5958
+ }
5959
+ /** 归一化 documentSymbol/workspaceSymbol 结果(DocumentSymbol | SymbolInformation)。 */
5960
+ function normalizeSymbol(item) {
5961
+ if (!item || typeof item !== "object") return null;
5962
+ const obj = item;
5963
+ if (typeof obj.name !== "string") return null;
5964
+ const selection = obj.selectionRange ?? (obj.location && obj.location.range);
5965
+ const range = obj.range ?? selection;
5966
+ if (!range || typeof range !== "object") return null;
5967
+ const r = range;
5968
+ if (!r.start || !r.end) return null;
5969
+ const symbol = {
5970
+ name: obj.name,
5971
+ kind: toInt(obj.kind),
5972
+ range: {
5973
+ start: {
5974
+ line: toInt(r.start.line),
5975
+ character: toInt(r.start.character)
5976
+ },
5977
+ end: {
5978
+ line: toInt(r.end.line),
5979
+ character: toInt(r.end.character)
5980
+ }
5981
+ },
5982
+ selectionRange: selection ? {
5983
+ start: {
5984
+ line: toInt(selection.start?.line ?? toInt(r.start.line)),
5985
+ character: toInt(selection.start?.character ?? toInt(r.start.character))
5986
+ },
5987
+ end: {
5988
+ line: toInt(selection.end?.line ?? toInt(r.end.line)),
5989
+ character: toInt(selection.end?.character ?? toInt(r.end.character))
5990
+ }
5991
+ } : {
5992
+ start: {
5993
+ line: toInt(r.start.line),
5994
+ character: toInt(r.start.character)
5995
+ },
5996
+ end: {
5997
+ line: toInt(r.end.line),
5998
+ character: toInt(r.end.character)
5999
+ }
6000
+ }
6001
+ };
6002
+ if (typeof obj.detail === "string" && obj.detail) symbol.detail = obj.detail;
6003
+ if (typeof obj.containerName === "string" && obj.containerName) symbol.containerName = obj.containerName;
6004
+ if (Array.isArray(obj.children)) {
6005
+ const kids = obj.children.map(normalizeSymbol).filter((s) => s !== null);
6006
+ if (kids.length) symbol.children = kids;
6007
+ }
6008
+ return symbol;
6009
+ }
6010
+ /** hover contents → 文本行(MarkupContent | MarkedString | MarkedString[])。 */
6011
+ function stringifyHoverContents(contents) {
6012
+ if (typeof contents === "string") return [contents];
6013
+ if (Array.isArray(contents)) return contents.map(stringifyHoverContents).flat().filter(Boolean);
6014
+ if (contents && typeof contents === "object") {
6015
+ const value = contents.value;
6016
+ if (typeof value === "string") return [value];
6017
+ }
6018
+ return [];
6019
+ }
6020
+ function toInt(value) {
6021
+ const n = Number(value);
6022
+ return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
6023
+ }
6024
+ //#endregion
6025
+ //#region src/lsp/manager.ts
6026
+ /**
6027
+ * dsh-vscode-mode host — LSP 服务器管理器。
6028
+ * 实例注册表(key=root|languageId,refcount 共享),崩溃退避重启,session/disposed 清理。
6029
+ * 作者 ddj 2026-08-27
6030
+ */
6031
+ /**
6032
+ * 创建 LSP 管理器。
6033
+ * @author ddj 2026年08月27号
6034
+ * @param logger 诊断日志
6035
+ * @returns 管理器
6036
+ */
6037
+ function createLspManager(logger) {
6038
+ const entries = /* @__PURE__ */ new Map();
6039
+ const keyOf = (root, languageId) => root + "|" + languageId;
6040
+ const log = (line) => logger?.("[lsp-manager] " + line);
6041
+ /** 同步摘除注册表项并后台回收(refs 归零/工作区清理用;statusAll 立即可见归零)。 */
6042
+ const dropEntry = (entry) => {
6043
+ if (entry.restartTimer) {
6044
+ clearTimeout(entry.restartTimer);
6045
+ entry.restartTimer = null;
6046
+ }
6047
+ entries.delete(entry.key);
6048
+ entry.server.dispose().catch(() => {});
6049
+ };
6050
+ const manager = {
6051
+ acquire(root, languageId, resolveSpec) {
6052
+ const key = keyOf(root, languageId);
6053
+ let entry = entries.get(key);
6054
+ if (!entry) {
6055
+ const spec = resolveSpec(languageId);
6056
+ const server = createLspServer(spec, root, languageId, log);
6057
+ server.onStateChange = (status) => manager.onStatusChange?.(status);
6058
+ entry = {
6059
+ key,
6060
+ server,
6061
+ refs: 0,
6062
+ failures: 0,
6063
+ restartTimer: null
6064
+ };
6065
+ entries.set(key, entry);
6066
+ if (spec.ready) server.start().then((ok) => {
6067
+ if (!ok && entries.has(key)) entry.failures++;
6068
+ });
6069
+ }
6070
+ entry.refs++;
6071
+ return entry.server;
6072
+ },
6073
+ release(root, languageId) {
6074
+ const entry = entries.get(keyOf(root, languageId));
6075
+ if (!entry) return;
6076
+ entry.refs = Math.max(0, entry.refs - 1);
6077
+ if (entry.refs === 0) dropEntry(entry);
6078
+ },
6079
+ releaseRoot(root) {
6080
+ for (const entry of [...entries.values()]) if (entry.server.root === root) dropEntry(entry);
6081
+ },
6082
+ statusAll() {
6083
+ return [...entries.values()].map((entry) => entry.server.status());
6084
+ },
6085
+ peek(root, languageId) {
6086
+ return entries.get(keyOf(root, languageId))?.server;
6087
+ },
6088
+ async disposeAll() {
6089
+ const list = [...entries.values()];
6090
+ entries.clear();
6091
+ for (const entry of list) {
6092
+ if (entry.restartTimer) clearTimeout(entry.restartTimer);
6093
+ await entry.server.dispose();
6094
+ }
6095
+ },
6096
+ onStatusChange: null
6097
+ };
6098
+ return manager;
6099
+ }
6100
+ //#endregion
6101
+ //#region src/lsp/zip.ts
6102
+ /**
6103
+ * dsh-vscode-mode host — 纯 Node ZIP 解压(零依赖,跨平台)。
6104
+ * 供 extmgr 解包 VSIX 使用;只实现 VSIX 需要的子集:
6105
+ * 读取 EOCD + 中央目录,支持 stored(0)/deflate(8),忽略 zip64(VSIX < 4GB)。
6106
+ * 作者 ddj 2026-08-27
6107
+ */
6108
+ const EOCD_SIG = 101010256;
6109
+ const CDIR_SIG = 33639248;
6110
+ const LHDR_SIG = 67324752;
6111
+ /**
6112
+ * 解析 ZIP 缓冲区 → 条目列表(不解压内容,仅元数据)。
6113
+ * @author ddj 2026年08月27号
6114
+ * @param buf zip 字节
6115
+ * @returns 条目元数据
6116
+ */
6117
+ function zipEntries(buf) {
6118
+ const eocd = findEocd(buf);
6119
+ if (eocd < 0) throw new Error("不是有效的 ZIP 文件(找不到 EOCD)");
6120
+ const total = buf.readUInt16LE(eocd + 10);
6121
+ let offset = buf.readUInt32LE(eocd + 16);
6122
+ const out = [];
6123
+ for (let i = 0; i < total; i++) {
6124
+ if (buf.readUInt32LE(offset) !== CDIR_SIG) throw new Error("ZIP 中央目录损坏");
6125
+ const method = buf.readUInt16LE(offset + 10);
6126
+ const compressed = buf.readUInt32LE(offset + 20);
6127
+ const uncompressed = buf.readUInt32LE(offset + 24);
6128
+ const nameLen = buf.readUInt16LE(offset + 28);
6129
+ const extraLen = buf.readUInt16LE(offset + 30);
6130
+ const commentLen = buf.readUInt16LE(offset + 32);
6131
+ const localOffset = buf.readUInt32LE(offset + 42);
6132
+ const name = buf.subarray(offset + 46, offset + 46 + nameLen).toString("utf8");
6133
+ out.push({
6134
+ name,
6135
+ method,
6136
+ compressed,
6137
+ uncompressed,
6138
+ localOffset
6139
+ });
6140
+ offset += 46 + nameLen + extraLen + commentLen;
6141
+ }
6142
+ return out;
6143
+ }
6144
+ /**
6145
+ * 解压 ZIP → 条目列表(目录条目含尾部 /)。
6146
+ * @author ddj 2026年08月27号
6147
+ * @param buf zip 字节
6148
+ * @returns 条目(含内容)
6149
+ */
6150
+ function unzip(buf) {
6151
+ const metas = zipEntries(buf);
6152
+ const out = [];
6153
+ for (const meta of metas) {
6154
+ const lh = meta.localOffset;
6155
+ if (buf.readUInt32LE(lh) !== LHDR_SIG) throw new Error("ZIP 本地头损坏:" + meta.name);
6156
+ const nameLen = buf.readUInt16LE(lh + 26);
6157
+ const extraLen = buf.readUInt16LE(lh + 28);
6158
+ const dataStart = lh + 30 + nameLen + extraLen;
6159
+ const isDir = /\/$/.test(meta.name);
6160
+ let data = Buffer.alloc(0);
6161
+ if (!isDir && meta.compressed > 0) {
6162
+ const raw = buf.subarray(dataStart, dataStart + meta.compressed);
6163
+ data = meta.method === 8 ? Buffer.from(inflateRawSync(raw)) : meta.method === 0 ? Buffer.from(raw) : Buffer.alloc(0);
6164
+ }
6165
+ out.push({
6166
+ path: meta.name,
6167
+ data,
6168
+ isDirectory: isDir
6169
+ });
6170
+ }
6171
+ return out;
6172
+ }
6173
+ /** 定位 EOCD(从尾部向前扫,容忍尾部注释)。 */
6174
+ function findEocd(buf) {
6175
+ const maxScan = Math.min(buf.length, 65557);
6176
+ for (let i = buf.length - 22; i >= buf.length - maxScan; i--) {
6177
+ if (i < 0) break;
6178
+ if (buf.readUInt32LE(i) === EOCD_SIG) return i;
6179
+ }
6180
+ return -1;
6181
+ }
6182
+ //#endregion
6183
+ //#region src/lsp/extmgr.ts
6184
+ /**
6185
+ * dsh-vscode-mode host — VSIX 扩展管理器(extmgr)。
6186
+ * 管理 DSH 专属扩展目录(~/.dsh/dsh-vscode-mode/extensions/),支持:
6187
+ * 本地 .vsix 安装、Open VSX 市场搜索/下载安装、卸载、更新检测与安装。
6188
+ * 已装扩展作为 LSP provider 的"扩展源"(kind=extension),见 providers.ts。
6189
+ * 作者 ddj 2026-08-27
6190
+ */
6191
+ const OPEN_VSX = "https://open-vsx.org";
6192
+ const META_FILE = "package.json";
6193
+ /** 当前平台(与 VSCode platform 命名对齐:win32-x64/darwin-arm64/linux-x64)。 */
6194
+ const PLATFORM = (() => {
6195
+ if (process.platform === "win32") return process.arch === "arm64" ? "win32-arm64" : "win32-x64";
6196
+ if (process.platform === "darwin") return process.arch === "arm64" ? "darwin-arm64" : "darwin-x64";
6197
+ return process.arch === "arm64" ? "linux-arm64" : "linux-x64";
6198
+ })();
6199
+ /** DSH 专属扩展目录。 */
6200
+ function extensionsRoot(home = dshHome()) {
6201
+ return join(home, PLUGIN_ID, "extensions");
6202
+ }
6203
+ /** 从 VSIX 字节里读 extension/package.json 清单。 */
6204
+ function vsixManifest(vsix) {
6205
+ for (const entry of unzip(vsix)) if (entry.path === "extension/package.json" || entry.path === "extension\\package.json") return JSON.parse(entry.data.toString("utf8"));
6206
+ throw new Error("VSIX 缺少 extension/package.json");
6207
+ }
6208
+ /**
6209
+ * 解包 VSIX 到目录(仅取 extension/ 子树,剥前缀;返回清单)。
6210
+ * @author ddj 2026年08月27号
6211
+ * @param vsix vsix 字节
6212
+ * @param dest 目标目录(已存在则先清空)
6213
+ * @returns 清单对象
6214
+ */
6215
+ function unpackVsix(vsix, dest) {
6216
+ rmSync(dest, {
6217
+ recursive: true,
6218
+ force: true
6219
+ });
6220
+ mkdirSync(dest, { recursive: true });
6221
+ let manifest = null;
6222
+ for (const entry of unzip(vsix)) {
6223
+ const rel = entry.path.replace(/^extension[\\/]/, "");
6224
+ if (!rel || entry.path.startsWith("..")) continue;
6225
+ const safe = rel.split(/[\\/]/).filter((part) => part && part !== ".." && part !== ".").join("/");
6226
+ if (!safe) continue;
6227
+ const target = join(dest, ...safe.split("/"));
6228
+ if (entry.isDirectory) {
6229
+ mkdirSync(target, { recursive: true });
6230
+ continue;
6231
+ }
6232
+ mkdirSync(dirname(target), { recursive: true });
6233
+ writeFileSync(target, entry.data);
6234
+ if (rel === "package.json") manifest = JSON.parse(entry.data.toString("utf8"));
6235
+ }
6236
+ if (!manifest) throw new Error("VSIX 解包后缺少 package.json");
6237
+ return manifest;
6238
+ }
6239
+ /** 扩展信息(清单 + 目录)。 */
6240
+ function extInfoOf(manifest, dir) {
6241
+ const namespace = String(manifest.publisher ?? "unknown");
6242
+ const name = String(manifest.name ?? "unknown");
6243
+ const version = String(manifest.version ?? "0.0.0");
6244
+ return {
6245
+ id: namespace + "." + name,
6246
+ namespace,
6247
+ name,
6248
+ version,
6249
+ displayName: typeof manifest.displayName === "string" ? manifest.displayName : void 0,
6250
+ description: typeof manifest.description === "string" ? manifest.description : void 0,
6251
+ dirName: basename(dir),
6252
+ dir
6253
+ };
6254
+ }
6255
+ /** 安装本地 .vsix 文件(已存在同 id 则升级替换)。 */
6256
+ async function installVsixFile(vsixPath, home = dshHome()) {
6257
+ return installVsixBuffer(readFileSync(vsixPath), home);
6258
+ }
6259
+ /** 安装 VSIX 字节(解包进 extensionsRoot;同名旧版本先清)。 */
6260
+ function installVsixBuffer(vsix, home = dshHome()) {
6261
+ const info = extInfoOf(vsixManifest(vsix), "");
6262
+ const target = join(extensionsRoot(home), info.id + "-" + info.version);
6263
+ return extInfoOf(unpackVsix(vsix, target), target);
6264
+ }
6265
+ /** 扫描已装扩展目录。 */
6266
+ function listInstalled(home = dshHome()) {
6267
+ const root = extensionsRoot(home);
6268
+ if (!existsSync(root)) return [];
6269
+ const out = [];
6270
+ for (const dir of readdirSync(root)) {
6271
+ const full = join(root, dir);
6272
+ const meta = join(full, META_FILE);
6273
+ if (!existsSync(meta)) continue;
6274
+ try {
6275
+ const manifest = JSON.parse(readFileSync(meta, "utf8"));
6276
+ out.push(extInfoOf(manifest, full));
6277
+ } catch (error) {}
6278
+ }
6279
+ return out;
6280
+ }
6281
+ /** 卸载扩展(按 id)。 */
6282
+ function uninstall(id, home = dshHome()) {
6283
+ const target = listInstalled(home).find((e) => e.id === id)?.dir;
6284
+ if (!target) return false;
6285
+ rmSync(target, {
6286
+ recursive: true,
6287
+ force: true
6288
+ });
6289
+ return true;
6290
+ }
6291
+ /** Open VSX 单扩展元数据(latest + 下载 URL)。 */
6292
+ async function marketGet(namespace, name, version) {
6293
+ const path = version ? namespace + "/" + name + "/" + version : namespace + "/" + name;
6294
+ const res = await fetch(OPEN_VSX + "/api/" + path, { headers: { accept: "application/json" } });
6295
+ if (!res.ok) return null;
6296
+ return await res.json();
6297
+ }
6298
+ /** Open VSX 搜索。 */
6299
+ async function marketSearch(query, size = 12) {
6300
+ const url = OPEN_VSX + "/api/-/search?query=" + encodeURIComponent(query) + "&size=" + size;
6301
+ const res = await fetch(url, { headers: { accept: "application/json" } });
6302
+ if (!res.ok) throw new Error("市场搜索失败:HTTP " + res.status);
6303
+ return ((await res.json()).extensions ?? []).map((item) => ({
6304
+ namespace: String(item.namespace ?? ""),
6305
+ name: String(item.name ?? ""),
6306
+ id: String(item.namespace ?? "") + "." + String(item.name ?? ""),
6307
+ version: String(item.version ?? ""),
6308
+ displayName: typeof item.displayName === "string" ? item.displayName : void 0,
6309
+ description: typeof item.description === "string" ? item.description : void 0,
6310
+ download: typeof item.files?.download === "string" ? item.files.download : void 0
6311
+ }));
6312
+ }
6313
+ /** 平台匹配的 VSIX 下载 URL(探测平台变体,失败回退 API 默认)。 */
6314
+ async function resolveDownload(namespace, name, version) {
6315
+ const candidate = OPEN_VSX + "/api/" + namespace + "/" + name + "/" + PLATFORM + "/" + version + "/file/" + namespace + "." + name + "-" + version + "@" + PLATFORM + ".vsix";
6316
+ try {
6317
+ if ((await fetch(candidate, { method: "HEAD" })).ok) return candidate;
6318
+ } catch (error) {}
6319
+ const download = ((await marketGet(namespace, name, version))?.files)?.download;
6320
+ if (typeof download === "string") return download;
6321
+ throw new Error("市场未提供 " + namespace + "." + name + "@" + version + " 的下载地址");
6322
+ }
6323
+ /** 从 Open VSX 安装/更新扩展。 */
6324
+ async function installFromMarket(namespace, name, version) {
6325
+ const meta = await marketGet(namespace, name, version);
6326
+ if (!meta) throw new Error("市场找不到 " + namespace + "." + name + (version ? "@" + version : ""));
6327
+ const url = await resolveDownload(namespace, name, version ?? String(meta.version ?? ""));
6328
+ const res = await fetch(url);
6329
+ if (!res.ok) throw new Error("下载失败:HTTP " + res.status);
6330
+ return installVsixBuffer(Buffer.from(await res.arrayBuffer()));
6331
+ }
6332
+ /** 更新已装扩展(对比市场最新版;返回 null 表示已最新)。 */
6333
+ async function updateExtension(id) {
6334
+ const installed = listInstalled().find((e) => e.id === id);
6335
+ if (!installed) throw new Error("扩展未安装:" + id);
6336
+ const meta = await marketGet(installed.namespace, installed.name);
6337
+ const latest = String(meta?.version ?? "");
6338
+ if (!latest || latest === installed.version) return null;
6339
+ return installFromMarket(installed.namespace, installed.name, latest);
6340
+ }
6341
+ //#endregion
6342
+ //#region src/lsp/providers.ts
6343
+ /**
6344
+ * dsh-vscode-mode host — 语言服务器 provider 解析。
6345
+ * 优先级:手动配置 > extmgr 扩展源(Phase 2 挂载) > 自动发现(~/.vscode/extensions、
6346
+ * ~/.dsh/dsh-vscode-mode/extensions、PATH)。产物 LspProviderSpec:{ ready, argv, cwd, reason },
6347
+ * 供 manager 启动服务器。
6348
+ * 作者 ddj 2026-08-27
6349
+ */
6350
+ /** 平台目录名(LuaLS/OmniSharp 的 server/bin/<平台>)。 */
6351
+ function platformServerDir(platform = process.platform) {
6352
+ if (platform === "win32") return "Windows";
6353
+ if (platform === "darwin") return "macOS";
6354
+ return "Linux";
6355
+ }
6356
+ /** exe 后缀(Windows 为 .exe)。 */
6357
+ function exeSuffix(platform = process.platform) {
6358
+ return platform === "win32" ? ".exe" : "";
6359
+ }
6360
+ /** 列出目录下匹配前缀的子目录(如 ~/.vscode/extensions/sumneko.lua-*)。 */
6361
+ function listMatchingDirs(dir, prefix) {
6362
+ if (!existsSync(dir)) return [];
6363
+ let entries;
6364
+ try {
6365
+ entries = readdirSync(dir, { withFileTypes: true });
6366
+ } catch (error) {
6367
+ return [];
6368
+ }
6369
+ return entries.filter((e) => e.isDirectory() && e.name.startsWith(prefix)).map((e) => join(dir, e.name));
6370
+ }
6371
+ /** VSCode 扩展目录(~/.vscode/extensions、~/.vscode-server/extensions + extmgr 目录;无则空数组)。 */
6372
+ function vscodeExtensionsDirs(home = dshHome()) {
6373
+ const userHome = homedir();
6374
+ return [
6375
+ join(userHome, ".vscode", "extensions"),
6376
+ join(userHome, ".vscode-server", "extensions"),
6377
+ extensionsRoot(home)
6378
+ ].filter((dir) => existsSync(dir));
6379
+ }
6380
+ /** 扩展源 provider 注册表:languageId → 启动参数。 */
6381
+ const extProviders = /* @__PURE__ */ new Map();
6382
+ /** 取扩展源 provider spec(未注册返回 null)。 */
6383
+ function extensionProviderFor(languageId) {
6384
+ const p = extProviders.get(languageId);
6385
+ if (!p || !p.argv.length) return null;
6386
+ return {
6387
+ languageId,
6388
+ kind: "extension",
6389
+ argv: p.argv,
6390
+ cwd: p.cwd,
6391
+ ready: true
6392
+ };
6393
+ }
6394
+ /** PATH 中查找可执行文件(不含扩展名校验,交由 spawn 报错)。 */
6395
+ function findInPath(name) {
6396
+ const pathEnv = process.env.PATH ?? "";
6397
+ const exts = process.platform === "win32" ? [
6398
+ "",
6399
+ ".exe",
6400
+ ".cmd",
6401
+ ".bat"
6402
+ ] : [""];
6403
+ for (const dir of pathEnv.split(sep === "\\" ? ";" : ":")) {
6404
+ if (!dir) continue;
6405
+ for (const ext of exts) {
6406
+ const candidate = join(dir, name + ext);
6407
+ if (existsSync(candidate)) return candidate;
6408
+ }
6409
+ }
6410
+ return null;
6411
+ }
6412
+ /** 查找 dotnet(PATH + 常见安装路径)。 */
6413
+ function findDotnet() {
6414
+ const inPath = findInPath("dotnet");
6415
+ if (inPath) return inPath;
6416
+ for (const c of ["C:/Program Files/dotnet/dotnet.exe", "C:/Program Files (x86)/dotnet/dotnet.exe"]) if (existsSync(c)) return c;
6417
+ return null;
6418
+ }
6419
+ /** LuaLS 自动发现候选(VSCode 扩展目录 + extmgr 目录下 sumneko.lua-* 的 server/bin 可执行)。 */
6420
+ function candidateLuaServers(home = dshHome()) {
6421
+ const plat = platformServerDir();
6422
+ const suffix = exeSuffix();
6423
+ const out = [];
6424
+ for (const dir of vscodeExtensionsDirs(home)) for (const extDir of listMatchingDirs(dir, "sumneko.lua-")) {
6425
+ const binDir = join(extDir, "server", "bin");
6426
+ const binName = "lua-language-server" + suffix;
6427
+ const bin = existsSync(join(binDir, binName)) ? join(binDir, binName) : join(binDir, plat, binName);
6428
+ if (existsSync(bin) && !out.includes(bin)) out.push(bin);
6429
+ }
6430
+ return out;
6431
+ }
6432
+ /** C# 自动发现候选:Roslyn dll(需 dotnet)或 OmniSharp 可执行。 */
6433
+ function candidateCSharpServers(home = dshHome()) {
6434
+ const out = {};
6435
+ for (const dir of vscodeExtensionsDirs(home)) for (const extDir of listMatchingDirs(dir, "ms-dotnettools.csharp-")) {
6436
+ const roslyn = join(extDir, ".roslyn", "Microsoft.CodeAnalysis.LanguageServer.dll");
6437
+ if (!out.roslynDll && existsSync(roslyn)) out.roslynDll = roslyn;
6438
+ const omni = join(extDir, ".omnisharp", "OmniSharp" + exeSuffix());
6439
+ if (!out.omnisharpExe && existsSync(omni)) out.omnisharpExe = omni;
6440
+ }
6441
+ const dotnet = findDotnet();
6442
+ if (dotnet) out.dotnet = dotnet;
6443
+ return out;
6444
+ }
6445
+ /**
6446
+ * 手动配置归一化:path 指向可执行文件/目录,command 为可执行名(PATH 解析)。
6447
+ * 未配置时返回空 reason(由各 resolve 兜底输出语言相关安装指引文案)。
6448
+ * @author ddj 2026年08月27号
6449
+ */
6450
+ function manualSpec(manual) {
6451
+ if (!manual) return {
6452
+ argv: [],
6453
+ ready: false
6454
+ };
6455
+ const pathValue = typeof manual.path === "string" && manual.path.trim() ? manual.path.trim() : "";
6456
+ const commandValue = typeof manual.command === "string" && manual.command.trim() ? manual.command.trim() : "";
6457
+ if (!pathValue && !commandValue) return {
6458
+ argv: [],
6459
+ ready: false
6460
+ };
6461
+ if (pathValue) {
6462
+ if (!existsSync(pathValue)) return {
6463
+ argv: [],
6464
+ ready: false,
6465
+ reason: "路径不存在:" + pathValue
6466
+ };
6467
+ return {
6468
+ argv: [pathValue],
6469
+ ready: true
6470
+ };
6471
+ }
6472
+ const found = findInPath(commandValue);
6473
+ if (!found) return {
6474
+ argv: [],
6475
+ ready: false,
6476
+ reason: "PATH 中找不到命令:" + commandValue
6477
+ };
6478
+ return {
6479
+ argv: [found],
6480
+ ready: true
6481
+ };
6482
+ }
6483
+ /**
6484
+ * 解析 Lua provider(优先级:手动 > 扩展源 > 自动发现)。
6485
+ * @author ddj 2026年08月27号
6486
+ * @param manual 手动配置
6487
+ * @param home DSH home(缺省真实;测试可注入临时目录)
6488
+ * @returns provider 规格
6489
+ */
6490
+ function resolveLuaProvider(manual, home = dshHome()) {
6491
+ const base = { languageId: "lua" };
6492
+ const manualResolved = manualSpec(manual);
6493
+ if (manualResolved.ready) return {
6494
+ ...base,
6495
+ kind: "manual",
6496
+ argv: manualResolved.argv,
6497
+ cwd: manualResolved.cwd,
6498
+ ready: true
6499
+ };
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
+ };
6524
+ }
6525
+ /**
6526
+ * 解析 C# provider(优先级:手动 > 扩展源 > 自动发现;需 dotnet + Roslyn dll;或 OmniSharp)。
6527
+ * @author ddj 2026年08月27号
6528
+ * @param manual 手动配置
6529
+ * @param home DSH home(缺省真实;测试可注入临时目录)
6530
+ * @returns provider 规格
6531
+ */
6532
+ function resolveCSharpProvider(manual, home = dshHome()) {
6533
+ const base = { languageId: "csharp" };
6534
+ const manualResolved = manualSpec(manual);
6535
+ if (manualResolved.ready) return {
6536
+ ...base,
6537
+ kind: "manual",
6538
+ argv: manualResolved.argv,
6539
+ cwd: manualResolved.cwd,
6540
+ ready: true
6541
+ };
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
+ };
6571
+ }
6572
+ const PROVIDER_RESOLVERS = {
6573
+ lua: resolveLuaProvider,
6574
+ csharp: resolveCSharpProvider
6575
+ };
6576
+ //#endregion
6577
+ //#region src/lsp/config.ts
6578
+ const LSP_SETTINGS_NS = "dsh-vscode-mode.languageServers";
6579
+ const LSP_LANGUAGES = ["lua", "csharp"];
6580
+ function sanitizeLang(raw) {
6581
+ if (!raw || typeof raw !== "object") return void 0;
6582
+ const obj = raw;
6583
+ const out = {};
6584
+ if (typeof obj.enabled === "boolean") out.enabled = obj.enabled;
6585
+ if (typeof obj.command === "string") out.command = obj.command;
6586
+ if (typeof obj.path === "string") out.path = obj.path;
6587
+ return out;
6588
+ }
6589
+ /** 从插件组合配置读取。 */
6590
+ function configFromPlugin(config) {
6591
+ const raw = config?.languageServers;
6592
+ if (!raw || typeof raw !== "object") return {};
6593
+ const out = {};
6594
+ for (const lang of LSP_LANGUAGES) {
6595
+ const v = sanitizeLang(raw[lang]);
6596
+ if (v) out[lang] = v;
6597
+ }
6598
+ return out;
6599
+ }
6600
+ /** 从 settings 描述符读取(无 settings 服务/无该 section → {})。 */
6601
+ function configFromSettings(ctx) {
6602
+ try {
6603
+ const value = (ctx.get("settings")?.describe?.({ redactSecrets: true })?.find((item) => item.ns === LSP_SETTINGS_NS))?.value;
6604
+ if (!value || typeof value !== "object") return {};
6605
+ const out = {};
6606
+ for (const lang of LSP_LANGUAGES) {
6607
+ const v = sanitizeLang(value[lang]);
6608
+ if (v) out[lang] = v;
6609
+ }
6610
+ return out;
6611
+ } catch (error) {
6612
+ return {};
6613
+ }
6614
+ }
6615
+ /** 合并配置:settings 覆盖插件配置。 */
6616
+ function mergeConfig(a, b) {
6617
+ const out = { ...a };
6618
+ for (const lang of LSP_LANGUAGES) {
6619
+ const av = a[lang];
6620
+ const bv = b[lang];
6621
+ if (bv) out[lang] = {
6622
+ ...av,
6623
+ ...bv
6624
+ };
6625
+ else if (av) out[lang] = av;
6626
+ }
6627
+ return out;
6628
+ }
6629
+ /**
6630
+ * 解析某语言的 provider spec(合并配置后)。
6631
+ * @author ddj 2026年08月27号
6632
+ * @param ctx DSH 上下文
6633
+ * @param pluginConfig 插件组合配置
6634
+ * @param languageId 语言 id
6635
+ * @returns provider 规格
6636
+ */
6637
+ function resolveProviderSpec(ctx, pluginConfig, languageId) {
6638
+ const resolver = PROVIDER_RESOLVERS[languageId];
6639
+ if (!resolver) return {
6640
+ languageId,
6641
+ kind: "none",
6642
+ argv: [],
6643
+ ready: false,
6644
+ reason: "不支持的语言:" + languageId
6645
+ };
6646
+ const lang = mergeConfig(configFromPlugin(pluginConfig), configFromSettings(ctx))[languageId];
6647
+ if (lang && lang.enabled === false) return {
6648
+ languageId,
6649
+ kind: "none",
6650
+ argv: [],
6651
+ ready: false,
6652
+ reason: "已在设置中禁用"
6653
+ };
6654
+ return resolver({
6655
+ command: lang?.command,
6656
+ path: lang?.path
6657
+ });
6658
+ }
6659
+ /** 语言 → 扩展名(供客户端判断是否触发 LSP)。 */
6660
+ function langOfPath(path) {
6661
+ const base = String(path || "").split(/[\\/]/).pop() || "";
6662
+ const dot = base.lastIndexOf(".");
6663
+ if (dot <= 0) return null;
6664
+ const ext = base.slice(dot + 1).toLowerCase();
6665
+ for (const [lang, exts] of Object.entries({
6666
+ lua: ["lua"],
6667
+ csharp: ["cs", "csx"]
6668
+ })) if (exts.includes(ext)) return lang;
6669
+ return null;
6670
+ }
6671
+ //#endregion
6672
+ //#region src/lsp/rpc.ts
6673
+ /**
6674
+ * 打开文档跟踪:openDocs(docKey→sessionId) + rootLangRefs 计数 + sessionAcquired 归属。
6675
+ * 保证 manager 的 acquire/release 一一配对(多会话共享同一 root|lang 也平衡)。
6676
+ */
6677
+ function createDocTracker() {
6678
+ const openDocs = /* @__PURE__ */ new Map();
6679
+ const rootLangRefs = /* @__PURE__ */ new Map();
6680
+ const sessionDocs = /* @__PURE__ */ new Map();
6681
+ const keyOf = (root, lang, path) => root + "|" + lang + "|" + path;
6682
+ const rootLangOf = (docKey) => docKey.slice(0, docKey.lastIndexOf("|"));
6683
+ const rememberDoc = (sessionId, docKey) => {
6684
+ const docs = sessionDocs.get(sessionId) ?? /* @__PURE__ */ new Set();
6685
+ docs.add(docKey);
6686
+ sessionDocs.set(sessionId, docs);
6687
+ };
6688
+ const forgetDoc = (sessionId, docKey) => {
6689
+ const docs = sessionDocs.get(sessionId);
6690
+ docs?.delete(docKey);
6691
+ if (docs?.size === 0) sessionDocs.delete(sessionId);
6692
+ };
6693
+ const releaseRef = (rl) => {
6694
+ const next = (rootLangRefs.get(rl) ?? 1) - 1;
6695
+ if (next <= 0) {
6696
+ rootLangRefs.delete(rl);
6697
+ return true;
6698
+ }
6699
+ rootLangRefs.set(rl, next);
6700
+ return false;
6701
+ };
6702
+ return {
6703
+ /** 记录文档打开;同一文档可由多个会话打开,但每个会话只计一次。 */
6704
+ open(root, lang, path, sessionId) {
6705
+ const docKey = keyOf(root, lang, path);
6706
+ const owners = openDocs.get(docKey) ?? /* @__PURE__ */ new Set();
6707
+ if (owners.has(sessionId)) return false;
6708
+ owners.add(sessionId);
6709
+ openDocs.set(docKey, owners);
6710
+ rememberDoc(sessionId, docKey);
6711
+ const rl = root + "|" + lang;
6712
+ const count = rootLangRefs.get(rl) ?? 0;
6713
+ rootLangRefs.set(rl, count + 1);
6714
+ return count === 0;
6715
+ },
6716
+ /** 记录文档关闭;返回 true 表示 root|lang 已无文档(应 release)。 */
6717
+ close(root, lang, path, sessionId) {
6718
+ const docKey = keyOf(root, lang, path);
6719
+ const owners = openDocs.get(docKey);
6720
+ if (!owners?.has(sessionId)) return false;
6721
+ owners.delete(sessionId);
6722
+ if (owners.size === 0) openDocs.delete(docKey);
6723
+ forgetDoc(sessionId, docKey);
6724
+ return releaseRef(root + "|" + lang);
6725
+ },
6726
+ /** 关闭某会话全部文档;返回全局引用归零、需要 release 的 root|lang。 */
6727
+ closeSession(sessionId) {
6728
+ const docs = sessionDocs.get(sessionId);
6729
+ if (!docs) return [];
6730
+ const toRelease = /* @__PURE__ */ new Set();
6731
+ for (const docKey of docs) {
6732
+ const owners = openDocs.get(docKey);
6733
+ owners?.delete(sessionId);
6734
+ if (owners?.size === 0) openDocs.delete(docKey);
6735
+ if (releaseRef(rootLangOf(docKey))) toRelease.add(rootLangOf(docKey));
6736
+ }
6737
+ sessionDocs.delete(sessionId);
6738
+ return [...toRelease];
6739
+ }
6740
+ };
6741
+ }
6742
+ /**
6743
+ * 构造 edrv.lsp.* handlers(一次性创建,tracker 状态跨请求保留)。
6744
+ * @author ddj 2026年08月27号
6745
+ * @param deps 依赖(ctx/pluginConfig/manager)
6746
+ * @returns { handlers, disposeSession } handlers 为局部 map,disposeSession 供 session/disposed 清理
6747
+ */
6748
+ function createLspRpc(deps) {
6749
+ const { ctx, pluginConfig, manager } = deps;
6750
+ const tracker = createDocTracker();
6751
+ /** 解析会话与工作区根(失败 → null + error)。 */
6752
+ const rootOf = async (sessionId) => {
6753
+ const session = sessionOf(ctx, sessionId);
6754
+ if (!session) return { err: "会话不存在" };
6755
+ if (!cwdOf(session)) return { err: "会话无工作区" };
6756
+ try {
6757
+ return {
6758
+ root: await searchRoot(ctx, session),
6759
+ sessionId: typeof session.id === "string" && session.id ? session.id : sessionId ?? ""
6760
+ };
6761
+ } catch (error) {
6762
+ return { err: String(error) };
6763
+ }
6764
+ };
6765
+ const acquireServer = (root, lang) => manager.acquire(root, lang, (languageId) => resolveProviderSpec(ctx, pluginConfig, languageId));
6766
+ const serverOf = (root, lang) => manager.peek(root, lang);
6767
+ const rootLocations = (locations, root) => locations.map((location) => ({
6768
+ ...location,
6769
+ root
6770
+ }));
6771
+ const disposeSession = (sessionId) => {
6772
+ for (const rl of tracker.closeSession(sessionId)) {
6773
+ const sep = rl.lastIndexOf("|");
6774
+ manager.release(rl.slice(0, sep), rl.slice(sep + 1));
6775
+ }
6776
+ };
6777
+ return {
6778
+ handlers: {
6779
+ "edrv.lsp.status": async (args) => {
6780
+ if (!args.sessionId) return {
6781
+ ok: true,
6782
+ servers: manager.statusAll()
6783
+ };
6784
+ const sc = await rootOf(args.sessionId);
6785
+ if ("err" in sc) return {
6786
+ ok: false,
6787
+ error: sc.err
6788
+ };
6789
+ return {
6790
+ ok: true,
6791
+ servers: manager.statusAll().filter((status) => status.root === sc.root)
6792
+ };
6793
+ },
6794
+ "edrv.lsp.configGet": async () => {
6795
+ const settings = configFromSettings(ctx);
6796
+ const plugin = configFromPlugin(pluginConfig);
6797
+ const merged = { ...plugin };
6798
+ for (const lang of Object.keys(settings)) merged[lang] = {
6799
+ ...plugin[lang],
6800
+ ...settings[lang]
6801
+ };
6802
+ return {
6803
+ ok: true,
6804
+ config: merged
6805
+ };
6806
+ },
6807
+ "edrv.lsp.configUpdate": async (args) => {
6808
+ const lang = args.languageId;
6809
+ if (!lang || !resolveProviderSpec(ctx, pluginConfig, lang).languageId) return {
6810
+ ok: false,
6811
+ error: "不支持的语言:" + lang
6812
+ };
6813
+ const current = configFromSettings(ctx);
6814
+ const target = {
6815
+ ...current,
6816
+ [lang]: { ...current[lang] ?? {} }
6817
+ }[lang];
6818
+ if (args.enabled !== void 0) target.enabled = args.enabled;
6819
+ if (args.command !== void 0) target.command = args.command || void 0;
6820
+ if (args.path !== void 0) target.path = args.path || void 0;
6821
+ try {
6822
+ const settings = ctx.get("settings");
6823
+ if (!settings?.update) return {
6824
+ ok: false,
6825
+ error: "设置服务不可用"
6826
+ };
6827
+ await settings.update(LSP_SETTINGS_NS, { [lang]: target });
6828
+ const stored = (settings.describe?.({ redactSecrets: true })?.find((item) => item.ns === LSP_SETTINGS_NS))?.value;
6829
+ return {
6830
+ ok: true,
6831
+ config: {
6832
+ ...configFromPlugin(pluginConfig),
6833
+ ...stored
6834
+ }
6835
+ };
6836
+ } catch (error) {
6837
+ return {
6838
+ ok: false,
6839
+ error: "配置保存失败:" + String(error)
6840
+ };
6841
+ }
6842
+ },
6843
+ "edrv.lsp.sync": async (args) => {
6844
+ const lang = langOfPath(args.path);
6845
+ if (!lang) return { ok: true };
6846
+ const sc = await rootOf(args.sessionId);
6847
+ if ("err" in sc) return {
6848
+ ok: false,
6849
+ error: sc.err
6850
+ };
6851
+ const server = tracker.open(sc.root, lang, args.path, sc.sessionId) ? acquireServer(sc.root, lang) : serverOf(sc.root, lang);
6852
+ if (!server) return {
6853
+ ok: false,
6854
+ error: "语言服务器未注册"
6855
+ };
6856
+ server.sync(args.path, args.text, args.version);
6857
+ return { ok: true };
6858
+ },
6859
+ "edrv.lsp.close": async (args) => {
6860
+ const lang = langOfPath(args.path);
6861
+ if (!lang) return { ok: true };
6862
+ const sc = await rootOf(args.sessionId);
6863
+ if ("err" in sc) return { ok: true };
6864
+ if (tracker.close(sc.root, lang, args.path, sc.sessionId)) manager.release(sc.root, lang);
6865
+ return { ok: true };
6866
+ },
6867
+ "edrv.lsp.definition": async (args) => {
6868
+ const lang = langOfPath(args.path);
6869
+ if (!lang) return {
6870
+ ok: true,
6871
+ locations: []
6872
+ };
6873
+ const sc = await rootOf(args.sessionId);
6874
+ if ("err" in sc) return {
6875
+ ok: false,
6876
+ error: sc.err
6877
+ };
6878
+ try {
6879
+ const server = serverOf(sc.root, lang);
6880
+ if (!server) return {
6881
+ ok: true,
6882
+ locations: []
6883
+ };
6884
+ const locations = await server.definition(args.path, args.position.line, args.position.character);
6885
+ const rooted = rootLocations(locations, sc.root);
6886
+ const truncated = rooted.length > 500;
6887
+ return {
6888
+ ok: true,
6889
+ locations: truncated ? rooted.slice(0, 500) : rooted,
6890
+ truncated: truncated || void 0
6891
+ };
6892
+ } catch (error) {
6893
+ return {
6894
+ ok: false,
6895
+ error: "LSP 定义查询失败:" + String(error)
6896
+ };
6897
+ }
6898
+ },
6899
+ "edrv.lsp.references": async (args) => {
6900
+ const lang = langOfPath(args.path);
6901
+ if (!lang) return {
6902
+ ok: true,
6903
+ locations: []
6904
+ };
6905
+ const sc = await rootOf(args.sessionId);
6906
+ if ("err" in sc) return {
6907
+ ok: false,
6908
+ error: sc.err
6909
+ };
6910
+ try {
6911
+ const server = serverOf(sc.root, lang);
6912
+ if (!server) return {
6913
+ ok: true,
6914
+ locations: []
6915
+ };
6916
+ const locations = await server.references(args.path, args.position.line, args.position.character, args.includeDeclaration !== false);
6917
+ const rooted = rootLocations(locations, sc.root);
6918
+ const truncated = rooted.length > 500;
6919
+ return {
6920
+ ok: true,
6921
+ locations: truncated ? rooted.slice(0, 500) : rooted,
6922
+ truncated: truncated || void 0
6923
+ };
6924
+ } catch (error) {
6925
+ return {
6926
+ ok: false,
6927
+ error: "LSP 引用查询失败:" + String(error)
6928
+ };
6929
+ }
6930
+ },
6931
+ "edrv.lsp.documentSymbol": async (args) => {
6932
+ const lang = langOfPath(args.path);
6933
+ if (!lang) return {
6934
+ ok: true,
6935
+ symbols: []
6936
+ };
6937
+ const sc = await rootOf(args.sessionId);
6938
+ if ("err" in sc) return {
6939
+ ok: false,
6940
+ error: sc.err
6941
+ };
6942
+ try {
6943
+ const server = serverOf(sc.root, lang);
6944
+ if (!server) return {
6945
+ ok: true,
6946
+ symbols: []
6947
+ };
6948
+ return {
6949
+ ok: true,
6950
+ symbols: await server.documentSymbol(args.path)
6951
+ };
6952
+ } catch (error) {
6953
+ return {
6954
+ ok: false,
6955
+ error: "LSP 大纲查询失败:" + String(error)
6956
+ };
6957
+ }
6958
+ },
6959
+ "edrv.lsp.workspaceSymbol": async (args) => {
6960
+ const sc = await rootOf(args.sessionId);
6961
+ if ("err" in sc) return {
6962
+ ok: false,
6963
+ error: sc.err
6964
+ };
6965
+ const out = { symbols: [] };
6966
+ for (const lang of ["lua", "csharp"]) {
6967
+ const existing = serverOf(sc.root, lang);
6968
+ if (!existing) continue;
6969
+ try {
6970
+ const symbols = await existing.workspaceSymbol(args.query);
6971
+ if (symbols.length) out.symbols.push(...symbols);
6972
+ } catch (error) {}
6973
+ }
6974
+ const truncated = out.symbols.length > 500;
6975
+ return {
6976
+ ok: true,
6977
+ symbols: out.symbols.slice(0, 500),
6978
+ truncated: truncated || void 0
6979
+ };
6980
+ },
6981
+ "edrv.lsp.hover": async (args) => {
6982
+ const lang = langOfPath(args.path);
6983
+ if (!lang) return {
6984
+ ok: true,
6985
+ hover: void 0
6986
+ };
6987
+ const sc = await rootOf(args.sessionId);
6988
+ if ("err" in sc) return {
6989
+ ok: false,
6990
+ error: sc.err
6991
+ };
6992
+ try {
6993
+ const server = serverOf(sc.root, lang);
6994
+ if (!server) return {
6995
+ ok: true,
6996
+ hover: void 0
6997
+ };
6998
+ return {
6999
+ ok: true,
7000
+ hover: await server.hover(args.path, args.position.line, args.position.character) ?? void 0
7001
+ };
7002
+ } catch (error) {
7003
+ return {
7004
+ ok: false,
7005
+ error: "LSP hover 查询失败:" + String(error)
7006
+ };
7007
+ }
7008
+ },
7009
+ "edrv.lsp.ext.list": async () => ({
7010
+ ok: true,
7011
+ extensions: listInstalled()
7012
+ }),
7013
+ "edrv.lsp.ext.market": async (args) => {
7014
+ try {
7015
+ return {
7016
+ ok: true,
7017
+ extensions: await marketSearch(args.query, args.size ?? 12)
7018
+ };
7019
+ } catch (error) {
7020
+ return {
7021
+ ok: false,
7022
+ error: "市场搜索失败:" + String(error)
7023
+ };
7024
+ }
7025
+ },
7026
+ "edrv.lsp.ext.install": async (args) => {
7027
+ try {
7028
+ let info;
7029
+ if (args.vsixPath) info = await installVsixFile(args.vsixPath);
7030
+ else if (args.namespace && args.name) info = await installFromMarket(args.namespace, args.name, args.version);
7031
+ else return {
7032
+ ok: false,
7033
+ error: "缺少安装源(vsixPath 或 namespace+name)"
7034
+ };
7035
+ return {
7036
+ ok: true,
7037
+ extension: info
7038
+ };
7039
+ } catch (error) {
7040
+ return {
7041
+ ok: false,
7042
+ error: "安装失败:" + String(error)
7043
+ };
7044
+ }
7045
+ },
7046
+ "edrv.lsp.ext.uninstall": async (args) => {
7047
+ return uninstall(args.id) ? { ok: true } : {
7048
+ ok: false,
7049
+ error: "扩展未安装:" + args.id
7050
+ };
7051
+ },
7052
+ "edrv.lsp.ext.update": async (args) => {
7053
+ try {
7054
+ const updated = await updateExtension(args.id);
7055
+ if (!updated) {
7056
+ const current = listInstalled().find((e) => e.id === args.id);
7057
+ return current ? {
7058
+ ok: true,
7059
+ extension: current,
7060
+ updated: false
7061
+ } : {
7062
+ ok: false,
7063
+ error: "扩展未安装:" + args.id
7064
+ };
7065
+ }
7066
+ return {
7067
+ ok: true,
7068
+ extension: updated,
7069
+ updated: true
7070
+ };
7071
+ } catch (error) {
7072
+ return {
7073
+ ok: false,
7074
+ error: "更新失败:" + String(error)
7075
+ };
7076
+ }
7077
+ },
7078
+ "edrv.lsp.ext.updates": async () => {
7079
+ try {
7080
+ const updates = [];
7081
+ for (const ext of listInstalled()) {
7082
+ const meta = await marketGet(ext.namespace, ext.name);
7083
+ const latest = String(meta?.version ?? "");
7084
+ if (latest && latest !== ext.version) updates.push({
7085
+ id: ext.id,
7086
+ current: ext.version,
7087
+ latest,
7088
+ displayName: ext.displayName
7089
+ });
7090
+ }
7091
+ return {
7092
+ ok: true,
7093
+ updates
7094
+ };
7095
+ } catch (error) {
7096
+ return {
7097
+ ok: false,
7098
+ error: "检查更新失败:" + String(error)
7099
+ };
7100
+ }
7101
+ }
7102
+ },
7103
+ disposeSession
7104
+ };
7105
+ }
7106
+ //#endregion
7107
+ //#region src/lsp/settings.ts
7108
+ /**
7109
+ * 安装 LSP 设置 section。
7110
+ * @author ddj 2026年08月27号
7111
+ * @param ctx DSH 上下文
7112
+ * @param entry 初始配置
7113
+ * @returns 是否成功安装
7114
+ */
7115
+ async function installLspSettingsSection(ctx, entry) {
7116
+ const deps = await loadSettingsDeps();
7117
+ if (!deps) return false;
7118
+ try {
7119
+ const z = deps.z;
7120
+ const langShape = {};
7121
+ for (const lang of LSP_LANGUAGES) langShape[lang] = z.object({
7122
+ enabled: z.boolean(),
7123
+ command: z.string(),
7124
+ path: z.string()
7125
+ }).default({});
7126
+ const schema = z.object(langShape).default({});
7127
+ deps.installSettingsSection(ctx, LSP_SETTINGS_NS, schema, entry, {
7128
+ setSource: () => {},
7129
+ onChange: () => {}
7130
+ });
7131
+ return true;
7132
+ } catch (error) {
7133
+ console.error("[dsh-vscode-mode] LSP 设置 section 安装失败:" + String(error));
7134
+ return false;
7135
+ }
7136
+ }
7137
+ //#endregion
7138
+ //#region src/index.ts
7139
+ /**
7140
+ * @dsh-external 生态 → dsh-vscode-mode:DSH 上的类 VSCode 编码体验(Host 半入口)。
7141
+ * 职责:装配 capture(tools/result 捕获 edit/write 差异)、RPC 分发(edrv.* / mcp.* / vscode.* / compat)、
7142
+ * webServer 路由(/edrv/rpc、/edrv/assets/*、/edrv/vendor/*)、工作区旁车持久化、
7143
+ * 兼容层装配(依赖守卫 + 重复装配自诊断 + 启动日志兼容性报告)。
7144
+ * 结构:shared/(双面契约) + model/store/capture/workspace/revert/registry/rpc/routes/compat(host 模块)。
7145
+ * 作者 ddj 2026-08-20
7146
+ */
7147
+ const name = "dsh-vscode-mode";
7148
+ const inject = [
7149
+ "sessions",
7150
+ "fs",
7151
+ "webServer",
7152
+ "loader",
7153
+ "tools",
7154
+ "workspaceRegistry",
7155
+ "agents"
7156
+ ];
7157
+ /**
7158
+ * 装配插件:挂事件监听、注册路由、安装兼容层。
7159
+ * @author ddj 2026年08月20号
7160
+ * @param ctx DSH 上下文(sessions/fs/webServer 由 inject 提供;sandboxPolicy/subprocess 惰性获取)
7161
+ * @param config 插件配置(可选 imageDir 覆盖图标目录)
7162
+ */
7163
+ function apply(ctx, config) {
7164
+ const registry = /* @__PURE__ */ new Map();
7165
+ const searcher = newSearcher(ctx);
7166
+ const contentSearcher = newContentSearcher(ctx);
7167
+ /** LSP 服务器管理器(语言智能:跳转/引用/大纲)。 */
7168
+ const lspManager = createLspManager((line) => ctx.logger?.debug?.("[" + name + "] " + line));
7169
+ /** 兼容性警告收集(route 护栏等写入,启动日志一并输出)。 */
7170
+ const warnings = [];
7171
+ setupOpenSettings(ctx, config, () => {});
7172
+ installLspSettingsSection(ctx, {});
7173
+ /** LSP RPC 与会话清理(一次性创建,tracker 状态跨请求保留)。 */
7174
+ const lspRpc = createLspRpc({
7175
+ ctx,
7176
+ pluginConfig: config,
7177
+ manager: lspManager
7178
+ });
7179
+ const lspHandlers = lspRpc.handlers;
7180
+ ctx.on("tools/result", (exec, result) => {
7181
+ captureToolResult(ctx, registry, exec, result);
7182
+ });
7183
+ ctx.on("session/disposed", (session) => {
7184
+ const cwd = cwdOf(session);
7185
+ if (cwd) {
7186
+ registry.delete(cwd);
7187
+ dropFileIndex(cwd);
7188
+ searcher.dispose(cwd);
7189
+ contentSearcher.dispose(cwd);
7190
+ disposeIndex(cwd);
7191
+ }
7192
+ const sid = session?.id;
7193
+ if (typeof sid === "string") lspRpc.disposeSession(sid);
7194
+ });
7195
+ registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher, lspHandlers), (warning) => warnings.push(warning));
7196
+ installIsolation(ctx);
7197
+ sweepTreeCache();
7198
+ logCompatSummary(ctx, warnings);
7199
+ ctx.effect(() => () => {
7200
+ lspManager.disposeAll().catch(() => {});
7201
+ disposeAllServers();
7202
+ });
7203
+ ctx.logger?.info?.("[" + name + "] 编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用,语言服务器 LSP 已接入)");
5297
7204
  }
5298
7205
  /**
5299
7206
  * 异步输出兼容性报告摘要(含重复装配/路由冲突自诊断)。