dsh-vscode-mode 0.21.3 → 0.22.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.
@@ -0,0 +1,241 @@
1
+ /**
2
+ * 待审差异构造:把两侧全文变成官方视图复用的 hunk,并提供块级 Keep/Revert 的文本合成。
3
+ * @author ddj 2026年10月29号
4
+ */
5
+ import type { OfficialDiff, ReviewHunk } from '../shared/review.js'
6
+ import type { ReviewBlock } from '../shared/reviewRegions.js'
7
+ import { logicalLines } from '../shared/lineEndings.js'
8
+
9
+ /** 每个 hunk 的上下文行数。 */
10
+ const CONTEXT = 3
11
+ /** LCS 单元格上限:超过后退化为整段替换,避免大文件卡死。 */
12
+ const CELL_LIMIT = 1_000_000
13
+
14
+ /** 一行差异操作。 */
15
+ type LineOp = { kind: ' ' | '-' | '+'; text: string }
16
+
17
+ /**
18
+ * @author ddj 2026年10月29号
19
+ * @description: 构造可直接交给官方渲染器的差异;两侧完全相同时返回空 hunk 列表。
20
+ * @param path 相对路径
21
+ * @param display 展示名
22
+ * @param oldText 基线全文
23
+ * @param newText 当前全文
24
+ * @returns 文本差异
25
+ */
26
+ export function textDiff(path: string, display: string, oldText: string, newText: string): OfficialDiff {
27
+ const hunks = buildHunks(logicalLines(oldText), logicalLines(newText))
28
+ return { kind: 'text', path, display, before: true, after: true, hunks, coarse: hunks.length > 0 && isCoarse(oldText, newText) }
29
+ }
30
+
31
+ /**
32
+ * @author ddj 2026年10月29号
33
+ * @description: 块级 Keep:把该块的新内容写进基线,返回推进后的基线。
34
+ * @param oldText 当前基线
35
+ * @param block 目标块
36
+ * @returns 新基线;块在当前基线中无法唯一定位时返回 null
37
+ */
38
+ export function keepBlock(oldText: string, block: ReviewBlock): string | null {
39
+ return spliceBlock(oldText, block.oldLines, block.newLines, block)
40
+ }
41
+
42
+ /**
43
+ * @author ddj 2026年10月29号
44
+ * @description: 块级 Revert:把该块在最新内容里换回基线内容,返回换回后的内容。
45
+ * @param newText 当前内容
46
+ * @param block 目标块
47
+ * @returns 新内容;块无法唯一定位时返回 null
48
+ */
49
+ export function revertBlock(newText: string, block: ReviewBlock): string | null {
50
+ return spliceBlock(newText, block.newLines, block.oldLines, block)
51
+ }
52
+
53
+ /**
54
+ * @author ddj 2026年10月29号
55
+ * @description: 按"上下文 + 目标行"唯一定位后做一次整块替换,定位不唯一就不动。
56
+ * @param content 目标全文
57
+ * @param from 要被替换的块内行
58
+ * @param to 替换成的块内行
59
+ * @param block 来源块,提供上下文;缺上下文时退化为只看块内行
60
+ * @returns 新全文或 null
61
+ */
62
+ function spliceBlock(content: string, from: string[], to: string[], block: ReviewBlock): string | null {
63
+ if (from.length === 0 && to.length === 0) return null
64
+ const lines = logicalLines(content)
65
+ const withContext = locateSpan(lines, [...block.contextBefore, ...from, ...block.contextAfter])
66
+ if (withContext !== null) {
67
+ const start = withContext + block.contextBefore.length
68
+ return spliceAt(lines, start, from.length, to)
69
+ }
70
+ const bare = locateSpan(lines, from)
71
+ return bare === null ? null : spliceAt(lines, bare, from.length, to)
72
+ }
73
+
74
+ /**
75
+ * @author ddj 2026年10月29号
76
+ * @description: 在指定偏移处把连续若干行换成另一组行。
77
+ * @param lines 目标行集合
78
+ * @param start 起始偏移
79
+ * @param count 被替换行数
80
+ * @param to 替换成的行
81
+ * @returns 新全文
82
+ */
83
+ function spliceAt(lines: string[], start: number, count: number, to: string[]): string {
84
+ return [...lines.slice(0, start), ...to, ...lines.slice(start + count)].join('\n')
85
+ }
86
+
87
+ /**
88
+ * @author ddj 2026年10月29号
89
+ * @description: 唯一定位一段行;出现多处命中即视为不可定位。
90
+ * @param lines 目标行集合
91
+ * @param pattern 待定位行
92
+ * @returns 唯一偏移或 null
93
+ */
94
+ function locateSpan(lines: string[], pattern: string[]): number | null {
95
+ if (pattern.length === 0) return null
96
+ let at = -1
97
+ for (let offset = 0; offset + pattern.length <= lines.length; offset++) {
98
+ if (!pattern.every((text, index) => lines[offset + index] === text)) continue
99
+ if (at >= 0) return null
100
+ at = offset
101
+ }
102
+ return at >= 0 ? at : null
103
+ }
104
+
105
+ /**
106
+ * @author ddj 2026年10月29号
107
+ * @description: 生成带上下文的标准 hunk 列表。
108
+ * @param oldLines 基线行
109
+ * @param newLines 当前行
110
+ * @returns hunk 列表
111
+ */
112
+ function buildHunks(oldLines: string[], newLines: string[]): ReviewHunk[] {
113
+ const ops = diffLines(oldLines, newLines)
114
+ const hunks: ReviewHunk[] = []
115
+ let index = 0
116
+ let oldLine = 0
117
+ let newLine = 0
118
+ while (index < ops.length) {
119
+ if (ops[index].kind === ' ') {
120
+ oldLine++
121
+ newLine++
122
+ index++
123
+ continue
124
+ }
125
+ const head = ops.slice(Math.max(0, index - CONTEXT), index)
126
+ const collected: LineOp[] = []
127
+ let probe = index
128
+ while (probe < ops.length) {
129
+ const op = ops[probe]
130
+ if (op.kind !== ' ') { collected.push(op); probe++; continue }
131
+ const rest = ops.slice(probe, probe + CONTEXT * 2 + 1)
132
+ if (rest.some((item) => item.kind !== ' ')) { collected.push(op); probe++; continue }
133
+ break
134
+ }
135
+ const tail = ops.slice(probe, probe + CONTEXT)
136
+ const oldStartLine = oldLine - head.length + 1
137
+ const newStartLine = newLine - head.length + 1
138
+ hunks.push(makeHunk([...head, ...collected, ...tail], oldStartLine, newStartLine))
139
+ for (const op of collected) {
140
+ if (op.kind !== '+') oldLine++
141
+ if (op.kind !== '-') newLine++
142
+ }
143
+ oldLine += head.length + tail.length
144
+ newLine += head.length + tail.length
145
+ index = probe + tail.length
146
+ }
147
+ return hunks
148
+ }
149
+
150
+ /**
151
+ * @author ddj 2026年10月29号
152
+ * @description: 由一段操作装配 hunk。
153
+ * @param ops 该 hunk 的操作
154
+ * @param oldStart 基线起始行
155
+ * @param newStart 当前起始行
156
+ * @returns hunk
157
+ */
158
+ function makeHunk(ops: LineOp[], oldStart: number, newStart: number): ReviewHunk {
159
+ const oldCount = ops.filter((op) => op.kind !== '+').length
160
+ const newCount = ops.filter((op) => op.kind !== '-').length
161
+ return { oldStart, oldLines: oldCount, newStart, newLines: newCount, lines: ops.map((op) => op.kind + op.text) }
162
+ }
163
+
164
+ /**
165
+ * @author ddj 2026年10月29号
166
+ * @description: 行级差异:先剪公共前后缀,剩余部分在限额内做 LCS,超限按整段替换。
167
+ * @param oldLines 基线行
168
+ * @param newLines 当前行
169
+ * @returns 顺序完整的操作列表
170
+ */
171
+ function diffLines(oldLines: string[], newLines: string[]): LineOp[] {
172
+ let head = 0
173
+ while (head < oldLines.length && head < newLines.length && oldLines[head] === newLines[head]) head++
174
+ let tail = 0
175
+ while (tail < oldLines.length - head && tail < newLines.length - head
176
+ && oldLines[oldLines.length - 1 - tail] === newLines[newLines.length - 1 - tail]) tail++
177
+ const left = oldLines.slice(head, oldLines.length - tail)
178
+ const right = newLines.slice(head, newLines.length - tail)
179
+ const middle = left.length * right.length > CELL_LIMIT ? coarseOps(left, right) : lcsOps(left, right)
180
+ return [
181
+ ...oldLines.slice(0, head).map((text) => ({ kind: ' ' as const, text })),
182
+ ...middle,
183
+ ...oldLines.slice(oldLines.length - tail).map((text) => ({ kind: ' ' as const, text })),
184
+ ]
185
+ }
186
+
187
+ /**
188
+ * @author ddj 2026年10月29号
189
+ * @description: 超限时的退化口径:整段删除再整段新增,绝不谎报逐行对应。
190
+ * @param left 基线剩余行
191
+ * @param right 当前剩余行
192
+ * @returns 操作列表
193
+ */
194
+ function coarseOps(left: string[], right: string[]): LineOp[] {
195
+ return [
196
+ ...left.map((text) => ({ kind: '-' as const, text })),
197
+ ...right.map((text) => ({ kind: '+' as const, text })),
198
+ ]
199
+ }
200
+
201
+ /**
202
+ * @author ddj 2026年10月29号
203
+ * @description: 限额内的 LCS 行差异。
204
+ * @param left 基线剩余行
205
+ * @param right 当前剩余行
206
+ * @returns 操作列表
207
+ */
208
+ function lcsOps(left: string[], right: string[]): LineOp[] {
209
+ const rows = left.length + 1
210
+ const cols = right.length + 1
211
+ const table = new Uint32Array(rows * cols)
212
+ for (let i = left.length - 1; i >= 0; i--) {
213
+ for (let j = right.length - 1; j >= 0; j--) {
214
+ table[i * cols + j] = left[i] === right[j]
215
+ ? table[(i + 1) * cols + j + 1] + 1
216
+ : Math.max(table[(i + 1) * cols + j], table[i * cols + j + 1])
217
+ }
218
+ }
219
+ const ops: LineOp[] = []
220
+ let i = 0
221
+ let j = 0
222
+ while (i < left.length && j < right.length) {
223
+ if (left[i] === right[j]) { ops.push({ kind: ' ', text: left[i] }); i++; j++; continue }
224
+ if (table[(i + 1) * cols + j] >= table[i * cols + j + 1]) { ops.push({ kind: '-', text: left[i] }); i++ }
225
+ else { ops.push({ kind: '+', text: right[j] }); j++ }
226
+ }
227
+ while (i < left.length) ops.push({ kind: '-', text: left[i++] })
228
+ while (j < right.length) ops.push({ kind: '+', text: right[j++] })
229
+ return ops
230
+ }
231
+
232
+ /**
233
+ * @author ddj 2026年10月29号
234
+ * @description: 判断这次差异是否触发了大文件退化口径。
235
+ * @param oldText 基线全文
236
+ * @param newText 当前全文
237
+ * @returns 是否粗粒度
238
+ */
239
+ function isCoarse(oldText: string, newText: string): boolean {
240
+ return logicalLines(oldText).length * logicalLines(newText).length > CELL_LIMIT
241
+ }
@@ -0,0 +1,233 @@
1
+ /**
2
+ * 待审条目内核:纯状态机,逐条对齐 dsh-diff-approval 的 pending 语义。
3
+ * 无 IO、无时钟、不改入参;调用方传入 now 与内容,便于测试与复用。
4
+ * @author ddj 2026年10月29号
5
+ */
6
+ import type { PendingEarlier, PendingLineage } from '../shared/pendingReview.js'
7
+
8
+ /** 内存态条目:比展示投影多保存两侧全文。 */
9
+ export interface PendingEntry {
10
+ id: string
11
+ path: string
12
+ earlierVersion: PendingEarlier
13
+ oldText: string
14
+ newText: string
15
+ updatedAt: number
16
+ unseen?: boolean
17
+ sessionId: string
18
+ sessionIds: string[]
19
+ lineage?: PendingLineage
20
+ }
21
+
22
+ /** 新建条目的入参。 */
23
+ export interface NewPending {
24
+ id: string
25
+ path: string
26
+ sessionId: string
27
+ earlierVersion: PendingEarlier
28
+ oldText: string
29
+ newText: string
30
+ updatedAt: number
31
+ lineage?: PendingLineage
32
+ }
33
+
34
+ /** 决定后对条目的局部推进;未出现的字段表示不改。 */
35
+ export interface PendingPatch {
36
+ oldText?: string
37
+ newText?: string
38
+ }
39
+
40
+ /**
41
+ * @author ddj 2026年10月29号
42
+ * @description: 严格比较两侧文本:不做 BOM/行尾归一化,归一化只允许用于展示。
43
+ * @param left 一侧全文
44
+ * @param right 另一侧全文
45
+ * @returns 两者是否逐码元相等
46
+ */
47
+ export function sameEntryText(left: string, right: string): boolean {
48
+ return left === right
49
+ }
50
+
51
+ /**
52
+ * @author ddj 2026年10月29号
53
+ * @description: 净零判定:基线等于当前内容即为已解决,但条目仍留在列表等待移除。
54
+ * @param entry 待审条目
55
+ * @returns 是否已无净差异
56
+ */
57
+ export function isSettled(entry: PendingEntry): boolean {
58
+ return sameEntryText(entry.oldText, entry.newText)
59
+ }
60
+
61
+ /**
62
+ * @author ddj 2026年10月29号
63
+ * @description: 合并同一身份的两条条目:基线取较早来源,最新内容取较晚来源,会话取并集。
64
+ * @param earlier 较早记录
65
+ * @param later 较晚记录
66
+ * @returns 合并后的新条目
67
+ */
68
+ export function mergeEntries(earlier: PendingEntry, later: PendingEntry): PendingEntry {
69
+ const merged: PendingEntry = {
70
+ ...later,
71
+ updatedAt: later.updatedAt,
72
+ sessionIds: unionSessions(earlier, later),
73
+ }
74
+ if (later.lineage === undefined && earlier.lineage !== undefined) merged.lineage = earlier.lineage
75
+ if (earlier.earlierVersion === 'none' && later.earlierVersion === 'none') {
76
+ merged.earlierVersion = 'none'
77
+ merged.oldText = earlier.oldText
78
+ return merged
79
+ }
80
+ // 基线一旦有历史版本,Revert 就是写回内容而不是删除文件;创建期的内容成为新基线。
81
+ merged.earlierVersion = 'file'
82
+ merged.oldText = earlier.earlierVersion === 'none' ? earlier.newText : earlier.oldText
83
+ return merged
84
+ }
85
+
86
+ /**
87
+ * @author ddj 2026年10月29号
88
+ * @description: 由新建入参构造条目,并把首个会话写进会话集合。
89
+ * @param input 新建入参
90
+ * @returns 新条目
91
+ */
92
+ export function entryFromNew(input: NewPending): PendingEntry {
93
+ const entry: PendingEntry = {
94
+ id: input.id,
95
+ path: input.path,
96
+ earlierVersion: input.earlierVersion,
97
+ oldText: input.oldText,
98
+ newText: input.newText,
99
+ updatedAt: input.updatedAt,
100
+ sessionId: input.sessionId,
101
+ sessionIds: [input.sessionId],
102
+ }
103
+ if (input.lineage !== undefined) entry.lineage = input.lineage
104
+ return entry
105
+ }
106
+
107
+ /**
108
+ * @author ddj 2026年10月29号
109
+ * @description: 应用一次决定带来的局部推进:块级 Keep 推进基线,块级 Revert 推进当前内容。
110
+ * @param entry 待审条目
111
+ * @param patch 要推进的一侧
112
+ * @param now 本次更新时间
113
+ * @returns 新条目;基线被推进时"创建"身份结束
114
+ */
115
+ export function applyUpdate(entry: PendingEntry, patch: PendingPatch, now: number): PendingEntry {
116
+ const next: PendingEntry = { ...entry, updatedAt: now }
117
+ if (patch.oldText !== undefined) {
118
+ next.oldText = patch.oldText
119
+ next.earlierVersion = 'file'
120
+ }
121
+ if (patch.newText !== undefined) next.newText = patch.newText
122
+ delete next.unseen
123
+ return next
124
+ }
125
+
126
+ /**
127
+ * @author ddj 2026年10月29号
128
+ * @description: 采纳外部改动:只替换当前内容并标记未读,基线保持不动。
129
+ * @param entry 待审条目
130
+ * @param content 磁盘上的最新全文
131
+ * @param now 本次更新时间
132
+ * @returns 新条目
133
+ */
134
+ export function adoptExternal(entry: PendingEntry, content: string, now: number): PendingEntry {
135
+ return { ...entry, newText: content, updatedAt: now, unseen: true }
136
+ }
137
+
138
+ /**
139
+ * @author ddj 2026年10月29号
140
+ * @description: 设置或清除未读圆点。
141
+ * @param entry 待审条目
142
+ * @param seen 是否已读
143
+ * @returns 新条目
144
+ */
145
+ export function touchSeen(entry: PendingEntry, seen: boolean): PendingEntry {
146
+ const next: PendingEntry = { ...entry }
147
+ if (seen) delete next.unseen
148
+ else next.unseen = true
149
+ return next
150
+ }
151
+
152
+ /**
153
+ * @author ddj 2026年10月29号
154
+ * @description: 撤销重放:条目内容回滚到快照,未读标记保留实时值。
155
+ * @param entry 快照条目
156
+ * @param live 实时条目,用于取未读标记
157
+ * @returns 重放后的新条目
158
+ */
159
+ export function withUnseen(entry: PendingEntry, live: PendingEntry | undefined): PendingEntry {
160
+ return touchSeen(entry, live ? live.unseen !== true : entry.unseen !== true)
161
+ }
162
+
163
+ /**
164
+ * @author ddj 2026年10月29号
165
+ * @description: 列表排序:按更新时间升序,与上游一致。
166
+ * @param left 左侧条目
167
+ * @param right 右侧条目
168
+ * @returns 比较结果
169
+ */
170
+ export function byUpdatedAt(left: PendingEntry, right: PendingEntry): number {
171
+ return left.updatedAt - right.updatedAt
172
+ }
173
+
174
+ /**
175
+ * @author ddj 2026年10月29号
176
+ * @description: 合并两条条目的会话集合并去重,且保证首会话在列。
177
+ * @param earlier 较早记录
178
+ * @param later 较晚记录
179
+ * @returns 去重后的会话集合
180
+ */
181
+ function unionSessions(earlier: PendingEntry, later: PendingEntry): string[] {
182
+ const merged: string[] = []
183
+ for (const sessionId of [earlier.sessionId, ...earlier.sessionIds, later.sessionId, ...later.sessionIds]) {
184
+ if (sessionId && !merged.includes(sessionId)) merged.push(sessionId)
185
+ }
186
+ return merged
187
+ }
188
+
189
+ /**
190
+ * 内容版本号分配器:只有内容变化才递增,条目删除后编号不复用。
191
+ * @author ddj 2026年10月29号
192
+ */
193
+ export class ContentVersions {
194
+ /** 已分配编号与快照文本。 */
195
+ private readonly state = new Map<string, { version: number; text: string }>()
196
+ /** 下一个可用编号。 */
197
+ private next = 1
198
+
199
+ /**
200
+ * @author ddj 2026年10月29号
201
+ * @description: 记录条目当前内容并返回其版本号;内容未变时沿用旧号。
202
+ * @param id 条目身份
203
+ * @param text 当前内容
204
+ * @returns 该条目的版本号
205
+ */
206
+ note(id: string, text: string): number {
207
+ const known = this.state.get(id)
208
+ if (known && known.text === text) return known.version
209
+ const version = this.next++
210
+ this.state.set(id, { version, text })
211
+ return version
212
+ }
213
+
214
+ /**
215
+ * @author ddj 2026年10月29号
216
+ * @description: 查询已分配版本号,供评论锚定校验。
217
+ * @param id 条目身份
218
+ * @returns 版本号,未记录时为 undefined
219
+ */
220
+ versionOf(id: string): number | undefined {
221
+ return this.state.get(id)?.version
222
+ }
223
+
224
+ /**
225
+ * @author ddj 2026年10月29号
226
+ * @description: 条目离开列表时丢弃其版本号,且不回收编号。
227
+ * @param id 条目身份
228
+ * @returns 无
229
+ */
230
+ drop(id: string): void {
231
+ this.state.delete(id)
232
+ }
233
+ }