dsh-vscode-mode 0.14.6 → 0.14.8
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 +10 -7
- package/lib/client.js +818 -123
- package/lib/client.js.map +1 -1
- package/lib/index.js +1002 -19
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/capture.ts +4 -1
- package/src/client/events.ts +11 -5
- package/src/client/openFlow.ts +33 -3
- package/src/client/rpc.ts +10 -0
- package/src/client/sidebar/menuItems.ts +7 -6
- package/src/client/sidebar/panels/FileExplorer.ts +89 -72
- package/src/client/sidebar/panels/explorerView.ts +165 -0
- package/src/client/sidebar/panels/useExplorerSvn.ts +141 -0
- package/src/client/styles/editor.css +12 -0
- package/src/client/svnNotice.ts +24 -0
- package/src/client/ui/EditorView.ts +11 -10
- package/src/client/ui/SvnOverlay.ts +24 -0
- package/src/dap/manager.ts +65 -17
- package/src/index.ts +9 -1
- package/src/model.ts +112 -1
- package/src/openBridge.ts +13 -0
- package/src/rpc.ts +3 -1
- package/src/shared/rpc.ts +6 -1
- package/src/shared/svn.ts +55 -0
- package/src/shared/svnExplorer.ts +97 -0
- package/src/store.ts +36 -1
- package/src/svn.ts +108 -6
- package/src/svnExplorer.ts +56 -0
- package/src/svnExplorerCache.ts +304 -0
- package/src/svnStatusStream.ts +135 -0
- package/unity/com.dsh.editor/Editor/DshCodeEditor.cs +58 -4
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs +13 -2
- package/unity/com.dsh.editor/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { Buffer as Buffer$1 } from "node:buffer";
|
|
|
10
10
|
import { execFile, spawn, spawnSync } from "node:child_process";
|
|
11
11
|
import z from "@deepseek-ai/schemastery";
|
|
12
12
|
import { inflateRawSync } from "node:zlib";
|
|
13
|
+
import { StringDecoder } from "node:string_decoder";
|
|
13
14
|
//#region src/hostImport.ts
|
|
14
15
|
/** Host-anchored optional package imports. @author ddj 2026年09月28号 */
|
|
15
16
|
/**
|
|
@@ -1371,6 +1372,21 @@ function tortoiseLaunchArgv(exe, action, absPath) {
|
|
|
1371
1372
|
"/path:" + absPath
|
|
1372
1373
|
];
|
|
1373
1374
|
}
|
|
1375
|
+
/** 条目状态 → 中文说明(徽标 tooltip / 面板行 tooltip)。 */
|
|
1376
|
+
const SVN_STATUS_LABEL = {
|
|
1377
|
+
normal: "未修改",
|
|
1378
|
+
added: "已添加(待提交)",
|
|
1379
|
+
deleted: "已删除(待提交)",
|
|
1380
|
+
replaced: "已替换",
|
|
1381
|
+
conflicted: "冲突",
|
|
1382
|
+
missing: "缺失(已被外部删除)",
|
|
1383
|
+
unversioned: "未纳入版本控制",
|
|
1384
|
+
obstructed: "类型冲突(被同名的其他类型占用)",
|
|
1385
|
+
ignored: "已被忽略",
|
|
1386
|
+
modified: "已修改",
|
|
1387
|
+
external: "外部定义",
|
|
1388
|
+
incomplete: "不完整(需 cleanup)"
|
|
1389
|
+
};
|
|
1374
1390
|
/**
|
|
1375
1391
|
* 是否属于「工作副本变更」(`normal`/`external` 不算;`ignored` 由开关决定是否收录)。
|
|
1376
1392
|
* @author ddj 2026年09月16号
|
|
@@ -1382,6 +1398,21 @@ function isSvnChange(entry) {
|
|
|
1382
1398
|
if (entry.status === "ignored") return false;
|
|
1383
1399
|
return true;
|
|
1384
1400
|
}
|
|
1401
|
+
/**
|
|
1402
|
+
* 文件夹聚合决胜优先级(强→弱):冲突/障碍/缺失等 danger 态先于常规修改,
|
|
1403
|
+
* 未版本控制最弱;未列出的状态(normal/external/ignored)不参与聚合。
|
|
1404
|
+
*/
|
|
1405
|
+
const SVN_FOLDER_STATUS_RANK = {
|
|
1406
|
+
conflicted: 90,
|
|
1407
|
+
obstructed: 80,
|
|
1408
|
+
missing: 70,
|
|
1409
|
+
incomplete: 60,
|
|
1410
|
+
deleted: 50,
|
|
1411
|
+
replaced: 40,
|
|
1412
|
+
modified: 30,
|
|
1413
|
+
added: 20,
|
|
1414
|
+
unversioned: 10
|
|
1415
|
+
};
|
|
1385
1416
|
/** W2-6 内容护栏嗅探窗(字符数;NUL/U+FFFD 只看前 8K,全量扫描大文本得不偿失)。 */
|
|
1386
1417
|
const GUARD_SNIFF_CHARS = 8192;
|
|
1387
1418
|
/**
|
|
@@ -4190,6 +4221,103 @@ function reconstructOriginal(records, content) {
|
|
|
4190
4221
|
}))
|
|
4191
4222
|
};
|
|
4192
4223
|
}
|
|
4224
|
+
/**
|
|
4225
|
+
* @public
|
|
4226
|
+
* @author ddj 2026年10月10号
|
|
4227
|
+
* 判断记录是否完全未决策;缺少逐块状态时沿用 pending 的 call 状态。
|
|
4228
|
+
* @param record 审查记录
|
|
4229
|
+
* @returns 未归档、未覆盖且没有用户决策时为 true
|
|
4230
|
+
*/
|
|
4231
|
+
function fullyPending(record) {
|
|
4232
|
+
return !record.archived && !record.superseded && record.decisions.call === "pending" && record.decisions.perHunk.every((value) => value === "pending");
|
|
4233
|
+
}
|
|
4234
|
+
/**
|
|
4235
|
+
* @public
|
|
4236
|
+
* @author ddj 2026年10月10号
|
|
4237
|
+
* 比较整组 hunk 是否逐块精确逆反;不把新建或纯 no-op 当作抵消。
|
|
4238
|
+
* @param prior 先前修改的差异块
|
|
4239
|
+
* @param later 后续修改的差异块
|
|
4240
|
+
* @returns 文本归一化后整组逆反且包含真实修改时为 true
|
|
4241
|
+
*/
|
|
4242
|
+
function hunksInverse(prior, later) {
|
|
4243
|
+
if (!prior.length || prior.length !== later.length) return false;
|
|
4244
|
+
let changed = false;
|
|
4245
|
+
for (let idx = 0; idx < prior.length; idx++) {
|
|
4246
|
+
const a = normalizeHunk(prior[idx]);
|
|
4247
|
+
const b = normalizeHunk(later[idx]);
|
|
4248
|
+
if (a.oldText !== b.newText || a.newText !== b.oldText) return false;
|
|
4249
|
+
if (!isNoopHunk(a)) changed = true;
|
|
4250
|
+
}
|
|
4251
|
+
return changed;
|
|
4252
|
+
}
|
|
4253
|
+
/**
|
|
4254
|
+
* @private
|
|
4255
|
+
* @author ddj 2026年10月10号
|
|
4256
|
+
* 同路径优先按捕获批次排序;旧记录仅使用有效时间,不拿 callId 伪造先后证据。
|
|
4257
|
+
* @param a 第一条记录
|
|
4258
|
+
* @param b 第二条记录
|
|
4259
|
+
* @returns 负数表示 a 更早,零表示先后无法区分
|
|
4260
|
+
*/
|
|
4261
|
+
function recordOrder(a, b) {
|
|
4262
|
+
if (a.path !== b.path) return a.path.localeCompare(b.path);
|
|
4263
|
+
if (Number.isInteger(a.batch) && Number.isInteger(b.batch) && a.batch > 0 && b.batch > 0 && a.batch !== b.batch) return a.batch - b.batch;
|
|
4264
|
+
const delta = Date.parse(a.at) - Date.parse(b.at);
|
|
4265
|
+
return Number.isFinite(delta) ? delta : 0;
|
|
4266
|
+
}
|
|
4267
|
+
/**
|
|
4268
|
+
* @private
|
|
4269
|
+
* @author ddj 2026年10月10号
|
|
4270
|
+
* 确认完整文件快照往返,防止相同文本在不同位置的反向修改被误隐藏。
|
|
4271
|
+
* @param prior 先前记录
|
|
4272
|
+
* @param later 后续记录
|
|
4273
|
+
* @returns 两端快照互换且文件存在性未变化时为 true;缺少证据时保留审查
|
|
4274
|
+
*/
|
|
4275
|
+
function snapshotsRestore(prior, later) {
|
|
4276
|
+
if (prior.create || later.create) return false;
|
|
4277
|
+
if (typeof prior.before !== "string" || typeof prior.after !== "string" || typeof later.before !== "string" || typeof later.after !== "string") return false;
|
|
4278
|
+
return normalizeForCompare(prior.before) === normalizeForCompare(later.after) && normalizeForCompare(prior.after) === normalizeForCompare(later.before);
|
|
4279
|
+
}
|
|
4280
|
+
/**
|
|
4281
|
+
* @public
|
|
4282
|
+
* @author ddj 2026年10月10号
|
|
4283
|
+
* 查找最近一条未决的整记录逆反;必须有文件快照证明原内容确实恢复。
|
|
4284
|
+
* @param records 工作区记录(不修改输入)
|
|
4285
|
+
* @param record 新记录
|
|
4286
|
+
* @returns 可安全配对的先前记录,证据不足或部分撤销时为 null
|
|
4287
|
+
*/
|
|
4288
|
+
function findInversePrior(records, record) {
|
|
4289
|
+
if (!fullyPending(record)) return null;
|
|
4290
|
+
let found = null;
|
|
4291
|
+
for (const prior of records) {
|
|
4292
|
+
if (prior.callId === record.callId || prior.path !== record.path || !fullyPending(prior)) continue;
|
|
4293
|
+
if (recordOrder(prior, record) >= 0 || !hunksInverse(prior.hunks, record.hunks)) continue;
|
|
4294
|
+
if (!snapshotsRestore(prior, record)) continue;
|
|
4295
|
+
if (!found || recordOrder(found, prior) < 0) found = prior;
|
|
4296
|
+
}
|
|
4297
|
+
return found;
|
|
4298
|
+
}
|
|
4299
|
+
/**
|
|
4300
|
+
* @public
|
|
4301
|
+
* @author ddj 2026年10月10号
|
|
4302
|
+
* 按捕获顺序贪心配对历史抵消记录,每条最多使用一次;桶上限为 200。
|
|
4303
|
+
* @param records 工作区记录(不修改输入或记录状态)
|
|
4304
|
+
* @returns 可以整对归档的记录对
|
|
4305
|
+
*/
|
|
4306
|
+
function findCanceledPairs(records) {
|
|
4307
|
+
const available = [];
|
|
4308
|
+
const pairs = [];
|
|
4309
|
+
const ordered = [...records].sort(recordOrder);
|
|
4310
|
+
for (const record of ordered) {
|
|
4311
|
+
const prior = findInversePrior(available, record);
|
|
4312
|
+
if (!prior) {
|
|
4313
|
+
if (fullyPending(record)) available.push(record);
|
|
4314
|
+
continue;
|
|
4315
|
+
}
|
|
4316
|
+
pairs.push([prior, record]);
|
|
4317
|
+
available.splice(available.indexOf(prior), 1);
|
|
4318
|
+
}
|
|
4319
|
+
return pairs;
|
|
4320
|
+
}
|
|
4193
4321
|
/** 文件当前最大批次号(无记录为 0)。 */
|
|
4194
4322
|
function fileMaxBatch(records, path) {
|
|
4195
4323
|
let m = 0;
|
|
@@ -5076,6 +5204,44 @@ async function autoArchiveStale(ctx, session, cwd, bucket) {
|
|
|
5076
5204
|
return stale.length;
|
|
5077
5205
|
}
|
|
5078
5206
|
/**
|
|
5207
|
+
* @public
|
|
5208
|
+
* @author ddj 2026年10月10号
|
|
5209
|
+
* 归档整记录精确抵消对;捕获时仅检查新记录,列表维护时检查整个桶。
|
|
5210
|
+
* @param ctx DSH 上下文
|
|
5211
|
+
* @param session 当前会话
|
|
5212
|
+
* @param cwd 工作区
|
|
5213
|
+
* @param bucket 活跃记录桶
|
|
5214
|
+
* @param record 可选的新捕获记录
|
|
5215
|
+
* @returns 本次归档记录数(不是配对数)
|
|
5216
|
+
* @throws 归档失败时恢复内存标志并透传错误,已保存的捕获记录不受影响
|
|
5217
|
+
*/
|
|
5218
|
+
async function archiveCanceled(ctx, session, cwd, bucket, record) {
|
|
5219
|
+
const records = [...bucket.values()];
|
|
5220
|
+
const prior = record ? findInversePrior(records, record) : null;
|
|
5221
|
+
const canceled = (record ? prior ? [[prior, record]] : [] : findCanceledPairs(records)).flat();
|
|
5222
|
+
if (!canceled.length) return 0;
|
|
5223
|
+
const original = canceled.map((rec) => ({
|
|
5224
|
+
rec,
|
|
5225
|
+
at: rec.at,
|
|
5226
|
+
superseded: rec.superseded
|
|
5227
|
+
}));
|
|
5228
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
5229
|
+
for (const rec of canceled) {
|
|
5230
|
+
rec.superseded = true;
|
|
5231
|
+
rec.at = at;
|
|
5232
|
+
}
|
|
5233
|
+
try {
|
|
5234
|
+
await archiveRecords(ctx, session, cwd, bucket, canceled, "编辑过程自抵消(新增后还原),自动跳过审查");
|
|
5235
|
+
} catch (error) {
|
|
5236
|
+
for (const item of original) {
|
|
5237
|
+
item.rec.superseded = item.superseded;
|
|
5238
|
+
item.rec.at = item.at;
|
|
5239
|
+
}
|
|
5240
|
+
throw error;
|
|
5241
|
+
}
|
|
5242
|
+
return canceled.length;
|
|
5243
|
+
}
|
|
5244
|
+
/**
|
|
5079
5245
|
* 追加归档条目(按 cwd+path+batch 合并,同 callId 去重)。
|
|
5080
5246
|
* @author ddj 2026年09月30号
|
|
5081
5247
|
* @param ctx DSH 上下文
|
|
@@ -6334,6 +6500,11 @@ async function persistRecord(ctx, registry, session, cwd, record) {
|
|
|
6334
6500
|
for (const [callId, rec] of prior) bucket.set(callId, rec);
|
|
6335
6501
|
throw error;
|
|
6336
6502
|
}
|
|
6503
|
+
try {
|
|
6504
|
+
await archiveCanceled(ctx, session, cwd, bucket, record);
|
|
6505
|
+
} catch (error) {
|
|
6506
|
+
log.warn("archive canceled edits failed: " + String(error));
|
|
6507
|
+
}
|
|
6337
6508
|
}
|
|
6338
6509
|
/**
|
|
6339
6510
|
* 捕获一次工具执行结果(tools/result):edit/write/remote_ssh_write 成功时
|
|
@@ -9135,6 +9306,18 @@ async function ensureOpenBridge(ctx, baseUrl) {
|
|
|
9135
9306
|
await writeOpenConfig(ctx, dir, baseUrl, inbox);
|
|
9136
9307
|
return join(dir, "dsh-open.ini");
|
|
9137
9308
|
}
|
|
9309
|
+
/** @public @author ddj 2026年10月12号
|
|
9310
|
+
* Provision the active profile's independent bridge at startup (desktop or web).
|
|
9311
|
+
* Mirrors unityInstall's per-profile bridge without requiring a registered project,
|
|
9312
|
+
* so Unity can select either target. Unknown profile returns null instead of failing.
|
|
9313
|
+
* @param ctx Active host context.
|
|
9314
|
+
* @param baseUrl Selected public Web URL (unused for desktop wake).
|
|
9315
|
+
* @returns Absolute profile bridge INI path, or null when no owned profile is active.
|
|
9316
|
+
*/
|
|
9317
|
+
async function ensureActiveBridge(ctx, baseUrl) {
|
|
9318
|
+
if (!findProfileDir(ctx)) return null;
|
|
9319
|
+
return ensureOpenBridge(ctx, baseUrl);
|
|
9320
|
+
}
|
|
9138
9321
|
/** @private @author ddj 2026年09月28号
|
|
9139
9322
|
* Replace a bridge file atomically without following a preexisting file link.
|
|
9140
9323
|
* @param path Destination in the validated bridge directory.
|
|
@@ -11409,6 +11592,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
11409
11592
|
staleChecked = true;
|
|
11410
11593
|
const staleStart = performance.now();
|
|
11411
11594
|
await autoArchiveStale(ctx, sc.session, sc.cwd, bucket);
|
|
11595
|
+
await archiveCanceled(ctx, sc.session, sc.cwd, bucket);
|
|
11412
11596
|
staleMs = performance.now() - staleStart;
|
|
11413
11597
|
}
|
|
11414
11598
|
}
|
|
@@ -18407,6 +18591,592 @@ var SvnUpdateJobs = class {
|
|
|
18407
18591
|
};
|
|
18408
18592
|
}
|
|
18409
18593
|
};
|
|
18594
|
+
//#endregion
|
|
18595
|
+
//#region src/shared/svnExplorer.ts
|
|
18596
|
+
const ICON_KIND = {
|
|
18597
|
+
normal: "clean",
|
|
18598
|
+
modified: "modified",
|
|
18599
|
+
replaced: "modified",
|
|
18600
|
+
added: "added",
|
|
18601
|
+
deleted: "deleted",
|
|
18602
|
+
missing: "deleted",
|
|
18603
|
+
incomplete: "conflict",
|
|
18604
|
+
obstructed: "conflict",
|
|
18605
|
+
conflicted: "conflict",
|
|
18606
|
+
unversioned: "unversioned",
|
|
18607
|
+
ignored: "ignored",
|
|
18608
|
+
external: "external"
|
|
18609
|
+
};
|
|
18610
|
+
/** @public @author ddj 2026年10月09号 @param entry Local status. @returns Effective content/property status. */
|
|
18611
|
+
function localStatusOf(entry) {
|
|
18612
|
+
if (entry.props === "conflicted") return "conflicted";
|
|
18613
|
+
if (entry.status === "normal" && entry.props === "modified") return "modified";
|
|
18614
|
+
return entry.status;
|
|
18615
|
+
}
|
|
18616
|
+
/** @private @author ddj 2026年10月09号 @param path Relative path. @returns Ancestors, excluding self/root. */
|
|
18617
|
+
function parentsOf(path) {
|
|
18618
|
+
const result = [];
|
|
18619
|
+
let dir = path;
|
|
18620
|
+
while (dir.includes("/")) {
|
|
18621
|
+
dir = dir.slice(0, dir.lastIndexOf("/"));
|
|
18622
|
+
result.push(dir);
|
|
18623
|
+
}
|
|
18624
|
+
return result;
|
|
18625
|
+
}
|
|
18626
|
+
/** @public @author ddj 2026年10月09号 @param index Confirmed snapshot. @param path Row path. @param isDir Directory row. @returns Overlay or null; absent entries never imply clean. */
|
|
18627
|
+
function explorerMarkOf(index, path, isDir) {
|
|
18628
|
+
if (!index) return null;
|
|
18629
|
+
let entry = index.entries.get(path);
|
|
18630
|
+
if (!entry) for (const parent of parentsOf(path)) {
|
|
18631
|
+
const ancestor = index.entries.get(parent);
|
|
18632
|
+
if (ancestor?.status === "ignored" || ancestor?.status === "unversioned") {
|
|
18633
|
+
entry = ancestor;
|
|
18634
|
+
break;
|
|
18635
|
+
}
|
|
18636
|
+
}
|
|
18637
|
+
let status = entry ? localStatusOf(entry) : null;
|
|
18638
|
+
const agg = isDir ? index.folders.get(path) : null;
|
|
18639
|
+
if (agg && (!status || (SVN_FOLDER_STATUS_RANK[agg.status] ?? 0) > (SVN_FOLDER_STATUS_RANK[status] ?? 0))) status = agg.status;
|
|
18640
|
+
if (!status) return null;
|
|
18641
|
+
if (status === "normal" && (entry?.versioned !== true || isDir && index.truncated)) return null;
|
|
18642
|
+
const title = status === "normal" ? "本地正常(已受控,未修改;不表示仓库无更新)" : SVN_STATUS_LABEL[status];
|
|
18643
|
+
return {
|
|
18644
|
+
kind: ICON_KIND[status],
|
|
18645
|
+
title: title + (agg ? ";子项 " + agg.count + " 处变更" : "")
|
|
18646
|
+
};
|
|
18647
|
+
}
|
|
18648
|
+
/** @public @author ddj 2026年10月09号 @param path WC-relative path. @param prefix Session path under WC. @returns Session-relative path or null outside session. */
|
|
18649
|
+
function explorerPathOf(path, prefix) {
|
|
18650
|
+
if (!prefix || prefix === ".") return path;
|
|
18651
|
+
return path.startsWith(prefix + "/") ? path.slice(prefix.length + 1) : null;
|
|
18652
|
+
}
|
|
18653
|
+
//#endregion
|
|
18654
|
+
//#region src/svnExplorer.ts
|
|
18655
|
+
/** Covers measured 125415-entry WC while bounding retained RPC objects, not XML bytes. */
|
|
18656
|
+
const EXPLORER_CAP = 25e4;
|
|
18657
|
+
/** @public @author ddj 2026年10月10号 @param raw Untrusted RPC request. @returns Whether bounded relative row/invalidation paths and tokens are legal. */
|
|
18658
|
+
function viewArgsValid(raw) {
|
|
18659
|
+
if (!raw || typeof raw !== "object") return false;
|
|
18660
|
+
const args = raw;
|
|
18661
|
+
if (!Array.isArray(args.rows) || args.rows.length > 1e3) return false;
|
|
18662
|
+
if (args.reason && ![
|
|
18663
|
+
"read",
|
|
18664
|
+
"paths",
|
|
18665
|
+
"full",
|
|
18666
|
+
"calibrate"
|
|
18667
|
+
].includes(args.reason)) return false;
|
|
18668
|
+
if (args.paths && (!Array.isArray(args.paths) || args.paths.length > 256 || !args.paths.every(viewPathValid))) return false;
|
|
18669
|
+
if (!args.rows.every((row) => row && typeof row.isDir === "boolean" && viewPathValid(row.path))) return false;
|
|
18670
|
+
if (args.rows.reduce((bytes, row) => bytes + Buffer.byteLength(JSON.stringify(row.path)) + 80, 0) > 245760) return false;
|
|
18671
|
+
if ([args.clientId, args.eventId].some((id) => id !== void 0 && (typeof id !== "string" || !id || id.length > 128))) return false;
|
|
18672
|
+
if (args.reason && args.reason !== "read" && (!args.clientId || !args.eventId)) return false;
|
|
18673
|
+
return [args.revision, args.generation].every((n) => n === void 0 || Number.isSafeInteger(n) && n >= 0);
|
|
18674
|
+
}
|
|
18675
|
+
/** @private @author ddj 2026年10月10号 @param path Untrusted relative path. @returns No absolute/traversal/control characters; max row size bounds JSON. */
|
|
18676
|
+
function viewPathValid(path) {
|
|
18677
|
+
if (typeof path !== "string" || path.length > 1024 || /[\x00-\x1f]/.test(path)) return false;
|
|
18678
|
+
const normalized = path.replace(/\\/g, "/");
|
|
18679
|
+
return !normalized.startsWith("/") && !/^[A-Za-z]:/.test(normalized) && !normalized.split("/").includes("..");
|
|
18680
|
+
}
|
|
18681
|
+
/** @public @author ddj 2026年10月10号 @param queue Scope-owned index. @param scopeId Canonical scope. @param prefix WC prefix. @param args Rows and consistency token. @returns Compact rows; page mismatch returns no mixed-version data. */
|
|
18682
|
+
function explorerViewOf(queue, scopeId, prefix, args) {
|
|
18683
|
+
const state = queue.state();
|
|
18684
|
+
const mismatch = args.revision !== void 0 && args.revision !== state.revision || args.generation !== void 0 && args.generation !== state.generation;
|
|
18685
|
+
const rows = mismatch ? [] : args.rows.map((row) => {
|
|
18686
|
+
const { mark, count } = queue.data.mark(row.path, row.isDir);
|
|
18687
|
+
return {
|
|
18688
|
+
path: row.path,
|
|
18689
|
+
kind: mark?.kind ?? null,
|
|
18690
|
+
count,
|
|
18691
|
+
stale: queue.stale(row.path, row.isDir)
|
|
18692
|
+
};
|
|
18693
|
+
});
|
|
18694
|
+
return {
|
|
18695
|
+
scopeId,
|
|
18696
|
+
prefix,
|
|
18697
|
+
...state,
|
|
18698
|
+
mismatch,
|
|
18699
|
+
rows
|
|
18700
|
+
};
|
|
18701
|
+
}
|
|
18702
|
+
/** @public @author ddj 2026年10月09号 @param xml Collected CLI output. @returns True only for a complete status document. */
|
|
18703
|
+
function statusXmlValid(xml) {
|
|
18704
|
+
return /^\s*(?:<\?xml[^>]*>\s*)?<status(?:\s[^>]*)?>/.test(xml) && /<\/status>\s*$/.test(xml);
|
|
18705
|
+
}
|
|
18706
|
+
/** @public @author ddj 2026年10月09号 @param entries WC-relative statuses. @param prefix Session prefix. @returns Session-relative subset. */
|
|
18707
|
+
function scopeEntriesOf(entries, prefix) {
|
|
18708
|
+
const scoped = [];
|
|
18709
|
+
for (const entry of entries) {
|
|
18710
|
+
const path = explorerPathOf(entry.path, prefix);
|
|
18711
|
+
if (path !== null) scoped.push({
|
|
18712
|
+
...entry,
|
|
18713
|
+
path
|
|
18714
|
+
});
|
|
18715
|
+
}
|
|
18716
|
+
return scoped;
|
|
18717
|
+
}
|
|
18718
|
+
//#endregion
|
|
18719
|
+
//#region src/svnExplorerCache.ts
|
|
18720
|
+
const QUIET_MS = 300;
|
|
18721
|
+
const MAX_WAIT_MS = 1e3;
|
|
18722
|
+
const EVENT_CAP$1 = 512;
|
|
18723
|
+
/** @public @author ddj 2026年10月10号 @param path Validated relative path. @param windows Case-insensitive platform. @returns Canonical lookup key, preserving row spelling separately. */
|
|
18724
|
+
function explorerKey(path, windows = process.platform === "win32") {
|
|
18725
|
+
const normalized = path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
18726
|
+
return windows ? normalized.toLowerCase() : normalized;
|
|
18727
|
+
}
|
|
18728
|
+
/** @private @author ddj 2026年10月10号 @param path Canonical descendant. @param root Canonical subtree. @returns Segment-bound membership including root. */
|
|
18729
|
+
function within(path, root) {
|
|
18730
|
+
return !root || path === root || path.startsWith(root + "/");
|
|
18731
|
+
}
|
|
18732
|
+
/** @private @author ddj 2026年10月10号 @param path Canonical entry. @returns Ancestors excluding virtual scope root. */
|
|
18733
|
+
function ancestors(path) {
|
|
18734
|
+
const parents = [];
|
|
18735
|
+
let parent = path;
|
|
18736
|
+
while (parent.includes("/")) {
|
|
18737
|
+
parent = parent.slice(0, parent.lastIndexOf("/"));
|
|
18738
|
+
parents.push(parent);
|
|
18739
|
+
}
|
|
18740
|
+
return parents;
|
|
18741
|
+
}
|
|
18742
|
+
/** @public @author ddj 2026年10月10号 @param paths Changed paths. @param windows Platform matching. @returns Minimal parent subtree set; empty root means full scan. */
|
|
18743
|
+
function scanRootsOf(paths, windows = process.platform === "win32") {
|
|
18744
|
+
const roots = [];
|
|
18745
|
+
for (const raw of paths) {
|
|
18746
|
+
const path = explorerKey(raw, windows);
|
|
18747
|
+
const parent = path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "";
|
|
18748
|
+
if (roots.some((root) => within(parent, root))) continue;
|
|
18749
|
+
for (let i = roots.length - 1; i >= 0; i--) if (within(roots[i], parent)) roots.splice(i, 1);
|
|
18750
|
+
roots.push(parent);
|
|
18751
|
+
}
|
|
18752
|
+
return roots;
|
|
18753
|
+
}
|
|
18754
|
+
/** @public @author ddj 2026年10月10号 Retains authoritative entries and decrementable ancestor counts; no client-wide payload. */
|
|
18755
|
+
var ExplorerIndex = class {
|
|
18756
|
+
windows;
|
|
18757
|
+
index = {
|
|
18758
|
+
entries: /* @__PURE__ */ new Map(),
|
|
18759
|
+
folders: /* @__PURE__ */ new Map(),
|
|
18760
|
+
truncated: false
|
|
18761
|
+
};
|
|
18762
|
+
counts = /* @__PURE__ */ new Map();
|
|
18763
|
+
children = /* @__PURE__ */ new Map();
|
|
18764
|
+
/** @public @author ddj 2026年10月10号 @param windows Case-insensitive lookup mode. */
|
|
18765
|
+
constructor(windows = process.platform === "win32") {
|
|
18766
|
+
this.windows = windows;
|
|
18767
|
+
}
|
|
18768
|
+
/** @private @author ddj 2026年10月10号 @param entry Entry contribution. @param delta Add or remove. Updates strongest status after downgrade. */
|
|
18769
|
+
contribute(entry, delta) {
|
|
18770
|
+
const status = localStatusOf(entry);
|
|
18771
|
+
if (status === "normal" || status === "ignored" || status === "external") return;
|
|
18772
|
+
for (const parent of ancestors(entry.path)) {
|
|
18773
|
+
const counts = this.counts.get(parent) ?? /* @__PURE__ */ new Map();
|
|
18774
|
+
const next = (counts.get(status) ?? 0) + delta;
|
|
18775
|
+
if (next > 0) counts.set(status, next);
|
|
18776
|
+
else counts.delete(status);
|
|
18777
|
+
if (!counts.size) {
|
|
18778
|
+
this.counts.delete(parent);
|
|
18779
|
+
this.index.folders.delete(parent);
|
|
18780
|
+
continue;
|
|
18781
|
+
}
|
|
18782
|
+
this.counts.set(parent, counts);
|
|
18783
|
+
let strongest = "normal";
|
|
18784
|
+
let count = 0;
|
|
18785
|
+
for (const [item, n] of counts) {
|
|
18786
|
+
count += n;
|
|
18787
|
+
if ((SVN_FOLDER_STATUS_RANK[item] ?? 0) > (SVN_FOLDER_STATUS_RANK[strongest] ?? 0)) strongest = item;
|
|
18788
|
+
}
|
|
18789
|
+
this.index.folders.set(parent, {
|
|
18790
|
+
status: strongest,
|
|
18791
|
+
count
|
|
18792
|
+
});
|
|
18793
|
+
}
|
|
18794
|
+
}
|
|
18795
|
+
/** @private @author ddj 2026年10月10号 @param path Canonical entry. Build direct child membership, including implicit ancestors. */
|
|
18796
|
+
link(path) {
|
|
18797
|
+
let child = path;
|
|
18798
|
+
while (child) {
|
|
18799
|
+
const parent = child.includes("/") ? child.slice(0, child.lastIndexOf("/")) : "";
|
|
18800
|
+
const siblings = this.children.get(parent) ?? /* @__PURE__ */ new Set();
|
|
18801
|
+
siblings.add(child);
|
|
18802
|
+
this.children.set(parent, siblings);
|
|
18803
|
+
child = parent;
|
|
18804
|
+
}
|
|
18805
|
+
}
|
|
18806
|
+
/** @private @author ddj 2026年10月10号 @param path Removed entry. Prune only empty implicit ancestors; retained siblings remain indexed. */
|
|
18807
|
+
unlink(path) {
|
|
18808
|
+
let child = path;
|
|
18809
|
+
while (child && !this.index.entries.has(child) && !this.children.get(child)?.size) {
|
|
18810
|
+
const parent = child.includes("/") ? child.slice(0, child.lastIndexOf("/")) : "";
|
|
18811
|
+
this.children.delete(child);
|
|
18812
|
+
const siblings = this.children.get(parent);
|
|
18813
|
+
siblings?.delete(child);
|
|
18814
|
+
if (!siblings?.size) this.children.delete(parent);
|
|
18815
|
+
child = parent;
|
|
18816
|
+
}
|
|
18817
|
+
}
|
|
18818
|
+
/** @private @author ddj 2026年10月10号 @param roots Subtree membership roots. @returns Retained affected entries only; no whole-index iteration for local replacement. */
|
|
18819
|
+
members(roots) {
|
|
18820
|
+
const result = /* @__PURE__ */ new Set(), pending = [...roots], visited = /* @__PURE__ */ new Set();
|
|
18821
|
+
while (pending.length) {
|
|
18822
|
+
const path = pending.pop();
|
|
18823
|
+
if (visited.has(path)) continue;
|
|
18824
|
+
visited.add(path);
|
|
18825
|
+
if (this.index.entries.has(path)) result.add(path);
|
|
18826
|
+
for (const child of this.children.get(path) ?? []) pending.push(child);
|
|
18827
|
+
}
|
|
18828
|
+
return [...result];
|
|
18829
|
+
}
|
|
18830
|
+
/** @public @author ddj 2026年10月10号 @param roots Replaced session-relative subtrees. @param result Complete or bounded scan. @throws Capacity mismatch on partial replacement; caller must full-calibrate. */
|
|
18831
|
+
replace(roots, result) {
|
|
18832
|
+
const keys = roots.map((root) => explorerKey(root, this.windows));
|
|
18833
|
+
const full = keys.includes("");
|
|
18834
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
18835
|
+
for (const entry of result.entries) {
|
|
18836
|
+
const path = explorerKey(entry.path, this.windows);
|
|
18837
|
+
if (keys.some((root) => within(path, root))) incoming.set(path, {
|
|
18838
|
+
...entry,
|
|
18839
|
+
path
|
|
18840
|
+
});
|
|
18841
|
+
}
|
|
18842
|
+
const removed = full ? [] : this.members(keys);
|
|
18843
|
+
if (!full && (result.truncated || this.index.entries.size - removed.length + incoming.size > 25e4)) throw new Error("局部 SVN 状态不完整,需要全量校准");
|
|
18844
|
+
if (full) this.clear();
|
|
18845
|
+
else for (const path of removed) {
|
|
18846
|
+
this.contribute(this.index.entries.get(path), -1);
|
|
18847
|
+
this.index.entries.delete(path);
|
|
18848
|
+
this.unlink(path);
|
|
18849
|
+
}
|
|
18850
|
+
for (const [path, entry] of incoming) {
|
|
18851
|
+
this.index.entries.set(path, entry);
|
|
18852
|
+
this.link(path);
|
|
18853
|
+
this.contribute(entry, 1);
|
|
18854
|
+
}
|
|
18855
|
+
if (full) this.index.truncated = result.truncated;
|
|
18856
|
+
}
|
|
18857
|
+
/** @public @author ddj 2026年10月10号 Release retained entries and aggregation counters. */
|
|
18858
|
+
clear() {
|
|
18859
|
+
this.index.entries.clear();
|
|
18860
|
+
this.index.folders.clear();
|
|
18861
|
+
this.counts.clear();
|
|
18862
|
+
this.children.clear();
|
|
18863
|
+
}
|
|
18864
|
+
/** @public @author ddj 2026年10月10号 @param path Display row spelling. @param isDir Directory row. @returns Exact/ancestor mark and dirty descendant count. */
|
|
18865
|
+
mark(path, isDir) {
|
|
18866
|
+
const key = explorerKey(path, this.windows);
|
|
18867
|
+
return {
|
|
18868
|
+
mark: explorerMarkOf(this.index, key, isDir),
|
|
18869
|
+
count: isDir ? this.index.folders.get(key)?.count ?? 0 : 0
|
|
18870
|
+
};
|
|
18871
|
+
}
|
|
18872
|
+
};
|
|
18873
|
+
/** @public @author ddj 2026年10月10号 Two retained scope indexes; active work is never evicted to satisfy a new reader. */
|
|
18874
|
+
var ExplorerScopes = class {
|
|
18875
|
+
scopes = /* @__PURE__ */ new Map();
|
|
18876
|
+
/** @public @author ddj 2026年10月10号 @param key Canonical root/session key. @param scan Scanner bound to this scope. @param now Clock. @param windows Matching. @returns Shared owner; refuses new scopes if all retained owners are busy. */
|
|
18877
|
+
get(key, scan, now = Date.now, windows = process.platform === "win32") {
|
|
18878
|
+
const existing = this.scopes.get(key);
|
|
18879
|
+
if (existing) {
|
|
18880
|
+
this.scopes.delete(key);
|
|
18881
|
+
this.scopes.set(key, existing);
|
|
18882
|
+
return existing;
|
|
18883
|
+
}
|
|
18884
|
+
if (this.scopes.size >= 2) {
|
|
18885
|
+
const idle = [...this.scopes].find(([, queue]) => queue.idle());
|
|
18886
|
+
if (!idle) throw new Error("SVN 状态扫描繁忙,请稍后重试");
|
|
18887
|
+
idle[1].dispose();
|
|
18888
|
+
this.scopes.delete(idle[0]);
|
|
18889
|
+
}
|
|
18890
|
+
const queue = new ExplorerQueue(scan, now, windows);
|
|
18891
|
+
this.scopes.set(key, queue);
|
|
18892
|
+
return queue;
|
|
18893
|
+
}
|
|
18894
|
+
/** @public @author ddj 2026年10月10号 Release all scoped work and retained state. */
|
|
18895
|
+
dispose() {
|
|
18896
|
+
for (const queue of this.scopes.values()) queue.dispose();
|
|
18897
|
+
this.scopes.clear();
|
|
18898
|
+
}
|
|
18899
|
+
};
|
|
18900
|
+
/** @public @author ddj 2026年10月10号 Coalesces generations without swallowing changes arriving during a scan. */
|
|
18901
|
+
var ExplorerQueue = class {
|
|
18902
|
+
scan;
|
|
18903
|
+
now;
|
|
18904
|
+
windows;
|
|
18905
|
+
data;
|
|
18906
|
+
generation = 0;
|
|
18907
|
+
observed = 0;
|
|
18908
|
+
revision = 0;
|
|
18909
|
+
confirmedAt = 0;
|
|
18910
|
+
scanMs = 0;
|
|
18911
|
+
error = "";
|
|
18912
|
+
fullDirty = false;
|
|
18913
|
+
dirty = /* @__PURE__ */ new Set();
|
|
18914
|
+
active = null;
|
|
18915
|
+
timer = null;
|
|
18916
|
+
pendingSince = null;
|
|
18917
|
+
lastEventAt = 0;
|
|
18918
|
+
controller = null;
|
|
18919
|
+
flight = null;
|
|
18920
|
+
stopped = false;
|
|
18921
|
+
events = /* @__PURE__ */ new Set();
|
|
18922
|
+
staleRoots = null;
|
|
18923
|
+
/** @public @author ddj 2026年10月10号 @param scan Managed scoped scan. @param now Injectable clock. @param windows Canonical path matching. */
|
|
18924
|
+
constructor(scan, now = Date.now, windows = process.platform === "win32") {
|
|
18925
|
+
this.scan = scan;
|
|
18926
|
+
this.now = now;
|
|
18927
|
+
this.windows = windows;
|
|
18928
|
+
this.data = new ExplorerIndex(windows);
|
|
18929
|
+
}
|
|
18930
|
+
/** @public @author ddj 2026年10月10号 @param change Path/full invalidation and idempotency token. @returns Whether a new change was registered. */
|
|
18931
|
+
invalidate(change = { full: true }) {
|
|
18932
|
+
if (this.stopped || change.event && this.events.has(change.event)) return false;
|
|
18933
|
+
if (change.event) {
|
|
18934
|
+
this.events.add(change.event);
|
|
18935
|
+
if (this.events.size > EVENT_CAP$1) this.events.delete(this.events.values().next().value);
|
|
18936
|
+
}
|
|
18937
|
+
this.generation++;
|
|
18938
|
+
this.staleRoots = null;
|
|
18939
|
+
this.lastEventAt = this.now();
|
|
18940
|
+
if (this.pendingSince === null) this.pendingSince = this.lastEventAt;
|
|
18941
|
+
if (change.full || !change.paths?.length || !this.confirmedAt || this.data.index.truncated) this.fullDirty = true;
|
|
18942
|
+
for (const path of change.paths ?? []) this.dirty.add(explorerKey(path, this.windows));
|
|
18943
|
+
if (this.dirty.size > 256 || scanRootsOf([...this.dirty], this.windows).length > 32) this.fullDirty = true;
|
|
18944
|
+
if (this.fullDirty) this.dirty.clear();
|
|
18945
|
+
this.schedule();
|
|
18946
|
+
return true;
|
|
18947
|
+
}
|
|
18948
|
+
/** @private @author ddj 2026年10月10号 Tail debounce has a hard max wait; active scans retain one merged follow-up batch. */
|
|
18949
|
+
schedule() {
|
|
18950
|
+
if (this.stopped || this.active || this.pendingSince === null) return;
|
|
18951
|
+
if (this.timer) clearTimeout(this.timer);
|
|
18952
|
+
const delay = Math.max(0, Math.min(this.lastEventAt + QUIET_MS, this.pendingSince + MAX_WAIT_MS) - this.now());
|
|
18953
|
+
this.timer = setTimeout(() => {
|
|
18954
|
+
this.timer = null;
|
|
18955
|
+
this.launch();
|
|
18956
|
+
}, delay);
|
|
18957
|
+
}
|
|
18958
|
+
/** @private @author ddj 2026年10月10号 Starts one scan, snapshots generation and dirtiness before yielding. */
|
|
18959
|
+
launch() {
|
|
18960
|
+
if (this.stopped || this.active || this.pendingSince === null) return;
|
|
18961
|
+
const roots = this.fullDirty ? [""] : scanRootsOf([...this.dirty], this.windows);
|
|
18962
|
+
const generation = this.generation;
|
|
18963
|
+
this.fullDirty = false;
|
|
18964
|
+
this.dirty.clear();
|
|
18965
|
+
this.pendingSince = null;
|
|
18966
|
+
this.active = roots;
|
|
18967
|
+
this.staleRoots = null;
|
|
18968
|
+
this.controller = new AbortController();
|
|
18969
|
+
this.flight = this.run(roots, generation, this.controller.signal);
|
|
18970
|
+
}
|
|
18971
|
+
/** @private @author ddj 2026年10月10号 @param roots Scan subtrees. @param generation Captured invalidation generation. @param signal Owner disposal. Applies known evidence but never confirms newer dirty paths. */
|
|
18972
|
+
async run(roots, generation, signal) {
|
|
18973
|
+
const started = this.now();
|
|
18974
|
+
try {
|
|
18975
|
+
const result = await this.scan(roots, signal);
|
|
18976
|
+
if (this.stopped) return;
|
|
18977
|
+
this.data.replace(result.roots ?? roots, result);
|
|
18978
|
+
this.observed = generation;
|
|
18979
|
+
this.revision++;
|
|
18980
|
+
this.error = "";
|
|
18981
|
+
if (generation === this.generation && !result.truncated) this.confirmedAt = this.now();
|
|
18982
|
+
} catch (failure) {
|
|
18983
|
+
if (this.stopped) return;
|
|
18984
|
+
this.error = String(failure);
|
|
18985
|
+
this.fullDirty = true;
|
|
18986
|
+
} finally {
|
|
18987
|
+
this.scanMs = Math.max(0, this.now() - started);
|
|
18988
|
+
this.active = null;
|
|
18989
|
+
this.staleRoots = null;
|
|
18990
|
+
this.controller = null;
|
|
18991
|
+
this.flight = null;
|
|
18992
|
+
this.schedule();
|
|
18993
|
+
}
|
|
18994
|
+
}
|
|
18995
|
+
/** @public @author ddj 2026年10月10号 @param path Display row. @param isDir Directory membership. @returns Whether evidence intersects active/queued changes or incomplete state. */
|
|
18996
|
+
stale(path, isDir) {
|
|
18997
|
+
if (!this.confirmedAt || this.fullDirty || this.data.index.truncated) return true;
|
|
18998
|
+
const key = explorerKey(path, this.windows);
|
|
18999
|
+
return (this.staleRoots ??= [...this.active ?? [], ...scanRootsOf([...this.dirty], this.windows)]).some((root) => within(key, root) || isDir && within(root, key));
|
|
19000
|
+
}
|
|
19001
|
+
/** @public @author ddj 2026年10月10号 @returns Lightweight scope metadata; freshness is independent of cache TTL. */
|
|
19002
|
+
state() {
|
|
19003
|
+
return {
|
|
19004
|
+
revision: this.revision,
|
|
19005
|
+
generation: this.generation,
|
|
19006
|
+
observedGeneration: this.observed,
|
|
19007
|
+
confirmedAt: this.confirmedAt,
|
|
19008
|
+
complete: this.confirmedAt > 0 && !this.data.index.truncated && !this.fullDirty && !this.active && !this.dirty.size,
|
|
19009
|
+
refreshing: this.active !== null || this.timer !== null,
|
|
19010
|
+
scanMs: this.scanMs,
|
|
19011
|
+
error: this.error
|
|
19012
|
+
};
|
|
19013
|
+
}
|
|
19014
|
+
/** @public @author ddj 2026年10月10号 @returns First-load confirmation; warm callers project immediately with stale flags. */
|
|
19015
|
+
async initial() {
|
|
19016
|
+
if (this.stopped) throw new Error("SVN 状态调度已释放");
|
|
19017
|
+
if (this.confirmedAt || this.revision || this.error) return;
|
|
19018
|
+
if (this.pendingSince === null && !this.active) this.invalidate({ full: true });
|
|
19019
|
+
if (this.timer) {
|
|
19020
|
+
clearTimeout(this.timer);
|
|
19021
|
+
this.timer = null;
|
|
19022
|
+
}
|
|
19023
|
+
this.launch();
|
|
19024
|
+
await this.flight;
|
|
19025
|
+
}
|
|
19026
|
+
/** @public @author ddj 2026年10月10号 @returns Whether an owner may evict this scope without killing shared work. */
|
|
19027
|
+
idle() {
|
|
19028
|
+
return !this.active && !this.timer;
|
|
19029
|
+
}
|
|
19030
|
+
/** @public @author ddj 2026年10月10号 Abort owned scans and timers; late completions cannot restore state. */
|
|
19031
|
+
dispose() {
|
|
19032
|
+
this.stopped = true;
|
|
19033
|
+
if (this.timer) clearTimeout(this.timer);
|
|
19034
|
+
this.timer = null;
|
|
19035
|
+
this.controller?.abort(/* @__PURE__ */ new Error("SVN 状态调度已释放"));
|
|
19036
|
+
this.dirty.clear();
|
|
19037
|
+
this.events.clear();
|
|
19038
|
+
this.data.clear();
|
|
19039
|
+
}
|
|
19040
|
+
};
|
|
19041
|
+
//#endregion
|
|
19042
|
+
//#region src/svnStatusStream.ts
|
|
19043
|
+
/** Bounded SVN XML framing; retain statuses, never the entire verbose document. @author ddj 2026年10月09号 */
|
|
19044
|
+
/** @public @author ddj 2026年10月09号 @param handle Managed pipe process. @param parse Status semantics. @param cap Entry bound. @param timeout Deadline. @returns EOF-validated statuses. @throws CLI, timeout or framing failure; never retries with inherit. */
|
|
19045
|
+
async function readStatusPipe(handle, parse, cap, timeout, signal) {
|
|
19046
|
+
const reader = new SvnStatusReader(parse, cap);
|
|
19047
|
+
/** @private @author ddj 2026年10月09号 Consume raw chunks through stream EOF, not just direct process exit. */
|
|
19048
|
+
async function consume() {
|
|
19049
|
+
if (!handle.stdout) throw new Error("SVN 状态管道不可用,未确认正常状态");
|
|
19050
|
+
for await (const chunk of handle.stdout) reader.push(chunk);
|
|
19051
|
+
}
|
|
19052
|
+
let timer;
|
|
19053
|
+
const expired = new Promise((_, reject) => {
|
|
19054
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("读取 SVN 图标状态超时")), timeout);
|
|
19055
|
+
timer.unref?.();
|
|
19056
|
+
});
|
|
19057
|
+
let abort;
|
|
19058
|
+
const cancelled = new Promise((_, reject) => {
|
|
19059
|
+
abort = () => reject(signal?.reason ?? /* @__PURE__ */ new Error("SVN 状态扫描已取消"));
|
|
19060
|
+
if (signal?.aborted) abort();
|
|
19061
|
+
else signal?.addEventListener("abort", abort, { once: true });
|
|
19062
|
+
});
|
|
19063
|
+
try {
|
|
19064
|
+
const [outcome] = await Promise.race([
|
|
19065
|
+
Promise.all([handle.done, consume()]),
|
|
19066
|
+
expired,
|
|
19067
|
+
cancelled
|
|
19068
|
+
]);
|
|
19069
|
+
if ((outcome.exitCode ?? outcome.code) !== 0) throw new Error(handle.collected?.stderr?.readFrom(0).text || "SVN 状态命令失败");
|
|
19070
|
+
return reader.finish();
|
|
19071
|
+
} catch (error) {
|
|
19072
|
+
try {
|
|
19073
|
+
await handle.terminate?.();
|
|
19074
|
+
} catch {}
|
|
19075
|
+
throw error;
|
|
19076
|
+
} finally {
|
|
19077
|
+
if (timer) clearTimeout(timer);
|
|
19078
|
+
if (abort) signal?.removeEventListener("abort", abort);
|
|
19079
|
+
}
|
|
19080
|
+
}
|
|
19081
|
+
const TAG_CAP = 65536;
|
|
19082
|
+
const STREAM_CAP = 536870912;
|
|
19083
|
+
/** @public @author ddj 2026年10月09号 Validates framing through EOF, including entries beyond the retained cap. */
|
|
19084
|
+
var SvnStatusReader = class {
|
|
19085
|
+
parse;
|
|
19086
|
+
cap;
|
|
19087
|
+
decoder = new StringDecoder("utf8");
|
|
19088
|
+
buffer = "";
|
|
19089
|
+
bytes = 0;
|
|
19090
|
+
stack = [];
|
|
19091
|
+
opened = false;
|
|
19092
|
+
closed = false;
|
|
19093
|
+
entry = "";
|
|
19094
|
+
changelist = "";
|
|
19095
|
+
entries = [];
|
|
19096
|
+
truncated = false;
|
|
19097
|
+
/** @public @author ddj 2026年10月09号 @param parse Existing status semantics. @param cap Retained entry bound. */
|
|
19098
|
+
constructor(parse, cap) {
|
|
19099
|
+
this.parse = parse;
|
|
19100
|
+
this.cap = cap;
|
|
19101
|
+
}
|
|
19102
|
+
/** @public @author ddj 2026年10月09号 @param chunk Raw pipe chunk. @throws Byte/tag limits or invalid framing. */
|
|
19103
|
+
push(chunk) {
|
|
19104
|
+
this.bytes += Buffer.byteLength(chunk);
|
|
19105
|
+
if (this.bytes > STREAM_CAP) throw new Error("SVN 状态流超过 512 MiB 安全上限");
|
|
19106
|
+
this.feed(typeof chunk === "string" ? chunk : this.decoder.write(chunk));
|
|
19107
|
+
}
|
|
19108
|
+
/** @private @author ddj 2026年10月09号 @param text Decoded chunk. Process each complete tag once; partial tags stay bounded. */
|
|
19109
|
+
feed(text) {
|
|
19110
|
+
this.buffer += text;
|
|
19111
|
+
let cursor = 0;
|
|
19112
|
+
while (cursor < this.buffer.length) {
|
|
19113
|
+
const start = this.buffer.indexOf("<", cursor);
|
|
19114
|
+
const end = start < 0 ? -1 : this.buffer.indexOf(">", start);
|
|
19115
|
+
const tail = start < 0 ? this.buffer.length : start;
|
|
19116
|
+
if (!this.stack.length && this.buffer.slice(cursor, tail).trim()) this.invalid();
|
|
19117
|
+
if (end < 0) {
|
|
19118
|
+
cursor = tail;
|
|
19119
|
+
break;
|
|
19120
|
+
}
|
|
19121
|
+
if (end - start > TAG_CAP) this.invalid();
|
|
19122
|
+
this.tag(this.buffer.slice(start, end + 1));
|
|
19123
|
+
cursor = end + 1;
|
|
19124
|
+
}
|
|
19125
|
+
this.buffer = this.buffer.slice(cursor);
|
|
19126
|
+
if (this.buffer.length > TAG_CAP) this.invalid();
|
|
19127
|
+
}
|
|
19128
|
+
/** @private @author ddj 2026年10月09号 @param token Complete XML tag. Validate nesting and capture only entry/wc-status attributes. */
|
|
19129
|
+
tag(token) {
|
|
19130
|
+
if (token.startsWith("<?xml ") && !this.opened && !this.stack.length) return;
|
|
19131
|
+
const match = /^<(\/?)([\w-]+)\b([^>]*?)(\/?)>$/.exec(token);
|
|
19132
|
+
if (!match || this.closed) this.invalid();
|
|
19133
|
+
const [, closing, name, attrs, self] = match;
|
|
19134
|
+
if (closing) {
|
|
19135
|
+
if (this.stack.pop() !== name) this.invalid();
|
|
19136
|
+
if (name === "entry") this.saveEntry();
|
|
19137
|
+
if (name === "changelist") this.changelist = "";
|
|
19138
|
+
if (name === "status") this.closed = true;
|
|
19139
|
+
return;
|
|
19140
|
+
}
|
|
19141
|
+
if (!this.stack.length) {
|
|
19142
|
+
if (name !== "status" || this.opened || self) this.invalid();
|
|
19143
|
+
this.opened = true;
|
|
19144
|
+
}
|
|
19145
|
+
if (name === "changelist") this.changelist = attrOf(attrs, "name");
|
|
19146
|
+
if (name === "entry") this.entry = token.replace(/\/>$/, ">");
|
|
19147
|
+
if (name === "wc-status" && this.entry) this.entry += "<wc-status" + attrs + "/>";
|
|
19148
|
+
if (!self) this.stack.push(name);
|
|
19149
|
+
else if (name === "entry") this.saveEntry();
|
|
19150
|
+
}
|
|
19151
|
+
/** @private @author ddj 2026年10月09号 Preserve existing property/tree-conflict/path semantics and bounded statuses. */
|
|
19152
|
+
saveEntry() {
|
|
19153
|
+
const result = this.parse(this.entry + "</entry>", 1);
|
|
19154
|
+
this.entry = "";
|
|
19155
|
+
for (const entry of result.entries) {
|
|
19156
|
+
if (this.entries.length >= this.cap) {
|
|
19157
|
+
this.truncated = true;
|
|
19158
|
+
continue;
|
|
19159
|
+
}
|
|
19160
|
+
this.entries.push(this.changelist ? {
|
|
19161
|
+
...entry,
|
|
19162
|
+
changelist: this.changelist
|
|
19163
|
+
} : entry);
|
|
19164
|
+
}
|
|
19165
|
+
}
|
|
19166
|
+
/** @private @author ddj 2026年10月09号 @throws Invalid/missing/truncated XML; never produces a clean snapshot. */
|
|
19167
|
+
invalid() {
|
|
19168
|
+
throw new Error("SVN 状态输出为空或不完整,未确认正常状态");
|
|
19169
|
+
}
|
|
19170
|
+
/** @public @author ddj 2026年10月09号 @returns Confirmed snapshot only after complete stream EOF. @throws Invalid framing. */
|
|
19171
|
+
finish() {
|
|
19172
|
+
this.feed(this.decoder.end());
|
|
19173
|
+
if (!this.opened || !this.closed || this.stack.length || this.buffer.trim()) this.invalid();
|
|
19174
|
+
return {
|
|
19175
|
+
entries: this.entries,
|
|
19176
|
+
truncated: this.truncated
|
|
19177
|
+
};
|
|
19178
|
+
}
|
|
19179
|
+
};
|
|
18410
19180
|
/** CLI 探测超时。 */
|
|
18411
19181
|
const CLI_GRACE_MS = 6e3;
|
|
18412
19182
|
/** svn update 超时(大工作副本留足预算)。 */
|
|
@@ -18603,7 +19373,7 @@ function parseStatusXml(xml, cap = CHANGES_CAP) {
|
|
|
18603
19373
|
}
|
|
18604
19374
|
if (!pendingPath) continue;
|
|
18605
19375
|
const item = attrOf(hit.attrs, "item") || "modified";
|
|
18606
|
-
const status = KNOWN_ITEM_STATUS.has(item) ? item : "modified";
|
|
19376
|
+
const status = attrOf(hit.attrs, "tree-conflicted") === "true" ? "conflicted" : KNOWN_ITEM_STATUS.has(item) ? item : "modified";
|
|
18607
19377
|
const path = changePathOf(pendingPath);
|
|
18608
19378
|
if (!path) {
|
|
18609
19379
|
pendingPath = null;
|
|
@@ -18734,7 +19504,8 @@ function parseRemoteStatusXml(xml, wcRoot, cap = CHANGES_CAP) {
|
|
|
18734
19504
|
const repos = /<repos-status\b([^>]*?)\/?>/.exec(match[2]);
|
|
18735
19505
|
if (!repos) continue;
|
|
18736
19506
|
const item = attrOf(repos[1], "item");
|
|
18737
|
-
|
|
19507
|
+
const props = attrOf(repos[1], "props");
|
|
19508
|
+
if ((!item || item === "none" || item === "normal") && props !== "modified") continue;
|
|
18738
19509
|
const rawPath = attrOf(match[1], "path");
|
|
18739
19510
|
const rel = relPathOfAbs(rawPath, wcRoot) ?? changePathOf(rawPath);
|
|
18740
19511
|
if (!rel) continue;
|
|
@@ -18744,7 +19515,7 @@ function parseRemoteStatusXml(xml, wcRoot, cap = CHANGES_CAP) {
|
|
|
18744
19515
|
}
|
|
18745
19516
|
outdated.push({
|
|
18746
19517
|
path: rel,
|
|
18747
|
-
item
|
|
19518
|
+
item: item && item !== "none" && item !== "normal" ? item : "modified"
|
|
18748
19519
|
});
|
|
18749
19520
|
}
|
|
18750
19521
|
return {
|
|
@@ -18839,6 +19610,9 @@ function createSvnRpc(deps) {
|
|
|
18839
19610
|
const updateRootCache = /* @__PURE__ */ new Map();
|
|
18840
19611
|
/** 变更清单缓存(键 = 工作副本根;TTL 短,仅合并同一轮 UI 的多处拉取)。 */
|
|
18841
19612
|
const changesCache = /* @__PURE__ */ new Map();
|
|
19613
|
+
/** Explorer snapshots are bounded and short-lived; concurrent scans share one subprocess. */
|
|
19614
|
+
const explorerCache = /* @__PURE__ */ new Map();
|
|
19615
|
+
const explorerFlight = /* @__PURE__ */ new Map();
|
|
18842
19616
|
/** 混合通道方案收件箱(key = 工作副本根;覆盖式入箱 + TTL,见 svn.aiPlanSubmit/Pending)。 */
|
|
18843
19617
|
const planInbox = /* @__PURE__ */ new Map();
|
|
18844
19618
|
/** 工作副本根 relative-url 缓存(日志路径映射用;探测失败也缓存空串避免反复 spawn)。 */
|
|
@@ -19382,8 +20156,151 @@ function createSvnRpc(deps) {
|
|
|
19382
20156
|
output: [...notes, ...failures].join("\n")
|
|
19383
20157
|
};
|
|
19384
20158
|
};
|
|
20159
|
+
/** @private @author ddj 2026年10月10号 @param root Native WC root. @param prefix Session prefix. @param paths Subtree targets. @returns Existing parent roots, merged with segment boundaries; missing session forces full. */
|
|
20160
|
+
const scopeRootsOf = async (root, prefix, paths) => {
|
|
20161
|
+
const roots = [];
|
|
20162
|
+
for (const raw of paths) {
|
|
20163
|
+
let path = raw;
|
|
20164
|
+
while (path) {
|
|
20165
|
+
try {
|
|
20166
|
+
if ((await stat(join(root, prefix, path))).isDirectory()) break;
|
|
20167
|
+
} catch {}
|
|
20168
|
+
path = path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "";
|
|
20169
|
+
}
|
|
20170
|
+
if (roots.some((parent) => !parent || path === parent || path.startsWith(parent + "/"))) continue;
|
|
20171
|
+
for (let i = roots.length - 1; i >= 0; i--) if (!path || roots[i].startsWith(path + "/")) roots.splice(i, 1);
|
|
20172
|
+
roots.push(path);
|
|
20173
|
+
}
|
|
20174
|
+
return roots;
|
|
20175
|
+
};
|
|
20176
|
+
/** @private @author ddj 2026年10月10号 @param cwd Session directory. @param root WC root. @param roots Bounded subtrees. @param signal Owner disposal. @returns Confirmed local statuses; malformed/pipe-empty output rejects. */
|
|
20177
|
+
const scanExplorer = async (cwd, root, roots = [""], signal) => {
|
|
20178
|
+
const target = svnPathOf(cwd, root, "");
|
|
20179
|
+
if (target === null) throw new Error("工作区不在 SVN 根目录内");
|
|
20180
|
+
const spawn = subprocess();
|
|
20181
|
+
if (!spawn) throw new Error("缺少 subprocess 服务");
|
|
20182
|
+
const fs = ctx.get("fs");
|
|
20183
|
+
const realRoot = fs?.resolve && fs?.processPath ? fs.processPath(await fs.resolve(root, {})) : root;
|
|
20184
|
+
const validRoots = await scopeRootsOf(realRoot, target, roots);
|
|
20185
|
+
const targets = validRoots.map((path) => {
|
|
20186
|
+
const rel = [target === "." ? "" : target, path].filter(Boolean).join("/") || ".";
|
|
20187
|
+
return rel.includes("@") ? rel + "@" : rel;
|
|
20188
|
+
});
|
|
20189
|
+
const parsed = await readStatusPipe(spawn({
|
|
20190
|
+
argv: [
|
|
20191
|
+
settings.svn().svnPath.trim() || "svn",
|
|
20192
|
+
"--non-interactive",
|
|
20193
|
+
"status",
|
|
20194
|
+
"--xml",
|
|
20195
|
+
"--verbose",
|
|
20196
|
+
"--no-ignore",
|
|
20197
|
+
"--",
|
|
20198
|
+
...targets
|
|
20199
|
+
],
|
|
20200
|
+
cwd: realRoot,
|
|
20201
|
+
stdio: {
|
|
20202
|
+
stdout: "pipe",
|
|
20203
|
+
stderr: { maxBytes: OUTPUT_CAP },
|
|
20204
|
+
stdin: "ignore"
|
|
20205
|
+
},
|
|
20206
|
+
graceMs: STATUS_GRACE_MS
|
|
20207
|
+
}), parseStatusXml, EXPLORER_CAP, STATUS_GRACE_MS, signal);
|
|
20208
|
+
return {
|
|
20209
|
+
entries: scopeEntriesOf(parsed.entries, target),
|
|
20210
|
+
truncated: parsed.truncated,
|
|
20211
|
+
roots: validRoots
|
|
20212
|
+
};
|
|
20213
|
+
};
|
|
20214
|
+
/** @private @author ddj 2026年10月09号 @param cwd Session directory. @param root WC root. @param force Bypass TTL. @returns Shared bounded scan; old cache entries are evicted. */
|
|
20215
|
+
const explorerOf = async (cwd, root, force) => {
|
|
20216
|
+
const pending = explorerFlight.get(cwd);
|
|
20217
|
+
if (pending) return pending;
|
|
20218
|
+
const hit = explorerCache.get(cwd);
|
|
20219
|
+
if (!force && hit && now() - hit.at < 5e3) return hit;
|
|
20220
|
+
const work = scanExplorer(cwd, root);
|
|
20221
|
+
explorerFlight.set(cwd, work);
|
|
20222
|
+
try {
|
|
20223
|
+
const result = await work;
|
|
20224
|
+
explorerCache.delete(cwd);
|
|
20225
|
+
if (explorerCache.size >= 2) explorerCache.delete(explorerCache.keys().next().value);
|
|
20226
|
+
explorerCache.set(cwd, {
|
|
20227
|
+
at: now(),
|
|
20228
|
+
...result
|
|
20229
|
+
});
|
|
20230
|
+
return result;
|
|
20231
|
+
} finally {
|
|
20232
|
+
explorerFlight.delete(cwd);
|
|
20233
|
+
}
|
|
20234
|
+
};
|
|
20235
|
+
const explorerScopes = new ExplorerScopes();
|
|
20236
|
+
/** @private @author ddj 2026年10月10号 @param args Validated view projection. @returns Scope-isolated compact evidence; no repository contact. */
|
|
20237
|
+
const viewExplorer = async (args) => {
|
|
20238
|
+
if (!viewArgsValid(args)) return {
|
|
20239
|
+
ok: false,
|
|
20240
|
+
error: "SVN 图标请求无效或超出行数/路径上限"
|
|
20241
|
+
};
|
|
20242
|
+
const sc = await requireSvnSession(ctx, args.sessionId);
|
|
20243
|
+
if ("err" in sc) return {
|
|
20244
|
+
ok: false,
|
|
20245
|
+
error: sc.err
|
|
20246
|
+
};
|
|
20247
|
+
const status = await statusOf(sc.cwd);
|
|
20248
|
+
if (!status.managed || !status.wcRoot || !status.svnCli) return {
|
|
20249
|
+
ok: false,
|
|
20250
|
+
error: "SVN 工作副本或命令不可用"
|
|
20251
|
+
};
|
|
20252
|
+
try {
|
|
20253
|
+
const root = status.wcRoot;
|
|
20254
|
+
const scopeId = JSON.stringify([explorerKey(root), explorerKey(sc.cwd)]);
|
|
20255
|
+
/** @private @author ddj 2026年10月10号 @param roots Scoped subtrees. @param signal Owner disposal. @returns Streamed scan. */
|
|
20256
|
+
const scan = (roots, signal) => scanExplorer(sc.cwd, root, roots, signal);
|
|
20257
|
+
const queue = explorerScopes.get(scopeId, scan, now);
|
|
20258
|
+
const reason = args.reason ?? "read";
|
|
20259
|
+
if (reason !== "read" && (reason !== "calibrate" || now() - queue.state().confirmedAt > 5e3)) queue.invalidate({
|
|
20260
|
+
full: reason !== "paths",
|
|
20261
|
+
paths: args.paths,
|
|
20262
|
+
event: JSON.stringify([args.clientId, args.eventId])
|
|
20263
|
+
});
|
|
20264
|
+
await queue.initial();
|
|
20265
|
+
return {
|
|
20266
|
+
ok: true,
|
|
20267
|
+
...explorerViewOf(queue, scopeId, svnPathOf(sc.cwd, root, "") ?? ".", args)
|
|
20268
|
+
};
|
|
20269
|
+
} catch (error) {
|
|
20270
|
+
return {
|
|
20271
|
+
ok: false,
|
|
20272
|
+
error: "读取 SVN 图标状态失败:" + String(error)
|
|
20273
|
+
};
|
|
20274
|
+
}
|
|
20275
|
+
};
|
|
19385
20276
|
return {
|
|
19386
20277
|
handlers: {
|
|
20278
|
+
"svn.explorerView": viewExplorer,
|
|
20279
|
+
/** @public @author ddj 2026年10月09号 @param args Session and refresh flag. @returns Read-only explorer snapshot; no repository contact. */
|
|
20280
|
+
"svn.explorerStatus": async (args) => {
|
|
20281
|
+
const sc = await requireSvnSession(ctx, args.sessionId);
|
|
20282
|
+
if ("err" in sc) return {
|
|
20283
|
+
ok: false,
|
|
20284
|
+
error: sc.err
|
|
20285
|
+
};
|
|
20286
|
+
const status = await statusOf(sc.cwd);
|
|
20287
|
+
if (!status.managed || !status.wcRoot || !status.svnCli) return {
|
|
20288
|
+
ok: false,
|
|
20289
|
+
error: "SVN 工作副本或命令不可用"
|
|
20290
|
+
};
|
|
20291
|
+
try {
|
|
20292
|
+
return {
|
|
20293
|
+
ok: true,
|
|
20294
|
+
...await explorerOf(sc.cwd, status.wcRoot, args.force === true),
|
|
20295
|
+
prefix: svnPathOf(sc.cwd, status.wcRoot, "") ?? "."
|
|
20296
|
+
};
|
|
20297
|
+
} catch (error) {
|
|
20298
|
+
return {
|
|
20299
|
+
ok: false,
|
|
20300
|
+
error: "读取 SVN 图标状态失败:" + String(error)
|
|
20301
|
+
};
|
|
20302
|
+
}
|
|
20303
|
+
},
|
|
19387
20304
|
"svn.status": async (args) => {
|
|
19388
20305
|
const sc = await requireSvnSession(ctx, args.sessionId);
|
|
19389
20306
|
if ("err" in sc) return {
|
|
@@ -19841,17 +20758,27 @@ function createSvnRpc(deps) {
|
|
|
19841
20758
|
error: "svn 命令不可用(可在设置页配置 svn 路径)"
|
|
19842
20759
|
};
|
|
19843
20760
|
try {
|
|
20761
|
+
const target = svnPathOf(sc.cwd, status.wcRoot, rel);
|
|
20762
|
+
if (target === null) return {
|
|
20763
|
+
ok: false,
|
|
20764
|
+
error: "路径不在 SVN 工作副本内"
|
|
20765
|
+
};
|
|
19844
20766
|
const argv = [
|
|
19845
20767
|
"status",
|
|
19846
20768
|
"-u",
|
|
19847
|
-
"--xml"
|
|
20769
|
+
"--xml",
|
|
20770
|
+
"--",
|
|
20771
|
+
target
|
|
19848
20772
|
];
|
|
19849
|
-
if (rel) argv.push("--", rel);
|
|
19850
20773
|
const outcome = await runSvn(status.wcRoot, argv, REMOTE_GRACE_MS, SUM_OUTPUT_CAP);
|
|
19851
20774
|
if (outcome.code !== 0) return {
|
|
19852
20775
|
ok: false,
|
|
19853
20776
|
error: "远端检查失败:" + tailOfText([outcome.stderr, outcome.stdout].filter(Boolean).join("\n"))
|
|
19854
20777
|
};
|
|
20778
|
+
if (!statusXmlValid(outcome.stdout)) return {
|
|
20779
|
+
ok: false,
|
|
20780
|
+
error: "远端状态输出为空或不完整"
|
|
20781
|
+
};
|
|
19855
20782
|
return {
|
|
19856
20783
|
ok: true,
|
|
19857
20784
|
...parseRemoteStatusXml(outcome.stdout, status.wcRoot)
|
|
@@ -20452,6 +21379,8 @@ function createSvnRpc(deps) {
|
|
|
20452
21379
|
}
|
|
20453
21380
|
},
|
|
20454
21381
|
dispose: () => {
|
|
21382
|
+
explorerScopes.dispose();
|
|
21383
|
+
explorerCache.clear();
|
|
20455
21384
|
updateJobs.dispose();
|
|
20456
21385
|
updateRootCache.clear();
|
|
20457
21386
|
}
|
|
@@ -21682,18 +22611,47 @@ var DapSession = class {
|
|
|
21682
22611
|
earlyTermHint() {
|
|
21683
22612
|
return "会话在初始化完成前终止(attach/连接目标失败)" + (this.extId === "nromanov.dotrush" && this.extPath ? ";适配器日志:" + join(this.extPath, "bin", "DebuggerMono", "logs", "Error.log") : ";请查看适配器自身日志与宿主日志 [dap-adapter] 段");
|
|
21684
22613
|
}
|
|
21685
|
-
/**
|
|
22614
|
+
/**
|
|
22615
|
+
* 按当前语言过滤并排序源码候选,供反向应答与栈帧解析共用。
|
|
22616
|
+
* @private
|
|
22617
|
+
* @author ddj 2026年10月09号
|
|
22618
|
+
* @param chunk 适配器上报的源码标识
|
|
22619
|
+
* @returns 语言合法、按路径相关度排序的候选
|
|
22620
|
+
*/
|
|
22621
|
+
async sourceCandidates(chunk) {
|
|
22622
|
+
const files = await this.deps.findFiles(this.workspacePath, chunk);
|
|
22623
|
+
const exts = this.frameExts();
|
|
22624
|
+
return rankSourceCandidates(chunk, exts.length ? files.filter((file) => hasSourceExtension(file, exts)) : files, this.workspacePath);
|
|
22625
|
+
}
|
|
22626
|
+
/**
|
|
22627
|
+
* 更新源码标识及 basename 缓存;无合法候选时删除旧映射。
|
|
22628
|
+
* @private
|
|
22629
|
+
* @author ddj 2026年10月09号
|
|
22630
|
+
* @param chunk 适配器上报的源码标识
|
|
22631
|
+
* @param file 合法候选首项,空值表示未解析
|
|
22632
|
+
* @description 副作用:更新当前会话源码缓存
|
|
22633
|
+
*/
|
|
22634
|
+
cacheSource(chunk, file) {
|
|
22635
|
+
for (const key of [sourceKeyOf(chunk), sourceKeyOf(sourceBaseOf(chunk))]) {
|
|
22636
|
+
if (!key) continue;
|
|
22637
|
+
if (file) this.sourceFiles.set(key, file);
|
|
22638
|
+
else this.sourceFiles.delete(key);
|
|
22639
|
+
}
|
|
22640
|
+
}
|
|
22641
|
+
/**
|
|
22642
|
+
* findFileReq 反向应答:只返回当前语言合法源码并缓存首命中。
|
|
22643
|
+
* @private
|
|
22644
|
+
* @author ddj 2026年10月09号
|
|
22645
|
+
* @param body 反向请求参数
|
|
22646
|
+
* @description 副作用:更新源码缓存并发送 findFileRsp
|
|
22647
|
+
*/
|
|
21686
22648
|
async respondFindFile(body) {
|
|
21687
22649
|
const seq = typeof body.seq === "number" ? body.seq : -1;
|
|
21688
22650
|
const chunk = String(body.file ?? "");
|
|
21689
|
-
const files = await this.
|
|
22651
|
+
const files = await this.sourceCandidates(chunk);
|
|
21690
22652
|
log.debug("[dap-trace] findFileReq seq=" + seq + " chunk=" + chunk + " candidates=" + files.length + " first=" + (files[0] ?? ""));
|
|
21691
22653
|
this.deps.trace?.(this.workspacePath, "[DEBUG findFileReq] seq=" + seq + " chunk=" + chunk + " candidates=" + files.length + " first=" + (files[0] ?? ""));
|
|
21692
|
-
|
|
21693
|
-
if (first) {
|
|
21694
|
-
this.sourceFiles.set(sourceKeyOf(chunk), first);
|
|
21695
|
-
this.sourceFiles.set(sourceKeyOf(sourceBaseOf(chunk)), first);
|
|
21696
|
-
}
|
|
22654
|
+
this.cacheSource(chunk, files[0] ?? "");
|
|
21697
22655
|
this.send({
|
|
21698
22656
|
seq: this.msgSeq++,
|
|
21699
22657
|
type: "request",
|
|
@@ -21831,25 +22789,40 @@ var DapSession = class {
|
|
|
21831
22789
|
this.error = void 0;
|
|
21832
22790
|
}
|
|
21833
22791
|
}
|
|
21834
|
-
/**
|
|
22792
|
+
/**
|
|
22793
|
+
* 适配器源码标识转帧视图;完整合法路径优先,错误语言缓存须重新解析。
|
|
22794
|
+
* @private
|
|
22795
|
+
* @author ddj 2026年10月09号
|
|
22796
|
+
* @param item 适配器栈帧
|
|
22797
|
+
* @param index 栈帧下标(EmmyLua frameId)
|
|
22798
|
+
* @returns 保留原始行号、只含合法源码路径的帧视图
|
|
22799
|
+
* @description 副作用:按需查找源码并更新会话缓存
|
|
22800
|
+
*/
|
|
21835
22801
|
async frameViewOf(item, index) {
|
|
21836
22802
|
const raw = item;
|
|
21837
22803
|
const source = raw.source ?? {};
|
|
21838
22804
|
const sourcePath = typeof source.path === "string" ? source.path : "";
|
|
21839
22805
|
const sourceName = typeof source.name === "string" ? source.name : "";
|
|
22806
|
+
const exts = this.frameExts();
|
|
21840
22807
|
const cached = this.sourceFiles.get(sourceKeyOf(sourcePath)) ?? this.sourceFiles.get(sourceKeyOf(sourceName));
|
|
21841
|
-
|
|
21842
|
-
const
|
|
22808
|
+
const validPath = sourcePath && !isBareChunk(sourcePath) && (exts.length ? hasSourceExtension(sourcePath, exts) : sourceBaseOf(sourcePath).includes("."));
|
|
22809
|
+
const validCache = cached && (!exts.length || hasSourceExtension(cached, exts));
|
|
22810
|
+
let selected = validPath ? sourcePath : validCache ? cached : sourcePath || sourceName;
|
|
22811
|
+
const needsLookup = !validPath && !validCache;
|
|
22812
|
+
if (cached && !validCache) {
|
|
22813
|
+
this.cacheSource(sourcePath, "");
|
|
22814
|
+
this.cacheSource(sourceName, "");
|
|
22815
|
+
}
|
|
21843
22816
|
log.debug("[dap-trace] frame rawPath=" + sourcePath + " name=" + sourceName + " cached=" + (cached ?? "") + " needsLookup=" + needsLookup + " selected=" + selected);
|
|
21844
22817
|
this.deps.trace?.(this.workspacePath, "[DEBUG frame] rawPath=" + sourcePath + " name=" + sourceName + " cached=" + (cached ?? "") + " needsLookup=" + needsLookup + " selected=" + selected);
|
|
21845
|
-
if (
|
|
21846
|
-
const candidates = await this.
|
|
22818
|
+
if (needsLookup) {
|
|
22819
|
+
const candidates = await this.sourceCandidates(sourceName || sourcePath);
|
|
21847
22820
|
log.debug("[dap-trace] frameLookup source=" + (sourceName || sourcePath) + " candidates=" + candidates.length + " first=" + (candidates[0] ?? ""));
|
|
21848
22821
|
this.deps.trace?.(this.workspacePath, "[DEBUG frameLookup] source=" + (sourceName || sourcePath) + " candidates=" + candidates.length + " first=" + (candidates[0] ?? ""));
|
|
21849
22822
|
if (candidates[0]) {
|
|
21850
22823
|
selected = candidates[0];
|
|
21851
|
-
this.
|
|
21852
|
-
} else if (isBareChunk(sourceName || sourcePath)) selected = "";
|
|
22824
|
+
this.cacheSource(sourceName || sourcePath, candidates[0]);
|
|
22825
|
+
} else if (exts.length || isBareChunk(sourceName || sourcePath)) selected = "";
|
|
21853
22826
|
}
|
|
21854
22827
|
const finalFile = selected ? sourcePathOf(selected, this.workspacePath) : "";
|
|
21855
22828
|
log.debug("[dap-trace] frameFinal name=" + sourceName + " file=" + finalFile + " line=" + String(raw.line ?? 0));
|
|
@@ -22211,6 +23184,16 @@ async function apply(ctx, config) {
|
|
|
22211
23184
|
await installPlanTool(ctx, submitPlan);
|
|
22212
23185
|
ctx.effect(mountInbox, "vscode-mode:open-inbox");
|
|
22213
23186
|
ctx.effect(() => shellMenuLifecycle(ctx), "vscode-mode:integration");
|
|
23187
|
+
/** @author ddj 2026年10月12号 @returns Active-profile bridge disposer; failures never block boot. */
|
|
23188
|
+
async function provisionBridge() {
|
|
23189
|
+
try {
|
|
23190
|
+
await ensureActiveBridge(ctx, await integrationBaseUrlOf(ctx));
|
|
23191
|
+
} catch (error) {
|
|
23192
|
+
noteWarning("外部打开桥接未就绪:" + String(error));
|
|
23193
|
+
}
|
|
23194
|
+
return () => {};
|
|
23195
|
+
}
|
|
23196
|
+
ctx.effect(provisionBridge, "vscode-mode:bridge");
|
|
22214
23197
|
sweepTreeCache();
|
|
22215
23198
|
await logCompatSummary(ctx, warnings);
|
|
22216
23199
|
log.info("编辑差异审查已装配(认证路由、项目 MCP 作用域、SVN 方案工具与外部打开通道)");
|