dsh-vscode-mode 0.10.0 → 0.11.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 +33 -3
- package/lib/client.js +1501 -269
- package/lib/client.js.map +1 -1
- package/lib/index.js +2163 -1042
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/dap/hover.ts +2 -7
- package/src/client/index.ts +25 -0
- package/src/client/luaReservedWords.ts +10 -0
- package/src/client/monaco/lsp/keywords.ts +121 -0
- package/src/client/monaco/lsp/lspClient.ts +68 -6
- package/src/client/monaco/lsp/memberScope.ts +90 -0
- package/src/client/monaco/lsp/providers.ts +14 -4
- package/src/client/settingsDocRouter.ts +52 -0
- package/src/client/sidebar/menuItems.ts +1 -0
- package/src/client/sidebar/types.ts +2 -0
- package/src/client/styles/editor.css +143 -37
- package/src/client/svnActions.ts +4 -4
- package/src/client/ui/ContextMenu.ts +4 -2
- package/src/client/ui/EditorView.ts +132 -52
- package/src/client/ui/SvnLogDialog.ts +200 -69
- package/src/client/ui/SvnUpdateDialog.ts +472 -0
- package/src/client/ui/svnUpdateModel.ts +135 -0
- package/src/index.ts +2 -0
- package/src/lsp/completionCap.ts +80 -0
- package/src/lsp/manager.ts +42 -10
- package/src/lsp/rpc.ts +8 -5
- package/src/lsp/server.ts +18 -4
- package/src/rpc.ts +74 -2
- package/src/settingsDocPath.ts +30 -0
- package/src/shared/rpc.ts +12 -2
- package/src/shared/svn.ts +38 -0
- package/src/svn.ts +102 -7
- package/src/svnText.ts +22 -13
- package/src/svnUpdateJob.ts +588 -0
package/src/svn.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 作者 ddj 2026年09月16号
|
|
8
8
|
*/
|
|
9
9
|
import { readdir, stat } from 'node:fs/promises'
|
|
10
|
-
import { basename, dirname, join } from 'node:path'
|
|
10
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
|
11
11
|
import { tmpdir } from 'node:os'
|
|
12
12
|
import type { Ctx, Session } from './store.js'
|
|
13
13
|
import { cwdOf, sessionOf } from './registry.js'
|
|
@@ -18,6 +18,8 @@ import { buildAiPrompt, svnAiPlanOf } from './ai/svnTriage.js'
|
|
|
18
18
|
import { attrOf, numAttrOf, scanXmlTags, textOf, unescapeXml } from './svnXml.js'
|
|
19
19
|
import { TEXT_OUTPUT_CAP, batchResultOf, tailOfText, updateResultOf } from './svnText.js'
|
|
20
20
|
import { LOG_CAP, LOG_DEFAULT_LIMIT, SVN_LOG_SHOW_ALL_CAP, parseLogXml, parseSvnInfoRevision } from './svnLog.js'
|
|
21
|
+
import { SvnUpdateJobs } from './svnUpdateJob.js'
|
|
22
|
+
import type { UpdateProcess } from './svnUpdateJob.js'
|
|
21
23
|
export { batchResultOf, commitResultOf, updateResultOf } from './svnText.js'
|
|
22
24
|
export { mapRepoPath, parseLogXml } from './svnLog.js'
|
|
23
25
|
|
|
@@ -387,6 +389,7 @@ interface SvnRunOutcome { code: number | null; stdout: string; stderr: string }
|
|
|
387
389
|
/** subprocess 产句柄最小结构面(done 载荷对齐 revert.ts 的 exitCode/code 双读)。 */
|
|
388
390
|
interface SpawnedHandle {
|
|
389
391
|
done: Promise<{ exitCode?: number; code?: number } | undefined>
|
|
392
|
+
terminate?: () => void
|
|
390
393
|
collected?: {
|
|
391
394
|
stdout?: { readFrom: (n: number) => { text: string } }
|
|
392
395
|
stderr?: { readFrom: (n: number) => { text: string } }
|
|
@@ -466,18 +469,35 @@ function safeRel(raw: unknown): string | null {
|
|
|
466
469
|
return rel
|
|
467
470
|
}
|
|
468
471
|
|
|
472
|
+
/**
|
|
473
|
+
* 会话根可在 SVN 工作副本的子目录;CLI 在 wcRoot 执行时必须换算目标坐标。
|
|
474
|
+
* @author ddj 2026年09月24号
|
|
475
|
+
* @param cwd 当前会话目录
|
|
476
|
+
* @param wcRoot SVN 工作副本根
|
|
477
|
+
* @param rel 会话相对目标
|
|
478
|
+
* @returns SVN 根相对目标;越界返回 null
|
|
479
|
+
*/
|
|
480
|
+
export function svnPathOf(cwd: string, wcRoot: string, rel: string): string | null {
|
|
481
|
+
if (safeRel(rel) === null) return null
|
|
482
|
+
const wcRel = relative(wcRoot, resolve(cwd, rel || '.'))
|
|
483
|
+
if (isAbsolute(wcRel) || wcRel === '..' || wcRel.startsWith('../') || wcRel.startsWith('..\\')) return null
|
|
484
|
+
return wcRel.replace(/\\/g, '/') || '.'
|
|
485
|
+
}
|
|
486
|
+
|
|
469
487
|
/**
|
|
470
488
|
* 创建 svn.* RPC handlers(缓存随装配生命周期;lsp/ai 同模式并入 buildHandlers)。
|
|
471
489
|
* @author ddj 2026年09月16号
|
|
472
490
|
* @param deps ctx + 设置读取面 + 可注入时钟
|
|
473
491
|
* @returns handlers(Partial<RpcHandlerMap> 形状)
|
|
474
492
|
*/
|
|
475
|
-
export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unknown
|
|
493
|
+
export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unknown>; dispose: () => void } {
|
|
476
494
|
const { ctx, settings } = deps
|
|
477
495
|
const now = deps.now ?? (() => Date.now())
|
|
478
496
|
const findRoot = deps.findRoot ?? ((cwd: string) => findSvnRoot(cwd))
|
|
479
497
|
const findTortoise = deps.findTortoise ?? ((dir: string) => findTortoiseProc(dir))
|
|
480
498
|
const statusCache = new Map<string, { at: number; payload: SvnStatusPayload }>()
|
|
499
|
+
/** 运行中更新按会话目录复用工作副本根;TTL 到期才重新扫描 .svn。 */
|
|
500
|
+
const updateRootCache = new Map<string, { at: number; root: string }>()
|
|
481
501
|
/** 变更清单缓存(键 = 工作副本根;TTL 短,仅合并同一轮 UI 的多处拉取)。 */
|
|
482
502
|
const changesCache = new Map<string, { at: number; value: { entries: SvnChangeEntry[]; truncated: boolean } }>()
|
|
483
503
|
/** 混合通道方案收件箱(key = 工作副本根;覆盖式入箱 + TTL,见 svn.aiPlanSubmit/Pending)。 */
|
|
@@ -485,6 +505,11 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
485
505
|
/** 工作副本根 relative-url 缓存(日志路径映射用;探测失败也缓存空串避免反复 spawn)。 */
|
|
486
506
|
const urlCache = new Map<string, { at: number; value: string }>()
|
|
487
507
|
let cliProbe: { at: number; exe: string; ok: boolean } | null = null
|
|
508
|
+
const updateJobs = new SvnUpdateJobs(
|
|
509
|
+
() => ctx.get('subprocess') as UpdateProcess | null,
|
|
510
|
+
deps.platform ?? process.platform,
|
|
511
|
+
now,
|
|
512
|
+
)
|
|
488
513
|
|
|
489
514
|
const subprocess = (): SpawnFn | null => {
|
|
490
515
|
const sub = ctx.get('subprocess') as { spawn?: unknown } | null | undefined
|
|
@@ -499,6 +524,9 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
499
524
|
let ok = false
|
|
500
525
|
const spawn = subprocess()
|
|
501
526
|
if (spawn) {
|
|
527
|
+
const controller = new AbortController()
|
|
528
|
+
const deadline = setTimeout(() => controller.abort(new Error('SVN CLI 探测超时')), CLI_GRACE_MS)
|
|
529
|
+
deadline.unref?.()
|
|
502
530
|
try {
|
|
503
531
|
const handle = spawn({
|
|
504
532
|
argv: [exe, '--version', '--quiet'],
|
|
@@ -507,11 +535,20 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
507
535
|
cwd: tmpdir(),
|
|
508
536
|
stdio: { stdout: 'inherit', stderr: 'inherit', stdin: 'ignore' },
|
|
509
537
|
graceMs: CLI_GRACE_MS,
|
|
538
|
+
signal: controller.signal,
|
|
539
|
+
})
|
|
540
|
+
const expired = new Promise<never>((_, reject) => {
|
|
541
|
+
controller.signal.addEventListener('abort', () => {
|
|
542
|
+
try { handle.terminate?.() } catch { /* 返回探测超时即可 */ }
|
|
543
|
+
reject(controller.signal.reason)
|
|
544
|
+
}, { once: true })
|
|
510
545
|
})
|
|
511
|
-
const outcome = await handle.done
|
|
546
|
+
const outcome = await Promise.race([handle.done, expired])
|
|
512
547
|
ok = (outcome?.exitCode ?? outcome?.code) === 0
|
|
513
548
|
} catch {
|
|
514
549
|
ok = false
|
|
550
|
+
} finally {
|
|
551
|
+
clearTimeout(deadline)
|
|
515
552
|
}
|
|
516
553
|
}
|
|
517
554
|
cliProbe = { at, exe, ok }
|
|
@@ -543,6 +580,28 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
543
580
|
return payload
|
|
544
581
|
}
|
|
545
582
|
|
|
583
|
+
/**
|
|
584
|
+
* 验证更新请求属于当前会话的工作副本;取消/轮询不依赖 CLI 仍可用。
|
|
585
|
+
* @author ddj 2026年09月24号
|
|
586
|
+
* @param sessionId 当前会话 id
|
|
587
|
+
* @param needCli 仅真正启动更新时才探测 SVN 客户端
|
|
588
|
+
* @returns 已校验的目录和状态,或错误
|
|
589
|
+
*/
|
|
590
|
+
const updateScope = async (sessionId?: string, needCli = false) => {
|
|
591
|
+
const sc = await requireSvnSession(ctx, sessionId)
|
|
592
|
+
if ('err' in sc) return { err: sc.err }
|
|
593
|
+
if (!needCli) {
|
|
594
|
+
const cached = updateRootCache.get(sc.cwd)
|
|
595
|
+
const root = cached && now() - cached.at < STATUS_TTL_MS ? cached.root : await findRoot(sc.cwd)
|
|
596
|
+
if (root && (!cached || now() - cached.at >= STATUS_TTL_MS)) updateRootCache.set(sc.cwd, { at: now(), root })
|
|
597
|
+
return root ? { cwd: sc.cwd, root, exe: '', cli: false } : { err: '当前工作区不受 SVN 管理' }
|
|
598
|
+
}
|
|
599
|
+
const status = await statusOf(sc.cwd)
|
|
600
|
+
if (!status.managed || !status.wcRoot) return { err: '当前工作区不受 SVN 管理' }
|
|
601
|
+
updateRootCache.set(sc.cwd, { at: now(), root: status.wcRoot })
|
|
602
|
+
return { cwd: sc.cwd, root: status.wcRoot, exe: status.svnPath, cli: status.svnCli }
|
|
603
|
+
}
|
|
604
|
+
|
|
546
605
|
/**
|
|
547
606
|
* 运行 svn CLI(管道收集;spawn 级失败回退 inherit,输出降级为空)。
|
|
548
607
|
* @author ddj 2026年08月26号 / 2026年09月17号
|
|
@@ -966,8 +1025,10 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
966
1025
|
const status = await statusOf(sc.cwd)
|
|
967
1026
|
if (!status.managed || !status.wcRoot) return { ok: false as const, error: '当前工作区不受 SVN 管理' }
|
|
968
1027
|
if (!status.svnCli) return { ok: false as const, error: 'svn 命令不可用(可在设置页配置 svn 路径)' }
|
|
1028
|
+
const svnTarget = svnPathOf(sc.cwd, status.wcRoot, rel)
|
|
1029
|
+
if (svnTarget === null) return { ok: false as const, error: '日志路径不在 SVN 工作副本内' }
|
|
969
1030
|
try {
|
|
970
|
-
const result = await logOf(status.wcRoot,
|
|
1031
|
+
const result = await logOf(status.wcRoot, svnTarget === '.' ? '' : svnTarget, args.limit ?? LOG_DEFAULT_LIMIT, {
|
|
971
1032
|
stopOnCopy: args.stopOnCopy === true,
|
|
972
1033
|
showMerged: args.showMerged === true,
|
|
973
1034
|
range: args.startRev && args.endRev
|
|
@@ -1026,6 +1087,8 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
1026
1087
|
const status = await statusOf(sc.cwd)
|
|
1027
1088
|
if (!status.managed || !status.wcRoot) return { ok: false as const, error: '当前工作区不受 SVN 管理' }
|
|
1028
1089
|
if (!status.svnCli) return { ok: false as const, error: 'svn 命令不可用(可在设置页配置 svn 路径)' }
|
|
1090
|
+
const svnRel = svnPathOf(sc.cwd, status.wcRoot, rel)
|
|
1091
|
+
if (!svnRel) return { ok: false as const, error: '比较路径不在 SVN 工作副本内' }
|
|
1029
1092
|
const fs = ctx.get('fs') as {
|
|
1030
1093
|
resolve?: (p: string, o: object) => Promise<unknown>
|
|
1031
1094
|
stat?: (t: unknown) => Promise<{ type?: string; size?: number } | null>
|
|
@@ -1039,7 +1102,7 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
1039
1102
|
if ((info.size ?? 0) > READ_CAP) return { ok: false as const, error: '文件过大(>8MB),不支持与工作副本比较' }
|
|
1040
1103
|
const working = await fs.readText(resolved)
|
|
1041
1104
|
// 左侧 = 该版本内容;cat 失败按「该版本尚无此文件」处理(新增语义,非错误)
|
|
1042
|
-
const leftOutcome = await runSvn(status.wcRoot, ['cat', '-r', String(Math.floor(revision)), '--',
|
|
1105
|
+
const leftOutcome = await runSvn(status.wcRoot, ['cat', '-r', String(Math.floor(revision)), '--', svnRel], READ_GRACE_MS)
|
|
1043
1106
|
if (leftOutcome.code !== 0) {
|
|
1044
1107
|
const text = (leftOutcome.stderr || leftOutcome.stdout).trim()
|
|
1045
1108
|
return { ok: true as const, left: null, right: working, reason: 'not-exist', error: tailOfText(text), ...guardSides(null, working) }
|
|
@@ -1363,7 +1426,7 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
1363
1426
|
if (!status.managed) return { ok: false as const, error: '当前工作区不受 SVN 管理' }
|
|
1364
1427
|
if (!status.svnCli) return { ok: false as const, error: 'svn 命令不可用(可在设置页配置 svn 路径)' }
|
|
1365
1428
|
try {
|
|
1366
|
-
const outcome = await runSvn(sc.cwd, ['update', rel || '.'], UPDATE_GRACE_MS)
|
|
1429
|
+
const outcome = await runSvn(sc.cwd, ['update', '--', rel || '.'], UPDATE_GRACE_MS)
|
|
1367
1430
|
const result = updateResultOf(outcome.stdout, outcome.stderr, outcome.code)
|
|
1368
1431
|
if (outcome.code !== 0) return { ok: false as const, error: '更新失败:' + tailOfText(result.output) }
|
|
1369
1432
|
return { ok: true as const, ...result }
|
|
@@ -1371,6 +1434,38 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
1371
1434
|
return { ok: false as const, error: '更新失败:' + String(error) }
|
|
1372
1435
|
}
|
|
1373
1436
|
},
|
|
1437
|
+
'svn.updateStart': async (args: { sessionId?: string; path?: string }) => {
|
|
1438
|
+
const scope = await updateScope(args.sessionId, true)
|
|
1439
|
+
if ('err' in scope) return { ok: false as const, error: scope.err }
|
|
1440
|
+
if (!scope.cli) return { ok: false as const, error: 'svn 命令不可用(可在设置页配置 svn 路径)' }
|
|
1441
|
+
const rel = safeRel(args.path)
|
|
1442
|
+
if (rel === null) return { ok: false as const, error: '路径不合法' }
|
|
1443
|
+
try {
|
|
1444
|
+
const fs = ctx.get('fs') as { resolve?: (p: string, o: object) => Promise<unknown>; processPath?: (t: unknown) => string } | undefined
|
|
1445
|
+
const cwd = fs?.resolve && fs?.processPath
|
|
1446
|
+
? fs.processPath(await fs.resolve(scope.cwd, {})) : scope.cwd
|
|
1447
|
+
return { ok: true as const, ...updateJobs.start({ cwd, root: scope.root, path: rel, exe: scope.exe }) }
|
|
1448
|
+
} catch (error) {
|
|
1449
|
+
return { ok: false as const, error: '启动 SVN 更新失败:' + String(error) }
|
|
1450
|
+
}
|
|
1451
|
+
},
|
|
1452
|
+
'svn.updatePoll': async (args: { sessionId?: string; jobId: string; since: number }) => {
|
|
1453
|
+
const scope = await updateScope(args.sessionId)
|
|
1454
|
+
if ('err' in scope) return { ok: false as const, error: scope.err }
|
|
1455
|
+
try { return { ok: true as const, ...updateJobs.poll(scope.root, args.jobId, args.since) } }
|
|
1456
|
+
catch (error) { return { ok: false as const, error: String(error) } }
|
|
1457
|
+
},
|
|
1458
|
+
'svn.updateCancel': async (args: { sessionId?: string; jobId: string }) => {
|
|
1459
|
+
const scope = await updateScope(args.sessionId)
|
|
1460
|
+
if ('err' in scope) return { ok: false as const, error: scope.err }
|
|
1461
|
+
try { return { ok: true as const, ...updateJobs.cancel(scope.root, args.jobId) } }
|
|
1462
|
+
catch (error) { return { ok: false as const, error: String(error) } }
|
|
1463
|
+
},
|
|
1464
|
+
'svn.updateActive': async (args: { sessionId?: string }) => {
|
|
1465
|
+
const scope = await updateScope(args.sessionId)
|
|
1466
|
+
if ('err' in scope) return { ok: false as const, error: scope.err }
|
|
1467
|
+
return { ok: true as const, job: updateJobs.active(scope.root) }
|
|
1468
|
+
},
|
|
1374
1469
|
'svn.tortoise': async (args: { sessionId?: string; action?: SvnAction; path?: string }) => {
|
|
1375
1470
|
const sc = await requireSvnSession(ctx, args.sessionId)
|
|
1376
1471
|
if ('err' in sc) return { ok: false as const, error: sc.err }
|
|
@@ -1397,5 +1492,5 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
1397
1492
|
}
|
|
1398
1493
|
},
|
|
1399
1494
|
}
|
|
1400
|
-
return { handlers }
|
|
1495
|
+
return { handlers, dispose: () => { updateJobs.dispose(); updateRootCache.clear() } }
|
|
1401
1496
|
}
|
package/src/svnText.ts
CHANGED
|
@@ -70,7 +70,24 @@ const UPDATE_ACTION_LABEL: Record<string, string> = {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/** update 结果行:行首若干空白 + 单字母 + 空白 + 路径(路径可含空格,取整段)。 */
|
|
73
|
-
const UPDATE_LINE_RE = /^([
|
|
73
|
+
const UPDATE_LINE_RE = /^([A-Z_ ]{1,4})\s{2,}(\S.*)$/
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 解析 update 的单行状态(正文/摘要不匹配)。属性列 U/C 也按该条目状态处理。
|
|
77
|
+
* @author ddj 2026年09月24号
|
|
78
|
+
* @param line 原始输出行
|
|
79
|
+
* @returns 可识别条目或 null
|
|
80
|
+
*/
|
|
81
|
+
export function updateLineOf(line: string): SvnUpdateEntry | null {
|
|
82
|
+
const hit = UPDATE_LINE_RE.exec(line)
|
|
83
|
+
if (!hit) return null
|
|
84
|
+
const status = hit[1].replace(/[ _]/g, '')
|
|
85
|
+
if (!status || /[^ADGCEUR]/.test(status)) return null
|
|
86
|
+
const action = status.includes('C') ? 'C' : status[0]
|
|
87
|
+
const path = hit[2].trim()
|
|
88
|
+
if (!path) return null
|
|
89
|
+
return { action: action as SvnUpdateEntry['action'], path, label: UPDATE_ACTION_LABEL[action] ?? action }
|
|
90
|
+
}
|
|
74
91
|
|
|
75
92
|
/**
|
|
76
93
|
* 解析 `svn update` 文本输出为**条目级**结构化结果(附冲突清单与一行摘要)。
|
|
@@ -95,18 +112,10 @@ export function updateResultOf(stdout: string, stderr: string, code: number | nu
|
|
|
95
112
|
const conflicts: string[] = []
|
|
96
113
|
for (const line of output.split(/\r?\n/)) {
|
|
97
114
|
// 条目行(`U a.txt` / `C conf.txt` / `A new.txt`)
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (path && entries.length < TEXT_ENTRY_CAP) {
|
|
103
|
-
entries.push({
|
|
104
|
-
action: action as SvnUpdateEntry['action'],
|
|
105
|
-
path,
|
|
106
|
-
label: UPDATE_ACTION_LABEL[action] ?? action,
|
|
107
|
-
})
|
|
108
|
-
}
|
|
109
|
-
if (action === 'C' && conflicts.length < TEXT_CONFLICT_CAP) conflicts.push(path)
|
|
115
|
+
const entry = updateLineOf(line)
|
|
116
|
+
if (entry) {
|
|
117
|
+
if (entries.length < TEXT_ENTRY_CAP) entries.push(entry)
|
|
118
|
+
if (entry.action === 'C' && conflicts.length < TEXT_CONFLICT_CAP) conflicts.push(entry.path)
|
|
110
119
|
continue
|
|
111
120
|
}
|
|
112
121
|
// 兼容 P1 的宽松口径:属性列冲突(` M C f` 等)也算冲突
|