reslop 0.1.7 → 0.1.8
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 +19 -1
- package/README.md +1 -1
- package/bin/reslop.js +3 -1
- package/lib/capabilities.js +106 -0
- package/lib/cli.js +18 -14
- package/lib/deps/changes.js +331 -0
- package/lib/deps/items.js +131 -0
- package/lib/deps/manifest.js +145 -0
- package/lib/deps/merge.js +120 -0
- package/lib/deps/proposals.js +164 -0
- package/lib/deps/reports.js +180 -0
- package/lib/deps/rows.js +177 -0
- package/lib/deps/scan.js +138 -0
- package/lib/deps/usage.js +68 -0
- package/lib/deps/versions.js +119 -0
- package/lib/deps.js +65 -1348
- package/lib/diff/align.js +226 -0
- package/lib/diff/display.js +101 -0
- package/lib/diff/edit.js +47 -0
- package/lib/diff/inline.js +300 -0
- package/lib/diff/items.js +101 -0
- package/lib/diff/parse.js +156 -0
- package/lib/diff/patch.js +103 -0
- package/lib/diff.js +13 -932
- package/lib/editor.js +37 -0
- package/lib/files.js +88 -3
- package/lib/git/branches.js +291 -0
- package/lib/git/changes.js +164 -0
- package/lib/git/dependencies.js +489 -0
- package/lib/git/proc.js +162 -0
- package/lib/git/read.js +253 -0
- package/lib/git/run.js +100 -0
- package/lib/git.js +82 -1193
- package/lib/github.js +92 -222
- package/lib/gitlab.js +72 -216
- package/lib/highlight/bash.js +5 -9
- package/lib/highlight/core.js +1 -1
- package/lib/highlight/css.js +1 -3
- package/lib/highlight/csv.js +1 -3
- package/lib/highlight/dart.js +6 -20
- package/lib/highlight/dot.js +13 -13
- package/lib/highlight/html.js +1 -5
- package/lib/highlight/js.js +151 -115
- package/lib/highlight/json.js +1 -3
- package/lib/highlight/log.js +1 -3
- package/lib/highlight/txt.js +5 -10
- package/lib/highlight.js +4 -4
- package/lib/keys.js +59 -167
- package/lib/npm/commands.js +100 -0
- package/lib/remote/changes.js +37 -0
- package/lib/remote/headers.js +36 -0
- package/lib/remote/http.js +122 -0
- package/lib/remote/notes.js +94 -0
- package/lib/render/branches.js +218 -0
- package/lib/render/chrome.js +575 -0
- package/lib/render/diff.js +307 -0
- package/lib/render/files.js +166 -0
- package/lib/render/frame.js +197 -0
- package/lib/render/notes.js +201 -0
- package/lib/render/present.js +25 -0
- package/lib/render/primitives.js +200 -0
- package/lib/render.js +13 -1687
- package/lib/review/format.js +434 -0
- package/lib/review/storage.js +150 -0
- package/lib/review/store.js +178 -0
- package/lib/review/templates.js +49 -0
- package/lib/review.js +14 -747
- package/lib/select.js +3 -5
- package/lib/session/accessors.js +25 -0
- package/lib/session/actions.js +275 -0
- package/lib/session/branches.js +304 -0
- package/lib/session/change-actions.js +320 -0
- package/lib/session/commit.js +124 -0
- package/lib/session/compose.js +540 -0
- package/lib/session/dispatch.js +105 -0
- package/lib/session/files-pane.js +234 -0
- package/lib/session/items.js +177 -0
- package/lib/session/lifecycle.js +476 -0
- package/lib/session/load.js +85 -0
- package/lib/session/navigation.js +93 -0
- package/lib/session/ops.js +110 -0
- package/lib/session/origins.js +7 -0
- package/lib/session/pointer.js +154 -0
- package/lib/session/progress.js +29 -0
- package/lib/session/review.js +88 -0
- package/lib/session/templates.js +64 -0
- package/lib/session/terminal.js +169 -0
- package/lib/session/todos.js +269 -0
- package/lib/session/update.js +125 -0
- package/lib/session/view.js +129 -0
- package/lib/session/watch.js +219 -0
- package/lib/session.js +574 -2354
- package/lib/source.js +11 -11
- package/lib/sys.js +25 -4
- package/lib/wrap.js +4 -2
- package/package.json +2 -2
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { diffChars, tokenSimilarity } = require('./inline.js');
|
|
4
|
+
|
|
5
|
+
const PAIR_MIN_SIM = 0.3;
|
|
6
|
+
const PAIR_HEAD_WEIGHT = 0.4;
|
|
7
|
+
const PAIR_TOKEN_WEIGHT = 0.6;
|
|
8
|
+
const TOKEN_RE = /[A-Za-z_$][\w$]*|\d+|[^\s]/g;
|
|
9
|
+
const CTX_TYPES = ['ctx', 'warn', 'note', 'noteSep'];
|
|
10
|
+
|
|
11
|
+
const isCtxType = (type) => CTX_TYPES.includes(type);
|
|
12
|
+
|
|
13
|
+
const lineTokens = (text) => {
|
|
14
|
+
TOKEN_RE.lastIndex = 0;
|
|
15
|
+
return text.match(TOKEN_RE) ?? [];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const pairScore = (a, b) => {
|
|
19
|
+
const token = tokenSimilarity(a, b);
|
|
20
|
+
const headA = a[0];
|
|
21
|
+
const headB = b[0];
|
|
22
|
+
const head = headA && headA === headB ? 1 : 0;
|
|
23
|
+
return PAIR_HEAD_WEIGHT * head + PAIR_TOKEN_WEIGHT * token;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const comparePair = (left, right) => {
|
|
27
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
28
|
+
if (left.dist !== right.dist) return left.dist - right.dist;
|
|
29
|
+
if (left.i !== right.i) return left.i - right.i;
|
|
30
|
+
return left.j - right.j;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const pairCandidates = (delTexts, addTexts) => {
|
|
34
|
+
if (!delTexts.length || !addTexts.length) return [];
|
|
35
|
+
const delTokens = delTexts.map(lineTokens);
|
|
36
|
+
const addTokens = addTexts.map(lineTokens);
|
|
37
|
+
const candidates = [];
|
|
38
|
+
for (let i = 0; i < delTexts.length; i++) {
|
|
39
|
+
for (let j = 0; j < addTexts.length; j++) {
|
|
40
|
+
const score = pairScore(delTokens[i], addTokens[j]);
|
|
41
|
+
if (score < PAIR_MIN_SIM) continue;
|
|
42
|
+
const dist = Math.abs(i - j);
|
|
43
|
+
candidates.push({ i, j, score, dist });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
candidates.sort(comparePair);
|
|
47
|
+
return candidates;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const pairIndices = (delTexts, addTexts) => {
|
|
51
|
+
const delToAdd = new Array(delTexts.length).fill(-1);
|
|
52
|
+
const addUsed = new Array(addTexts.length).fill(false);
|
|
53
|
+
const candidates = pairCandidates(delTexts, addTexts);
|
|
54
|
+
for (const pair of candidates) {
|
|
55
|
+
if (delToAdd[pair.i] >= 0 || addUsed[pair.j]) continue;
|
|
56
|
+
delToAdd[pair.i] = pair.j;
|
|
57
|
+
addUsed[pair.j] = true;
|
|
58
|
+
}
|
|
59
|
+
if (delTexts.length !== addTexts.length) return delToAdd;
|
|
60
|
+
for (let i = 0; i < delTexts.length; i++) {
|
|
61
|
+
if (delToAdd[i] >= 0 || addUsed[i]) continue;
|
|
62
|
+
delToAdd[i] = i;
|
|
63
|
+
addUsed[i] = true;
|
|
64
|
+
}
|
|
65
|
+
return delToAdd;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const changeSides = (block) => {
|
|
69
|
+
const dels = [];
|
|
70
|
+
const adds = [];
|
|
71
|
+
for (const line of block) {
|
|
72
|
+
if (line.type === 'del') dels.push(line);
|
|
73
|
+
if (line.type === 'add') adds.push(line);
|
|
74
|
+
}
|
|
75
|
+
return { dels, adds };
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const applyInlineSpans = (dels, adds, delToAdd) => {
|
|
79
|
+
const addSeen = new Set();
|
|
80
|
+
for (let i = 0; i < dels.length; i++) {
|
|
81
|
+
const j = delToAdd[i];
|
|
82
|
+
if (j < 0) {
|
|
83
|
+
dels[i].spans = [{ text: dels[i].text, changed: true }];
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
addSeen.add(j);
|
|
87
|
+
const diff = diffChars(dels[i].text, adds[j].text);
|
|
88
|
+
dels[i].spans = diff.oldSpans;
|
|
89
|
+
adds[j].spans = diff.newSpans;
|
|
90
|
+
}
|
|
91
|
+
for (let j = 0; j < adds.length; j++) {
|
|
92
|
+
if (addSeen.has(j)) continue;
|
|
93
|
+
adds[j].spans = [{ text: adds[j].text, changed: true }];
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const attachInline = (lines) => {
|
|
98
|
+
const painted = lines.map((line) => ({ ...line }));
|
|
99
|
+
const { dels, adds } = changeSides(painted);
|
|
100
|
+
const delToAdd = pairIndices(
|
|
101
|
+
dels.map((line) => line.text),
|
|
102
|
+
adds.map((line) => line.text),
|
|
103
|
+
);
|
|
104
|
+
applyInlineSpans(dels, adds, delToAdd);
|
|
105
|
+
for (const line of painted) {
|
|
106
|
+
if (isCtxType(line.type)) {
|
|
107
|
+
line.spans = [{ text: line.text, changed: false }];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return painted;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const addPartners = (delToAdd, addCount) => {
|
|
114
|
+
const addToDel = new Array(addCount).fill(-1);
|
|
115
|
+
for (let i = 0; i < delToAdd.length; i++) {
|
|
116
|
+
const j = delToAdd[i];
|
|
117
|
+
if (j >= 0) addToDel[j] = i;
|
|
118
|
+
}
|
|
119
|
+
return addToDel;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const walkAligned = (dels, adds, delToAdd, emit) => {
|
|
123
|
+
const addToDel = addPartners(delToAdd, adds.length);
|
|
124
|
+
const usedAdd = new Array(adds.length).fill(false);
|
|
125
|
+
let addPos = 0;
|
|
126
|
+
const takeUnpairedAdds = (limit) => {
|
|
127
|
+
while (addPos < limit) {
|
|
128
|
+
const free = addToDel[addPos] < 0 && !usedAdd[addPos];
|
|
129
|
+
if (free) {
|
|
130
|
+
emit.unpairedAdd(adds[addPos]);
|
|
131
|
+
usedAdd[addPos] = true;
|
|
132
|
+
}
|
|
133
|
+
addPos += 1;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
for (let i = 0; i < dels.length; i++) {
|
|
137
|
+
const j = delToAdd[i];
|
|
138
|
+
if (j < 0) {
|
|
139
|
+
emit.unpairedDel(dels[i]);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
takeUnpairedAdds(j);
|
|
143
|
+
emit.pair(dels[i], adds[j]);
|
|
144
|
+
usedAdd[j] = true;
|
|
145
|
+
if (addPos <= j) addPos = j + 1;
|
|
146
|
+
}
|
|
147
|
+
takeUnpairedAdds(adds.length);
|
|
148
|
+
for (let j = 0; j < adds.length; j++) {
|
|
149
|
+
if (!usedAdd[j]) emit.unpairedAdd(adds[j]);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const alignChangeBlock = (dels, adds, delToAdd) => {
|
|
154
|
+
const out = [];
|
|
155
|
+
walkAligned(dels, adds, delToAdd, {
|
|
156
|
+
unpairedDel: (del) => {
|
|
157
|
+
out.push(del);
|
|
158
|
+
},
|
|
159
|
+
unpairedAdd: (add) => {
|
|
160
|
+
out.push(add);
|
|
161
|
+
},
|
|
162
|
+
pair: (del, add) => {
|
|
163
|
+
out.push(del);
|
|
164
|
+
out.push(add);
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
return out;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const mapHunkBlocks = (lines, onCtx, onChange) => {
|
|
171
|
+
const out = [];
|
|
172
|
+
let i = 0;
|
|
173
|
+
while (i < lines.length) {
|
|
174
|
+
if (isCtxType(lines[i].type)) {
|
|
175
|
+
out.push(onCtx(lines[i]));
|
|
176
|
+
i += 1;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const start = i;
|
|
180
|
+
while (i < lines.length && !isCtxType(lines[i].type)) i += 1;
|
|
181
|
+
const block = lines.slice(start, i);
|
|
182
|
+
const { dels, adds } = changeSides(block);
|
|
183
|
+
const delToAdd = pairIndices(
|
|
184
|
+
dels.map((line) => line.text),
|
|
185
|
+
adds.map((line) => line.text),
|
|
186
|
+
);
|
|
187
|
+
for (const row of onChange(dels, adds, delToAdd)) out.push(row);
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const alignPairs = (lines) =>
|
|
193
|
+
mapHunkBlocks(lines, (line) => line, alignChangeBlock);
|
|
194
|
+
|
|
195
|
+
const alignSplitBlock = (dels, adds, delToAdd) => {
|
|
196
|
+
const out = [];
|
|
197
|
+
walkAligned(dels, adds, delToAdd, {
|
|
198
|
+
unpairedDel: (del) => {
|
|
199
|
+
out.push({ left: del, right: null });
|
|
200
|
+
},
|
|
201
|
+
unpairedAdd: (add) => {
|
|
202
|
+
out.push({ left: null, right: add });
|
|
203
|
+
},
|
|
204
|
+
pair: (del, add) => {
|
|
205
|
+
out.push({ left: del, right: add });
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
return out;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const splitCtxRow = (line) => ({ left: line, right: line });
|
|
212
|
+
|
|
213
|
+
const alignSplit = (lines) =>
|
|
214
|
+
mapHunkBlocks(lines, splitCtxRow, alignSplitBlock);
|
|
215
|
+
|
|
216
|
+
const LAYOUT_ALIGN = {
|
|
217
|
+
mixed: alignPairs,
|
|
218
|
+
side: alignSplit,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
module.exports = {
|
|
222
|
+
isCtxType,
|
|
223
|
+
pairIndices,
|
|
224
|
+
attachInline,
|
|
225
|
+
LAYOUT_ALIGN,
|
|
226
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const align = require('./align.js');
|
|
4
|
+
const { isCtxType, attachInline, LAYOUT_ALIGN } = align;
|
|
5
|
+
const { flattenBlock } = require('./patch.js');
|
|
6
|
+
|
|
7
|
+
const DISPLAY_CONTEXT = 3;
|
|
8
|
+
|
|
9
|
+
const windowAroundBlock = (lines, blockId, radius) => {
|
|
10
|
+
let start = -1;
|
|
11
|
+
let end = 0;
|
|
12
|
+
for (let i = 0; i < lines.length; i++) {
|
|
13
|
+
if (lines[i].blockId !== blockId) continue;
|
|
14
|
+
if (start < 0) start = i;
|
|
15
|
+
end = i + 1;
|
|
16
|
+
}
|
|
17
|
+
if (start < 0) return lines;
|
|
18
|
+
const from = Math.max(0, start - radius);
|
|
19
|
+
const to = Math.min(lines.length, end + radius);
|
|
20
|
+
return lines.slice(from, to);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const insertAddIndex = (lines) => {
|
|
24
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
25
|
+
if (isCtxType(lines[i].type)) continue;
|
|
26
|
+
return i + 1;
|
|
27
|
+
}
|
|
28
|
+
return lines.length;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const taggedAdd = (text, blockId, start, end) => ({
|
|
32
|
+
type: 'add',
|
|
33
|
+
text,
|
|
34
|
+
noNl: false,
|
|
35
|
+
blockId,
|
|
36
|
+
wrap: true,
|
|
37
|
+
editStart: start,
|
|
38
|
+
editEnd: end,
|
|
39
|
+
editLast: true,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const replaceAdds = (lines, overlay, blockId) => {
|
|
43
|
+
const text = overlay.text ?? '';
|
|
44
|
+
const keepEmpty = overlay.keepEmpty === true;
|
|
45
|
+
if (text === '' && !keepEmpty) {
|
|
46
|
+
return lines.filter((line) => line.type !== 'add');
|
|
47
|
+
}
|
|
48
|
+
const texts = text.split('\n');
|
|
49
|
+
const tagged = [];
|
|
50
|
+
let offset = 0;
|
|
51
|
+
for (let i = 0; i < texts.length; i++) {
|
|
52
|
+
const row = texts[i];
|
|
53
|
+
const start = offset;
|
|
54
|
+
const end = start + row.length;
|
|
55
|
+
const isLast = i === texts.length - 1;
|
|
56
|
+
tagged.push(taggedAdd(row, blockId, start, end));
|
|
57
|
+
offset = isLast ? end : end + 1;
|
|
58
|
+
}
|
|
59
|
+
const out = [];
|
|
60
|
+
let placed = 0;
|
|
61
|
+
let lastAdd = -1;
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
if (line.type !== 'add') {
|
|
64
|
+
out.push(line);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (placed >= tagged.length) continue;
|
|
68
|
+
out.push(tagged[placed]);
|
|
69
|
+
lastAdd = out.length - 1;
|
|
70
|
+
placed += 1;
|
|
71
|
+
}
|
|
72
|
+
const extras = tagged.slice(placed);
|
|
73
|
+
if (!extras.length) return out;
|
|
74
|
+
const at = lastAdd >= 0 ? lastAdd + 1 : insertAddIndex(out);
|
|
75
|
+
out.splice(at, 0, ...extras);
|
|
76
|
+
return out;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const blockAddText = (hunk, blockId) => {
|
|
80
|
+
if (!hunk) return '';
|
|
81
|
+
const parts = [];
|
|
82
|
+
for (const line of hunk.lines) {
|
|
83
|
+
if (line.type !== 'add') continue;
|
|
84
|
+
if (line.blockId !== blockId) continue;
|
|
85
|
+
parts.push(line.text);
|
|
86
|
+
}
|
|
87
|
+
return parts.join('\n');
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const displayLines = (hunk, blockId, layout = 'unified', radius, overlay) => {
|
|
91
|
+
const flat = flattenBlock(hunk, blockId, 'new');
|
|
92
|
+
const source = overlay ? replaceAdds(flat, overlay, blockId) : flat;
|
|
93
|
+
const around = radius ?? DISPLAY_CONTEXT;
|
|
94
|
+
const windowed = windowAroundBlock(source, blockId, around);
|
|
95
|
+
const painted = attachInline(windowed);
|
|
96
|
+
const alignRows = LAYOUT_ALIGN[layout];
|
|
97
|
+
if (alignRows) return alignRows(painted);
|
|
98
|
+
return painted;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
module.exports = { DISPLAY_CONTEXT, displayLines, blockAddText };
|
package/lib/diff/edit.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const contentLines = (content) => {
|
|
4
|
+
if (content === '') return { lines: [], ended: false };
|
|
5
|
+
const ended = content.endsWith('\n');
|
|
6
|
+
const body = ended ? content.slice(0, -1) : content;
|
|
7
|
+
return { lines: body.split('\n'), ended };
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const joinLines = (lines, ended) => {
|
|
11
|
+
const body = lines.join('\n');
|
|
12
|
+
if (!ended) return body;
|
|
13
|
+
if (!lines.length) return '\n';
|
|
14
|
+
return `${body}\n`;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const addBlockRange = (hunk, blockId) => {
|
|
18
|
+
let newLine = hunk.newStart;
|
|
19
|
+
let start = 0;
|
|
20
|
+
let count = 0;
|
|
21
|
+
for (const line of hunk.lines) {
|
|
22
|
+
if (line.type === 'del') {
|
|
23
|
+
if (line.blockId === blockId && start === 0) start = newLine;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (line.type === 'add' && line.blockId === blockId) {
|
|
27
|
+
if (count === 0) start = newLine;
|
|
28
|
+
count += 1;
|
|
29
|
+
}
|
|
30
|
+
newLine += 1;
|
|
31
|
+
}
|
|
32
|
+
if (start === 0) start = hunk.newStart;
|
|
33
|
+
return { start, count };
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const replaceBlockAdds = (content, hunk, blockId, text) => {
|
|
37
|
+
const range = addBlockRange(hunk, blockId);
|
|
38
|
+
const parsed = contentLines(content);
|
|
39
|
+
const last = parsed.lines.length;
|
|
40
|
+
const from = Math.min(last, Math.max(0, range.start - 1));
|
|
41
|
+
const added = text === '' ? [] : text.split('\n');
|
|
42
|
+
const next = parsed.lines.slice();
|
|
43
|
+
next.splice(from, range.count, ...added);
|
|
44
|
+
return joinLines(next, parsed.ended);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
module.exports = { addBlockRange, replaceBlockAdds };
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { graphemes } = require('../ansi.js');
|
|
4
|
+
|
|
5
|
+
const LCS_CELL_LIMIT = 250000;
|
|
6
|
+
const NOISY_TINY_SPANS = 6;
|
|
7
|
+
const WORD_REFINE_MIN = 0.6;
|
|
8
|
+
const PIECE_RE = /[A-Za-z_$][\w$]*|\d+|\s+|./g;
|
|
9
|
+
const CAMEL_RE = /[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|\d+|./g;
|
|
10
|
+
|
|
11
|
+
const collapseSpans = (parts) => {
|
|
12
|
+
const spans = [];
|
|
13
|
+
for (const part of parts) {
|
|
14
|
+
const last = spans[spans.length - 1];
|
|
15
|
+
if (last && last.changed === part.changed) {
|
|
16
|
+
last.text += part.text;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
spans.push({ text: part.text, changed: part.changed });
|
|
20
|
+
}
|
|
21
|
+
return spans;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const isNoisy = (spans) => {
|
|
25
|
+
let tiny = 0;
|
|
26
|
+
for (const span of spans) {
|
|
27
|
+
if (span.changed && graphemes(span.text).length <= 2) tiny += 1;
|
|
28
|
+
}
|
|
29
|
+
return tiny > NOISY_TINY_SPANS;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const wholeChanged = (a, b) => ({
|
|
33
|
+
oldSpans: a.length ? [{ text: a.join(''), changed: true }] : [],
|
|
34
|
+
newSpans: b.length ? [{ text: b.join(''), changed: true }] : [],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const lcsLength = (a, b) => {
|
|
38
|
+
const n = a.length;
|
|
39
|
+
const m = b.length;
|
|
40
|
+
if (n === 0 || m === 0) return 0;
|
|
41
|
+
if (n * m > LCS_CELL_LIMIT) {
|
|
42
|
+
let lo = 0;
|
|
43
|
+
while (lo < n && lo < m && a[lo] === b[lo]) lo += 1;
|
|
44
|
+
let hiA = n;
|
|
45
|
+
let hiB = m;
|
|
46
|
+
while (hiA > lo && hiB > lo && a[hiA - 1] === b[hiB - 1]) {
|
|
47
|
+
hiA -= 1;
|
|
48
|
+
hiB -= 1;
|
|
49
|
+
}
|
|
50
|
+
return lo + (n - hiA);
|
|
51
|
+
}
|
|
52
|
+
let prev = new Uint32Array(m + 1);
|
|
53
|
+
let cur = new Uint32Array(m + 1);
|
|
54
|
+
for (let i = 1; i <= n; i++) {
|
|
55
|
+
const ai = a[i - 1];
|
|
56
|
+
for (let j = 1; j <= m; j++) {
|
|
57
|
+
if (ai === b[j - 1]) cur[j] = prev[j - 1] + 1;
|
|
58
|
+
else cur[j] = prev[j] > cur[j - 1] ? prev[j] : cur[j - 1];
|
|
59
|
+
}
|
|
60
|
+
const swap = prev;
|
|
61
|
+
prev = cur;
|
|
62
|
+
cur = swap;
|
|
63
|
+
cur.fill(0);
|
|
64
|
+
}
|
|
65
|
+
return prev[m];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const tokenSimilarity = (a, b) => {
|
|
69
|
+
if (a.length === 0 && b.length === 0) return 1;
|
|
70
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
71
|
+
const lcs = lcsLength(a, b);
|
|
72
|
+
return (2 * lcs) / (a.length + b.length);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const similarity = (left, right) =>
|
|
76
|
+
tokenSimilarity(graphemes(left), graphemes(right));
|
|
77
|
+
|
|
78
|
+
const fillLcsTable = (a, b) => {
|
|
79
|
+
const n = a.length;
|
|
80
|
+
const m = b.length;
|
|
81
|
+
const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
|
|
82
|
+
for (let i = 1; i <= n; i++) {
|
|
83
|
+
const ai = a[i - 1];
|
|
84
|
+
const row = dp[i];
|
|
85
|
+
const prev = dp[i - 1];
|
|
86
|
+
for (let j = 1; j <= m; j++) {
|
|
87
|
+
if (ai === b[j - 1]) row[j] = prev[j - 1] + 1;
|
|
88
|
+
else row[j] = prev[j] > row[j - 1] ? prev[j] : row[j - 1];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return dp;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const tracebackLcs = (a, b, dp) => {
|
|
95
|
+
const rev = [];
|
|
96
|
+
let i = a.length;
|
|
97
|
+
let j = b.length;
|
|
98
|
+
while (i > 0 && j > 0) {
|
|
99
|
+
if (a[i - 1] === b[j - 1]) {
|
|
100
|
+
rev.push({ type: 'eq', text: a[i - 1] });
|
|
101
|
+
i -= 1;
|
|
102
|
+
j -= 1;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (dp[i - 1][j] >= dp[i][j - 1]) {
|
|
106
|
+
rev.push({ type: 'del', text: a[i - 1] });
|
|
107
|
+
i -= 1;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
rev.push({ type: 'add', text: b[j - 1] });
|
|
111
|
+
j -= 1;
|
|
112
|
+
}
|
|
113
|
+
while (i > 0) {
|
|
114
|
+
rev.push({ type: 'del', text: a[i - 1] });
|
|
115
|
+
i -= 1;
|
|
116
|
+
}
|
|
117
|
+
while (j > 0) {
|
|
118
|
+
rev.push({ type: 'add', text: b[j - 1] });
|
|
119
|
+
j -= 1;
|
|
120
|
+
}
|
|
121
|
+
rev.reverse();
|
|
122
|
+
return rev;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const lcsOps = (a, b) => tracebackLcs(a, b, fillLcsTable(a, b));
|
|
126
|
+
|
|
127
|
+
const lcsSpans = (a, b) => {
|
|
128
|
+
const oldParts = [];
|
|
129
|
+
const newParts = [];
|
|
130
|
+
for (const op of lcsOps(a, b)) {
|
|
131
|
+
if (op.type === 'eq') {
|
|
132
|
+
oldParts.push({ text: op.text, changed: false });
|
|
133
|
+
newParts.push({ text: op.text, changed: false });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (op.type === 'del') {
|
|
137
|
+
oldParts.push({ text: op.text, changed: true });
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
newParts.push({ text: op.text, changed: true });
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
oldSpans: collapseSpans(oldParts),
|
|
144
|
+
newSpans: collapseSpans(newParts),
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const trimSharedEnds = (a, b) => {
|
|
149
|
+
let lo = 0;
|
|
150
|
+
while (lo < a.length && lo < b.length && a[lo] === b[lo]) lo += 1;
|
|
151
|
+
let hiA = a.length;
|
|
152
|
+
let hiB = b.length;
|
|
153
|
+
while (hiA > lo && hiB > lo && a[hiA - 1] === b[hiB - 1]) {
|
|
154
|
+
hiA -= 1;
|
|
155
|
+
hiB -= 1;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
prefix: a.slice(0, lo),
|
|
159
|
+
suffix: a.slice(hiA),
|
|
160
|
+
midA: a.slice(lo, hiA),
|
|
161
|
+
midB: b.slice(lo, hiB),
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const diffTokenMiddle = (midA, midB) => {
|
|
166
|
+
if (midA.length * midB.length > LCS_CELL_LIMIT) {
|
|
167
|
+
return wholeChanged(midA, midB);
|
|
168
|
+
}
|
|
169
|
+
if (midA.length === 0 && midB.length === 0) {
|
|
170
|
+
return { oldSpans: [], newSpans: [] };
|
|
171
|
+
}
|
|
172
|
+
if (midA.length === 0 || midB.length === 0) return wholeChanged(midA, midB);
|
|
173
|
+
return lcsSpans(midA, midB);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const joinAffix = (prefix, mid, suffix) => {
|
|
177
|
+
const parts = [];
|
|
178
|
+
if (prefix.length) {
|
|
179
|
+
parts.push({ text: prefix.join(''), changed: false });
|
|
180
|
+
}
|
|
181
|
+
for (const span of mid) parts.push(span);
|
|
182
|
+
if (suffix.length) {
|
|
183
|
+
parts.push({ text: suffix.join(''), changed: false });
|
|
184
|
+
}
|
|
185
|
+
return collapseSpans(parts);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const joinTrimmedSpans = (prefix, mid, suffix) => ({
|
|
189
|
+
oldSpans: joinAffix(prefix, mid.oldSpans, suffix),
|
|
190
|
+
newSpans: joinAffix(prefix, mid.newSpans, suffix),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const diffTokens = (a, b) => {
|
|
194
|
+
if (a.length === 0 && b.length === 0) {
|
|
195
|
+
return { oldSpans: [], newSpans: [] };
|
|
196
|
+
}
|
|
197
|
+
if (a.length === 0 || b.length === 0) return wholeChanged(a, b);
|
|
198
|
+
const trimmed = trimSharedEnds(a, b);
|
|
199
|
+
const mid = diffTokenMiddle(trimmed.midA, trimmed.midB);
|
|
200
|
+
const joined = joinTrimmedSpans(trimmed.prefix, mid, trimmed.suffix);
|
|
201
|
+
if (isNoisy(joined.oldSpans) || isNoisy(joined.newSpans)) {
|
|
202
|
+
return wholeChanged(a, b);
|
|
203
|
+
}
|
|
204
|
+
return joined;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const splitCamel = (part) => {
|
|
208
|
+
if (!/[a-z]/.test(part) || !/[A-Z]/.test(part)) return [part];
|
|
209
|
+
return part.match(CAMEL_RE) ?? [part];
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const splitIdentWords = (part) => {
|
|
213
|
+
const words = [];
|
|
214
|
+
for (const chunk of part.split(/(_+)/)) {
|
|
215
|
+
if (chunk === '') continue;
|
|
216
|
+
if (chunk.includes('_')) {
|
|
217
|
+
words.push(chunk);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
for (const word of splitCamel(chunk)) words.push(word);
|
|
221
|
+
}
|
|
222
|
+
return words;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const splitPieces = (text) => {
|
|
226
|
+
if (text === '') return [];
|
|
227
|
+
PIECE_RE.lastIndex = 0;
|
|
228
|
+
const coarse = text.match(PIECE_RE) ?? [];
|
|
229
|
+
const pieces = [];
|
|
230
|
+
for (const part of coarse) {
|
|
231
|
+
for (const word of splitIdentWords(part)) pieces.push(word);
|
|
232
|
+
}
|
|
233
|
+
return pieces;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const spansFromWordOps = (ops) => {
|
|
237
|
+
const oldParts = [];
|
|
238
|
+
const newParts = [];
|
|
239
|
+
let i = 0;
|
|
240
|
+
while (i < ops.length) {
|
|
241
|
+
const op = ops[i];
|
|
242
|
+
if (op.type === 'eq') {
|
|
243
|
+
oldParts.push({ text: op.text, changed: false });
|
|
244
|
+
newParts.push({ text: op.text, changed: false });
|
|
245
|
+
i += 1;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const dels = [];
|
|
249
|
+
const adds = [];
|
|
250
|
+
while (i < ops.length && ops[i].type !== 'eq') {
|
|
251
|
+
if (ops[i].type === 'del') dels.push(ops[i].text);
|
|
252
|
+
else adds.push(ops[i].text);
|
|
253
|
+
i += 1;
|
|
254
|
+
}
|
|
255
|
+
const one = dels.length === 1 && adds.length === 1;
|
|
256
|
+
const close = one && similarity(dels[0], adds[0]) >= WORD_REFINE_MIN;
|
|
257
|
+
if (close) {
|
|
258
|
+
const inner = diffTokens(graphemes(dels[0]), graphemes(adds[0]));
|
|
259
|
+
for (const span of inner.oldSpans) oldParts.push(span);
|
|
260
|
+
for (const span of inner.newSpans) newParts.push(span);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (dels.length) {
|
|
264
|
+
oldParts.push({ text: dels.join(''), changed: true });
|
|
265
|
+
}
|
|
266
|
+
if (adds.length) {
|
|
267
|
+
newParts.push({ text: adds.join(''), changed: true });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
oldSpans: collapseSpans(oldParts),
|
|
272
|
+
newSpans: collapseSpans(newParts),
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const diffChars = (oldText, newText) => {
|
|
277
|
+
const a = splitPieces(oldText);
|
|
278
|
+
const b = splitPieces(newText);
|
|
279
|
+
if (a.length === 0 && b.length === 0) {
|
|
280
|
+
return { oldSpans: [], newSpans: [] };
|
|
281
|
+
}
|
|
282
|
+
if (a.length === 0 || b.length === 0) return wholeChanged(a, b);
|
|
283
|
+
const { prefix, suffix, midA, midB } = trimSharedEnds(a, b);
|
|
284
|
+
let mid;
|
|
285
|
+
if (midA.length === 0 && midB.length === 0) {
|
|
286
|
+
mid = { oldSpans: [], newSpans: [] };
|
|
287
|
+
} else if (midA.length === 0 || midB.length === 0) {
|
|
288
|
+
mid = wholeChanged(midA, midB);
|
|
289
|
+
} else if (midA.length * midB.length > LCS_CELL_LIMIT) {
|
|
290
|
+
mid = wholeChanged(midA, midB);
|
|
291
|
+
} else {
|
|
292
|
+
mid = spansFromWordOps(lcsOps(midA, midB));
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
oldSpans: joinAffix(prefix, mid.oldSpans, suffix),
|
|
296
|
+
newSpans: joinAffix(prefix, mid.newSpans, suffix),
|
|
297
|
+
};
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
module.exports = { tokenSimilarity, diffChars };
|