reslop 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## [Unreleased][unreleased]
4
4
 
5
+ ## [0.1.5][] - 2026-09-14
6
+
7
+ - Added `r` to reload diffs from disk
8
+ - Added `e` to edit added lines in place
9
+ - Added `d` on quit to discard a new review file
10
+ - Added `c` to commit, amend, or fixup staged changes
11
+ - Reassign hotkeys to free needed keys for new operations
12
+
5
13
  ## [0.1.4][] - 2026-09-13
6
14
 
7
15
  - Support a GitLab MR by URL and import review comments
@@ -41,7 +49,8 @@
41
49
  - Feedback and todos that produce a repair plan for an agent
42
50
  - Intra-line highlighting in unified, mixed, and side-by-side layouts
43
51
 
44
- [unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.1.4...HEAD
52
+ [unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.1.5...HEAD
53
+ [0.1.5]: https://github.com/tshemsedinov/reslop/compare/v0.1.4...v0.1.5
45
54
  [0.1.4]: https://github.com/tshemsedinov/reslop/compare/v0.1.3...v0.1.4
46
55
  [0.1.3]: https://github.com/tshemsedinov/reslop/compare/v0.1.2...v0.1.3
47
56
  [0.1.2]: https://github.com/tshemsedinov/reslop/compare/v0.1.1...v0.1.2
package/README.md CHANGED
@@ -18,7 +18,7 @@ Review → Plan → Repair → Verify
18
18
 
19
19
  - Review uncommitted git diffs, a given commit, a GitHub PR, or a GitLab MR.
20
20
  - Stage, unstage, or revert each contiguous block of diff lines.
21
- - Leave feedback and todos that become a repair plan for the agent.
21
+ - Leave feedback, todos, and in-place code proposals that become a repair plan.
22
22
  - Import GitHub PR and GitLab MR review comments into the local plan for AI.
23
23
  - Review each npm dependency once across `package.json` and the lockfile.
24
24
  - Propose unused removals, npm audit fixes, and outdated updates as diffs.
@@ -38,7 +38,7 @@ Works on Linux, macOS, and Windows. On Windows use Windows Terminal.
38
38
  - `reslop 7ac260c` show that commit (read-only)
39
39
  - `reslop https://github.com/metarhia/metacom/pull/555` GitHub/GitLab PR/MR
40
40
  - `reslop -n` start a new review even if the latest is still editing
41
- - `reslop -r` read-only: no stage, unstage, revert, or npm apply
41
+ - `reslop -r` read-only mode
42
42
 
43
43
  Reviews go in `.review/YYYY-MM-DD-NN.md` with frontmatter `status`:
44
44
 
@@ -49,24 +49,25 @@ Reviews go in `.review/YYYY-MM-DD-NN.md` with frontmatter `status`:
49
49
 
50
50
  ## Hotkeys
51
51
 
52
- | Key | Action |
53
- | ------------------- | ---------------------------------------- |
54
- | `a` | Stage this block (`git add`) |
55
- | `u` | Unstage this block (keep worktree) |
56
- | `r` | Restore this block to the last commit |
57
- | `f` | Feedback on this diff block |
58
- | `t` | New todo for this file |
59
- | `l` | File list (git status of this scope) |
60
- | `m` | Cycle unified / mixed / side-by-side |
61
- | `⏎` | Open the selected file |
62
- | `Esc` | Quit file list or a tool |
63
- | `→` / `j` | Next remaining block / next file |
64
- | `←` / `k` | Previous remaining block / previous file |
65
- | `Ctrl-e` / `Ctrl-y` | Scroll one line down / up |
66
- | `Ctrl-f` / `Ctrl-b` | Scroll page down / up |
67
- | `Ctrl-d` / `Ctrl-u` | Scroll half-page down / up |
68
- | `⌫` / `Del` | Remove the selected todo |
69
- | `q` | Quit |
52
+ | Key | Action |
53
+ | ------------------- | ----------------------------------------------- |
54
+ | `a` | Stage this diff (git add) |
55
+ | `u` | Unstage this diff |
56
+ | `d` | Drop this diff to the last commit |
57
+ | `c` | Commit: then `c` commit / `a` amend / `f` fixup |
58
+ | `f` | Feedback on this diff |
59
+ | `t` | New todo |
60
+ | `e` | Edit added lines |
61
+ | `r` | Reload diffs from disk |
62
+ | `m` | Mode: unified / mixed / side-by-side |
63
+ | `⏎` | Open the selected file |
64
+ | `→` / `j` | Next remaining diff / next file |
65
+ | `←` / `k` | Previous remaining diff / previous file |
66
+ | `Ctrl-e` / `Ctrl-y` | Scroll one line down / up |
67
+ | `Ctrl-f` / `Ctrl-b` | Scroll page down / up |
68
+ | `Ctrl-d` / `Ctrl-u` | Scroll half-page down / up |
69
+ | `⌫` / `Del` | Remove the selected todo |
70
+ | `Esc` / `q` | Quit |
70
71
 
71
72
  ## License
72
73
 
package/lib/diff.js CHANGED
@@ -773,10 +773,78 @@ const windowAroundBlock = (lines, blockId, radius) => {
773
773
  return lines.slice(from, to);
774
774
  };
775
775
 
776
- const displayLines = (hunk, blockId, layout = 'unified', radius) => {
776
+ const insertAddIndex = (lines) => {
777
+ for (let i = lines.length - 1; i >= 0; i--) {
778
+ if (isCtxType(lines[i].type)) continue;
779
+ return i + 1;
780
+ }
781
+ return lines.length;
782
+ };
783
+
784
+ const taggedAdd = (text, blockId, start, end) => ({
785
+ type: 'add',
786
+ text,
787
+ noNl: false,
788
+ blockId,
789
+ wrap: true,
790
+ editStart: start,
791
+ editEnd: end,
792
+ editLast: true,
793
+ });
794
+
795
+ const replaceAdds = (lines, overlay, blockId) => {
796
+ const text = overlay.text ?? '';
797
+ const keepEmpty = overlay.keepEmpty === true;
798
+ if (text === '' && !keepEmpty) {
799
+ return lines.filter((line) => line.type !== 'add');
800
+ }
801
+ const texts = text.split('\n');
802
+ const tagged = [];
803
+ let offset = 0;
804
+ for (let i = 0; i < texts.length; i++) {
805
+ const row = texts[i];
806
+ const start = offset;
807
+ const end = start + row.length;
808
+ const isLast = i === texts.length - 1;
809
+ tagged.push(taggedAdd(row, blockId, start, end));
810
+ offset = isLast ? end : end + 1;
811
+ }
812
+ const out = [];
813
+ let placed = 0;
814
+ let lastAdd = -1;
815
+ for (const line of lines) {
816
+ if (line.type !== 'add') {
817
+ out.push(line);
818
+ continue;
819
+ }
820
+ if (placed >= tagged.length) continue;
821
+ out.push(tagged[placed]);
822
+ lastAdd = out.length - 1;
823
+ placed += 1;
824
+ }
825
+ const extras = tagged.slice(placed);
826
+ if (!extras.length) return out;
827
+ const at = lastAdd >= 0 ? lastAdd + 1 : insertAddIndex(out);
828
+ out.splice(at, 0, ...extras);
829
+ return out;
830
+ };
831
+
832
+ const blockAddText = (hunk, blockId) => {
833
+ if (!hunk) return '';
834
+ const parts = [];
835
+ for (const line of hunk.lines) {
836
+ if (line.type !== 'add') continue;
837
+ if (line.blockId !== blockId) continue;
838
+ parts.push(line.text);
839
+ }
840
+ return parts.join('\n');
841
+ };
842
+
843
+ const displayLines = (hunk, blockId, layout = 'unified', radius, overlay) => {
777
844
  const flat = flattenBlock(hunk, blockId, 'new');
845
+ const source = overlay ? replaceAdds(flat, overlay, blockId) : flat;
778
846
  const around = radius ?? DISPLAY_CONTEXT;
779
- const windowed = windowAroundBlock(flat, blockId, around);
847
+ const windowed = windowAroundBlock(source, blockId, around);
780
848
  const lines = windowed.map((line) => ({ ...line }));
781
849
  const painted = attachInline(lines);
782
850
  const align = LAYOUT_ALIGN[layout];
@@ -877,6 +945,7 @@ module.exports = {
877
945
  countSides,
878
946
  formatPatch,
879
947
  displayLines,
948
+ blockAddText,
880
949
  synthesizeNewFile,
881
950
  itemsFromFiles,
882
951
  refreshIndexPatches,
package/lib/git.js CHANGED
@@ -845,6 +845,28 @@ const revertItem = (top, item) => {
845
845
  }
846
846
  };
847
847
 
848
+ const lastMessage = (top) => {
849
+ const result = runGit(['log', '-1', '--format=%B'], top);
850
+ if (result.status !== 0) return '';
851
+ return (result.stdout ?? '').replace(/\s+$/, '');
852
+ };
853
+
854
+ const commitChanges = (top, kind, message) => {
855
+ if (kind === 'fixup') {
856
+ const target = (message ?? '').trim() || 'HEAD';
857
+ const result = runGit(['commit', `--fixup=${target}`], top);
858
+ requireOk(result);
859
+ return;
860
+ }
861
+ const args = ['commit'];
862
+ if (kind === 'amend') args.push('--amend');
863
+ args.push('-F', '-');
864
+ const text = message ?? '';
865
+ const body = text.endsWith('\n') ? text : `${text}\n`;
866
+ const result = runGit(args, top, body);
867
+ requireOk(result);
868
+ };
869
+
848
870
  const revertFile = (top, rel, items) => {
849
871
  const paths = new Set([rel]);
850
872
  for (const item of items) {
@@ -867,6 +889,8 @@ const createGitRepo = () => ({
867
889
  unstage: unstageItem,
868
890
  revert: revertItem,
869
891
  revertFile,
892
+ commit: commitChanges,
893
+ lastMessage,
870
894
  });
871
895
 
872
896
  module.exports = {
@@ -876,5 +900,7 @@ module.exports = {
876
900
  addItem,
877
901
  unstageItem,
878
902
  revertItem,
903
+ commitChanges,
904
+ lastMessage,
879
905
  createGitRepo,
880
906
  };
package/lib/keys.js CHANGED
@@ -3,18 +3,36 @@
3
3
  const ACTIONS = {
4
4
  add: { keys: ['a'], label: 'Add' },
5
5
  unstage: { keys: ['u'], label: 'Unstage' },
6
- revert: { keys: ['r'], label: 'Revert' },
7
- prev: { keys: ['left', 'p', 'k'], hot: '←', label: 'Prev' },
8
- next: { keys: ['right', 'n', 'j'], hot: '→', label: 'Next' },
6
+ revert: { keys: ['d'], label: 'Drop' },
7
+ commit: { keys: ['c'], label: 'Commit' },
8
+ prev: { keys: ['left', 'p', 'k'], hot: '←', label: '←' },
9
+ next: { keys: ['right', 'n', 'j'], hot: '→', label: '→' },
9
10
  layout: { keys: ['m'], label: 'Mode' },
10
- files: { keys: ['l'], label: 'Files' },
11
11
  feedback: { keys: ['f'], label: 'Feedback' },
12
12
  todo: { keys: ['t'], label: 'Todo' },
13
- quit: { keys: ['q', 'ctrl-c'], label: 'Quit' },
13
+ code: { keys: ['e'], label: 'Edit' },
14
+ reload: { keys: ['r'], label: 'Reload' },
15
+ quit: { keys: ['q', 'ctrl-c'], label: 'Q' },
14
16
  };
15
17
  const ACTION_IDS = Object.keys(ACTIONS);
16
18
 
17
- const FILES_DISABLED = ['layout', 'feedback'];
19
+ const DIFF_DISABLED = ['reload'];
20
+ const FILES_DISABLED = ['layout', 'feedback', 'code'];
21
+ const TODO_DISABLED = [
22
+ 'add',
23
+ 'unstage',
24
+ 'revert',
25
+ 'layout',
26
+ 'feedback',
27
+ 'code',
28
+ 'reload',
29
+ ];
30
+
31
+ const disabledActions = (pane, item) => {
32
+ if (pane === 'files') return FILES_DISABLED;
33
+ if (item && item.origin === 'todo') return TODO_DISABLED;
34
+ return DIFF_DISABLED;
35
+ };
18
36
 
19
37
  const SCROLL = {
20
38
  up: 'scrollUp',
@@ -197,7 +215,10 @@ const hitAction = (hits, x) => {
197
215
  module.exports = {
198
216
  ACTIONS,
199
217
  ACTION_IDS,
218
+ DIFF_DISABLED,
200
219
  FILES_DISABLED,
220
+ TODO_DISABLED,
221
+ disabledActions,
201
222
  decodeChunk,
202
223
  actionFromKey,
203
224
  actionHot,
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) {
@@ -546,20 +561,22 @@ const countsKind = (sourceKind, revShort) => {
546
561
  const countsLine = (counts, revShort, sourceKind, sourceLabel) => {
547
562
  const feedback = counts.feedback ?? 0;
548
563
  const todo = counts.todo ?? 0;
549
- const notes = `feedback ${feedback} todo ${todo}`;
564
+ const code = counts.code ?? 0;
565
+ const notes = `feedback ${feedback} todo ${todo} code ${code}`;
550
566
  const kind = countsKind(sourceKind, revShort);
551
567
  return COUNTS_KIND[kind]({ counts, notes, sourceLabel, revShort });
552
568
  };
553
569
 
554
- const paintQuitPrompt = (width, color) => {
570
+ const paintChoicePrompt = (choices, width, color) => {
555
571
  const bgRgb = THEME.chromeBg;
556
- let body = QUIT_PROMPT;
572
+ let body = choices.map((choice) => `${choice.lead}${choice.label}`).join('');
557
573
  if (color) {
558
- const finish = ` ${QUIT_FINISH_LABEL}`;
559
- const rest = ` ${QUIT_CONTINUE_LABEL}`;
560
- const paintQuit = (label, letter) =>
561
- paintMarkedWord(label, letter, color, QUIT_MARK);
562
- 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('');
563
580
  }
564
581
  const pad = Math.max(0, width - visibleWidth(body));
565
582
  return body + fill(pad, THEME.mutedFg, bgRgb, color);
@@ -567,7 +584,12 @@ const paintQuitPrompt = (width, color) => {
567
584
 
568
585
  const paintStatusLine = (view, status, width, color) => {
569
586
  const { mode, counts, revShort, sourceKind, sourceLabel } = view;
570
- 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
+ }
571
593
  const left = countsLine(counts ?? {}, revShort, sourceKind, sourceLabel);
572
594
  let right = '';
573
595
  let msgFg = THEME.chromeFg;
@@ -680,6 +702,12 @@ const paintFileRow = (entry, width, color, selected, cols) => {
680
702
 
681
703
  const todoInnerWidth = (width) => Math.max(1, width - 2);
682
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
+
683
711
  const checkLeadLen = (text) => {
684
712
  const match = CHECK_LEAD.exec(text);
685
713
  return match ? match[0].length : 0;
@@ -792,11 +820,11 @@ const paintIdleNote = (view, width, color) => {
792
820
 
793
821
  const paintNotePanel = (view, width, color, cap) => {
794
822
  const empty = { rows: [], cursor: null, templateHits: [] };
795
- if ((view.pane ?? 'diff') === 'files') return empty;
796
823
  const compose = view.compose;
797
- if (compose && compose.kind === 'feedback') {
824
+ if (compose && (compose.kind === 'feedback' || compose.kind === 'commit')) {
798
825
  return paintComposeNote(view, width, color, cap);
799
826
  }
827
+ if ((view.pane ?? 'diff') === 'files') return empty;
800
828
  return paintIdleNote(view, width, color);
801
829
  };
802
830
 
@@ -851,34 +879,93 @@ const wrapSoftLine = (line, width) => {
851
879
  const gutterW = Math.min(GUTTER_W, Math.max(0, width));
852
880
  const budget = Math.max(1, width - gutterW);
853
881
  const rows = wrapPlain(line.text ?? '', budget);
854
- return rows.map((text) => {
855
- const spans = [{ text, changed: false }];
856
- return { ...line, text, spans };
857
- });
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;
858
905
  };
859
906
 
860
907
  const expandSoftRows = (lines, width, split) => {
861
- const wrapWidth = split ? splitWidths(width).leftW : width;
908
+ const { leftW, rightW } = splitWidths(width);
862
909
  const out = [];
863
910
  for (const row of lines) {
864
- const line = split ? row.left || row.right : row;
865
- if (!line || !SOFT_WRAP.includes(line.type)) {
866
- 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);
917
+ continue;
918
+ }
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
+ }
867
926
  continue;
868
927
  }
869
- const parts = wrapSoftLine(line, wrapWidth);
870
- for (const part of parts) {
871
- if (split) out.push({ left: part, right: part });
872
- else out.push(part);
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;
873
933
  }
934
+ out.push(row);
874
935
  }
875
936
  return out;
876
937
  };
877
938
 
878
- 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;
879
966
  const lang = detectLang(itemPath(item));
880
967
  const splitBody = layout === 'side' && !item.file.isBinary;
881
- const raw = dropLeadingEmpty(bodyLines(item, layout), splitBody);
968
+ const raw = dropLeadingEmpty(bodyLines(item, layout, overlay), splitBody);
882
969
  const lines = expandSoftRows(raw, width, splitBody);
883
970
  const body = [];
884
971
  if (splitBody) {
@@ -890,7 +977,9 @@ const paintBodyDiff = (item, layout, width, color) => {
890
977
  body.push(paintDiffLine(line, width, color, item.origin, lang));
891
978
  }
892
979
  }
893
- return { body, splitBody };
980
+ const edit = overlay ? overlay.cursor : null;
981
+ const cursor = codeCursorInRows(lines, splitBody, width, edit);
982
+ return { body, splitBody, cursor };
894
983
  };
895
984
 
896
985
  const bodyKind = (view) => {
@@ -911,7 +1000,7 @@ const BODY_PAINT = {
911
1000
  fileHits: [],
912
1001
  }),
913
1002
  diff: (view, width, color) => ({
914
- ...paintBodyDiff(view.item, view.layout ?? 'unified', width, color),
1003
+ ...paintBodyDiff(view, width, color),
915
1004
  fileHits: [],
916
1005
  todoOwners: [],
917
1006
  }),
@@ -1028,14 +1117,17 @@ const frameResult = (text, painted, layout, windowed, hits, bodyH) => {
1028
1117
  };
1029
1118
  };
1030
1119
 
1120
+ const hiddenActions = (view) => {
1121
+ const pane = view.pane ?? 'diff';
1122
+ return disabledActions(pane, view.item);
1123
+ };
1124
+
1031
1125
  const renderFrame = (view, options = {}) => {
1032
1126
  const width = Math.max(20, options.width ?? 80);
1033
1127
  const height = Math.max(6, options.height ?? 24);
1034
1128
  const color = options.color ?? true;
1035
1129
  const rows = [paintHeader(view, width, color)];
1036
- const pane = view.pane ?? 'diff';
1037
- const disabled = pane === 'files' ? FILES_DISABLED : [];
1038
- const layout = layoutButtons(width, false, disabled);
1130
+ const layout = layoutButtons(width, false, hiddenActions(view));
1039
1131
  const notes = frameNotes(view, width, height, color);
1040
1132
  const headerLines = rows.length;
1041
1133
  const bodyH = notes.bodyH;
@@ -1080,9 +1172,8 @@ const presentRows = (rows, options = {}) => {
1080
1172
  };
1081
1173
 
1082
1174
  module.exports = {
1083
- QUIT_FINISH,
1084
- QUIT_CONTINUE,
1085
1175
  QUIT_PROMPT,
1176
+ COMMIT_PROMPT,
1086
1177
  DEL_CHAR_BG,
1087
1178
  ADD_CHAR_BG,
1088
1179
  paintDiffLine,
@@ -1090,6 +1181,7 @@ module.exports = {
1090
1181
  layoutButtons,
1091
1182
  todoInnerWidth,
1092
1183
  noteInnerWidth,
1184
+ codeInnerWidth,
1093
1185
  renderFrame,
1094
1186
  presentCursor,
1095
1187
  presentRows,