reslop 0.1.4 → 0.1.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 +10 -1
- package/README.md +21 -20
- package/lib/diff.js +71 -2
- package/lib/git.js +26 -0
- package/lib/keys.js +27 -6
- package/lib/render.js +145 -53
- package/lib/review.js +145 -3
- package/lib/select.js +7 -12
- package/lib/session.js +236 -22
- package/package.json +1 -1
package/lib/review.js
CHANGED
|
@@ -16,6 +16,8 @@ const FEED_BLOCK_ONLY = /^block (\d+)$/;
|
|
|
16
16
|
const FEED_META = /^<!-- reslop:(.*) -->\s*$/;
|
|
17
17
|
const FEED_SUFFIX = /^(.*) - (.+):(\d+):(\d+):(\d+)$/;
|
|
18
18
|
const CHECK_ITEM = /^- \[([ xX])\] (.*)$/;
|
|
19
|
+
const CODE_HEAD = /^- \[([ xX])\] code `([^`]+)`\s*$/;
|
|
20
|
+
const FENCE_LINE = /^(`{3,})\s*$/;
|
|
19
21
|
const FILE_QUOTE = /^> (.+)$/;
|
|
20
22
|
const STATUS_LINE = /^status:\s*(\S+)\s*$/;
|
|
21
23
|
const GIT_ORIGINS = ['unstaged', 'staged', 'untracked', 'commit', 'pr'];
|
|
@@ -28,6 +30,7 @@ const AGENT_INSTRUCTIONS = [
|
|
|
28
30
|
'Execute reviews with `status: ready` or `status: partial`.',
|
|
29
31
|
'Do not start `editing` or `done` review files.',
|
|
30
32
|
'Work through unchecked items. After finishing one, mark it `[x]`.',
|
|
33
|
+
'For `code` items, replace added lines at that location with the fence.',
|
|
31
34
|
'Set `status` to `partial` if some remain, or `done` if all are `[x]`.',
|
|
32
35
|
'Do not redo items already marked `[x]`.',
|
|
33
36
|
].join('\n');
|
|
@@ -169,13 +172,14 @@ const createStore = (reviewPath, templates = []) => ({
|
|
|
169
172
|
dirty: false,
|
|
170
173
|
nextTodoId: 1,
|
|
171
174
|
feedback: new Map(),
|
|
175
|
+
code: new Map(),
|
|
172
176
|
todos: [],
|
|
173
177
|
templates: [...templates],
|
|
174
178
|
status: 'editing',
|
|
175
179
|
});
|
|
176
180
|
|
|
177
181
|
const noteCounts = (store) => {
|
|
178
|
-
const counts = { feedback: 0, todo: 0 };
|
|
182
|
+
const counts = { feedback: 0, todo: 0, code: 0 };
|
|
179
183
|
if (!store) return counts;
|
|
180
184
|
for (const todo of store.todos) {
|
|
181
185
|
if (todo.text.trim()) counts.todo += 1;
|
|
@@ -183,12 +187,13 @@ const noteCounts = (store) => {
|
|
|
183
187
|
for (const note of store.feedback.values()) {
|
|
184
188
|
if (note.text.trim()) counts.feedback += 1;
|
|
185
189
|
}
|
|
190
|
+
counts.code = store.code.size;
|
|
186
191
|
return counts;
|
|
187
192
|
};
|
|
188
193
|
|
|
189
194
|
const hasNotes = (store) => {
|
|
190
195
|
const counts = noteCounts(store);
|
|
191
|
-
return counts.feedback > 0 || counts.todo > 0;
|
|
196
|
+
return counts.feedback > 0 || counts.todo > 0 || counts.code > 0;
|
|
192
197
|
};
|
|
193
198
|
|
|
194
199
|
const rememberTemplate = (store, prevText, nextText) => {
|
|
@@ -224,6 +229,29 @@ const setFeedback = (store, key, note) => {
|
|
|
224
229
|
store.dirty = true;
|
|
225
230
|
};
|
|
226
231
|
|
|
232
|
+
const setCode = (store, key, note) => {
|
|
233
|
+
const prev = store.code.get(key);
|
|
234
|
+
if (note.clear) {
|
|
235
|
+
if (!prev) return;
|
|
236
|
+
store.code.delete(key);
|
|
237
|
+
store.dirty = true;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const text = note.text ?? '';
|
|
241
|
+
if (prev && prev.text === text) return;
|
|
242
|
+
store.code.set(key, {
|
|
243
|
+
file: note.file,
|
|
244
|
+
oldStart: note.oldStart ?? 0,
|
|
245
|
+
newStart: note.newStart ?? 0,
|
|
246
|
+
blockId: note.blockId ?? 0,
|
|
247
|
+
origin: note.origin ?? '',
|
|
248
|
+
header: note.header ?? '',
|
|
249
|
+
text,
|
|
250
|
+
done: note.done ?? prev?.done ?? false,
|
|
251
|
+
});
|
|
252
|
+
store.dirty = true;
|
|
253
|
+
};
|
|
254
|
+
|
|
227
255
|
const addTodo = (store, file, text = '') => {
|
|
228
256
|
const id = store.nextTodoId;
|
|
229
257
|
store.nextTodoId += 1;
|
|
@@ -330,6 +358,20 @@ const storeFeedback = (store, note, key) => {
|
|
|
330
358
|
store.feedback.set(key || feedbackKey(next), next);
|
|
331
359
|
};
|
|
332
360
|
|
|
361
|
+
const storeCode = (store, note, key) => {
|
|
362
|
+
const next = {
|
|
363
|
+
file: note.file,
|
|
364
|
+
oldStart: note.oldStart ?? 0,
|
|
365
|
+
newStart: note.newStart ?? 0,
|
|
366
|
+
blockId: note.blockId ?? 0,
|
|
367
|
+
origin: note.origin ?? '',
|
|
368
|
+
header: note.header ?? '',
|
|
369
|
+
text: note.text,
|
|
370
|
+
done: note.done ?? false,
|
|
371
|
+
};
|
|
372
|
+
store.code.set(key || feedbackKey(next), next);
|
|
373
|
+
};
|
|
374
|
+
|
|
333
375
|
const feedbackLine = (note) => {
|
|
334
376
|
const loc = feedbackKey(note);
|
|
335
377
|
return `${checkboxLine(note.text, note.done)} - ${loc}`;
|
|
@@ -368,7 +410,7 @@ const parseFeedSuffix = (textItem) => {
|
|
|
368
410
|
const fileGroup = (groups, file) => {
|
|
369
411
|
let group = groups.get(file);
|
|
370
412
|
if (!group) {
|
|
371
|
-
group = { todos: [], feedback: [] };
|
|
413
|
+
group = { todos: [], feedback: [], code: [] };
|
|
372
414
|
groups.set(file, group);
|
|
373
415
|
}
|
|
374
416
|
return group;
|
|
@@ -385,6 +427,10 @@ const groupReviewNotes = (store) => {
|
|
|
385
427
|
if (!note.text.trim()) continue;
|
|
386
428
|
fileGroup(groups, note.file).feedback.push({ key, note });
|
|
387
429
|
}
|
|
430
|
+
for (const key of store.code.keys()) {
|
|
431
|
+
const note = store.code.get(key);
|
|
432
|
+
fileGroup(groups, note.file).code.push({ key, note });
|
|
433
|
+
}
|
|
388
434
|
return groups;
|
|
389
435
|
};
|
|
390
436
|
|
|
@@ -405,6 +451,26 @@ const reviewHeader = (store) => {
|
|
|
405
451
|
];
|
|
406
452
|
};
|
|
407
453
|
|
|
454
|
+
const fenceFor = (text) => {
|
|
455
|
+
let n = 3;
|
|
456
|
+
for (const line of text.split('\n')) {
|
|
457
|
+
const match = /^(`{3,})/.exec(line);
|
|
458
|
+
if (!match) continue;
|
|
459
|
+
if (match[1].length >= n) n = match[1].length + 1;
|
|
460
|
+
}
|
|
461
|
+
return '`'.repeat(n);
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const emitCodeItem = (lines, note) => {
|
|
465
|
+
const loc = feedbackKey(note);
|
|
466
|
+
const mark = note.done ? 'x' : ' ';
|
|
467
|
+
const fence = fenceFor(note.text);
|
|
468
|
+
lines.push(`- [${mark}] code \`${loc}\``);
|
|
469
|
+
lines.push(fence);
|
|
470
|
+
if (note.text !== '') lines.push(note.text);
|
|
471
|
+
lines.push(fence);
|
|
472
|
+
};
|
|
473
|
+
|
|
408
474
|
const emitReviewGroups = (lines, groups) => {
|
|
409
475
|
for (const file of groups.keys()) {
|
|
410
476
|
const group = groups.get(file);
|
|
@@ -416,6 +482,9 @@ const emitReviewGroups = (lines, groups) => {
|
|
|
416
482
|
for (const item of group.feedback) {
|
|
417
483
|
lines.push(feedbackLine(item.note));
|
|
418
484
|
}
|
|
485
|
+
for (const item of group.code) {
|
|
486
|
+
emitCodeItem(lines, item.note);
|
|
487
|
+
}
|
|
419
488
|
lines.push('');
|
|
420
489
|
}
|
|
421
490
|
};
|
|
@@ -543,18 +612,90 @@ const applyReviewLine = (store, file, section, pending, line) => {
|
|
|
543
612
|
return { file, section, pending };
|
|
544
613
|
};
|
|
545
614
|
|
|
615
|
+
const startCodePending = (line) => {
|
|
616
|
+
const match = CODE_HEAD.exec(line);
|
|
617
|
+
if (!match) return null;
|
|
618
|
+
const loc = parseLoc(match[2]);
|
|
619
|
+
if (!loc) return null;
|
|
620
|
+
return {
|
|
621
|
+
done: isDoneMark(match[1]),
|
|
622
|
+
origin: loc.origin,
|
|
623
|
+
file: loc.file,
|
|
624
|
+
oldStart: loc.oldStart,
|
|
625
|
+
newStart: loc.newStart,
|
|
626
|
+
blockId: loc.blockId,
|
|
627
|
+
open: '',
|
|
628
|
+
lines: [],
|
|
629
|
+
};
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
const storeCodeNote = (store, pending, text) => {
|
|
633
|
+
storeCode(store, {
|
|
634
|
+
file: pending.file,
|
|
635
|
+
oldStart: pending.oldStart,
|
|
636
|
+
newStart: pending.newStart,
|
|
637
|
+
blockId: pending.blockId,
|
|
638
|
+
origin: pending.origin,
|
|
639
|
+
header: '',
|
|
640
|
+
text,
|
|
641
|
+
done: pending.done,
|
|
642
|
+
});
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
const flushPendingCode = (store, pending) => {
|
|
646
|
+
if (!pending) return;
|
|
647
|
+
const text = pending.open ? pending.lines.join('\n') : '';
|
|
648
|
+
storeCodeNote(store, pending, text);
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
const takeCodeLine = (store, pending, line) => {
|
|
652
|
+
const fence = FENCE_LINE.exec(line);
|
|
653
|
+
if (!pending.open) {
|
|
654
|
+
if (fence) {
|
|
655
|
+
pending.open = fence[1];
|
|
656
|
+
return pending;
|
|
657
|
+
}
|
|
658
|
+
if (!line.trim()) return pending;
|
|
659
|
+
storeCodeNote(store, pending, '');
|
|
660
|
+
return 'fallthrough';
|
|
661
|
+
}
|
|
662
|
+
if (fence && fence[1].length >= pending.open.length) {
|
|
663
|
+
storeCodeNote(store, pending, pending.lines.join('\n'));
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
pending.lines.push(line);
|
|
667
|
+
return pending;
|
|
668
|
+
};
|
|
669
|
+
|
|
546
670
|
const parseReview = (text, reviewPath, templates = []) => {
|
|
547
671
|
const store = createStore(reviewPath, templates);
|
|
548
672
|
store.status = parseFrontmatterStatus(text) || 'editing';
|
|
549
673
|
let file = '';
|
|
550
674
|
let section = '';
|
|
551
675
|
let pending = null;
|
|
676
|
+
let pendingCode = null;
|
|
552
677
|
for (const line of text.split('\n')) {
|
|
678
|
+
if (pendingCode) {
|
|
679
|
+
const nextCode = takeCodeLine(store, pendingCode, line);
|
|
680
|
+
if (nextCode === 'fallthrough') {
|
|
681
|
+
pendingCode = null;
|
|
682
|
+
} else {
|
|
683
|
+
pendingCode = nextCode;
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const started = startCodePending(line);
|
|
688
|
+
if (started) {
|
|
689
|
+
pending = flushPendingFeedback(store, pending);
|
|
690
|
+
pendingCode = started;
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
553
693
|
const next = applyReviewLine(store, file, section, pending, line);
|
|
554
694
|
file = next.file;
|
|
555
695
|
section = next.section;
|
|
556
696
|
pending = next.pending;
|
|
557
697
|
}
|
|
698
|
+
flushPendingCode(store, pendingCode);
|
|
558
699
|
flushPendingFeedback(store, pending);
|
|
559
700
|
store.dirty = false;
|
|
560
701
|
return store;
|
|
@@ -679,6 +820,7 @@ module.exports = {
|
|
|
679
820
|
hasNotes,
|
|
680
821
|
rememberTemplate,
|
|
681
822
|
setFeedback,
|
|
823
|
+
setCode,
|
|
682
824
|
addTodo,
|
|
683
825
|
removeTodo,
|
|
684
826
|
setTodoText,
|
package/lib/select.js
CHANGED
|
@@ -7,12 +7,6 @@ const { fg: fgSeq, bg: bgSeq, seq } = ansi;
|
|
|
7
7
|
const { stripAnsi, graphemes, graphemeWidth } = ansi;
|
|
8
8
|
|
|
9
9
|
const CSI_FINAL = /[@-~]/;
|
|
10
|
-
const SGR_RESET = 0;
|
|
11
|
-
const SGR_FG = 38;
|
|
12
|
-
const SGR_BG = 48;
|
|
13
|
-
const SGR_DEFAULT_FG = 39;
|
|
14
|
-
const SGR_DEFAULT_BG = 49;
|
|
15
|
-
const SGR_TRUECOLOR = 2;
|
|
16
10
|
|
|
17
11
|
const isRange = (sel) => {
|
|
18
12
|
if (!sel || !sel.start || !sel.end) return false;
|
|
@@ -81,7 +75,8 @@ const parseParams = (body) => {
|
|
|
81
75
|
};
|
|
82
76
|
|
|
83
77
|
const applyRgb = (state, field, params, i) => {
|
|
84
|
-
const
|
|
78
|
+
const TRUECOLOR = 2;
|
|
79
|
+
const isTruecolor = params[i + 1] === TRUECOLOR;
|
|
85
80
|
const hasRgb = isTruecolor && i + 4 < params.length;
|
|
86
81
|
if (!hasRgb) return 1;
|
|
87
82
|
state[field] = [params[i + 2], params[i + 3], params[i + 4]];
|
|
@@ -89,18 +84,18 @@ const applyRgb = (state, field, params, i) => {
|
|
|
89
84
|
};
|
|
90
85
|
|
|
91
86
|
const SGR = {
|
|
92
|
-
|
|
87
|
+
0: (state) => {
|
|
93
88
|
state.fg = null;
|
|
94
89
|
state.bg = null;
|
|
95
90
|
return 1;
|
|
96
91
|
},
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
92
|
+
38: (state, params, i) => applyRgb(state, 'fg', params, i),
|
|
93
|
+
48: (state, params, i) => applyRgb(state, 'bg', params, i),
|
|
94
|
+
39: (state) => {
|
|
100
95
|
state.fg = null;
|
|
101
96
|
return 1;
|
|
102
97
|
},
|
|
103
|
-
|
|
98
|
+
49: (state) => {
|
|
104
99
|
state.bg = null;
|
|
105
100
|
return 1;
|
|
106
101
|
},
|