reslop 0.1.1 → 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 +13 -6
- package/README.md +11 -9
- package/bin/reslop.js +2 -2
- package/lib/ansi.js +0 -1
- package/lib/cli.js +44 -43
- package/lib/clipboard.js +1 -1
- package/lib/deps.js +1440 -0
- package/lib/detect.js +4 -4
- package/lib/diff.js +31 -55
- package/lib/editor.js +1 -1
- package/lib/files.js +22 -3
- package/lib/git.js +360 -36
- package/lib/github.js +28 -34
- package/lib/highlight.js +126 -140
- package/lib/keys.js +29 -39
- package/lib/render.js +172 -129
- package/lib/review.js +22 -42
- package/lib/select.js +16 -18
- package/lib/session.js +97 -208
- package/lib/source.js +7 -3
- package/package.json +1 -1
package/lib/render.js
CHANGED
|
@@ -2,20 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
const ansi = require('./ansi.js');
|
|
4
4
|
const { displayLines } = require('./diff.js');
|
|
5
|
-
const
|
|
5
|
+
const highlight = require('./highlight.js');
|
|
6
6
|
const { detectLang } = require('./detect.js');
|
|
7
7
|
const keys = require('./keys.js');
|
|
8
|
-
const { ACTIONS, actionHot, actionLetter, buttonWord } = keys;
|
|
9
|
-
const { FILES_DISABLED } = keys;
|
|
10
8
|
const { overlayRows } = require('./select.js');
|
|
11
9
|
const { itemPath } = require('./files.js');
|
|
12
|
-
const { isDoneMark } = require('./review.js');
|
|
13
10
|
const wrap = require('./wrap.js');
|
|
14
|
-
const { wrapPlain, wrapDoc, wrapMultiline, cursorInWrap, wrapMove } = wrap;
|
|
15
11
|
|
|
16
|
-
const {
|
|
17
|
-
|
|
18
|
-
const {
|
|
12
|
+
const { tokenize, overlayTokens } = highlight;
|
|
13
|
+
const { ACTIONS, ACTION_IDS, actionLetter, buttonWord } = keys;
|
|
14
|
+
const { FILES_DISABLED } = keys;
|
|
15
|
+
const { wrapPlain, wrapMultiline, cursorInWrap } = wrap;
|
|
16
|
+
const { THEME, CODE_FG, codeFg, paint, visibleWidth } = ansi;
|
|
17
|
+
const { truncateVisible, fg, RESET, ESC, EL, seq } = ansi;
|
|
19
18
|
|
|
20
19
|
const STYLES = {
|
|
21
20
|
del: {
|
|
@@ -32,6 +31,27 @@ const STYLES = {
|
|
|
32
31
|
chBg: THEME.addCharBg,
|
|
33
32
|
mark: '+',
|
|
34
33
|
},
|
|
34
|
+
warn: {
|
|
35
|
+
baseFg: THEME.delLineFg,
|
|
36
|
+
baseBg: THEME.delLineBg,
|
|
37
|
+
chFg: THEME.delCharFg,
|
|
38
|
+
chBg: THEME.delCharBg,
|
|
39
|
+
mark: ' ',
|
|
40
|
+
},
|
|
41
|
+
note: {
|
|
42
|
+
baseFg: THEME.chromeFg,
|
|
43
|
+
baseBg: THEME.noteBg,
|
|
44
|
+
chFg: THEME.chromeFg,
|
|
45
|
+
chBg: THEME.noteBg,
|
|
46
|
+
mark: ' ',
|
|
47
|
+
},
|
|
48
|
+
noteSep: {
|
|
49
|
+
baseFg: THEME.mutedFg,
|
|
50
|
+
baseBg: THEME.chromeBg,
|
|
51
|
+
chFg: THEME.mutedFg,
|
|
52
|
+
chBg: THEME.chromeBg,
|
|
53
|
+
mark: ' ',
|
|
54
|
+
},
|
|
35
55
|
ctx: {
|
|
36
56
|
baseFg: THEME.ctxFg,
|
|
37
57
|
baseBg: THEME.ctxBg,
|
|
@@ -56,6 +76,15 @@ const STAGED_STYLES = {
|
|
|
56
76
|
chBg: THEME.stagedAddCharBg,
|
|
57
77
|
mark: '+',
|
|
58
78
|
},
|
|
79
|
+
warn: {
|
|
80
|
+
baseFg: THEME.stagedDelLineFg,
|
|
81
|
+
baseBg: THEME.stagedDelLineBg,
|
|
82
|
+
chFg: THEME.stagedDelCharFg,
|
|
83
|
+
chBg: THEME.stagedDelCharBg,
|
|
84
|
+
mark: ' ',
|
|
85
|
+
},
|
|
86
|
+
note: STYLES.note,
|
|
87
|
+
noteSep: STYLES.noteSep,
|
|
59
88
|
ctx: STYLES.ctx,
|
|
60
89
|
};
|
|
61
90
|
|
|
@@ -76,7 +105,6 @@ const FILE_STATUS_FG = {
|
|
|
76
105
|
|
|
77
106
|
const INFO_STATUS = [
|
|
78
107
|
'copied',
|
|
79
|
-
'skipped',
|
|
80
108
|
'staged',
|
|
81
109
|
'unstaged',
|
|
82
110
|
'not staged',
|
|
@@ -85,7 +113,6 @@ const INFO_STATUS = [
|
|
|
85
113
|
'read only',
|
|
86
114
|
'last block',
|
|
87
115
|
'first block',
|
|
88
|
-
'all skipped',
|
|
89
116
|
'first file',
|
|
90
117
|
'last file',
|
|
91
118
|
'nothing to review',
|
|
@@ -107,10 +134,14 @@ const BUTTON_GAP = ' ';
|
|
|
107
134
|
const LOGO = '👁️';
|
|
108
135
|
const FILE_MARK = '▶';
|
|
109
136
|
const FILE_LIST_PAD = 1;
|
|
137
|
+
const CHECK_DONE = 'xX';
|
|
110
138
|
const TODO_CAPTION = 'TODO:';
|
|
111
139
|
const INPUT_MAX = 4;
|
|
112
140
|
const NOTE_MAX = 4;
|
|
113
141
|
const NOTE_PAD = 1;
|
|
142
|
+
const SOFT_WRAP = ['warn', 'note'];
|
|
143
|
+
const DEL_CHAR_BG = ansi.bg(THEME.delCharBg);
|
|
144
|
+
const ADD_CHAR_BG = ansi.bg(THEME.addCharBg);
|
|
114
145
|
|
|
115
146
|
const lineStyles = (origin) => (origin === 'staged' ? STAGED_STYLES : STYLES);
|
|
116
147
|
|
|
@@ -137,7 +168,32 @@ const paintSpans = (pieces, lineStyle, budget, color) => {
|
|
|
137
168
|
return { text: out, used };
|
|
138
169
|
};
|
|
139
170
|
|
|
171
|
+
const paintAuditNote = (line, width, color) => {
|
|
172
|
+
const style = STYLES.note;
|
|
173
|
+
const gutterW = Math.min(GUTTER_W, Math.max(0, width));
|
|
174
|
+
const label = `${style.mark} `.slice(0, gutterW);
|
|
175
|
+
const gutter = paint(label, style.baseFg, style.baseBg, color);
|
|
176
|
+
const budget = Math.max(0, width - gutterW);
|
|
177
|
+
const text = truncateVisible(line.text ?? '', budget);
|
|
178
|
+
const used = visibleWidth(text);
|
|
179
|
+
let out = gutter;
|
|
180
|
+
if (text) out += paint(text, style.baseFg, style.baseBg, color);
|
|
181
|
+
if (used < budget) {
|
|
182
|
+
out += fill(budget - used, style.baseFg, style.baseBg, color);
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const paintAuditRule = (width, color) => {
|
|
188
|
+
const style = STYLES.noteSep;
|
|
189
|
+
const n = Math.max(0, width);
|
|
190
|
+
const bar = '─'.repeat(n);
|
|
191
|
+
return paint(bar, style.baseFg, style.baseBg, color);
|
|
192
|
+
};
|
|
193
|
+
|
|
140
194
|
const paintDiffLine = (line, width, color, origin, lang) => {
|
|
195
|
+
if (line.type === 'noteSep') return paintAuditRule(width, color);
|
|
196
|
+
if (line.type === 'note') return paintAuditNote(line, width, color);
|
|
141
197
|
const styles = lineStyles(origin);
|
|
142
198
|
const style = styles[line.type] ?? styles.ctx;
|
|
143
199
|
const diffSpans = line.spans ?? [{ text: line.text, changed: false }];
|
|
@@ -183,19 +239,14 @@ const paintSplitBlank = (width, color) => {
|
|
|
183
239
|
);
|
|
184
240
|
};
|
|
185
241
|
|
|
186
|
-
const
|
|
187
|
-
|
|
242
|
+
const paintBodyFill = (width, color, kind) => {
|
|
243
|
+
if (kind === 'files') {
|
|
188
244
|
const inner = Math.max(1, width - 1);
|
|
189
245
|
const main = fill(inner, THEME.ctxFg, THEME.ctxBg, color);
|
|
190
246
|
const edge = fill(1, THEME.ctxFg, THEME.ctxBg, color);
|
|
191
247
|
return main + edge;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
const paintBodyFill = (width, color, kind) => {
|
|
197
|
-
const paint = FILL_KIND[kind];
|
|
198
|
-
if (paint) return paint(width, color);
|
|
248
|
+
}
|
|
249
|
+
if (kind === 'split') return paintSplitBlank(width, color);
|
|
199
250
|
return fill(width, THEME.ctxFg, THEME.ctxBg, color);
|
|
200
251
|
};
|
|
201
252
|
|
|
@@ -215,22 +266,18 @@ const splitCheck = (text) => {
|
|
|
215
266
|
if (!match) return null;
|
|
216
267
|
const start = match.index;
|
|
217
268
|
const token = match[0];
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
after: text.slice(start + token.length),
|
|
223
|
-
};
|
|
269
|
+
const before = text.slice(0, start);
|
|
270
|
+
const mark = match[1];
|
|
271
|
+
const after = text.slice(start + token.length);
|
|
272
|
+
return { before, mark, token, after };
|
|
224
273
|
};
|
|
225
274
|
|
|
226
275
|
const paintCheckBar = (text, width, fgRgb, bgRgb, color) => {
|
|
227
276
|
const clipped = truncateVisible(text, width);
|
|
277
|
+
const parts = color ? splitCheck(clipped) : null;
|
|
278
|
+
if (!parts) return paintBar(text, width, fgRgb, bgRgb, color);
|
|
228
279
|
const pad = Math.max(0, width - visibleWidth(clipped));
|
|
229
|
-
const
|
|
230
|
-
if (!color) return filled;
|
|
231
|
-
const parts = splitCheck(clipped);
|
|
232
|
-
if (!parts) return `${seq(fgRgb, bgRgb)}${EL}${filled}${RESET}`;
|
|
233
|
-
const done = isDoneMark(parts.mark);
|
|
280
|
+
const done = CHECK_DONE.includes(parts.mark);
|
|
234
281
|
const chipFg = done ? THEME.checkDoneFg : THEME.checkFg;
|
|
235
282
|
const chipBg = done ? THEME.checkDoneBg : THEME.checkBg;
|
|
236
283
|
const after = `${parts.after}${' '.repeat(pad)}`;
|
|
@@ -241,10 +288,15 @@ const paintCheckBar = (text, width, fgRgb, bgRgb, color) => {
|
|
|
241
288
|
return out;
|
|
242
289
|
};
|
|
243
290
|
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
291
|
+
const ORIGIN_LABEL = {
|
|
292
|
+
commit: (view) => view.revShort || 'commit',
|
|
293
|
+
pr: (view) => view.sourceLabel || 'pr',
|
|
294
|
+
todo: () => 'todo',
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const originLabel = (origin, view) => {
|
|
298
|
+
const named = ORIGIN_LABEL[origin];
|
|
299
|
+
if (named) return named(view);
|
|
248
300
|
return origin ?? 'unknown';
|
|
249
301
|
};
|
|
250
302
|
|
|
@@ -281,7 +333,7 @@ const headerTarget = (view) => {
|
|
|
281
333
|
if (!view.item) return { path: '', origin: '', n: 0, total: view.total ?? 0 };
|
|
282
334
|
return {
|
|
283
335
|
path: itemPath(view.item),
|
|
284
|
-
origin: originLabel(view.item.origin, view
|
|
336
|
+
origin: originLabel(view.item.origin, view),
|
|
285
337
|
n: view.index + 1,
|
|
286
338
|
total: view.total,
|
|
287
339
|
};
|
|
@@ -295,17 +347,20 @@ const headerPrefix = (view) => {
|
|
|
295
347
|
return ` ${LOGO} reslop: `;
|
|
296
348
|
};
|
|
297
349
|
|
|
350
|
+
const headerSuffix = ({ origin, n, total }) =>
|
|
351
|
+
origin ? ` ${origin} ${n}/${total}` : ` ${n}/${total}`;
|
|
352
|
+
|
|
298
353
|
const headerText = (view) => {
|
|
299
354
|
const prefix = headerPrefix(view);
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
return `${prefix}${path}${suffix}`;
|
|
355
|
+
const target = headerTarget(view);
|
|
356
|
+
return `${prefix}${target.path}${headerSuffix(target)}`;
|
|
303
357
|
};
|
|
304
358
|
|
|
305
359
|
const headerPieces = (view, width) => {
|
|
306
360
|
const repo = view.repoName || '';
|
|
307
|
-
const
|
|
308
|
-
const
|
|
361
|
+
const target = headerTarget(view);
|
|
362
|
+
const path = target.path;
|
|
363
|
+
const suffix = headerSuffix(target);
|
|
309
364
|
const right = ' ';
|
|
310
365
|
const chrome = ` ${LOGO} reslop: `;
|
|
311
366
|
const prefixW = visibleWidth(headerPrefix(view));
|
|
@@ -336,7 +391,7 @@ const headerPieces = (view, width) => {
|
|
|
336
391
|
const paintHeader = (view, width, color) => {
|
|
337
392
|
const pieces = headerPieces(view, width);
|
|
338
393
|
if (!color) {
|
|
339
|
-
return pieces.map((
|
|
394
|
+
return pieces.map((piece) => piece.text).join('');
|
|
340
395
|
}
|
|
341
396
|
const bgRgb = THEME.headerBg;
|
|
342
397
|
let out = `${seq(THEME.headerChromeFg, bgRgb)}${EL}`;
|
|
@@ -355,7 +410,8 @@ const bodyLines = (item, layout) => {
|
|
|
355
410
|
return [{ type: 'ctx', text, spans: [{ text, changed: false }] }];
|
|
356
411
|
}
|
|
357
412
|
if (!item.hunk) return [];
|
|
358
|
-
|
|
413
|
+
const radius = item.dep ? Number.MAX_SAFE_INTEGER : undefined;
|
|
414
|
+
return displayLines(item.hunk, item.blockId, layout, radius);
|
|
359
415
|
};
|
|
360
416
|
|
|
361
417
|
const sourceEmpty = (line) => !line || !(line.text ?? '').trim();
|
|
@@ -373,26 +429,6 @@ const dropLeadingEmpty = (lines, split) => {
|
|
|
373
429
|
return i ? lines.slice(i) : lines;
|
|
374
430
|
};
|
|
375
431
|
|
|
376
|
-
const helpLines = () => {
|
|
377
|
-
const lines = [' keys and buttons', ''];
|
|
378
|
-
for (const action of ACTIONS) {
|
|
379
|
-
const keys = actionHot(action);
|
|
380
|
-
let label = action.label;
|
|
381
|
-
if (action.id === 'layout') {
|
|
382
|
-
label = 'Mode (unified / mixed / side)';
|
|
383
|
-
}
|
|
384
|
-
lines.push(` ${keys.padEnd(16)} ${label}`);
|
|
385
|
-
}
|
|
386
|
-
lines.push(' enter open file / save note');
|
|
387
|
-
lines.push(' esc files, then quit');
|
|
388
|
-
lines.push(' del, backspace remove todo');
|
|
389
|
-
lines.push(' j, k, wheel scroll / move');
|
|
390
|
-
lines.push(' drag copy');
|
|
391
|
-
lines.push(' ctrl-z, ctrl-y undo / redo');
|
|
392
|
-
lines.push(' ?, h help');
|
|
393
|
-
return lines;
|
|
394
|
-
};
|
|
395
|
-
|
|
396
432
|
const BUTTON_MARK = {
|
|
397
433
|
bgRgb: THEME.buttonBg,
|
|
398
434
|
fgRgb: THEME.buttonFg,
|
|
@@ -429,15 +465,16 @@ const layoutButtons = (width, compact = false, disabled = []) => {
|
|
|
429
465
|
const hits = [];
|
|
430
466
|
const parts = [];
|
|
431
467
|
let x = 0;
|
|
432
|
-
for (const
|
|
468
|
+
for (const id of ACTION_IDS) {
|
|
469
|
+
const action = { id, ...ACTIONS[id] };
|
|
433
470
|
const letter = actionLetter(action);
|
|
434
471
|
const word = buttonWord(action);
|
|
435
472
|
const label = short ? letter : word;
|
|
436
473
|
const piece = `${BUTTON_GAP}${label}`;
|
|
437
474
|
const w = visibleWidth(piece);
|
|
438
475
|
if (x + w > width && parts.length) break;
|
|
439
|
-
const isDisabled = disabled.includes(
|
|
440
|
-
if (!isDisabled) hits.push({ id
|
|
476
|
+
const isDisabled = disabled.includes(id);
|
|
477
|
+
if (!isDisabled) hits.push({ id, x0: x, x1: x + w });
|
|
441
478
|
parts.push({ action, piece, letter, label, disabled: isDisabled });
|
|
442
479
|
x += w;
|
|
443
480
|
}
|
|
@@ -445,7 +482,7 @@ const layoutButtons = (width, compact = false, disabled = []) => {
|
|
|
445
482
|
};
|
|
446
483
|
if (compact) return build(true);
|
|
447
484
|
const words = build(false);
|
|
448
|
-
if (words.parts.length ===
|
|
485
|
+
if (words.parts.length === ACTION_IDS.length) return words;
|
|
449
486
|
return build(true);
|
|
450
487
|
};
|
|
451
488
|
|
|
@@ -467,26 +504,38 @@ const paintButtons = (layout, width, color) => {
|
|
|
467
504
|
return out;
|
|
468
505
|
};
|
|
469
506
|
|
|
507
|
+
const COUNTS_KIND = {
|
|
508
|
+
pr: ({ counts, notes, sourceLabel }) => {
|
|
509
|
+
const n = counts.pr ?? 0;
|
|
510
|
+
const label = sourceLabel || 'pr';
|
|
511
|
+
return ` pr ${label} ${n} ${notes}`;
|
|
512
|
+
},
|
|
513
|
+
commit: ({ counts, notes, revShort }) => {
|
|
514
|
+
const n = counts.commit ?? 0;
|
|
515
|
+
return ` commit ${revShort} ${n} ${notes}`;
|
|
516
|
+
},
|
|
517
|
+
worktree: ({ counts, notes }) => {
|
|
518
|
+
const staged = counts.staged ?? 0;
|
|
519
|
+
const unstaged = counts.unstaged ?? 0;
|
|
520
|
+
const untracked = counts.untracked ?? 0;
|
|
521
|
+
const gitCounts = `unstaged ${unstaged} untracked ${untracked}`;
|
|
522
|
+
const git = `staged ${staged} ${gitCounts}`;
|
|
523
|
+
return ` ${git} ${notes}`;
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const countsKind = (sourceKind, revShort) => {
|
|
528
|
+
if (sourceKind === 'pr') return 'pr';
|
|
529
|
+
if (revShort) return 'commit';
|
|
530
|
+
return 'worktree';
|
|
531
|
+
};
|
|
532
|
+
|
|
470
533
|
const countsLine = (counts, revShort, sourceKind, sourceLabel) => {
|
|
471
|
-
const skipped = counts.skipped ?? 0;
|
|
472
534
|
const feedback = counts.feedback ?? 0;
|
|
473
535
|
const todo = counts.todo ?? 0;
|
|
474
536
|
const notes = `feedback ${feedback} todo ${todo}`;
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
const n = counts.pr ?? 0;
|
|
478
|
-
const label = sourceLabel || 'pr';
|
|
479
|
-
return ` pr ${label} ${n} ${notes}${extra}`;
|
|
480
|
-
}
|
|
481
|
-
if (revShort) {
|
|
482
|
-
const n = counts.commit ?? 0;
|
|
483
|
-
return ` commit ${revShort} ${n} ${notes}${extra}`;
|
|
484
|
-
}
|
|
485
|
-
const staged = counts.staged ?? 0;
|
|
486
|
-
const unstaged = counts.unstaged ?? 0;
|
|
487
|
-
const untracked = counts.untracked ?? 0;
|
|
488
|
-
const git = `staged ${staged} unstaged ${unstaged} untracked ${untracked}`;
|
|
489
|
-
return ` ${git} ${notes}${extra}`;
|
|
537
|
+
const kind = countsKind(sourceKind, revShort);
|
|
538
|
+
return COUNTS_KIND[kind]({ counts, notes, sourceLabel, revShort });
|
|
490
539
|
};
|
|
491
540
|
|
|
492
541
|
const paintQuitPrompt = (width, color) => {
|
|
@@ -669,8 +718,7 @@ const paintIdleNote = (view, width, color) => {
|
|
|
669
718
|
const text = view.noteText;
|
|
670
719
|
if (!text) return empty;
|
|
671
720
|
const inner = noteInnerWidth(width);
|
|
672
|
-
const
|
|
673
|
-
const wrapped = wrapMultiline(`${prefix}${text}`, inner);
|
|
721
|
+
const wrapped = wrapMultiline(`feedback: ${text}`, inner);
|
|
674
722
|
const rows = [];
|
|
675
723
|
for (const line of wrapped.slice(0, NOTE_MAX)) {
|
|
676
724
|
const painted = paintNoteLine(
|
|
@@ -688,7 +736,7 @@ const paintIdleNote = (view, width, color) => {
|
|
|
688
736
|
|
|
689
737
|
const paintNotePanel = (view, width, color, cap) => {
|
|
690
738
|
const empty = { rows: [], cursor: null, templateHits: [] };
|
|
691
|
-
if (
|
|
739
|
+
if ((view.pane ?? 'diff') === 'files') return empty;
|
|
692
740
|
const compose = view.compose;
|
|
693
741
|
if (compose && compose.kind === 'feedback') {
|
|
694
742
|
return paintComposeNote(view, width, color, cap);
|
|
@@ -696,16 +744,6 @@ const paintNotePanel = (view, width, color, cap) => {
|
|
|
696
744
|
return paintIdleNote(view, width, color);
|
|
697
745
|
};
|
|
698
746
|
|
|
699
|
-
const paintBodyHelp = (width, color) => {
|
|
700
|
-
const body = [];
|
|
701
|
-
for (const line of helpLines()) {
|
|
702
|
-
for (const row of wrapPlain(line, width)) {
|
|
703
|
-
body.push(paintBar(row, width, THEME.chromeFg, THEME.ctxBg, color));
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
return body;
|
|
707
|
-
};
|
|
708
|
-
|
|
709
747
|
const paintBodyFiles = (view, width, color, bodyH, headerLines) => {
|
|
710
748
|
const files = view.files ?? [];
|
|
711
749
|
const cols = fileColumns(files);
|
|
@@ -753,10 +791,39 @@ const paintBodyTodo = (view, width, color) => {
|
|
|
753
791
|
return { body, todoOwners, cursor };
|
|
754
792
|
};
|
|
755
793
|
|
|
794
|
+
const wrapSoftLine = (line, width) => {
|
|
795
|
+
const gutterW = Math.min(GUTTER_W, Math.max(0, width));
|
|
796
|
+
const budget = Math.max(1, width - gutterW);
|
|
797
|
+
const rows = wrapPlain(line.text ?? '', budget);
|
|
798
|
+
return rows.map((text) => {
|
|
799
|
+
const spans = [{ text, changed: false }];
|
|
800
|
+
return { ...line, text, spans };
|
|
801
|
+
});
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
const expandSoftRows = (lines, width, split) => {
|
|
805
|
+
const wrapWidth = split ? splitWidths(width).leftW : width;
|
|
806
|
+
const out = [];
|
|
807
|
+
for (const row of lines) {
|
|
808
|
+
const line = split ? row.left || row.right : row;
|
|
809
|
+
if (!line || !SOFT_WRAP.includes(line.type)) {
|
|
810
|
+
out.push(row);
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
const parts = wrapSoftLine(line, wrapWidth);
|
|
814
|
+
for (const part of parts) {
|
|
815
|
+
if (split) out.push({ left: part, right: part });
|
|
816
|
+
else out.push(part);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return out;
|
|
820
|
+
};
|
|
821
|
+
|
|
756
822
|
const paintBodyDiff = (item, layout, width, color) => {
|
|
757
823
|
const lang = detectLang(itemPath(item));
|
|
758
824
|
const splitBody = layout === 'side' && !item.file.isBinary;
|
|
759
|
-
const
|
|
825
|
+
const raw = dropLeadingEmpty(bodyLines(item, layout), splitBody);
|
|
826
|
+
const lines = expandSoftRows(raw, width, splitBody);
|
|
760
827
|
const body = [];
|
|
761
828
|
if (splitBody) {
|
|
762
829
|
for (const row of lines) {
|
|
@@ -771,7 +838,6 @@ const paintBodyDiff = (item, layout, width, color) => {
|
|
|
771
838
|
};
|
|
772
839
|
|
|
773
840
|
const bodyKind = (view) => {
|
|
774
|
-
if (view.help) return 'help';
|
|
775
841
|
if ((view.pane ?? 'diff') === 'files') return 'files';
|
|
776
842
|
if (!view.item) return 'empty';
|
|
777
843
|
if (view.item.origin === 'todo') return 'todo';
|
|
@@ -779,11 +845,6 @@ const bodyKind = (view) => {
|
|
|
779
845
|
};
|
|
780
846
|
|
|
781
847
|
const BODY_PAINT = {
|
|
782
|
-
help: (view, width, color) => ({
|
|
783
|
-
body: paintBodyHelp(width, color),
|
|
784
|
-
fileHits: [],
|
|
785
|
-
todoOwners: [],
|
|
786
|
-
}),
|
|
787
848
|
files: (view, width, color, bodyH, headerLines) => ({
|
|
788
849
|
...paintBodyFiles(view, width, color, bodyH, headerLines),
|
|
789
850
|
todoOwners: [],
|
|
@@ -837,10 +898,10 @@ const windowBody = (view, width, color, bodyH, headerLines, paintedBody) => {
|
|
|
837
898
|
const todoOwners = paintedBody.todoOwners;
|
|
838
899
|
const splitBody = paintedBody.splitBody === true;
|
|
839
900
|
const pane = view.pane ?? 'diff';
|
|
840
|
-
const listPane = pane === 'files'
|
|
901
|
+
const listPane = pane === 'files';
|
|
841
902
|
const fillKind = bodyFillKind(listPane, splitBody);
|
|
842
903
|
let shift = 0;
|
|
843
|
-
if (!listPane &&
|
|
904
|
+
if (!listPane && body.length > 0) {
|
|
844
905
|
body.unshift(paintBodyFill(width, color, fillKind));
|
|
845
906
|
if (todoOwners.length) todoOwners.unshift(null);
|
|
846
907
|
shift = 1;
|
|
@@ -961,36 +1022,18 @@ const presentRows = (rows, options = {}) => {
|
|
|
961
1022
|
return parts.join('');
|
|
962
1023
|
};
|
|
963
1024
|
|
|
964
|
-
const DEL_CHAR_BG = ansi.bg(THEME.delCharBg);
|
|
965
|
-
const ADD_CHAR_BG = ansi.bg(THEME.addCharBg);
|
|
966
|
-
const DEL_LINE_FG = ansi.fg(THEME.delLineFg);
|
|
967
|
-
const ADD_LINE_FG = ansi.fg(THEME.addLineFg);
|
|
968
|
-
|
|
969
1025
|
module.exports = {
|
|
970
|
-
LOGO,
|
|
971
|
-
FILE_MARK,
|
|
972
1026
|
QUIT_FINISH,
|
|
973
1027
|
QUIT_CONTINUE,
|
|
974
1028
|
QUIT_PROMPT,
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
renderFrame,
|
|
978
|
-
presentCursor,
|
|
979
|
-
presentRows,
|
|
980
|
-
layoutButtons,
|
|
1029
|
+
DEL_CHAR_BG,
|
|
1030
|
+
ADD_CHAR_BG,
|
|
981
1031
|
paintDiffLine,
|
|
982
|
-
paintSplitRow,
|
|
983
|
-
bodyLines,
|
|
984
1032
|
headerText,
|
|
985
|
-
|
|
986
|
-
wrapDoc,
|
|
987
|
-
wrapMultiline,
|
|
988
|
-
cursorInWrap,
|
|
989
|
-
wrapMove,
|
|
990
|
-
noteInnerWidth,
|
|
1033
|
+
layoutButtons,
|
|
991
1034
|
todoInnerWidth,
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1035
|
+
noteInnerWidth,
|
|
1036
|
+
renderFrame,
|
|
1037
|
+
presentCursor,
|
|
1038
|
+
presentRows,
|
|
996
1039
|
};
|
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 = {
|
|
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+)$/;
|
|
@@ -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) => {
|
|
@@ -354,25 +350,19 @@ const parseLoc = (raw) => {
|
|
|
354
350
|
fileParts = fileParts.slice(1);
|
|
355
351
|
}
|
|
356
352
|
if (!fileParts.length) return null;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
file: fileParts.join(':'),
|
|
360
|
-
oldStart,
|
|
361
|
-
newStart,
|
|
362
|
-
blockId,
|
|
363
|
-
};
|
|
353
|
+
const file = fileParts.join(':');
|
|
354
|
+
return { origin, file, oldStart, newStart, blockId };
|
|
364
355
|
};
|
|
365
356
|
|
|
366
357
|
const parseFeedSuffix = (textItem) => {
|
|
367
358
|
const match = FEED_SUFFIX.exec(textItem);
|
|
368
359
|
if (!match) return null;
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
};
|
|
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 };
|
|
376
366
|
};
|
|
377
367
|
|
|
378
368
|
const fileGroup = (groups, file) => {
|
|
@@ -390,7 +380,8 @@ const groupReviewNotes = (store) => {
|
|
|
390
380
|
if (!todo.text.trim()) continue;
|
|
391
381
|
fileGroup(groups, todo.file).todos.push(todo);
|
|
392
382
|
}
|
|
393
|
-
for (const
|
|
383
|
+
for (const key of store.feedback.keys()) {
|
|
384
|
+
const note = store.feedback.get(key);
|
|
394
385
|
if (!note.text.trim()) continue;
|
|
395
386
|
fileGroup(groups, note.file).feedback.push({ key, note });
|
|
396
387
|
}
|
|
@@ -415,7 +406,8 @@ const reviewHeader = (store) => {
|
|
|
415
406
|
};
|
|
416
407
|
|
|
417
408
|
const emitReviewGroups = (lines, groups) => {
|
|
418
|
-
for (const
|
|
409
|
+
for (const file of groups.keys()) {
|
|
410
|
+
const group = groups.get(file);
|
|
419
411
|
lines.push(`## ${file}`);
|
|
420
412
|
lines.push('');
|
|
421
413
|
for (const todo of group.todos) {
|
|
@@ -448,12 +440,9 @@ const parseFeedbackHeading = (line) => {
|
|
|
448
440
|
if (!withHead) {
|
|
449
441
|
return { file, newStart, header: rest, blockId: 0 };
|
|
450
442
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
header: withHead[1],
|
|
455
|
-
blockId: toInt(withHead[2]),
|
|
456
|
-
};
|
|
443
|
+
const header = withHead[1];
|
|
444
|
+
const blockId = toInt(withHead[2]);
|
|
445
|
+
return { file, newStart, header, blockId };
|
|
457
446
|
};
|
|
458
447
|
|
|
459
448
|
const flushPendingFeedback = (store, pending) => {
|
|
@@ -675,19 +664,12 @@ const mergeTodos = (gitItems, todos, extraFiles = []) => {
|
|
|
675
664
|
};
|
|
676
665
|
|
|
677
666
|
module.exports = {
|
|
678
|
-
REVIEW_DIR,
|
|
679
|
-
TEMPLATES_FILE,
|
|
680
667
|
TEMPLATE_SHOW,
|
|
681
668
|
AUTOSAVE_MS,
|
|
682
|
-
REVIEW_STATUSES,
|
|
683
|
-
AGENT_INSTRUCTIONS,
|
|
684
|
-
dateStamp,
|
|
685
669
|
allocateReviewPath,
|
|
686
670
|
listReviewNames,
|
|
687
|
-
compareReviewNames,
|
|
688
671
|
latestReviewName,
|
|
689
672
|
parseFrontmatterStatus,
|
|
690
|
-
readReviewStatus,
|
|
691
673
|
resolveReviewPath,
|
|
692
674
|
rankedTemplates,
|
|
693
675
|
prefixTemplates,
|
|
@@ -700,7 +682,6 @@ module.exports = {
|
|
|
700
682
|
addTodo,
|
|
701
683
|
removeTodo,
|
|
702
684
|
setTodoText,
|
|
703
|
-
isDoneMark,
|
|
704
685
|
checkLabel,
|
|
705
686
|
feedbackKey,
|
|
706
687
|
applyImportedNotes,
|
|
@@ -709,6 +690,5 @@ module.exports = {
|
|
|
709
690
|
loadReview,
|
|
710
691
|
loadTemplates,
|
|
711
692
|
flushReview,
|
|
712
|
-
makeTodoItem,
|
|
713
693
|
mergeTodos,
|
|
714
694
|
};
|