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.
- package/LICENSE +21 -0
- package/README.en.md +385 -0
- package/README.md +347 -2
- package/cordis.patch.yml +259 -0
- package/icon.svg +7 -0
- package/lib/client.js +62602 -0
- package/lib/index.js +4287 -0
- package/lib/p2-bridge-refs.js +10 -0
- package/lib/p2-bridge.js +16 -0
- package/lib/p2-router-fs.js +252 -0
- package/lib/p2-router-refs.js +466 -0
- package/lib/p2-router-spill.js +62 -0
- package/lib/p2-router-subprocess.js +64 -0
- package/lib/p2-shell-bash.js +24 -0
- package/lib/ssh-proc-arI5lm4W.js +632 -0
- package/lib/types/changes/recorder.d.ts +240 -0
- package/lib/types/changes/status.d.ts +201 -0
- package/lib/types/changes/text-diff.d.ts +65 -0
- package/lib/types/client/CodeEditor.d.ts +125 -0
- package/lib/types/client/Dialog.d.ts +14 -0
- package/lib/types/client/DirectoryFlow.d.ts +30 -0
- package/lib/types/client/EditorPane.d.ts +30 -0
- package/lib/types/client/HostsView.d.ts +10 -0
- package/lib/types/client/NativeExplorer.d.ts +39 -0
- package/lib/types/client/RemoteDirectoryPicker.d.ts +16 -0
- package/lib/types/client/RemoteEditorView.d.ts +20 -0
- package/lib/types/client/RemoteExplorer.d.ts +18 -0
- package/lib/types/client/RemoteRoot.d.ts +16 -0
- package/lib/types/client/SearchView.d.ts +14 -0
- package/lib/types/client/SettingsHosts.d.ts +15 -0
- package/lib/types/client/TerminalView.d.ts +16 -0
- package/lib/types/client/WorkspaceWizard.d.ts +16 -0
- package/lib/types/client/WorkspacesView.d.ts +14 -0
- package/lib/types/client/XTerm.d.ts +15 -0
- package/lib/types/client/api.d.ts +162 -0
- package/lib/types/client/changes/ChangesDock.d.ts +35 -0
- package/lib/types/client/changes/model.d.ts +36 -0
- package/lib/types/client/changes/undo-affordance.d.ts +48 -0
- package/lib/types/client/changes/undo-context.d.ts +25 -0
- package/lib/types/client/composer.d.ts +165 -0
- package/lib/types/client/i18n.d.ts +17 -0
- package/lib/types/client/index.d.ts +14 -0
- package/lib/types/client/remote-file-open-target.d.ts +247 -0
- package/lib/types/client/remote-file-open.d.ts +35 -0
- package/lib/types/client/reveal.d.ts +15 -0
- package/lib/types/client/scm/DiffView.d.ts +24 -0
- package/lib/types/client/scm/HistoryPanel.d.ts +12 -0
- package/lib/types/client/scm/RepoPicker.d.ts +24 -0
- package/lib/types/client/scm/ScmPanel.d.ts +15 -0
- package/lib/types/client/scm/actions.d.ts +170 -0
- package/lib/types/client/scm/diff-model.d.ts +29 -0
- package/lib/types/client/scm/stage-queue.d.ts +99 -0
- package/lib/types/client/scm/status-map.d.ts +157 -0
- package/lib/types/client/scm/untracked-dirs.d.ts +72 -0
- package/lib/types/client/selection-popup.d.ts +61 -0
- package/lib/types/client/store.d.ts +437 -0
- package/lib/types/client/theme.d.ts +15 -0
- package/lib/types/client/ui.d.ts +46 -0
- package/lib/types/client/upload-plan.d.ts +78 -0
- package/lib/types/client/upload-source.d.ts +46 -0
- package/lib/types/client/use-remote-chat-target.d.ts +17 -0
- package/lib/types/client/vsc/FindWidget.d.ts +81 -0
- package/lib/types/client/vsc/change-marks.d.ts +150 -0
- package/lib/types/client/vsc/editor-langs.d.ts +5 -0
- package/lib/types/client/vsc/editor-palette.d.ts +37 -0
- package/lib/types/client/vsc/editor-theme.d.ts +32 -0
- package/lib/types/client/vsc/fallback-highlight.d.ts +26 -0
- package/lib/types/client/vsc/kit.d.ts +195 -0
- package/lib/types/client/vsc/markdown.d.ts +11 -0
- package/lib/types/client/vsc/reveal-line.d.ts +38 -0
- package/lib/types/client/vsc/tokenize.d.ts +104 -0
- package/lib/types/deploy.d.ts +94 -0
- package/lib/types/fs-ops.d.ts +8 -0
- package/lib/types/git-format.d.ts +34 -0
- package/lib/types/git-ops.d.ts +183 -0
- package/lib/types/http.d.ts +14 -0
- package/lib/types/importers.d.ts +20 -0
- package/lib/types/index.d.ts +87 -0
- package/lib/types/keygen.d.ts +19 -0
- package/lib/types/loopback.d.ts +7 -0
- package/lib/types/p2/bridge-refs.d.ts +21 -0
- package/lib/types/p2/bridge.d.ts +24 -0
- package/lib/types/p2/file-refs.d.ts +73 -0
- package/lib/types/p2/helper-rpc.d.ts +63 -0
- package/lib/types/p2/in-flight-gate.d.ts +25 -0
- package/lib/types/p2/router-fs.d.ts +92 -0
- package/lib/types/p2/router-refs.d.ts +45 -0
- package/lib/types/p2/router-spill.d.ts +11 -0
- package/lib/types/p2/router-subprocess.d.ts +23 -0
- package/lib/types/p2/shell-bash.d.ts +16 -0
- package/lib/types/p2/ssh-proc.d.ts +22 -0
- package/lib/types/p2/worlds.d.ts +179 -0
- package/lib/types/paths.d.ts +12 -0
- package/lib/types/pool.d.ts +105 -0
- package/lib/types/protocol.d.ts +452 -0
- package/lib/types/routes.d.ts +27 -0
- package/lib/types/search-ops.d.ts +68 -0
- package/lib/types/sftp.d.ts +90 -0
- package/lib/types/store.d.ts +66 -0
- package/lib/types/terminal.d.ts +51 -0
- package/lib/types/timeouts.d.ts +83 -0
- package/lib/types/tools.d.ts +41 -0
- package/lib/worlds-PUumRIh7.js +18765 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +189 -4
- package/src/changes/recorder.ts +902 -0
- package/src/changes/status.ts +448 -0
- package/src/changes/text-diff.ts +377 -0
- package/src/client/CodeEditor.tsx +923 -0
- package/src/client/Dialog.tsx +76 -0
- package/src/client/DirectoryFlow.tsx +431 -0
- package/src/client/EditorPane.tsx +512 -0
- package/src/client/HostsView.tsx +312 -0
- package/src/client/NativeExplorer.tsx +121 -0
- package/src/client/RemoteDirectoryPicker.tsx +157 -0
- package/src/client/RemoteEditorView.tsx +54 -0
- package/src/client/RemoteExplorer.tsx +1398 -0
- package/src/client/RemoteRoot.tsx +389 -0
- package/src/client/SearchView.tsx +407 -0
- package/src/client/SettingsHosts.tsx +136 -0
- package/src/client/TerminalView.tsx +308 -0
- package/src/client/WorkspaceWizard.tsx +199 -0
- package/src/client/WorkspacesView.tsx +93 -0
- package/src/client/XTerm.tsx +308 -0
- package/src/client/api.ts +331 -0
- package/src/client/changes/ChangesDock.tsx +348 -0
- package/src/client/changes/changes.css +230 -0
- package/src/client/changes/model.ts +347 -0
- package/src/client/changes/undo-affordance.ts +389 -0
- package/src/client/changes/undo-context.ts +34 -0
- package/src/client/composer.ts +436 -0
- package/src/client/css.d.ts +7 -0
- package/src/client/i18n.ts +702 -0
- package/src/client/index.tsx +286 -0
- package/src/client/remote-file-open-target.ts +553 -0
- package/src/client/remote-file-open.ts +261 -0
- package/src/client/reveal.ts +33 -0
- package/src/client/scm/DiffView.tsx +570 -0
- package/src/client/scm/HistoryPanel.tsx +353 -0
- package/src/client/scm/RepoPicker.tsx +238 -0
- package/src/client/scm/ScmPanel.tsx +857 -0
- package/src/client/scm/actions.ts +460 -0
- package/src/client/scm/diff-model.ts +206 -0
- package/src/client/scm/stage-queue.ts +264 -0
- package/src/client/scm/status-map.ts +381 -0
- package/src/client/scm/untracked-dirs.ts +139 -0
- package/src/client/selection-popup.ts +157 -0
- package/src/client/store.tsx +1670 -0
- package/src/client/theme.ts +54 -0
- package/src/client/ui.tsx +136 -0
- package/src/client/upload-plan.ts +153 -0
- package/src/client/upload-source.ts +145 -0
- package/src/client/use-remote-chat-target.ts +153 -0
- package/src/client/vsc/FindWidget.tsx +446 -0
- package/src/client/vsc/change-marks.ts +586 -0
- package/src/client/vsc/editor-langs.ts +137 -0
- package/src/client/vsc/editor-palette.ts +107 -0
- package/src/client/vsc/editor-theme.ts +93 -0
- package/src/client/vsc/fallback-highlight.ts +145 -0
- package/src/client/vsc/kit.tsx +777 -0
- package/src/client/vsc/markdown.tsx +172 -0
- package/src/client/vsc/reveal-line.ts +142 -0
- package/src/client/vsc/tokenize.ts +569 -0
- package/src/client/vsc/vsc.css +2669 -0
- package/src/deploy.ts +481 -0
- package/src/fs-ops.ts +57 -0
- package/src/git-format.ts +205 -0
- package/src/git-ops.ts +693 -0
- package/src/http.ts +55 -0
- package/src/importers.ts +141 -0
- package/src/index.ts +442 -0
- package/src/keygen.ts +77 -0
- package/src/loopback.ts +47 -0
- package/src/p2/bridge-refs.ts +27 -0
- package/src/p2/bridge.ts +32 -0
- package/src/p2/file-refs.ts +392 -0
- package/src/p2/helper-rpc.ts +394 -0
- package/src/p2/in-flight-gate.ts +63 -0
- package/src/p2/router-fs.ts +327 -0
- package/src/p2/router-refs.ts +166 -0
- package/src/p2/router-spill.ts +75 -0
- package/src/p2/router-subprocess.ts +92 -0
- package/src/p2/shell-bash.ts +30 -0
- package/src/p2/ssh-proc.ts +802 -0
- package/src/p2/worlds.ts +735 -0
- package/src/paths.ts +50 -0
- package/src/pool.ts +811 -0
- package/src/protocol.ts +460 -0
- package/src/routes.ts +802 -0
- package/src/search-ops.ts +220 -0
- package/src/sftp.ts +677 -0
- package/src/store.ts +364 -0
- package/src/terminal.ts +291 -0
- package/src/timeouts.ts +122 -0
- package/src/tools.ts +320 -0
- package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.helper.json +16 -0
- package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.tar.gz +4 -0
- package/vendor/dsh-remote-helpers/dsh-ssh-helper-0.2.1-alpha.2-linux-x64.tar.gz.sha256 +1 -0
- 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
|
+
}
|