reslop 0.1.3 → 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 +19 -1
- package/README.md +40 -38
- package/lib/api.js +3 -0
- package/lib/cli.js +31 -8
- package/lib/clipboard.js +11 -11
- package/lib/diff.js +71 -2
- package/lib/git.js +56 -16
- package/lib/gitlab.js +520 -0
- package/lib/keys.js +27 -6
- package/lib/render.js +154 -59
- package/lib/review.js +145 -3
- package/lib/select.js +7 -12
- package/lib/session.js +256 -44
- package/lib/source.js +5 -2
- package/lib/sys.js +59 -0
- package/package.json +2 -2
package/lib/render.js
CHANGED
|
@@ -11,7 +11,7 @@ const wrap = require('./wrap.js');
|
|
|
11
11
|
|
|
12
12
|
const { tokenize, overlayTokens } = highlight;
|
|
13
13
|
const { ACTIONS, ACTION_IDS, actionLetter, buttonWord } = keys;
|
|
14
|
-
const {
|
|
14
|
+
const { disabledActions } = keys;
|
|
15
15
|
const { wrapPlain, wrapMultiline, cursorInWrap } = wrap;
|
|
16
16
|
const { THEME, CODE_FG, codeFg, paint, visibleWidth } = ansi;
|
|
17
17
|
const { truncateVisible, fg, RESET, ESC, EL, seq } = ansi;
|
|
@@ -118,7 +118,7 @@ const INFO_STATUS = [
|
|
|
118
118
|
'staged',
|
|
119
119
|
'unstaged',
|
|
120
120
|
'not staged',
|
|
121
|
-
'
|
|
121
|
+
'dropped',
|
|
122
122
|
'already staged',
|
|
123
123
|
'read only',
|
|
124
124
|
'last block',
|
|
@@ -130,14 +130,29 @@ const INFO_STATUS = [
|
|
|
130
130
|
'mixed',
|
|
131
131
|
'side-by-side',
|
|
132
132
|
'saved',
|
|
133
|
+
'reloaded',
|
|
133
134
|
'not a diff block',
|
|
135
|
+
'committed',
|
|
136
|
+
'amended',
|
|
137
|
+
'fixup',
|
|
134
138
|
];
|
|
135
139
|
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
const QUIT_CHOICES = [
|
|
141
|
+
{ letter: 'f', label: 'finish as ready', lead: ' ' },
|
|
142
|
+
{ letter: 'c', label: 'continue next time', lead: ' ' },
|
|
143
|
+
{ letter: 'd', label: 'discard', lead: ' ' },
|
|
144
|
+
];
|
|
145
|
+
const QUIT_PROMPT = QUIT_CHOICES.map(
|
|
146
|
+
(choice) => `${choice.lead}${choice.label}`,
|
|
147
|
+
).join('');
|
|
148
|
+
const COMMIT_CHOICES = [
|
|
149
|
+
{ letter: 'c', label: 'commit', lead: ' ' },
|
|
150
|
+
{ letter: 'a', label: 'amend', lead: ' ' },
|
|
151
|
+
{ letter: 'f', label: 'fixup', lead: ' ' },
|
|
152
|
+
];
|
|
153
|
+
const COMMIT_PROMPT = COMMIT_CHOICES.map(
|
|
154
|
+
(choice) => `${choice.lead}${choice.label}`,
|
|
155
|
+
).join('');
|
|
141
156
|
|
|
142
157
|
const GUTTER_W = 2;
|
|
143
158
|
const BUTTON_GAP = ' ';
|
|
@@ -414,17 +429,21 @@ const paintHeader = (view, width, color) => {
|
|
|
414
429
|
return `${out}${RESET}`;
|
|
415
430
|
};
|
|
416
431
|
|
|
417
|
-
const bodyLines = (item, layout) => {
|
|
432
|
+
const bodyLines = (item, layout, overlay) => {
|
|
418
433
|
if (item.file.isBinary) {
|
|
419
434
|
const text = '(binary)';
|
|
420
435
|
return [{ type: 'ctx', text, spans: [{ text, changed: false }] }];
|
|
421
436
|
}
|
|
422
437
|
if (!item.hunk) return [];
|
|
423
438
|
const radius = item.dep ? Number.MAX_SAFE_INTEGER : undefined;
|
|
424
|
-
return displayLines(item.hunk, item.blockId, layout, radius);
|
|
439
|
+
return displayLines(item.hunk, item.blockId, layout, radius, overlay);
|
|
425
440
|
};
|
|
426
441
|
|
|
427
|
-
const sourceEmpty = (line) =>
|
|
442
|
+
const sourceEmpty = (line) => {
|
|
443
|
+
if (!line) return true;
|
|
444
|
+
if (line.editStart !== undefined) return false;
|
|
445
|
+
return !(line.text ?? '').trim();
|
|
446
|
+
};
|
|
428
447
|
|
|
429
448
|
const displayRowEmpty = (row, split) => {
|
|
430
449
|
if (!split) return sourceEmpty(row);
|
|
@@ -470,12 +489,13 @@ const paintMarkedWord = (label, letter, color, mark) => {
|
|
|
470
489
|
return out;
|
|
471
490
|
};
|
|
472
491
|
|
|
473
|
-
const layoutButtons = (width, compact = false,
|
|
492
|
+
const layoutButtons = (width, compact = false, hidden = []) => {
|
|
493
|
+
const ids = ACTION_IDS.filter((id) => !hidden.includes(id));
|
|
474
494
|
const build = (short) => {
|
|
475
495
|
const hits = [];
|
|
476
496
|
const parts = [];
|
|
477
497
|
let x = 0;
|
|
478
|
-
for (const id of
|
|
498
|
+
for (const id of ids) {
|
|
479
499
|
const action = { id, ...ACTIONS[id] };
|
|
480
500
|
const letter = actionLetter(action);
|
|
481
501
|
const word = buttonWord(action);
|
|
@@ -483,16 +503,15 @@ const layoutButtons = (width, compact = false, disabled = []) => {
|
|
|
483
503
|
const piece = `${BUTTON_GAP}${label}`;
|
|
484
504
|
const w = visibleWidth(piece);
|
|
485
505
|
if (x + w > width && parts.length) break;
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
parts.push({ action, piece, letter, label, disabled: isDisabled });
|
|
506
|
+
hits.push({ id, x0: x, x1: x + w });
|
|
507
|
+
parts.push({ action, piece, letter, label });
|
|
489
508
|
x += w;
|
|
490
509
|
}
|
|
491
510
|
return { hits, parts, width: x };
|
|
492
511
|
};
|
|
493
512
|
if (compact) return build(true);
|
|
494
513
|
const words = build(false);
|
|
495
|
-
if (words.parts.length ===
|
|
514
|
+
if (words.parts.length === ids.length) return words;
|
|
496
515
|
return build(true);
|
|
497
516
|
};
|
|
498
517
|
|
|
@@ -501,11 +520,7 @@ const paintButtons = (layout, width, color) => {
|
|
|
501
520
|
let used = 0;
|
|
502
521
|
for (const part of layout.parts) {
|
|
503
522
|
out += paint(BUTTON_GAP, THEME.buttonFg, THEME.buttonBg, color);
|
|
504
|
-
|
|
505
|
-
out += paint(part.label, THEME.buttonFg, THEME.buttonBg, color);
|
|
506
|
-
} else {
|
|
507
|
-
out += paintMarkedWord(part.label, part.letter, color, BUTTON_MARK);
|
|
508
|
-
}
|
|
523
|
+
out += paintMarkedWord(part.label, part.letter, color, BUTTON_MARK);
|
|
509
524
|
used += visibleWidth(part.piece);
|
|
510
525
|
}
|
|
511
526
|
if (used < width) {
|
|
@@ -514,12 +529,15 @@ const paintButtons = (layout, width, color) => {
|
|
|
514
529
|
return out;
|
|
515
530
|
};
|
|
516
531
|
|
|
532
|
+
const remoteCounts = (kind, { counts, notes, sourceLabel }) => {
|
|
533
|
+
const n = counts.pr ?? 0;
|
|
534
|
+
const label = sourceLabel || kind;
|
|
535
|
+
return ` ${kind} ${label} ${n} ${notes}`;
|
|
536
|
+
};
|
|
537
|
+
|
|
517
538
|
const COUNTS_KIND = {
|
|
518
|
-
pr: (
|
|
519
|
-
|
|
520
|
-
const label = sourceLabel || 'pr';
|
|
521
|
-
return ` pr ${label} ${n} ${notes}`;
|
|
522
|
-
},
|
|
539
|
+
pr: (args) => remoteCounts('pr', args),
|
|
540
|
+
mr: (args) => remoteCounts('mr', args),
|
|
523
541
|
commit: ({ counts, notes, revShort }) => {
|
|
524
542
|
const n = counts.commit ?? 0;
|
|
525
543
|
return ` commit ${revShort} ${n} ${notes}`;
|
|
@@ -535,7 +553,7 @@ const COUNTS_KIND = {
|
|
|
535
553
|
};
|
|
536
554
|
|
|
537
555
|
const countsKind = (sourceKind, revShort) => {
|
|
538
|
-
if (sourceKind === 'pr') return
|
|
556
|
+
if (sourceKind === 'pr' || sourceKind === 'mr') return sourceKind;
|
|
539
557
|
if (revShort) return 'commit';
|
|
540
558
|
return 'worktree';
|
|
541
559
|
};
|
|
@@ -543,20 +561,22 @@ const countsKind = (sourceKind, revShort) => {
|
|
|
543
561
|
const countsLine = (counts, revShort, sourceKind, sourceLabel) => {
|
|
544
562
|
const feedback = counts.feedback ?? 0;
|
|
545
563
|
const todo = counts.todo ?? 0;
|
|
546
|
-
const
|
|
564
|
+
const code = counts.code ?? 0;
|
|
565
|
+
const notes = `feedback ${feedback} todo ${todo} code ${code}`;
|
|
547
566
|
const kind = countsKind(sourceKind, revShort);
|
|
548
567
|
return COUNTS_KIND[kind]({ counts, notes, sourceLabel, revShort });
|
|
549
568
|
};
|
|
550
569
|
|
|
551
|
-
const
|
|
570
|
+
const paintChoicePrompt = (choices, width, color) => {
|
|
552
571
|
const bgRgb = THEME.chromeBg;
|
|
553
|
-
let body =
|
|
572
|
+
let body = choices.map((choice) => `${choice.lead}${choice.label}`).join('');
|
|
554
573
|
if (color) {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
574
|
+
body = choices
|
|
575
|
+
.map((choice) => {
|
|
576
|
+
const label = `${choice.lead}${choice.label}`;
|
|
577
|
+
return paintMarkedWord(label, choice.letter, color, QUIT_MARK);
|
|
578
|
+
})
|
|
579
|
+
.join('');
|
|
560
580
|
}
|
|
561
581
|
const pad = Math.max(0, width - visibleWidth(body));
|
|
562
582
|
return body + fill(pad, THEME.mutedFg, bgRgb, color);
|
|
@@ -564,7 +584,12 @@ const paintQuitPrompt = (width, color) => {
|
|
|
564
584
|
|
|
565
585
|
const paintStatusLine = (view, status, width, color) => {
|
|
566
586
|
const { mode, counts, revShort, sourceKind, sourceLabel } = view;
|
|
567
|
-
if (mode === 'confirmQuit')
|
|
587
|
+
if (mode === 'confirmQuit') {
|
|
588
|
+
return paintChoicePrompt(QUIT_CHOICES, width, color);
|
|
589
|
+
}
|
|
590
|
+
if (mode === 'confirmCommit') {
|
|
591
|
+
return paintChoicePrompt(COMMIT_CHOICES, width, color);
|
|
592
|
+
}
|
|
568
593
|
const left = countsLine(counts ?? {}, revShort, sourceKind, sourceLabel);
|
|
569
594
|
let right = '';
|
|
570
595
|
let msgFg = THEME.chromeFg;
|
|
@@ -677,6 +702,12 @@ const paintFileRow = (entry, width, color, selected, cols) => {
|
|
|
677
702
|
|
|
678
703
|
const todoInnerWidth = (width) => Math.max(1, width - 2);
|
|
679
704
|
|
|
705
|
+
const codeInnerWidth = (width, layout = 'unified') => {
|
|
706
|
+
const lineW = layout === 'side' ? splitWidths(width).rightW : width;
|
|
707
|
+
const gutterW = Math.min(GUTTER_W, Math.max(0, lineW));
|
|
708
|
+
return Math.max(1, lineW - gutterW);
|
|
709
|
+
};
|
|
710
|
+
|
|
680
711
|
const checkLeadLen = (text) => {
|
|
681
712
|
const match = CHECK_LEAD.exec(text);
|
|
682
713
|
return match ? match[0].length : 0;
|
|
@@ -789,11 +820,11 @@ const paintIdleNote = (view, width, color) => {
|
|
|
789
820
|
|
|
790
821
|
const paintNotePanel = (view, width, color, cap) => {
|
|
791
822
|
const empty = { rows: [], cursor: null, templateHits: [] };
|
|
792
|
-
if ((view.pane ?? 'diff') === 'files') return empty;
|
|
793
823
|
const compose = view.compose;
|
|
794
|
-
if (compose && compose.kind === 'feedback') {
|
|
824
|
+
if (compose && (compose.kind === 'feedback' || compose.kind === 'commit')) {
|
|
795
825
|
return paintComposeNote(view, width, color, cap);
|
|
796
826
|
}
|
|
827
|
+
if ((view.pane ?? 'diff') === 'files') return empty;
|
|
797
828
|
return paintIdleNote(view, width, color);
|
|
798
829
|
};
|
|
799
830
|
|
|
@@ -848,34 +879,93 @@ const wrapSoftLine = (line, width) => {
|
|
|
848
879
|
const gutterW = Math.min(GUTTER_W, Math.max(0, width));
|
|
849
880
|
const budget = Math.max(1, width - gutterW);
|
|
850
881
|
const rows = wrapPlain(line.text ?? '', budget);
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
882
|
+
const changed = line.wrap === true;
|
|
883
|
+
const pieces = [];
|
|
884
|
+
let used = 0;
|
|
885
|
+
for (let i = 0; i < rows.length; i++) {
|
|
886
|
+
const text = rows[i];
|
|
887
|
+
const last = i === rows.length - 1;
|
|
888
|
+
const spans = [{ text, changed }];
|
|
889
|
+
const next = { ...line, text, spans, editLast: last };
|
|
890
|
+
if (line.editStart !== undefined) {
|
|
891
|
+
next.editStart = line.editStart + used;
|
|
892
|
+
if (last) next.editEnd = line.editEnd;
|
|
893
|
+
else next.editEnd = next.editStart + text.length;
|
|
894
|
+
}
|
|
895
|
+
used += text.length;
|
|
896
|
+
pieces.push(next);
|
|
897
|
+
}
|
|
898
|
+
return pieces;
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
const shouldWrap = (line) => {
|
|
902
|
+
if (!line) return false;
|
|
903
|
+
if (SOFT_WRAP.includes(line.type)) return true;
|
|
904
|
+
return line.wrap === true;
|
|
855
905
|
};
|
|
856
906
|
|
|
857
907
|
const expandSoftRows = (lines, width, split) => {
|
|
858
|
-
const
|
|
908
|
+
const { leftW, rightW } = splitWidths(width);
|
|
859
909
|
const out = [];
|
|
860
910
|
for (const row of lines) {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
911
|
+
if (!split) {
|
|
912
|
+
if (!shouldWrap(row)) {
|
|
913
|
+
out.push(row);
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
for (const part of wrapSoftLine(row, width)) out.push(part);
|
|
864
917
|
continue;
|
|
865
918
|
}
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
919
|
+
const right = row.right;
|
|
920
|
+
if (shouldWrap(right)) {
|
|
921
|
+
const parts = wrapSoftLine(right, rightW);
|
|
922
|
+
out.push({ left: row.left, right: parts[0] });
|
|
923
|
+
for (let i = 1; i < parts.length; i++) {
|
|
924
|
+
out.push({ left: null, right: parts[i] });
|
|
925
|
+
}
|
|
926
|
+
continue;
|
|
870
927
|
}
|
|
928
|
+
const left = row.left;
|
|
929
|
+
if (shouldWrap(left)) {
|
|
930
|
+
const parts = wrapSoftLine(left, leftW);
|
|
931
|
+
for (const part of parts) out.push({ left: part, right: part });
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
out.push(row);
|
|
871
935
|
}
|
|
872
936
|
return out;
|
|
873
937
|
};
|
|
874
938
|
|
|
875
|
-
const
|
|
939
|
+
const onCodePiece = (line, cursor) => {
|
|
940
|
+
if (!line || line.editStart === undefined) return false;
|
|
941
|
+
if (cursor < line.editStart) return false;
|
|
942
|
+
if (line.editLast) return cursor <= line.editEnd;
|
|
943
|
+
return cursor < line.editEnd;
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
const codeCursorInRows = (rows, split, width, cursor) => {
|
|
947
|
+
if (cursor === undefined || cursor === null) return null;
|
|
948
|
+
const { leftW, rightW } = splitWidths(width);
|
|
949
|
+
for (let i = 0; i < rows.length; i++) {
|
|
950
|
+
const line = split ? rows[i].right : rows[i];
|
|
951
|
+
if (!onCodePiece(line, cursor)) continue;
|
|
952
|
+
const lineW = split ? rightW : width;
|
|
953
|
+
const gutterW = Math.min(GUTTER_W, Math.max(0, lineW));
|
|
954
|
+
const take = Math.min(cursor - line.editStart, line.text.length);
|
|
955
|
+
const col = visibleWidth(line.text.slice(0, take));
|
|
956
|
+
const x0 = split ? leftW + 1 : 0;
|
|
957
|
+
return { x: x0 + gutterW + col + 1, row: i };
|
|
958
|
+
}
|
|
959
|
+
return null;
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
const paintBodyDiff = (view, width, color) => {
|
|
963
|
+
const item = view.item;
|
|
964
|
+
const layout = view.layout ?? 'unified';
|
|
965
|
+
const overlay = view.codeOverlay;
|
|
876
966
|
const lang = detectLang(itemPath(item));
|
|
877
967
|
const splitBody = layout === 'side' && !item.file.isBinary;
|
|
878
|
-
const raw = dropLeadingEmpty(bodyLines(item, layout), splitBody);
|
|
968
|
+
const raw = dropLeadingEmpty(bodyLines(item, layout, overlay), splitBody);
|
|
879
969
|
const lines = expandSoftRows(raw, width, splitBody);
|
|
880
970
|
const body = [];
|
|
881
971
|
if (splitBody) {
|
|
@@ -887,7 +977,9 @@ const paintBodyDiff = (item, layout, width, color) => {
|
|
|
887
977
|
body.push(paintDiffLine(line, width, color, item.origin, lang));
|
|
888
978
|
}
|
|
889
979
|
}
|
|
890
|
-
|
|
980
|
+
const edit = overlay ? overlay.cursor : null;
|
|
981
|
+
const cursor = codeCursorInRows(lines, splitBody, width, edit);
|
|
982
|
+
return { body, splitBody, cursor };
|
|
891
983
|
};
|
|
892
984
|
|
|
893
985
|
const bodyKind = (view) => {
|
|
@@ -908,7 +1000,7 @@ const BODY_PAINT = {
|
|
|
908
1000
|
fileHits: [],
|
|
909
1001
|
}),
|
|
910
1002
|
diff: (view, width, color) => ({
|
|
911
|
-
...paintBodyDiff(view
|
|
1003
|
+
...paintBodyDiff(view, width, color),
|
|
912
1004
|
fileHits: [],
|
|
913
1005
|
todoOwners: [],
|
|
914
1006
|
}),
|
|
@@ -1025,14 +1117,17 @@ const frameResult = (text, painted, layout, windowed, hits, bodyH) => {
|
|
|
1025
1117
|
};
|
|
1026
1118
|
};
|
|
1027
1119
|
|
|
1120
|
+
const hiddenActions = (view) => {
|
|
1121
|
+
const pane = view.pane ?? 'diff';
|
|
1122
|
+
return disabledActions(pane, view.item);
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1028
1125
|
const renderFrame = (view, options = {}) => {
|
|
1029
1126
|
const width = Math.max(20, options.width ?? 80);
|
|
1030
1127
|
const height = Math.max(6, options.height ?? 24);
|
|
1031
1128
|
const color = options.color ?? true;
|
|
1032
1129
|
const rows = [paintHeader(view, width, color)];
|
|
1033
|
-
const
|
|
1034
|
-
const disabled = pane === 'files' ? FILES_DISABLED : [];
|
|
1035
|
-
const layout = layoutButtons(width, false, disabled);
|
|
1130
|
+
const layout = layoutButtons(width, false, hiddenActions(view));
|
|
1036
1131
|
const notes = frameNotes(view, width, height, color);
|
|
1037
1132
|
const headerLines = rows.length;
|
|
1038
1133
|
const bodyH = notes.bodyH;
|
|
@@ -1077,9 +1172,8 @@ const presentRows = (rows, options = {}) => {
|
|
|
1077
1172
|
};
|
|
1078
1173
|
|
|
1079
1174
|
module.exports = {
|
|
1080
|
-
QUIT_FINISH,
|
|
1081
|
-
QUIT_CONTINUE,
|
|
1082
1175
|
QUIT_PROMPT,
|
|
1176
|
+
COMMIT_PROMPT,
|
|
1083
1177
|
DEL_CHAR_BG,
|
|
1084
1178
|
ADD_CHAR_BG,
|
|
1085
1179
|
paintDiffLine,
|
|
@@ -1087,6 +1181,7 @@ module.exports = {
|
|
|
1087
1181
|
layoutButtons,
|
|
1088
1182
|
todoInnerWidth,
|
|
1089
1183
|
noteInnerWidth,
|
|
1184
|
+
codeInnerWidth,
|
|
1090
1185
|
renderFrame,
|
|
1091
1186
|
presentCursor,
|
|
1092
1187
|
presentRows,
|
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
|
},
|