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/session.js
CHANGED
|
@@ -9,19 +9,19 @@ const review = require('./review.js');
|
|
|
9
9
|
const wrap = require('./wrap.js');
|
|
10
10
|
const { extractText, isRange } = require('./select.js');
|
|
11
11
|
const { copyText } = require('./clipboard.js');
|
|
12
|
+
const { watchResize } = require('./sys.js');
|
|
12
13
|
const { fileEntries, itemPath } = require('./files.js');
|
|
13
14
|
const { Editor } = require('./editor.js');
|
|
14
|
-
const { refreshIndexPatches } = require('./diff.js');
|
|
15
|
+
const { refreshIndexPatches, blockAddText } = require('./diff.js');
|
|
15
16
|
|
|
16
17
|
const { renderFrame, presentRows, presentCursor } = render;
|
|
17
|
-
const { noteInnerWidth, todoInnerWidth } = render;
|
|
18
|
-
const { QUIT_FINISH, QUIT_CONTINUE } = render;
|
|
18
|
+
const { noteInnerWidth, todoInnerWidth, codeInnerWidth } = render;
|
|
19
19
|
const { actionFromKey, hitAction, decodeChunk } = keys;
|
|
20
|
-
const {
|
|
20
|
+
const { disabledActions } = keys;
|
|
21
21
|
const { cursorInWrap, wrapDoc, wrapMove } = wrap;
|
|
22
22
|
const { AUTOSAVE_MS, listReviewNames, loadTemplates } = review;
|
|
23
23
|
const { resolveReviewPath, loadReview, createStore } = review;
|
|
24
|
-
const { noteCounts, hasNotes, setFeedback, addTodo } = review;
|
|
24
|
+
const { noteCounts, hasNotes, setFeedback, setCode, addTodo } = review;
|
|
25
25
|
const { applyImportedNotes } = review;
|
|
26
26
|
const { setTodoText, removeTodo, flushReview, mergeTodos } = review;
|
|
27
27
|
const { feedbackKey, checkLabel, rankedTemplates } = review;
|
|
@@ -29,6 +29,10 @@ const { prefixTemplates, TEMPLATE_SHOW, rememberTemplate } = review;
|
|
|
29
29
|
|
|
30
30
|
const LAYOUTS = ['unified', 'mixed', 'side'];
|
|
31
31
|
const READ_ONLY_ORIGINS = ['commit', 'pr'];
|
|
32
|
+
const SOURCE_KIND = {
|
|
33
|
+
'github-pr': 'pr',
|
|
34
|
+
'gitlab-mr': 'mr',
|
|
35
|
+
};
|
|
32
36
|
const ENTER_TERM = '\x1b[?1049h\x1b[?25l\x1b[?7l\x1b[?1002h\x1b[?1006h';
|
|
33
37
|
const LEAVE_TERM =
|
|
34
38
|
'\x1b[?1006l\x1b[?1002l\x1b[?7h\x1b[0 q\x1b[?12l\x1b[?25h\x1b[?1049l';
|
|
@@ -82,6 +86,13 @@ const itemFeedKey = (item) => {
|
|
|
82
86
|
return feedbackKey({ file, oldStart, newStart, blockId });
|
|
83
87
|
};
|
|
84
88
|
|
|
89
|
+
const canEditCode = (item) => {
|
|
90
|
+
if (!item || item.origin === 'todo') return false;
|
|
91
|
+
if (!item.hunk || !item.file) return false;
|
|
92
|
+
if (item.file.isBinary) return false;
|
|
93
|
+
return true;
|
|
94
|
+
};
|
|
95
|
+
|
|
85
96
|
const isAbortError = (error) => !!(error && error.code === 'ABORT');
|
|
86
97
|
|
|
87
98
|
const sameCursor = (a, b) => {
|
|
@@ -91,12 +102,26 @@ const sameCursor = (a, b) => {
|
|
|
91
102
|
};
|
|
92
103
|
|
|
93
104
|
const QUIT_CONFIRM = {
|
|
94
|
-
|
|
95
|
-
|
|
105
|
+
f: (session) => session.finishQuit('ready'),
|
|
106
|
+
c: (session) => session.finishQuit('editing'),
|
|
107
|
+
d: (session) => session.discardQuit(),
|
|
96
108
|
'ctrl-c': (session) => session.finishQuit('ready'),
|
|
97
109
|
escape: (session) => session.cancelQuit(),
|
|
98
110
|
};
|
|
99
111
|
|
|
112
|
+
const COMMIT_CONFIRM = {
|
|
113
|
+
c: (session) => session.startGitCommit('commit'),
|
|
114
|
+
a: (session) => session.startGitCommit('amend'),
|
|
115
|
+
f: (session) => session.startGitCommit('fixup'),
|
|
116
|
+
escape: (session) => session.cancelCommit(),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const COMMIT_STATUS = {
|
|
120
|
+
commit: 'committed',
|
|
121
|
+
amend: 'amended',
|
|
122
|
+
fixup: 'fixup',
|
|
123
|
+
};
|
|
124
|
+
|
|
100
125
|
const COMPOSE_EDITS = ['backspace', 'delete', 'ctrl-z', 'ctrl-y'];
|
|
101
126
|
|
|
102
127
|
const COMPOSE_SPECIAL = {
|
|
@@ -112,7 +137,7 @@ const COMPOSE_SPECIAL = {
|
|
|
112
137
|
'ctrl-e': (session) => session.editor.end(),
|
|
113
138
|
'ctrl-z': (session) => session.editor.undoEdit(),
|
|
114
139
|
'ctrl-y': (session) => session.editor.redoEdit(),
|
|
115
|
-
tab: (session) => session.
|
|
140
|
+
tab: (session) => session.composeTab(),
|
|
116
141
|
};
|
|
117
142
|
|
|
118
143
|
const HANDLERS = {
|
|
@@ -125,6 +150,9 @@ const HANDLERS = {
|
|
|
125
150
|
files: (session) => session.onFiles(),
|
|
126
151
|
feedback: (session) => session.onFeedback(),
|
|
127
152
|
todo: (session) => session.onTodo(),
|
|
153
|
+
code: (session) => session.onCode(),
|
|
154
|
+
reload: (session) => session.onReload(),
|
|
155
|
+
commit: (session) => session.onCommit(),
|
|
128
156
|
open: (session) => session.onOpenFile(),
|
|
129
157
|
removeTodo: (session) => session.removeFocusedTodo(),
|
|
130
158
|
quit: (session) => session.onQuit(),
|
|
@@ -138,9 +166,12 @@ const HANDLERS = {
|
|
|
138
166
|
|
|
139
167
|
const KEY_MODE = {
|
|
140
168
|
confirmQuit: (session, key) => session.onConfirmQuit(key),
|
|
169
|
+
confirmCommit: (session, key) => session.onConfirmCommit(key),
|
|
141
170
|
compose: (session, key) => session.handleComposeKey(key),
|
|
142
171
|
};
|
|
143
172
|
|
|
173
|
+
const COMPOSE_LEAVE = ['next', 'prev', 'todo'];
|
|
174
|
+
|
|
144
175
|
const MOUSE_KIND = {
|
|
145
176
|
wheelUp: (session) => session.onScroll(-1),
|
|
146
177
|
wheelDown: (session) => session.onScroll(1),
|
|
@@ -195,8 +226,10 @@ class Session {
|
|
|
195
226
|
this.readFileSync = options.readFileSync ?? fs.readFileSync;
|
|
196
227
|
this.writeFileSync = options.writeFileSync ?? fs.writeFileSync;
|
|
197
228
|
this.mkdirSync = options.mkdirSync ?? fs.mkdirSync;
|
|
229
|
+
this.unlinkSync = options.unlinkSync ?? fs.unlinkSync;
|
|
198
230
|
this.now = options.now ?? systemNow;
|
|
199
231
|
this.forceNewReview = options.newReview === true;
|
|
232
|
+
this.readOnly = options.readOnly === true;
|
|
200
233
|
this.audit = options.audit === true;
|
|
201
234
|
this.outdatedMap = options.outdatedMap;
|
|
202
235
|
this.auditMap = options.auditMap;
|
|
@@ -230,6 +263,7 @@ class Session {
|
|
|
230
263
|
this.mode = 'review';
|
|
231
264
|
this.editor = null;
|
|
232
265
|
this.composeKind = null;
|
|
266
|
+
this.commitKind = null;
|
|
233
267
|
this.composeTodoId = null;
|
|
234
268
|
this.composeBaseline = '';
|
|
235
269
|
this.todoFocus = 0;
|
|
@@ -524,6 +558,7 @@ class Session {
|
|
|
524
558
|
pr: 0,
|
|
525
559
|
todo: 0,
|
|
526
560
|
feedback: 0,
|
|
561
|
+
code: 0,
|
|
527
562
|
};
|
|
528
563
|
for (const item of this.items) {
|
|
529
564
|
if (!Object.hasOwn(counts, item.origin)) continue;
|
|
@@ -532,13 +567,14 @@ class Session {
|
|
|
532
567
|
const notes = noteCounts(this.notes);
|
|
533
568
|
counts.feedback = notes.feedback;
|
|
534
569
|
counts.todo = notes.todo;
|
|
570
|
+
counts.code = notes.code;
|
|
535
571
|
return counts;
|
|
536
572
|
}
|
|
537
573
|
|
|
538
574
|
view() {
|
|
539
575
|
const templates = this.shownTemplates();
|
|
540
576
|
const change = this.change;
|
|
541
|
-
const
|
|
577
|
+
const sourceKind = (change && SOURCE_KIND[change.source]) || '';
|
|
542
578
|
const repoName = this.repoName || path.basename(this.top || this.cwd || '');
|
|
543
579
|
return {
|
|
544
580
|
item: this.current(),
|
|
@@ -556,13 +592,14 @@ class Session {
|
|
|
556
592
|
repoName,
|
|
557
593
|
revShort: this.revShort || '',
|
|
558
594
|
sourceLabel: this.sourceLabel || '',
|
|
559
|
-
sourceKind
|
|
595
|
+
sourceKind,
|
|
560
596
|
mode: this.mode,
|
|
561
597
|
compose: this.composeView(),
|
|
562
598
|
noteText: this.idleNoteText(),
|
|
563
599
|
todos: this.todoTexts(),
|
|
564
600
|
todoFocus: this.clampedTodoFocus(),
|
|
565
601
|
todoEdit: this.todoEditView(),
|
|
602
|
+
codeOverlay: this.codeOverlayView(),
|
|
566
603
|
templates,
|
|
567
604
|
templateIndex: this.clampedTemplateIndex(templates),
|
|
568
605
|
};
|
|
@@ -595,18 +632,18 @@ class Session {
|
|
|
595
632
|
|
|
596
633
|
dispatch(action) {
|
|
597
634
|
if (this.mode === 'confirmQuit') return;
|
|
635
|
+
if (this.mode === 'confirmCommit') return;
|
|
598
636
|
if (this.mode === 'compose') {
|
|
599
|
-
if (action
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
637
|
+
if (!COMPOSE_LEAVE.includes(action)) return;
|
|
602
638
|
this.commitCompose();
|
|
603
639
|
this.closeCompose();
|
|
604
640
|
}
|
|
605
641
|
if (this.pane === 'files') {
|
|
606
642
|
if (action === 'next') return void this.onFileMove(1);
|
|
607
643
|
if (action === 'prev') return void this.onFileMove(-1);
|
|
608
|
-
if (FILES_DISABLED.includes(action)) return;
|
|
609
644
|
}
|
|
645
|
+
const hidden = disabledActions(this.pane, this.current());
|
|
646
|
+
if (hidden.includes(action)) return;
|
|
610
647
|
const handler = HANDLERS[action];
|
|
611
648
|
if (!handler) return;
|
|
612
649
|
handler(this);
|
|
@@ -616,8 +653,7 @@ class Session {
|
|
|
616
653
|
return this.items[this.index] ?? null;
|
|
617
654
|
}
|
|
618
655
|
|
|
619
|
-
|
|
620
|
-
const here = this.index;
|
|
656
|
+
refreshFromRepo() {
|
|
621
657
|
this.loadPromise = null;
|
|
622
658
|
this.bumpLoad();
|
|
623
659
|
const options = this.loadOptions({
|
|
@@ -625,10 +661,6 @@ class Session {
|
|
|
625
661
|
});
|
|
626
662
|
const loaded = this.repo.load(this.cwd, this.paths, options);
|
|
627
663
|
this.applyLoaded(loaded);
|
|
628
|
-
this.index = here;
|
|
629
|
-
this.clampIndex();
|
|
630
|
-
this.syncReviewPath();
|
|
631
|
-
this.syncFileCursor();
|
|
632
664
|
this.scroll = 0;
|
|
633
665
|
this.clearSelection();
|
|
634
666
|
if (loaded.pending) {
|
|
@@ -639,6 +671,22 @@ class Session {
|
|
|
639
671
|
this.finishIfEmpty('reload');
|
|
640
672
|
}
|
|
641
673
|
|
|
674
|
+
reloadAfterChange() {
|
|
675
|
+
const here = this.index;
|
|
676
|
+
this.refreshFromRepo();
|
|
677
|
+
this.index = here;
|
|
678
|
+
this.clampIndex();
|
|
679
|
+
this.syncReviewPath();
|
|
680
|
+
this.syncFileCursor();
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
onReload() {
|
|
684
|
+
this.dismissed.clear();
|
|
685
|
+
this.status = '';
|
|
686
|
+
this.refreshFromRepo();
|
|
687
|
+
if (!this.done && !this.pendingExtras) this.status = 'reloaded';
|
|
688
|
+
}
|
|
689
|
+
|
|
642
690
|
clearSelection() {
|
|
643
691
|
this.selection = null;
|
|
644
692
|
this.mouseAnchor = null;
|
|
@@ -692,16 +740,20 @@ class Session {
|
|
|
692
740
|
return items;
|
|
693
741
|
}
|
|
694
742
|
|
|
695
|
-
|
|
696
|
-
if (!
|
|
697
|
-
|
|
698
|
-
if (isReadOnlyOrigin(first.origin)) {
|
|
743
|
+
guardWritable(item) {
|
|
744
|
+
if (!item) return false;
|
|
745
|
+
if (this.readOnly || isReadOnlyOrigin(item.origin)) {
|
|
699
746
|
this.status = 'read only';
|
|
700
747
|
return false;
|
|
701
748
|
}
|
|
702
749
|
return true;
|
|
703
750
|
}
|
|
704
751
|
|
|
752
|
+
guardFileGit(items) {
|
|
753
|
+
if (!items.length) return false;
|
|
754
|
+
return this.guardWritable(items[0]);
|
|
755
|
+
}
|
|
756
|
+
|
|
705
757
|
applyFileGit(items, label, write, afterItem) {
|
|
706
758
|
try {
|
|
707
759
|
for (const item of items) {
|
|
@@ -785,7 +837,7 @@ class Session {
|
|
|
785
837
|
let ok = false;
|
|
786
838
|
try {
|
|
787
839
|
this.repo.revertFile(this.top, rel, items);
|
|
788
|
-
this.status = '
|
|
840
|
+
this.status = 'dropped';
|
|
789
841
|
ok = true;
|
|
790
842
|
} catch (error) {
|
|
791
843
|
this.status = error.message;
|
|
@@ -797,11 +849,7 @@ class Session {
|
|
|
797
849
|
onAdd() {
|
|
798
850
|
if (this.pane === 'files') return void this.onFileAdd();
|
|
799
851
|
const item = this.activeDiffItem();
|
|
800
|
-
if (!item) return;
|
|
801
|
-
if (isReadOnlyOrigin(item.origin)) {
|
|
802
|
-
this.status = 'read only';
|
|
803
|
-
return;
|
|
804
|
-
}
|
|
852
|
+
if (!this.guardWritable(item)) return;
|
|
805
853
|
if (item.origin === 'staged') {
|
|
806
854
|
this.status = 'already staged';
|
|
807
855
|
return;
|
|
@@ -823,11 +871,7 @@ class Session {
|
|
|
823
871
|
onUnstage() {
|
|
824
872
|
if (this.pane === 'files') return void this.onFileUnstage();
|
|
825
873
|
const item = this.activeDiffItem();
|
|
826
|
-
if (!item) return;
|
|
827
|
-
if (isReadOnlyOrigin(item.origin)) {
|
|
828
|
-
this.status = 'read only';
|
|
829
|
-
return;
|
|
830
|
-
}
|
|
874
|
+
if (!this.guardWritable(item)) return;
|
|
831
875
|
if (item.origin !== 'staged') {
|
|
832
876
|
this.status = 'not staged';
|
|
833
877
|
return;
|
|
@@ -844,14 +888,10 @@ class Session {
|
|
|
844
888
|
onRevert() {
|
|
845
889
|
if (this.pane === 'files') return void this.onFileRevert();
|
|
846
890
|
const item = this.activeDiffItem();
|
|
847
|
-
if (!item) return;
|
|
848
|
-
if (isReadOnlyOrigin(item.origin)) {
|
|
849
|
-
this.status = 'read only';
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
891
|
+
if (!this.guardWritable(item)) return;
|
|
852
892
|
this.applyGit(
|
|
853
893
|
item,
|
|
854
|
-
'
|
|
894
|
+
'dropped',
|
|
855
895
|
(top, current) => this.repo.revert(top, current),
|
|
856
896
|
() => {
|
|
857
897
|
if (needsReload(item)) this.dismissed.add(itemKey(item));
|
|
@@ -913,6 +953,23 @@ class Session {
|
|
|
913
953
|
this.exitCode = 0;
|
|
914
954
|
}
|
|
915
955
|
|
|
956
|
+
removeReviewFile() {
|
|
957
|
+
const file = this.notes && this.notes.reviewPath;
|
|
958
|
+
if (!file) return;
|
|
959
|
+
try {
|
|
960
|
+
this.unlinkSync(file);
|
|
961
|
+
} catch {
|
|
962
|
+
// ignore missing review file
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
discardQuit() {
|
|
967
|
+
if (this.notes) this.notes.dirty = false;
|
|
968
|
+
if (!this.didResumeReview) this.removeReviewFile();
|
|
969
|
+
this.done = true;
|
|
970
|
+
this.exitCode = 0;
|
|
971
|
+
}
|
|
972
|
+
|
|
916
973
|
cancelQuit() {
|
|
917
974
|
this.mode = 'review';
|
|
918
975
|
this.status = '';
|
|
@@ -924,6 +981,69 @@ class Session {
|
|
|
924
981
|
if (handler) handler(this);
|
|
925
982
|
}
|
|
926
983
|
|
|
984
|
+
canCommit() {
|
|
985
|
+
if (this.readOnly || this.rev || this.change) {
|
|
986
|
+
this.status = 'read only';
|
|
987
|
+
return false;
|
|
988
|
+
}
|
|
989
|
+
for (const item of this.items) {
|
|
990
|
+
if (isReadOnlyOrigin(item.origin)) {
|
|
991
|
+
this.status = 'read only';
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return true;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
onCommit() {
|
|
999
|
+
if (!this.canCommit()) return;
|
|
1000
|
+
this.mode = 'confirmCommit';
|
|
1001
|
+
this.status = '';
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
cancelCommit() {
|
|
1005
|
+
this.mode = 'review';
|
|
1006
|
+
this.commitKind = null;
|
|
1007
|
+
this.status = '';
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
startGitCommit(kind) {
|
|
1011
|
+
this.commitKind = kind;
|
|
1012
|
+
let text = '';
|
|
1013
|
+
if (kind === 'fixup') text = 'HEAD';
|
|
1014
|
+
if (kind === 'amend' && this.repo.lastMessage) {
|
|
1015
|
+
text = this.repo.lastMessage(this.top) ?? '';
|
|
1016
|
+
}
|
|
1017
|
+
this.openCompose('commit', text);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
onConfirmCommit(key) {
|
|
1021
|
+
const commitKey = key.length === 1 ? key.toLowerCase() : key;
|
|
1022
|
+
const handler = COMMIT_CONFIRM[commitKey];
|
|
1023
|
+
if (handler) handler(this);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
finishGitCommit() {
|
|
1027
|
+
const kind = this.commitKind;
|
|
1028
|
+
const message = this.editor ? this.editor.text : '';
|
|
1029
|
+
if (kind !== 'fixup' && !message.trim()) {
|
|
1030
|
+
this.status = 'empty commit message';
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
try {
|
|
1034
|
+
this.repo.commit(this.top, kind, message);
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
this.status = error.message;
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
this.closeCompose();
|
|
1040
|
+
this.dismissed.clear();
|
|
1041
|
+
this.refreshFromRepo();
|
|
1042
|
+
if (!this.done && !this.pendingExtras) {
|
|
1043
|
+
this.status = COMMIT_STATUS[kind] ?? 'committed';
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
927
1047
|
onFeedback() {
|
|
928
1048
|
if (this.pane === 'files') {
|
|
929
1049
|
this.status = 'not a diff block';
|
|
@@ -954,6 +1074,22 @@ class Session {
|
|
|
954
1074
|
this.createTodo(file);
|
|
955
1075
|
}
|
|
956
1076
|
|
|
1077
|
+
onCode() {
|
|
1078
|
+
if (this.pane === 'files') {
|
|
1079
|
+
this.status = 'not a diff block';
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
const item = this.current();
|
|
1083
|
+
if (!canEditCode(item)) {
|
|
1084
|
+
this.status = 'not a diff block';
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
const prev = this.notes.code.get(itemFeedKey(item));
|
|
1088
|
+
const original = blockAddText(item.hunk, item.blockId);
|
|
1089
|
+
const text = prev ? prev.text : original;
|
|
1090
|
+
this.openCompose('code', text);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
957
1093
|
todoPageIndex(file) {
|
|
958
1094
|
return this.items.findIndex(
|
|
959
1095
|
(item) => item.origin === 'todo' && itemPath(item) === file,
|
|
@@ -1086,6 +1222,29 @@ class Session {
|
|
|
1086
1222
|
return;
|
|
1087
1223
|
}
|
|
1088
1224
|
if (this.composeKind === 'todo') this.commitTodo(text);
|
|
1225
|
+
if (this.composeKind === 'code') this.commitCode(text);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
commitCode(text) {
|
|
1229
|
+
const item = this.current();
|
|
1230
|
+
if (!canEditCode(item)) return;
|
|
1231
|
+
const original = blockAddText(item.hunk, item.blockId);
|
|
1232
|
+
const key = itemFeedKey(item);
|
|
1233
|
+
const hunk = item.hunk;
|
|
1234
|
+
const note = {
|
|
1235
|
+
file: itemPath(item),
|
|
1236
|
+
oldStart: hunk.oldStart ?? 0,
|
|
1237
|
+
newStart: hunk.newStart ?? 0,
|
|
1238
|
+
blockId: item.blockId ?? 0,
|
|
1239
|
+
origin: item.origin,
|
|
1240
|
+
header: hunk.header ?? '',
|
|
1241
|
+
text,
|
|
1242
|
+
};
|
|
1243
|
+
if (text === original) {
|
|
1244
|
+
setCode(this.notes, key, { ...note, clear: true });
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
setCode(this.notes, key, note);
|
|
1089
1248
|
}
|
|
1090
1249
|
|
|
1091
1250
|
commitTodo(text) {
|
|
@@ -1116,6 +1275,7 @@ class Session {
|
|
|
1116
1275
|
this.mode = 'review';
|
|
1117
1276
|
this.editor = null;
|
|
1118
1277
|
this.composeKind = null;
|
|
1278
|
+
this.commitKind = null;
|
|
1119
1279
|
this.composeTodoId = null;
|
|
1120
1280
|
this.composeBaseline = '';
|
|
1121
1281
|
this.templateIndex = -1;
|
|
@@ -1176,7 +1336,9 @@ class Session {
|
|
|
1176
1336
|
|
|
1177
1337
|
composeView() {
|
|
1178
1338
|
if (this.mode !== 'compose' || !this.editor) return null;
|
|
1179
|
-
if (this.composeKind !== 'feedback')
|
|
1339
|
+
if (this.composeKind !== 'feedback' && this.composeKind !== 'commit') {
|
|
1340
|
+
return null;
|
|
1341
|
+
}
|
|
1180
1342
|
const kind = this.composeKind;
|
|
1181
1343
|
const text = this.editor.text;
|
|
1182
1344
|
const cursor = this.editor.cursor;
|
|
@@ -1189,6 +1351,34 @@ class Session {
|
|
|
1189
1351
|
return { cursor: this.editor.cursor };
|
|
1190
1352
|
}
|
|
1191
1353
|
|
|
1354
|
+
liveCodeText() {
|
|
1355
|
+
const item = this.current();
|
|
1356
|
+
if (!canEditCode(item)) return null;
|
|
1357
|
+
if (this.mode === 'compose' && this.composeKind === 'code' && this.editor) {
|
|
1358
|
+
return this.editor.text;
|
|
1359
|
+
}
|
|
1360
|
+
if (!this.notes) return null;
|
|
1361
|
+
const note = this.notes.code.get(itemFeedKey(item));
|
|
1362
|
+
if (!note) return null;
|
|
1363
|
+
return note.text;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
codeOverlayView() {
|
|
1367
|
+
const text = this.liveCodeText();
|
|
1368
|
+
if (text === null) return null;
|
|
1369
|
+
const editing = this.mode === 'compose' && this.composeKind === 'code';
|
|
1370
|
+
const cursor = editing && this.editor ? this.editor.cursor : null;
|
|
1371
|
+
return { text, keepEmpty: editing, cursor };
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
composeTab() {
|
|
1375
|
+
if (this.composeKind === 'code') {
|
|
1376
|
+
this.editor.insert('\t');
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
this.applyTemplate();
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1192
1382
|
fileTodos(rel) {
|
|
1193
1383
|
if (!this.notes || !rel) return [];
|
|
1194
1384
|
const todos = [];
|
|
@@ -1270,6 +1460,10 @@ class Session {
|
|
|
1270
1460
|
}
|
|
1271
1461
|
|
|
1272
1462
|
saveCompose() {
|
|
1463
|
+
if (this.composeKind === 'commit') {
|
|
1464
|
+
this.finishGitCommit();
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1273
1467
|
this.commitCompose();
|
|
1274
1468
|
if (this.composeKind === 'feedback') {
|
|
1275
1469
|
const next = this.editor ? this.editor.text : '';
|
|
@@ -1282,7 +1476,10 @@ class Session {
|
|
|
1282
1476
|
|
|
1283
1477
|
autosave() {
|
|
1284
1478
|
const text = this.editor ? this.editor.text : '';
|
|
1285
|
-
|
|
1479
|
+
const editingCode = this.mode === 'compose' && this.composeKind === 'code';
|
|
1480
|
+
if (this.mode === 'compose' && (editingCode || text.trim())) {
|
|
1481
|
+
this.commitCompose();
|
|
1482
|
+
}
|
|
1286
1483
|
if (this.notes && this.notes.dirty) this.flushReview();
|
|
1287
1484
|
}
|
|
1288
1485
|
|
|
@@ -1308,6 +1505,11 @@ class Session {
|
|
|
1308
1505
|
this.moveTodoLine(delta, todoInnerWidth(size.width));
|
|
1309
1506
|
return;
|
|
1310
1507
|
}
|
|
1508
|
+
if (this.composeKind === 'code') {
|
|
1509
|
+
const inner = codeInnerWidth(size.width, this.layout);
|
|
1510
|
+
this.editor.moveLine(delta, inner);
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1311
1513
|
const width = noteInnerWidth(size.width);
|
|
1312
1514
|
const shown = this.shownTemplates();
|
|
1313
1515
|
if (shown.length && this.composeKind === 'feedback') {
|
|
@@ -1357,10 +1559,20 @@ class Session {
|
|
|
1357
1559
|
this.selectTemplate(this.templateIndex);
|
|
1358
1560
|
return;
|
|
1359
1561
|
}
|
|
1562
|
+
if (this.composeKind === 'code') {
|
|
1563
|
+
this.editor.insert('\n');
|
|
1564
|
+
this.resetBlink();
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1360
1567
|
this.saveCompose();
|
|
1361
1568
|
return;
|
|
1362
1569
|
}
|
|
1363
1570
|
if (key === 'escape' || key === 'ctrl-s') {
|
|
1571
|
+
if (this.composeKind === 'commit' && key === 'escape') {
|
|
1572
|
+
this.closeCompose();
|
|
1573
|
+
this.status = '';
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1364
1576
|
this.saveCompose();
|
|
1365
1577
|
return;
|
|
1366
1578
|
}
|
|
@@ -1653,7 +1865,7 @@ class Session {
|
|
|
1653
1865
|
CURSOR_BLINK_MS,
|
|
1654
1866
|
);
|
|
1655
1867
|
this.stdin.on('data', (chunk) => onData(chunk), { signal });
|
|
1656
|
-
|
|
1868
|
+
watchResize(this.stdout, process, () => this.draw(), signal);
|
|
1657
1869
|
});
|
|
1658
1870
|
}
|
|
1659
1871
|
}
|
package/lib/source.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { parseGithubPrUrl } = require('./github.js');
|
|
4
|
+
const { parseGitlabMrUrl } = require('./gitlab.js');
|
|
4
5
|
|
|
5
6
|
const selectChangeSource = (paths) => {
|
|
6
7
|
if (!paths.length) return { kind: 'local', paths };
|
|
7
8
|
const first = paths[0];
|
|
8
9
|
const pr = parseGithubPrUrl(first);
|
|
9
|
-
if (
|
|
10
|
-
|
|
10
|
+
if (pr) return { kind: 'github-pr', pr, paths: paths.slice(1) };
|
|
11
|
+
const mr = parseGitlabMrUrl(first);
|
|
12
|
+
if (mr) return { kind: 'gitlab-mr', mr, paths: paths.slice(1) };
|
|
13
|
+
return { kind: 'local', paths };
|
|
11
14
|
};
|
|
12
15
|
|
|
13
16
|
const nop = () => {};
|
package/lib/sys.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const IS_WIN = process.platform === 'win32';
|
|
7
|
+
|
|
8
|
+
const UNIX_CLIP = [
|
|
9
|
+
{ cmd: 'wl-copy', args: [] },
|
|
10
|
+
{ cmd: 'xclip', args: ['-selection', 'clipboard'] },
|
|
11
|
+
{ cmd: 'xsel', args: ['--clipboard', '--input'] },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const npmBin = (platform = process.platform) =>
|
|
15
|
+
platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
16
|
+
|
|
17
|
+
const spawnBase = (extra = {}) => ({ windowsHide: true, ...extra });
|
|
18
|
+
|
|
19
|
+
const clipTools = (platform = process.platform) => {
|
|
20
|
+
if (platform === 'win32') return [{ cmd: 'clip', args: [] }];
|
|
21
|
+
if (platform === 'darwin') return [{ cmd: 'pbcopy', args: [] }];
|
|
22
|
+
return UNIX_CLIP;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const watchResize = (stdout, proc, onResize, signal) => {
|
|
26
|
+
if (stdout && typeof stdout.on === 'function') {
|
|
27
|
+
stdout.on('resize', onResize, { signal });
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
proc.on('SIGWINCH', onResize, { signal });
|
|
31
|
+
} catch {
|
|
32
|
+
// SIGWINCH is not available on Windows
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const canonPath = (value) => {
|
|
37
|
+
try {
|
|
38
|
+
return fs.realpathSync.native(value);
|
|
39
|
+
} catch {
|
|
40
|
+
return path.resolve(value);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const samePath = (left, right) => {
|
|
45
|
+
if (left === right) return true;
|
|
46
|
+
const a = canonPath(left);
|
|
47
|
+
const b = canonPath(right);
|
|
48
|
+
if (a === b) return true;
|
|
49
|
+
return IS_WIN && a.toLowerCase() === b.toLowerCase();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
IS_WIN,
|
|
54
|
+
npmBin,
|
|
55
|
+
spawnBase,
|
|
56
|
+
clipTools,
|
|
57
|
+
watchResize,
|
|
58
|
+
samePath,
|
|
59
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reslop",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Interactive code review and repair planning for AI-assisted changes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"code-review",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"disable": "node scripts/disable.js",
|
|
54
54
|
"lint": "eslint . && prettier -c \"**/*.js\" \"**/*.json\" \"**/*.md\"",
|
|
55
55
|
"fix": "eslint . --fix && prettier --write \"**/*.js\" \"**/*.json\" \"**/*.md\"",
|
|
56
|
-
"test": "npm run -s lint && node --test
|
|
56
|
+
"test": "npm run -s lint && node --test"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"eslint": "^9.39.4",
|