reslop 0.1.9 → 0.2.1
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 +25 -1
- package/README.md +3 -1
- package/bin/reslop.js +2 -2
- package/lib/ansi.js +148 -5
- package/lib/api.js +4 -2
- package/lib/capabilities.js +13 -14
- package/lib/cli.js +38 -49
- package/lib/clipboard.js +1 -1
- package/lib/{deps/changes.js → deps-diff.js} +309 -19
- package/lib/deps.js +498 -29
- package/lib/detect.js +1 -7
- package/lib/diff/align.js +7 -10
- package/lib/diff/diff.js +53 -0
- package/lib/diff/display.js +5 -4
- package/lib/diff/inline.js +26 -21
- package/lib/diff/items.js +6 -4
- package/lib/diff/patch.js +1 -3
- package/lib/diff/unit.js +392 -0
- package/lib/files.js +5 -8
- package/lib/{git/branches.js → git-branches.js} +191 -11
- package/lib/{git/dependencies.js → git-deps.js} +111 -38
- package/lib/git-reword-todo.js +20 -0
- package/lib/git.js +606 -68
- package/lib/github.js +28 -197
- package/lib/gitlab.js +24 -195
- package/lib/highlight/bash.js +1 -3
- package/lib/highlight/css.js +1 -3
- package/lib/highlight/js.js +11 -14
- package/lib/highlight.js +6 -9
- package/lib/keys.js +14 -8
- package/lib/manifest.js +330 -0
- package/lib/remote.js +374 -0
- package/lib/render/branches.js +18 -100
- package/lib/render/chrome.js +71 -14
- package/lib/render/commits.js +69 -60
- package/lib/render/diff.js +20 -20
- package/lib/render/files.js +6 -34
- package/lib/render/frame.js +36 -13
- package/lib/render/notes.js +26 -8
- package/lib/render/primitives.js +99 -11
- package/lib/{render.js → render/render.js} +8 -8
- package/lib/render/unit.js +41 -0
- package/lib/review-merge.js +270 -0
- package/lib/review.js +782 -14
- package/lib/select.js +5 -10
- package/lib/session/accessors.js +9 -1
- package/lib/session/actions.js +61 -18
- package/lib/session/branches.js +116 -128
- package/lib/session/change-actions.js +202 -123
- package/lib/session/commit.js +201 -115
- package/lib/session/compose.js +181 -15
- package/lib/session/dispatch.js +110 -60
- package/lib/session/files-pane.js +156 -148
- package/lib/session/items.js +22 -30
- package/lib/session/lifecycle.js +151 -54
- package/lib/session/load.js +19 -33
- package/lib/session/navigation.js +12 -1
- package/lib/session/ops.js +28 -41
- package/lib/session/pointer.js +57 -57
- package/lib/session/review.js +23 -37
- package/lib/session/terminal.js +6 -12
- package/lib/session/todos.js +37 -13
- package/lib/session/unit.js +208 -0
- package/lib/session/update.js +33 -46
- package/lib/session/view.js +78 -9
- package/lib/session/watch.js +128 -24
- package/lib/session.js +22 -246
- package/lib/source.js +3 -3
- package/lib/update.js +40 -75
- package/lib/{git/proc.js → utilities.js} +75 -11
- package/lib/wrap.js +1 -3
- package/package.json +2 -2
- package/lib/deps/items.js +0 -134
- package/lib/deps/manifest.js +0 -152
- package/lib/deps/merge.js +0 -120
- package/lib/deps/proposals.js +0 -164
- package/lib/deps/reports.js +0 -180
- package/lib/deps/rows.js +0 -176
- package/lib/deps/scan.js +0 -138
- package/lib/deps/usage.js +0 -68
- package/lib/deps/versions.js +0 -119
- package/lib/diff.js +0 -33
- package/lib/git/changes.js +0 -164
- package/lib/git/read.js +0 -253
- package/lib/git/run.js +0 -100
- package/lib/npm/commands.js +0 -100
- package/lib/remote/changes.js +0 -37
- package/lib/remote/headers.js +0 -36
- package/lib/remote/http.js +0 -122
- package/lib/remote/notes.js +0 -94
- package/lib/review/format.js +0 -434
- package/lib/review/storage.js +0 -150
- package/lib/review/store.js +0 -178
- package/lib/review/templates.js +0 -49
- package/lib/session/origins.js +0 -7
- package/lib/sys.js +0 -84
package/lib/render/notes.js
CHANGED
|
@@ -11,7 +11,7 @@ const { THEME, visibleWidth, truncateVisible, seq, EL, RESET, BOLD } = ansi;
|
|
|
11
11
|
const { paintBar, paintCheckBar, windowRows } = primitives;
|
|
12
12
|
const { LIST_PANES, CHECK_LEAD, paneResult } = primitives;
|
|
13
13
|
|
|
14
|
-
const NOTE_COMPOSE = ['feedback'
|
|
14
|
+
const NOTE_COMPOSE = ['feedback'];
|
|
15
15
|
const INPUT_MAX = 4;
|
|
16
16
|
const NOTE_MAX = 4;
|
|
17
17
|
const NOTE_PAD = 1;
|
|
@@ -36,10 +36,11 @@ const wrapTodoItem = (text, inner) => {
|
|
|
36
36
|
const bodyWidth = Math.max(1, inner - lead);
|
|
37
37
|
const hang = ' '.repeat(lead);
|
|
38
38
|
const wrapped = wrapMultiline(body, bodyWidth);
|
|
39
|
-
const
|
|
40
|
-
|
|
39
|
+
const { length } = wrapped;
|
|
40
|
+
const lines = new Array(length);
|
|
41
|
+
for (let i = 0; i < length; i++) {
|
|
41
42
|
const prefix = i === 0 ? mark : hang;
|
|
42
|
-
lines
|
|
43
|
+
lines[i] = `${prefix}${wrapped[i]}`;
|
|
43
44
|
}
|
|
44
45
|
return lines;
|
|
45
46
|
};
|
|
@@ -56,6 +57,7 @@ const paintTodoList = (texts, focus, width, color, edit) => {
|
|
|
56
57
|
const inner = todoInnerWidth(width);
|
|
57
58
|
const rows = [];
|
|
58
59
|
const owners = [];
|
|
60
|
+
const checks = [];
|
|
59
61
|
let cursor = null;
|
|
60
62
|
for (let i = 0; i < texts.length; i++) {
|
|
61
63
|
const selected = i === focus;
|
|
@@ -64,14 +66,22 @@ const paintTodoList = (texts, focus, width, color, edit) => {
|
|
|
64
66
|
const text = texts[i];
|
|
65
67
|
const wrapped = wrapTodoItem(text, inner);
|
|
66
68
|
const rowStart = rows.length;
|
|
67
|
-
for (
|
|
68
|
-
|
|
69
|
+
for (let line = 0; line < wrapped.length; line++) {
|
|
70
|
+
const painted = paintCheckBar(
|
|
71
|
+
` ${wrapped[line]}`,
|
|
72
|
+
width,
|
|
73
|
+
fgRgb,
|
|
74
|
+
bgRgb,
|
|
75
|
+
color,
|
|
76
|
+
);
|
|
77
|
+
rows.push(painted);
|
|
69
78
|
owners.push(i);
|
|
79
|
+
checks.push(line === 0);
|
|
70
80
|
}
|
|
71
81
|
if (!edit || !selected) continue;
|
|
72
82
|
cursor = todoCursor(text, edit, inner, rowStart);
|
|
73
83
|
}
|
|
74
|
-
return { rows, owners, cursor };
|
|
84
|
+
return { rows, owners, checks, cursor };
|
|
75
85
|
};
|
|
76
86
|
|
|
77
87
|
const noteInnerWidth = (width) => Math.max(1, width - NOTE_PAD * 2);
|
|
@@ -183,13 +193,21 @@ const paintBodyTodo = (view, width, color) => {
|
|
|
183
193
|
const caption = paintTodoCaption(width, color);
|
|
184
194
|
const body = [caption, ...painted.rows];
|
|
185
195
|
const todoOwners = [null, ...painted.owners];
|
|
196
|
+
const todoChecks = [false, ...painted.checks];
|
|
186
197
|
let cursor = null;
|
|
187
198
|
if (painted.cursor) {
|
|
188
199
|
cursor = { x: painted.cursor.x, row: painted.cursor.row + 1 };
|
|
189
200
|
}
|
|
190
201
|
const fileHits = [];
|
|
191
202
|
const splitBody = false;
|
|
192
|
-
return paneResult({
|
|
203
|
+
return paneResult({
|
|
204
|
+
body,
|
|
205
|
+
fileHits,
|
|
206
|
+
todoOwners,
|
|
207
|
+
todoChecks,
|
|
208
|
+
cursor,
|
|
209
|
+
splitBody,
|
|
210
|
+
});
|
|
193
211
|
};
|
|
194
212
|
|
|
195
213
|
module.exports = {
|
package/lib/render/primitives.js
CHANGED
|
@@ -104,6 +104,13 @@ const listWindowStart = (focus, offset, listH, count) => {
|
|
|
104
104
|
return start;
|
|
105
105
|
};
|
|
106
106
|
|
|
107
|
+
const extraIdx = (extraRow, cursor, count) => {
|
|
108
|
+
if (!extraRow) return -1;
|
|
109
|
+
if (extraRow.at === 'start') return 0;
|
|
110
|
+
if (extraRow.at === 'replace') return extraRow.index ?? cursor;
|
|
111
|
+
return count;
|
|
112
|
+
};
|
|
113
|
+
|
|
107
114
|
const paintCursorList = (
|
|
108
115
|
items,
|
|
109
116
|
cursor,
|
|
@@ -112,15 +119,16 @@ const paintCursorList = (
|
|
|
112
119
|
bodyH,
|
|
113
120
|
headerLines,
|
|
114
121
|
paintRow,
|
|
115
|
-
|
|
122
|
+
extraRow,
|
|
116
123
|
offset = 0,
|
|
117
124
|
) => {
|
|
118
125
|
const pad = bodyH >= FILE_LIST_PAD * 2 + 1 ? FILE_LIST_PAD : 0;
|
|
119
126
|
const listH = bodyH - pad * 2;
|
|
120
|
-
const
|
|
127
|
+
const inserting = extraRow && extraRow.at === 'start';
|
|
128
|
+
const extra = extraRow && extraRow.at !== 'replace' ? 1 : 0;
|
|
121
129
|
const count = items.length + extra;
|
|
122
|
-
const
|
|
123
|
-
const focus =
|
|
130
|
+
const extraAt = extraIdx(extraRow, cursor, items.length);
|
|
131
|
+
const focus = extraRow ? extraAt : cursor;
|
|
124
132
|
const start = listWindowStart(focus, offset, listH, count);
|
|
125
133
|
const blank = paintBodyFill(width, color, 'files');
|
|
126
134
|
const body = [];
|
|
@@ -129,19 +137,20 @@ const paintCursorList = (
|
|
|
129
137
|
if (pad) body.push(blank);
|
|
130
138
|
for (let i = 0; i < listH; i++) {
|
|
131
139
|
const idx = start + i;
|
|
132
|
-
if (
|
|
133
|
-
body.push(
|
|
134
|
-
if (
|
|
135
|
-
textCursor = { x:
|
|
140
|
+
if (extraRow && idx === extraAt) {
|
|
141
|
+
body.push(extraRow.row);
|
|
142
|
+
if (extraRow.cursor) {
|
|
143
|
+
textCursor = { x: extraRow.cursor.x, row: body.length - 1 };
|
|
136
144
|
}
|
|
137
145
|
continue;
|
|
138
146
|
}
|
|
139
|
-
const
|
|
147
|
+
const itemIdx = inserting ? idx - 1 : idx;
|
|
148
|
+
const entry = items[itemIdx];
|
|
140
149
|
if (!entry) break;
|
|
141
|
-
const selected =
|
|
150
|
+
const selected = extraRow ? false : itemIdx === cursor;
|
|
142
151
|
body.push(paintRow(entry, width, color, selected));
|
|
143
152
|
const y = headerLines + pad + i + 1;
|
|
144
|
-
fileHits.push({ y, cursor:
|
|
153
|
+
fileHits.push({ y, cursor: itemIdx });
|
|
145
154
|
}
|
|
146
155
|
if (pad) body.push(blank);
|
|
147
156
|
return { body, fileHits, cursor: textCursor, offset: start };
|
|
@@ -171,16 +180,94 @@ const paintBracketName = (label, nameFg, wrapFg, bgRgb, color, nameBold) => {
|
|
|
171
180
|
);
|
|
172
181
|
};
|
|
173
182
|
|
|
183
|
+
const paintListEdit = (compose, width, color, edgeW) => {
|
|
184
|
+
const inner = Math.max(1, width - edgeW);
|
|
185
|
+
const prefix = ` ${FILE_MARK} `;
|
|
186
|
+
const prefixW = visibleWidth(prefix);
|
|
187
|
+
const rest = Math.max(1, inner - prefixW);
|
|
188
|
+
const text = `${compose.text ?? ''}`.replaceAll('\n', ' ');
|
|
189
|
+
const label = `[${text}]`;
|
|
190
|
+
const clipped = truncateVisible(label, rest);
|
|
191
|
+
const pad = Math.max(0, rest - visibleWidth(clipped));
|
|
192
|
+
const left = `${prefix}${clipped}${' '.repeat(pad)}`;
|
|
193
|
+
const fgRgb = THEME.chromeFg;
|
|
194
|
+
const bgRgb = THEME.buttonBg;
|
|
195
|
+
const edge = fill(edgeW, fgRgb, THEME.ctxBg, color);
|
|
196
|
+
let row = `${left}${edge}`;
|
|
197
|
+
if (color) {
|
|
198
|
+
const gap = ' '.repeat(pad);
|
|
199
|
+
row = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
200
|
+
row += paint(prefix, fgRgb, bgRgb, color);
|
|
201
|
+
row += paintBracketName(
|
|
202
|
+
clipped,
|
|
203
|
+
THEME.buttonHotFg,
|
|
204
|
+
THEME.mutedFg,
|
|
205
|
+
bgRgb,
|
|
206
|
+
color,
|
|
207
|
+
);
|
|
208
|
+
row += paint(gap, fgRgb, bgRgb, color);
|
|
209
|
+
row += edge;
|
|
210
|
+
}
|
|
211
|
+
const col = Math.min((compose.cursor ?? 0) + 1, visibleWidth(clipped));
|
|
212
|
+
return { row, cursor: { x: prefixW + col + 1 } };
|
|
213
|
+
};
|
|
214
|
+
|
|
174
215
|
const paneResult = (fields) => ({
|
|
175
216
|
body: fields.body,
|
|
176
217
|
fileHits: fields.fileHits,
|
|
177
218
|
todoOwners: fields.todoOwners,
|
|
219
|
+
todoChecks: fields.todoChecks ?? [],
|
|
178
220
|
cursor: fields.cursor ?? null,
|
|
179
221
|
splitBody: fields.splitBody === true,
|
|
180
222
|
listScroll: fields.listScroll ?? 0,
|
|
181
223
|
});
|
|
182
224
|
|
|
225
|
+
const measureColumns = (entries, rowFields, align) => {
|
|
226
|
+
const keys = Object.keys(align);
|
|
227
|
+
const widths = Object.fromEntries(keys.map((key) => [key, 0]));
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
const fields = rowFields(entry);
|
|
230
|
+
for (const key of keys) {
|
|
231
|
+
widths[key] = Math.max(widths[key], visibleWidth(fields[key]));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return widths;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const padColumns = (fields, widths, align) => {
|
|
238
|
+
const parts = {};
|
|
239
|
+
for (const key of Object.keys(align)) {
|
|
240
|
+
const width = widths?.[key] ?? visibleWidth(fields[key]);
|
|
241
|
+
parts[key] = padVisible(fields[key], width, align[key]);
|
|
242
|
+
}
|
|
243
|
+
return parts;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const columnGap = (key) => (key === 'date' ? ' ' : ' ');
|
|
247
|
+
|
|
248
|
+
const joinColumns = (parts, widths) => {
|
|
249
|
+
let text = '';
|
|
250
|
+
for (const key of Object.keys(parts)) {
|
|
251
|
+
if (widths[key]) text += columnGap(key) + parts[key];
|
|
252
|
+
}
|
|
253
|
+
return text;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const paintColumns = (parts, widths, fgRgb, bgRgb, color, tones) => {
|
|
257
|
+
let text = '';
|
|
258
|
+
for (const key of Object.keys(parts)) {
|
|
259
|
+
if (!widths[key]) continue;
|
|
260
|
+
text += paint(columnGap(key), fgRgb, bgRgb, color);
|
|
261
|
+
text += paint(parts[key], tones[key] ?? fgRgb, bgRgb, color);
|
|
262
|
+
}
|
|
263
|
+
return text;
|
|
264
|
+
};
|
|
265
|
+
|
|
183
266
|
module.exports = {
|
|
267
|
+
measureColumns,
|
|
268
|
+
padColumns,
|
|
269
|
+
joinColumns,
|
|
270
|
+
paintColumns,
|
|
184
271
|
GUTTER_W,
|
|
185
272
|
FILE_MARK,
|
|
186
273
|
LIST_PANES,
|
|
@@ -194,6 +281,7 @@ module.exports = {
|
|
|
194
281
|
paintBodyFill,
|
|
195
282
|
windowRows,
|
|
196
283
|
paintCursorList,
|
|
284
|
+
paintListEdit,
|
|
197
285
|
branchLabel,
|
|
198
286
|
paintBracketName,
|
|
199
287
|
paneResult,
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const chrome = require('./
|
|
4
|
-
const notes = require('./
|
|
5
|
-
const diff = require('./
|
|
6
|
-
const present = require('./
|
|
7
|
-
const { renderFrame } = require('./
|
|
3
|
+
const chrome = require('./chrome.js');
|
|
4
|
+
const notes = require('./notes.js');
|
|
5
|
+
const diff = require('./diff.js');
|
|
6
|
+
const present = require('./present.js');
|
|
7
|
+
const { renderFrame } = require('./frame.js');
|
|
8
8
|
|
|
9
9
|
const { QUIT_PROMPT, PUSH_PROMPT } = chrome;
|
|
10
10
|
const { updatePrompt, dropPrompt } = chrome;
|
|
11
11
|
const { formatBusyStatus, headerText, layoutButtons } = chrome;
|
|
12
|
-
const {
|
|
12
|
+
const { delCharBg, addCharBg, paintDiffLine, codeInnerWidth } = diff;
|
|
13
13
|
const { todoInnerWidth, todoBodyWidth, noteInnerWidth } = notes;
|
|
14
14
|
const { presentCursor, presentRows } = present;
|
|
15
15
|
|
|
@@ -18,8 +18,8 @@ module.exports = {
|
|
|
18
18
|
PUSH_PROMPT,
|
|
19
19
|
updatePrompt,
|
|
20
20
|
dropPrompt,
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
delCharBg,
|
|
22
|
+
addCharBg,
|
|
23
23
|
paintDiffLine,
|
|
24
24
|
headerText,
|
|
25
25
|
layoutButtons,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { detectLang } = require('../detect.js');
|
|
4
|
+
const primitives = require('./primitives.js');
|
|
5
|
+
const diff = require('./diff.js');
|
|
6
|
+
|
|
7
|
+
const { paneResult } = primitives;
|
|
8
|
+
const { paintDiffLine, codeCursorInRows } = diff;
|
|
9
|
+
const { sameHunk } = require('../diff/diff.js');
|
|
10
|
+
|
|
11
|
+
const paintBodyUnit = (view, width, color) => {
|
|
12
|
+
const lines = view.unitLines ?? [];
|
|
13
|
+
const item = view.item;
|
|
14
|
+
const lang = detectLang(view.reviewPath ?? '');
|
|
15
|
+
const overlay = view.codeOverlay;
|
|
16
|
+
const editing = view.mode === 'compose' && overlay;
|
|
17
|
+
const { length } = lines;
|
|
18
|
+
const body = new Array(length);
|
|
19
|
+
for (let i = 0; i < length; i++) {
|
|
20
|
+
const line = lines[i];
|
|
21
|
+
const isBlock = item && sameHunk(line.item, item);
|
|
22
|
+
const row = { ...line };
|
|
23
|
+
if (editing) {
|
|
24
|
+
row.mark = ' ';
|
|
25
|
+
} else if ((row.type === 'add' || row.type === 'del') && !isBlock) {
|
|
26
|
+
row.mark = ' ';
|
|
27
|
+
}
|
|
28
|
+
body[i] = paintDiffLine(row, width, color, row.origin, lang);
|
|
29
|
+
}
|
|
30
|
+
const edit = overlay ? overlay.cursor : null;
|
|
31
|
+
const cursor = codeCursorInRows(lines, false, width, edit);
|
|
32
|
+
return paneResult({
|
|
33
|
+
body,
|
|
34
|
+
fileHits: [],
|
|
35
|
+
todoOwners: [],
|
|
36
|
+
cursor,
|
|
37
|
+
splitBody: false,
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
module.exports = { paintBodyUnit };
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { TODO_FILE } = require('./files.js');
|
|
4
|
+
|
|
5
|
+
const flatText = (text) => `${text ?? ''}`.trim().replace(/\n/g, ' ');
|
|
6
|
+
|
|
7
|
+
const snapTodo = (todo) => ({
|
|
8
|
+
file: todo.file ?? '',
|
|
9
|
+
text: flatText(todo.text),
|
|
10
|
+
done: todo.done === true,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const snapList = (todos) => {
|
|
14
|
+
const list = [];
|
|
15
|
+
for (const todo of todos ?? []) {
|
|
16
|
+
const snap = snapTodo(todo);
|
|
17
|
+
if (!snap.text) continue;
|
|
18
|
+
list.push(snap);
|
|
19
|
+
}
|
|
20
|
+
return list;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const copyMap = (map) => {
|
|
24
|
+
const next = new Map();
|
|
25
|
+
if (!map) return next;
|
|
26
|
+
for (const [key, note] of map) next.set(key, { ...note });
|
|
27
|
+
return next;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const snapshotSide = (store) => ({
|
|
31
|
+
status: store.status,
|
|
32
|
+
todos: snapList(store.todos),
|
|
33
|
+
feedback: copyMap(store.feedback),
|
|
34
|
+
code: copyMap(store.code),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const captureBaseline = (store) => {
|
|
38
|
+
store.baseline = snapshotSide(store);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const claim = (list, used, text, read) => {
|
|
42
|
+
for (let i = 0; i < list.length; i++) {
|
|
43
|
+
if (used[i]) continue;
|
|
44
|
+
if (read(list[i]) !== text) continue;
|
|
45
|
+
used[i] = true;
|
|
46
|
+
return i;
|
|
47
|
+
}
|
|
48
|
+
return -1;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const pickDone = (baseDone, ourDone, theirDone) => {
|
|
52
|
+
if (ourDone === theirDone) return ourDone;
|
|
53
|
+
if (theirDone === baseDone) return ourDone;
|
|
54
|
+
if (ourDone === baseDone) return theirDone;
|
|
55
|
+
return ourDone;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const withDone = (todo, done) => {
|
|
59
|
+
if (todo.done === done) return todo;
|
|
60
|
+
return { id: todo.id, file: todo.file, text: todo.text, done };
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const liveText = (todo) => flatText(todo.text);
|
|
64
|
+
|
|
65
|
+
const snapText = (snap) => snap.text;
|
|
66
|
+
|
|
67
|
+
const flags = (n) => new Array(n).fill(false);
|
|
68
|
+
|
|
69
|
+
const adoptTodo = (store, snap) => {
|
|
70
|
+
const id = store.nextTodoId;
|
|
71
|
+
store.nextTodoId += 1;
|
|
72
|
+
const file = snap.file || TODO_FILE;
|
|
73
|
+
return { id, file, text: snap.text, done: snap.done };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const findMerged = (merged, alias, text) => {
|
|
77
|
+
const linked = alias.get(text);
|
|
78
|
+
if (linked) {
|
|
79
|
+
const at = merged.indexOf(linked);
|
|
80
|
+
if (at >= 0) return at;
|
|
81
|
+
}
|
|
82
|
+
for (let i = 0; i < merged.length; i++) {
|
|
83
|
+
if (liveText(merged[i]) === text) return i;
|
|
84
|
+
}
|
|
85
|
+
return -1;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const insertTheir = (store, merged, theirs, theirKeep, alias) => {
|
|
89
|
+
for (let i = 0; i < theirs.length; i++) {
|
|
90
|
+
if (!theirKeep[i]) continue;
|
|
91
|
+
let at = merged.length;
|
|
92
|
+
let placed = false;
|
|
93
|
+
for (let j = i - 1; j >= 0 && !placed; j--) {
|
|
94
|
+
const prev = findMerged(merged, alias, theirs[j].text);
|
|
95
|
+
if (prev < 0) continue;
|
|
96
|
+
at = prev + 1;
|
|
97
|
+
placed = true;
|
|
98
|
+
}
|
|
99
|
+
for (let j = i + 1; j < theirs.length && !placed; j++) {
|
|
100
|
+
const next = findMerged(merged, alias, theirs[j].text);
|
|
101
|
+
if (next < 0) continue;
|
|
102
|
+
at = next;
|
|
103
|
+
placed = true;
|
|
104
|
+
}
|
|
105
|
+
const item = adoptTodo(store, theirs[i]);
|
|
106
|
+
merged.splice(at, 0, item);
|
|
107
|
+
alias.set(theirs[i].text, item);
|
|
108
|
+
}
|
|
109
|
+
return merged;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const keepOurs = (ours, kept, alias, index, text) => {
|
|
113
|
+
kept[index] = true;
|
|
114
|
+
alias.set(text, ours[index]);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const mergeBaseTodo = (
|
|
118
|
+
base,
|
|
119
|
+
ours,
|
|
120
|
+
theirs,
|
|
121
|
+
used,
|
|
122
|
+
kept,
|
|
123
|
+
dropped,
|
|
124
|
+
theirKeep,
|
|
125
|
+
alias,
|
|
126
|
+
) => {
|
|
127
|
+
const text = base.text;
|
|
128
|
+
const oi = claim(ours, used.ours, text, liveText);
|
|
129
|
+
const ti = claim(theirs, used.theirs, text, snapText);
|
|
130
|
+
if (oi >= 0 && ti >= 0) {
|
|
131
|
+
const done = pickDone(base.done, ours[oi].done === true, theirs[ti].done);
|
|
132
|
+
ours[oi] = withDone(ours[oi], done);
|
|
133
|
+
keepOurs(ours, kept, alias, oi, text);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (oi >= 0) {
|
|
137
|
+
if (ours[oi].done === base.done) {
|
|
138
|
+
dropped[oi] = true;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
keepOurs(ours, kept, alias, oi, text);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (ti >= 0 && theirs[ti].done !== base.done) theirKeep[ti] = true;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const replaceTodo = (todo, snap) => ({
|
|
148
|
+
id: todo.id,
|
|
149
|
+
file: todo.file,
|
|
150
|
+
text: snap.text,
|
|
151
|
+
done: snap.done,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const fillGaps = (merged, gaps, theirs, theirKeep, alias) => {
|
|
155
|
+
let filled = 0;
|
|
156
|
+
for (let i = 0; i < theirs.length && filled < gaps.length; i++) {
|
|
157
|
+
if (!theirKeep[i]) continue;
|
|
158
|
+
const gap = gaps[filled];
|
|
159
|
+
const item = replaceTodo(gap.todo, theirs[i]);
|
|
160
|
+
merged.splice(gap.at, 0, item);
|
|
161
|
+
alias.set(theirs[i].text, item);
|
|
162
|
+
theirKeep[i] = false;
|
|
163
|
+
filled += 1;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const mergeFreshTodo = (ours, theirs, used, kept, alias, index) => {
|
|
168
|
+
const text = liveText(ours[index]);
|
|
169
|
+
const ti = claim(theirs, used.theirs, text, snapText);
|
|
170
|
+
if (ti < 0) {
|
|
171
|
+
kept[index] = true;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const done = ours[index].done === true || theirs[ti].done === true;
|
|
175
|
+
ours[index] = withDone(ours[index], done);
|
|
176
|
+
keepOurs(ours, kept, alias, index, text);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const mergeTodos = (store, base, theirs) => {
|
|
180
|
+
const ours = [];
|
|
181
|
+
for (const todo of store.todos) {
|
|
182
|
+
if (!liveText(todo)) continue;
|
|
183
|
+
ours.push(todo);
|
|
184
|
+
}
|
|
185
|
+
const used = { ours: flags(ours.length), theirs: flags(theirs.length) };
|
|
186
|
+
const kept = flags(ours.length);
|
|
187
|
+
const dropped = flags(ours.length);
|
|
188
|
+
const theirKeep = flags(theirs.length);
|
|
189
|
+
const alias = new Map();
|
|
190
|
+
for (const item of base) {
|
|
191
|
+
mergeBaseTodo(item, ours, theirs, used, kept, dropped, theirKeep, alias);
|
|
192
|
+
}
|
|
193
|
+
for (let i = 0; i < ours.length; i++) {
|
|
194
|
+
if (used.ours[i]) continue;
|
|
195
|
+
mergeFreshTodo(ours, theirs, used, kept, alias, i);
|
|
196
|
+
}
|
|
197
|
+
for (let i = 0; i < theirs.length; i++) {
|
|
198
|
+
if (!used.theirs[i]) theirKeep[i] = true;
|
|
199
|
+
}
|
|
200
|
+
const merged = [];
|
|
201
|
+
const gaps = [];
|
|
202
|
+
let cursor = 0;
|
|
203
|
+
for (let i = 0; i < ours.length; i++) {
|
|
204
|
+
if (kept[i]) {
|
|
205
|
+
merged.push(ours[i]);
|
|
206
|
+
cursor += 1;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (!dropped[i]) continue;
|
|
210
|
+
gaps.push({ at: cursor, todo: ours[i] });
|
|
211
|
+
cursor += 1;
|
|
212
|
+
}
|
|
213
|
+
fillGaps(merged, gaps, theirs, theirKeep, alias);
|
|
214
|
+
store.todos = insertTheir(store, merged, theirs, theirKeep, alias);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const sameNote = (left, right) =>
|
|
218
|
+
left.text === right.text && !!left.done === !!right.done;
|
|
219
|
+
|
|
220
|
+
const cloneNote = (note, done) => {
|
|
221
|
+
if (!!note.done === done) return note;
|
|
222
|
+
return { ...note, done };
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const pickNote = (ours, base, theirs) => {
|
|
226
|
+
if (ours && theirs && ours.text === theirs.text) {
|
|
227
|
+
const baseDone = base ? base.done === true : ours.done === true;
|
|
228
|
+
const done = pickDone(baseDone, ours.done === true, theirs.done === true);
|
|
229
|
+
return cloneNote(ours, done);
|
|
230
|
+
}
|
|
231
|
+
if (!base) return ours || theirs || null;
|
|
232
|
+
if (!ours) {
|
|
233
|
+
if (!theirs || sameNote(theirs, base)) return null;
|
|
234
|
+
return theirs;
|
|
235
|
+
}
|
|
236
|
+
if (!theirs) {
|
|
237
|
+
if (sameNote(ours, base)) return null;
|
|
238
|
+
return ours;
|
|
239
|
+
}
|
|
240
|
+
if (sameNote(theirs, base)) return ours;
|
|
241
|
+
if (sameNote(ours, base)) return theirs;
|
|
242
|
+
return ours;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const mergeMap = (ours, base, theirs) => {
|
|
246
|
+
const keys = new Set();
|
|
247
|
+
for (const key of ours.keys()) keys.add(key);
|
|
248
|
+
for (const key of base.keys()) keys.add(key);
|
|
249
|
+
for (const key of theirs.keys()) keys.add(key);
|
|
250
|
+
for (const key of keys) {
|
|
251
|
+
const next = pickNote(ours.get(key), base.get(key), theirs.get(key));
|
|
252
|
+
if (!next) ours.delete(key);
|
|
253
|
+
else ours.set(key, next);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const mergeReview = (store, disk) => {
|
|
258
|
+
const base = store.baseline;
|
|
259
|
+
const baseTodos = base ? base.todos : [];
|
|
260
|
+
const baseStatus = base ? base.status : store.status;
|
|
261
|
+
mergeTodos(store, baseTodos, snapList(disk.todos));
|
|
262
|
+
const feedback = base ? base.feedback : new Map();
|
|
263
|
+
const code = base ? base.code : new Map();
|
|
264
|
+
mergeMap(store.feedback, feedback, disk.feedback);
|
|
265
|
+
mergeMap(store.code, code, disk.code);
|
|
266
|
+
if (store.status === baseStatus && disk.status) store.status = disk.status;
|
|
267
|
+
store.baseline = snapshotSide(disk);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
module.exports = { captureBaseline, mergeReview };
|