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 +10 -1
- package/README.md +21 -20
- package/lib/diff.js +71 -2
- package/lib/git.js +26 -0
- package/lib/keys.js +27 -6
- package/lib/render.js +145 -53
- package/lib/review.js +145 -3
- package/lib/select.js +7 -12
- package/lib/session.js +236 -22
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
|
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
|
|
55
|
-
| `u` | Unstage this
|
|
56
|
-
| `
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
|
|
|
62
|
-
| `
|
|
63
|
-
|
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
| `Ctrl-
|
|
67
|
-
| `Ctrl-
|
|
68
|
-
|
|
|
69
|
-
| `
|
|
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
|
|
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(
|
|
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: ['
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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) {
|
|
@@ -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
|
|
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
|
|
570
|
+
const paintChoicePrompt = (choices, width, color) => {
|
|
555
571
|
const bgRgb = THEME.chromeBg;
|
|
556
|
-
let body =
|
|
572
|
+
let body = choices.map((choice) => `${choice.lead}${choice.label}`).join('');
|
|
557
573
|
if (color) {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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')
|
|
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
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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
|
|
908
|
+
const { leftW, rightW } = splitWidths(width);
|
|
862
909
|
const out = [];
|
|
863
910
|
for (const row of lines) {
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|