reslop 0.0.1 → 0.1.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/CHANGELOG.md +11 -3
- package/LICENSE +1 -1
- package/README.md +83 -3
- package/bin/reslop.js +23 -0
- package/lib/ansi.js +231 -0
- package/lib/api.js +32 -0
- package/lib/cli.js +181 -0
- package/lib/clipboard.js +42 -0
- package/lib/detect.js +84 -0
- package/lib/diff.js +906 -0
- package/lib/editor.js +148 -0
- package/lib/files.js +47 -0
- package/lib/git.js +261 -0
- package/lib/highlight.js +1234 -0
- package/lib/keys.js +208 -0
- package/lib/render.js +964 -0
- package/lib/review.js +687 -0
- package/lib/select.js +218 -0
- package/lib/session.js +1416 -0
- package/lib/wrap.js +118 -0
- package/package.json +33 -22
- package/.editorconfig +0 -12
- package/.gitattributes +0 -1
- package/.prettierignore +0 -2
- package/AUTHORS +0 -1
- package/CONTRIBUTING.md +0 -41
- package/SECURITY.md +0 -16
- package/eslint.config.js +0 -5
- package/main.js +0 -3
- package/prettier.config.js +0 -10
- package/tsconfig.json +0 -11
package/lib/diff.js
ADDED
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { graphemes } = require('./ansi.js');
|
|
4
|
+
|
|
5
|
+
const LCS_CELL_LIMIT = 250000;
|
|
6
|
+
const DISPLAY_CONTEXT = 3;
|
|
7
|
+
const NOISY_TINY_SPANS = 6;
|
|
8
|
+
const PAIR_MIN_SIM = 0.3;
|
|
9
|
+
const PAIR_HEAD_WEIGHT = 0.4;
|
|
10
|
+
const PAIR_TOKEN_WEIGHT = 0.6;
|
|
11
|
+
const WORD_REFINE_MIN = 0.6;
|
|
12
|
+
const TOKEN_RE = /[A-Za-z_$][\w$]*|\d+|[^\s]/g;
|
|
13
|
+
const PIECE_RE = /[A-Za-z_$][\w$]*|\d+|\s+|./g;
|
|
14
|
+
const LINE_MARK = Object.assign(Object.create(null), {
|
|
15
|
+
add: '+',
|
|
16
|
+
del: '-',
|
|
17
|
+
ctx: ' ',
|
|
18
|
+
});
|
|
19
|
+
const LINE_TYPE = Object.assign(Object.create(null), {
|
|
20
|
+
'+': 'add',
|
|
21
|
+
'-': 'del',
|
|
22
|
+
' ': 'ctx',
|
|
23
|
+
});
|
|
24
|
+
const INDEX_SIDE = Object.assign(Object.create(null), {
|
|
25
|
+
staged: 'new',
|
|
26
|
+
unstaged: 'old',
|
|
27
|
+
untracked: 'old',
|
|
28
|
+
commit: 'old',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const collapseSpans = (parts) => {
|
|
32
|
+
const spans = [];
|
|
33
|
+
for (const part of parts) {
|
|
34
|
+
const last = spans[spans.length - 1];
|
|
35
|
+
if (last && last.changed === part.changed) {
|
|
36
|
+
last.text += part.text;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
spans.push({ text: part.text, changed: part.changed });
|
|
40
|
+
}
|
|
41
|
+
return spans;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const isNoisy = (spans) => {
|
|
45
|
+
let tiny = 0;
|
|
46
|
+
for (const span of spans) {
|
|
47
|
+
if (span.changed && graphemes(span.text).length <= 2) tiny += 1;
|
|
48
|
+
}
|
|
49
|
+
return tiny > NOISY_TINY_SPANS;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const wholeChanged = (a, b) => ({
|
|
53
|
+
oldSpans: a.length ? [{ text: a.join(''), changed: true }] : [],
|
|
54
|
+
newSpans: b.length ? [{ text: b.join(''), changed: true }] : [],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const lcsLength = (a, b) => {
|
|
58
|
+
const n = a.length;
|
|
59
|
+
const m = b.length;
|
|
60
|
+
if (n === 0 || m === 0) return 0;
|
|
61
|
+
if (n * m > LCS_CELL_LIMIT) {
|
|
62
|
+
let lo = 0;
|
|
63
|
+
while (lo < n && lo < m && a[lo] === b[lo]) lo += 1;
|
|
64
|
+
let hiA = n;
|
|
65
|
+
let hiB = m;
|
|
66
|
+
while (hiA > lo && hiB > lo && a[hiA - 1] === b[hiB - 1]) {
|
|
67
|
+
hiA -= 1;
|
|
68
|
+
hiB -= 1;
|
|
69
|
+
}
|
|
70
|
+
return lo + (n - hiA);
|
|
71
|
+
}
|
|
72
|
+
let prev = new Uint32Array(m + 1);
|
|
73
|
+
let cur = new Uint32Array(m + 1);
|
|
74
|
+
for (let i = 1; i <= n; i++) {
|
|
75
|
+
const ai = a[i - 1];
|
|
76
|
+
for (let j = 1; j <= m; j++) {
|
|
77
|
+
if (ai === b[j - 1]) cur[j] = prev[j - 1] + 1;
|
|
78
|
+
else cur[j] = prev[j] > cur[j - 1] ? prev[j] : cur[j - 1];
|
|
79
|
+
}
|
|
80
|
+
const swap = prev;
|
|
81
|
+
prev = cur;
|
|
82
|
+
cur = swap;
|
|
83
|
+
cur.fill(0);
|
|
84
|
+
}
|
|
85
|
+
return prev[m];
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const similarity = (left, right) => {
|
|
89
|
+
const a = graphemes(left);
|
|
90
|
+
const b = graphemes(right);
|
|
91
|
+
if (a.length === 0 && b.length === 0) return 1;
|
|
92
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
93
|
+
const lcs = lcsLength(a, b);
|
|
94
|
+
return (2 * lcs) / (a.length + b.length);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const lineTokens = (text) => {
|
|
98
|
+
TOKEN_RE.lastIndex = 0;
|
|
99
|
+
return text.match(TOKEN_RE) ?? [];
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const tokenSimilarity = (a, b) => {
|
|
103
|
+
if (a.length === 0 && b.length === 0) return 1;
|
|
104
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
105
|
+
const lcs = lcsLength(a, b);
|
|
106
|
+
return (2 * lcs) / (a.length + b.length);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const pairScore = (left, right) => {
|
|
110
|
+
const a = lineTokens(left);
|
|
111
|
+
const b = lineTokens(right);
|
|
112
|
+
const token = tokenSimilarity(a, b);
|
|
113
|
+
const headA = a[0];
|
|
114
|
+
const headB = b[0];
|
|
115
|
+
const head = headA && headA === headB ? 1 : 0;
|
|
116
|
+
return PAIR_HEAD_WEIGHT * head + PAIR_TOKEN_WEIGHT * token;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const comparePair = (left, right) => {
|
|
120
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
121
|
+
if (left.dist !== right.dist) return left.dist - right.dist;
|
|
122
|
+
if (left.i !== right.i) return left.i - right.i;
|
|
123
|
+
return left.j - right.j;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const pairCandidates = (delTexts, addTexts) => {
|
|
127
|
+
const candidates = [];
|
|
128
|
+
for (let i = 0; i < delTexts.length; i++) {
|
|
129
|
+
for (let j = 0; j < addTexts.length; j++) {
|
|
130
|
+
const score = pairScore(delTexts[i], addTexts[j]);
|
|
131
|
+
if (score < PAIR_MIN_SIM) continue;
|
|
132
|
+
const dist = Math.abs(i - j);
|
|
133
|
+
candidates.push({ i, j, score, dist });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
candidates.sort(comparePair);
|
|
137
|
+
return candidates;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const fillLcsTable = (a, b) => {
|
|
141
|
+
const n = a.length;
|
|
142
|
+
const m = b.length;
|
|
143
|
+
const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
|
|
144
|
+
for (let i = 1; i <= n; i++) {
|
|
145
|
+
const ai = a[i - 1];
|
|
146
|
+
const row = dp[i];
|
|
147
|
+
const prev = dp[i - 1];
|
|
148
|
+
for (let j = 1; j <= m; j++) {
|
|
149
|
+
if (ai === b[j - 1]) row[j] = prev[j - 1] + 1;
|
|
150
|
+
else row[j] = prev[j] > row[j - 1] ? prev[j] : row[j - 1];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return dp;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const tracebackLcs = (a, b, dp) => {
|
|
157
|
+
const rev = [];
|
|
158
|
+
let i = a.length;
|
|
159
|
+
let j = b.length;
|
|
160
|
+
while (i > 0 && j > 0) {
|
|
161
|
+
if (a[i - 1] === b[j - 1]) {
|
|
162
|
+
rev.push({ type: 'eq', text: a[i - 1] });
|
|
163
|
+
i -= 1;
|
|
164
|
+
j -= 1;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (dp[i - 1][j] >= dp[i][j - 1]) {
|
|
168
|
+
rev.push({ type: 'del', text: a[i - 1] });
|
|
169
|
+
i -= 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
rev.push({ type: 'add', text: b[j - 1] });
|
|
173
|
+
j -= 1;
|
|
174
|
+
}
|
|
175
|
+
while (i > 0) {
|
|
176
|
+
rev.push({ type: 'del', text: a[i - 1] });
|
|
177
|
+
i -= 1;
|
|
178
|
+
}
|
|
179
|
+
while (j > 0) {
|
|
180
|
+
rev.push({ type: 'add', text: b[j - 1] });
|
|
181
|
+
j -= 1;
|
|
182
|
+
}
|
|
183
|
+
rev.reverse();
|
|
184
|
+
return rev;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const lcsOps = (a, b) => tracebackLcs(a, b, fillLcsTable(a, b));
|
|
188
|
+
|
|
189
|
+
const lcsSpans = (a, b) => {
|
|
190
|
+
const oldParts = [];
|
|
191
|
+
const newParts = [];
|
|
192
|
+
for (const op of lcsOps(a, b)) {
|
|
193
|
+
if (op.type === 'eq') {
|
|
194
|
+
oldParts.push({ text: op.text, changed: false });
|
|
195
|
+
newParts.push({ text: op.text, changed: false });
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (op.type === 'del') {
|
|
199
|
+
oldParts.push({ text: op.text, changed: true });
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
newParts.push({ text: op.text, changed: true });
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
oldSpans: collapseSpans(oldParts),
|
|
206
|
+
newSpans: collapseSpans(newParts),
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const trimSharedEnds = (a, b) => {
|
|
211
|
+
let lo = 0;
|
|
212
|
+
while (lo < a.length && lo < b.length && a[lo] === b[lo]) lo += 1;
|
|
213
|
+
let hiA = a.length;
|
|
214
|
+
let hiB = b.length;
|
|
215
|
+
while (hiA > lo && hiB > lo && a[hiA - 1] === b[hiB - 1]) {
|
|
216
|
+
hiA -= 1;
|
|
217
|
+
hiB -= 1;
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
prefix: a.slice(0, lo),
|
|
221
|
+
suffix: a.slice(hiA),
|
|
222
|
+
midA: a.slice(lo, hiA),
|
|
223
|
+
midB: b.slice(lo, hiB),
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const diffTokenMiddle = (midA, midB) => {
|
|
228
|
+
if (midA.length * midB.length > LCS_CELL_LIMIT) {
|
|
229
|
+
return wholeChanged(midA, midB);
|
|
230
|
+
}
|
|
231
|
+
if (midA.length === 0 && midB.length === 0) {
|
|
232
|
+
return { oldSpans: [], newSpans: [] };
|
|
233
|
+
}
|
|
234
|
+
if (midA.length === 0 || midB.length === 0) return wholeChanged(midA, midB);
|
|
235
|
+
return lcsSpans(midA, midB);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const joinTrimmedSpans = (prefix, mid, suffix) => {
|
|
239
|
+
const oldParts = [];
|
|
240
|
+
const newParts = [];
|
|
241
|
+
if (prefix.length) {
|
|
242
|
+
const text = prefix.join('');
|
|
243
|
+
oldParts.push({ text, changed: false });
|
|
244
|
+
newParts.push({ text, changed: false });
|
|
245
|
+
}
|
|
246
|
+
for (const span of mid.oldSpans) oldParts.push(span);
|
|
247
|
+
for (const span of mid.newSpans) newParts.push(span);
|
|
248
|
+
if (suffix.length) {
|
|
249
|
+
const text = suffix.join('');
|
|
250
|
+
oldParts.push({ text, changed: false });
|
|
251
|
+
newParts.push({ text, changed: false });
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
oldSpans: collapseSpans(oldParts),
|
|
255
|
+
newSpans: collapseSpans(newParts),
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const diffTokens = (a, b) => {
|
|
260
|
+
if (a.length === 0 && b.length === 0) {
|
|
261
|
+
return { oldSpans: [], newSpans: [] };
|
|
262
|
+
}
|
|
263
|
+
if (a.length === 0 || b.length === 0) return wholeChanged(a, b);
|
|
264
|
+
const trimmed = trimSharedEnds(a, b);
|
|
265
|
+
const mid = diffTokenMiddle(trimmed.midA, trimmed.midB);
|
|
266
|
+
const joined = joinTrimmedSpans(trimmed.prefix, mid, trimmed.suffix);
|
|
267
|
+
if (isNoisy(joined.oldSpans) || isNoisy(joined.newSpans)) {
|
|
268
|
+
return wholeChanged(a, b);
|
|
269
|
+
}
|
|
270
|
+
return joined;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const splitIdentWords = (part) => {
|
|
274
|
+
const words = [];
|
|
275
|
+
for (const word of part.split(/(_+)/)) {
|
|
276
|
+
if (word === '') continue;
|
|
277
|
+
words.push(word);
|
|
278
|
+
}
|
|
279
|
+
return words;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const splitPieces = (text) => {
|
|
283
|
+
if (text === '') return [];
|
|
284
|
+
PIECE_RE.lastIndex = 0;
|
|
285
|
+
const coarse = text.match(PIECE_RE) ?? [];
|
|
286
|
+
const pieces = [];
|
|
287
|
+
for (const part of coarse) {
|
|
288
|
+
for (const word of splitIdentWords(part)) pieces.push(word);
|
|
289
|
+
}
|
|
290
|
+
return pieces;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const spansFromWordOps = (ops) => {
|
|
294
|
+
const oldParts = [];
|
|
295
|
+
const newParts = [];
|
|
296
|
+
let i = 0;
|
|
297
|
+
while (i < ops.length) {
|
|
298
|
+
const op = ops[i];
|
|
299
|
+
if (op.type === 'eq') {
|
|
300
|
+
oldParts.push({ text: op.text, changed: false });
|
|
301
|
+
newParts.push({ text: op.text, changed: false });
|
|
302
|
+
i += 1;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const dels = [];
|
|
306
|
+
const adds = [];
|
|
307
|
+
while (i < ops.length && ops[i].type !== 'eq') {
|
|
308
|
+
if (ops[i].type === 'del') dels.push(ops[i].text);
|
|
309
|
+
else adds.push(ops[i].text);
|
|
310
|
+
i += 1;
|
|
311
|
+
}
|
|
312
|
+
const one = dels.length === 1 && adds.length === 1;
|
|
313
|
+
const close = one && similarity(dels[0], adds[0]) >= WORD_REFINE_MIN;
|
|
314
|
+
if (close) {
|
|
315
|
+
const inner = diffTokens(graphemes(dels[0]), graphemes(adds[0]));
|
|
316
|
+
for (const span of inner.oldSpans) oldParts.push(span);
|
|
317
|
+
for (const span of inner.newSpans) newParts.push(span);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (dels.length) {
|
|
321
|
+
oldParts.push({ text: dels.join(''), changed: true });
|
|
322
|
+
}
|
|
323
|
+
if (adds.length) {
|
|
324
|
+
newParts.push({ text: adds.join(''), changed: true });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
oldSpans: collapseSpans(oldParts),
|
|
329
|
+
newSpans: collapseSpans(newParts),
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const joinAffix = (prefix, mid, suffix) => {
|
|
334
|
+
const parts = [];
|
|
335
|
+
if (prefix.length) {
|
|
336
|
+
parts.push({ text: prefix.join(''), changed: false });
|
|
337
|
+
}
|
|
338
|
+
for (const span of mid) parts.push(span);
|
|
339
|
+
if (suffix.length) {
|
|
340
|
+
parts.push({ text: suffix.join(''), changed: false });
|
|
341
|
+
}
|
|
342
|
+
return collapseSpans(parts);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const diffChars = (oldText, newText) => {
|
|
346
|
+
const a = splitPieces(oldText);
|
|
347
|
+
const b = splitPieces(newText);
|
|
348
|
+
if (a.length === 0 && b.length === 0) {
|
|
349
|
+
return { oldSpans: [], newSpans: [] };
|
|
350
|
+
}
|
|
351
|
+
if (a.length === 0 || b.length === 0) return wholeChanged(a, b);
|
|
352
|
+
let lo = 0;
|
|
353
|
+
while (lo < a.length && lo < b.length && a[lo] === b[lo]) lo += 1;
|
|
354
|
+
let hiA = a.length;
|
|
355
|
+
let hiB = b.length;
|
|
356
|
+
while (hiA > lo && hiB > lo && a[hiA - 1] === b[hiB - 1]) {
|
|
357
|
+
hiA -= 1;
|
|
358
|
+
hiB -= 1;
|
|
359
|
+
}
|
|
360
|
+
const prefix = a.slice(0, lo);
|
|
361
|
+
const suffix = a.slice(hiA);
|
|
362
|
+
const midA = a.slice(lo, hiA);
|
|
363
|
+
const midB = b.slice(lo, hiB);
|
|
364
|
+
let mid;
|
|
365
|
+
if (midA.length === 0 && midB.length === 0) {
|
|
366
|
+
mid = { oldSpans: [], newSpans: [] };
|
|
367
|
+
} else if (midA.length === 0 || midB.length === 0) {
|
|
368
|
+
mid = wholeChanged(midA, midB);
|
|
369
|
+
} else if (midA.length * midB.length > LCS_CELL_LIMIT) {
|
|
370
|
+
mid = wholeChanged(midA, midB);
|
|
371
|
+
} else {
|
|
372
|
+
mid = spansFromWordOps(lcsOps(midA, midB));
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
oldSpans: joinAffix(prefix, mid.oldSpans, suffix),
|
|
376
|
+
newSpans: joinAffix(prefix, mid.newSpans, suffix),
|
|
377
|
+
};
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const pairIndices = (delTexts, addTexts) => {
|
|
381
|
+
const delToAdd = new Array(delTexts.length).fill(-1);
|
|
382
|
+
const addUsed = new Array(addTexts.length).fill(false);
|
|
383
|
+
const candidates = pairCandidates(delTexts, addTexts);
|
|
384
|
+
for (const pair of candidates) {
|
|
385
|
+
if (delToAdd[pair.i] >= 0 || addUsed[pair.j]) continue;
|
|
386
|
+
delToAdd[pair.i] = pair.j;
|
|
387
|
+
addUsed[pair.j] = true;
|
|
388
|
+
}
|
|
389
|
+
if (delTexts.length !== addTexts.length) return delToAdd;
|
|
390
|
+
for (let i = 0; i < delTexts.length; i++) {
|
|
391
|
+
if (delToAdd[i] >= 0 || addUsed[i]) continue;
|
|
392
|
+
delToAdd[i] = i;
|
|
393
|
+
addUsed[i] = true;
|
|
394
|
+
}
|
|
395
|
+
return delToAdd;
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const changeSides = (block) => {
|
|
399
|
+
const dels = [];
|
|
400
|
+
const adds = [];
|
|
401
|
+
for (const line of block) {
|
|
402
|
+
if (line.type === 'del') dels.push(line);
|
|
403
|
+
if (line.type === 'add') adds.push(line);
|
|
404
|
+
}
|
|
405
|
+
return { dels, adds };
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const applyInlineSpans = (dels, adds, delToAdd) => {
|
|
409
|
+
const addSeen = new Set();
|
|
410
|
+
for (let i = 0; i < dels.length; i++) {
|
|
411
|
+
const j = delToAdd[i];
|
|
412
|
+
if (j < 0) {
|
|
413
|
+
dels[i].spans = [{ text: dels[i].text, changed: true }];
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
addSeen.add(j);
|
|
417
|
+
const diff = diffChars(dels[i].text, adds[j].text);
|
|
418
|
+
dels[i].spans = diff.oldSpans;
|
|
419
|
+
adds[j].spans = diff.newSpans;
|
|
420
|
+
}
|
|
421
|
+
for (let j = 0; j < adds.length; j++) {
|
|
422
|
+
if (addSeen.has(j)) continue;
|
|
423
|
+
adds[j].spans = [{ text: adds[j].text, changed: true }];
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const attachInline = (lines) => {
|
|
428
|
+
const { dels, adds } = changeSides(lines);
|
|
429
|
+
const delToAdd = pairIndices(
|
|
430
|
+
dels.map((line) => line.text),
|
|
431
|
+
adds.map((line) => line.text),
|
|
432
|
+
);
|
|
433
|
+
applyInlineSpans(dels, adds, delToAdd);
|
|
434
|
+
for (const line of lines) {
|
|
435
|
+
if (line.type === 'ctx') {
|
|
436
|
+
line.spans = [{ text: line.text, changed: false }];
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return lines;
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const addPartners = (delToAdd, addCount) => {
|
|
443
|
+
const addToDel = new Array(addCount).fill(-1);
|
|
444
|
+
for (let i = 0; i < delToAdd.length; i++) {
|
|
445
|
+
const j = delToAdd[i];
|
|
446
|
+
if (j >= 0) addToDel[j] = i;
|
|
447
|
+
}
|
|
448
|
+
return addToDel;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const walkAligned = (dels, adds, delToAdd, emit) => {
|
|
452
|
+
const addToDel = addPartners(delToAdd, adds.length);
|
|
453
|
+
const usedAdd = new Array(adds.length).fill(false);
|
|
454
|
+
let addPos = 0;
|
|
455
|
+
const takeUnpairedAdds = (limit) => {
|
|
456
|
+
while (addPos < limit) {
|
|
457
|
+
const free = addToDel[addPos] < 0 && !usedAdd[addPos];
|
|
458
|
+
if (free) {
|
|
459
|
+
emit.unpairedAdd(adds[addPos]);
|
|
460
|
+
usedAdd[addPos] = true;
|
|
461
|
+
}
|
|
462
|
+
addPos += 1;
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
for (let i = 0; i < dels.length; i++) {
|
|
466
|
+
const j = delToAdd[i];
|
|
467
|
+
if (j < 0) {
|
|
468
|
+
emit.unpairedDel(dels[i]);
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
takeUnpairedAdds(j);
|
|
472
|
+
emit.pair(dels[i], adds[j]);
|
|
473
|
+
usedAdd[j] = true;
|
|
474
|
+
if (addPos <= j) addPos = j + 1;
|
|
475
|
+
}
|
|
476
|
+
takeUnpairedAdds(adds.length);
|
|
477
|
+
for (let j = 0; j < adds.length; j++) {
|
|
478
|
+
if (!usedAdd[j]) emit.unpairedAdd(adds[j]);
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const alignChangeBlock = (dels, adds, delToAdd) => {
|
|
483
|
+
const out = [];
|
|
484
|
+
walkAligned(dels, adds, delToAdd, {
|
|
485
|
+
unpairedDel: (del) => {
|
|
486
|
+
out.push(del);
|
|
487
|
+
},
|
|
488
|
+
unpairedAdd: (add) => {
|
|
489
|
+
out.push(add);
|
|
490
|
+
},
|
|
491
|
+
pair: (del, add) => {
|
|
492
|
+
out.push(del);
|
|
493
|
+
out.push(add);
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
return out;
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const mapHunkBlocks = (lines, onCtx, onChange) => {
|
|
500
|
+
const out = [];
|
|
501
|
+
let i = 0;
|
|
502
|
+
while (i < lines.length) {
|
|
503
|
+
if (lines[i].type === 'ctx') {
|
|
504
|
+
out.push(onCtx(lines[i]));
|
|
505
|
+
i += 1;
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
const start = i;
|
|
509
|
+
while (i < lines.length && lines[i].type !== 'ctx') i += 1;
|
|
510
|
+
const block = lines.slice(start, i);
|
|
511
|
+
const { dels, adds } = changeSides(block);
|
|
512
|
+
const delToAdd = pairIndices(
|
|
513
|
+
dels.map((line) => line.text),
|
|
514
|
+
adds.map((line) => line.text),
|
|
515
|
+
);
|
|
516
|
+
for (const row of onChange(dels, adds, delToAdd)) out.push(row);
|
|
517
|
+
}
|
|
518
|
+
return out;
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const alignPairs = (lines) =>
|
|
522
|
+
mapHunkBlocks(lines, (line) => line, alignChangeBlock);
|
|
523
|
+
|
|
524
|
+
const alignSplitBlock = (dels, adds, delToAdd) => {
|
|
525
|
+
const out = [];
|
|
526
|
+
walkAligned(dels, adds, delToAdd, {
|
|
527
|
+
unpairedDel: (del) => {
|
|
528
|
+
out.push({ left: del, right: null });
|
|
529
|
+
},
|
|
530
|
+
unpairedAdd: (add) => {
|
|
531
|
+
out.push({ left: null, right: add });
|
|
532
|
+
},
|
|
533
|
+
pair: (del, add) => {
|
|
534
|
+
out.push({ left: del, right: add });
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
return out;
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const alignSplit = (lines) =>
|
|
541
|
+
mapHunkBlocks(
|
|
542
|
+
lines,
|
|
543
|
+
(line) => ({ left: line, right: line }),
|
|
544
|
+
alignSplitBlock,
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
const parseGitPaths = (line) => {
|
|
548
|
+
const rest = line.slice('diff --git '.length);
|
|
549
|
+
const mid = rest.lastIndexOf(' b/');
|
|
550
|
+
if (mid < 0 || !rest.startsWith('a/')) return { oldPath: '', newPath: '' };
|
|
551
|
+
const oldPath = rest.slice(2, mid);
|
|
552
|
+
const newPath = rest.slice(mid + 3);
|
|
553
|
+
return { oldPath, newPath };
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const stripAb = (raw) => {
|
|
557
|
+
if (raw === '/dev/null') return raw;
|
|
558
|
+
if (raw.startsWith('a/') || raw.startsWith('b/')) return raw.slice(2);
|
|
559
|
+
return raw;
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
const parsePathLine = (line) => {
|
|
563
|
+
const space = line.indexOf(' ');
|
|
564
|
+
if (space < 0) return '';
|
|
565
|
+
let rest = line.slice(space + 1);
|
|
566
|
+
if (rest.startsWith('"') && rest.endsWith('"')) {
|
|
567
|
+
rest = rest.slice(1, -1);
|
|
568
|
+
}
|
|
569
|
+
const tab = rest.indexOf('\t');
|
|
570
|
+
if (tab >= 0) rest = rest.slice(0, tab);
|
|
571
|
+
return stripAb(rest);
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const parseHunkHeader = (line) => {
|
|
575
|
+
const match = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/.exec(line);
|
|
576
|
+
if (!match) return null;
|
|
577
|
+
const oldStart = parseInt(match[1], 10);
|
|
578
|
+
const oldCount = match[2] === undefined ? 1 : parseInt(match[2], 10);
|
|
579
|
+
const newStart = parseInt(match[3], 10);
|
|
580
|
+
const newCount = match[4] === undefined ? 1 : parseInt(match[4], 10);
|
|
581
|
+
return { oldStart, oldCount, newStart, newCount, header: line };
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const closeFile = (file, files) => {
|
|
585
|
+
if (file) files.push(file);
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const createFile = (line) => {
|
|
589
|
+
const paths = parseGitPaths(line);
|
|
590
|
+
return {
|
|
591
|
+
oldPath: paths.oldPath,
|
|
592
|
+
newPath: paths.newPath,
|
|
593
|
+
isNew: false,
|
|
594
|
+
isDeleted: false,
|
|
595
|
+
isBinary: false,
|
|
596
|
+
preamble: [line],
|
|
597
|
+
hunks: [],
|
|
598
|
+
};
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const startGitFile = (line, ctx) => {
|
|
602
|
+
ctx.hunk = null;
|
|
603
|
+
closeFile(ctx.file, ctx.files);
|
|
604
|
+
ctx.file = createFile(line);
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
const markBinary = (line, ctx) => {
|
|
608
|
+
ctx.file.isBinary = true;
|
|
609
|
+
ctx.file.preamble.push(line);
|
|
610
|
+
ctx.hunk = null;
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const markNewFile = (line, ctx) => {
|
|
614
|
+
ctx.file.isNew = true;
|
|
615
|
+
ctx.file.preamble.push(line);
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const markDeleted = (line, ctx) => {
|
|
619
|
+
ctx.file.isDeleted = true;
|
|
620
|
+
ctx.file.preamble.push(line);
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const setOldPath = (line, ctx) => {
|
|
624
|
+
const parsed = parsePathLine(line);
|
|
625
|
+
if (parsed === '/dev/null') ctx.file.isNew = true;
|
|
626
|
+
else ctx.file.oldPath = parsed;
|
|
627
|
+
ctx.file.preamble.push(line);
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const setNewPath = (line, ctx) => {
|
|
631
|
+
const parsed = parsePathLine(line);
|
|
632
|
+
if (parsed === '/dev/null') ctx.file.isDeleted = true;
|
|
633
|
+
else ctx.file.newPath = parsed;
|
|
634
|
+
ctx.file.preamble.push(line);
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const startHunk = (line, ctx) => {
|
|
638
|
+
const meta = parseHunkHeader(line);
|
|
639
|
+
if (!meta) return;
|
|
640
|
+
ctx.hunk = { ...meta, lines: [] };
|
|
641
|
+
ctx.file.hunks.push(ctx.hunk);
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const DIFF_PREFIX = [
|
|
645
|
+
{ prefix: 'diff --git ', apply: startGitFile, needFile: false },
|
|
646
|
+
{ prefix: 'Binary files ', apply: markBinary, needFile: true },
|
|
647
|
+
{ prefix: 'GIT binary', apply: markBinary, needFile: true },
|
|
648
|
+
{ prefix: 'new file mode', apply: markNewFile, needFile: true },
|
|
649
|
+
{ prefix: 'deleted file mode', apply: markDeleted, needFile: true },
|
|
650
|
+
{ prefix: '--- ', apply: setOldPath, needFile: true },
|
|
651
|
+
{ prefix: '+++ ', apply: setNewPath, needFile: true },
|
|
652
|
+
{ prefix: '@@ ', apply: startHunk, needFile: true },
|
|
653
|
+
];
|
|
654
|
+
|
|
655
|
+
const takeHunkLine = (line, hunk) => {
|
|
656
|
+
if (line.startsWith('\\')) {
|
|
657
|
+
const prev = hunk.lines[hunk.lines.length - 1];
|
|
658
|
+
if (prev) prev.noNl = true;
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const type = LINE_TYPE[line[0]];
|
|
662
|
+
if (type) {
|
|
663
|
+
hunk.lines.push({ type, text: line.slice(1), noNl: false });
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
hunk.lines.push({ type: 'ctx', text: line, noNl: false });
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const parseDiff = (text) => {
|
|
670
|
+
const files = [];
|
|
671
|
+
if (!text) return files;
|
|
672
|
+
const rawLines = text.split('\n');
|
|
673
|
+
if (rawLines.at(-1) === '') rawLines.pop();
|
|
674
|
+
const ctx = { files, file: null, hunk: null };
|
|
675
|
+
for (const raw of rawLines) {
|
|
676
|
+
const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
|
|
677
|
+
const rule = DIFF_PREFIX.find((entry) => line.startsWith(entry.prefix));
|
|
678
|
+
if (rule) {
|
|
679
|
+
if (rule.needFile && !ctx.file) continue;
|
|
680
|
+
rule.apply(line, ctx);
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
if (!ctx.file) continue;
|
|
684
|
+
if (!ctx.hunk) {
|
|
685
|
+
ctx.file.preamble.push(line);
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
takeHunkLine(line, ctx.hunk);
|
|
689
|
+
}
|
|
690
|
+
closeFile(ctx.file, files);
|
|
691
|
+
return files;
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
const splitHunk = (hunk) => {
|
|
695
|
+
const { oldStart, oldCount, newStart, newCount, header } = hunk;
|
|
696
|
+
const blocks = [];
|
|
697
|
+
let current = null;
|
|
698
|
+
let blockId = -1;
|
|
699
|
+
const lines = hunk.lines.map((line) => {
|
|
700
|
+
if (line.type === 'ctx') {
|
|
701
|
+
current = null;
|
|
702
|
+
return { ...line, blockId: null };
|
|
703
|
+
}
|
|
704
|
+
if (!current) {
|
|
705
|
+
blockId += 1;
|
|
706
|
+
current = { id: blockId };
|
|
707
|
+
blocks.push(current);
|
|
708
|
+
}
|
|
709
|
+
return { ...line, blockId };
|
|
710
|
+
});
|
|
711
|
+
const nextHunk = { oldStart, oldCount, newStart, newCount, header, lines };
|
|
712
|
+
return { hunk: nextHunk, blocks };
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
const otherContext = (contextSide, blockId) => {
|
|
716
|
+
if (contextSide === 'new') return 'new';
|
|
717
|
+
if (contextSide === 'old') return 'old';
|
|
718
|
+
return contextSide[blockId] ?? 'old';
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
const flattenBlock = (hunk, blockId, contextSide) => {
|
|
722
|
+
const out = [];
|
|
723
|
+
for (const line of hunk.lines) {
|
|
724
|
+
if (line.blockId === blockId || line.type === 'ctx') {
|
|
725
|
+
out.push(line);
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
const side = otherContext(contextSide, line.blockId);
|
|
729
|
+
if (side === 'old' && line.type === 'del') {
|
|
730
|
+
out.push({ type: 'ctx', text: line.text, noNl: line.noNl });
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
if (side === 'new' && line.type === 'add') {
|
|
734
|
+
out.push({ type: 'ctx', text: line.text, noNl: line.noNl });
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return out;
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
const countSides = (lines) => {
|
|
741
|
+
let oldCount = 0;
|
|
742
|
+
let newCount = 0;
|
|
743
|
+
for (const line of lines) {
|
|
744
|
+
if (line.type === 'ctx' || line.type === 'del') oldCount += 1;
|
|
745
|
+
if (line.type === 'ctx' || line.type === 'add') newCount += 1;
|
|
746
|
+
}
|
|
747
|
+
return { oldCount, newCount };
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
const formatHunkHeader = (oldStart, oldCount, newStart, newCount) =>
|
|
751
|
+
`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`;
|
|
752
|
+
|
|
753
|
+
const lineMark = (type) => LINE_MARK[type] ?? ' ';
|
|
754
|
+
|
|
755
|
+
const formatBodyLine = (line) => {
|
|
756
|
+
const rows = [`${lineMark(line.type)}${line.text}`];
|
|
757
|
+
if (line.noNl) rows.push('\');
|
|
758
|
+
return rows;
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
const formatPatch = (file, hunk, blockId, contextSide) => {
|
|
762
|
+
const lines = flattenBlock(hunk, blockId, contextSide);
|
|
763
|
+
const counts = countSides(lines);
|
|
764
|
+
const header = formatHunkHeader(
|
|
765
|
+
hunk.oldStart,
|
|
766
|
+
counts.oldCount,
|
|
767
|
+
hunk.newStart,
|
|
768
|
+
counts.newCount,
|
|
769
|
+
);
|
|
770
|
+
const parts = [...file.preamble, header];
|
|
771
|
+
for (const line of lines) {
|
|
772
|
+
for (const row of formatBodyLine(line)) parts.push(row);
|
|
773
|
+
}
|
|
774
|
+
return `${parts.join('\n')}\n`;
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
const LAYOUT_ALIGN = Object.assign(Object.create(null), {
|
|
778
|
+
mixed: alignPairs,
|
|
779
|
+
side: alignSplit,
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
const windowAroundBlock = (lines, blockId, radius) => {
|
|
783
|
+
let start = -1;
|
|
784
|
+
let end = 0;
|
|
785
|
+
for (let i = 0; i < lines.length; i++) {
|
|
786
|
+
if (lines[i].blockId !== blockId) continue;
|
|
787
|
+
if (start < 0) start = i;
|
|
788
|
+
end = i + 1;
|
|
789
|
+
}
|
|
790
|
+
if (start < 0) return lines;
|
|
791
|
+
const from = Math.max(0, start - radius);
|
|
792
|
+
const to = Math.min(lines.length, end + radius);
|
|
793
|
+
return lines.slice(from, to);
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
const displayLines = (hunk, blockId, layout = 'unified') => {
|
|
797
|
+
const flat = flattenBlock(hunk, blockId, 'new');
|
|
798
|
+
const windowed = windowAroundBlock(flat, blockId, DISPLAY_CONTEXT);
|
|
799
|
+
const lines = windowed.map((line) => ({ ...line }));
|
|
800
|
+
const painted = attachInline(lines);
|
|
801
|
+
const align = LAYOUT_ALIGN[layout];
|
|
802
|
+
if (align) return align(painted);
|
|
803
|
+
return painted;
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
const splitLines = (content) => {
|
|
807
|
+
if (content === '') return { lines: [''], noNl: true };
|
|
808
|
+
const parts = content.split('\n');
|
|
809
|
+
const noNl = parts.at(-1) !== '';
|
|
810
|
+
if (!noNl) parts.pop();
|
|
811
|
+
return { lines: parts, noNl };
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
const synthesizeNewFile = (relPath, content) => {
|
|
815
|
+
const { lines, noNl } = splitLines(content);
|
|
816
|
+
const body = lines.map((text, index) => ({
|
|
817
|
+
type: 'add',
|
|
818
|
+
text,
|
|
819
|
+
noNl: noNl && index === lines.length - 1,
|
|
820
|
+
blockId: 0,
|
|
821
|
+
}));
|
|
822
|
+
const oldStart = 0;
|
|
823
|
+
const oldCount = 0;
|
|
824
|
+
const newStart = 1;
|
|
825
|
+
const newCount = body.length;
|
|
826
|
+
const header = formatHunkHeader(oldStart, oldCount, newStart, newCount);
|
|
827
|
+
const hunk = { oldStart, oldCount, newStart, newCount, header, lines: body };
|
|
828
|
+
const preamble = [
|
|
829
|
+
`diff --git a/${relPath} b/${relPath}`,
|
|
830
|
+
'new file mode 100644',
|
|
831
|
+
'--- /dev/null',
|
|
832
|
+
`+++ b/${relPath}`,
|
|
833
|
+
];
|
|
834
|
+
return {
|
|
835
|
+
oldPath: relPath,
|
|
836
|
+
newPath: relPath,
|
|
837
|
+
isNew: true,
|
|
838
|
+
isDeleted: false,
|
|
839
|
+
isBinary: false,
|
|
840
|
+
preamble,
|
|
841
|
+
hunks: [hunk],
|
|
842
|
+
};
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
const itemsFromFiles = (files, origin) => {
|
|
846
|
+
const items = [];
|
|
847
|
+
for (const file of files) {
|
|
848
|
+
if (file.isBinary) {
|
|
849
|
+
const hunk = null;
|
|
850
|
+
const blockId = null;
|
|
851
|
+
const patchAdd = '';
|
|
852
|
+
const patchRevert = '';
|
|
853
|
+
items.push({ origin, file, hunk, blockId, patchAdd, patchRevert });
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
for (const rawHunk of file.hunks) {
|
|
857
|
+
const { hunk, blocks } = splitHunk(rawHunk);
|
|
858
|
+
for (const block of blocks) {
|
|
859
|
+
const blockId = block.id;
|
|
860
|
+
const indexSide = INDEX_SIDE[origin] ?? 'old';
|
|
861
|
+
const patchAdd = formatPatch(file, hunk, blockId, indexSide);
|
|
862
|
+
const patchRevert = formatPatch(file, hunk, blockId, 'new');
|
|
863
|
+
items.push({ origin, file, hunk, blockId, patchAdd, patchRevert });
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return items;
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
const refreshIndexPatches = (items, item) => {
|
|
871
|
+
if (!item.hunk || !item.file) return;
|
|
872
|
+
const siblings = [];
|
|
873
|
+
for (const other of items) {
|
|
874
|
+
if (other.hunk === item.hunk && other.file === item.file) {
|
|
875
|
+
siblings.push(other);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
const sides = Object.create(null);
|
|
879
|
+
for (const other of siblings) {
|
|
880
|
+
sides[other.blockId] = INDEX_SIDE[other.origin] ?? 'old';
|
|
881
|
+
}
|
|
882
|
+
for (const other of siblings) {
|
|
883
|
+
other.patchAdd = formatPatch(other.file, other.hunk, other.blockId, sides);
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
module.exports = {
|
|
888
|
+
LCS_CELL_LIMIT,
|
|
889
|
+
DISPLAY_CONTEXT,
|
|
890
|
+
PAIR_MIN_SIM,
|
|
891
|
+
diffChars,
|
|
892
|
+
similarity,
|
|
893
|
+
pairScore,
|
|
894
|
+
pairIndices,
|
|
895
|
+
attachInline,
|
|
896
|
+
parseDiff,
|
|
897
|
+
splitHunk,
|
|
898
|
+
flattenBlock,
|
|
899
|
+
formatPatch,
|
|
900
|
+
displayLines,
|
|
901
|
+
alignSplit,
|
|
902
|
+
synthesizeNewFile,
|
|
903
|
+
itemsFromFiles,
|
|
904
|
+
refreshIndexPatches,
|
|
905
|
+
countSides,
|
|
906
|
+
};
|