dsh-vscode-mode 0.14.5 → 0.14.7
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 +6 -4
- package/lib/client.js +969 -166
- package/lib/client.js.map +1 -1
- package/lib/index.js +980 -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/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/state/editorFindState.ts +102 -0
- package/src/client/state/editorViewState.ts +56 -0
- package/src/client/styles/editor.css +12 -0
- package/src/client/svnNotice.ts +24 -0
- package/src/client/ui/EditorView.ts +27 -50
- package/src/client/ui/SvnOverlay.ts +24 -0
- package/src/dap/manager.ts +65 -17
- package/src/model.ts +112 -1
- package/src/rpc.ts +2 -0
- 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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/** Bounded SVN XML framing; retain statuses, never the entire verbose document. @author ddj 2026年10月09号 */
|
|
2
|
+
import { StringDecoder } from 'node:string_decoder'
|
|
3
|
+
import type { Readable } from 'node:stream'
|
|
4
|
+
import { attrOf } from './svnXml.js'
|
|
5
|
+
import type { SvnChangeEntry } from './shared/svn.js'
|
|
6
|
+
|
|
7
|
+
export interface StatusPipe {
|
|
8
|
+
stdout?: Readable | null
|
|
9
|
+
done: Promise<{ exitCode?: number | null; code?: number | null }>
|
|
10
|
+
terminate?: () => unknown
|
|
11
|
+
collected?: { stderr?: { readFrom: (offset: number) => { text: string } } }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** @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. */
|
|
15
|
+
export async function readStatusPipe(handle: StatusPipe, parse: StatusParser, cap: number, timeout: number, signal?: AbortSignal) {
|
|
16
|
+
const reader = new SvnStatusReader(parse, cap)
|
|
17
|
+
/** @private @author ddj 2026年10月09号 Consume raw chunks through stream EOF, not just direct process exit. */
|
|
18
|
+
async function consume(): Promise<void> {
|
|
19
|
+
if (!handle.stdout) throw new Error('SVN 状态管道不可用,未确认正常状态')
|
|
20
|
+
for await (const chunk of handle.stdout) reader.push(chunk)
|
|
21
|
+
}
|
|
22
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
23
|
+
const expired = new Promise<never>((_, reject) => {
|
|
24
|
+
timer = setTimeout(() => reject(new Error('读取 SVN 图标状态超时')), timeout)
|
|
25
|
+
timer.unref?.()
|
|
26
|
+
})
|
|
27
|
+
let abort: (() => void) | undefined
|
|
28
|
+
const cancelled = new Promise<never>((_, reject) => {
|
|
29
|
+
abort = () => reject(signal?.reason ?? new Error('SVN 状态扫描已取消'))
|
|
30
|
+
if (signal?.aborted) abort()
|
|
31
|
+
else signal?.addEventListener('abort', abort, { once: true })
|
|
32
|
+
})
|
|
33
|
+
try {
|
|
34
|
+
const [outcome] = await Promise.race([Promise.all([handle.done, consume()]), expired, cancelled])
|
|
35
|
+
if ((outcome.exitCode ?? outcome.code) !== 0) throw new Error(handle.collected?.stderr?.readFrom(0).text || 'SVN 状态命令失败')
|
|
36
|
+
return reader.finish()
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// Keep managed teardown observable but do not mask the original failure.
|
|
39
|
+
try { await handle.terminate?.() } catch { /* Preserve CLI/stream failure. */ }
|
|
40
|
+
throw error
|
|
41
|
+
} finally {
|
|
42
|
+
if (timer) clearTimeout(timer)
|
|
43
|
+
if (abort) signal?.removeEventListener('abort', abort)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type StatusParser = (xml: string, cap: number) => { entries: SvnChangeEntry[]; truncated: boolean }
|
|
48
|
+
const TAG_CAP = 64 * 1024
|
|
49
|
+
const STREAM_CAP = 512 * 1024 * 1024
|
|
50
|
+
|
|
51
|
+
/** @public @author ddj 2026年10月09号 Validates framing through EOF, including entries beyond the retained cap. */
|
|
52
|
+
export class SvnStatusReader {
|
|
53
|
+
private readonly decoder = new StringDecoder('utf8')
|
|
54
|
+
private buffer = ''
|
|
55
|
+
private bytes = 0
|
|
56
|
+
private readonly stack: string[] = []
|
|
57
|
+
private opened = false
|
|
58
|
+
private closed = false
|
|
59
|
+
private entry = ''
|
|
60
|
+
private changelist = ''
|
|
61
|
+
private readonly entries: SvnChangeEntry[] = []
|
|
62
|
+
private truncated = false
|
|
63
|
+
|
|
64
|
+
/** @public @author ddj 2026年10月09号 @param parse Existing status semantics. @param cap Retained entry bound. */
|
|
65
|
+
constructor(private readonly parse: StatusParser, private readonly cap: number) {}
|
|
66
|
+
|
|
67
|
+
/** @public @author ddj 2026年10月09号 @param chunk Raw pipe chunk. @throws Byte/tag limits or invalid framing. */
|
|
68
|
+
push(chunk: Buffer | string): void {
|
|
69
|
+
this.bytes += Buffer.byteLength(chunk)
|
|
70
|
+
if (this.bytes > STREAM_CAP) throw new Error('SVN 状态流超过 512 MiB 安全上限')
|
|
71
|
+
this.feed(typeof chunk === 'string' ? chunk : this.decoder.write(chunk))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @private @author ddj 2026年10月09号 @param text Decoded chunk. Process each complete tag once; partial tags stay bounded. */
|
|
75
|
+
private feed(text: string): void {
|
|
76
|
+
this.buffer += text
|
|
77
|
+
let cursor = 0
|
|
78
|
+
while (cursor < this.buffer.length) {
|
|
79
|
+
const start = this.buffer.indexOf('<', cursor)
|
|
80
|
+
const end = start < 0 ? -1 : this.buffer.indexOf('>', start)
|
|
81
|
+
const tail = start < 0 ? this.buffer.length : start
|
|
82
|
+
if (!this.stack.length && this.buffer.slice(cursor, tail).trim()) this.invalid()
|
|
83
|
+
if (end < 0) { cursor = tail; break }
|
|
84
|
+
if (end - start > TAG_CAP) this.invalid()
|
|
85
|
+
this.tag(this.buffer.slice(start, end + 1))
|
|
86
|
+
cursor = end + 1
|
|
87
|
+
}
|
|
88
|
+
this.buffer = this.buffer.slice(cursor)
|
|
89
|
+
if (this.buffer.length > TAG_CAP) this.invalid()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** @private @author ddj 2026年10月09号 @param token Complete XML tag. Validate nesting and capture only entry/wc-status attributes. */
|
|
93
|
+
private tag(token: string): void {
|
|
94
|
+
if (token.startsWith('<?xml ') && !this.opened && !this.stack.length) return
|
|
95
|
+
const match = /^<(\/?)([\w-]+)\b([^>]*?)(\/?)>$/.exec(token)
|
|
96
|
+
if (!match || this.closed) this.invalid()
|
|
97
|
+
const [, closing, name, attrs, self] = match!
|
|
98
|
+
if (closing) {
|
|
99
|
+
if (this.stack.pop() !== name) this.invalid()
|
|
100
|
+
if (name === 'entry') this.saveEntry()
|
|
101
|
+
if (name === 'changelist') this.changelist = ''
|
|
102
|
+
if (name === 'status') this.closed = true
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
if (!this.stack.length) {
|
|
106
|
+
if (name !== 'status' || this.opened || self) this.invalid()
|
|
107
|
+
this.opened = true
|
|
108
|
+
}
|
|
109
|
+
if (name === 'changelist') this.changelist = attrOf(attrs, 'name')
|
|
110
|
+
if (name === 'entry') this.entry = token.replace(/\/>$/, '>')
|
|
111
|
+
if (name === 'wc-status' && this.entry) this.entry += '<wc-status' + attrs + '/>'
|
|
112
|
+
if (!self) this.stack.push(name)
|
|
113
|
+
else if (name === 'entry') this.saveEntry()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** @private @author ddj 2026年10月09号 Preserve existing property/tree-conflict/path semantics and bounded statuses. */
|
|
117
|
+
private saveEntry(): void {
|
|
118
|
+
const result = this.parse(this.entry + '</entry>', 1)
|
|
119
|
+
this.entry = ''
|
|
120
|
+
for (const entry of result.entries) {
|
|
121
|
+
if (this.entries.length >= this.cap) { this.truncated = true; continue }
|
|
122
|
+
this.entries.push(this.changelist ? { ...entry, changelist: this.changelist } : entry)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** @private @author ddj 2026年10月09号 @throws Invalid/missing/truncated XML; never produces a clean snapshot. */
|
|
127
|
+
private invalid(): never { throw new Error('SVN 状态输出为空或不完整,未确认正常状态') }
|
|
128
|
+
|
|
129
|
+
/** @public @author ddj 2026年10月09号 @returns Confirmed snapshot only after complete stream EOF. @throws Invalid framing. */
|
|
130
|
+
finish(): { entries: SvnChangeEntry[]; truncated: boolean } {
|
|
131
|
+
this.feed(this.decoder.end())
|
|
132
|
+
if (!this.opened || !this.closed || this.stack.length || this.buffer.trim()) this.invalid()
|
|
133
|
+
return { entries: this.entries, truncated: this.truncated }
|
|
134
|
+
}
|
|
135
|
+
}
|