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.
- package/CHANGELOG.md +14 -1
- package/README.md +1 -2
- package/lib/capabilities.js +0 -4
- package/lib/clipboard.js +39 -1
- package/lib/deps-diff.js +0 -3
- package/lib/deps.js +144 -176
- package/lib/diff/inline.js +66 -1
- package/lib/diff/unit.js +11 -28
- package/lib/editor.js +69 -9
- package/lib/files.js +17 -16
- package/lib/git-deps.js +58 -95
- package/lib/github.js +7 -7
- package/lib/gitlab.js +3 -3
- package/lib/highlight/core.js +11 -0
- package/lib/highlight/dot.js +2 -12
- package/lib/highlight/log.js +4 -11
- package/lib/highlight/txt.js +2 -12
- package/lib/keys.js +22 -12
- package/lib/manifest.js +0 -4
- package/lib/npm-commands.js +1 -22
- package/lib/remote.js +1 -7
- package/lib/render/branches.js +4 -13
- package/lib/render/chrome.js +85 -31
- package/lib/render/commits.js +95 -207
- package/lib/render/diff.js +31 -1
- package/lib/render/files.js +7 -16
- package/lib/render/frame.js +95 -110
- package/lib/render/notes.js +71 -16
- package/lib/render/npm.js +104 -18
- package/lib/render/primitives.js +108 -45
- package/lib/render/unit.js +4 -1
- package/lib/report-parse.js +0 -1
- package/lib/report-run.js +0 -1
- package/lib/report-test.js +40 -47
- package/lib/report-tools.js +0 -7
- package/lib/review-merge.js +101 -131
- package/lib/review.js +64 -77
- package/lib/select.js +52 -0
- package/lib/session/actions.js +23 -22
- package/lib/session/branches.js +5 -3
- package/lib/session/change-actions.js +173 -146
- package/lib/session/commit.js +32 -13
- package/lib/session/compose.js +120 -77
- package/lib/session/dispatch.js +36 -21
- package/lib/session/edit-click.js +57 -0
- package/lib/session/files-pane.js +24 -24
- package/lib/session/items.js +69 -89
- package/lib/session/lifecycle.js +31 -96
- package/lib/session/load.js +51 -58
- package/lib/session/navigation.js +105 -96
- package/lib/session/npm.js +158 -77
- package/lib/session/ops.js +192 -51
- package/lib/session/pointer.js +11 -5
- package/lib/session/progress.js +9 -6
- package/lib/session/review.js +35 -54
- package/lib/session/tasks.js +292 -0
- package/lib/session/terminal.js +13 -17
- package/lib/session/unit.js +3 -4
- package/lib/session/update.js +76 -97
- package/lib/session/view.js +17 -14
- package/lib/session/watch.js +47 -27
- package/lib/session.js +34 -79
- package/lib/update.js +1 -5
- package/lib/utilities.js +21 -1
- package/lib/wrap.js +15 -0
- package/package.json +1 -1
- package/lib/session/todos.js +0 -293
package/lib/render/primitives.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
126
|
-
|
|
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
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
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
|
|
206
|
-
return {
|
|
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
|
-
|
|
328
|
+
listLine,
|
|
329
|
+
paintListWindow,
|
|
268
330
|
paintCursorList,
|
|
269
331
|
paintListEdit,
|
|
332
|
+
fieldWindow,
|
|
270
333
|
branchLabel,
|
|
271
334
|
paneResult,
|
|
272
335
|
};
|
package/lib/render/unit.js
CHANGED
|
@@ -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,
|
|
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
|
|
package/lib/report-parse.js
CHANGED
package/lib/report-run.js
CHANGED
package/lib/report-test.js
CHANGED
|
@@ -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,
|
|
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 (
|
|
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
|
|
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
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
package/lib/report-tools.js
CHANGED
|
@@ -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
|
};
|
package/lib/review-merge.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const files = require('./files.js');
|
|
4
|
-
const {
|
|
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
|
-
|
|
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.
|
|
72
|
-
store.
|
|
73
|
-
const file = snap.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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (
|
|
184
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
211
|
-
gaps
|
|
212
|
-
|
|
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
|
-
|
|
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
|
|
230
|
+
const baseTodos = base?.tasks ?? base?.todos ?? [];
|
|
261
231
|
const baseStatus = base ? base.status : store.status;
|
|
262
|
-
|
|
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);
|