reslop 0.1.0 → 0.1.2
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 +25 -9
- package/README.md +25 -40
- package/bin/reslop.js +2 -2
- package/lib/ansi.js +0 -1
- package/lib/api.js +5 -0
- package/lib/cli.js +84 -79
- package/lib/clipboard.js +1 -1
- package/lib/deps.js +1440 -0
- package/lib/detect.js +4 -4
- package/lib/diff.js +40 -63
- package/lib/editor.js +1 -1
- package/lib/files.js +23 -4
- package/lib/git.js +367 -18
- package/lib/github.js +625 -0
- package/lib/highlight.js +149 -166
- package/lib/keys.js +34 -41
- package/lib/render.js +218 -143
- package/lib/review.js +52 -45
- package/lib/select.js +21 -31
- package/lib/session.js +298 -235
- package/lib/source.js +30 -0
- package/package.json +37 -18
package/lib/review.js
CHANGED
|
@@ -3,17 +3,13 @@
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
|
|
6
|
-
const { itemPath } = require('./files.js');
|
|
6
|
+
const { itemPath, REVIEW_DIR } = require('./files.js');
|
|
7
7
|
|
|
8
|
-
const REVIEW_DIR = '.review';
|
|
9
8
|
const TEMPLATES_FILE = 'templates.json';
|
|
10
9
|
const TEMPLATE_SHOW = 5;
|
|
11
10
|
const AUTOSAVE_MS = 3000;
|
|
12
11
|
const REVIEW_NAME = /^(\d{4}-\d{2}-\d{2})-(\d+)\.md$/;
|
|
13
12
|
const REVIEW_STATUSES = ['editing', 'ready', 'partial', 'done'];
|
|
14
|
-
const STATUS_ALIAS = Object.assign(Object.create(null), {
|
|
15
|
-
pending: 'ready',
|
|
16
|
-
});
|
|
17
13
|
const FEED_HEAD = /^### Feedback `([^`]+)` \+(\d+) \((.*)\)\s*$/;
|
|
18
14
|
const FEED_BLOCK = /^(.*), block (\d+)$/;
|
|
19
15
|
const FEED_BLOCK_ONLY = /^block (\d+)$/;
|
|
@@ -22,11 +18,11 @@ const FEED_SUFFIX = /^(.*) - (.+):(\d+):(\d+):(\d+)$/;
|
|
|
22
18
|
const CHECK_ITEM = /^- \[([ xX])\] (.*)$/;
|
|
23
19
|
const FILE_QUOTE = /^> (.+)$/;
|
|
24
20
|
const STATUS_LINE = /^status:\s*(\S+)\s*$/;
|
|
25
|
-
const GIT_ORIGINS = ['unstaged', 'staged', 'untracked', 'commit'];
|
|
26
|
-
const SECTION_LINE =
|
|
21
|
+
const GIT_ORIGINS = ['unstaged', 'staged', 'untracked', 'commit', 'pr'];
|
|
22
|
+
const SECTION_LINE = {
|
|
27
23
|
'### Todo': 'todo',
|
|
28
24
|
'### Feedback': 'feedback',
|
|
29
|
-
}
|
|
25
|
+
};
|
|
30
26
|
|
|
31
27
|
const AGENT_INSTRUCTIONS = [
|
|
32
28
|
'Execute reviews with `status: ready` or `status: partial`.',
|
|
@@ -94,7 +90,7 @@ const latestReviewName = (names) => {
|
|
|
94
90
|
|
|
95
91
|
const canonicalStatus = (raw) => {
|
|
96
92
|
if (!raw) return '';
|
|
97
|
-
const name =
|
|
93
|
+
const name = raw === 'pending' ? 'ready' : raw;
|
|
98
94
|
if (REVIEW_STATUSES.includes(name)) return name;
|
|
99
95
|
return '';
|
|
100
96
|
};
|
|
@@ -116,7 +112,8 @@ const parseFrontmatterStatus = (text) => {
|
|
|
116
112
|
|
|
117
113
|
const readReviewStatus = (reviewPath, readFileSync = fs.readFileSync) => {
|
|
118
114
|
try {
|
|
119
|
-
|
|
115
|
+
const text = readFileSync(reviewPath, 'utf8');
|
|
116
|
+
return parseFrontmatterStatus(text);
|
|
120
117
|
} catch {
|
|
121
118
|
return '';
|
|
122
119
|
}
|
|
@@ -133,10 +130,9 @@ const resolveReviewPath = (dir, date, existingNames, options = {}) => {
|
|
|
133
130
|
if (status === 'editing') return { reviewPath, resume: true };
|
|
134
131
|
}
|
|
135
132
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
};
|
|
133
|
+
const reviewPath = allocateReviewPath(dir, date, existingNames);
|
|
134
|
+
const resume = false;
|
|
135
|
+
return { reviewPath, resume };
|
|
140
136
|
};
|
|
141
137
|
|
|
142
138
|
const rankedTemplates = (templates) => {
|
|
@@ -294,6 +290,32 @@ const feedbackKey = (note) => {
|
|
|
294
290
|
return `${file}:${oldStart}:${newStart}:${block}`;
|
|
295
291
|
};
|
|
296
292
|
|
|
293
|
+
const applyImportedNotes = (store, imported) => {
|
|
294
|
+
if (!store || !imported) return;
|
|
295
|
+
const feedbacks = imported.feedback ?? [];
|
|
296
|
+
for (const note of feedbacks) {
|
|
297
|
+
const key = feedbackKey(note);
|
|
298
|
+
const prev = store.feedback.get(key);
|
|
299
|
+
const text = prev ? `${prev.text}\n\n${note.text}` : note.text;
|
|
300
|
+
const done = prev ? prev.done && !!note.done : !!note.done;
|
|
301
|
+
setFeedback(store, key, {
|
|
302
|
+
file: note.file,
|
|
303
|
+
oldStart: note.oldStart ?? 0,
|
|
304
|
+
newStart: note.newStart ?? 0,
|
|
305
|
+
blockId: note.blockId ?? 0,
|
|
306
|
+
origin: note.origin ?? '',
|
|
307
|
+
header: note.header ?? '',
|
|
308
|
+
text,
|
|
309
|
+
done,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
const todos = imported.todos ?? [];
|
|
313
|
+
for (const todo of todos) {
|
|
314
|
+
const added = addTodo(store, todo.file || '', todo.text ?? '');
|
|
315
|
+
if (todo.done) added.done = true;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
297
319
|
const storeFeedback = (store, note, key) => {
|
|
298
320
|
const next = {
|
|
299
321
|
file: note.file,
|
|
@@ -328,25 +350,19 @@ const parseLoc = (raw) => {
|
|
|
328
350
|
fileParts = fileParts.slice(1);
|
|
329
351
|
}
|
|
330
352
|
if (!fileParts.length) return null;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
file: fileParts.join(':'),
|
|
334
|
-
oldStart,
|
|
335
|
-
newStart,
|
|
336
|
-
blockId,
|
|
337
|
-
};
|
|
353
|
+
const file = fileParts.join(':');
|
|
354
|
+
return { origin, file, oldStart, newStart, blockId };
|
|
338
355
|
};
|
|
339
356
|
|
|
340
357
|
const parseFeedSuffix = (textItem) => {
|
|
341
358
|
const match = FEED_SUFFIX.exec(textItem);
|
|
342
359
|
if (!match) return null;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
};
|
|
360
|
+
const text = match[1];
|
|
361
|
+
const file = match[2];
|
|
362
|
+
const oldStart = toInt(match[3]);
|
|
363
|
+
const newStart = toInt(match[4]);
|
|
364
|
+
const blockId = toInt(match[5]);
|
|
365
|
+
return { text, file, oldStart, newStart, blockId };
|
|
350
366
|
};
|
|
351
367
|
|
|
352
368
|
const fileGroup = (groups, file) => {
|
|
@@ -364,7 +380,8 @@ const groupReviewNotes = (store) => {
|
|
|
364
380
|
if (!todo.text.trim()) continue;
|
|
365
381
|
fileGroup(groups, todo.file).todos.push(todo);
|
|
366
382
|
}
|
|
367
|
-
for (const
|
|
383
|
+
for (const key of store.feedback.keys()) {
|
|
384
|
+
const note = store.feedback.get(key);
|
|
368
385
|
if (!note.text.trim()) continue;
|
|
369
386
|
fileGroup(groups, note.file).feedback.push({ key, note });
|
|
370
387
|
}
|
|
@@ -389,7 +406,8 @@ const reviewHeader = (store) => {
|
|
|
389
406
|
};
|
|
390
407
|
|
|
391
408
|
const emitReviewGroups = (lines, groups) => {
|
|
392
|
-
for (const
|
|
409
|
+
for (const file of groups.keys()) {
|
|
410
|
+
const group = groups.get(file);
|
|
393
411
|
lines.push(`## ${file}`);
|
|
394
412
|
lines.push('');
|
|
395
413
|
for (const todo of group.todos) {
|
|
@@ -422,12 +440,9 @@ const parseFeedbackHeading = (line) => {
|
|
|
422
440
|
if (!withHead) {
|
|
423
441
|
return { file, newStart, header: rest, blockId: 0 };
|
|
424
442
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
header: withHead[1],
|
|
429
|
-
blockId: toInt(withHead[2]),
|
|
430
|
-
};
|
|
443
|
+
const header = withHead[1];
|
|
444
|
+
const blockId = toInt(withHead[2]);
|
|
445
|
+
return { file, newStart, header, blockId };
|
|
431
446
|
};
|
|
432
447
|
|
|
433
448
|
const flushPendingFeedback = (store, pending) => {
|
|
@@ -649,19 +664,12 @@ const mergeTodos = (gitItems, todos, extraFiles = []) => {
|
|
|
649
664
|
};
|
|
650
665
|
|
|
651
666
|
module.exports = {
|
|
652
|
-
REVIEW_DIR,
|
|
653
|
-
TEMPLATES_FILE,
|
|
654
667
|
TEMPLATE_SHOW,
|
|
655
668
|
AUTOSAVE_MS,
|
|
656
|
-
REVIEW_STATUSES,
|
|
657
|
-
AGENT_INSTRUCTIONS,
|
|
658
|
-
dateStamp,
|
|
659
669
|
allocateReviewPath,
|
|
660
670
|
listReviewNames,
|
|
661
|
-
compareReviewNames,
|
|
662
671
|
latestReviewName,
|
|
663
672
|
parseFrontmatterStatus,
|
|
664
|
-
readReviewStatus,
|
|
665
673
|
resolveReviewPath,
|
|
666
674
|
rankedTemplates,
|
|
667
675
|
prefixTemplates,
|
|
@@ -674,14 +682,13 @@ module.exports = {
|
|
|
674
682
|
addTodo,
|
|
675
683
|
removeTodo,
|
|
676
684
|
setTodoText,
|
|
677
|
-
isDoneMark,
|
|
678
685
|
checkLabel,
|
|
679
686
|
feedbackKey,
|
|
687
|
+
applyImportedNotes,
|
|
680
688
|
serializeReview,
|
|
681
689
|
parseReview,
|
|
682
690
|
loadReview,
|
|
683
691
|
loadTemplates,
|
|
684
692
|
flushReview,
|
|
685
|
-
makeTodoItem,
|
|
686
693
|
mergeTodos,
|
|
687
694
|
};
|
package/lib/select.js
CHANGED
|
@@ -2,17 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const ansi = require('./ansi.js');
|
|
4
4
|
|
|
5
|
-
const {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
THEME,
|
|
9
|
-
fg: fgSeq,
|
|
10
|
-
bg: bgSeq,
|
|
11
|
-
seq,
|
|
12
|
-
stripAnsi,
|
|
13
|
-
graphemes,
|
|
14
|
-
graphemeWidth,
|
|
15
|
-
} = ansi;
|
|
5
|
+
const { ESC, RESET, THEME } = ansi;
|
|
6
|
+
const { fg: fgSeq, bg: bgSeq, seq } = ansi;
|
|
7
|
+
const { stripAnsi, graphemes, graphemeWidth } = ansi;
|
|
16
8
|
|
|
17
9
|
const CSI_FINAL = /[@-~]/;
|
|
18
10
|
const SGR_RESET = 0;
|
|
@@ -28,12 +20,12 @@ const isRange = (sel) => {
|
|
|
28
20
|
};
|
|
29
21
|
|
|
30
22
|
const ordered = (sel) => {
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
if (
|
|
35
|
-
if (
|
|
36
|
-
return
|
|
23
|
+
const start = sel.start;
|
|
24
|
+
const end = sel.end;
|
|
25
|
+
if (start.y < end.y) return { first: start, last: end };
|
|
26
|
+
if (start.y > end.y) return { first: end, last: start };
|
|
27
|
+
if (start.x <= end.x) return { first: start, last: end };
|
|
28
|
+
return { first: end, last: start };
|
|
37
29
|
};
|
|
38
30
|
|
|
39
31
|
const slicePlain = (plain, fromCol, toCol) => {
|
|
@@ -51,25 +43,25 @@ const slicePlain = (plain, fromCol, toCol) => {
|
|
|
51
43
|
};
|
|
52
44
|
|
|
53
45
|
const lineRange = (rowIndex, sel) => {
|
|
54
|
-
const
|
|
46
|
+
const { first, last } = ordered(sel);
|
|
55
47
|
const y = rowIndex + 1;
|
|
56
|
-
if (y <
|
|
48
|
+
if (y < first.y || y > last.y) return null;
|
|
57
49
|
let from = 0;
|
|
58
|
-
let to =
|
|
59
|
-
if (y ===
|
|
60
|
-
if (y ===
|
|
50
|
+
let to = Infinity;
|
|
51
|
+
if (y === first.y) from = first.x - 1;
|
|
52
|
+
if (y === last.y) to = last.x;
|
|
61
53
|
if (to <= from) return null;
|
|
62
54
|
return { from, to };
|
|
63
55
|
};
|
|
64
56
|
|
|
65
57
|
const extractText = (rows, sel) => {
|
|
66
58
|
if (!isRange(sel) || !rows || !rows.length) return '';
|
|
67
|
-
const
|
|
59
|
+
const { first, last } = ordered(sel);
|
|
68
60
|
const lines = [];
|
|
69
|
-
for (let
|
|
70
|
-
const range = lineRange(
|
|
61
|
+
for (let row = first.y - 1; row <= last.y - 1; row++) {
|
|
62
|
+
const range = lineRange(row, sel);
|
|
71
63
|
if (!range) continue;
|
|
72
|
-
const plain = stripAnsi(rows[
|
|
64
|
+
const plain = stripAnsi(rows[row] ?? '');
|
|
73
65
|
const chunk = slicePlain(plain, range.from, range.to);
|
|
74
66
|
lines.push(chunk.replace(/\s+$/g, ''));
|
|
75
67
|
}
|
|
@@ -96,7 +88,7 @@ const applyRgb = (state, field, params, i) => {
|
|
|
96
88
|
return 5;
|
|
97
89
|
};
|
|
98
90
|
|
|
99
|
-
const SGR =
|
|
91
|
+
const SGR = {
|
|
100
92
|
[SGR_RESET]: (state) => {
|
|
101
93
|
state.fg = null;
|
|
102
94
|
state.bg = null;
|
|
@@ -112,7 +104,7 @@ const SGR = Object.assign(Object.create(null), {
|
|
|
112
104
|
state.bg = null;
|
|
113
105
|
return 1;
|
|
114
106
|
},
|
|
115
|
-
}
|
|
107
|
+
};
|
|
116
108
|
|
|
117
109
|
const applySgr = (params, state) => {
|
|
118
110
|
let i = 0;
|
|
@@ -210,9 +202,7 @@ const overlayRows = (rows, sel) => {
|
|
|
210
202
|
|
|
211
203
|
module.exports = {
|
|
212
204
|
isRange,
|
|
213
|
-
ordered,
|
|
214
205
|
extractText,
|
|
215
|
-
overlayRows,
|
|
216
206
|
overlayRange,
|
|
217
|
-
|
|
207
|
+
overlayRows,
|
|
218
208
|
};
|