dsh-remote-vs 0.0.0-stage → 0.8.3

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.
Files changed (200) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +385 -0
  3. package/README.md +347 -2
  4. package/cordis.patch.yml +259 -0
  5. package/icon.svg +7 -0
  6. package/lib/client.js +62602 -0
  7. package/lib/index.js +4287 -0
  8. package/lib/p2-bridge-refs.js +10 -0
  9. package/lib/p2-bridge.js +16 -0
  10. package/lib/p2-router-fs.js +252 -0
  11. package/lib/p2-router-refs.js +466 -0
  12. package/lib/p2-router-spill.js +62 -0
  13. package/lib/p2-router-subprocess.js +64 -0
  14. package/lib/p2-shell-bash.js +24 -0
  15. package/lib/ssh-proc-arI5lm4W.js +632 -0
  16. package/lib/types/changes/recorder.d.ts +240 -0
  17. package/lib/types/changes/status.d.ts +201 -0
  18. package/lib/types/changes/text-diff.d.ts +65 -0
  19. package/lib/types/client/CodeEditor.d.ts +125 -0
  20. package/lib/types/client/Dialog.d.ts +14 -0
  21. package/lib/types/client/DirectoryFlow.d.ts +30 -0
  22. package/lib/types/client/EditorPane.d.ts +30 -0
  23. package/lib/types/client/HostsView.d.ts +10 -0
  24. package/lib/types/client/NativeExplorer.d.ts +39 -0
  25. package/lib/types/client/RemoteDirectoryPicker.d.ts +16 -0
  26. package/lib/types/client/RemoteEditorView.d.ts +20 -0
  27. package/lib/types/client/RemoteExplorer.d.ts +18 -0
  28. package/lib/types/client/RemoteRoot.d.ts +16 -0
  29. package/lib/types/client/SearchView.d.ts +14 -0
  30. package/lib/types/client/SettingsHosts.d.ts +15 -0
  31. package/lib/types/client/TerminalView.d.ts +16 -0
  32. package/lib/types/client/WorkspaceWizard.d.ts +16 -0
  33. package/lib/types/client/WorkspacesView.d.ts +14 -0
  34. package/lib/types/client/XTerm.d.ts +15 -0
  35. package/lib/types/client/api.d.ts +162 -0
  36. package/lib/types/client/changes/ChangesDock.d.ts +35 -0
  37. package/lib/types/client/changes/model.d.ts +36 -0
  38. package/lib/types/client/changes/undo-affordance.d.ts +48 -0
  39. package/lib/types/client/changes/undo-context.d.ts +25 -0
  40. package/lib/types/client/composer.d.ts +165 -0
  41. package/lib/types/client/i18n.d.ts +17 -0
  42. package/lib/types/client/index.d.ts +14 -0
  43. package/lib/types/client/remote-file-open-target.d.ts +247 -0
  44. package/lib/types/client/remote-file-open.d.ts +35 -0
  45. package/lib/types/client/reveal.d.ts +15 -0
  46. package/lib/types/client/scm/DiffView.d.ts +24 -0
  47. package/lib/types/client/scm/HistoryPanel.d.ts +12 -0
  48. package/lib/types/client/scm/RepoPicker.d.ts +24 -0
  49. package/lib/types/client/scm/ScmPanel.d.ts +15 -0
  50. package/lib/types/client/scm/actions.d.ts +170 -0
  51. package/lib/types/client/scm/diff-model.d.ts +29 -0
  52. package/lib/types/client/scm/stage-queue.d.ts +99 -0
  53. package/lib/types/client/scm/status-map.d.ts +157 -0
  54. package/lib/types/client/scm/untracked-dirs.d.ts +72 -0
  55. package/lib/types/client/selection-popup.d.ts +61 -0
  56. package/lib/types/client/store.d.ts +437 -0
  57. package/lib/types/client/theme.d.ts +15 -0
  58. package/lib/types/client/ui.d.ts +46 -0
  59. package/lib/types/client/upload-plan.d.ts +78 -0
  60. package/lib/types/client/upload-source.d.ts +46 -0
  61. package/lib/types/client/use-remote-chat-target.d.ts +17 -0
  62. package/lib/types/client/vsc/FindWidget.d.ts +81 -0
  63. package/lib/types/client/vsc/change-marks.d.ts +150 -0
  64. package/lib/types/client/vsc/editor-langs.d.ts +5 -0
  65. package/lib/types/client/vsc/editor-palette.d.ts +37 -0
  66. package/lib/types/client/vsc/editor-theme.d.ts +32 -0
  67. package/lib/types/client/vsc/fallback-highlight.d.ts +26 -0
  68. package/lib/types/client/vsc/kit.d.ts +195 -0
  69. package/lib/types/client/vsc/markdown.d.ts +11 -0
  70. package/lib/types/client/vsc/reveal-line.d.ts +38 -0
  71. package/lib/types/client/vsc/tokenize.d.ts +104 -0
  72. package/lib/types/deploy.d.ts +94 -0
  73. package/lib/types/fs-ops.d.ts +8 -0
  74. package/lib/types/git-format.d.ts +34 -0
  75. package/lib/types/git-ops.d.ts +183 -0
  76. package/lib/types/http.d.ts +14 -0
  77. package/lib/types/importers.d.ts +20 -0
  78. package/lib/types/index.d.ts +87 -0
  79. package/lib/types/keygen.d.ts +19 -0
  80. package/lib/types/loopback.d.ts +7 -0
  81. package/lib/types/p2/bridge-refs.d.ts +21 -0
  82. package/lib/types/p2/bridge.d.ts +24 -0
  83. package/lib/types/p2/file-refs.d.ts +73 -0
  84. package/lib/types/p2/helper-rpc.d.ts +63 -0
  85. package/lib/types/p2/in-flight-gate.d.ts +25 -0
  86. package/lib/types/p2/router-fs.d.ts +92 -0
  87. package/lib/types/p2/router-refs.d.ts +45 -0
  88. package/lib/types/p2/router-spill.d.ts +11 -0
  89. package/lib/types/p2/router-subprocess.d.ts +23 -0
  90. package/lib/types/p2/shell-bash.d.ts +16 -0
  91. package/lib/types/p2/ssh-proc.d.ts +22 -0
  92. package/lib/types/p2/worlds.d.ts +179 -0
  93. package/lib/types/paths.d.ts +12 -0
  94. package/lib/types/pool.d.ts +105 -0
  95. package/lib/types/protocol.d.ts +452 -0
  96. package/lib/types/routes.d.ts +27 -0
  97. package/lib/types/search-ops.d.ts +68 -0
  98. package/lib/types/sftp.d.ts +90 -0
  99. package/lib/types/store.d.ts +66 -0
  100. package/lib/types/terminal.d.ts +51 -0
  101. package/lib/types/timeouts.d.ts +83 -0
  102. package/lib/types/tools.d.ts +41 -0
  103. package/lib/worlds-PUumRIh7.js +18765 -0
  104. package/locale/en.json +6 -0
  105. package/locale/zh.json +6 -0
  106. package/package.json +189 -4
  107. package/src/changes/recorder.ts +902 -0
  108. package/src/changes/status.ts +448 -0
  109. package/src/changes/text-diff.ts +377 -0
  110. package/src/client/CodeEditor.tsx +923 -0
  111. package/src/client/Dialog.tsx +76 -0
  112. package/src/client/DirectoryFlow.tsx +431 -0
  113. package/src/client/EditorPane.tsx +512 -0
  114. package/src/client/HostsView.tsx +312 -0
  115. package/src/client/NativeExplorer.tsx +121 -0
  116. package/src/client/RemoteDirectoryPicker.tsx +157 -0
  117. package/src/client/RemoteEditorView.tsx +54 -0
  118. package/src/client/RemoteExplorer.tsx +1398 -0
  119. package/src/client/RemoteRoot.tsx +389 -0
  120. package/src/client/SearchView.tsx +407 -0
  121. package/src/client/SettingsHosts.tsx +136 -0
  122. package/src/client/TerminalView.tsx +308 -0
  123. package/src/client/WorkspaceWizard.tsx +199 -0
  124. package/src/client/WorkspacesView.tsx +93 -0
  125. package/src/client/XTerm.tsx +308 -0
  126. package/src/client/api.ts +331 -0
  127. package/src/client/changes/ChangesDock.tsx +348 -0
  128. package/src/client/changes/changes.css +230 -0
  129. package/src/client/changes/model.ts +347 -0
  130. package/src/client/changes/undo-affordance.ts +389 -0
  131. package/src/client/changes/undo-context.ts +34 -0
  132. package/src/client/composer.ts +436 -0
  133. package/src/client/css.d.ts +7 -0
  134. package/src/client/i18n.ts +702 -0
  135. package/src/client/index.tsx +286 -0
  136. package/src/client/remote-file-open-target.ts +553 -0
  137. package/src/client/remote-file-open.ts +261 -0
  138. package/src/client/reveal.ts +33 -0
  139. package/src/client/scm/DiffView.tsx +570 -0
  140. package/src/client/scm/HistoryPanel.tsx +353 -0
  141. package/src/client/scm/RepoPicker.tsx +238 -0
  142. package/src/client/scm/ScmPanel.tsx +857 -0
  143. package/src/client/scm/actions.ts +460 -0
  144. package/src/client/scm/diff-model.ts +206 -0
  145. package/src/client/scm/stage-queue.ts +264 -0
  146. package/src/client/scm/status-map.ts +381 -0
  147. package/src/client/scm/untracked-dirs.ts +139 -0
  148. package/src/client/selection-popup.ts +157 -0
  149. package/src/client/store.tsx +1670 -0
  150. package/src/client/theme.ts +54 -0
  151. package/src/client/ui.tsx +136 -0
  152. package/src/client/upload-plan.ts +153 -0
  153. package/src/client/upload-source.ts +145 -0
  154. package/src/client/use-remote-chat-target.ts +153 -0
  155. package/src/client/vsc/FindWidget.tsx +446 -0
  156. package/src/client/vsc/change-marks.ts +586 -0
  157. package/src/client/vsc/editor-langs.ts +137 -0
  158. package/src/client/vsc/editor-palette.ts +107 -0
  159. package/src/client/vsc/editor-theme.ts +93 -0
  160. package/src/client/vsc/fallback-highlight.ts +145 -0
  161. package/src/client/vsc/kit.tsx +777 -0
  162. package/src/client/vsc/markdown.tsx +172 -0
  163. package/src/client/vsc/reveal-line.ts +142 -0
  164. package/src/client/vsc/tokenize.ts +569 -0
  165. package/src/client/vsc/vsc.css +2669 -0
  166. package/src/deploy.ts +481 -0
  167. package/src/fs-ops.ts +57 -0
  168. package/src/git-format.ts +205 -0
  169. package/src/git-ops.ts +693 -0
  170. package/src/http.ts +55 -0
  171. package/src/importers.ts +141 -0
  172. package/src/index.ts +442 -0
  173. package/src/keygen.ts +77 -0
  174. package/src/loopback.ts +47 -0
  175. package/src/p2/bridge-refs.ts +27 -0
  176. package/src/p2/bridge.ts +32 -0
  177. package/src/p2/file-refs.ts +392 -0
  178. package/src/p2/helper-rpc.ts +394 -0
  179. package/src/p2/in-flight-gate.ts +63 -0
  180. package/src/p2/router-fs.ts +327 -0
  181. package/src/p2/router-refs.ts +166 -0
  182. package/src/p2/router-spill.ts +75 -0
  183. package/src/p2/router-subprocess.ts +92 -0
  184. package/src/p2/shell-bash.ts +30 -0
  185. package/src/p2/ssh-proc.ts +802 -0
  186. package/src/p2/worlds.ts +735 -0
  187. package/src/paths.ts +50 -0
  188. package/src/pool.ts +811 -0
  189. package/src/protocol.ts +460 -0
  190. package/src/routes.ts +802 -0
  191. package/src/search-ops.ts +220 -0
  192. package/src/sftp.ts +677 -0
  193. package/src/store.ts +364 -0
  194. package/src/terminal.ts +291 -0
  195. package/src/timeouts.ts +122 -0
  196. package/src/tools.ts +320 -0
  197. package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.helper.json +16 -0
  198. package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.tar.gz +4 -0
  199. package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.tar.gz.sha256 +1 -0
  200. package/vendor/dsh-remote-helpers/glibc-ceiling.patch +31 -0
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Turn a file's unified diff into what the editor needs to draw it.
3
+ *
4
+ * The recorder speaks git (`UnifiedHunk`: `@@` headers plus raw ` `/`+`/`-`
5
+ * lines); the editor speaks line ranges (`ChangeMark`). This module is the one
6
+ * place that translates, and it is deliberately pure so the mapping can be
7
+ * asserted without a browser.
8
+ *
9
+ * Two kinds of information come out:
10
+ * - `marks`: line ranges to paint (add / modified / deleted-anchor). A modified
11
+ * range may also carry the **inline** detail — which characters of a line
12
+ * actually changed (`mark.inline`) and which ones disappeared from it
13
+ * (`mark.removed`). "This whole line is blue" cannot answer "what did this
14
+ * edit do" for a one-word change, which is what the inline half is for.
15
+ * - `hunks`: one entry per diff hunk, the line to jump to for "next change".
16
+ *
17
+ * The inline half is a **bounded heuristic**, not a diff library: pairs are
18
+ * matched by position, the common prefix/suffix of a pair is stripped first
19
+ * (that alone covers most real edits) and only what is left goes through a
20
+ * character LCS. Every step has a cap (see the `MAX_*` constants below): a
21
+ * pathological file must end up with a coarser highlight, never with a frozen
22
+ * editor. Nothing here imports CodeMirror — the checks run it under plain Node.
23
+ */
24
+
25
+ import type { UnifiedHunk } from '../../protocol.ts'
26
+ // One definition of the editor's mark vocabulary: the CodeMirror module owns
27
+ // it, this module only produces it. A type-only import keeps this file free of
28
+ // a CodeMirror runtime dependency, which is what lets the pure checks run it
29
+ // under plain Node.
30
+ import type { ChangeMark, ChangeSet, InlineRange, RemovedChip } from '../vsc/change-marks.ts'
31
+
32
+ export type { ChangeMark, ChangeSet, InlineRange, RemovedChip }
33
+
34
+ /** An empty set (a file that ended up unchanged). */
35
+ export const EMPTY_CHANGE_SET: ChangeSet = { marks: [], hunks: [], stamp: 'empty' }
36
+
37
+ /**
38
+ * 行内计算的上限。四条都是"宁可少画,不可卡死":
39
+ *
40
+ * - `MAX_INLINE_LINE`:单行超过这么长就不做字符级比较(压缩过的 bundle、base64
41
+ * 常量都是这种行,LCS 的收益为零而代价是平方级的)。整行标记照旧。
42
+ * - `MAX_PAIRS_PER_HUNK`:一个 hunk 里配对数超过这么多就整块跳过行内计算(生成
43
+ * 代码被整体重排时,逐行 LCS 只会产出一屏噪声)。
44
+ * - `MAX_REFINE_CHARS`:剥掉前后缀后两个中段的**长度之和**超过这么多,就不做
45
+ * LCS 求精,整个中段算一个区间。
46
+ * - `MAX_CHIP_CHARS`:删除 chip 的文本上限,超出部分截掉(`truncated` 标记它)。
47
+ */
48
+ const MAX_INLINE_LINE = 2000
49
+ const MAX_PAIRS_PER_HUNK = 200
50
+ const MAX_REFINE_CHARS = 200
51
+ const MAX_CHIP_CHARS = 60
52
+
53
+ /**
54
+ * LCS 里连续匹配短于这么长就不算"这段没变"。
55
+ *
56
+ * 字符级 LCS 会把巧合的单字符当成未改动:`value` → `result` 的 LCS 里有 `u`、`l`
57
+ * 两个孤立匹配,照它上色就得到 `res[ul]t` 这种"中间两个字没变"的假象 —— 恰好毁掉
58
+ * 这个功能最该服务的场景(改了一个词)。所以只有成串的匹配才算未改动。
59
+ */
60
+ const MIN_MATCH_RUN = 2
61
+
62
+ /** One added line of a block: its 1-based number and its text (leading `+` gone). */
63
+ interface AddedLine {
64
+ line: number
65
+ text: string
66
+ }
67
+
68
+ /** How many lines of this hunk carry `sign` (an upper bound on the pair count). */
69
+ function countSigned(hunk: UnifiedHunk, sign: string): number {
70
+ let count = 0
71
+ for (const raw of hunk.lines) if (raw[0] === sign) count += 1
72
+ return count
73
+ }
74
+
75
+ /**
76
+ * Strip the common prefix/suffix of a pair, leaving the two middles.
77
+ *
78
+ * @param before - the deleted line's text.
79
+ * @param after - the added line's text.
80
+ * @returns the middles plus `head`, the column at which they start in the line.
81
+ */
82
+ function splitCommon(before: string, after: string): { head: number; oldMid: string; newMid: string } {
83
+ const shortest = Math.min(before.length, after.length)
84
+ let head = 0
85
+ while (head < shortest && before[head] === after[head]) head += 1
86
+ // The suffix may not eat into the prefix: in `aaa` → `aa` the prefix already
87
+ // consumed the whole shorter line, and a suffix on top of it would produce a
88
+ // negative middle.
89
+ let tail = 0
90
+ while (tail < shortest - head && before[before.length - 1 - tail] === after[after.length - 1 - tail]) tail += 1
91
+ return {
92
+ head,
93
+ oldMid: before.slice(head, before.length - tail),
94
+ newMid: after.slice(head, after.length - tail),
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Character LCS refinement: which columns of `after` really changed.
100
+ *
101
+ * @param before - the deleted middle (non-empty, length already capped).
102
+ * @param after - the added middle (non-empty, length already capped).
103
+ * @param offset - where the middles start in the line, added to every column.
104
+ * @returns one range per run of changed columns, ascending, non-empty.
105
+ */
106
+ function refineRanges(before: string, after: string, offset: number): InlineRange[] {
107
+ const rows = before.length
108
+ const columns = after.length
109
+ // `table[i][j]` = length of the LCS of `before.slice(i)` and `after.slice(j)`.
110
+ // The caller has capped `rows + columns`, so this table stays small.
111
+ const table: number[][] = []
112
+ for (let i = rows; i >= 0; i -= 1) {
113
+ const row = new Array<number>(columns + 1).fill(0)
114
+ table[i] = row
115
+ if (i === rows) continue
116
+ const next = table[i + 1]
117
+ for (let j = columns - 1; j >= 0; j -= 1) {
118
+ row[j] = before[i] === after[j] ? next[j + 1] + 1 : Math.max(next[j], row[j + 1])
119
+ }
120
+ }
121
+ // Walk the table to get one longest common subsequence (both indices strictly
122
+ // increase), then keep only its runs of length >= MIN_MATCH_RUN.
123
+ const kept: boolean[] = new Array<boolean>(columns).fill(false)
124
+ let runStart = -1
125
+ let runLength = 0
126
+ const closeRun = (): void => {
127
+ if (runLength >= MIN_MATCH_RUN) {
128
+ for (let index = runStart; index < runStart + runLength; index += 1) kept[index] = true
129
+ }
130
+ runStart = -1
131
+ runLength = 0
132
+ }
133
+ let i = 0
134
+ let j = 0
135
+ while (i < rows && j < columns) {
136
+ if (before[i] === after[j]) {
137
+ if (runLength === 0) runStart = j
138
+ runLength += 1
139
+ i += 1
140
+ j += 1
141
+ continue
142
+ }
143
+ closeRun()
144
+ if (table[i + 1][j] >= table[i][j + 1]) i += 1
145
+ else j += 1
146
+ }
147
+ closeRun()
148
+ const ranges: InlineRange[] = []
149
+ let start = -1
150
+ for (let index = 0; index <= columns; index += 1) {
151
+ const changed = index < columns && !kept[index]
152
+ if (changed && start < 0) start = index
153
+ else if (!changed && start >= 0) {
154
+ ranges.push({ from: offset + start, to: offset + index })
155
+ start = -1
156
+ }
157
+ }
158
+ return ranges
159
+ }
160
+
161
+ /** A removed chip for `text` at column `at`, or undefined when there is nothing to show. */
162
+ function chipAt(at: number, text: string): RemovedChip | undefined {
163
+ if (text === '') return undefined
164
+ const truncated = text.length > MAX_CHIP_CHARS
165
+ return { at, text: truncated ? text.slice(0, MAX_CHIP_CHARS) : text, truncated }
166
+ }
167
+
168
+ /**
169
+ * Attach `line` only when the fragment is NOT on the mark's anchor line.
170
+ *
171
+ * Single-line changes are the common case, and there the fragment's line is
172
+ * simply the mark's own line — writing it out would repeat the mark on every
173
+ * entry. Multi-line blocks (a modified block is one mark spanning every added
174
+ * line) do need it, because the mark alone cannot say which of its lines a
175
+ * fragment belongs to.
176
+ */
177
+ function rangeOnLine(range: InlineRange, line: number, anchor: number): InlineRange {
178
+ return line === anchor ? range : { ...range, line }
179
+ }
180
+
181
+ /** {@link rangeOnLine} for a chip. */
182
+ function chipOnLine(chip: RemovedChip, line: number, anchor: number): RemovedChip {
183
+ return line === anchor ? chip : { ...chip, line }
184
+ }
185
+
186
+ /**
187
+ * Do these lines allow a character-level comparison at all?
188
+ *
189
+ * Both caps that are per line rather than per hunk live here.
190
+ */
191
+ function shortEnough(deleted: readonly string[], added: readonly AddedLine[]): boolean {
192
+ return deleted.every(text => text.length <= MAX_INLINE_LINE)
193
+ && added.every(item => item.text.length <= MAX_INLINE_LINE)
194
+ }
195
+
196
+ /**
197
+ * Fill in `mark.inline` / `mark.removed` for one modified block.
198
+ *
199
+ * Pairing is positional: the i-th deleted line of the block goes with the i-th
200
+ * added line (git puts every deletion of a block before its additions, and a
201
+ * block with unequal counts leaves the extras as pure additions/removals).
202
+ *
203
+ * @param mark - the block's mark (mutated).
204
+ * @param deleted - the block's deleted texts, in order.
205
+ * @param added - the block's added lines, in order.
206
+ */
207
+ function attachInline(mark: ChangeMark, deleted: readonly string[], added: readonly AddedLine[]): void {
208
+ const pairs = Math.min(deleted.length, added.length)
209
+ const inline: InlineRange[] = []
210
+ const removed: RemovedChip[] = []
211
+ for (let index = 0; index < pairs; index += 1) {
212
+ const before = deleted[index]
213
+ const after = added[index].text
214
+ const line = added[index].line
215
+ // A pair that came out identical (the block was reordered, not rewritten).
216
+ if (before === after) continue
217
+ const { head, oldMid, newMid } = splitCommon(before, after)
218
+ if (newMid.length === 0) {
219
+ // The line was only shortened: there is no new character to paint, so the
220
+ // removed text becomes a chip sitting exactly at the cut.
221
+ const chip = chipAt(head, oldMid)
222
+ if (chip !== undefined) removed.push(chipOnLine(chip, line, mark.from))
223
+ continue
224
+ }
225
+ if (oldMid.length === 0 || oldMid.length + newMid.length > MAX_REFINE_CHARS) {
226
+ // Pure insertion inside the line, or a middle too long to refine: the
227
+ // whole middle counts as one range.
228
+ inline.push(rangeOnLine({ from: head, to: head + newMid.length }, line, mark.from))
229
+ continue
230
+ }
231
+ for (const range of refineRanges(oldMid, newMid, head)) inline.push(rangeOnLine(range, line, mark.from))
232
+ }
233
+ // Deleted lines with no added counterpart: their text no longer exists in the
234
+ // new file, so it hangs on the anchor line's first column as a chip.
235
+ for (let index = pairs; index < deleted.length; index += 1) {
236
+ const chip = chipAt(0, deleted[index])
237
+ if (chip !== undefined) removed.push(chip)
238
+ }
239
+ if (inline.length > 0) mark.inline = inline
240
+ if (removed.length > 0) mark.removed = removed
241
+ }
242
+
243
+ /**
244
+ * Walk one hunk and emit its marks.
245
+ *
246
+ * Deletions have no line of their own in the new file, so they anchor on the
247
+ * line where they were removed — the same convention the editor's gutter uses
248
+ * when it paints a "−" beside the surviving line.
249
+ *
250
+ * @param hunk - one served hunk.
251
+ * @param marks - destination list (mutated).
252
+ * @param index - this hunk's position in the served list (stamped onto每块).
253
+ * @returns the hunk's first jumpable line, or undefined when it paints nothing.
254
+ */
255
+ function collectHunk(hunk: UnifiedHunk, marks: ChangeMark[], index: number): number | undefined {
256
+ let line = hunk.newStart
257
+ let first: number | undefined
258
+ // A run of consecutive +/- lines is one block: a block with both signs is a
259
+ // modification, a block with only additions is an insertion, and a block with
260
+ // only deletions is a removal anchored where it used to be.
261
+ let added: AddedLine[] = []
262
+ let deleted: string[] = []
263
+ // Per-hunk cap: too many pairs and the whole hunk keeps line-level marks only.
264
+ const inlineAllowed = Math.min(countSigned(hunk, '+'), countSigned(hunk, '-')) <= MAX_PAIRS_PER_HUNK
265
+
266
+ const flush = (): void => {
267
+ if (added.length > 0) {
268
+ const from = added[0].line
269
+ const to = added[added.length - 1].line
270
+ const kind = deleted.length > 0 ? 'mod' : 'add'
271
+ const mark: ChangeMark = { from, to, kind, hunk: index }
272
+ // A pure insertion is skipped on purpose: every character of those lines
273
+ // is new, which the line background already says — painting the whole line
274
+ // again would only make it a darker green than its neighbours.
275
+ if (deleted.length > 0 && inlineAllowed && shortEnough(deleted, added)) {
276
+ attachInline(mark, deleted, added)
277
+ }
278
+ marks.push(mark)
279
+ first ??= from
280
+ } else if (deleted.length > 0) {
281
+ const anchor = Math.max(1, line)
282
+ const mark: ChangeMark = { from: anchor, to: anchor, kind: 'del', hunk: index }
283
+ if (inlineAllowed && deleted.every(text => text.length <= MAX_INLINE_LINE)) {
284
+ const chips: RemovedChip[] = []
285
+ for (const text of deleted) {
286
+ const chip = chipAt(0, text)
287
+ if (chip !== undefined) chips.push(chip)
288
+ }
289
+ if (chips.length > 0) mark.removed = chips
290
+ }
291
+ marks.push(mark)
292
+ first ??= anchor
293
+ }
294
+ added = []
295
+ deleted = []
296
+ }
297
+
298
+ for (const raw of hunk.lines) {
299
+ const sign = raw[0]
300
+ if (sign === '+') {
301
+ added.push({ line, text: raw.slice(1) })
302
+ line += 1
303
+ continue
304
+ }
305
+ if (sign === '-') {
306
+ deleted.push(raw.slice(1))
307
+ continue
308
+ }
309
+ // Context (or the "" marker) closes the block.
310
+ flush()
311
+ if (sign === ' ' || raw === '') line += 1
312
+ }
313
+ flush()
314
+ return first
315
+ }
316
+
317
+ /**
318
+ * Build the editable set for one file.
319
+ *
320
+ * @param hunks - served hunks for the whole session.
321
+ * @param stamp - identity of the comparison (file + baseline + turn).
322
+ * @returns the mark set the editor paints, or {@link EMPTY_CHANGE_SET}.
323
+ */
324
+ export function changeSetFromHunks(hunks: readonly UnifiedHunk[], stamp: string): ChangeSet {
325
+ if (hunks.length === 0) return { marks: [], hunks: [], stamp, patches: [] }
326
+ const marks: ChangeMark[] = []
327
+ // `hunks[i]` / `patches[i]` must stay in step with the mark's `hunk` index, so
328
+ // order is preserved: `hunks` is the jump order (ascending line), while
329
+ // `patches` keeps the served order the marks point into.
330
+ const anchors: { line: number; served: number }[] = []
331
+ const patches: string[] = []
332
+ hunks.forEach((hunk, served) => {
333
+ patches.push(hunk.patch)
334
+ const first = collectHunk(hunk, marks, served)
335
+ if (first !== undefined) anchors.push({ line: first, served })
336
+ })
337
+ marks.sort((left, right) => left.from - right.from)
338
+ // Jump order is ascending by line; the mark's `hunk` is a key into `patches`,
339
+ // never into this list — that indirection is what keeps both orderings right.
340
+ const order = [...anchors].sort((left, right) => left.line - right.line)
341
+ return {
342
+ marks,
343
+ hunks: [...new Set(order.map(anchor => anchor.line))],
344
+ stamp,
345
+ patches,
346
+ }
347
+ }