reslop 0.2.3 → 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 +27 -1
- package/README.md +8 -4
- package/bin/reslop.js +4 -2
- package/lib/api.js +2 -1
- package/lib/capabilities.js +0 -4
- package/lib/cli.js +19 -10
- package/lib/clipboard.js +45 -4
- package/lib/deps-diff.js +61 -118
- package/lib/deps.js +166 -235
- package/lib/detect.js +4 -2
- package/lib/diff/align.js +2 -1
- package/lib/diff/diff.js +6 -3
- package/lib/diff/display.js +2 -1
- package/lib/diff/inline.js +68 -2
- package/lib/diff/patch.js +2 -1
- package/lib/diff/unit.js +15 -30
- package/lib/editor.js +91 -13
- package/lib/files.js +54 -27
- package/lib/git-branches.js +107 -154
- package/lib/git-deps.js +78 -155
- package/lib/git-worktree.js +508 -0
- package/lib/git.js +23 -580
- package/lib/github.js +9 -8
- package/lib/gitlab.js +5 -4
- package/lib/highlight/bash.js +2 -1
- package/lib/highlight/core.js +11 -0
- package/lib/highlight/css.js +2 -1
- package/lib/highlight/csv.js +2 -1
- package/lib/highlight/dot.js +3 -12
- package/lib/highlight/html.js +2 -1
- package/lib/highlight/json.js +2 -1
- package/lib/highlight/log.js +5 -11
- package/lib/highlight/txt.js +3 -12
- package/lib/highlight.js +2 -1
- package/lib/keys.js +22 -12
- package/lib/manifest.js +4 -6
- package/lib/npm-commands.js +82 -24
- package/lib/remote.js +21 -20
- package/lib/render/branches.js +9 -27
- package/lib/render/chrome.js +109 -35
- package/lib/render/commits.js +216 -216
- package/lib/render/diff.js +160 -47
- package/lib/render/files.js +9 -26
- package/lib/render/frame.js +97 -118
- package/lib/render/notes.js +74 -26
- package/lib/render/npm.js +104 -30
- package/lib/render/present.js +2 -1
- package/lib/render/primitives.js +128 -52
- package/lib/render/render.js +2 -1
- package/lib/render/unit.js +17 -11
- package/lib/report-model.js +6 -4
- package/lib/report-parse.js +8 -14
- package/lib/report-render.js +38 -21
- package/lib/report-run.js +12 -11
- package/lib/report-test.js +45 -51
- package/lib/report-tools.js +44 -25
- package/lib/review-merge.js +102 -131
- package/lib/review.js +69 -78
- package/lib/select.js +52 -0
- package/lib/session/actions.js +72 -87
- package/lib/session/branches.js +18 -52
- package/lib/session/change-actions.js +175 -147
- package/lib/session/commit.js +65 -61
- package/lib/session/compose.js +152 -83
- package/lib/session/dispatch.js +53 -46
- package/lib/session/edit-click.js +57 -0
- package/lib/session/files-pane.js +30 -51
- package/lib/session/items.js +73 -90
- package/lib/session/lifecycle.js +37 -98
- package/lib/session/load.js +52 -57
- package/lib/session/navigation.js +105 -95
- package/lib/session/npm.js +205 -71
- package/lib/session/ops.js +200 -45
- package/lib/session/pointer.js +33 -8
- package/lib/session/progress.js +9 -6
- package/lib/session/review.js +35 -53
- package/lib/session/tasks.js +292 -0
- package/lib/session/terminal.js +17 -19
- package/lib/session/unit.js +3 -4
- package/lib/session/update.js +76 -96
- package/lib/session/view.js +45 -16
- package/lib/session/watch.js +47 -27
- package/lib/session.js +66 -97
- package/lib/source.js +6 -3
- package/lib/update.js +5 -13
- package/lib/utilities.js +25 -3
- package/lib/wrap.js +18 -1
- package/package.json +1 -1
- package/lib/session/todos.js +0 -293
package/lib/review-merge.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const files = require('./files.js');
|
|
4
|
+
const { TASKS_FILE } = files;
|
|
4
5
|
|
|
5
6
|
const flatText = (text) => `${text ?? ''}`.trim().replace(/\n/g, ' ');
|
|
6
7
|
|
|
@@ -29,7 +30,7 @@ const copyMap = (map) => {
|
|
|
29
30
|
|
|
30
31
|
const snapshotSide = (store) => ({
|
|
31
32
|
status: store.status,
|
|
32
|
-
|
|
33
|
+
tasks: snapList(store.tasks),
|
|
33
34
|
feedback: copyMap(store.feedback),
|
|
34
35
|
code: copyMap(store.code),
|
|
35
36
|
});
|
|
@@ -67,83 +68,12 @@ const snapText = (snap) => snap.text;
|
|
|
67
68
|
const flags = (n) => new Array(n).fill(false);
|
|
68
69
|
|
|
69
70
|
const adoptTodo = (store, snap) => {
|
|
70
|
-
const id = store.
|
|
71
|
-
store.
|
|
72
|
-
const file = snap.file ||
|
|
71
|
+
const id = store.nextTaskId;
|
|
72
|
+
store.nextTaskId += 1;
|
|
73
|
+
const file = snap.file || TASKS_FILE;
|
|
73
74
|
return { id, file, text: snap.text, done: snap.done };
|
|
74
75
|
};
|
|
75
76
|
|
|
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
77
|
const replaceTodo = (todo, snap) => ({
|
|
148
78
|
id: todo.id,
|
|
149
79
|
file: todo.file,
|
|
@@ -151,68 +81,109 @@ const replaceTodo = (todo, snap) => ({
|
|
|
151
81
|
done: snap.done,
|
|
152
82
|
});
|
|
153
83
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
+
}
|
|
164
117
|
}
|
|
165
|
-
};
|
|
166
118
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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);
|
|
173
130
|
}
|
|
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
131
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (
|
|
183
|
-
|
|
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);
|
|
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);
|
|
192
137
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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;
|
|
196
150
|
}
|
|
197
|
-
|
|
198
|
-
|
|
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;
|
|
199
156
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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;
|
|
208
166
|
}
|
|
209
|
-
|
|
210
|
-
gaps
|
|
211
|
-
|
|
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;
|
|
212
185
|
}
|
|
213
|
-
|
|
214
|
-
store.todos = insertTheir(store, merged, theirs, theirKeep, alias);
|
|
215
|
-
};
|
|
186
|
+
}
|
|
216
187
|
|
|
217
188
|
const sameNote = (left, right) =>
|
|
218
189
|
left.text === right.text && !!left.done === !!right.done;
|
|
@@ -256,9 +227,9 @@ const mergeMap = (ours, base, theirs) => {
|
|
|
256
227
|
|
|
257
228
|
const mergeReview = (store, disk) => {
|
|
258
229
|
const base = store.baseline;
|
|
259
|
-
const baseTodos = base
|
|
230
|
+
const baseTodos = base?.tasks ?? base?.todos ?? [];
|
|
260
231
|
const baseStatus = base ? base.status : store.status;
|
|
261
|
-
|
|
232
|
+
store.tasks = new TodoMerge(store, snapList(disk.tasks)).merge(baseTodos);
|
|
262
233
|
const feedback = base ? base.feedback : new Map();
|
|
263
234
|
const code = base ? base.code : new Map();
|
|
264
235
|
mergeMap(store.feedback, feedback, disk.feedback);
|
package/lib/review.js
CHANGED
|
@@ -3,28 +3,32 @@
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
const {
|
|
6
|
+
const files = require('./files.js');
|
|
7
|
+
const { TASKS_FILE, REVIEW_DIR } = files;
|
|
8
|
+
const reviewMerge = require('./review-merge.js');
|
|
9
|
+
const { captureBaseline, mergeReview } = reviewMerge;
|
|
10
|
+
const utilities = require('./utilities.js');
|
|
11
|
+
const { pad2, dateStamp } = utilities;
|
|
8
12
|
|
|
9
13
|
const createStore = (reviewPath, templates = []) => ({
|
|
10
14
|
reviewPath,
|
|
11
15
|
dirty: false,
|
|
12
|
-
|
|
16
|
+
nextTaskId: 1,
|
|
13
17
|
feedback: new Map(),
|
|
14
18
|
code: new Map(),
|
|
15
|
-
|
|
19
|
+
tasks: [],
|
|
16
20
|
templates: [...templates],
|
|
17
21
|
status: 'editing',
|
|
18
22
|
baseline: null,
|
|
19
23
|
});
|
|
20
24
|
|
|
21
25
|
const noteCounts = (store) => {
|
|
22
|
-
const counts = { feedback: 0,
|
|
26
|
+
const counts = { feedback: 0, tasks: 0, tasksDone: 0, code: 0 };
|
|
23
27
|
if (!store) return counts;
|
|
24
|
-
for (const
|
|
25
|
-
if (!
|
|
26
|
-
counts.
|
|
27
|
-
if (
|
|
28
|
+
for (const task of store.tasks) {
|
|
29
|
+
if (!task.text.trim()) continue;
|
|
30
|
+
counts.tasks += 1;
|
|
31
|
+
if (task.done) counts.tasksDone += 1;
|
|
28
32
|
}
|
|
29
33
|
for (const note of store.feedback.values()) {
|
|
30
34
|
if (note.text.trim()) counts.feedback += 1;
|
|
@@ -35,21 +39,20 @@ const noteCounts = (store) => {
|
|
|
35
39
|
|
|
36
40
|
const hasNotes = (store) => {
|
|
37
41
|
const counts = noteCounts(store);
|
|
38
|
-
return counts.feedback > 0 || counts.
|
|
42
|
+
return counts.feedback > 0 || counts.tasks > 0 || counts.code > 0;
|
|
39
43
|
};
|
|
40
44
|
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
const prev = store.feedback.get(key);
|
|
45
|
-
if (!trimmed) {
|
|
45
|
+
const putNote = (store, notes, key, note, clear) => {
|
|
46
|
+
const prev = notes.get(key);
|
|
47
|
+
if (clear) {
|
|
46
48
|
if (!prev) return;
|
|
47
|
-
|
|
49
|
+
notes.delete(key);
|
|
48
50
|
store.dirty = true;
|
|
49
51
|
return;
|
|
50
52
|
}
|
|
53
|
+
const text = note.text ?? '';
|
|
51
54
|
if (prev && prev.text === text) return;
|
|
52
|
-
|
|
55
|
+
notes.set(key, {
|
|
53
56
|
file: note.file,
|
|
54
57
|
oldStart: note.oldStart ?? 0,
|
|
55
58
|
newStart: note.newStart ?? 0,
|
|
@@ -62,42 +65,28 @@ const setFeedback = (store, key, note) => {
|
|
|
62
65
|
store.dirty = true;
|
|
63
66
|
};
|
|
64
67
|
|
|
68
|
+
const setFeedback = (store, key, note) => {
|
|
69
|
+
const clear = !(note.text ?? '').trim();
|
|
70
|
+
putNote(store, store.feedback, key, note, clear);
|
|
71
|
+
};
|
|
72
|
+
|
|
65
73
|
const setCode = (store, key, note) => {
|
|
66
|
-
|
|
67
|
-
if (note.clear) {
|
|
68
|
-
if (!prev) return;
|
|
69
|
-
store.code.delete(key);
|
|
70
|
-
store.dirty = true;
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
const text = note.text ?? '';
|
|
74
|
-
if (prev && prev.text === text) return;
|
|
75
|
-
store.code.set(key, {
|
|
76
|
-
file: note.file,
|
|
77
|
-
oldStart: note.oldStart ?? 0,
|
|
78
|
-
newStart: note.newStart ?? 0,
|
|
79
|
-
blockId: note.blockId ?? 0,
|
|
80
|
-
origin: note.origin ?? '',
|
|
81
|
-
header: note.header ?? '',
|
|
82
|
-
text,
|
|
83
|
-
done: note.done ?? prev?.done ?? false,
|
|
84
|
-
});
|
|
85
|
-
store.dirty = true;
|
|
74
|
+
putNote(store, store.code, key, note, Boolean(note.clear));
|
|
86
75
|
};
|
|
87
76
|
|
|
88
|
-
const
|
|
89
|
-
const id = store.
|
|
90
|
-
store.
|
|
77
|
+
const addTask = (store, file, text = '') => {
|
|
78
|
+
const id = store.nextTaskId;
|
|
79
|
+
store.nextTaskId += 1;
|
|
91
80
|
const todo = { id, file, text, done: false };
|
|
92
|
-
store.
|
|
81
|
+
store.tasks.push(todo);
|
|
93
82
|
store.dirty = true;
|
|
94
83
|
return todo;
|
|
95
84
|
};
|
|
96
85
|
|
|
97
|
-
const
|
|
86
|
+
const removeTask = (store, id) => {
|
|
98
87
|
const next = [];
|
|
99
88
|
let found = false;
|
|
100
|
-
for (const todo of store.
|
|
89
|
+
for (const todo of store.tasks) {
|
|
101
90
|
if (todo.id === id) {
|
|
102
91
|
found = true;
|
|
103
92
|
continue;
|
|
@@ -105,28 +94,28 @@ const removeTodo = (store, id) => {
|
|
|
105
94
|
next.push(todo);
|
|
106
95
|
}
|
|
107
96
|
if (!found) return false;
|
|
108
|
-
store.
|
|
97
|
+
store.tasks = next;
|
|
109
98
|
store.dirty = true;
|
|
110
99
|
return true;
|
|
111
100
|
};
|
|
112
101
|
|
|
113
|
-
const
|
|
114
|
-
const i = store.
|
|
102
|
+
const setTaskText = (store, id, text) => {
|
|
103
|
+
const i = store.tasks.findIndex((todo) => todo.id === id);
|
|
115
104
|
if (i < 0) return;
|
|
116
|
-
const prev = store.
|
|
105
|
+
const prev = store.tasks[i];
|
|
117
106
|
const trimmed = text.trim();
|
|
118
|
-
if (!trimmed) return void
|
|
107
|
+
if (!trimmed) return void removeTask(store, id);
|
|
119
108
|
if (prev.text === text) return;
|
|
120
|
-
store.
|
|
109
|
+
store.tasks[i] = { id: prev.id, file: prev.file, text, done: prev.done };
|
|
121
110
|
store.dirty = true;
|
|
122
111
|
};
|
|
123
112
|
|
|
124
|
-
const
|
|
125
|
-
const i = store.
|
|
113
|
+
const setTaskDone = (store, id, done) => {
|
|
114
|
+
const i = store.tasks.findIndex((todo) => todo.id === id);
|
|
126
115
|
if (i < 0) return;
|
|
127
|
-
const prev = store.
|
|
116
|
+
const prev = store.tasks[i];
|
|
128
117
|
if (prev.done === done) return;
|
|
129
|
-
store.
|
|
118
|
+
store.tasks[i] = { id: prev.id, file: prev.file, text: prev.text, done };
|
|
130
119
|
store.dirty = true;
|
|
131
120
|
};
|
|
132
121
|
|
|
@@ -162,9 +151,9 @@ const applyImportedNotes = (store, imported) => {
|
|
|
162
151
|
done,
|
|
163
152
|
});
|
|
164
153
|
}
|
|
165
|
-
const todos = imported.todos ?? [];
|
|
154
|
+
const todos = imported.tasks ?? imported.todos ?? [];
|
|
166
155
|
for (const todo of todos) {
|
|
167
|
-
const added =
|
|
156
|
+
const added = addTask(store, todo.file || '', todo.text ?? '');
|
|
168
157
|
if (todo.done) added.done = true;
|
|
169
158
|
}
|
|
170
159
|
};
|
|
@@ -231,6 +220,8 @@ const SECTION_LINE = {
|
|
|
231
220
|
'### Todo': 'todo',
|
|
232
221
|
'### Feedback': 'feedback',
|
|
233
222
|
};
|
|
223
|
+
const BACKLOG_HEAD = 'Backlog';
|
|
224
|
+
const LEGACY_BACKLOG = 'TODOs';
|
|
234
225
|
|
|
235
226
|
const AGENT_INSTRUCTIONS = [
|
|
236
227
|
'- Execute reviews with `status` `ready, partial, editing`',
|
|
@@ -240,6 +231,8 @@ const AGENT_INSTRUCTIONS = [
|
|
|
240
231
|
'- If not `editing`, set `status` to `partial` if some remain, ' +
|
|
241
232
|
'or `done` if all are `[x]`',
|
|
242
233
|
'- Do not redo items already marked `[x]`',
|
|
234
|
+
'- Run the full check with `npm t`',
|
|
235
|
+
'- Run specific test files with `reslop t -- node --test <files>`',
|
|
243
236
|
].join('\n');
|
|
244
237
|
|
|
245
238
|
const toInt = (text) => parseInt(text, 10);
|
|
@@ -269,9 +262,9 @@ const parseFrontmatterStatus = (text) => {
|
|
|
269
262
|
const isDoneMark = (mark) => mark === 'x' || mark === 'X';
|
|
270
263
|
|
|
271
264
|
const pushTodo = (builder, file, text, done) => {
|
|
272
|
-
const id = builder.
|
|
273
|
-
builder.
|
|
274
|
-
builder.
|
|
265
|
+
const id = builder.nextTaskId;
|
|
266
|
+
builder.nextTaskId += 1;
|
|
267
|
+
builder.tasks.push({ id, file, text, done });
|
|
275
268
|
};
|
|
276
269
|
|
|
277
270
|
const checkboxLine = (text, done) => {
|
|
@@ -346,7 +339,7 @@ const parseFeedSuffix = (textItem) => {
|
|
|
346
339
|
const fileGroup = (groups, file) => {
|
|
347
340
|
let group = groups.get(file);
|
|
348
341
|
if (!group) {
|
|
349
|
-
group = {
|
|
342
|
+
group = { tasks: [], feedback: [], code: [] };
|
|
350
343
|
groups.set(file, group);
|
|
351
344
|
}
|
|
352
345
|
return group;
|
|
@@ -354,9 +347,9 @@ const fileGroup = (groups, file) => {
|
|
|
354
347
|
|
|
355
348
|
const groupReviewNotes = (notes) => {
|
|
356
349
|
const groups = new Map();
|
|
357
|
-
for (const todo of notes.
|
|
350
|
+
for (const todo of notes.tasks) {
|
|
358
351
|
if (!todo.text.trim()) continue;
|
|
359
|
-
fileGroup(groups,
|
|
352
|
+
fileGroup(groups, TASKS_FILE).tasks.push(todo);
|
|
360
353
|
}
|
|
361
354
|
for (const key of notes.feedback.keys()) {
|
|
362
355
|
const note = notes.feedback.get(key);
|
|
@@ -407,12 +400,19 @@ const emitCodeItem = (lines, note) => {
|
|
|
407
400
|
lines.push(fence);
|
|
408
401
|
};
|
|
409
402
|
|
|
403
|
+
const sectionTitle = (file) => (file === TASKS_FILE ? BACKLOG_HEAD : file);
|
|
404
|
+
|
|
405
|
+
const backlogFile = (name) => {
|
|
406
|
+
if (name === BACKLOG_HEAD || name === LEGACY_BACKLOG) return TASKS_FILE;
|
|
407
|
+
return name;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
410
|
const emitReviewGroups = (lines, groups) => {
|
|
411
411
|
for (const file of groups.keys()) {
|
|
412
412
|
const group = groups.get(file);
|
|
413
|
-
lines.push(`## ${file}`);
|
|
413
|
+
lines.push(`## ${sectionTitle(file)}`);
|
|
414
414
|
lines.push('');
|
|
415
|
-
for (const todo of group.
|
|
415
|
+
for (const todo of group.tasks) {
|
|
416
416
|
lines.push(checkboxLine(todo.text, todo.done));
|
|
417
417
|
}
|
|
418
418
|
for (const item of group.feedback) {
|
|
@@ -458,9 +458,9 @@ const flushPendingFeedback = (builder, pending) => {
|
|
|
458
458
|
|
|
459
459
|
const headingFile = (line) => {
|
|
460
460
|
const quoted = FILE_QUOTE.exec(line);
|
|
461
|
-
if (quoted) return quoted[1];
|
|
461
|
+
if (quoted) return backlogFile(quoted[1]);
|
|
462
462
|
if (line.startsWith('## ') && line !== '## Agent instructions') {
|
|
463
|
-
return line.slice(3);
|
|
463
|
+
return backlogFile(line.slice(3));
|
|
464
464
|
}
|
|
465
465
|
return null;
|
|
466
466
|
};
|
|
@@ -642,15 +642,6 @@ const TEMPLATES_FILE = 'templates.json';
|
|
|
642
642
|
const AUTOSAVE_MS = 3000;
|
|
643
643
|
const REVIEW_NAME = /^(\d{4}-\d{2}-\d{2})-(\d+)\.md$/;
|
|
644
644
|
|
|
645
|
-
const pad2 = (n) => `${n}`.padStart(2, '0');
|
|
646
|
-
|
|
647
|
-
const dateStamp = (date) => {
|
|
648
|
-
const y = date.getFullYear();
|
|
649
|
-
const month = pad2(date.getMonth() + 1);
|
|
650
|
-
const day = pad2(date.getDate());
|
|
651
|
-
return `${y}-${month}-${day}`;
|
|
652
|
-
};
|
|
653
|
-
|
|
654
645
|
const allocateReviewPath = (dir, date, existingNames) => {
|
|
655
646
|
const stamp = dateStamp(date);
|
|
656
647
|
let max = -1;
|
|
@@ -798,10 +789,10 @@ module.exports = {
|
|
|
798
789
|
rememberTemplate,
|
|
799
790
|
setFeedback,
|
|
800
791
|
setCode,
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
792
|
+
addTask,
|
|
793
|
+
removeTask,
|
|
794
|
+
setTaskText,
|
|
795
|
+
setTaskDone,
|
|
805
796
|
checkLabel,
|
|
806
797
|
feedbackKey,
|
|
807
798
|
applyImportedNotes,
|
package/lib/select.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const ansi = require('./ansi.js');
|
|
4
4
|
|
|
5
5
|
const { ESC, RESET, THEME, seq, stripAnsi, graphemes, graphemeWidth } = ansi;
|
|
6
|
+
const { visibleWidth } = ansi;
|
|
6
7
|
|
|
7
8
|
const CSI_FINAL = /[@-~]/;
|
|
8
9
|
|
|
@@ -177,6 +178,56 @@ const overlayRange = (line, fromCol, toCol) => {
|
|
|
177
178
|
return out;
|
|
178
179
|
};
|
|
179
180
|
|
|
181
|
+
const selectionOf = (source) => {
|
|
182
|
+
if (!source || typeof source.anchor !== 'number') return null;
|
|
183
|
+
const cursor = source.cursor ?? 0;
|
|
184
|
+
if (source.anchor === cursor) return null;
|
|
185
|
+
return { anchor: source.anchor, cursor };
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const editorSelection = (view) => {
|
|
189
|
+
if (!view) return null;
|
|
190
|
+
if (view.compose) return selectionOf(view.compose);
|
|
191
|
+
return selectionOf(view.codeOverlay);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const spanCols = (hit, lo, hi) => {
|
|
195
|
+
const start = hit.start ?? 0;
|
|
196
|
+
const from = Math.max(lo, start);
|
|
197
|
+
const to = Math.min(hi, hit.limit ?? start);
|
|
198
|
+
if (to <= from) return null;
|
|
199
|
+
const text = hit.text ?? '';
|
|
200
|
+
const head = Math.max(0, from - start);
|
|
201
|
+
const tail = Math.max(0, to - start);
|
|
202
|
+
const c0 = visibleWidth(text.slice(0, head));
|
|
203
|
+
const c1 = visibleWidth(text.slice(0, tail));
|
|
204
|
+
const scroll = hit.scroll ?? 0;
|
|
205
|
+
const room = Math.max(0, (hit.x1 ?? hit.textX ?? 1) - (hit.textX ?? 1));
|
|
206
|
+
const vis0 = Math.max(c0, scroll);
|
|
207
|
+
const vis1 = Math.min(c1, scroll + room);
|
|
208
|
+
if (vis1 <= vis0) return null;
|
|
209
|
+
const origin = (hit.textX ?? 1) - 1;
|
|
210
|
+
return { from: origin + vis0 - scroll, to: origin + vis1 - scroll };
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const markEditorSelection = (rows, view, hits, color) => {
|
|
214
|
+
if (!color || !hits) return rows;
|
|
215
|
+
const sel = editorSelection(view);
|
|
216
|
+
if (!sel) return rows;
|
|
217
|
+
const lo = Math.min(sel.anchor, sel.cursor);
|
|
218
|
+
const hi = Math.max(sel.anchor, sel.cursor);
|
|
219
|
+
const next = rows.slice();
|
|
220
|
+
for (const hit of hits) {
|
|
221
|
+
if (!hit.live || !hit.y) continue;
|
|
222
|
+
const range = spanCols(hit, lo, hi);
|
|
223
|
+
if (!range) continue;
|
|
224
|
+
const i = hit.y - 1;
|
|
225
|
+
if (i < 0 || i >= next.length) continue;
|
|
226
|
+
next[i] = overlayRange(next[i], range.from, range.to);
|
|
227
|
+
}
|
|
228
|
+
return next;
|
|
229
|
+
};
|
|
230
|
+
|
|
180
231
|
const overlayRows = (rows, sel) => {
|
|
181
232
|
if (!isRange(sel)) return rows;
|
|
182
233
|
const { length } = rows;
|
|
@@ -193,4 +244,5 @@ module.exports = {
|
|
|
193
244
|
extractText,
|
|
194
245
|
overlayRange,
|
|
195
246
|
overlayRows,
|
|
247
|
+
markEditorSelection,
|
|
196
248
|
};
|