dsh-vscode-mode 0.5.3 → 0.6.0

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.
Files changed (65) hide show
  1. package/README.md +55 -2
  2. package/lib/client.js +5173 -355
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +2356 -126
  5. package/lib/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/client/dap/BpWidget.ts +262 -0
  8. package/src/client/dap/bpMenu.ts +91 -0
  9. package/src/client/dap/bpRowMenu.ts +53 -0
  10. package/src/client/dap/breakpoints.ts +222 -0
  11. package/src/client/dap/decorate.ts +128 -0
  12. package/src/client/dap/hintLine.ts +29 -0
  13. package/src/client/dap/hover.ts +166 -0
  14. package/src/client/dap/hoverMode.ts +159 -0
  15. package/src/client/dap/hoverTree.ts +722 -0
  16. package/src/client/dap/launchInsert.ts +229 -0
  17. package/src/client/dap/launchSnippetProvider.ts +206 -0
  18. package/src/client/dap/modelPath.ts +23 -0
  19. package/src/client/dap/panelSplit.ts +105 -0
  20. package/src/client/dap/pidPick.ts +24 -0
  21. package/src/client/dap/store.ts +571 -0
  22. package/src/client/dap/toolbarDrag.ts +51 -0
  23. package/src/client/dap/trace.ts +22 -0
  24. package/src/client/dap/variableTree.ts +62 -0
  25. package/src/client/index.ts +9 -0
  26. package/src/client/monaco/lsp/index.ts +15 -0
  27. package/src/client/monaco/lsp/providers.ts +2 -0
  28. package/src/client/monaco/theme.ts +15 -0
  29. package/src/client/sidebar/panels/DebugPanel.ts +613 -0
  30. package/src/client/sidebar/panels/SvnPanel.ts +167 -23
  31. package/src/client/sidebar/panels/index.ts +19 -0
  32. package/src/client/sidebar/types.ts +2 -0
  33. package/src/client/styles/editor.css +176 -0
  34. package/src/client/svnActions.ts +18 -1
  35. package/src/client/svnLog.ts +3 -3
  36. package/src/client/svnStatus.ts +140 -2
  37. package/src/client/ui/EditorView.ts +544 -16
  38. package/src/client/ui/SideBySideDiff.tsx +214 -29
  39. package/src/client/ui/SvnDiffPanel.ts +21 -8
  40. package/src/client/ui/SvnLogDialog.ts +44 -2
  41. package/src/client/ui/SvnPatchDialog.ts +63 -0
  42. package/src/client/ui/SvnSumDialog.ts +77 -0
  43. package/src/client/ui/commandCatalog.ts +49 -0
  44. package/src/client/ui/svnExport.ts +71 -0
  45. package/src/dap/configSnippets.ts +269 -0
  46. package/src/dap/discovery.ts +198 -0
  47. package/src/dap/launchConfig.ts +127 -0
  48. package/src/dap/manager.ts +588 -0
  49. package/src/dap/processList.ts +76 -0
  50. package/src/dap/protocol.ts +80 -0
  51. package/src/dap/provider.ts +49 -0
  52. package/src/dap/rpc.ts +192 -0
  53. package/src/dap/sourcePath.ts +82 -0
  54. package/src/index.ts +7 -3
  55. package/src/lsp/providers.ts +3 -2
  56. package/src/reveal.ts +31 -20
  57. package/src/rpc.ts +11 -3
  58. package/src/search/ripgrep.ts +127 -11
  59. package/src/shared/dap.ts +262 -0
  60. package/src/shared/keybindings.ts +7 -0
  61. package/src/shared/rpc.ts +40 -2
  62. package/src/shared/svn.ts +147 -0
  63. package/src/shared/svnActions.ts +20 -0
  64. package/src/svn.ts +320 -11
  65. package/src/workspace.ts +0 -75
package/lib/index.js CHANGED
@@ -6,7 +6,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { homedir, tmpdir } from "node:os";
7
7
  import { createHash, randomUUID } from "node:crypto";
8
8
  import { Buffer as Buffer$1 } from "node:buffer";
9
- import { spawn } from "node:child_process";
9
+ import { execFile, spawn } from "node:child_process";
10
10
  import { inflateRawSync } from "node:zlib";
11
11
  //#region src/mcp.ts
12
12
  /**
@@ -134,7 +134,13 @@ const KEYBINDING_DEFAULTS = {
134
134
  "edrv.nextEditorRow": "Ctrl+Alt+ArrowDown",
135
135
  "edrv.prevEditorRow": "Ctrl+Alt+ArrowUp",
136
136
  "edrv.addSelectionRef": "Ctrl+U",
137
- "edrv.closeTab": "Ctrl+F4"
137
+ "edrv.closeTab": "Ctrl+F4",
138
+ "edrv.debugToggleBreakpoint": "F9",
139
+ "edrv.debugStartContinue": "F5",
140
+ "edrv.debugStepOver": "F10",
141
+ "edrv.debugStepInto": "F11",
142
+ "edrv.debugStepOut": "Shift+F11",
143
+ "edrv.debugStop": "Shift+F5"
138
144
  };
139
145
  //#endregion
140
146
  //#region src/shared/integration.ts
@@ -183,6 +189,23 @@ function tortoiseLaunchArgv(exe, action, absPath) {
183
189
  "/path:" + absPath
184
190
  ];
185
191
  }
192
+ /** W2-6 内容护栏嗅探窗(字符数;NUL/U+FFFD 只看前 8K,全量扫描大文本得不偿失)。 */
193
+ const GUARD_SNIFF_CHARS = 8192;
194
+ /**
195
+ * 文本级内容护栏(W2-6,纯函数):对喂给差异视图/补丁的文本做低成本嗅探。
196
+ * 二进制锚点取 NUL 字节(比 svn:mime-type 更兜底——mime 缺省的「伪文本」二进制也拦得住);
197
+ * 编码提示取 U+FFFD(host fs.readText 按 UTF-8 解码,无效序列会变成替换符)。
198
+ * @author ddj 2026年09月20号
199
+ * @param text 待嗅探文本(null/undefined 按空处理)
200
+ * @returns 护栏标记(干净内容返回空对象)
201
+ */
202
+ function contentGuardOf(text) {
203
+ if (typeof text !== "string" || !text) return {};
204
+ const window = text.slice(0, GUARD_SNIFF_CHARS);
205
+ if (window.includes("\0")) return { binary: true };
206
+ if (window.includes("�")) return { encodingHint: true };
207
+ return {};
208
+ }
186
209
  //#endregion
187
210
  //#region src/shared/logger.ts
188
211
  /**
@@ -3347,7 +3370,12 @@ function pathMatch(value, query) {
3347
3370
  const STDOUT_CAP$1 = 4 << 20;
3348
3371
  const STDERR_CAP$2 = 65536;
3349
3372
  const GRACE_MS$1 = 2e4;
3350
- /** 默认排除目录(文件/内容搜索共用,导出供 provider 复用)。 */
3373
+ /**
3374
+ * 默认排除目录(文件/内容搜索共用,导出供 provider 复用)。
3375
+ * 语义为**大小写敏感**的路径段匹配:`build` 只排除小写构建产物目录,
3376
+ * 不得用 `--glob-case-insensitive` 之类的全局开关,「Module/Build」这类
3377
+ * 源码目录(如 IslandSplash 的 Module/Build/Logic)必须保持可搜索。
3378
+ */
3351
3379
  const EXCLUDES = [
3352
3380
  "node_modules",
3353
3381
  ".git",
@@ -3375,13 +3403,31 @@ const FILE_EXCLUDES = [
3375
3403
  "**/*.map"
3376
3404
  ];
3377
3405
  /**
3378
- * query 转成 rg 的字面 glob。
3379
- * @author ddj 2026年08月24号
3380
- * @param query 已规范化 query
3406
+ * 将查询文本转成大小写不敏感的 rg 正选 glob。
3407
+ * 逐字符把有大小写的字母展开成 `[aA]` 字符类,**不用**大小写不敏感开关:
3408
+ * 全局 `--glob-case-insensitive` 会把 `-g/--glob` 全部当成 `--iglob`,
3409
+ * 连带让 `!**\/build/**` 大小写不敏感,从而排除 `Module/Build/` 这类源码目录
3410
+ * (Ctrl+P 搜 BuildLogic 恒「无匹配文件」);
3411
+ * ② 把查询词改用 `--iglob` 也不行——rg 的正选 glob 会覆盖全部黑名单 glob,
3412
+ * node_modules/.git 等排除项集体失效。
3413
+ * 字符类方案同时满足「查询不区分大小写」且「排除项保持大小写敏感」。
3414
+ * @author ddj 2026年08月24号 / 2026年09月20号
3415
+ * @param query 已规范化查询文本
3381
3416
  * @returns 安全 glob
3382
3417
  */
3383
3418
  function queryGlob(query) {
3384
- return "**/*" + pathText(query).replace(/[\*?\[\]{}!]/g, (char) => "\\" + char) + "*";
3419
+ const value = pathText(query);
3420
+ let body = "";
3421
+ for (const char of value) {
3422
+ const lower = char.toLowerCase();
3423
+ const upper = char.toUpperCase();
3424
+ if (lower !== upper && lower.length === 1 && upper.length === 1) {
3425
+ body += "[" + lower + upper + "]";
3426
+ continue;
3427
+ }
3428
+ body += /[\*?\[\]{}!]/.test(char) ? "\\" + char : char;
3429
+ }
3430
+ return "**/*" + body + "*";
3385
3431
  }
3386
3432
  /**
3387
3433
  * 返回当前平台的 rg 可执行文件路径,不让可选依赖影响模块加载。
@@ -3496,32 +3542,45 @@ function rgExitFailure(code, stderrText) {
3496
3542
  };
3497
3543
  }
3498
3544
  /**
3499
- * 使用打包 ripgrep 搜索工作区文件。
3500
- * @author ddj 2026年08月24号
3501
- * @param input provider 输入
3502
- * @returns 有界搜索结果
3545
+ * 构造文件搜索 rg argv(查询 glob 大小写不敏感,排除 glob 大小写敏感)。
3546
+ * 大小写不敏感**只能**靠 queryGlob 的字符类实现:全局 `--glob-case-insensitive`
3547
+ * 会波及排除项(误排除 `Module/Build/`),查询词用 `--iglob` 又会覆盖全部黑名单 glob。
3548
+ * @author ddj 2026年09月20号
3549
+ * @param binary rg 可执行文件路径
3550
+ * @param root 搜索根目录
3551
+ * @param query 已规范化查询文本
3552
+ * @returns 完整 argv(末位为根目录)
3503
3553
  */
3504
- async function searchRipgrep(input) {
3505
- const sub = input.ctx.get("subprocess");
3506
- if (!sub) throw new Error("缺少 subprocess");
3507
- const binary = ripgrepPath();
3508
- if (!binary) throw new Error("ripgrep 不可用");
3509
- const root = input.root ?? await searchRoot(input.ctx, input.session);
3554
+ function filesArgv(binary, root, query) {
3510
3555
  const argv = [
3511
3556
  binary,
3512
3557
  "--no-config",
3513
3558
  "--files",
3514
3559
  "--hidden",
3515
3560
  "--no-ignore",
3516
- "--glob-case-insensitive",
3517
3561
  "--glob",
3518
- queryGlob(prepareQuery(input.query).text)
3562
+ queryGlob(query)
3519
3563
  ];
3520
3564
  for (const excluded of EXCLUDES) argv.push("--glob", "!**/" + excluded + "/**");
3521
3565
  for (const excluded of FILE_EXCLUDES) argv.push("--glob", "!" + excluded);
3522
3566
  argv.push("--", root);
3567
+ return argv;
3568
+ }
3569
+ /**
3570
+ * 使用打包 ripgrep 搜索工作区文件。
3571
+ * @author ddj 2026年08月24号
3572
+ * @param input provider 输入
3573
+ * @returns 有界搜索结果
3574
+ */
3575
+ async function searchRipgrep(input) {
3576
+ const sub = input.ctx.get("subprocess");
3577
+ if (!sub) throw new Error("缺少 subprocess");
3578
+ const binary = ripgrepPath();
3579
+ if (!binary) throw new Error("ripgrep 不可用");
3580
+ const root = input.root ?? await searchRoot(input.ctx, input.session);
3581
+ const query = prepareQuery(input.query);
3523
3582
  const handle = sub.spawn({
3524
- argv,
3583
+ argv: filesArgv(binary, root, query.text),
3525
3584
  cwd: root,
3526
3585
  stdio: {
3527
3586
  stdin: "ignore",
@@ -3556,6 +3615,93 @@ async function searchRipgrep(input) {
3556
3615
  };
3557
3616
  }
3558
3617
  /**
3618
+ * 构造 chunkname 定向查找的 rg argv。
3619
+ * 与 {@link filesArgv} 同源(同样的排除 glob 与大小写口径),差异只在查询 glob 由
3620
+ * chunkname 推导:chunkname 可能带 chunk 前缀(`@`)与无扩展名 stem,故先剥 `@`、
3621
+ * 取 basename、去掉扩展名后作为片段匹配,仍走 {@link queryGlob} 的字符类方案
3622
+ * ——避免 `--glob-case-insensitive` 波及排除项(见 queryGlob 注释)。
3623
+ * @author ddj 2026年09月21号
3624
+ * @param binary rg 可执行文件路径
3625
+ * @param root 搜索根目录
3626
+ * @param chunk 适配器上报的 chunkname
3627
+ * @returns 完整 argv(末位为根目录);chunk 为空时返回 null
3628
+ */
3629
+ function chunkFilesArgv(binary, root, chunk) {
3630
+ const clean = normalizeChunk(chunk);
3631
+ if (!clean) return null;
3632
+ const argv = [
3633
+ binary,
3634
+ "--no-config",
3635
+ "--files",
3636
+ "--hidden",
3637
+ "--no-ignore",
3638
+ "--glob",
3639
+ queryGlob(clean)
3640
+ ];
3641
+ for (const excluded of EXCLUDES) argv.push("--glob", "!**/" + excluded + "/**");
3642
+ for (const excluded of FILE_EXCLUDES) argv.push("--glob", "!" + excluded);
3643
+ argv.push("--", root);
3644
+ return argv;
3645
+ }
3646
+ /**
3647
+ * chunkname → 用于片段匹配的纯文件名片段。
3648
+ * 剥 `@` 前缀、统一分隔符、取 basename、去掉扩展名(xLua chunkname 惯例不含扩展名),
3649
+ * 使 `GuideSystemMgr` 能命中 `GuideSystemMgr.lua`。
3650
+ * @author ddj 2026年09月21号
3651
+ * @param chunk 适配器上报的 chunkname
3652
+ * @returns 纯片段(空输入返回空串)
3653
+ */
3654
+ function normalizeChunk(chunk) {
3655
+ const text = String(chunk ?? "").replace(/^@/, "").replaceAll("\\", "/").trim();
3656
+ if (!text) return "";
3657
+ return (text.split("/").pop() ?? "").replace(/\.[^.]+$/, "");
3658
+ }
3659
+ /**
3660
+ * 按 chunkname 定向查找工作区文件(DAP findFile 反向匹配专用)。
3661
+ *
3662
+ * 为什么不用工作区全量文件索引:22 万文件的 Unity 工程里,全量枚举按到达序截断,
3663
+ * `Assets/Scripts/Lua/**` 会被 `GameData`/`Entitas` 等目录挤出配额,导致断点命中后
3664
+ * 无法定位源文件(实测前 6000 条中 .lua 数为 0)。此处改为让 rg 在遍历时过滤,
3665
+ * 命中规模与工作区总规模无关(实测同一工作区 0.37s 返回正确结果)。
3666
+ * @author ddj 2026年09月21号
3667
+ * @param ctx DSH 上下文
3668
+ * @param root 工作区根(subprocess 可访问路径)
3669
+ * @param chunk 适配器上报的 chunkname
3670
+ * @param maxResults 候选上限
3671
+ * @returns 绝对路径候选(任何失败都返回空数组,不抛错)
3672
+ */
3673
+ async function findFilesByChunk(ctx, root, chunk, maxResults = 50) {
3674
+ const sub = ctx.get("subprocess");
3675
+ const binary = ripgrepPath();
3676
+ if (!sub || !binary || !root) return [];
3677
+ const argv = chunkFilesArgv(binary, root, chunk);
3678
+ if (!argv) return [];
3679
+ let handle;
3680
+ try {
3681
+ handle = sub.spawn({
3682
+ argv,
3683
+ cwd: root,
3684
+ stdio: {
3685
+ stdin: "ignore",
3686
+ stdout: { maxBytes: STDOUT_CAP$1 },
3687
+ stderr: { maxBytes: STDERR_CAP$2 }
3688
+ },
3689
+ graceMs: GRACE_MS$1
3690
+ });
3691
+ } catch (error) {
3692
+ return [];
3693
+ }
3694
+ let code;
3695
+ try {
3696
+ const outcome = await handle.done;
3697
+ code = outcome.exitCode ?? outcome.code;
3698
+ } catch (error) {
3699
+ return [];
3700
+ }
3701
+ if (code !== 0 && code !== 1 && code !== 2) return [];
3702
+ return parseOutput(handle, maxResults).files;
3703
+ }
3704
+ /**
3559
3705
  * 构造 ripgrep provider。
3560
3706
  * @author ddj 2026年08月24号
3561
3707
  * @returns provider 实例
@@ -6528,10 +6674,14 @@ function clientsAlive() {
6528
6674
  //#region src/reveal.ts
6529
6675
  /**
6530
6676
  * dsh-vscode-mode host — 在 OS 文件浏览器中打开/定位路径(reveal 能力)。
6531
- * 纯函数 revealCommand 平台分发 + revealInExplorer ctx.subprocess.spawn 发射
6532
- * (argv 数组、无 shell 插值,沿 workspace/revert 的 subprocess 契约)。
6677
+ * 纯函数 revealCommand 平台分发 + revealSpawnOpts 启动选项 + revealInExplorer 发射。
6533
6678
  * 文件 → 资源管理器选中定位;目录 → 打开目录;Linux 无通用定位协议 → 打开所在目录。
6534
- * 作者 ddj 2026-08-27
6679
+ *
6680
+ * 刻意**不经** ctx.subprocess.spawn:DSH 的 Windows Job runner 硬编码
6681
+ * `windowsHide: true`(dsh-subprocess-local 的 launchWindowsJob 与 runner 内部 spawn),
6682
+ * 连 explorer.exe 的 GUI 窗口一并隐藏——RPC 回 ok:true 但窗口根本不出现。
6683
+ * GUI 拉起属 fire-and-forget,用 node child_process + detached + unref 才贴合语义。
6684
+ * 作者 ddj 2026年08月27号 / 2026年09月20号
6535
6685
  */
6536
6686
  /**
6537
6687
  * 构造平台 opener 的 argv(纯函数,platform 可注入便于单测)。
@@ -6557,32 +6707,47 @@ function revealCommand(absPath, isDir, platform = process.platform) {
6557
6707
  }
6558
6708
  }
6559
6709
  /**
6560
- * host subprocess 服务发射 opener(fire-and-forget 语义)。
6561
- * Explorer GUI 分离进程,非零退出码不视为失败;仅 spawn 级错误回失败。
6562
- * @author ddj 2026年08月27号
6563
- * @param ctx DSH host 上下文
6710
+ * 构造 opener 子进程启动选项。
6711
+ * `windowsHide` 必须为 **false**:置 true 会把 Explorer GUI 窗口一起隐藏,
6712
+ * 症状即「RPC ok:true 但窗口不出现」(本文件头注释记录的回归)。
6713
+ * @author ddj 2026年09月20号
6714
+ * @param cwd 子进程工作目录(打开文件时用其父目录)
6715
+ * @returns child_process.spawn 选项
6716
+ */
6717
+ function revealSpawnOpts(cwd) {
6718
+ return {
6719
+ cwd,
6720
+ stdio: "ignore",
6721
+ windowsHide: false,
6722
+ detached: true
6723
+ };
6724
+ }
6725
+ /**
6726
+ * 发射 opener 拉起 OS 文件浏览器(fire-and-forget 语义)。
6727
+ * Explorer 为 GUI 分离进程,非零退出码不视为失败,故只等 spawn 事件即返回;
6728
+ * 子进程脱离父进程组并 unref,避免阻塞宿主退出。
6729
+ * @author ddj 2026年08月27号 / 2026年09月20号
6564
6730
  * @param absPath 绝对路径
6565
6731
  * @param isDir 是否为目录
6566
6732
  * @returns 成功或失败原因
6567
6733
  */
6568
- async function revealInExplorer(ctx, absPath, isDir) {
6569
- const sub = ctx.get("subprocess");
6570
- if (!sub || typeof sub.spawn !== "function") return {
6734
+ async function revealInExplorer(absPath, isDir) {
6735
+ const [program, ...args] = revealCommand(absPath, isDir).argv;
6736
+ if (!program) return {
6571
6737
  ok: false,
6572
- error: "打开文件浏览器不可用:缺少 subprocess 服务"
6738
+ error: "打开失败:opener 命令为空"
6573
6739
  };
6574
- const { argv } = revealCommand(absPath, isDir);
6575
6740
  try {
6576
- await sub.spawn({
6577
- argv,
6578
- cwd: dirname(absPath),
6579
- stdio: {
6580
- stdout: { maxBytes: 65536 },
6581
- stderr: { maxBytes: 65536 },
6582
- stdin: "ignore"
6583
- },
6584
- graceMs: 1e4
6585
- }).done;
6741
+ await new Promise((resolve, reject) => {
6742
+ const child = spawn(program, args, revealSpawnOpts(dirname(absPath)));
6743
+ child.once("spawn", () => {
6744
+ child.unref();
6745
+ resolve();
6746
+ });
6747
+ child.once("error", (error) => {
6748
+ reject(error);
6749
+ });
6750
+ });
6586
6751
  return { ok: true };
6587
6752
  } catch (error) {
6588
6753
  return {
@@ -7375,11 +7540,12 @@ async function integrationBaseUrlOf(ctx) {
7375
7540
  return (typeof value?.integrationBaseUrl === "string" ? value.integrationBaseUrl.trim() : "") || "http://127.0.0.1:3080";
7376
7541
  }
7377
7542
  /** 各方法 handler 表(类型由 shared/rpc 的 RpcHandlerMap 约束)。 */
7378
- function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx), lspHandlers, aiHandlers, fileVersions, svnHandlers) {
7543
+ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx), lspHandlers, aiHandlers, fileVersions, svnHandlers, dapHandlers) {
7379
7544
  return {
7380
7545
  ...lspHandlers ?? {},
7381
7546
  ...aiHandlers ?? {},
7382
7547
  ...svnHandlers ?? {},
7548
+ ...dapHandlers ?? {},
7383
7549
  "edrv.list": async (args) => {
7384
7550
  const sc = await requireSession(ctx, args.sessionId);
7385
7551
  if ("err" in sc) return {
@@ -7493,6 +7659,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
7493
7659
  };
7494
7660
  try {
7495
7661
  const target = await resolveTarget(ctx, sc.session, args.path);
7662
+ debugRecord(ctx, sc.cwd, "[DEBUG path.resolve] input=" + String(args.path ?? "") + " resolved=" + fs.processPath(target), "debug");
7496
7663
  const info = await fs.stat(target);
7497
7664
  if (!info || info.type !== "file") return {
7498
7665
  ok: false,
@@ -7552,6 +7719,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
7552
7719
  };
7553
7720
  try {
7554
7721
  const target = await resolveTarget(ctx, sc.session, args.path);
7722
+ debugRecord(ctx, sc.cwd, "[DEBUG path.resolve] input=" + String(args.path ?? "") + " resolved=" + fs.processPath(target), "debug");
7555
7723
  const info = await fs.stat(target);
7556
7724
  if (!info || info.type !== "file") return {
7557
7725
  ok: false,
@@ -7615,6 +7783,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
7615
7783
  };
7616
7784
  try {
7617
7785
  const target = await resolveTarget(ctx, sc.session, args.path);
7786
+ debugRecord(ctx, sc.cwd, "[DEBUG path.resolve] input=" + String(args.path ?? "") + " resolved=" + fs.processPath(target), "debug");
7618
7787
  const rev = typeof args.rev === "string" && args.rev ? args.rev : null;
7619
7788
  const intent = rev ? {
7620
7789
  kind: "replaceIfVersion",
@@ -7655,6 +7824,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
7655
7824
  };
7656
7825
  try {
7657
7826
  const target = await resolveTarget(ctx, sc.session, args.path);
7827
+ debugRecord(ctx, sc.cwd, "[DEBUG path.resolve] input=" + String(args.path ?? "") + " resolved=" + fs.processPath(target), "debug");
7658
7828
  const info = await fs.stat(target);
7659
7829
  if (!info || info.type !== "file") return {
7660
7830
  ok: false,
@@ -7850,7 +8020,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
7850
8020
  };
7851
8021
  },
7852
8022
  "edrv.dlog.reveal": async () => {
7853
- const revealed = await revealInExplorer(ctx, pluginLogRoot(), true);
8023
+ const revealed = await revealInExplorer(pluginLogRoot(), true);
7854
8024
  if (!revealed.ok) return {
7855
8025
  ok: false,
7856
8026
  error: revealed.error
@@ -7954,7 +8124,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
7954
8124
  error: "路径不存在"
7955
8125
  };
7956
8126
  const abs = fs.processPath(target);
7957
- const outcome = await revealInExplorer(ctx, abs, info.type === "directory");
8127
+ const outcome = await revealInExplorer(abs, info.type === "directory");
7958
8128
  if (!outcome.ok) return {
7959
8129
  ok: false,
7960
8130
  error: outcome.error
@@ -8618,8 +8788,8 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
8618
8788
  * 统一入口:按方法分发到 handler 表。
8619
8789
  * @author ddj 2026年08月20号 / 2026年08月26号
8620
8790
  */
8621
- async function handleRpc(ctx, registry, method, args, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx), lspHandlers, aiHandlers, fileVersions, svnHandlers) {
8622
- const handler = buildHandlers(ctx, registry, searcher, contentSearcher, lspHandlers, aiHandlers, fileVersions, svnHandlers)[method];
8791
+ async function handleRpc(ctx, registry, method, args, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx), lspHandlers, aiHandlers, fileVersions, svnHandlers, dapHandlers) {
8792
+ const handler = buildHandlers(ctx, registry, searcher, contentSearcher, lspHandlers, aiHandlers, fileVersions, svnHandlers, dapHandlers)[method];
8623
8793
  if (!handler) return {
8624
8794
  ok: false,
8625
8795
  error: "未知方法: " + String(method)
@@ -8754,14 +8924,6 @@ function installIsolation(ctx) {
8754
8924
  }, "vscode-mode:mcp-isolation");
8755
8925
  }
8756
8926
  //#endregion
8757
- //#region src/workspace.ts
8758
- /** cwd → { at, files }(随会话销毁清理)。 */
8759
- const fileIndex = /* @__PURE__ */ new Map();
8760
- /** 会话销毁时清理文件索引缓存。 */
8761
- function dropFileIndex(cwd) {
8762
- fileIndex.delete(cwd);
8763
- }
8764
- //#endregion
8765
8927
  //#region src/lsp/jsonrpc.ts
8766
8928
  /**
8767
8929
  * dsh-vscode-mode host — LSP JSON-RPC 帧编解码(纯函数,node 可测)。
@@ -10247,6 +10409,11 @@ async function updateExtension(id) {
10247
10409
  * 产物 LspProviderSpec:{ ready, argv, cwd, reason },供 manager 启动服务器。
10248
10410
  * 作者 ddj 2026-08-27
10249
10411
  */
10412
+ /** 语言 → 扩展名(用于判断哪些文件触发该语言服务器)。 */
10413
+ const LSP_LANG_EXT = {
10414
+ lua: ["lua"],
10415
+ csharp: ["cs", "csx"]
10416
+ };
10250
10417
  /** 平台目录名(LuaLS/OmniSharp 的 server/bin/<平台>)。 */
10251
10418
  function platformServerDir(platform = process.platform) {
10252
10419
  if (platform === "win32") return "Windows";
@@ -10397,7 +10564,8 @@ function sdkEnvOf(sdkRoot) {
10397
10564
  if (version) env.DOTNET_SDK_PATH = join(sdkRoot, "sdk", version);
10398
10565
  return env;
10399
10566
  }
10400
- /** 读扩展目录 package.json 的 version(读取失败不影响入口使用,返回 undefined)。 */
10567
+ /** 读扩展目录 package.json 的 version(读取失败不影响入口使用,返回 undefined)。
10568
+ * 导出供 dap/discovery 复用(扩展清单版本排序同口径,避免第二份实现)。 */
10401
10569
  function manifestVersionOf(extDir) {
10402
10570
  try {
10403
10571
  const manifest = JSON.parse(readFileSync(join(extDir, "package.json"), "utf8"));
@@ -12732,6 +12900,21 @@ const READ_GRACE_MS = 3e4;
12732
12900
  const MUTATE_GRACE_MS = 12e4;
12733
12901
  /** 文本读取上限(对齐 rpc.ts READ_CAP:>8MB 拒绝整文件读取/差异)。 */
12734
12902
  const READ_CAP = 8388608;
12903
+ /**
12904
+ * W2-1 补丁文本输出上限(16MB)。实测教训复用(PROGRESS §六.1):DSH stdout 收集器
12905
+ * 超限**保留尾部**——补丁被截断时头部 `Index:` 会丢失,host 以「不以 Index: 开头」
12906
+ * 标记 truncated,客户端明示「不完整」而非让用户拿半份补丁去用。
12907
+ */
12908
+ const PATCH_OUTPUT_CAP = 16777216;
12909
+ /** W2-2 目录对比 summarize 输出上限(8MB;大目录长区间的变更条目 XML)。 */
12910
+ const SUM_OUTPUT_CAP = 8388608;
12911
+ /**
12912
+ * W2-3 远端检查超时(15s)。M3 实测:无凭据/离线时 `status -u` 会长时间挂起
12913
+ * (受限沙箱内 120s 无输出),远端检查必须短超时 + 失败降级,绝不阻塞面板。
12914
+ */
12915
+ const REMOTE_GRACE_MS = 15e3;
12916
+ /** W2-2 summarize 服务器往返预算(大目录长区间可能较慢)。 */
12917
+ const SUM_GRACE_MS = 6e4;
12735
12918
  /** 工作副本根向上查找(≤10 层,命中含 .svn 的最近祖先)。 */
12736
12919
  const SVN_ROOT_MAX_DEPTH = 10;
12737
12920
  /** 默认存在性探测(node fs;异常一律按不存在)。 */
@@ -12892,6 +13075,130 @@ function parseStatusXml(xml, cap = CHANGES_CAP) {
12892
13075
  truncated
12893
13076
  };
12894
13077
  }
13078
+ /**
13079
+ * 绝对路径 → 工作副本相对路径(W2-2/W2-3:M3/M5 实测 XML 中 path 为绝对路径)。
13080
+ * Windows 大小写不敏感文件系统:前缀匹配按小写比较,剥离后保留原大小写。
13081
+ * @author ddj 2026年09月20号
13082
+ * @param abs 绝对路径(`/` 或 `\` 分隔均可)
13083
+ * @param wcRoot 工作副本根绝对路径
13084
+ * @returns 相对路径(`/` 分隔);不在根下返回 null
13085
+ */
13086
+ function relPathOfAbs(abs, wcRoot) {
13087
+ const norm = (p) => String(p ?? "").replace(/\\/g, "/").replace(/\/+$/, "");
13088
+ const a = norm(abs);
13089
+ const r = norm(wcRoot);
13090
+ if (!r || !a.toLowerCase().startsWith(r.toLowerCase() + "/")) return null;
13091
+ return a.slice(r.length + 1);
13092
+ }
13093
+ /**
13094
+ * W2-1 补丁的 `-x` 扩展选项组合(纯函数)。
13095
+ * 冒烟实测修正(2026-09-20):本机 svn 内部 diff 的上下文行数只认 `-U<N>`,
13096
+ * `--unified=N` 报 E200016 erroneous argument(07 计划 §1 当年只核对了 help 文案,未实测)。
13097
+ * @author ddj 2026年09月20号
13098
+ * @param opts 空白语义 / EOL 忽略 / 上下文行数
13099
+ * @returns 空格连接的单个 `-x` 值;无选项时空串
13100
+ */
13101
+ function patchExtOf(opts) {
13102
+ const parts = [];
13103
+ if (opts.whitespace === "b") parts.push("-b");
13104
+ if (opts.whitespace === "w") parts.push("-w");
13105
+ if (opts.ignoreEol === true) parts.push("--ignore-eol-style");
13106
+ const unified = Math.floor(Number(opts.unified));
13107
+ if (Number.isFinite(unified) && unified >= 0 && unified <= 100 && unified !== 3) parts.push("-U" + unified);
13108
+ return parts.join(" ");
13109
+ }
13110
+ /**
13111
+ * 解析 `svn diff --summarize --xml` 输出(W2-2;M5 实测形态,svn 1.14.5):
13112
+ * `<diff><paths><path item="modified" kind="file" props="none">ABS:\\PATH</path>...`
13113
+ * - path 的**文本内容**是绝对路径(非属性)→ relPathOfAbs 归一为工作副本相对;
13114
+ * - 属性输出顺序不稳定(与 M1 log 同款教训),只按字段名取值、不做字符串快照;
13115
+ * - 区间无变更时 `<paths>` 为空 → entries 为空数组(非错误)。
13116
+ * @author ddj 2026年09月20号
13117
+ * @param xml summarize --xml 的 stdout
13118
+ * @param wcRoot 工作副本根(路径归一锚点)
13119
+ * @param cap 条目上限(超出标记 truncated)
13120
+ * @returns 变更条目与是否截断
13121
+ */
13122
+ function parseSummarizeXml(xml, wcRoot, cap = CHANGES_CAP) {
13123
+ const entries = [];
13124
+ let truncated = false;
13125
+ if (typeof xml !== "string" || !xml) return {
13126
+ entries,
13127
+ truncated
13128
+ };
13129
+ const re = /<path\b([^>]*?)>([\s\S]*?)<\/path>/g;
13130
+ let match;
13131
+ while ((match = re.exec(xml)) !== null) {
13132
+ const abs = unescapeXml(match[2].trim());
13133
+ const rel = relPathOfAbs(abs, wcRoot) ?? changePathOf(abs);
13134
+ if (!rel) continue;
13135
+ if (entries.length >= cap) {
13136
+ truncated = true;
13137
+ break;
13138
+ }
13139
+ entries.push({
13140
+ path: rel,
13141
+ item: attrOf(match[1], "item") || "none",
13142
+ kind: attrOf(match[1], "kind") || "none",
13143
+ props: attrOf(match[1], "props") || "none"
13144
+ });
13145
+ }
13146
+ return {
13147
+ entries,
13148
+ truncated
13149
+ };
13150
+ }
13151
+ /**
13152
+ * 解析 `svn status -u --xml` 输出(W2-3;M3 实测形态,svn 1.14.5):
13153
+ * `<entry path="ABS"><wc-status ...>...</wc-status><repos-status item="modified" props="none"/></entry>` + `<against revision="N"/>`
13154
+ * - 落后锚点 = 存在 `<repos-status>` 且 `item !== 'none'`(实测落后文件为 modified);
13155
+ * 远端无变化的条目**没有** repos-status 元素(省略,非 'none' 值);
13156
+ * - 离线/无凭据时会长时间挂起(M3 实测 120s+),调用方必须用 REMOTE_GRACE_MS 短超时。
13157
+ * @author ddj 2026年09月20号
13158
+ * @param xml status -u --xml 的 stdout
13159
+ * @param wcRoot 工作副本根(路径归一锚点)
13160
+ * @param cap 条目上限(超出标记 truncated)
13161
+ * @returns 落后清单 + against 修订
13162
+ */
13163
+ function parseRemoteStatusXml(xml, wcRoot, cap = CHANGES_CAP) {
13164
+ const outdated = [];
13165
+ let againstRev = null;
13166
+ let truncated = false;
13167
+ if (typeof xml !== "string" || !xml) return {
13168
+ outdated,
13169
+ againstRev,
13170
+ truncated
13171
+ };
13172
+ const against = /<against\b([^>]*?)\/?>/.exec(xml);
13173
+ if (against) {
13174
+ const rev = numAttrOf(against[1], "revision");
13175
+ if (rev !== void 0) againstRev = rev;
13176
+ }
13177
+ const re = /<entry\b([^>]*?)>([\s\S]*?)<\/entry>/g;
13178
+ let match;
13179
+ while ((match = re.exec(xml)) !== null) {
13180
+ const repos = /<repos-status\b([^>]*?)\/?>/.exec(match[2]);
13181
+ if (!repos) continue;
13182
+ const item = attrOf(repos[1], "item");
13183
+ if (!item || item === "none") continue;
13184
+ const rawPath = attrOf(match[1], "path");
13185
+ const rel = relPathOfAbs(rawPath, wcRoot) ?? changePathOf(rawPath);
13186
+ if (!rel) continue;
13187
+ if (outdated.length >= cap) {
13188
+ truncated = true;
13189
+ break;
13190
+ }
13191
+ outdated.push({
13192
+ path: rel,
13193
+ item
13194
+ });
13195
+ }
13196
+ return {
13197
+ outdated,
13198
+ againstRev,
13199
+ truncated
13200
+ };
13201
+ }
12895
13202
  /** 进程名(去路径去 .exe;前台助手用)。 */
12896
13203
  function processNameOf(exe) {
12897
13204
  return (exe.split(/[\\/]/).pop() ?? exe).replace(/\.exe$/i, "");
@@ -13349,6 +13656,48 @@ function createSvnRpc(deps) {
13349
13656
  error: sides[0].error ?? sides[1].error
13350
13657
  };
13351
13658
  };
13659
+ /**
13660
+ * 扫描冲突副本文件(W2-4;svn 冲突时的标准后缀 `.mine`/`.working`/`.rN`,纯本地 fs 只读)。
13661
+ * @author ddj 2026年09月20号
13662
+ * @param wcRoot 工作副本根
13663
+ * @param rel 冲突文件的工作副本相对路径
13664
+ * @returns 副本清单(rev 升序 → working → mine;默认对比对 = 首个 .rN ↔ .mine)
13665
+ */
13666
+ const conflictArtifactsOf = async (wcRoot, rel) => {
13667
+ const abs = join(wcRoot, rel);
13668
+ const base = basename(abs);
13669
+ const names = await readdir(dirname(abs));
13670
+ const rankOf = (kind) => kind === "rev" ? 0 : kind === "working" ? 1 : 2;
13671
+ const artifacts = [];
13672
+ for (const name of names) {
13673
+ let kind = null;
13674
+ let rev;
13675
+ if (name === base + ".mine") kind = "mine";
13676
+ else if (name === base + ".working") kind = "working";
13677
+ else if (name.startsWith(base + ".r") && /^\d+$/.test(name.slice(base.length + 2))) {
13678
+ kind = "rev";
13679
+ rev = Number.parseInt(name.slice(base.length + 2), 10);
13680
+ }
13681
+ if (kind === null) continue;
13682
+ const artifactPath = relPathOfAbs(join(dirname(abs), name), wcRoot);
13683
+ if (!artifactPath) continue;
13684
+ artifacts.push({
13685
+ path: artifactPath,
13686
+ name,
13687
+ kind,
13688
+ ...rev !== void 0 ? { rev } : {}
13689
+ });
13690
+ }
13691
+ return artifacts.sort((a, b) => rankOf(a.kind) - rankOf(b.kind) || (a.rev ?? 0) - (b.rev ?? 0) || a.name.localeCompare(b.name));
13692
+ };
13693
+ /** W2-6:双侧内容护栏标记(干净时返回空对象,展开进载荷不添字段)。 */
13694
+ const guardSides = (a, b) => {
13695
+ const merged = {
13696
+ ...contentGuardOf(a ?? ""),
13697
+ ...contentGuardOf(b ?? "")
13698
+ };
13699
+ return merged.binary || merged.encodingHint ? merged : {};
13700
+ };
13352
13701
  return { handlers: {
13353
13702
  "svn.status": async (args) => {
13354
13703
  const sc = await requireSvnSession(ctx, args.sessionId);
@@ -13441,13 +13790,15 @@ function createSvnRpc(deps) {
13441
13790
  base: null,
13442
13791
  working,
13443
13792
  reason: /pristine|E200009/i.test(text) ? "no-pristine" : "cat-failed",
13444
- error: tailOfText(text)
13793
+ error: tailOfText(text),
13794
+ ...guardSides(null, working)
13445
13795
  };
13446
13796
  }
13447
13797
  return {
13448
13798
  ok: true,
13449
13799
  base: outcome.stdout,
13450
- working
13800
+ working,
13801
+ ...guardSides(outcome.stdout, working)
13451
13802
  };
13452
13803
  } catch (error) {
13453
13804
  return {
@@ -13528,9 +13879,11 @@ function createSvnRpc(deps) {
13528
13879
  error: "svn 命令不可用(可在设置页配置 svn 路径)"
13529
13880
  };
13530
13881
  try {
13882
+ const result = await diffRevSides(status.wcRoot, rel, Math.floor(revision));
13531
13883
  return {
13532
13884
  ok: true,
13533
- ...await diffRevSides(status.wcRoot, rel, Math.floor(revision))
13885
+ ...result,
13886
+ ...guardSides(result.left, result.right)
13534
13887
  };
13535
13888
  } catch (error) {
13536
13889
  return {
@@ -13566,9 +13919,11 @@ function createSvnRpc(deps) {
13566
13919
  error: "svn 命令不可用(可在设置页配置 svn 路径)"
13567
13920
  };
13568
13921
  try {
13922
+ const result = await pairSides(status.wcRoot, rel, Math.floor(revA), Math.floor(revB));
13569
13923
  return {
13570
13924
  ok: true,
13571
- ...await pairSides(status.wcRoot, rel, Math.floor(revA), Math.floor(revB))
13925
+ ...result,
13926
+ ...guardSides(result.left, result.right)
13572
13927
  };
13573
13928
  } catch (error) {
13574
13929
  return {
@@ -13631,12 +13986,14 @@ function createSvnRpc(deps) {
13631
13986
  left: null,
13632
13987
  right: working,
13633
13988
  reason: "not-exist",
13634
- error: tailOfText((leftOutcome.stderr || leftOutcome.stdout).trim())
13989
+ error: tailOfText((leftOutcome.stderr || leftOutcome.stdout).trim()),
13990
+ ...guardSides(null, working)
13635
13991
  };
13636
13992
  return {
13637
13993
  ok: true,
13638
13994
  left: leftOutcome.stdout,
13639
- right: working
13995
+ right: working,
13996
+ ...guardSides(leftOutcome.stdout, working)
13640
13997
  };
13641
13998
  } catch (error) {
13642
13999
  return {
@@ -13645,7 +14002,7 @@ function createSvnRpc(deps) {
13645
14002
  };
13646
14003
  }
13647
14004
  },
13648
- "svn.wcRev": async (args) => {
14005
+ "svn.patchText": async (args) => {
13649
14006
  const sc = await requireSvnSession(ctx, args.sessionId);
13650
14007
  if ("err" in sc) return {
13651
14008
  ok: false,
@@ -13653,8 +14010,8 @@ function createSvnRpc(deps) {
13653
14010
  };
13654
14011
  const rel = safeRel(args.path);
13655
14012
  if (rel === null || rel === "") return {
13656
- ok: true,
13657
- revision: null
14013
+ ok: false,
14014
+ error: "路径不合法"
13658
14015
  };
13659
14016
  const status = await statusOf(sc.cwd);
13660
14017
  if (!status.managed || !status.wcRoot) return {
@@ -13665,27 +14022,55 @@ function createSvnRpc(deps) {
13665
14022
  ok: false,
13666
14023
  error: "svn 命令不可用(可在设置页配置 svn 路径)"
13667
14024
  };
13668
- const outcome = await runSvn(status.wcRoot, [
13669
- "info",
13670
- "--xml",
13671
- "--",
13672
- rel
13673
- ], READ_GRACE_MS);
13674
- if (outcome.code !== 0) return {
13675
- ok: true,
13676
- revision: null
13677
- };
13678
- return {
13679
- ok: true,
13680
- revision: parseSvnInfoRevision(outcome.stdout)
13681
- };
14025
+ try {
14026
+ const ext = patchExtOf({
14027
+ whitespace: args.whitespace,
14028
+ ignoreEol: args.ignoreEol === true,
14029
+ unified: args.unified
14030
+ });
14031
+ const argv = [
14032
+ "diff",
14033
+ ...ext ? ["-x", ext] : [],
14034
+ "--",
14035
+ rel
14036
+ ];
14037
+ const outcome = await runSvn(status.wcRoot, argv, READ_GRACE_MS, PATCH_OUTPUT_CAP);
14038
+ if (outcome.code !== 0) return {
14039
+ ok: false,
14040
+ error: "生成补丁失败:" + tailOfText([outcome.stderr, outcome.stdout].filter(Boolean).join("\n"))
14041
+ };
14042
+ const text = outcome.stdout;
14043
+ const head = text.slice(0, 200);
14044
+ return {
14045
+ ok: true,
14046
+ text,
14047
+ truncated: !head.startsWith("Index:") && !head.startsWith("Property changes on:"),
14048
+ binary: text.includes("Cannot display:")
14049
+ };
14050
+ } catch (error) {
14051
+ return {
14052
+ ok: false,
14053
+ error: "生成补丁失败:" + String(error)
14054
+ };
14055
+ }
13682
14056
  },
13683
- "svn.cleanup": async (args) => {
14057
+ "svn.diffSum": async (args) => {
13684
14058
  const sc = await requireSvnSession(ctx, args.sessionId);
13685
14059
  if ("err" in sc) return {
13686
14060
  ok: false,
13687
14061
  error: sc.err
13688
14062
  };
14063
+ const rel = safeRel(args.path);
14064
+ if (rel === null) return {
14065
+ ok: false,
14066
+ error: "路径不合法"
14067
+ };
14068
+ const revA = Number(args.revA);
14069
+ const revB = Number(args.revB);
14070
+ if (!Number.isFinite(revA) || revA < 1 || !Number.isFinite(revB) || revB < 1) return {
14071
+ ok: false,
14072
+ error: "版本号不合法"
14073
+ };
13689
14074
  const status = await statusOf(sc.cwd);
13690
14075
  if (!status.managed || !status.wcRoot) return {
13691
14076
  ok: false,
@@ -13695,29 +14080,32 @@ function createSvnRpc(deps) {
13695
14080
  ok: false,
13696
14081
  error: "svn 命令不可用(可在设置页配置 svn 路径)"
13697
14082
  };
13698
- const argv = ["cleanup"];
13699
- if (args.removeUnversioned === true) argv.push("--remove-unversioned");
13700
- if (args.removeIgnored === true) argv.push("--remove-ignored");
13701
14083
  try {
13702
- const outcome = await runSvn(status.wcRoot, argv, MUTATE_GRACE_MS);
13703
- const result = batchResultOf(outcome.stdout, outcome.stderr, outcome.code, "清理");
14084
+ const argv = [
14085
+ "diff",
14086
+ "--summarize",
14087
+ "--xml",
14088
+ "-r",
14089
+ String(Math.floor(revA)) + ":" + String(Math.floor(revB))
14090
+ ];
14091
+ if (rel) argv.push("--", rel);
14092
+ const outcome = await runSvn(status.wcRoot, argv, SUM_GRACE_MS, SUM_OUTPUT_CAP);
13704
14093
  if (outcome.code !== 0) return {
13705
14094
  ok: false,
13706
- error: "清理失败:" + tailOfText(result.output)
14095
+ error: "目录对比失败:" + tailOfText([outcome.stderr, outcome.stdout].filter(Boolean).join("\n"))
13707
14096
  };
13708
- changesCache.delete(status.wcRoot);
13709
14097
  return {
13710
14098
  ok: true,
13711
- ...result
14099
+ ...parseSummarizeXml(outcome.stdout, status.wcRoot)
13712
14100
  };
13713
14101
  } catch (error) {
13714
14102
  return {
13715
14103
  ok: false,
13716
- error: "清理失败:" + String(error)
14104
+ error: "目录对比失败:" + String(error)
13717
14105
  };
13718
14106
  }
13719
14107
  },
13720
- "svn.update": async (args) => {
14108
+ "svn.remoteStatus": async (args) => {
13721
14109
  const sc = await requireSvnSession(ctx, args.sessionId);
13722
14110
  if ("err" in sc) return {
13723
14111
  ok: false,
@@ -13729,7 +14117,7 @@ function createSvnRpc(deps) {
13729
14117
  error: "路径不合法"
13730
14118
  };
13731
14119
  const status = await statusOf(sc.cwd);
13732
- if (!status.managed) return {
14120
+ if (!status.managed || !status.wcRoot) return {
13733
14121
  ok: false,
13734
14122
  error: "当前工作区不受 SVN 管理"
13735
14123
  };
@@ -13738,73 +14126,1912 @@ function createSvnRpc(deps) {
13738
14126
  error: "svn 命令不可用(可在设置页配置 svn 路径)"
13739
14127
  };
13740
14128
  try {
13741
- const outcome = await runSvn(sc.cwd, ["update", rel || "."], UPDATE_GRACE_MS);
13742
- const result = updateResultOf(outcome.stdout, outcome.stderr, outcome.code);
14129
+ const argv = [
14130
+ "status",
14131
+ "-u",
14132
+ "--xml"
14133
+ ];
14134
+ if (rel) argv.push("--", rel);
14135
+ const outcome = await runSvn(status.wcRoot, argv, REMOTE_GRACE_MS, SUM_OUTPUT_CAP);
13743
14136
  if (outcome.code !== 0) return {
13744
14137
  ok: false,
13745
- error: "更新失败:" + tailOfText(result.output)
14138
+ error: "远端检查失败:" + tailOfText([outcome.stderr, outcome.stdout].filter(Boolean).join("\n"))
13746
14139
  };
13747
14140
  return {
13748
14141
  ok: true,
13749
- ...result
14142
+ ...parseRemoteStatusXml(outcome.stdout, status.wcRoot)
13750
14143
  };
13751
14144
  } catch (error) {
13752
14145
  return {
13753
14146
  ok: false,
13754
- error: "更新失败:" + String(error)
14147
+ error: "远端检查失败:" + String(error)
13755
14148
  };
13756
14149
  }
13757
14150
  },
13758
- "svn.tortoise": async (args) => {
14151
+ "svn.conflictArtifacts": async (args) => {
13759
14152
  const sc = await requireSvnSession(ctx, args.sessionId);
13760
14153
  if ("err" in sc) return {
13761
14154
  ok: false,
13762
14155
  error: sc.err
13763
14156
  };
13764
- const action = args.action;
13765
- if (action !== "update" && action !== "commit" && action !== "log" && action !== "diff" && action !== "blame" && action !== "revert") return {
13766
- ok: false,
13767
- error: "未知动作: " + String(action)
13768
- };
13769
14157
  const rel = safeRel(args.path);
13770
- if (rel === null) return {
14158
+ if (rel === null || rel === "") return {
13771
14159
  ok: false,
13772
14160
  error: "路径不合法"
13773
14161
  };
13774
14162
  const status = await statusOf(sc.cwd);
13775
- if (!status.managed) return {
14163
+ if (!status.managed || !status.wcRoot) return {
13776
14164
  ok: false,
13777
14165
  error: "当前工作区不受 SVN 管理"
13778
14166
  };
13779
- if (!status.tortoise) return {
13780
- ok: false,
13781
- error: "TortoiseProc.exe 不可用(仅 Windows;可在设置页配置目录)"
13782
- };
13783
- const fs = ctx.get("fs");
13784
- if (!fs?.resolve || !fs?.stat || !fs?.processPath) return {
13785
- ok: false,
13786
- error: "缺少 fs"
13787
- };
13788
14167
  try {
13789
- const target = await fs.resolve(rel || ".", { cwd: sc.cwd });
13790
- if (!await fs.stat(target)) return {
13791
- ok: false,
13792
- error: "路径不存在"
13793
- };
13794
- const abs = fs.processPath(target);
13795
- launchTortoise(status.tortoiseExe, action, abs);
13796
14168
  return {
13797
14169
  ok: true,
13798
- launched: abs
14170
+ artifacts: await conflictArtifactsOf(status.wcRoot, rel)
13799
14171
  };
13800
14172
  } catch (error) {
13801
14173
  return {
13802
14174
  ok: false,
13803
- error: "启动 TortoiseSVN 失败:" + String(error)
14175
+ error: "扫描冲突副本失败:" + String(error)
13804
14176
  };
13805
14177
  }
13806
- }
13807
- } };
14178
+ },
14179
+ "svn.diffLocalPair": async (args) => {
14180
+ const sc = await requireSvnSession(ctx, args.sessionId);
14181
+ if ("err" in sc) return {
14182
+ ok: false,
14183
+ error: sc.err
14184
+ };
14185
+ const left = safeRel(args.left);
14186
+ const right = safeRel(args.right);
14187
+ if (!left || !right) return {
14188
+ ok: false,
14189
+ error: "路径不合法(需工作副本内相对路径)"
14190
+ };
14191
+ const status = await statusOf(sc.cwd);
14192
+ if (!status.managed || !status.wcRoot) return {
14193
+ ok: false,
14194
+ error: "当前工作区不受 SVN 管理"
14195
+ };
14196
+ const fs = ctx.get("fs");
14197
+ if (!fs?.resolve || !fs?.stat || !fs?.readText) return {
14198
+ ok: false,
14199
+ error: "缺少 fs"
14200
+ };
14201
+ try {
14202
+ const sides = [];
14203
+ for (const rel of [left, right]) {
14204
+ const resolved = await fs.resolve(rel, { cwd: sc.cwd });
14205
+ const info = await fs.stat(resolved);
14206
+ if (!info || info.type !== "file") return {
14207
+ ok: false,
14208
+ error: "文件不存在:" + rel
14209
+ };
14210
+ if ((info.size ?? 0) > READ_CAP) return {
14211
+ ok: false,
14212
+ error: "文件过大(>8MB):" + rel
14213
+ };
14214
+ sides.push(await fs.readText(resolved));
14215
+ }
14216
+ return {
14217
+ ok: true,
14218
+ left: sides[0],
14219
+ right: sides[1],
14220
+ ...guardSides(sides[0], sides[1])
14221
+ };
14222
+ } catch (error) {
14223
+ return {
14224
+ ok: false,
14225
+ error: "读取冲突副本失败:" + String(error)
14226
+ };
14227
+ }
14228
+ },
14229
+ "svn.fileSizes": async (args) => {
14230
+ const sc = await requireSvnSession(ctx, args.sessionId);
14231
+ if ("err" in sc) return {
14232
+ ok: false,
14233
+ error: sc.err
14234
+ };
14235
+ const list = Array.isArray(args.paths) ? args.paths.slice(0, 200) : [];
14236
+ if (!list.length) return {
14237
+ ok: true,
14238
+ sizes: []
14239
+ };
14240
+ const fs = ctx.get("fs");
14241
+ if (!fs?.resolve || !fs?.stat) return {
14242
+ ok: false,
14243
+ error: "缺少 fs"
14244
+ };
14245
+ const sizes = [];
14246
+ for (const raw of list) {
14247
+ const rel = safeRel(raw);
14248
+ if (rel === null || rel === "") {
14249
+ sizes.push({
14250
+ path: String(raw ?? ""),
14251
+ size: null
14252
+ });
14253
+ continue;
14254
+ }
14255
+ try {
14256
+ const resolved = await fs.resolve(rel, { cwd: sc.cwd });
14257
+ const info = await fs.stat(resolved);
14258
+ sizes.push({
14259
+ path: rel,
14260
+ size: info && info.type === "file" ? info.size ?? null : null
14261
+ });
14262
+ } catch {
14263
+ sizes.push({
14264
+ path: rel,
14265
+ size: null
14266
+ });
14267
+ }
14268
+ }
14269
+ return {
14270
+ ok: true,
14271
+ sizes
14272
+ };
14273
+ },
14274
+ "svn.wcRev": async (args) => {
14275
+ const sc = await requireSvnSession(ctx, args.sessionId);
14276
+ if ("err" in sc) return {
14277
+ ok: false,
14278
+ error: sc.err
14279
+ };
14280
+ const rel = safeRel(args.path);
14281
+ if (rel === null || rel === "") return {
14282
+ ok: true,
14283
+ revision: null
14284
+ };
14285
+ const status = await statusOf(sc.cwd);
14286
+ if (!status.managed || !status.wcRoot) return {
14287
+ ok: false,
14288
+ error: "当前工作区不受 SVN 管理"
14289
+ };
14290
+ if (!status.svnCli) return {
14291
+ ok: false,
14292
+ error: "svn 命令不可用(可在设置页配置 svn 路径)"
14293
+ };
14294
+ const outcome = await runSvn(status.wcRoot, [
14295
+ "info",
14296
+ "--xml",
14297
+ "--",
14298
+ rel
14299
+ ], READ_GRACE_MS);
14300
+ if (outcome.code !== 0) return {
14301
+ ok: true,
14302
+ revision: null
14303
+ };
14304
+ return {
14305
+ ok: true,
14306
+ revision: parseSvnInfoRevision(outcome.stdout)
14307
+ };
14308
+ },
14309
+ "svn.cleanup": async (args) => {
14310
+ const sc = await requireSvnSession(ctx, args.sessionId);
14311
+ if ("err" in sc) return {
14312
+ ok: false,
14313
+ error: sc.err
14314
+ };
14315
+ const status = await statusOf(sc.cwd);
14316
+ if (!status.managed || !status.wcRoot) return {
14317
+ ok: false,
14318
+ error: "当前工作区不受 SVN 管理"
14319
+ };
14320
+ if (!status.svnCli) return {
14321
+ ok: false,
14322
+ error: "svn 命令不可用(可在设置页配置 svn 路径)"
14323
+ };
14324
+ const argv = ["cleanup"];
14325
+ if (args.removeUnversioned === true) argv.push("--remove-unversioned");
14326
+ if (args.removeIgnored === true) argv.push("--remove-ignored");
14327
+ try {
14328
+ const outcome = await runSvn(status.wcRoot, argv, MUTATE_GRACE_MS);
14329
+ const result = batchResultOf(outcome.stdout, outcome.stderr, outcome.code, "清理");
14330
+ if (outcome.code !== 0) return {
14331
+ ok: false,
14332
+ error: "清理失败:" + tailOfText(result.output)
14333
+ };
14334
+ changesCache.delete(status.wcRoot);
14335
+ return {
14336
+ ok: true,
14337
+ ...result
14338
+ };
14339
+ } catch (error) {
14340
+ return {
14341
+ ok: false,
14342
+ error: "清理失败:" + String(error)
14343
+ };
14344
+ }
14345
+ },
14346
+ "svn.update": async (args) => {
14347
+ const sc = await requireSvnSession(ctx, args.sessionId);
14348
+ if ("err" in sc) return {
14349
+ ok: false,
14350
+ error: sc.err
14351
+ };
14352
+ const rel = safeRel(args.path);
14353
+ if (rel === null) return {
14354
+ ok: false,
14355
+ error: "路径不合法"
14356
+ };
14357
+ const status = await statusOf(sc.cwd);
14358
+ if (!status.managed) return {
14359
+ ok: false,
14360
+ error: "当前工作区不受 SVN 管理"
14361
+ };
14362
+ if (!status.svnCli) return {
14363
+ ok: false,
14364
+ error: "svn 命令不可用(可在设置页配置 svn 路径)"
14365
+ };
14366
+ try {
14367
+ const outcome = await runSvn(sc.cwd, ["update", rel || "."], UPDATE_GRACE_MS);
14368
+ const result = updateResultOf(outcome.stdout, outcome.stderr, outcome.code);
14369
+ if (outcome.code !== 0) return {
14370
+ ok: false,
14371
+ error: "更新失败:" + tailOfText(result.output)
14372
+ };
14373
+ return {
14374
+ ok: true,
14375
+ ...result
14376
+ };
14377
+ } catch (error) {
14378
+ return {
14379
+ ok: false,
14380
+ error: "更新失败:" + String(error)
14381
+ };
14382
+ }
14383
+ },
14384
+ "svn.tortoise": async (args) => {
14385
+ const sc = await requireSvnSession(ctx, args.sessionId);
14386
+ if ("err" in sc) return {
14387
+ ok: false,
14388
+ error: sc.err
14389
+ };
14390
+ const action = args.action;
14391
+ if (action !== "update" && action !== "commit" && action !== "log" && action !== "diff" && action !== "blame" && action !== "revert") return {
14392
+ ok: false,
14393
+ error: "未知动作: " + String(action)
14394
+ };
14395
+ const rel = safeRel(args.path);
14396
+ if (rel === null) return {
14397
+ ok: false,
14398
+ error: "路径不合法"
14399
+ };
14400
+ const status = await statusOf(sc.cwd);
14401
+ if (!status.managed) return {
14402
+ ok: false,
14403
+ error: "当前工作区不受 SVN 管理"
14404
+ };
14405
+ if (!status.tortoise) return {
14406
+ ok: false,
14407
+ error: "TortoiseProc.exe 不可用(仅 Windows;可在设置页配置目录)"
14408
+ };
14409
+ const fs = ctx.get("fs");
14410
+ if (!fs?.resolve || !fs?.stat || !fs?.processPath) return {
14411
+ ok: false,
14412
+ error: "缺少 fs"
14413
+ };
14414
+ try {
14415
+ const target = await fs.resolve(rel || ".", { cwd: sc.cwd });
14416
+ if (!await fs.stat(target)) return {
14417
+ ok: false,
14418
+ error: "路径不存在"
14419
+ };
14420
+ const abs = fs.processPath(target);
14421
+ launchTortoise(status.tortoiseExe, action, abs);
14422
+ return {
14423
+ ok: true,
14424
+ launched: abs
14425
+ };
14426
+ } catch (error) {
14427
+ return {
14428
+ ok: false,
14429
+ error: "启动 TortoiseSVN 失败:" + String(error)
14430
+ };
14431
+ }
14432
+ }
14433
+ } };
14434
+ }
14435
+ //#endregion
14436
+ //#region src/shared/dap.ts
14437
+ /** 全部合法动作(运行时校验用)。 */
14438
+ const DAP_ACTIONS = [
14439
+ "continue",
14440
+ "next",
14441
+ "stepIn",
14442
+ "stepOut",
14443
+ "pause",
14444
+ "disconnect"
14445
+ ];
14446
+ /** 调试配置文件(工作区相对路径;插件专属,与 VS Code 的 .vscode/launch.json 互不干扰)。 */
14447
+ const DAP_LAUNCH_REL = ".dsh/launch.json";
14448
+ /** 旧共用配置路径(仅首读迁移源:复制到 DAP_LAUNCH_REL 后不再读取,永不回写)。 */
14449
+ const DAP_LEGACY_LAUNCH_REL = ".vscode/launch.json";
14450
+ /**
14451
+ * 配置 → DAP request 类型:显式 request 优先;emmylua_attach 旧配置缺 request 时按 attach 兼容。
14452
+ * @author ddj 2026年09月21号
14453
+ * @param config 调试配置(取 type/request 两字段)
14454
+ * @returns 'attach' | 'launch'
14455
+ */
14456
+ function requestOfConfig(config) {
14457
+ if (config.request === "attach" || config.request === "launch") return config.request;
14458
+ return config.type === "emmylua_attach" ? "attach" : "launch";
14459
+ }
14460
+ /**
14461
+ * 是否应发送 configurationDone(DAP 标准握手收尾:断点下发完 → configurationDone → 目标开跑)。
14462
+ * 仅当适配器在 initialize 响应声明 supportsConfigurationDoneRequest 时发送;
14463
+ * emmylua 适配器未声明该能力(实测响应体仅 5 个 supports* 字段)→ 不发,Lua 行为不变。
14464
+ * @author ddj 2026年09月21号
14465
+ * @param caps initialize 响应 body(capabilities)
14466
+ */
14467
+ function needsConfigurationDone(caps) {
14468
+ return caps?.supportsConfigurationDoneRequest === true;
14469
+ }
14470
+ /**
14471
+ * stopped 事件 body → 当前线程 id:多线程适配器(coreclr 等)取真实 threadId,
14472
+ * 缺省/非法回退 1(单线程适配器 emmylua 语义不变)。
14473
+ * @author ddj 2026年09月21号
14474
+ * @param body stopped 事件 body
14475
+ */
14476
+ function threadIdOfStopped(body) {
14477
+ const id = body.threadId;
14478
+ return typeof id === "number" && Number.isInteger(id) && id > 0 ? id : 1;
14479
+ }
14480
+ /**
14481
+ * setBreakpoints 响应 body → 断点回执(DAP 顺序与请求一致;缺行回退请求行)。
14482
+ * 适配器会把断点挪到最近可绑定行并给出 verified/message——必须消费而非造假回执。
14483
+ * @author ddj 2026年09月21号
14484
+ * @param body setBreakpoints 响应 body(breakpoints 数组)
14485
+ * @param items 本次请求的断点(回退基准)
14486
+ */
14487
+ function bpAcksOfResponse(body, items) {
14488
+ const list = body?.breakpoints;
14489
+ if (!Array.isArray(list)) return items.map((it) => ({
14490
+ line: it.line,
14491
+ verified: true
14492
+ }));
14493
+ return list.map((item, i) => {
14494
+ const b = item ?? {};
14495
+ const ack = {
14496
+ line: typeof b.line === "number" && b.line >= 1 ? b.line : items[i]?.line ?? 0,
14497
+ verified: b.verified === true
14498
+ };
14499
+ if (typeof b.message === "string" && b.message) ack.message = b.message;
14500
+ return ack;
14501
+ });
14502
+ }
14503
+ //#endregion
14504
+ //#region src/dap/sourcePath.ts
14505
+ /**
14506
+ * dsh-vscode-mode host — DAP source 路径归一化与候选排序。
14507
+ * 适配器可能上报绝对路径、工作区相对路径、file:// URI 或 Lua chunkname;
14508
+ * 这里统一转换,避免调用栈只显示 rawFile 但无法跳转。
14509
+ * 作者 ddj 2026年09月29号
14510
+ */
14511
+ /** 统一路径分隔符并去掉 chunkname 的 @ 前缀。 */
14512
+ function normalizeSourcePath(value) {
14513
+ let text = String(value ?? "").trim();
14514
+ if (!text) return "";
14515
+ if (/^file:\/\//i.test(text)) text = fileUriPath(text);
14516
+ return text.replace(/^@/, "").replace(/\\/g, "/");
14517
+ }
14518
+ /** 将 file:// URI 转成本地路径(兼容 Windows 盘符与 UNC)。 */
14519
+ function fileUriPath(value) {
14520
+ try {
14521
+ const url = new URL(value);
14522
+ let path = decodeURIComponent(url.pathname);
14523
+ if (url.hostname && url.hostname !== "localhost") path = "//" + url.hostname + path;
14524
+ if (/^\/[A-Za-z]:/.test(path)) path = path.slice(1);
14525
+ return path;
14526
+ } catch {
14527
+ return value.replace(/^file:\/\//i, "");
14528
+ }
14529
+ }
14530
+ /** 取路径 basename(大小写保持原样)。 */
14531
+ function sourceBaseOf(value) {
14532
+ return normalizeSourcePath(value).split("/").pop() ?? "";
14533
+ }
14534
+ /** 规范化路径键(大小写不敏感,供候选缓存使用)。 */
14535
+ function sourceKeyOf(value) {
14536
+ return normalizeSourcePath(value).toLowerCase();
14537
+ }
14538
+ /**
14539
+ * 把 source 路径转换为工作区相对路径;工作区外路径保留规范化绝对路径,
14540
+ * 不再返回空串,调用方仍可交给现有 tabPathOf/absoluteOf 继续处理。
14541
+ */
14542
+ function sourcePathOf(value, workspacePath) {
14543
+ const source = normalizeSourcePath(value);
14544
+ if (!source) return "";
14545
+ const root = normalizeSourcePath(workspacePath).replace(/\/+$/, "");
14546
+ if (root && source.toLowerCase().startsWith((root + "/").toLowerCase())) return source.slice(root.length + 1);
14547
+ return source;
14548
+ }
14549
+ /**
14550
+ * 对 findFile 候选按 chunkname 相关度排序:完整路径后缀 > 路径片段 > basename/stem,
14551
+ * 同分时保持确定性字典序,避免同名 Lua 文件随机取第一项。
14552
+ */
14553
+ function rankSourceCandidates(chunk, files, workspacePath) {
14554
+ const needle = normalizeSourcePath(chunk).toLowerCase();
14555
+ const needleParts = needle.split("/").filter(Boolean);
14556
+ const needleBase = sourceBaseOf(needle).toLowerCase();
14557
+ const needleStem = needleBase.replace(/\.[^.]+$/, "");
14558
+ return [...files].sort((a, b) => scoreSource(b, needle, needleParts, needleBase, needleStem, workspacePath) - scoreSource(a, needle, needleParts, needleBase, needleStem, workspacePath) || a.localeCompare(b));
14559
+ }
14560
+ /** 计算单个候选相关度。 */
14561
+ function scoreSource(file, needle, parts, base, stem, workspacePath) {
14562
+ const rel = sourcePathOf(file, workspacePath).toLowerCase();
14563
+ const fileBase = sourceBaseOf(rel).toLowerCase();
14564
+ const fileStem = fileBase.replace(/\.[^.]+$/, "");
14565
+ let score = 0;
14566
+ if (needle && (rel === needle || rel.endsWith("/" + needle) || needle.endsWith("/" + rel))) score += 1e5;
14567
+ if (base && fileBase === base) score += 1e4;
14568
+ if (stem && fileStem === stem) score += 5e3;
14569
+ const relParts = rel.split("/").filter(Boolean);
14570
+ let suffix = 0;
14571
+ for (let i = 1; i <= Math.min(parts.length, relParts.length); i++) {
14572
+ if (parts[parts.length - i] !== relParts[relParts.length - i]) break;
14573
+ suffix += 1;
14574
+ }
14575
+ return score + suffix * 100;
14576
+ }
14577
+ //#endregion
14578
+ //#region src/dap/launchConfig.ts
14579
+ /**
14580
+ * 清洗 JSONC 为可 JSON.parse 文本:去 // 与 /* *\/ 注释、去尾逗号(字符串字面量感知)。
14581
+ * @author ddj 2026年09月29号
14582
+ * @param text 原始 launch.json 文本
14583
+ * @returns 清洗后文本
14584
+ */
14585
+ function stripJsonc(text) {
14586
+ let out = "";
14587
+ let i = 0;
14588
+ const n = text.length;
14589
+ let state = "code";
14590
+ while (i < n) {
14591
+ const ch = text[i];
14592
+ const next = i + 1 < n ? text[i + 1] : "";
14593
+ if (state === "string") {
14594
+ out += ch;
14595
+ if (ch === "\\") {
14596
+ out += next;
14597
+ i += 2;
14598
+ continue;
14599
+ }
14600
+ if (ch === "\"") state = "code";
14601
+ i += 1;
14602
+ continue;
14603
+ }
14604
+ if (ch === "\"") {
14605
+ state = "string";
14606
+ out += ch;
14607
+ i += 1;
14608
+ continue;
14609
+ }
14610
+ if (ch === "/" && next === "/") {
14611
+ while (i < n && text[i] !== "\n") i += 1;
14612
+ continue;
14613
+ }
14614
+ if (ch === "/" && next === "*") {
14615
+ i += 2;
14616
+ while (i + 1 < n && !(text[i] === "*" && text[i + 1] === "/")) i += 1;
14617
+ i += 2;
14618
+ continue;
14619
+ }
14620
+ out += ch;
14621
+ i += 1;
14622
+ }
14623
+ return out.replace(/,(\s*[}\]])/g, "$1");
14624
+ }
14625
+ /**
14626
+ * 解析 launch.json 文本为全量调试配置列表(非法/缺 name/type 的条目跳过)。
14627
+ * @author ddj 2026年09月29号 / 2026年09月21号
14628
+ * @param text launch.json 文本(JSONC)
14629
+ * @param workspacePath 工作区根(${workspaceFolder}/${cwd} 替换基准;空 = 不替换)
14630
+ * @returns 全量配置(raw 原样透传)
14631
+ */
14632
+ function parseLaunchConfigs(text, workspacePath = "") {
14633
+ let data;
14634
+ try {
14635
+ data = JSON.parse(stripJsonc(text));
14636
+ } catch {
14637
+ return [];
14638
+ }
14639
+ const list = data?.configurations;
14640
+ if (!Array.isArray(list)) return [];
14641
+ const out = [];
14642
+ for (const item of list) {
14643
+ if (!item || typeof item !== "object" || Array.isArray(item)) continue;
14644
+ const cfg = item;
14645
+ const type = asString(cfg.type);
14646
+ const name = asString(cfg.name);
14647
+ if (!type || !name) continue;
14648
+ out.push({
14649
+ name,
14650
+ type,
14651
+ request: asString(cfg.request),
14652
+ processName: asString(cfg.processName),
14653
+ pid: asPositive(cfg.pid),
14654
+ processId: asPositive(cfg.processId),
14655
+ host: asString(cfg.host),
14656
+ port: asPositive(cfg.port),
14657
+ sourcePaths: stringListOf(cfg.sourcePaths),
14658
+ ext: stringListOf(cfg.ext),
14659
+ captureLog: typeof cfg.captureLog === "boolean" ? cfg.captureLog : void 0,
14660
+ raw: expandRaw(cfg, workspacePath)
14661
+ });
14662
+ }
14663
+ return out;
14664
+ }
14665
+ /**
14666
+ * 配置是否含 ${command:...} 占位(VS Code 命令,本插件无法解析,需用户改为具体值)。
14667
+ * @author ddj 2026年09月21号
14668
+ * @param config 调试配置
14669
+ */
14670
+ function hasCmdPlaceholder(config) {
14671
+ return JSON.stringify(config.raw ?? {}).includes("${command:");
14672
+ }
14673
+ /** ${workspaceFolder}/${cwd} → 工作区根(大小写不敏感;其余占位保持原样交适配器)。 */
14674
+ function expandLaunchVars(text, workspacePath) {
14675
+ if (!workspacePath) return text;
14676
+ return text.replace(/\$\{workspaceFolder\}/gi, workspacePath).replace(/\$\{cwd\}/gi, workspacePath);
14677
+ }
14678
+ /** raw 逐字段递归展开(字符串内占位替换;数组/嵌套对象同口径)。 */
14679
+ function expandRaw(raw, workspacePath) {
14680
+ const out = {};
14681
+ for (const [key, value] of Object.entries(raw)) out[key] = expandValue(value, workspacePath);
14682
+ return out;
14683
+ }
14684
+ /** 单值展开。 */
14685
+ function expandValue(value, workspacePath) {
14686
+ if (typeof value === "string") return expandLaunchVars(value, workspacePath);
14687
+ if (Array.isArray(value)) return value.map((v) => expandValue(v, workspacePath));
14688
+ if (value && typeof value === "object") return expandRaw(value, workspacePath);
14689
+ return value;
14690
+ }
14691
+ /** unknown → 字符串(非字符串返回 undefined)。 */
14692
+ function asString(value) {
14693
+ return typeof value === "string" && value ? value : void 0;
14694
+ }
14695
+ /** unknown → 正数(number 且 >0;其余 undefined)。 */
14696
+ function asPositive(value) {
14697
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
14698
+ }
14699
+ /** 字符串数组字段清洗(非字符串元素剔除;空数组返回 undefined)。 */
14700
+ function stringListOf(raw) {
14701
+ if (!Array.isArray(raw)) return void 0;
14702
+ const items = raw.filter((v) => typeof v === "string" && v.length > 0);
14703
+ return items.length ? items : void 0;
14704
+ }
14705
+ //#endregion
14706
+ //#region src/dap/configSnippets.ts
14707
+ /**
14708
+ * dsh-vscode-mode host — launch.json「添加配置」片段提取。
14709
+ * 扫描 VSCode 扩展清单 contributes.debuggers[]:
14710
+ * - configurationSnippets[](label/description/body)优先;缺失时 initialConfigurations[] 兜底
14711
+ * (条目本身就是配置体,label 取 body.name → debugger.label → type);
14712
+ * - %key% 占位符按 package.nls.json → package.nls.zh-cn.json 合并表解析(zh 覆盖 base),
14713
+ * 解析失败剥 %…% 外壳(避免裸占位符上屏);
14714
+ * - body 缺 type/name 时补齐(parseLaunchConfigs 要求两者,缺了插进去也不可见)。
14715
+ * 与 discovery 各自独立缓存(片段随扩展安装变化;force 重扫,测试隔离)。
14716
+ * 作者 ddj 2026年09月22号
14717
+ */
14718
+ /** 发现缓存(null = 未加载;按 home 隔离,测试用 force/clear 复位)。 */
14719
+ let cache$1 = null;
14720
+ let cacheHome$1 = "";
14721
+ /**
14722
+ * 全部可添加调试配置片段(按 label 字典序稳定输出,type+label 去重)。
14723
+ * @author ddj 2026年09月22号
14724
+ * @param force 强制重扫(扩展安装后 / 测试隔离用)
14725
+ * @param home DSH home(缺省真实;测试可注入)
14726
+ * @returns 片段列表
14727
+ */
14728
+ function dapConfigSnippets(force = false, home = dshHome()) {
14729
+ if (!force && cache$1 !== null && home === cacheHome$1) return cache$1;
14730
+ cache$1 = scanSnippets(home);
14731
+ cacheHome$1 = home;
14732
+ return cache$1;
14733
+ }
14734
+ /**
14735
+ * 按适配器可用性标注片段(与调试工具条同源裁决 debuggerDecls):
14736
+ * type 命中 decl → 透传 available/reason;未命中(清单无该 type 的可用适配器声明)
14737
+ * → available=false +「未发现该类型适配器」。下拉据此过滤「选得进、跑不了」的条目。
14738
+ * @author ddj 2026年09月22号
14739
+ * @param snippets 片段列表(dapConfigSnippets 产物)
14740
+ * @param decls 适配器声明表(debuggerDecls())
14741
+ * @returns 带可用性标注的片段列表(新数组,不改入参)
14742
+ */
14743
+ function annotateSnippets(snippets, decls) {
14744
+ const byType = new Map(decls.map((decl) => [decl.type, decl]));
14745
+ return snippets.map((snip) => {
14746
+ const decl = byType.get(snip.type);
14747
+ if (!decl) return {
14748
+ ...snip,
14749
+ available: false,
14750
+ reason: "未发现该类型适配器"
14751
+ };
14752
+ const annotated = {
14753
+ ...snip,
14754
+ available: decl.available
14755
+ };
14756
+ if (!decl.available && decl.reason) annotated.reason = decl.reason;
14757
+ return annotated;
14758
+ });
14759
+ }
14760
+ /** 扫描全部扩展根收集片段(先到先得去重,输出按 label 排序)。 */
14761
+ function scanSnippets(home) {
14762
+ const out = [];
14763
+ const seen = /* @__PURE__ */ new Set();
14764
+ for (const root of vscodeExtensionsDirs(home)) for (const extDir of subDirsOf$1(root)) collectExt(extDir, out, seen);
14765
+ return out.sort((a, b) => a.label.localeCompare(b.label));
14766
+ }
14767
+ /** 单扩展处理:有产出原料才读 nls(多数扩展无调试片段,不白读两个小文件)。 */
14768
+ function collectExt(extDir, out, seen) {
14769
+ const plans = debuggersOf$1(extDir).map((entry) => ({
14770
+ entry,
14771
+ type: strOf(entry.type),
14772
+ items: rawItemsOf(entry)
14773
+ })).filter((plan) => Boolean(plan.type) && plan.items.length > 0);
14774
+ if (!plans.length) return;
14775
+ const nls = readNlsTable(extDir);
14776
+ for (const plan of plans) for (const item of plan.items) {
14777
+ const snippet = projectSnippet(plan.entry, plan.type, item, nls);
14778
+ const key = plan.type + "\0" + snippet.label;
14779
+ if (seen.has(key)) continue;
14780
+ seen.add(key);
14781
+ out.push(snippet);
14782
+ }
14783
+ }
14784
+ /** 清单条目 → 片段投影(nls 解析 + type/name 补齐)。 */
14785
+ function projectSnippet(entry, type, item, nls) {
14786
+ const body = resolveNlsDeep(item.body, nls);
14787
+ if (!strOf(body.type)) body.type = type;
14788
+ const label = resolveNlsText(item.label ?? "", nls) || strOf(body.name) || resolveNlsText(strOf(entry.label), nls) || type;
14789
+ if (!strOf(body.name)) body.name = label;
14790
+ const snippet = {
14791
+ label,
14792
+ type: strOf(body.type) || type,
14793
+ bodyText: JSON.stringify(body, null, 2)
14794
+ };
14795
+ const description = resolveNlsText(item.description ?? "", nls);
14796
+ if (description) snippet.description = description;
14797
+ return snippet;
14798
+ }
14799
+ /** 读扩展根下的扩展子目录(根不存在返回空;与 discovery 同口径)。 */
14800
+ function subDirsOf$1(root) {
14801
+ let entries;
14802
+ try {
14803
+ entries = readdirSync(root, { withFileTypes: true });
14804
+ } catch {
14805
+ return [];
14806
+ }
14807
+ return entries.filter((e) => e.isDirectory()).map((e) => join(root, e.name));
14808
+ }
14809
+ /** 读某扩展的 contributes.debuggers(无清单/无声明/解析失败返回空)。 */
14810
+ function debuggersOf$1(extDir) {
14811
+ try {
14812
+ const text = readFileSync(join(extDir, "package.json"), "utf8").replace(/^/, "");
14813
+ const list = JSON.parse(text).contributes?.debuggers;
14814
+ return Array.isArray(list) ? list : [];
14815
+ } catch {
14816
+ return [];
14817
+ }
14818
+ }
14819
+ /** 条目 → 原料列表:configurationSnippets 优先,initialConfigurations 兜底(条目即配置体)。 */
14820
+ function rawItemsOf(entry) {
14821
+ const out = [];
14822
+ const snippets = asArray(entry.configurationSnippets);
14823
+ for (const raw of snippets) {
14824
+ const snippet = asRecord(raw);
14825
+ if (!snippet) continue;
14826
+ const body = asRecord(snippet.body);
14827
+ if (!body) continue;
14828
+ out.push({
14829
+ label: strOf(snippet.label),
14830
+ description: strOf(snippet.description),
14831
+ body
14832
+ });
14833
+ }
14834
+ if (out.length) return out;
14835
+ for (const raw of asArray(entry.initialConfigurations)) {
14836
+ const body = asRecord(raw);
14837
+ if (body) out.push({ body });
14838
+ }
14839
+ return out;
14840
+ }
14841
+ /**
14842
+ * 读扩展 nls 表(base 先入,zh-cn 后入覆盖;文件缺失/解析失败按空跳过)。
14843
+ * @author ddj 2026年09月22号
14844
+ * @param extDir 扩展根目录
14845
+ * @returns %key% → 文本 表
14846
+ */
14847
+ function readNlsTable(extDir) {
14848
+ const table = {};
14849
+ for (const file of ["package.nls.json", "package.nls.zh-cn.json"]) try {
14850
+ const path = join(extDir, file);
14851
+ if (!existsSync(path)) continue;
14852
+ const data = JSON.parse(readFileSync(path, "utf8").replace(/^/, ""));
14853
+ for (const [key, value] of Object.entries(data)) if (typeof value === "string") table[key] = value;
14854
+ } catch {}
14855
+ return table;
14856
+ }
14857
+ /**
14858
+ * 解析单串 %key% 占位(最多 3 轮:nls 值可再含占位;未命中剥 %…% 外壳)。
14859
+ * @author ddj 2026年09月22号
14860
+ * @param value 原文
14861
+ * @param table nls 表
14862
+ * @returns 解析后文本(无占位原样返回)
14863
+ */
14864
+ function resolveNlsText(value, table) {
14865
+ if (!value.includes("%")) return value;
14866
+ let out = value;
14867
+ for (let round = 0; round < 3; round += 1) {
14868
+ const next = out.replace(/%([A-Za-z0-9_.-]+)%/g, (_match, key) => table[key] ?? key);
14869
+ if (next === out) break;
14870
+ out = next;
14871
+ }
14872
+ return out;
14873
+ }
14874
+ /** 深度解析任意值内的 %key%(字符串逐个过 resolveNlsText,对象/数组递归)。 */
14875
+ function resolveNlsDeep(value, table) {
14876
+ if (typeof value === "string") return resolveNlsText(value, table);
14877
+ if (Array.isArray(value)) return value.map((item) => resolveNlsDeep(item, table));
14878
+ if (value && typeof value === "object") {
14879
+ const out = {};
14880
+ for (const [key, item] of Object.entries(value)) out[key] = resolveNlsDeep(item, table);
14881
+ return out;
14882
+ }
14883
+ return value;
14884
+ }
14885
+ /** unknown → 非空字符串(其余空串)。 */
14886
+ function strOf(value) {
14887
+ return typeof value === "string" && value ? value : "";
14888
+ }
14889
+ /** unknown → 普通对象(数组/原始值返回 null)。 */
14890
+ function asRecord(value) {
14891
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
14892
+ }
14893
+ /** unknown → 数组(单对象包一层,兼容 initialConfigurations 单对象写法)。 */
14894
+ function asArray(value) {
14895
+ if (Array.isArray(value)) return value;
14896
+ return value && typeof value === "object" ? [value] : [];
14897
+ }
14898
+ //#endregion
14899
+ //#region src/dap/processList.ts
14900
+ /**
14901
+ * dsh-vscode-mode host — 调试目标进程枚举(Windows)。
14902
+ * 用 PowerShell Get-Process 取 窗口标题/进程名/路径(完整 Unicode,无 GBK 乱码),
14903
+ * 等价替代 VS Code 扩展的 emmy_tool.exe list_processes(扩展侧 QuickPick 的数据源),
14904
+ * 匹配语义一致:processName 对 窗口标题 或 进程文件名 做**包含**匹配。
14905
+ * emmylua_attach 仅支持 Windows(适配器原生限制),非 Windows 返回空表。
14906
+ * 作者 ddj 2026年09月29号
14907
+ */
14908
+ /** PowerShell 单行命令:仅带主窗口的进程,JSON 输出(UTF-8)。 */
14909
+ const PROCESS_LIST_CMD = "$OutputEncoding=[Text.Encoding]::UTF8;[Console]::OutputEncoding=[Text.Encoding]::UTF8;Get-Process | Where-Object { $_.MainWindowHandle -ne 0 } | Select-Object Id,ProcessName,MainWindowTitle,Path | ConvertTo-Json -Compress -Depth 2";
14910
+ /**
14911
+ * 解析 ConvertTo-Json 输出为进程列表(纯函数,可单测)。
14912
+ * 单条时 PowerShell 输出对象而非数组,两种形状都接受。
14913
+ * @author ddj 2026年09月29号
14914
+ * @param text PowerShell stdout
14915
+ * @returns 进程列表(无效行剔除)
14916
+ */
14917
+ function parseProcessListJson(text) {
14918
+ let data;
14919
+ try {
14920
+ data = JSON.parse(text);
14921
+ } catch {
14922
+ return [];
14923
+ }
14924
+ const rows = Array.isArray(data) ? data : [data];
14925
+ const out = [];
14926
+ for (const row of rows) {
14927
+ if (!row || typeof row !== "object") continue;
14928
+ const item = row;
14929
+ const pid = typeof item.Id === "number" ? item.Id : NaN;
14930
+ if (!Number.isFinite(pid) || pid <= 0) continue;
14931
+ const path = typeof item.Path === "string" ? item.Path : "";
14932
+ const name = path ? path.split(/[\\/]/).pop() ?? item.ProcessName ?? "" : (item.ProcessName ?? "") + ".exe";
14933
+ out.push({
14934
+ pid,
14935
+ title: typeof item.MainWindowTitle === "string" ? item.MainWindowTitle : "",
14936
+ path,
14937
+ name
14938
+ });
14939
+ }
14940
+ return out;
14941
+ }
14942
+ /**
14943
+ * 按 processName 过滤进程(与 VS Code 扩展口径一致:标题或文件名包含匹配;空串全量)。
14944
+ * @author ddj 2026年09月29号
14945
+ * @param items 全量进程
14946
+ * @param processName 过滤词
14947
+ */
14948
+ function filterProcesses(items, processName) {
14949
+ const needle = (processName ?? "").trim();
14950
+ if (!needle) return items;
14951
+ return items.filter((it) => it.title.includes(needle) || it.name.includes(needle));
14952
+ }
14953
+ /**
14954
+ * 枚举带主窗口的进程(Windows;spawn 失败/非 Windows 返回空表不抛错)。
14955
+ * @author ddj 2026年09月29号
14956
+ */
14957
+ function listProcesses() {
14958
+ if (process.platform !== "win32") return Promise.resolve([]);
14959
+ return new Promise((resolve) => {
14960
+ execFile("powershell.exe", [
14961
+ "-NoProfile",
14962
+ "-NonInteractive",
14963
+ "-Command",
14964
+ PROCESS_LIST_CMD
14965
+ ], {
14966
+ encoding: "utf8",
14967
+ windowsHide: true,
14968
+ timeout: 15e3,
14969
+ maxBuffer: 8 << 20
14970
+ }, (error, stdout) => {
14971
+ if (error && !stdout) {
14972
+ resolve([]);
14973
+ return;
14974
+ }
14975
+ resolve(parseProcessListJson(String(stdout ?? "")));
14976
+ });
14977
+ });
14978
+ }
14979
+ //#endregion
14980
+ //#region src/dap/discovery.ts
14981
+ /**
14982
+ * dsh-vscode-mode host — 调试适配器发现(清单驱动,VSCode contributes.debuggers 同源语义)。
14983
+ * 扫描全部扩展根(复用 lsp 侧 vscodeExtensionsDirs:~/.vscode/extensions、
14984
+ * ~/.vscode-server/extensions、插件扩展目录),读各扩展 package.json 的
14985
+ * contributes.debuggers[] 声明,解析为 DapAdapterDecl:
14986
+ * - program 按平台取 windows/linux/osx 覆盖,相对扩展根解析;
14987
+ * - runtime='node' → 由 node 跑 js 入口;无 runtime → program 即可执行(args 为清单声明);
14988
+ * - 同 type 多扩展并存 → 按「清单版本降序(numeric) → 路径升序」取唯一(与 LSP 候选口径一致);
14989
+ * - 入口不存在 / runtime 不支持 → available=false + reason(如 DotRush 按需下载的 clrdbg,
14990
+ * 诚实标注不假成功,调用方据此在下拉置灰并提示原因)。
14991
+ * 作者 ddj 2026年09月21号
14992
+ */
14993
+ /** 发现缓存(扩展清单会话内基本不变;测试用 force 重扫)。 */
14994
+ let cacheReady = false;
14995
+ let cacheHome = "";
14996
+ let cache = [];
14997
+ /**
14998
+ * 发现全部调试适配器声明(按 type 去重、type 字典序稳定输出)。
14999
+ * @author ddj 2026年09月21号
15000
+ * @param force 强制重扫(扩展安装后 / 测试隔离用)
15001
+ * @param home DSH home(缺省真实;测试可注入)
15002
+ */
15003
+ function debuggerDecls(force = false, home = dshHome()) {
15004
+ if (cacheReady && !force && home === cacheHome) return cache;
15005
+ cache = scanDecls(home);
15006
+ cacheHome = home;
15007
+ cacheReady = true;
15008
+ return cache;
15009
+ }
15010
+ /** 扫描全部扩展根并收集声明(同 type 去重后返回)。 */
15011
+ function scanDecls(home) {
15012
+ const hits = [];
15013
+ for (const root of vscodeExtensionsDirs(home)) for (const extDir of subDirsOf(root)) for (const entry of debuggersOf(extDir)) {
15014
+ const decl = declOf(extDir, entry);
15015
+ if (decl) hits.push({
15016
+ decl,
15017
+ version: manifestVersionOf(extDir) ?? ""
15018
+ });
15019
+ }
15020
+ return pickPerType(hits);
15021
+ }
15022
+ /** 同 type 去重:清单版本降序(numeric)→ 路径升序;输出按 type 字典序。 */
15023
+ function pickPerType(hits) {
15024
+ const byType = /* @__PURE__ */ new Map();
15025
+ for (const hit of hits) {
15026
+ if (!betterOf(hit, byType.get(hit.decl.type))) continue;
15027
+ byType.set(hit.decl.type, hit);
15028
+ }
15029
+ return [...byType.values()].map((h) => h.decl).sort((a, b) => a.type.localeCompare(b.type));
15030
+ }
15031
+ /** 候选比较:版本降序优先,平版本路径升序(无前任直接胜出)。 */
15032
+ function betterOf(hit, prev) {
15033
+ if (!prev) return true;
15034
+ const byVersion = compareVersion(hit.version, prev.version);
15035
+ if (byVersion !== 0) return byVersion > 0;
15036
+ return hit.decl.extensionPath.localeCompare(prev.decl.extensionPath) < 0;
15037
+ }
15038
+ /** 清单版本比较(numeric 感知,与 LSP 候选排序同口径):>0 表示 a 更新。 */
15039
+ function compareVersion(a, b) {
15040
+ return String(a || "").localeCompare(String(b || ""), void 0, { numeric: true });
15041
+ }
15042
+ /** 枚举扩展根下的扩展子目录(根不存在返回空)。 */
15043
+ function subDirsOf(root) {
15044
+ let entries;
15045
+ try {
15046
+ entries = readdirSync(root, { withFileTypes: true });
15047
+ } catch {
15048
+ return [];
15049
+ }
15050
+ return entries.filter((e) => e.isDirectory()).map((e) => join(root, e.name));
15051
+ }
15052
+ /** 读某扩展的 contributes.debuggers 声明(无清单/无声明/解析失败返回空)。 */
15053
+ function debuggersOf(extDir) {
15054
+ try {
15055
+ const text = readFileSync(join(extDir, "package.json"), "utf8").replace(/^\uFEFF/, "");
15056
+ const list = JSON.parse(text).contributes?.debuggers;
15057
+ return Array.isArray(list) ? list : [];
15058
+ } catch {
15059
+ return [];
15060
+ }
15061
+ }
15062
+ /** 清单条目 → 声明(缺 type/program 视为无效条目跳过)。 */
15063
+ function declOf(extDir, entry) {
15064
+ const type = typeof entry.type === "string" ? entry.type : "";
15065
+ if (!type || typeof entry.program !== "string" || !entry.program) return null;
15066
+ const languages = stringsOf(entry.languages);
15067
+ const resolved = resolveProgram(extDir, entry);
15068
+ const runtime = typeof entry.runtime === "string" && entry.runtime ? entry.runtime : void 0;
15069
+ const runtimeBad = runtime !== void 0 && runtime !== "node";
15070
+ const available = resolved.available && !runtimeBad;
15071
+ const reason = !resolved.available ? resolved.reason : runtimeBad ? "暂不支持运行时 \"" + runtime + "\"(仅支持 node 与原生可执行)" : void 0;
15072
+ return {
15073
+ type,
15074
+ label: typeof entry.label === "string" && entry.label ? entry.label : type,
15075
+ extensionId: extensionIdOf(extDir),
15076
+ extensionPath: extDir,
15077
+ program: resolved.program,
15078
+ args: stringsOf(entry.args),
15079
+ runtime,
15080
+ languages,
15081
+ exts: extsOf(entry, languages),
15082
+ available,
15083
+ reason
15084
+ };
15085
+ }
15086
+ /** 平台 program 解析:windows/linux/osx(darwin) 覆盖优先,相对扩展根(剥 ./)。 */
15087
+ function resolveProgram(extDir, entry) {
15088
+ const plat = process.platform;
15089
+ const override = plat === "win32" ? entry.windows?.program : plat === "darwin" ? entry.osx?.program ?? entry.darwin?.program : entry.linux?.program;
15090
+ const rel = typeof override === "string" && override ? override : entry.program;
15091
+ if (typeof rel !== "string" || !rel) return {
15092
+ program: "",
15093
+ available: false,
15094
+ reason: "清单未声明本平台 program"
15095
+ };
15096
+ const program = join(extDir, ...rel.replace(/^\.\//, "").split(/[\\/]/));
15097
+ if (!existsSync(program)) return {
15098
+ program,
15099
+ available: false,
15100
+ reason: "适配器入口不存在(扩展可能需按需下载):" + rel
15101
+ };
15102
+ return {
15103
+ program,
15104
+ available: true
15105
+ };
15106
+ }
15107
+ /** 来源扩展 id(目录名 publisher.name-version 取 version 前段;解析失败回退目录名)。 */
15108
+ function extensionIdOf(extDir) {
15109
+ const base = extDir.split(/[\\/]/).pop() ?? "";
15110
+ const dot = base.lastIndexOf("-");
15111
+ return dot > 0 ? base.slice(0, dot) : base;
15112
+ }
15113
+ /** 可下断点扩展名:优先清单 configurationAttributes 的 ext 缺省(emmylua 的 .lua 家族),
15114
+ * 否则按声明 languages 映射(LSP 侧同源);两者皆空 = 不限(不过滤断点)。 */
15115
+ function extsOf(entry, languages) {
15116
+ const attrs = entry.configurationAttributes;
15117
+ const fromManifest = stringsOf(attrs?.attach?.properties?.ext?.default ?? attrs?.launch?.properties?.ext?.default);
15118
+ if (fromManifest.length) return fromManifest;
15119
+ const out = [];
15120
+ for (const lang of languages) for (const ext of LSP_LANG_EXT[lang] ?? []) {
15121
+ const dotted = "." + ext.replace(/^\./, "");
15122
+ if (!out.includes(dotted)) out.push(dotted);
15123
+ }
15124
+ return out;
15125
+ }
15126
+ /** unknown → 字符串数组(非字符串元素剔除)。 */
15127
+ function stringsOf(value) {
15128
+ if (!Array.isArray(value)) return [];
15129
+ return value.filter((v) => typeof v === "string" && v.length > 0);
15130
+ }
15131
+ //#endregion
15132
+ //#region src/dap/provider.ts
15133
+ /**
15134
+ * dsh-vscode-mode host — 调试适配器规格解析:按调试类型从扩展清单声明(discovery)
15135
+ * 解析 spawn 规格,通用不限定语言:
15136
+ * - runtime='node' → node 跑 js 入口(emmylua 形态,等价旧 process.execPath 硬编码);
15137
+ * - 无 runtime → program 即原生可执行(clrdbg/monodbg 形态,args 为清单声明如 --interpreter=vscode);
15138
+ * - 其它 runtime 明确不支持(decl.available=false 带原因,不做假启动)。
15139
+ * 旧版仅探测 tangzx.emmylua 两型(attach/new),由清单驱动后自然覆盖并扩展到任意适配器。
15140
+ * 作者 ddj 2026年09月29号 / 2026年09月21号
15141
+ */
15142
+ /**
15143
+ * 解析某调试类型的适配器启动规格。
15144
+ * @author ddj 2026年09月29号 / 2026年09月21号
15145
+ * @param type 调试类型(launch.json type = 清单 contributes.debuggers.type)
15146
+ * @param force 强制重扫扩展清单(测试隔离用)
15147
+ * @param home DSH home(缺省真实;测试可注入)
15148
+ * @returns 启动规格;类型未发现 / 不可用 / runtime 不支持返回 null(原因见 discovery 声明)
15149
+ */
15150
+ function resolveAdapterSpec(type, force = false, home) {
15151
+ const decl = debuggerDecls(force, home).find((d) => d.type === type);
15152
+ if (!decl || !decl.available) return null;
15153
+ const base = {
15154
+ type,
15155
+ extensionPath: decl.extensionPath,
15156
+ extensionId: decl.extensionId,
15157
+ exts: decl.exts,
15158
+ detail: decl.extensionPath
15159
+ };
15160
+ if (decl.runtime === "node") return {
15161
+ ...base,
15162
+ command: process.execPath,
15163
+ args: [decl.program, ...decl.args]
15164
+ };
15165
+ if (!decl.runtime) return {
15166
+ ...base,
15167
+ command: decl.program,
15168
+ args: [...decl.args]
15169
+ };
15170
+ return null;
15171
+ }
15172
+ //#endregion
15173
+ //#region src/dap/protocol.ts
15174
+ /** 客户端 → 适配器请求帧构造。 */
15175
+ function dapRequest(seq, command, args) {
15176
+ return args === void 0 ? {
15177
+ seq,
15178
+ type: "request",
15179
+ command
15180
+ } : {
15181
+ seq,
15182
+ type: "request",
15183
+ command,
15184
+ arguments: args
15185
+ };
15186
+ }
15187
+ /** 响应守卫。 */
15188
+ function isDapResponse(msg) {
15189
+ return msg.type === "response" && typeof msg.request_seq === "number";
15190
+ }
15191
+ /** 事件守卫。 */
15192
+ function isDapEvent(msg) {
15193
+ return msg.type === "event" && typeof msg.event === "string";
15194
+ }
15195
+ /** 事件体安全取值。 */
15196
+ function eventBody(event) {
15197
+ return event.body && typeof event.body === "object" ? event.body : {};
15198
+ }
15199
+ /** 响应体安全取值。 */
15200
+ function responseBody(response) {
15201
+ return response.body && typeof response.body === "object" ? response.body : {};
15202
+ }
15203
+ /**
15204
+ * initialize 请求参数(适配器协商口径:path 格式、行列 1 基)。
15205
+ * @author ddj 2026年09月29号
15206
+ * @param adapterId 适配器 id
15207
+ */
15208
+ function initializeArgs(adapterId) {
15209
+ return {
15210
+ adapterID: adapterId,
15211
+ locale: "zh-cn",
15212
+ linesStartAt1: true,
15213
+ columnsStartAt1: true,
15214
+ pathFormat: "path",
15215
+ supportsVariableType: true,
15216
+ supportsVariablePaging: false,
15217
+ supportsRunInTerminalRequest: false
15218
+ };
15219
+ }
15220
+ //#endregion
15221
+ //#region src/dap/manager.ts
15222
+ /**
15223
+ * dsh-vscode-mode host — 调试会话管理器(DAP 桥核心,适配器通用)。
15224
+ * 单例调试会话:spawn 扩展清单声明的 DAP 适配器(node js 或原生 exe)↔ 本管理器翻译成
15225
+ * edrv.dap.* 视图。要点(emmylua 源自适配器源码逆向,探针验证;通用语义对齐 DAP 3.x):
15226
+ * - 帧协议与 LSP 相同(Content-Length)→ 复用 lsp/jsonrpc.ts;
15227
+ * - initialize → attach/launch 响应后适配器异步连目标,连上才发 initialized 事件,
15228
+ * 此时需把已存断点全量重发;随后按能力位发 configurationDone(emmylua 未声明该能力
15229
+ * 不发,行为不变;clrdbg 等声明了必须发,否则目标挂起等待);
15230
+ * - 停帧后 stackTrace/scopes/variables/evaluate 才有效(适配器未连接时 evaluate 永不回包,
15231
+ * 必须守卫 + 请求级超时);
15232
+ * - stackTrace 期间 emmylua 适配器可能发 findFileReq **反向请求**等 findFileRsp
15233
+ * (chunkname→文件由 IDE 侧解析),本管理器经注入的文件索引回调应答;
15234
+ * 其它适配器直接上报可打开的源码路径,不走该扩展事件;
15235
+ * - 线程:emmylua 固定 threadId=1;通用适配器取 stopped 事件的真实 threadId,
15236
+ * 栈/单步/暂停均携带;
15237
+ * - emmylua 的 scopes frameId 是 stacks 数组下标(私有语义),标准适配器用原生 frame id;
15238
+ * - 适配器对无效 pid 会未捕获异常崩溃 → attach 前必须先经进程枚举校验 pid。
15239
+ * 作者 ddj 2026年09月29号 / 2026年09月21号
15240
+ */
15241
+ const EVENT_CAP = 500;
15242
+ const REQUEST_TIMEOUT_MS = 8e3;
15243
+ /** emmylua 兼容 shim 的来源扩展 id(extensionPath/pid 等缺省注入仅对其生效)。 */
15244
+ const EMMYLUA_EXT_ID = "tangzx.emmylua";
15245
+ /** 判断 source 是否已经是可定位的源码路径(exts 由调用方按当前适配器给出)。 */
15246
+ function hasSourceExtension(value, exts) {
15247
+ const path = normalizeSourcePath(value).toLowerCase();
15248
+ if (!path) return false;
15249
+ return exts.some((ext) => path.endsWith(String(ext).toLowerCase()));
15250
+ }
15251
+ /**
15252
+ * 是否为「裸 chunkname」:无目录分隔符且非盘符/UNC 前缀。
15253
+ *
15254
+ * 这类值(如 xLua 上报的 `GuideSystemMgr`)被 {@link sourcePathOf} 处理后仍是原串,
15255
+ * 客户端会按工作区根拼接(`<cwd>/GuideSystemMgr`)——该路径必然不存在,
15256
+ * 于是断点命中后页签打不开、报「文件加载失败」。故反查失败时须判定为未解析,
15257
+ * 而不是把它当成文件路径下发。
15258
+ * @author ddj 2026年09月21号
15259
+ * @param value 适配器上报的 source 值
15260
+ * @returns 是否裸 chunkname
15261
+ */
15262
+ function isBareChunk(value) {
15263
+ const text = normalizeSourcePath(value);
15264
+ if (!text) return false;
15265
+ if (text.includes("/")) return false;
15266
+ return !/^[a-z]:/i.test(text);
15267
+ }
15268
+ /**
15269
+ * 单例调试会话。生命周期:start → starting →(initialized)running →(stopped)paused
15270
+ * →(terminated/disconnect)terminated → dispose 回 idle。
15271
+ * @author ddj 2026年09月29号
15272
+ */
15273
+ var DapSession = class {
15274
+ deps;
15275
+ child = null;
15276
+ phase = "idle";
15277
+ config = null;
15278
+ workspacePath = "";
15279
+ pid;
15280
+ error;
15281
+ /** spawn 的扩展根目录(emmylua 兼容 shim 注入 extensionPath)。 */
15282
+ extPath = "";
15283
+ /** 来源扩展 id(emmylua 兼容 shim 与 frameId 语义判定)。 */
15284
+ extId = "";
15285
+ /** 当前适配器可下断点扩展名(断点语言过滤;空 = 不限)。 */
15286
+ exts = [];
15287
+ /** initialize 响应 capabilities(configurationDone 门控)。 */
15288
+ caps = null;
15289
+ /** 当前停帧线程(多线程适配器取 stopped 事件真实 threadId;emmylua 恒 1)。 */
15290
+ threadId = 1;
15291
+ /** 断点(绝对路径),initialized 与每次 setBreakpoints 时全量下发。 */
15292
+ breakpoints = [];
15293
+ /** findFileReq chunkname → 已排序候选首路径(frame source.path 缺失时回退)。 */
15294
+ sourceFiles = /* @__PURE__ */ new Map();
15295
+ /** 停帧调用栈缓存(paused 期间供 stackTrace/topFrame)。 */
15296
+ frames = [];
15297
+ stateTop = null;
15298
+ events = [];
15299
+ eventSeq = 0;
15300
+ msgSeq = 1;
15301
+ /** 适配器 stderr 尾部缓冲(保留末尾 STDERR_TAIL_CAP 字符,退出 fail 消息附带用)。 */
15302
+ stderrTail = "";
15303
+ pending = /* @__PURE__ */ new Map();
15304
+ constructor(deps) {
15305
+ this.deps = deps;
15306
+ }
15307
+ /** 当前状态视图。 */
15308
+ stateOf() {
15309
+ return {
15310
+ phase: this.phase,
15311
+ configName: this.config?.name,
15312
+ configType: this.config?.type,
15313
+ pid: this.pid,
15314
+ error: this.error,
15315
+ topFrame: this.phase === "paused" ? this.stateTop : null
15316
+ };
15317
+ }
15318
+ /**
15319
+ * 启动调试会话(异步推进:spawn → initialize → attach/launch)。
15320
+ * @author ddj 2026年09月29号
15321
+ * @param spec 适配器启动规格
15322
+ * @param config 调试配置
15323
+ * @param workspacePath 工作区绝对路径(断点/findFile 基准)
15324
+ * @param pid 附加目标 pid(attach 型必填且必须已校验存在)
15325
+ */
15326
+ start(spec, config, workspacePath, pid) {
15327
+ if (this.child) this.killChild();
15328
+ this.config = config;
15329
+ this.workspacePath = workspacePath;
15330
+ this.extPath = spec.extensionPath;
15331
+ this.extId = spec.extensionId;
15332
+ this.exts = spec.exts;
15333
+ this.caps = null;
15334
+ this.threadId = 1;
15335
+ this.pid = pid;
15336
+ this.error = void 0;
15337
+ this.stderrTail = "";
15338
+ this.frames = [];
15339
+ this.sourceFiles.clear();
15340
+ this.stateTop = null;
15341
+ this.setPhase("starting");
15342
+ try {
15343
+ this.spawnAdapter(spec);
15344
+ this.handshake(spec, config);
15345
+ } catch (error) {
15346
+ this.fail("启动适配器失败:" + String(error));
15347
+ }
15348
+ }
15349
+ /** 停止会话(disconnect 尽力发送 + 杀进程树)。 */
15350
+ stop() {
15351
+ if (!this.child) {
15352
+ this.setPhase("idle");
15353
+ return;
15354
+ }
15355
+ this.request("disconnect").catch(() => {});
15356
+ this.killChild();
15357
+ this.pushEvent({ kind: "terminated" });
15358
+ this.setPhase("terminated");
15359
+ }
15360
+ /** 释放会话(插件卸载/进程退出):杀进程树并复位。 */
15361
+ dispose() {
15362
+ this.killChild();
15363
+ this.setPhase("idle");
15364
+ }
15365
+ /**
15366
+ * poll:状态 + since 之后的增量事件(纯读,无副作用)。
15367
+ * @author ddj 2026年09月29号
15368
+ * @param since 客户端已有游标
15369
+ */
15370
+ poll(since) {
15371
+ const events = this.events.filter((e) => e.seq > since);
15372
+ const nextSeq = this.eventSeq;
15373
+ return {
15374
+ state: this.stateOf(),
15375
+ events,
15376
+ nextSeq
15377
+ };
15378
+ }
15379
+ /**
15380
+ * 设置某文件断点(DAP per-source replace-all 语义)。
15381
+ * 不属于当前适配器语言的文件不入适配器(防跨语言串发),回执 verified=false 带原因;
15382
+ * 适配器在线时消费真实回执(可能挪行/判不可验证),离线时按旧口径乐观回执(start 后 flush 重发)。
15383
+ * @author ddj 2026年09月29号 / 2026年09月21号
15384
+ * @param absFile 目标文件绝对路径
15385
+ * @param items 该文件断点行列表
15386
+ */
15387
+ async setBreakpoints(absFile, items) {
15388
+ const allowed = this.bpAllowed(absFile);
15389
+ this.breakpoints = this.breakpoints.filter((bp) => bp.file !== absFile);
15390
+ if (allowed) for (const p of items) this.breakpoints.push({
15391
+ file: absFile,
15392
+ line: p.line,
15393
+ condition: p.condition,
15394
+ hitCondition: p.hitCondition,
15395
+ logMessage: p.logMessage
15396
+ });
15397
+ const res = allowed ? await this.request("setBreakpoints", {
15398
+ source: {
15399
+ name: absFile.split(/[\\/]/).pop() ?? "",
15400
+ path: absFile
15401
+ },
15402
+ breakpoints: items.map((p) => ({
15403
+ line: p.line,
15404
+ condition: p.condition,
15405
+ hitCondition: p.hitCondition,
15406
+ logMessage: p.logMessage
15407
+ }))
15408
+ }) : null;
15409
+ if (!res || !isDapResponse(res) || !res.success) return items.map((p) => ({
15410
+ line: p.line,
15411
+ verified: allowed,
15412
+ ...allowed ? {} : { message: "当前调试器不处理该文件类型" }
15413
+ }));
15414
+ return bpAcksOfResponse(responseBody(res), items);
15415
+ }
15416
+ /** 调用栈(paused 限定;threadId 取当前停帧线程)。 */
15417
+ async stackTrace() {
15418
+ if (this.phase !== "paused") return this.frames;
15419
+ const res = await this.request("stackTrace", { threadId: this.threadId });
15420
+ if (!res || !isDapResponse(res) || !res.success) return this.frames;
15421
+ const raw = responseBody(res).stackFrames;
15422
+ if (!Array.isArray(raw)) return this.frames;
15423
+ this.frames = await Promise.all(raw.map((item, index) => this.frameViewOf(item, index)));
15424
+ this.stateTop = this.frames[0] ? {
15425
+ file: this.frames[0].file,
15426
+ rawFile: this.frames[0].rawFile,
15427
+ line: this.frames[0].line,
15428
+ name: this.frames[0].name
15429
+ } : null;
15430
+ return this.frames;
15431
+ }
15432
+ /** 作用域列表(paused 限定;frameId 语义随适配器:emmylua=栈下标,标准=原生 frame id)。 */
15433
+ async scopes(frameId) {
15434
+ if (this.phase !== "paused") return [];
15435
+ const res = await this.request("scopes", { frameId });
15436
+ if (!res || !isDapResponse(res) || !res.success) return [];
15437
+ const raw = responseBody(res).scopes;
15438
+ if (!Array.isArray(raw)) return [];
15439
+ return raw.map((item) => {
15440
+ const s = item;
15441
+ return {
15442
+ name: String(s.name ?? ""),
15443
+ ref: typeof s.variablesReference === "number" ? s.variablesReference : 0,
15444
+ hint: typeof s.presentationHint === "string" ? s.presentationHint : void 0
15445
+ };
15446
+ });
15447
+ }
15448
+ /** 变量子级(paused 限定)。 */
15449
+ async variables(ref) {
15450
+ if (this.phase !== "paused") return [];
15451
+ const res = await this.request("variables", { variablesReference: ref });
15452
+ if (!res || !isDapResponse(res) || !res.success) return [];
15453
+ const raw = responseBody(res).variables;
15454
+ if (!Array.isArray(raw)) return [];
15455
+ return raw.map((item) => {
15456
+ const v = item;
15457
+ return {
15458
+ name: String(v.name ?? ""),
15459
+ value: String(v.value ?? ""),
15460
+ type: typeof v.type === "string" ? v.type : void 0,
15461
+ ref: typeof v.variablesReference === "number" ? v.variablesReference : 0
15462
+ };
15463
+ });
15464
+ }
15465
+ /** 表达式求值(REPL/监视;paused 限定,避免适配器挂起)。 */
15466
+ async evaluate(expression, frameId) {
15467
+ if (this.phase !== "paused") return {
15468
+ result: "未暂停:仅在断点暂停时可求值",
15469
+ ref: 0
15470
+ };
15471
+ const res = await this.request("evaluate", {
15472
+ expression,
15473
+ frameId: frameId ?? 0,
15474
+ context: "repl"
15475
+ });
15476
+ if (!res || !isDapResponse(res) || !res.success) return {
15477
+ result: "求值失败",
15478
+ ref: 0
15479
+ };
15480
+ const body = responseBody(res);
15481
+ return {
15482
+ result: String(body.result ?? ""),
15483
+ type: typeof body.type === "string" ? body.type : void 0,
15484
+ ref: typeof body.variablesReference === "number" ? body.variablesReference : 0
15485
+ };
15486
+ }
15487
+ /** 调试控制动作(线程型命令携带当前停帧线程;emmylua 单线程 threadId 恒 1 不受影响)。 */
15488
+ action(action) {
15489
+ if (!DAP_ACTIONS.includes(action)) return;
15490
+ const command = action === "disconnect" ? "disconnect" : action;
15491
+ this.request(command, action === "disconnect" ? void 0 : { threadId: this.threadId }).catch(() => {});
15492
+ if (action === "continue" || action === "next" || action === "stepIn" || action === "stepOut") this.setPhase("running");
15493
+ }
15494
+ /** spawn 适配器进程并接好帧解析。 */
15495
+ spawnAdapter(spec) {
15496
+ const child = spawn(spec.command, spec.args, {
15497
+ stdio: [
15498
+ "pipe",
15499
+ "pipe",
15500
+ "pipe"
15501
+ ],
15502
+ windowsHide: true
15503
+ });
15504
+ this.child = child;
15505
+ const parser = createFrameParser();
15506
+ child.stdout?.on("data", (chunk) => {
15507
+ for (const message of parser.push(chunk)) this.handleMessage(message);
15508
+ });
15509
+ child.stderr?.on("data", (chunk) => {
15510
+ log.debug("[dap-adapter] " + chunk.toString("utf8").trim());
15511
+ this.stderrTail = (this.stderrTail + chunk.toString("utf8")).slice(-2e3);
15512
+ });
15513
+ child.on("error", (error) => this.fail("适配器进程错误:" + String(error)));
15514
+ child.on("exit", (code) => {
15515
+ if (this.child === child && this.phase !== "idle" && this.phase !== "terminated") this.fail("适配器退出(code=" + code + ")" + this.stderrSuffix());
15516
+ });
15517
+ }
15518
+ /**
15519
+ * stderr 尾部摘要(供 fail 消息附带):取末尾若干非空行、以 | 折叠单行化。
15520
+ * 无 stderr 内容时返回空串(不改变既有消息文案)。
15521
+ * @author ddj 2026年09月22号
15522
+ * @returns 形如「;stderr: xxx | yyy」的摘要,或空串
15523
+ */
15524
+ stderrSuffix() {
15525
+ const text = this.stderrTail.trim();
15526
+ if (!text) return "";
15527
+ const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
15528
+ if (!lines.length) return "";
15529
+ return ";stderr: " + lines.slice(-6).join(" | ");
15530
+ }
15531
+ /** initialize → attach/launch 握手(initialized 事件后重发断点 + 按能力位收尾)。 */
15532
+ async handshake(spec, config) {
15533
+ const init = await this.request("initialize", initializeArgs(spec.type));
15534
+ if (!init || !isDapResponse(init) || !init.success) {
15535
+ this.fail("initialize 失败:" + (init && isDapResponse(init) ? init.message ?? "" : "超时"));
15536
+ return;
15537
+ }
15538
+ this.caps = responseBody(init);
15539
+ const request = requestOfConfig(config);
15540
+ const res = await this.request(request, this.requestArgs(config));
15541
+ if (!res || !isDapResponse(res) || !res.success) this.fail((request === "attach" ? "attach 失败:" : "launch 失败:") + (res && isDapResponse(res) ? res.message ?? "" : "超时"));
15542
+ }
15543
+ /**
15544
+ * attach/launch 请求参数:launch.json 原样透传(含未识别字段,对齐 VS Code 语义),
15545
+ * emmylua 兼容缺省注入(对齐扩展自身 resolveDebugConfiguration:extensionPath/pid 等)。
15546
+ * @author ddj 2026年09月21号
15547
+ * @param config 调试配置
15548
+ */
15549
+ requestArgs(config) {
15550
+ const args = { ...config.raw ?? {} };
15551
+ if (args.cwd === void 0 && this.workspacePath) args.cwd = this.workspacePath;
15552
+ if (this.extId !== EMMYLUA_EXT_ID) {
15553
+ if (!(config.type === "unity") && this.pid && requestOfConfig(config) === "attach" && args.processId === void 0) args.processId = this.pid;
15554
+ return args;
15555
+ }
15556
+ if (args.extensionPath === void 0) args.extensionPath = this.extPath;
15557
+ if (requestOfConfig(config) === "attach") {
15558
+ args.pid = this.pid ?? (typeof args.pid === "number" && args.pid > 0 ? args.pid : 0);
15559
+ if (!Array.isArray(args.sourcePaths)) args.sourcePaths = [this.workspacePath];
15560
+ if (!Array.isArray(args.ext)) args.ext = [".lua"];
15561
+ if (args.captureLog === void 0) args.captureLog = false;
15562
+ return args;
15563
+ }
15564
+ if (args.host === void 0) args.host = "localhost";
15565
+ if (args.port === void 0) args.port = 9966;
15566
+ if (args.ideConnectDebugger === void 0) args.ideConnectDebugger = true;
15567
+ if (!Array.isArray(args.sourcePaths)) args.sourcePaths = [this.workspacePath];
15568
+ if (!Array.isArray(args.ext)) args.ext = [".lua"];
15569
+ return args;
15570
+ }
15571
+ /** DAP 消息分流:响应 → pending;事件 → handleEvent。 */
15572
+ handleMessage(message) {
15573
+ if (isDapResponse(message)) {
15574
+ const entry = this.pending.get(message.request_seq);
15575
+ if (entry) {
15576
+ this.pending.delete(message.request_seq);
15577
+ entry.resolve(message);
15578
+ }
15579
+ return;
15580
+ }
15581
+ if (isDapEvent(message)) this.handleEvent(message);
15582
+ }
15583
+ /** 事件处理:initialized/stopped/terminated/output/log/findFileReq。 */
15584
+ handleEvent(event) {
15585
+ const body = eventBody(event);
15586
+ if (event.event === "initialized") {
15587
+ this.setPhase("running");
15588
+ this.flushBreakpoints();
15589
+ if (needsConfigurationDone(this.caps)) this.request("configurationDone").catch(() => {});
15590
+ return;
15591
+ }
15592
+ if (event.event === "stopped") {
15593
+ this.threadId = threadIdOfStopped(body);
15594
+ this.handleStopped(String(body.reason ?? "breakpoint"));
15595
+ return;
15596
+ }
15597
+ if (event.event === "terminated") {
15598
+ if (this.phase === "starting") {
15599
+ const hint = this.earlyTermHint();
15600
+ this.pushEvent({
15601
+ kind: "output",
15602
+ text: hint + "\n"
15603
+ });
15604
+ this.deps.trace?.(this.workspacePath, "[DEBUG terminated] " + hint);
15605
+ }
15606
+ this.pushEvent({ kind: "terminated" });
15607
+ this.setPhase("terminated");
15608
+ return;
15609
+ }
15610
+ if (event.event === "continued") {
15611
+ this.setPhase("running");
15612
+ this.pushEvent({ kind: "continued" });
15613
+ return;
15614
+ }
15615
+ if (event.event === "findFileReq") {
15616
+ this.respondFindFile(body);
15617
+ return;
15618
+ }
15619
+ if (event.event === "output" || event.event === "log") this.pushEvent({
15620
+ kind: "output",
15621
+ text: String(body.output ?? body.message ?? "")
15622
+ });
15623
+ }
15624
+ /** 停帧:置 paused + 预拉调用栈(首帧进 state.topFrame 驱动跳转高亮)。 */
15625
+ async handleStopped(reason) {
15626
+ this.pushEvent({
15627
+ kind: "stopped",
15628
+ reason
15629
+ });
15630
+ this.setPhase("paused");
15631
+ try {
15632
+ await this.stackTrace();
15633
+ } catch {}
15634
+ }
15635
+ /**
15636
+ * 初始化前终止的控制台提示:指明失败阶段并给出适配器日志位置(根因常在适配器自家日志)。
15637
+ * DotRush 扩展的 monodbg 日志固定在扩展根 bin/DebuggerMono/logs,直接给绝对路径;
15638
+ * 其它适配器回退到通用指引(宿主日志 [dap-adapter] 段有 stderr 镜像)。
15639
+ * @author ddj 2026年09月22号
15640
+ * @returns 提示文本(不含换行)
15641
+ */
15642
+ earlyTermHint() {
15643
+ return "会话在初始化完成前终止(attach/连接目标失败)" + (this.extId === "nromanov.dotrush" && this.extPath ? ";适配器日志:" + join(this.extPath, "bin", "DebuggerMono", "logs", "Error.log") : ";请查看适配器自身日志与宿主日志 [dap-adapter] 段");
15644
+ }
15645
+ /** findFileReq 反向应答:按完整 chunkname 排序工作区候选并缓存首命中。 */
15646
+ async respondFindFile(body) {
15647
+ const seq = typeof body.seq === "number" ? body.seq : -1;
15648
+ const chunk = String(body.file ?? "");
15649
+ const files = await this.deps.findFiles(this.workspacePath, chunk);
15650
+ log.debug("[dap-trace] findFileReq seq=" + seq + " chunk=" + chunk + " candidates=" + files.length + " first=" + (files[0] ?? ""));
15651
+ this.deps.trace?.(this.workspacePath, "[DEBUG findFileReq] seq=" + seq + " chunk=" + chunk + " candidates=" + files.length + " first=" + (files[0] ?? ""));
15652
+ const first = files[0];
15653
+ if (first) {
15654
+ this.sourceFiles.set(sourceKeyOf(chunk), first);
15655
+ this.sourceFiles.set(sourceKeyOf(sourceBaseOf(chunk)), first);
15656
+ }
15657
+ this.send({
15658
+ seq: this.msgSeq++,
15659
+ type: "request",
15660
+ command: "findFileRsp",
15661
+ arguments: {
15662
+ seq,
15663
+ files
15664
+ }
15665
+ });
15666
+ }
15667
+ /** 发送已存断点(initialized 后调用一次;只发属于当前适配器语言的文件)。 */
15668
+ flushBreakpoints() {
15669
+ if (!this.breakpoints.length) return;
15670
+ const byFile = /* @__PURE__ */ new Map();
15671
+ for (const bp of this.breakpoints) {
15672
+ if (!this.bpAllowed(bp.file)) continue;
15673
+ const list = byFile.get(bp.file) ?? [];
15674
+ list.push(bp);
15675
+ byFile.set(bp.file, list);
15676
+ }
15677
+ for (const [file, list] of byFile) this.request("setBreakpoints", {
15678
+ source: {
15679
+ name: file.split(/[\\/]/).pop() ?? "",
15680
+ path: file
15681
+ },
15682
+ breakpoints: list.map((bp) => ({
15683
+ line: bp.line,
15684
+ condition: bp.condition,
15685
+ hitCondition: bp.hitCondition,
15686
+ logMessage: bp.logMessage
15687
+ }))
15688
+ }).catch(() => {});
15689
+ }
15690
+ /** 断点文件是否属于当前适配器(按声明扩展名过滤;未知语言不过滤,兼容旧链路)。 */
15691
+ bpAllowed(file) {
15692
+ if (!this.exts.length) return true;
15693
+ const lower = file.toLowerCase();
15694
+ return this.exts.some((ext) => lower.endsWith(ext));
15695
+ }
15696
+ /** 栈帧源码扩展名判定集:emmylua 沿用旧口径(.lua 家族 + 配置 ext),通用适配器用声明 exts。 */
15697
+ frameExts() {
15698
+ if (this.extId !== EMMYLUA_EXT_ID) return this.exts;
15699
+ return [.../* @__PURE__ */ new Set([
15700
+ ...this.config?.ext ?? [],
15701
+ ".lua",
15702
+ ".lua.bytes"
15703
+ ])];
15704
+ }
15705
+ /** 栈帧 id:emmylua 的 scopes 以 stacks 下标为 frameId(私有语义);标准适配器用原生 id。 */
15706
+ frameIdOf(raw, index) {
15707
+ if (this.extId === EMMYLUA_EXT_ID) return index;
15708
+ return typeof raw.id === "number" ? raw.id : index;
15709
+ }
15710
+ /** 发送请求(seq 自增 + 超时守卫;适配器不回包时拒绝而非悬挂)。 */
15711
+ request(command, args) {
15712
+ if (!this.child?.stdin?.writable) return Promise.resolve(null);
15713
+ const seq = this.msgSeq++;
15714
+ const frame = dapRequest(seq, command, args);
15715
+ return new Promise((resolve) => {
15716
+ const timer = setTimeout(() => {
15717
+ this.pending.delete(seq);
15718
+ resolve(null);
15719
+ }, REQUEST_TIMEOUT_MS);
15720
+ this.pending.set(seq, {
15721
+ resolve,
15722
+ timer
15723
+ });
15724
+ this.child.stdin.write(encodeMessage(frame));
15725
+ });
15726
+ }
15727
+ /** 直发原始帧(反向请求应答等无需等待响应的场景)。 */
15728
+ send(frame) {
15729
+ if (!this.child?.stdin?.writable) return;
15730
+ this.child.stdin.write(encodeMessage(frame));
15731
+ }
15732
+ /** 追加事件(环形截断)。 */
15733
+ pushEvent(partial) {
15734
+ this.eventSeq += 1;
15735
+ this.events.push({
15736
+ seq: this.eventSeq,
15737
+ ...partial
15738
+ });
15739
+ if (this.events.length > EVENT_CAP) this.events.splice(0, this.events.length - EVENT_CAP);
15740
+ }
15741
+ /** 记录错误并终止会话。 */
15742
+ fail(message) {
15743
+ this.error = message;
15744
+ log.warn("[dap] " + message);
15745
+ this.pushEvent({
15746
+ kind: "output",
15747
+ text: message + "\n"
15748
+ });
15749
+ this.pushEvent({ kind: "terminated" });
15750
+ this.killChild();
15751
+ this.setPhase("terminated");
15752
+ }
15753
+ /** 杀适配器进程树(Windows taskkill /T /F,同 LSP transport 口径)。 */
15754
+ killChild() {
15755
+ const child = this.child;
15756
+ this.child = null;
15757
+ for (const entry of this.pending.values()) {
15758
+ clearTimeout(entry.timer);
15759
+ entry.resolve(null);
15760
+ }
15761
+ this.pending.clear();
15762
+ if (!child) return;
15763
+ try {
15764
+ child.kill();
15765
+ } catch {}
15766
+ if (process.platform === "win32" && child.pid) try {
15767
+ spawn("taskkill", [
15768
+ "/PID",
15769
+ String(child.pid),
15770
+ "/T",
15771
+ "/F"
15772
+ ], {
15773
+ windowsHide: true,
15774
+ stdio: "ignore"
15775
+ });
15776
+ } catch {}
15777
+ setTimeout(() => {
15778
+ try {
15779
+ child.kill("SIGKILL");
15780
+ } catch {}
15781
+ }, 1500).unref?.();
15782
+ }
15783
+ /** 相位切换(running→running 等幂等跳过)。 */
15784
+ setPhase(phase) {
15785
+ if (this.phase === phase) return;
15786
+ this.phase = phase;
15787
+ if (phase === "idle") {
15788
+ this.events = [];
15789
+ this.config = null;
15790
+ this.pid = void 0;
15791
+ this.error = void 0;
15792
+ }
15793
+ }
15794
+ /** 适配器上报路径 → 帧视图(file 始终保留可打开的规范路径)。 */
15795
+ async frameViewOf(item, index) {
15796
+ const raw = item;
15797
+ const source = raw.source ?? {};
15798
+ const sourcePath = typeof source.path === "string" ? source.path : "";
15799
+ const sourceName = typeof source.name === "string" ? source.name : "";
15800
+ const cached = this.sourceFiles.get(sourceKeyOf(sourcePath)) ?? this.sourceFiles.get(sourceKeyOf(sourceName));
15801
+ let selected = cached || sourcePath || sourceName;
15802
+ const needsLookup = !selected || !hasSourceExtension(selected, this.frameExts());
15803
+ log.debug("[dap-trace] frame rawPath=" + sourcePath + " name=" + sourceName + " cached=" + (cached ?? "") + " needsLookup=" + needsLookup + " selected=" + selected);
15804
+ this.deps.trace?.(this.workspacePath, "[DEBUG frame] rawPath=" + sourcePath + " name=" + sourceName + " cached=" + (cached ?? "") + " needsLookup=" + needsLookup + " selected=" + selected);
15805
+ if (!cached && needsLookup) {
15806
+ const candidates = await this.deps.findFiles(this.workspacePath, sourceName || sourcePath);
15807
+ log.debug("[dap-trace] frameLookup source=" + (sourceName || sourcePath) + " candidates=" + candidates.length + " first=" + (candidates[0] ?? ""));
15808
+ this.deps.trace?.(this.workspacePath, "[DEBUG frameLookup] source=" + (sourceName || sourcePath) + " candidates=" + candidates.length + " first=" + (candidates[0] ?? ""));
15809
+ if (candidates[0]) {
15810
+ selected = candidates[0];
15811
+ this.sourceFiles.set(sourceKeyOf(sourceName || sourcePath), candidates[0]);
15812
+ } else if (isBareChunk(sourceName || sourcePath)) selected = "";
15813
+ }
15814
+ const finalFile = selected ? sourcePathOf(selected, this.workspacePath) : "";
15815
+ log.debug("[dap-trace] frameFinal name=" + sourceName + " file=" + finalFile + " line=" + String(raw.line ?? 0));
15816
+ this.deps.trace?.(this.workspacePath, "[DEBUG frameFinal] name=" + sourceName + " file=" + finalFile + " line=" + String(raw.line ?? 0));
15817
+ return {
15818
+ id: this.frameIdOf(raw, index),
15819
+ name: String(raw.name ?? ""),
15820
+ file: finalFile,
15821
+ rawFile: sourceName || sourcePath || selected,
15822
+ line: typeof raw.line === "number" ? raw.line : 0
15823
+ };
15824
+ }
15825
+ };
15826
+ //#endregion
15827
+ //#region src/dap/rpc.ts
15828
+ /**
15829
+ * dsh-vscode-mode host — 调试 RPC(edrv.dap.*)装配。
15830
+ * 单例 DapSession + 适配器发现(扩展清单驱动,通用不限定语言)+ 进程枚举 +
15831
+ * launch.json 读取(全量透传)+ findFile 反向匹配(chunkname → ripgrep 定向查找源文件)。
15832
+ * 作者 ddj 2026年09月29号 / 2026年09月21号
15833
+ */
15834
+ /**
15835
+ * 解析工作区根为 subprocess 执行世界可见的真实路径。
15836
+ * DAP 会话无 DSH Session 对象,不能让 fs.resolve('.') 落回 host 进程目录,
15837
+ * 因此显式以 cwd 为基准解析 `.`(= cwd 本身)。
15838
+ * @author ddj 2026年09月21号
15839
+ * @param ctx DSH 上下文
15840
+ * @param workspacePath 工作区绝对路径
15841
+ * @returns 可交给 rg 的根路径;fs 不可用或解析失败返回空串
15842
+ */
15843
+ async function rootPathOf(ctx, workspacePath) {
15844
+ const fs = ctx.get("fs");
15845
+ if (!fs || !workspacePath) return "";
15846
+ try {
15847
+ const target = await fs.resolve(".", { cwd: workspacePath });
15848
+ return fs.processPath(target) || "";
15849
+ } catch (error) {
15850
+ return "";
15851
+ }
15852
+ }
15853
+ /**
15854
+ * 创建调试 RPC(单例会话随插件装配创建,ctx.effect 里调 disposeDap 清理)。
15855
+ * @author ddj 2026年09月29号
15856
+ * @param ctx DSH 上下文
15857
+ */
15858
+ function createDapRpc(ctx) {
15859
+ const session = new DapSession({
15860
+ findFiles: async (workspacePath, chunk) => {
15861
+ const clean = normalizeSourcePath(chunk);
15862
+ if (!clean) return [];
15863
+ const root = await rootPathOf(ctx, workspacePath);
15864
+ if (!root) {
15865
+ debugRecord(ctx, workspacePath, "[DEBUG findFiles] chunk=" + clean + " root=(无法解析) candidates=0", "debug");
15866
+ return [];
15867
+ }
15868
+ const ranked = rankSourceCandidates(clean, await findFilesByChunk(ctx, root, clean), workspacePath);
15869
+ log.debug("[dap-trace] findFiles chunk=" + clean + " cwd=" + workspacePath + " root=" + root + " candidates=" + ranked.length + " first=" + (ranked[0] ?? ""));
15870
+ debugRecord(ctx, workspacePath, "[DEBUG findFiles] chunk=" + clean + " root=" + root + " candidates=" + ranked.length + " first=" + (ranked[0] ?? ""), "debug");
15871
+ return ranked;
15872
+ },
15873
+ trace: (workspacePath, message) => debugRecord(ctx, workspacePath, message, "debug")
15874
+ });
15875
+ return {
15876
+ handlers: {
15877
+ "edrv.dap.configs": async (args) => {
15878
+ const configs = await readLaunchConfigs(ctx, args.workspacePath);
15879
+ return {
15880
+ ok: true,
15881
+ configs,
15882
+ adapters: debuggerDecls().map((d) => ({
15883
+ type: d.type,
15884
+ label: d.label,
15885
+ available: d.available,
15886
+ reason: d.reason
15887
+ })),
15888
+ source: configs.length ? "launchjson" : "none"
15889
+ };
15890
+ },
15891
+ "edrv.dap.snippets": async () => ({
15892
+ ok: true,
15893
+ snippets: annotateSnippets(dapConfigSnippets(), debuggerDecls())
15894
+ }),
15895
+ "edrv.dap.processes": async (args) => {
15896
+ return {
15897
+ ok: true,
15898
+ items: filterProcesses(await listProcesses(), args.processName)
15899
+ };
15900
+ },
15901
+ "edrv.dap.start": async (args) => {
15902
+ const config = args.config;
15903
+ if (hasCmdPlaceholder(config)) return {
15904
+ ok: false,
15905
+ error: "配置含 ${command:...} 占位(VS Code 命令),本插件无法解析,请在 launch.json 中改为具体值"
15906
+ };
15907
+ const spec = resolveAdapterSpec(config.type);
15908
+ if (!spec) return {
15909
+ ok: false,
15910
+ error: unknownTypeMessage(config.type)
15911
+ };
15912
+ let pid = args.pid && args.pid > 0 ? args.pid : void 0;
15913
+ if (requestOfConfig(config) === "attach" && !pid && !config.processId) {
15914
+ const resolved = await resolvePid(config.processName);
15915
+ if (typeof resolved === "string") return {
15916
+ ok: false,
15917
+ error: resolved
15918
+ };
15919
+ pid = resolved;
15920
+ }
15921
+ session.start(spec, config, args.workspacePath, pid);
15922
+ return {
15923
+ ok: true,
15924
+ phase: "starting"
15925
+ };
15926
+ },
15927
+ "edrv.dap.stop": async () => {
15928
+ session.stop();
15929
+ return { ok: true };
15930
+ },
15931
+ "edrv.dap.poll": async (args) => ({
15932
+ ok: true,
15933
+ ...session.poll(args.since)
15934
+ }),
15935
+ "edrv.dap.setBreakpoints": async (args) => {
15936
+ const abs = join(args.workspacePath, args.file.replace(/\\/g, "/"));
15937
+ return {
15938
+ ok: true,
15939
+ breakpoints: await session.setBreakpoints(abs, args.points)
15940
+ };
15941
+ },
15942
+ "edrv.dap.stackTrace": async () => ({
15943
+ ok: true,
15944
+ frames: await session.stackTrace()
15945
+ }),
15946
+ "edrv.dap.scopes": async (args) => ({
15947
+ ok: true,
15948
+ scopes: await session.scopes(args.frameId)
15949
+ }),
15950
+ "edrv.dap.variables": async (args) => ({
15951
+ ok: true,
15952
+ variables: await session.variables(args.ref)
15953
+ }),
15954
+ "edrv.dap.evaluate": async (args) => ({
15955
+ ok: true,
15956
+ ...await session.evaluate(args.expression, args.frameId)
15957
+ }),
15958
+ "edrv.dap.command": async (args) => {
15959
+ if (!DAP_ACTIONS.includes(args.action)) return {
15960
+ ok: false,
15961
+ error: "未知动作:" + args.action
15962
+ };
15963
+ if (args.action === "disconnect") session.stop();
15964
+ else session.action(args.action);
15965
+ return { ok: true };
15966
+ }
15967
+ },
15968
+ dispose: () => session.dispose()
15969
+ };
15970
+ }
15971
+ /**
15972
+ * processName → pid:唯一命中直用;0 命中或多命中返回错误文案(客户端引导用 processes 列表选择)。
15973
+ * @author ddj 2026年09月29号
15974
+ * @param processName 进程名(标题/文件名包含匹配)
15975
+ */
15976
+ async function resolvePid(processName) {
15977
+ const items = filterProcesses(await listProcesses(), processName);
15978
+ if (items.length === 1) return items[0].pid;
15979
+ if (items.length === 0) return "未找到匹配进程:" + (processName || "(空)");
15980
+ return "匹配到 " + items.length + " 个进程,请先在进程列表中选择(edrv.dap.processes)";
15981
+ }
15982
+ /**
15983
+ * 读工作区调试配置:优先 `.dsh/launch.json`(插件专属);缺失时从旧共用
15984
+ * `.vscode/launch.json` 一次性迁移(只读 legacy 源、永不回写;复制失败当次
15985
+ * 仍用 legacy 内容响应不丢配置,下次读重试);都缺失 → 空表。
15986
+ * @author ddj 2026年09月22号
15987
+ * @param ctx DSH 上下文
15988
+ * @param workspacePath 工作区绝对路径
15989
+ * @returns 解析后的配置列表
15990
+ */
15991
+ async function readLaunchConfigs(ctx, workspacePath) {
15992
+ const fs = ctx.get("fs");
15993
+ if (!fs || !workspacePath) return [];
15994
+ try {
15995
+ const target = await fs.resolve(DAP_LAUNCH_REL, { cwd: workspacePath });
15996
+ let text = "";
15997
+ let exists = true;
15998
+ try {
15999
+ text = String(await fs.readText(target) ?? "");
16000
+ } catch {
16001
+ exists = false;
16002
+ }
16003
+ if (exists) return parseLaunchConfigs(text, workspacePath);
16004
+ let legacy = "";
16005
+ let legacyExists = true;
16006
+ try {
16007
+ const source = await fs.resolve(DAP_LEGACY_LAUNCH_REL, { cwd: workspacePath });
16008
+ legacy = String(await fs.readText(source) ?? "");
16009
+ } catch {
16010
+ legacyExists = false;
16011
+ }
16012
+ if (!legacyExists) return [];
16013
+ try {
16014
+ const realPath = fs.processPath(target);
16015
+ await mkdir(join(realPath, ".."), { recursive: true });
16016
+ await writeFile(realPath, legacy, "utf8");
16017
+ log.debug("[dap] 已迁移 .vscode/launch.json → .dsh/launch.json(" + workspacePath + ")");
16018
+ } catch (error) {
16019
+ log.debug("[dap] launch.json 迁移失败(下次读重试,本次以旧源响应):" + String(error));
16020
+ }
16021
+ return parseLaunchConfigs(legacy, workspacePath);
16022
+ } catch (error) {
16023
+ log.debug("[dap] launch.json 读取失败(视为无配置):" + String(error));
16024
+ return [];
16025
+ }
16026
+ }
16027
+ /**
16028
+ * 未知调试类型的错误文案:列出已识别类型(含不可用原因),引导用户安装/排查。
16029
+ * @author ddj 2026年09月21号
16030
+ * @param type launch.json 中的调试类型
16031
+ */
16032
+ function unknownTypeMessage(type) {
16033
+ const known = debuggerDecls().map((d) => d.type + (d.available ? "" : "(" + (d.reason ?? "不可用") + ")")).join(" / ");
16034
+ return "未发现调试类型「" + type + "」的适配器(已识别:" + (known || "无,请安装带调试器的扩展") + ")";
13808
16035
  }
13809
16036
  //#endregion
13810
16037
  //#region src/index.ts
@@ -13866,6 +16093,9 @@ function apply(ctx, config) {
13866
16093
  ctx,
13867
16094
  settings: openSettings
13868
16095
  }).handlers;
16096
+ /** 调试 RPC(DAP 桥单例:spawn 扩展适配器 + 事件缓冲 + findFile 反向匹配)。 */
16097
+ const dapRpc = createDapRpc(ctx);
16098
+ const dapHandlers = dapRpc.handlers;
13869
16099
  /** 文件磁盘新鲜度观察器(客户端轮询 edrv.versions;变化时顺带失效目录树缓存)。 */
13870
16100
  const fileVersions = createFileVersions(ctx);
13871
16101
  ctx.on("tools/result", (exec, result) => {
@@ -13875,7 +16105,6 @@ function apply(ctx, config) {
13875
16105
  const cwd = cwdOf(session);
13876
16106
  if (cwd) {
13877
16107
  registry.delete(cwd);
13878
- dropFileIndex(cwd);
13879
16108
  searcher.dispose(cwd);
13880
16109
  contentSearcher.dispose(cwd);
13881
16110
  disposeIndex(cwd);
@@ -13883,7 +16112,7 @@ function apply(ctx, config) {
13883
16112
  const sid = session?.id;
13884
16113
  if (typeof sid === "string") lspRpc.disposeSession(sid);
13885
16114
  });
13886
- registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher, lspHandlers, aiHandlers, fileVersions, svnHandlers), (warning) => warnings.push(warning));
16115
+ registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher, lspHandlers, aiHandlers, fileVersions, svnHandlers, dapHandlers), (warning) => warnings.push(warning));
13887
16116
  installIsolation(ctx);
13888
16117
  ctx.effect(() => shellMenuLifecycle(ctx));
13889
16118
  sweepTreeCache();
@@ -13893,6 +16122,7 @@ function apply(ctx, config) {
13893
16122
  disposeAllServers();
13894
16123
  });
13895
16124
  ctx.effect(() => () => fileVersions.dispose());
16125
+ ctx.effect(() => () => dapRpc.dispose());
13896
16126
  hookExitReclaim();
13897
16127
  log.info("编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用,语言服务器 LSP 已接入,规则注入" + (rulesInstalled ? "已接入" : "未接入") + ",技能组" + (skillsDispatched ? "装配中" : "未装配") + ")");
13898
16128
  }