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.
Files changed (96) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +3 -1
  3. package/bin/reslop.js +2 -2
  4. package/lib/ansi.js +148 -5
  5. package/lib/api.js +4 -2
  6. package/lib/capabilities.js +13 -14
  7. package/lib/cli.js +38 -49
  8. package/lib/clipboard.js +1 -1
  9. package/lib/{deps/changes.js → deps-diff.js} +309 -19
  10. package/lib/deps.js +498 -29
  11. package/lib/detect.js +1 -7
  12. package/lib/diff/align.js +7 -10
  13. package/lib/diff/diff.js +53 -0
  14. package/lib/diff/display.js +5 -4
  15. package/lib/diff/inline.js +26 -21
  16. package/lib/diff/items.js +6 -4
  17. package/lib/diff/patch.js +1 -3
  18. package/lib/diff/unit.js +392 -0
  19. package/lib/files.js +5 -8
  20. package/lib/{git/branches.js → git-branches.js} +191 -11
  21. package/lib/{git/dependencies.js → git-deps.js} +111 -38
  22. package/lib/git-reword-todo.js +20 -0
  23. package/lib/git.js +606 -68
  24. package/lib/github.js +28 -197
  25. package/lib/gitlab.js +24 -195
  26. package/lib/highlight/bash.js +1 -3
  27. package/lib/highlight/css.js +1 -3
  28. package/lib/highlight/js.js +11 -14
  29. package/lib/highlight.js +6 -9
  30. package/lib/keys.js +14 -8
  31. package/lib/manifest.js +330 -0
  32. package/lib/remote.js +374 -0
  33. package/lib/render/branches.js +18 -100
  34. package/lib/render/chrome.js +71 -14
  35. package/lib/render/commits.js +69 -60
  36. package/lib/render/diff.js +20 -20
  37. package/lib/render/files.js +6 -34
  38. package/lib/render/frame.js +36 -13
  39. package/lib/render/notes.js +26 -8
  40. package/lib/render/primitives.js +99 -11
  41. package/lib/{render.js → render/render.js} +8 -8
  42. package/lib/render/unit.js +41 -0
  43. package/lib/review-merge.js +270 -0
  44. package/lib/review.js +782 -14
  45. package/lib/select.js +5 -10
  46. package/lib/session/accessors.js +9 -1
  47. package/lib/session/actions.js +61 -18
  48. package/lib/session/branches.js +116 -128
  49. package/lib/session/change-actions.js +202 -123
  50. package/lib/session/commit.js +201 -115
  51. package/lib/session/compose.js +181 -15
  52. package/lib/session/dispatch.js +110 -60
  53. package/lib/session/files-pane.js +156 -148
  54. package/lib/session/items.js +22 -30
  55. package/lib/session/lifecycle.js +151 -54
  56. package/lib/session/load.js +19 -33
  57. package/lib/session/navigation.js +12 -1
  58. package/lib/session/ops.js +28 -41
  59. package/lib/session/pointer.js +57 -57
  60. package/lib/session/review.js +23 -37
  61. package/lib/session/terminal.js +6 -12
  62. package/lib/session/todos.js +37 -13
  63. package/lib/session/unit.js +208 -0
  64. package/lib/session/update.js +33 -46
  65. package/lib/session/view.js +78 -9
  66. package/lib/session/watch.js +128 -24
  67. package/lib/session.js +22 -246
  68. package/lib/source.js +3 -3
  69. package/lib/update.js +40 -75
  70. package/lib/{git/proc.js → utilities.js} +75 -11
  71. package/lib/wrap.js +1 -3
  72. package/package.json +2 -2
  73. package/lib/deps/items.js +0 -134
  74. package/lib/deps/manifest.js +0 -152
  75. package/lib/deps/merge.js +0 -120
  76. package/lib/deps/proposals.js +0 -164
  77. package/lib/deps/reports.js +0 -180
  78. package/lib/deps/rows.js +0 -176
  79. package/lib/deps/scan.js +0 -138
  80. package/lib/deps/usage.js +0 -68
  81. package/lib/deps/versions.js +0 -119
  82. package/lib/diff.js +0 -33
  83. package/lib/git/changes.js +0 -164
  84. package/lib/git/read.js +0 -253
  85. package/lib/git/run.js +0 -100
  86. package/lib/npm/commands.js +0 -100
  87. package/lib/remote/changes.js +0 -37
  88. package/lib/remote/headers.js +0 -36
  89. package/lib/remote/http.js +0 -122
  90. package/lib/remote/notes.js +0 -94
  91. package/lib/review/format.js +0 -434
  92. package/lib/review/storage.js +0 -150
  93. package/lib/review/store.js +0 -178
  94. package/lib/review/templates.js +0 -49
  95. package/lib/session/origins.js +0 -7
  96. package/lib/sys.js +0 -84
@@ -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', 'commit'];
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 lines = [];
40
- for (let i = 0; i < wrapped.length; i++) {
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.push(`${prefix}${wrapped[i]}`);
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 (const line of wrapped) {
68
- rows.push(paintCheckBar(` ${line}`, width, fgRgb, bgRgb, color));
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({ body, fileHits, todoOwners, cursor, splitBody });
203
+ return paneResult({
204
+ body,
205
+ fileHits,
206
+ todoOwners,
207
+ todoChecks,
208
+ cursor,
209
+ splitBody,
210
+ });
193
211
  };
194
212
 
195
213
  module.exports = {
@@ -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
- tail,
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 extra = tail ? 1 : 0;
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 last = count - 1;
123
- const focus = extra ? last : cursor;
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 (tail && idx === items.length) {
133
- body.push(tail.row);
134
- if (tail.cursor) {
135
- textCursor = { x: tail.cursor.x, row: body.length - 1 };
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 entry = items[idx];
147
+ const itemIdx = inserting ? idx - 1 : idx;
148
+ const entry = items[itemIdx];
140
149
  if (!entry) break;
141
- const selected = extra ? false : idx === cursor;
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: idx });
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('./render/chrome.js');
4
- const notes = require('./render/notes.js');
5
- const diff = require('./render/diff.js');
6
- const present = require('./render/present.js');
7
- const { renderFrame } = require('./render/frame.js');
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 { DEL_CHAR_BG, ADD_CHAR_BG, paintDiffLine, codeInnerWidth } = diff;
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
- DEL_CHAR_BG,
22
- ADD_CHAR_BG,
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 };