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.
- package/README.md +55 -2
- package/lib/client.js +5173 -355
- package/lib/client.js.map +1 -1
- package/lib/index.js +2356 -126
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/dap/BpWidget.ts +262 -0
- package/src/client/dap/bpMenu.ts +91 -0
- package/src/client/dap/bpRowMenu.ts +53 -0
- package/src/client/dap/breakpoints.ts +222 -0
- package/src/client/dap/decorate.ts +128 -0
- package/src/client/dap/hintLine.ts +29 -0
- package/src/client/dap/hover.ts +166 -0
- package/src/client/dap/hoverMode.ts +159 -0
- package/src/client/dap/hoverTree.ts +722 -0
- package/src/client/dap/launchInsert.ts +229 -0
- package/src/client/dap/launchSnippetProvider.ts +206 -0
- package/src/client/dap/modelPath.ts +23 -0
- package/src/client/dap/panelSplit.ts +105 -0
- package/src/client/dap/pidPick.ts +24 -0
- package/src/client/dap/store.ts +571 -0
- package/src/client/dap/toolbarDrag.ts +51 -0
- package/src/client/dap/trace.ts +22 -0
- package/src/client/dap/variableTree.ts +62 -0
- package/src/client/index.ts +9 -0
- package/src/client/monaco/lsp/index.ts +15 -0
- package/src/client/monaco/lsp/providers.ts +2 -0
- package/src/client/monaco/theme.ts +15 -0
- package/src/client/sidebar/panels/DebugPanel.ts +613 -0
- package/src/client/sidebar/panels/SvnPanel.ts +167 -23
- package/src/client/sidebar/panels/index.ts +19 -0
- package/src/client/sidebar/types.ts +2 -0
- package/src/client/styles/editor.css +176 -0
- package/src/client/svnActions.ts +18 -1
- package/src/client/svnLog.ts +3 -3
- package/src/client/svnStatus.ts +140 -2
- package/src/client/ui/EditorView.ts +544 -16
- package/src/client/ui/SideBySideDiff.tsx +214 -29
- package/src/client/ui/SvnDiffPanel.ts +21 -8
- package/src/client/ui/SvnLogDialog.ts +44 -2
- package/src/client/ui/SvnPatchDialog.ts +63 -0
- package/src/client/ui/SvnSumDialog.ts +77 -0
- package/src/client/ui/commandCatalog.ts +49 -0
- package/src/client/ui/svnExport.ts +71 -0
- package/src/dap/configSnippets.ts +269 -0
- package/src/dap/discovery.ts +198 -0
- package/src/dap/launchConfig.ts +127 -0
- package/src/dap/manager.ts +588 -0
- package/src/dap/processList.ts +76 -0
- package/src/dap/protocol.ts +80 -0
- package/src/dap/provider.ts +49 -0
- package/src/dap/rpc.ts +192 -0
- package/src/dap/sourcePath.ts +82 -0
- package/src/index.ts +7 -3
- package/src/lsp/providers.ts +3 -2
- package/src/reveal.ts +31 -20
- package/src/rpc.ts +11 -3
- package/src/search/ripgrep.ts +127 -11
- package/src/shared/dap.ts +262 -0
- package/src/shared/keybindings.ts +7 -0
- package/src/shared/rpc.ts +40 -2
- package/src/shared/svn.ts +147 -0
- package/src/shared/svnActions.ts +20 -0
- package/src/svn.ts +320 -11
- 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
|
-
/**
|
|
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
|
-
*
|
|
3379
|
-
*
|
|
3380
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
3500
|
-
*
|
|
3501
|
-
*
|
|
3502
|
-
* @
|
|
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
|
-
|
|
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(
|
|
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 平台分发 +
|
|
6532
|
-
* (argv 数组、无 shell 插值,沿 workspace/revert 的 subprocess 契约)。
|
|
6677
|
+
* 纯函数 revealCommand 平台分发 + revealSpawnOpts 启动选项 + revealInExplorer 发射。
|
|
6533
6678
|
* 文件 → 资源管理器选中定位;目录 → 打开目录;Linux 无通用定位协议 → 打开所在目录。
|
|
6534
|
-
*
|
|
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
|
-
*
|
|
6561
|
-
* Explorer
|
|
6562
|
-
*
|
|
6563
|
-
* @
|
|
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(
|
|
6569
|
-
const
|
|
6570
|
-
if (!
|
|
6734
|
+
async function revealInExplorer(absPath, isDir) {
|
|
6735
|
+
const [program, ...args] = revealCommand(absPath, isDir).argv;
|
|
6736
|
+
if (!program) return {
|
|
6571
6737
|
ok: false,
|
|
6572
|
-
error: "
|
|
6738
|
+
error: "打开失败:opener 命令为空"
|
|
6573
6739
|
};
|
|
6574
|
-
const { argv } = revealCommand(absPath, isDir);
|
|
6575
6740
|
try {
|
|
6576
|
-
await
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
})
|
|
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(
|
|
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(
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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.
|
|
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:
|
|
13657
|
-
|
|
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
|
-
|
|
13669
|
-
|
|
13670
|
-
|
|
13671
|
-
|
|
13672
|
-
|
|
13673
|
-
|
|
13674
|
-
|
|
13675
|
-
|
|
13676
|
-
|
|
13677
|
-
|
|
13678
|
-
|
|
13679
|
-
|
|
13680
|
-
|
|
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.
|
|
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
|
|
13703
|
-
|
|
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: "
|
|
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
|
-
...
|
|
14099
|
+
...parseSummarizeXml(outcome.stdout, status.wcRoot)
|
|
13712
14100
|
};
|
|
13713
14101
|
} catch (error) {
|
|
13714
14102
|
return {
|
|
13715
14103
|
ok: false,
|
|
13716
|
-
error: "
|
|
14104
|
+
error: "目录对比失败:" + String(error)
|
|
13717
14105
|
};
|
|
13718
14106
|
}
|
|
13719
14107
|
},
|
|
13720
|
-
"svn.
|
|
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
|
|
13742
|
-
|
|
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: "
|
|
14138
|
+
error: "远端检查失败:" + tailOfText([outcome.stderr, outcome.stdout].filter(Boolean).join("\n"))
|
|
13746
14139
|
};
|
|
13747
14140
|
return {
|
|
13748
14141
|
ok: true,
|
|
13749
|
-
...
|
|
14142
|
+
...parseRemoteStatusXml(outcome.stdout, status.wcRoot)
|
|
13750
14143
|
};
|
|
13751
14144
|
} catch (error) {
|
|
13752
14145
|
return {
|
|
13753
14146
|
ok: false,
|
|
13754
|
-
error: "
|
|
14147
|
+
error: "远端检查失败:" + String(error)
|
|
13755
14148
|
};
|
|
13756
14149
|
}
|
|
13757
14150
|
},
|
|
13758
|
-
"svn.
|
|
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
|
-
|
|
14170
|
+
artifacts: await conflictArtifactsOf(status.wcRoot, rel)
|
|
13799
14171
|
};
|
|
13800
14172
|
} catch (error) {
|
|
13801
14173
|
return {
|
|
13802
14174
|
ok: false,
|
|
13803
|
-
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
|
}
|