reslop 0.2.4 → 0.2.5

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 (67) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/README.md +1 -2
  3. package/lib/capabilities.js +0 -4
  4. package/lib/clipboard.js +39 -1
  5. package/lib/deps-diff.js +0 -3
  6. package/lib/deps.js +144 -176
  7. package/lib/diff/inline.js +66 -1
  8. package/lib/diff/unit.js +11 -28
  9. package/lib/editor.js +69 -9
  10. package/lib/files.js +17 -16
  11. package/lib/git-deps.js +58 -95
  12. package/lib/github.js +7 -7
  13. package/lib/gitlab.js +3 -3
  14. package/lib/highlight/core.js +11 -0
  15. package/lib/highlight/dot.js +2 -12
  16. package/lib/highlight/log.js +4 -11
  17. package/lib/highlight/txt.js +2 -12
  18. package/lib/keys.js +22 -12
  19. package/lib/manifest.js +0 -4
  20. package/lib/npm-commands.js +1 -22
  21. package/lib/remote.js +1 -7
  22. package/lib/render/branches.js +4 -13
  23. package/lib/render/chrome.js +85 -31
  24. package/lib/render/commits.js +95 -207
  25. package/lib/render/diff.js +31 -1
  26. package/lib/render/files.js +7 -16
  27. package/lib/render/frame.js +95 -110
  28. package/lib/render/notes.js +71 -16
  29. package/lib/render/npm.js +104 -18
  30. package/lib/render/primitives.js +108 -45
  31. package/lib/render/unit.js +4 -1
  32. package/lib/report-parse.js +0 -1
  33. package/lib/report-run.js +0 -1
  34. package/lib/report-test.js +40 -47
  35. package/lib/report-tools.js +0 -7
  36. package/lib/review-merge.js +101 -131
  37. package/lib/review.js +64 -77
  38. package/lib/select.js +52 -0
  39. package/lib/session/actions.js +23 -22
  40. package/lib/session/branches.js +5 -3
  41. package/lib/session/change-actions.js +173 -146
  42. package/lib/session/commit.js +32 -13
  43. package/lib/session/compose.js +120 -77
  44. package/lib/session/dispatch.js +36 -21
  45. package/lib/session/edit-click.js +57 -0
  46. package/lib/session/files-pane.js +24 -24
  47. package/lib/session/items.js +69 -89
  48. package/lib/session/lifecycle.js +31 -96
  49. package/lib/session/load.js +51 -58
  50. package/lib/session/navigation.js +105 -96
  51. package/lib/session/npm.js +158 -77
  52. package/lib/session/ops.js +192 -51
  53. package/lib/session/pointer.js +11 -5
  54. package/lib/session/progress.js +9 -6
  55. package/lib/session/review.js +35 -54
  56. package/lib/session/tasks.js +292 -0
  57. package/lib/session/terminal.js +13 -17
  58. package/lib/session/unit.js +3 -4
  59. package/lib/session/update.js +76 -97
  60. package/lib/session/view.js +17 -14
  61. package/lib/session/watch.js +47 -27
  62. package/lib/session.js +34 -79
  63. package/lib/update.js +1 -5
  64. package/lib/utilities.js +21 -1
  65. package/lib/wrap.js +15 -0
  66. package/package.json +1 -1
  67. package/lib/session/todos.js +0 -293
@@ -5,10 +5,11 @@ const actions = require('../session/actions.js');
5
5
  const { LIST_PANES } = actions;
6
6
 
7
7
  const { THEME, paint, visibleWidth, truncateVisible, seq, EL, RESET } = ansi;
8
+ const { graphemes, graphemeWidth } = ansi;
8
9
 
9
10
  const GUTTER_W = 2;
10
11
  const FILE_MARK = '▶';
11
- const FILE_LIST_PAD = 1;
12
+ const LIST_PAD = 1;
12
13
  const CHECK_DONE = 'xX';
13
14
  const CHECK_TOKEN = /\[([ xX])\]/;
14
15
  const CHECK_LEAD = /^\[[ xX]\] /;
@@ -122,61 +123,107 @@ const extraIdx = (extraRow, cursor, count) => {
122
123
  const paneResult = (fields = {}) => ({
123
124
  body: fields.body ?? [],
124
125
  fileHits: fields.fileHits ?? [],
125
- todoOwners: fields.todoOwners ?? [],
126
- todoChecks: fields.todoChecks ?? [],
126
+ taskOwners: fields.taskOwners ?? [],
127
+ taskChecks: fields.taskChecks ?? [],
127
128
  cursor: fields.cursor ?? null,
128
129
  splitBody: fields.splitBody === true,
129
130
  listScroll: fields.listScroll ?? 0,
131
+ editHits: fields.editHits ?? [],
130
132
  });
131
133
 
132
- const paintCursorList = (
133
- items,
134
- cursor,
135
- width,
136
- color,
137
- bodyH,
138
- headerLines,
139
- paintRow,
140
- extraRow,
141
- offset = 0,
142
- ) => {
143
- const pad = bodyH >= FILE_LIST_PAD * 2 + 1 ? FILE_LIST_PAD : 0;
134
+ const NO_EDITS = [];
135
+
136
+ const listLine = (row, index, cursor = null, edits = NO_EDITS) => {
137
+ const hit = index >= 0;
138
+ return { row, index, hit, cursor, edits };
139
+ };
140
+
141
+ const rowCursor = (cursor, row) => {
142
+ const out = { x: cursor.x, row };
143
+ if (typeof cursor.scroll === 'number') out.scroll = cursor.scroll;
144
+ return out;
145
+ };
146
+
147
+ const paintListWindow = (count, focus, pane, lineAt) => {
148
+ const { width, color, bodyH, headerLines, offset } = pane;
149
+ const pad = bodyH >= LIST_PAD * 2 + 1 ? LIST_PAD : 0;
144
150
  const listH = bodyH - pad * 2;
145
- const inserting = extraRow && extraRow.at === 'start';
146
- const extra = extraRow && extraRow.at !== 'replace' ? 1 : 0;
147
- const count = items.length + extra;
148
- const extraAt = extraIdx(extraRow, cursor, items.length);
149
- const focus = extraRow ? extraAt : cursor;
150
151
  const start = listWindowStart(focus, offset, listH, count);
151
152
  const blank = paintBodyFill(width, color, 'files');
152
153
  const body = [];
153
154
  const fileHits = [];
154
- let textCursor = null;
155
+ const editHits = [];
156
+ let cursor = null;
155
157
  if (pad) body.push(blank);
156
158
  for (let i = 0; i < listH; i++) {
157
- const idx = start + i;
158
- if (extraRow && idx === extraAt) {
159
- body.push(extraRow.row);
160
- if (extraRow.cursor) {
161
- textCursor = { x: extraRow.cursor.x, row: body.length - 1 };
162
- }
163
- continue;
159
+ const line = lineAt(start + i);
160
+ if (!line) break;
161
+ const row = body.push(line.row) - 1;
162
+ if (line.cursor) cursor = rowCursor(line.cursor, row);
163
+ for (const edit of line.edits) editHits.push({ ...edit, row });
164
+ if (line.hit) {
165
+ fileHits.push({ y: headerLines + pad + i + 1, cursor: line.index });
164
166
  }
165
- const itemIdx = inserting ? idx - 1 : idx;
166
- const entry = items[itemIdx];
167
- if (!entry) break;
168
- const selected = extraRow ? false : itemIdx === cursor;
169
- body.push(paintRow(entry, width, color, selected));
170
- const y = headerLines + pad + i + 1;
171
- fileHits.push({ y, cursor: itemIdx });
172
167
  }
173
168
  if (pad) body.push(blank);
174
- return paneResult({
175
- body,
176
- fileHits,
177
- cursor: textCursor,
178
- listScroll: start,
179
- });
169
+ return paneResult({ body, fileHits, editHits, cursor, listScroll: start });
170
+ };
171
+
172
+ const editLine = (extraRow) => {
173
+ const { row, cursor, edit, edits = [] } = extraRow;
174
+ return listLine(row, -1, cursor, edit ? [...edits, edit] : edits);
175
+ };
176
+
177
+ const paintCursorList = (items, cursor, pane, paintRow, extraRow = null) => {
178
+ const edit = extraRow ? editLine(extraRow) : null;
179
+ const inserting = edit !== null && extraRow.at === 'start';
180
+ const extra = edit && extraRow.at !== 'replace' ? 1 : 0;
181
+ const extraAt = extraIdx(extraRow, cursor, items.length);
182
+ const lineAt = (idx) => {
183
+ if (edit && idx === extraAt) return edit;
184
+ const index = inserting ? idx - 1 : idx;
185
+ const entry = items[index];
186
+ if (!entry) return null;
187
+ return listLine(paintRow(entry, !edit && index === cursor), index);
188
+ };
189
+ const focus = edit ? extraAt : cursor;
190
+ return paintListWindow(items.length + extra, focus, pane, lineAt);
191
+ };
192
+
193
+ const sliceVisible = (text, skip, width) => {
194
+ if (width <= 0) return '';
195
+ const parts = graphemes(`${text ?? ''}`);
196
+ let used = 0;
197
+ let i = 0;
198
+ const drop = Math.max(0, skip);
199
+ while (i < parts.length) {
200
+ const w = graphemeWidth(parts[i], used);
201
+ if (used + w > drop) break;
202
+ used += w;
203
+ i += 1;
204
+ }
205
+ let out = '';
206
+ let taken = 0;
207
+ for (; i < parts.length; i++) {
208
+ const w = graphemeWidth(parts[i], drop + taken);
209
+ if (taken + w > width) break;
210
+ out += parts[i];
211
+ taken += w;
212
+ }
213
+ return out;
214
+ };
215
+
216
+ const fieldWindow = (text, cursor, scroll, width) => {
217
+ const raw = `${text ?? ''}`;
218
+ if (width <= 0) return { text: '', col: 0, scroll: 0 };
219
+ const at = Math.max(0, Math.min(cursor ?? 0, raw.length));
220
+ const col = visibleWidth(raw.slice(0, at));
221
+ let next = scroll ?? 0;
222
+ if (next < 0) next = 0;
223
+ if (col < next) next = col;
224
+ if (col >= next + width) next = col - width + 1;
225
+ const shown = sliceVisible(raw, next, width);
226
+ return { text: shown, col: col - next, scroll: next };
180
227
  };
181
228
 
182
229
  const branchLabel = (name) => `${name ?? ''}`.trim();
@@ -187,7 +234,8 @@ const paintListEdit = (compose, width, color, edgeW = 0) => {
187
234
  const prefixW = visibleWidth(prefix);
188
235
  const rest = Math.max(1, inner - prefixW);
189
236
  const text = `${compose.text ?? ''}`.replaceAll('\n', ' ');
190
- const clipped = truncateVisible(text, rest);
237
+ const win = fieldWindow(text, compose.cursor, compose.scrollCol, rest);
238
+ const clipped = win.text;
191
239
  const pad = Math.max(0, rest - visibleWidth(clipped));
192
240
  const left = `${prefix}${clipped}${' '.repeat(pad)}`;
193
241
  const fgRgb = THEME.chromeFg;
@@ -202,8 +250,21 @@ const paintListEdit = (compose, width, color, edgeW = 0) => {
202
250
  row += paint(gap, fgRgb, bgRgb, color);
203
251
  row += edge;
204
252
  }
205
- const col = Math.min(compose.cursor ?? 0, visibleWidth(clipped));
206
- return { row, cursor: { x: prefixW + col + 1 } };
253
+ const flat = text;
254
+ return {
255
+ row,
256
+ cursor: { x: prefixW + win.col + 1, scroll: win.scroll },
257
+ edit: {
258
+ x0: 1,
259
+ textX: prefixW + 1,
260
+ x1: prefixW + 1 + rest,
261
+ start: 0,
262
+ text: flat,
263
+ limit: flat.length,
264
+ scroll: win.scroll,
265
+ live: true,
266
+ },
267
+ };
207
268
  };
208
269
 
209
270
  const measureColumns = (entries, rowFields, align) => {
@@ -264,9 +325,11 @@ module.exports = {
264
325
  paintSplitRule,
265
326
  paintBodyFill,
266
327
  windowRows,
267
- listWindowStart,
328
+ listLine,
329
+ paintListWindow,
268
330
  paintCursorList,
269
331
  paintListEdit,
332
+ fieldWindow,
270
333
  branchLabel,
271
334
  paneResult,
272
335
  };
@@ -6,7 +6,8 @@ const primitives = require('./primitives.js');
6
6
  const diff = require('./diff.js');
7
7
 
8
8
  const { paneResult } = primitives;
9
- const { paintDiffLine, codeCursorInRows, expandSoftRows } = diff;
9
+ const { paintDiffLine, codeCursorInRows, codeEditHits } = diff;
10
+ const { expandSoftRows } = diff;
10
11
  const diffLib = require('../diff/diff.js');
11
12
  const { sameHunk } = diffLib;
12
13
 
@@ -35,9 +36,11 @@ const paintBodyUnit = (view, width, color) => {
35
36
  }
36
37
  const edit = overlay ? overlay.cursor : null;
37
38
  const cursor = codeCursorInRows(source, false, width, edit, scrollCol);
39
+ const editHits = editing ? codeEditHits(source, false, width, scrollCol) : [];
38
40
  return paneResult({
39
41
  body,
40
42
  cursor,
43
+ editHits,
41
44
  });
42
45
  };
43
46
 
@@ -84,6 +84,5 @@ const buildDocument = (stdout, stderr, root, exit, hint) => {
84
84
 
85
85
  module.exports = {
86
86
  toolHint,
87
- parseReports,
88
87
  buildDocument,
89
88
  };
package/lib/report-run.js CHANGED
@@ -160,7 +160,6 @@ const runCapture = async (proc, deps = {}) => {
160
160
  };
161
161
 
162
162
  module.exports = {
163
- outputMode,
164
163
  winCommand,
165
164
  runCapture,
166
165
  };
@@ -3,6 +3,8 @@
3
3
  const model = require('./report-model.js');
4
4
  const npmCommands = require('./npm-commands.js');
5
5
  const { isPassingTest } = npmCommands;
6
+ const utilities = require('./utilities.js');
7
+ const { unquote } = utilities;
6
8
 
7
9
  const { summary, problem, report, userTrace } = model;
8
10
 
@@ -69,17 +71,6 @@ const problemFromFields = (name, fields, root) => {
69
71
  });
70
72
  };
71
73
 
72
- const yamlValue = (raw) => {
73
- const text = `${raw ?? ''}`.trim();
74
- if (text.length < 2) return text;
75
- const open = text[0];
76
- const close = text[text.length - 1];
77
- const code = open.charCodeAt(0);
78
- const quoted = open === close && (code === 34 || code === 39);
79
- if (quoted) return text.slice(1, -1);
80
- return text;
81
- };
82
-
83
74
  const yamlFields = (lines) => {
84
75
  const fields = [];
85
76
  let index = 0;
@@ -93,7 +84,7 @@ const yamlFields = (lines) => {
93
84
  const value = match[3].trim().replace(/,$/, '');
94
85
  const block = value === '|-' || value === '|' || value === '>';
95
86
  if (!block) {
96
- fields.push([key, yamlValue(value)]);
87
+ fields.push([key, unquote(value)]);
97
88
  index += 1;
98
89
  continue;
99
90
  }
@@ -124,16 +115,23 @@ const tapName = (line) => {
124
115
  return match[1].trim();
125
116
  };
126
117
 
118
+ const STAT_FIELD = {
119
+ tests: 'tests',
120
+ pass: 'passed',
121
+ fail: 'failed',
122
+ skipped: 'skipped',
123
+ skip: 'skipped',
124
+ };
125
+
126
+ const applyStat = (stats, key, value) => {
127
+ if (key === 'duration_ms') stats.duration = value;
128
+ const field = Object.hasOwn(STAT_FIELD, key) ? STAT_FIELD[key] : '';
129
+ if (field) stats[field] = Number(value);
130
+ };
131
+
127
132
  const applyTapComment = (stats, line) => {
128
133
  const match = /^#\s+(\w+)\s+(.+)$/.exec(line.trim());
129
- if (!match) return;
130
- const key = match[1];
131
- const value = match[2].trim();
132
- if (key === 'tests') stats.tests = Number(value);
133
- if (key === 'pass') stats.passed = Number(value);
134
- if (key === 'fail') stats.failed = Number(value);
135
- if (key === 'skipped' || key === 'skip') stats.skipped = Number(value);
136
- if (key === 'duration_ms') stats.duration = value;
134
+ if (match) applyStat(stats, match[1], match[2].trim());
137
135
  };
138
136
 
139
137
  const isTapLine = (line) => {
@@ -259,6 +257,8 @@ const jestTrace = (text, file, root) => {
259
257
  return userTrace(file, root);
260
258
  };
261
259
 
260
+ const durationText = (value) => (value === undefined ? '' : `${value}`);
261
+
262
262
  const jestProblem = (item, file, root) => {
263
263
  const messages = item.failureMessages || [];
264
264
  const text = messages.join('\n');
@@ -270,7 +270,7 @@ const jestProblem = (item, file, root) => {
270
270
  trace: jestTrace(text, file, root),
271
271
  expected: expected ? expected[1] : '',
272
272
  actual: received ? received[1] : '',
273
- duration: item.duration === undefined ? '' : `${item.duration}`,
273
+ duration: durationText(item.duration),
274
274
  });
275
275
  };
276
276
 
@@ -298,29 +298,27 @@ const parseJest = (data, root) => {
298
298
  return report('test', { summary: stats, problems });
299
299
  };
300
300
 
301
+ const PLAYWRIGHT_FAILED = ['failed', 'timedOut'];
302
+
303
+ const playwrightProblem = (spec, result, root) => {
304
+ const error = result.error || {};
305
+ return problem({
306
+ name: spec.title || '',
307
+ message: error.message || result.status,
308
+ trace: userTrace(error.stack || '', root),
309
+ duration: durationText(result.duration),
310
+ });
311
+ };
312
+
301
313
  const walkPlaywright = (suite, problems, root) => {
302
- const specs = suite.specs || [];
303
- for (const spec of specs) {
304
- const tests = spec.tests || [];
305
- for (const item of tests) {
306
- const results = item.results || [];
307
- for (const result of results) {
308
- const status = result.status;
309
- if (status !== 'failed' && status !== 'timedOut') continue;
310
- const error = result.error || {};
311
- problems.push(
312
- problem({
313
- name: spec.title || '',
314
- message: error.message || status,
315
- trace: userTrace(error.stack || '', root),
316
- duration: result.duration === undefined ? '' : `${result.duration}`,
317
- }),
318
- );
319
- }
314
+ for (const spec of suite.specs || []) {
315
+ const results = (spec.tests || []).flatMap((item) => item.results || []);
316
+ for (const result of results) {
317
+ if (!PLAYWRIGHT_FAILED.includes(result.status)) continue;
318
+ problems.push(playwrightProblem(spec, result, root));
320
319
  }
321
320
  }
322
- const children = suite.suites || [];
323
- for (const child of children) walkPlaywright(child, problems, root);
321
+ for (const child of suite.suites || []) walkPlaywright(child, problems, root);
324
322
  };
325
323
 
326
324
  const parsePlaywright = (data, root) => {
@@ -349,12 +347,7 @@ const specStats = (stats, line) => {
349
347
  const match = /^[ℹi]\s+(\w+)\s+(.+)$/.exec(line.trim());
350
348
  if (!match) return false;
351
349
  const key = match[1];
352
- const value = match[2].trim();
353
- if (key === 'tests') stats.tests = Number(value);
354
- if (key === 'pass') stats.passed = Number(value);
355
- if (key === 'fail') stats.failed = Number(value);
356
- if (key === 'skipped') stats.skipped = Number(value);
357
- if (key === 'duration_ms') stats.duration = value;
350
+ applyStat(stats, key, match[2].trim());
358
351
  return key === 'tests' || key === 'pass' || key === 'fail';
359
352
  };
360
353
 
@@ -325,13 +325,6 @@ const parseToolText = (text, root) => {
325
325
  };
326
326
 
327
327
  module.exports = {
328
- parseEslintJson,
329
- parseEslintStylish,
330
- parseTsc,
331
- parsePrettier,
332
- parseAudit,
333
- parseOutdated,
334
- parseLs,
335
328
  parseToolJson,
336
329
  parseToolText,
337
330
  };
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const files = require('./files.js');
4
- const { TODO_FILE } = files;
4
+ const { TASKS_FILE } = files;
5
5
 
6
6
  const flatText = (text) => `${text ?? ''}`.trim().replace(/\n/g, ' ');
7
7
 
@@ -30,7 +30,7 @@ const copyMap = (map) => {
30
30
 
31
31
  const snapshotSide = (store) => ({
32
32
  status: store.status,
33
- todos: snapList(store.todos),
33
+ tasks: snapList(store.tasks),
34
34
  feedback: copyMap(store.feedback),
35
35
  code: copyMap(store.code),
36
36
  });
@@ -68,83 +68,12 @@ const snapText = (snap) => snap.text;
68
68
  const flags = (n) => new Array(n).fill(false);
69
69
 
70
70
  const adoptTodo = (store, snap) => {
71
- const id = store.nextTodoId;
72
- store.nextTodoId += 1;
73
- const file = snap.file || TODO_FILE;
71
+ const id = store.nextTaskId;
72
+ store.nextTaskId += 1;
73
+ const file = snap.file || TASKS_FILE;
74
74
  return { id, file, text: snap.text, done: snap.done };
75
75
  };
76
76
 
77
- const findMerged = (merged, alias, text) => {
78
- const linked = alias.get(text);
79
- if (linked) {
80
- const at = merged.indexOf(linked);
81
- if (at >= 0) return at;
82
- }
83
- for (let i = 0; i < merged.length; i++) {
84
- if (liveText(merged[i]) === text) return i;
85
- }
86
- return -1;
87
- };
88
-
89
- const insertTheir = (store, merged, theirs, theirKeep, alias) => {
90
- for (let i = 0; i < theirs.length; i++) {
91
- if (!theirKeep[i]) continue;
92
- let at = merged.length;
93
- let placed = false;
94
- for (let j = i - 1; j >= 0 && !placed; j--) {
95
- const prev = findMerged(merged, alias, theirs[j].text);
96
- if (prev < 0) continue;
97
- at = prev + 1;
98
- placed = true;
99
- }
100
- for (let j = i + 1; j < theirs.length && !placed; j++) {
101
- const next = findMerged(merged, alias, theirs[j].text);
102
- if (next < 0) continue;
103
- at = next;
104
- placed = true;
105
- }
106
- const item = adoptTodo(store, theirs[i]);
107
- merged.splice(at, 0, item);
108
- alias.set(theirs[i].text, item);
109
- }
110
- return merged;
111
- };
112
-
113
- const keepOurs = (ours, kept, alias, index, text) => {
114
- kept[index] = true;
115
- alias.set(text, ours[index]);
116
- };
117
-
118
- const mergeBaseTodo = (
119
- base,
120
- ours,
121
- theirs,
122
- used,
123
- kept,
124
- dropped,
125
- theirKeep,
126
- alias,
127
- ) => {
128
- const text = base.text;
129
- const oi = claim(ours, used.ours, text, liveText);
130
- const ti = claim(theirs, used.theirs, text, snapText);
131
- if (oi >= 0 && ti >= 0) {
132
- const done = pickDone(base.done, ours[oi].done === true, theirs[ti].done);
133
- ours[oi] = withDone(ours[oi], done);
134
- keepOurs(ours, kept, alias, oi, text);
135
- return;
136
- }
137
- if (oi >= 0) {
138
- if (ours[oi].done === base.done) {
139
- dropped[oi] = true;
140
- return;
141
- }
142
- keepOurs(ours, kept, alias, oi, text);
143
- return;
144
- }
145
- if (ti >= 0 && theirs[ti].done !== base.done) theirKeep[ti] = true;
146
- };
147
-
148
77
  const replaceTodo = (todo, snap) => ({
149
78
  id: todo.id,
150
79
  file: todo.file,
@@ -152,68 +81,109 @@ const replaceTodo = (todo, snap) => ({
152
81
  done: snap.done,
153
82
  });
154
83
 
155
- const fillGaps = (merged, gaps, theirs, theirKeep, alias) => {
156
- let filled = 0;
157
- for (let i = 0; i < theirs.length && filled < gaps.length; i++) {
158
- if (!theirKeep[i]) continue;
159
- const gap = gaps[filled];
160
- const item = replaceTodo(gap.todo, theirs[i]);
161
- merged.splice(gap.at, 0, item);
162
- alias.set(theirs[i].text, item);
163
- theirKeep[i] = false;
164
- filled += 1;
84
+ class TodoMerge {
85
+ constructor(store, theirs) {
86
+ this.store = store;
87
+ this.ours = store.tasks.filter((todo) => liveText(todo));
88
+ this.theirs = theirs;
89
+ this.usedOurs = flags(this.ours.length);
90
+ this.usedTheirs = flags(theirs.length);
91
+ this.kept = flags(this.ours.length);
92
+ this.dropped = flags(this.ours.length);
93
+ this.theirKeep = flags(theirs.length);
94
+ this.alias = new Map();
95
+ }
96
+
97
+ keepOurs(index, text) {
98
+ this.kept[index] = true;
99
+ this.alias.set(text, this.ours[index]);
100
+ }
101
+
102
+ mergeBase(base) {
103
+ const { ours, theirs } = this;
104
+ const text = base.text;
105
+ const oi = claim(ours, this.usedOurs, text, liveText);
106
+ const ti = claim(theirs, this.usedTheirs, text, snapText);
107
+ if (oi >= 0 && ti >= 0) {
108
+ const done = pickDone(base.done, ours[oi].done === true, theirs[ti].done);
109
+ ours[oi] = withDone(ours[oi], done);
110
+ this.keepOurs(oi, text);
111
+ } else if (oi >= 0) {
112
+ if (ours[oi].done === base.done) this.dropped[oi] = true;
113
+ else this.keepOurs(oi, text);
114
+ } else if (ti >= 0 && theirs[ti].done !== base.done) {
115
+ this.theirKeep[ti] = true;
116
+ }
165
117
  }
166
- };
167
118
 
168
- const mergeFreshTodo = (ours, theirs, used, kept, alias, index) => {
169
- const text = liveText(ours[index]);
170
- const ti = claim(theirs, used.theirs, text, snapText);
171
- if (ti < 0) {
172
- kept[index] = true;
173
- return;
119
+ mergeFresh(index) {
120
+ const { ours, theirs } = this;
121
+ const text = liveText(ours[index]);
122
+ const ti = claim(theirs, this.usedTheirs, text, snapText);
123
+ if (ti < 0) {
124
+ this.kept[index] = true;
125
+ return;
126
+ }
127
+ const done = ours[index].done === true || theirs[ti].done === true;
128
+ ours[index] = withDone(ours[index], done);
129
+ this.keepOurs(index, text);
174
130
  }
175
- const done = ours[index].done === true || theirs[ti].done === true;
176
- ours[index] = withDone(ours[index], done);
177
- keepOurs(ours, kept, alias, index, text);
178
- };
179
131
 
180
- const mergeTodos = (store, base, theirs) => {
181
- const ours = [];
182
- for (const todo of store.todos) {
183
- if (!liveText(todo)) continue;
184
- ours.push(todo);
185
- }
186
- const used = { ours: flags(ours.length), theirs: flags(theirs.length) };
187
- const kept = flags(ours.length);
188
- const dropped = flags(ours.length);
189
- const theirKeep = flags(theirs.length);
190
- const alias = new Map();
191
- for (const item of base) {
192
- mergeBaseTodo(item, ours, theirs, used, kept, dropped, theirKeep, alias);
132
+ findMerged(merged, text) {
133
+ const linked = this.alias.get(text);
134
+ const at = linked ? merged.indexOf(linked) : -1;
135
+ if (at >= 0) return at;
136
+ return merged.findIndex((todo) => liveText(todo) === text);
193
137
  }
194
- for (let i = 0; i < ours.length; i++) {
195
- if (used.ours[i]) continue;
196
- mergeFreshTodo(ours, theirs, used, kept, alias, i);
138
+
139
+ theirPlace(merged, index) {
140
+ const { theirs } = this;
141
+ for (let j = index - 1; j >= 0; j--) {
142
+ const prev = this.findMerged(merged, theirs[j].text);
143
+ if (prev >= 0) return prev + 1;
144
+ }
145
+ for (let j = index + 1; j < theirs.length; j++) {
146
+ const next = this.findMerged(merged, theirs[j].text);
147
+ if (next >= 0) return next;
148
+ }
149
+ return merged.length;
197
150
  }
198
- for (let i = 0; i < theirs.length; i++) {
199
- if (!used.theirs[i]) theirKeep[i] = true;
151
+
152
+ place(merged, at, index, item) {
153
+ merged.splice(at, 0, item);
154
+ this.alias.set(this.theirs[index].text, item);
155
+ this.theirKeep[index] = false;
200
156
  }
201
- const merged = [];
202
- const gaps = [];
203
- let cursor = 0;
204
- for (let i = 0; i < ours.length; i++) {
205
- if (kept[i]) {
206
- merged.push(ours[i]);
207
- cursor += 1;
208
- continue;
157
+
158
+ merge(base) {
159
+ const { ours, theirs } = this;
160
+ for (const item of base) this.mergeBase(item);
161
+ for (let i = 0; i < ours.length; i++) {
162
+ if (!this.usedOurs[i]) this.mergeFresh(i);
163
+ }
164
+ for (let i = 0; i < theirs.length; i++) {
165
+ if (!this.usedTheirs[i]) this.theirKeep[i] = true;
209
166
  }
210
- if (!dropped[i]) continue;
211
- gaps.push({ at: cursor, todo: ours[i] });
212
- cursor += 1;
167
+ const merged = [];
168
+ const gaps = [];
169
+ for (let i = 0; i < ours.length; i++) {
170
+ const at = merged.length + gaps.length;
171
+ if (this.kept[i]) merged.push(ours[i]);
172
+ else if (this.dropped[i]) gaps.push({ at, task: ours[i] });
173
+ }
174
+ for (let i = 0; i < theirs.length && gaps.length; i++) {
175
+ if (!this.theirKeep[i]) continue;
176
+ const gap = gaps.shift();
177
+ this.place(merged, gap.at, i, replaceTodo(gap.task, theirs[i]));
178
+ }
179
+ for (let i = 0; i < theirs.length; i++) {
180
+ if (!this.theirKeep[i]) continue;
181
+ const at = this.theirPlace(merged, i);
182
+ this.place(merged, at, i, adoptTodo(this.store, theirs[i]));
183
+ }
184
+ return merged;
213
185
  }
214
- fillGaps(merged, gaps, theirs, theirKeep, alias);
215
- store.todos = insertTheir(store, merged, theirs, theirKeep, alias);
216
- };
186
+ }
217
187
 
218
188
  const sameNote = (left, right) =>
219
189
  left.text === right.text && !!left.done === !!right.done;
@@ -257,9 +227,9 @@ const mergeMap = (ours, base, theirs) => {
257
227
 
258
228
  const mergeReview = (store, disk) => {
259
229
  const base = store.baseline;
260
- const baseTodos = base ? base.todos : [];
230
+ const baseTodos = base?.tasks ?? base?.todos ?? [];
261
231
  const baseStatus = base ? base.status : store.status;
262
- mergeTodos(store, baseTodos, snapList(disk.todos));
232
+ store.tasks = new TodoMerge(store, snapList(disk.tasks)).merge(baseTodos);
263
233
  const feedback = base ? base.feedback : new Map();
264
234
  const code = base ? base.code : new Map();
265
235
  mergeMap(store.feedback, feedback, disk.feedback);