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/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 { FILES_DISABLED } = keys;
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
- 'reverted',
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 QUIT_FINISH = 'f';
137
- const QUIT_CONTINUE = 'c';
138
- const QUIT_FINISH_LABEL = 'finish as ready';
139
- const QUIT_CONTINUE_LABEL = 'continue next time';
140
- const QUIT_PROMPT = ` ${QUIT_FINISH_LABEL} ${QUIT_CONTINUE_LABEL}`;
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) => !line || !(line.text ?? '').trim();
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, disabled = []) => {
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 ACTION_IDS) {
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
- const isDisabled = disabled.includes(id);
487
- if (!isDisabled) hits.push({ id, x0: x, x1: x + w });
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 === ACTION_IDS.length) return words;
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
- if (part.disabled) {
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: ({ counts, notes, sourceLabel }) => {
519
- const n = counts.pr ?? 0;
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 'pr';
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 notes = `feedback ${feedback} todo ${todo}`;
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 paintQuitPrompt = (width, color) => {
570
+ const paintChoicePrompt = (choices, width, color) => {
552
571
  const bgRgb = THEME.chromeBg;
553
- let body = QUIT_PROMPT;
572
+ let body = choices.map((choice) => `${choice.lead}${choice.label}`).join('');
554
573
  if (color) {
555
- const finish = ` ${QUIT_FINISH_LABEL}`;
556
- const rest = ` ${QUIT_CONTINUE_LABEL}`;
557
- const paintQuit = (label, letter) =>
558
- paintMarkedWord(label, letter, color, QUIT_MARK);
559
- body = paintQuit(finish, QUIT_FINISH) + paintQuit(rest, QUIT_CONTINUE);
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') return paintQuitPrompt(width, color);
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
- return rows.map((text) => {
852
- const spans = [{ text, changed: false }];
853
- return { ...line, text, spans };
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 wrapWidth = split ? splitWidths(width).leftW : width;
908
+ const { leftW, rightW } = splitWidths(width);
859
909
  const out = [];
860
910
  for (const row of lines) {
861
- const line = split ? row.left || row.right : row;
862
- if (!line || !SOFT_WRAP.includes(line.type)) {
863
- out.push(row);
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 parts = wrapSoftLine(line, wrapWidth);
867
- for (const part of parts) {
868
- if (split) out.push({ left: part, right: part });
869
- else out.push(part);
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 paintBodyDiff = (item, layout, width, color) => {
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
- return { body, splitBody };
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.item, view.layout ?? 'unified', width, color),
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 pane = view.pane ?? 'diff';
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 isTruecolor = params[i + 1] === SGR_TRUECOLOR;
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
- [SGR_RESET]: (state) => {
87
+ 0: (state) => {
93
88
  state.fg = null;
94
89
  state.bg = null;
95
90
  return 1;
96
91
  },
97
- [SGR_FG]: (state, params, i) => applyRgb(state, 'fg', params, i),
98
- [SGR_BG]: (state, params, i) => applyRgb(state, 'bg', params, i),
99
- [SGR_DEFAULT_FG]: (state) => {
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
- [SGR_DEFAULT_BG]: (state) => {
98
+ 49: (state) => {
104
99
  state.bg = null;
105
100
  return 1;
106
101
  },