reslop 0.1.6 → 0.1.7
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 +11 -1
- package/README.md +33 -19
- package/lib/ansi.js +1 -0
- package/lib/files.js +33 -4
- package/lib/git.js +41 -0
- package/lib/keys.js +35 -6
- package/lib/render.js +147 -24
- package/lib/review.js +6 -63
- package/lib/session.js +306 -160
- package/package.json +1 -1
package/lib/session.js
CHANGED
|
@@ -10,22 +10,24 @@ const wrap = require('./wrap.js');
|
|
|
10
10
|
const { extractText, isRange } = require('./select.js');
|
|
11
11
|
const { copyText } = require('./clipboard.js');
|
|
12
12
|
const { watchResize } = require('./sys.js');
|
|
13
|
-
const
|
|
13
|
+
const files = require('./files.js');
|
|
14
|
+
const { fileEntries, itemPath, TODO_FILE } = files;
|
|
15
|
+
const { isTodoItem, isTodosEntry, REPO_TODOS_LABEL, makeTodoItem } = files;
|
|
14
16
|
const { Editor } = require('./editor.js');
|
|
15
17
|
const { refreshIndexPatches, blockAddText } = require('./diff.js');
|
|
16
18
|
const update = require('./update.js');
|
|
17
19
|
const { checkUpdate, installUpdate, markSkipped } = update;
|
|
18
20
|
|
|
19
21
|
const { renderFrame, presentRows, presentCursor } = render;
|
|
20
|
-
const { noteInnerWidth,
|
|
22
|
+
const { noteInnerWidth, codeInnerWidth } = render;
|
|
21
23
|
const { actionFromKey, hitAction, decodeChunk } = keys;
|
|
22
24
|
const { disabledActions } = keys;
|
|
23
|
-
const { cursorInWrap, wrapDoc
|
|
25
|
+
const { cursorInWrap, wrapDoc } = wrap;
|
|
24
26
|
const { AUTOSAVE_MS, listReviewNames, loadTemplates } = review;
|
|
25
27
|
const { resolveReviewPath, loadReview, createStore } = review;
|
|
26
28
|
const { noteCounts, hasNotes, setFeedback, setCode, addTodo } = review;
|
|
27
29
|
const { applyImportedNotes } = review;
|
|
28
|
-
const { setTodoText, removeTodo, flushReview
|
|
30
|
+
const { setTodoText, removeTodo, flushReview } = review;
|
|
29
31
|
const { feedbackKey, checkLabel, rankedTemplates } = review;
|
|
30
32
|
const { prefixTemplates, TEMPLATE_SHOW, rememberTemplate } = review;
|
|
31
33
|
|
|
@@ -72,7 +74,7 @@ const npmBusyKind = (item) => {
|
|
|
72
74
|
const itemKey = (item) => {
|
|
73
75
|
if (!item || !item.file) return '';
|
|
74
76
|
const rel = itemPath(item);
|
|
75
|
-
if (item
|
|
77
|
+
if (isTodoItem(item)) return `todo:${TODO_FILE}`;
|
|
76
78
|
const origin = item.origin ?? '';
|
|
77
79
|
const depId = depChangeKey(item);
|
|
78
80
|
if (depId) return `${origin}:${rel}:dep:${depId}`;
|
|
@@ -87,7 +89,7 @@ const itemKey = (item) => {
|
|
|
87
89
|
};
|
|
88
90
|
|
|
89
91
|
const itemFeedKey = (item) => {
|
|
90
|
-
if (!item || !item.file || item
|
|
92
|
+
if (!item || !item.file || isTodoItem(item)) return '';
|
|
91
93
|
const hunk = item.hunk;
|
|
92
94
|
const depId = depChangeKey(item);
|
|
93
95
|
const file = depId ? `${itemPath(item)}#${depId}` : itemPath(item);
|
|
@@ -98,7 +100,7 @@ const itemFeedKey = (item) => {
|
|
|
98
100
|
};
|
|
99
101
|
|
|
100
102
|
const canEditCode = (item) => {
|
|
101
|
-
if (!item || item
|
|
103
|
+
if (!item || isTodoItem(item)) return false;
|
|
102
104
|
if (!item.hunk || !item.file) return false;
|
|
103
105
|
if (item.file.isBinary) return false;
|
|
104
106
|
return true;
|
|
@@ -115,7 +117,6 @@ const sameCursor = (a, b) => {
|
|
|
115
117
|
const QUIT_CONFIRM = {
|
|
116
118
|
f: (session) => session.finishQuit('ready'),
|
|
117
119
|
c: (session) => session.finishQuit('editing'),
|
|
118
|
-
d: (session) => session.discardQuit(),
|
|
119
120
|
'ctrl-c': (session) => session.finishQuit('ready'),
|
|
120
121
|
escape: (session) => session.cancelQuit(),
|
|
121
122
|
};
|
|
@@ -133,6 +134,12 @@ const UPDATE_CONFIRM = {
|
|
|
133
134
|
escape: (session) => session.declineUpdate(),
|
|
134
135
|
};
|
|
135
136
|
|
|
137
|
+
const DROP_CONFIRM = {
|
|
138
|
+
y: (session) => session.confirmDropBranch(),
|
|
139
|
+
n: (session) => session.cancelDropBranch(),
|
|
140
|
+
escape: (session) => session.cancelDropBranch(),
|
|
141
|
+
};
|
|
142
|
+
|
|
136
143
|
const PUSH_CONFIRM = {
|
|
137
144
|
f: (session) => session.onForcePush(),
|
|
138
145
|
escape: (session) => session.cancelPush(),
|
|
@@ -142,6 +149,7 @@ const CONFIRM_MODE = [
|
|
|
142
149
|
'confirmQuit',
|
|
143
150
|
'confirmCommit',
|
|
144
151
|
'confirmUpdate',
|
|
152
|
+
'confirmDrop',
|
|
145
153
|
'confirmPush',
|
|
146
154
|
];
|
|
147
155
|
|
|
@@ -186,6 +194,8 @@ const HANDLERS = {
|
|
|
186
194
|
pull: (session) => session.onPull(),
|
|
187
195
|
push: (session) => session.onPush(),
|
|
188
196
|
newBranch: (session) => session.onNewBranch(),
|
|
197
|
+
rebase: (session) => session.onRebaseBranch(),
|
|
198
|
+
drop: (session) => session.onDropBranch(),
|
|
189
199
|
open: (session) => session.onOpenFile(),
|
|
190
200
|
removeTodo: (session) => session.removeFocusedTodo(),
|
|
191
201
|
quit: (session) => session.onQuit(),
|
|
@@ -201,11 +211,38 @@ const KEY_MODE = {
|
|
|
201
211
|
confirmQuit: (session, key) => session.onConfirmQuit(key),
|
|
202
212
|
confirmCommit: (session, key) => session.onConfirmCommit(key),
|
|
203
213
|
confirmUpdate: (session, key) => session.onConfirmUpdate(key),
|
|
214
|
+
confirmDrop: (session, key) => session.onConfirmDrop(key),
|
|
204
215
|
confirmPush: (session, key) => session.onConfirmPush(key),
|
|
205
216
|
compose: (session, key) => session.handleComposeKey(key),
|
|
206
217
|
};
|
|
207
218
|
|
|
208
219
|
const COMPOSE_LEAVE = ['next', 'prev', 'todo'];
|
|
220
|
+
const TODO_TYPE_SKIP = [
|
|
221
|
+
'enter',
|
|
222
|
+
'escape',
|
|
223
|
+
'tab',
|
|
224
|
+
'backspace',
|
|
225
|
+
'delete',
|
|
226
|
+
'left',
|
|
227
|
+
'right',
|
|
228
|
+
'up',
|
|
229
|
+
'down',
|
|
230
|
+
'home',
|
|
231
|
+
'end',
|
|
232
|
+
'j',
|
|
233
|
+
'k',
|
|
234
|
+
'n',
|
|
235
|
+
'p',
|
|
236
|
+
'q',
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
const isTodoTypeKey = (key) => {
|
|
240
|
+
if (!key) return false;
|
|
241
|
+
if (TODO_TYPE_SKIP.includes(key)) return false;
|
|
242
|
+
if (key.startsWith('ctrl-')) return false;
|
|
243
|
+
if (key.startsWith('alt-')) return false;
|
|
244
|
+
return key.charCodeAt(0) >= 32;
|
|
245
|
+
};
|
|
209
246
|
|
|
210
247
|
const MOUSE_KIND = {
|
|
211
248
|
wheelUp: (session) => session.onScroll(-1),
|
|
@@ -274,7 +311,6 @@ class Session {
|
|
|
274
311
|
this.readFileSync = options.readFileSync ?? fs.readFileSync;
|
|
275
312
|
this.writeFileSync = options.writeFileSync ?? fs.writeFileSync;
|
|
276
313
|
this.mkdirSync = options.mkdirSync ?? fs.mkdirSync;
|
|
277
|
-
this.unlinkSync = options.unlinkSync ?? fs.unlinkSync;
|
|
278
314
|
this.now = options.now ?? systemNow;
|
|
279
315
|
this.forceNewReview = options.newReview === true;
|
|
280
316
|
this.readOnly = options.readOnly === true;
|
|
@@ -296,6 +332,7 @@ class Session {
|
|
|
296
332
|
this.branch = '';
|
|
297
333
|
this.branches = [];
|
|
298
334
|
this.branchCursor = 0;
|
|
335
|
+
this.dropName = '';
|
|
299
336
|
this.scroll = 0;
|
|
300
337
|
this.status = '';
|
|
301
338
|
this.layout = 'unified';
|
|
@@ -319,7 +356,8 @@ class Session {
|
|
|
319
356
|
this.composeTodoId = null;
|
|
320
357
|
this.composeBaseline = '';
|
|
321
358
|
this.todoFocus = 0;
|
|
322
|
-
this.
|
|
359
|
+
this.todoOpen = false;
|
|
360
|
+
this.todoItem = makeTodoItem();
|
|
323
361
|
this.templateIndex = -1;
|
|
324
362
|
this.templateFocus = false;
|
|
325
363
|
this.saveTimer = null;
|
|
@@ -378,7 +416,8 @@ class Session {
|
|
|
378
416
|
}
|
|
379
417
|
|
|
380
418
|
applyLoaded(loaded) {
|
|
381
|
-
const
|
|
419
|
+
const wasTodo = this.todoOpen === true;
|
|
420
|
+
const here = itemKey(this.current());
|
|
382
421
|
this.top = loaded.top ?? this.cwd;
|
|
383
422
|
const items = loaded.items;
|
|
384
423
|
this.items = items.filter((item) => !this.dismissed.has(itemKey(item)));
|
|
@@ -401,8 +440,8 @@ class Session {
|
|
|
401
440
|
applyImportedNotes(this.notes, imported);
|
|
402
441
|
this.importedRemoteNotes = true;
|
|
403
442
|
}
|
|
404
|
-
this.
|
|
405
|
-
if (here) {
|
|
443
|
+
this.todoOpen = wasTodo;
|
|
444
|
+
if (here && !wasTodo) {
|
|
406
445
|
const idx = this.items.findIndex((item) => itemKey(item) === here);
|
|
407
446
|
if (idx >= 0) this.index = idx;
|
|
408
447
|
}
|
|
@@ -430,6 +469,7 @@ class Session {
|
|
|
430
469
|
|
|
431
470
|
finishIfEmpty(kind) {
|
|
432
471
|
if (this.items.length) return;
|
|
472
|
+
if (this.notes && hasNotes(this.notes)) return;
|
|
433
473
|
if (kind === 'keep') return;
|
|
434
474
|
if (this.notes && hasNotes(this.notes)) this.flushReview();
|
|
435
475
|
this.done = true;
|
|
@@ -625,10 +665,24 @@ class Session {
|
|
|
625
665
|
this.notes = createStore(resolved.reviewPath, templates);
|
|
626
666
|
}
|
|
627
667
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
668
|
+
repoNameOf() {
|
|
669
|
+
return this.repoName || path.basename(this.top || this.cwd || '');
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
todosEntry() {
|
|
673
|
+
const notes = noteCounts(this.notes);
|
|
674
|
+
return {
|
|
675
|
+
path: REPO_TODOS_LABEL,
|
|
676
|
+
kind: 'todos',
|
|
677
|
+
status: 'todos',
|
|
678
|
+
remaining: notes.todo,
|
|
679
|
+
staged: notes.todoDone,
|
|
680
|
+
firstIndex: 0,
|
|
681
|
+
openIndex: 0,
|
|
682
|
+
added: 0,
|
|
683
|
+
removed: 0,
|
|
684
|
+
unstaged: 0,
|
|
685
|
+
};
|
|
632
686
|
}
|
|
633
687
|
|
|
634
688
|
clampIndex() {
|
|
@@ -644,10 +698,10 @@ class Session {
|
|
|
644
698
|
|
|
645
699
|
fileList() {
|
|
646
700
|
const entries = fileEntries(this.items);
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
return
|
|
701
|
+
const labeled = this.sourceLabel
|
|
702
|
+
? relabelStatus(entries, 'pr', this.sourceLabel)
|
|
703
|
+
: relabelStatus(entries, 'commit', this.revShort);
|
|
704
|
+
return [this.todosEntry(), ...labeled];
|
|
651
705
|
}
|
|
652
706
|
|
|
653
707
|
syncReviewPath() {
|
|
@@ -673,8 +727,12 @@ class Session {
|
|
|
673
727
|
this.fileCursor = 0;
|
|
674
728
|
return;
|
|
675
729
|
}
|
|
676
|
-
const
|
|
677
|
-
const idx =
|
|
730
|
+
const item = this.current();
|
|
731
|
+
const idx = isTodoItem(item)
|
|
732
|
+
? files.findIndex((entry) => isTodosEntry(entry))
|
|
733
|
+
: files.findIndex(
|
|
734
|
+
(entry) => !isTodosEntry(entry) && entry.path === this.reviewPath,
|
|
735
|
+
);
|
|
678
736
|
if (idx >= 0) {
|
|
679
737
|
this.fileCursor = idx;
|
|
680
738
|
return;
|
|
@@ -716,7 +774,7 @@ class Session {
|
|
|
716
774
|
const templates = this.shownTemplates();
|
|
717
775
|
const change = this.change;
|
|
718
776
|
const sourceKind = (change && SOURCE_KIND[change.source]) || '';
|
|
719
|
-
const repoName = this.
|
|
777
|
+
const repoName = this.repoNameOf();
|
|
720
778
|
return {
|
|
721
779
|
item: this.current(),
|
|
722
780
|
index: this.index,
|
|
@@ -749,6 +807,7 @@ class Session {
|
|
|
749
807
|
templateIndex: this.clampedTemplateIndex(templates),
|
|
750
808
|
updateFrom: this.updateFrom,
|
|
751
809
|
updateTo: this.updateTo,
|
|
810
|
+
dropName: this.dropName,
|
|
752
811
|
};
|
|
753
812
|
}
|
|
754
813
|
|
|
@@ -793,7 +852,10 @@ class Session {
|
|
|
793
852
|
if (action === 'next') return void this.onBranchMove(1);
|
|
794
853
|
if (action === 'prev') return void this.onBranchMove(-1);
|
|
795
854
|
}
|
|
796
|
-
|
|
855
|
+
let target = this.current();
|
|
856
|
+
if (this.pane === 'files') target = this.fileCursorEntry();
|
|
857
|
+
if (this.pane === 'branches') target = this.branches[this.branchCursor];
|
|
858
|
+
const hidden = disabledActions(this.pane, target);
|
|
797
859
|
if (hidden.includes(action)) return;
|
|
798
860
|
const handler = HANDLERS[action];
|
|
799
861
|
if (!handler) return;
|
|
@@ -801,6 +863,7 @@ class Session {
|
|
|
801
863
|
}
|
|
802
864
|
|
|
803
865
|
current() {
|
|
866
|
+
if (this.todoOpen) return this.todoItem;
|
|
804
867
|
return this.items[this.index] ?? null;
|
|
805
868
|
}
|
|
806
869
|
|
|
@@ -920,7 +983,7 @@ class Session {
|
|
|
920
983
|
activeDiffItem() {
|
|
921
984
|
const item = this.current();
|
|
922
985
|
if (!item) return null;
|
|
923
|
-
if (item
|
|
986
|
+
if (isTodoItem(item)) {
|
|
924
987
|
this.status = 'not a diff block';
|
|
925
988
|
return null;
|
|
926
989
|
}
|
|
@@ -934,11 +997,11 @@ class Session {
|
|
|
934
997
|
|
|
935
998
|
fileActionItems() {
|
|
936
999
|
const entry = this.fileCursorEntry();
|
|
937
|
-
if (!entry) return [];
|
|
1000
|
+
if (!entry || isTodosEntry(entry)) return [];
|
|
938
1001
|
const rel = entry.path;
|
|
939
1002
|
const items = [];
|
|
940
1003
|
for (const item of this.items) {
|
|
941
|
-
if (item
|
|
1004
|
+
if (isTodoItem(item)) continue;
|
|
942
1005
|
if (itemPath(item) !== rel) continue;
|
|
943
1006
|
items.push(item);
|
|
944
1007
|
}
|
|
@@ -991,6 +1054,10 @@ class Session {
|
|
|
991
1054
|
|
|
992
1055
|
onFileAdd() {
|
|
993
1056
|
const items = this.fileActionItems();
|
|
1057
|
+
if (!items.length) {
|
|
1058
|
+
this.status = 'not a diff block';
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
994
1061
|
if (!this.guardFileGit(items)) return;
|
|
995
1062
|
const rel = itemPath(items[0]);
|
|
996
1063
|
const targets = items.filter((item) => item.origin !== 'staged');
|
|
@@ -1029,6 +1096,10 @@ class Session {
|
|
|
1029
1096
|
|
|
1030
1097
|
onFileUnstage() {
|
|
1031
1098
|
const items = this.fileActionItems();
|
|
1099
|
+
if (!items.length) {
|
|
1100
|
+
this.status = 'not a diff block';
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1032
1103
|
if (!this.guardFileGit(items)) return;
|
|
1033
1104
|
const rel = itemPath(items[0]);
|
|
1034
1105
|
const targets = items.filter((item) => item.origin === 'staged');
|
|
@@ -1051,6 +1122,10 @@ class Session {
|
|
|
1051
1122
|
|
|
1052
1123
|
onFileRevert() {
|
|
1053
1124
|
const items = this.fileActionItems();
|
|
1125
|
+
if (!items.length) {
|
|
1126
|
+
this.status = 'not a diff block';
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1054
1129
|
if (!this.guardFileGit(items)) return;
|
|
1055
1130
|
const rel = itemPath(items[0]);
|
|
1056
1131
|
for (const item of items) {
|
|
@@ -1145,10 +1220,23 @@ class Session {
|
|
|
1145
1220
|
}
|
|
1146
1221
|
|
|
1147
1222
|
onNext() {
|
|
1223
|
+
if (this.todoOpen) {
|
|
1224
|
+
this.closeTodoPage();
|
|
1225
|
+
if (this.items.length) this.index = 0;
|
|
1226
|
+
this.scroll = 0;
|
|
1227
|
+
this.syncReviewPath();
|
|
1228
|
+
this.syncFileCursor();
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1148
1231
|
this.moveRemaining(1);
|
|
1149
1232
|
}
|
|
1150
1233
|
|
|
1151
1234
|
onPrev() {
|
|
1235
|
+
if (this.todoOpen) return;
|
|
1236
|
+
if (this.index <= 0) {
|
|
1237
|
+
this.openTodoPage();
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1152
1240
|
this.moveRemaining(-1);
|
|
1153
1241
|
}
|
|
1154
1242
|
|
|
@@ -1176,23 +1264,6 @@ class Session {
|
|
|
1176
1264
|
this.exitCode = 0;
|
|
1177
1265
|
}
|
|
1178
1266
|
|
|
1179
|
-
removeReviewFile() {
|
|
1180
|
-
const file = this.notes && this.notes.reviewPath;
|
|
1181
|
-
if (!file) return;
|
|
1182
|
-
try {
|
|
1183
|
-
this.unlinkSync(file);
|
|
1184
|
-
} catch {
|
|
1185
|
-
// ignore missing review file
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
discardQuit() {
|
|
1190
|
-
if (this.notes) this.notes.dirty = false;
|
|
1191
|
-
if (!this.didResumeReview) this.removeReviewFile();
|
|
1192
|
-
this.done = true;
|
|
1193
|
-
this.exitCode = 0;
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
1267
|
cancelQuit() {
|
|
1197
1268
|
this.mode = 'review';
|
|
1198
1269
|
this.status = '';
|
|
@@ -1426,6 +1497,83 @@ class Session {
|
|
|
1426
1497
|
if (!this.status) this.status = done;
|
|
1427
1498
|
}
|
|
1428
1499
|
|
|
1500
|
+
refreshBranchList() {
|
|
1501
|
+
if (!this.repo.listBranches) return;
|
|
1502
|
+
try {
|
|
1503
|
+
this.branches = this.repo.listBranches(this.top);
|
|
1504
|
+
} catch (error) {
|
|
1505
|
+
this.status = error.message;
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
const last = this.branches.length - 1;
|
|
1509
|
+
if (this.branchCursor > last) this.branchCursor = Math.max(0, last);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
onRebaseBranch() {
|
|
1513
|
+
if (this.pane !== 'branches') return;
|
|
1514
|
+
if (!this.canCommit()) return;
|
|
1515
|
+
const entry = this.branches[this.branchCursor];
|
|
1516
|
+
if (!entry || entry.current) return;
|
|
1517
|
+
const onto = entry.name;
|
|
1518
|
+
const done = `rebased onto ${onto}`;
|
|
1519
|
+
if (this.uiOpen && this.repo.rebaseAsync) {
|
|
1520
|
+
const write = (top) => this.repo.rebaseAsync(top, onto);
|
|
1521
|
+
const after = () => {
|
|
1522
|
+
this.refreshBranchList();
|
|
1523
|
+
this.refreshFromRepo({ keepEmpty: true, doneStatus: done });
|
|
1524
|
+
};
|
|
1525
|
+
return void this.runGitBusy('rebasing', done, write, after);
|
|
1526
|
+
}
|
|
1527
|
+
const ok = this.runRepo(done, (top) => this.repo.rebase(top, onto));
|
|
1528
|
+
if (!ok) return;
|
|
1529
|
+
this.refreshBranchList();
|
|
1530
|
+
this.refreshFromRepo({ keepEmpty: true });
|
|
1531
|
+
if (!this.status) this.status = done;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
onDropBranch() {
|
|
1535
|
+
if (this.pane !== 'branches') return;
|
|
1536
|
+
if (!this.canCommit()) return;
|
|
1537
|
+
const entry = this.branches[this.branchCursor];
|
|
1538
|
+
if (!entry || entry.current) return;
|
|
1539
|
+
this.dropName = entry.name;
|
|
1540
|
+
this.mode = 'confirmDrop';
|
|
1541
|
+
this.status = '';
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
onConfirmDrop(key) {
|
|
1545
|
+
const dropKey = key.length === 1 ? key.toLowerCase() : key;
|
|
1546
|
+
const handler = DROP_CONFIRM[dropKey];
|
|
1547
|
+
if (handler) handler(this);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
cancelDropBranch() {
|
|
1551
|
+
this.mode = 'review';
|
|
1552
|
+
this.dropName = '';
|
|
1553
|
+
this.status = '';
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
confirmDropBranch() {
|
|
1557
|
+
const name = this.dropName;
|
|
1558
|
+
this.mode = 'review';
|
|
1559
|
+
this.dropName = '';
|
|
1560
|
+
if (!name) return;
|
|
1561
|
+
const done = `dropped ${name}`;
|
|
1562
|
+
if (this.uiOpen && this.repo.dropAsync) {
|
|
1563
|
+
const write = (top) => this.repo.dropAsync(top, name);
|
|
1564
|
+
const after = () => {
|
|
1565
|
+
this.refreshBranchList();
|
|
1566
|
+
this.refreshFromRepo({ keepEmpty: true, doneStatus: done });
|
|
1567
|
+
};
|
|
1568
|
+
return void this.runGitBusy('dropping', done, write, after);
|
|
1569
|
+
}
|
|
1570
|
+
const ok = this.runRepo(done, (top) => this.repo.drop(top, name));
|
|
1571
|
+
if (!ok) return;
|
|
1572
|
+
this.refreshBranchList();
|
|
1573
|
+
this.refreshFromRepo({ keepEmpty: true });
|
|
1574
|
+
if (!this.status) this.status = done;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1429
1577
|
onNewBranch() {
|
|
1430
1578
|
if (this.pane !== 'branches') return;
|
|
1431
1579
|
if (!this.canCommit()) return;
|
|
@@ -1518,7 +1666,7 @@ class Session {
|
|
|
1518
1666
|
return;
|
|
1519
1667
|
}
|
|
1520
1668
|
const item = this.current();
|
|
1521
|
-
if (!item || item
|
|
1669
|
+
if (!item || isTodoItem(item)) {
|
|
1522
1670
|
this.status = 'not a diff block';
|
|
1523
1671
|
return;
|
|
1524
1672
|
}
|
|
@@ -1527,19 +1675,14 @@ class Session {
|
|
|
1527
1675
|
this.openCompose('feedback', text);
|
|
1528
1676
|
}
|
|
1529
1677
|
|
|
1530
|
-
|
|
1678
|
+
onTodo() {
|
|
1531
1679
|
if (this.pane === 'files') {
|
|
1532
1680
|
const entry = this.fileCursorEntry();
|
|
1533
|
-
|
|
1681
|
+
if (entry && !isTodosEntry(entry)) {
|
|
1682
|
+
this.index = entry.openIndex ?? entry.firstIndex;
|
|
1683
|
+
}
|
|
1534
1684
|
}
|
|
1535
|
-
|
|
1536
|
-
return item ? itemPath(item) : '';
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
onTodo() {
|
|
1540
|
-
const file = this.todoFile();
|
|
1541
|
-
if (!file) return;
|
|
1542
|
-
this.createTodo(file);
|
|
1685
|
+
this.createTodo();
|
|
1543
1686
|
}
|
|
1544
1687
|
|
|
1545
1688
|
onCode() {
|
|
@@ -1558,17 +1701,9 @@ class Session {
|
|
|
1558
1701
|
this.openCompose('code', text);
|
|
1559
1702
|
}
|
|
1560
1703
|
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
);
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
focusTodoPage(file, todoId) {
|
|
1568
|
-
const idx = this.todoPageIndex(file);
|
|
1569
|
-
if (idx < 0) return false;
|
|
1570
|
-
this.index = idx;
|
|
1571
|
-
const todos = this.fileTodos(file);
|
|
1704
|
+
focusTodoPage(todoId) {
|
|
1705
|
+
this.openTodoPage();
|
|
1706
|
+
const todos = this.listTodos();
|
|
1572
1707
|
let focus = 0;
|
|
1573
1708
|
if (todoId === null) {
|
|
1574
1709
|
focus = todos.length;
|
|
@@ -1590,53 +1725,86 @@ class Session {
|
|
|
1590
1725
|
);
|
|
1591
1726
|
}
|
|
1592
1727
|
|
|
1593
|
-
|
|
1594
|
-
this.
|
|
1595
|
-
this.
|
|
1596
|
-
this.
|
|
1728
|
+
openTodoPage() {
|
|
1729
|
+
this.todoOpen = true;
|
|
1730
|
+
this.pane = 'diff';
|
|
1731
|
+
this.scroll = 0;
|
|
1732
|
+
this.clearSelection();
|
|
1733
|
+
this.syncReviewPath();
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
closeTodoPage() {
|
|
1737
|
+
this.todoOpen = false;
|
|
1597
1738
|
this.syncReviewPath();
|
|
1739
|
+
this.syncFileCursor();
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
startDraftCompose() {
|
|
1743
|
+
this.focusTodoPage(null);
|
|
1598
1744
|
this.openCompose('todo', '', null);
|
|
1599
1745
|
}
|
|
1600
1746
|
|
|
1601
1747
|
editFocusedTodo() {
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
const todos = this.fileTodos(itemPath(item));
|
|
1748
|
+
if (!this.todoOpen) return;
|
|
1749
|
+
const todos = this.listTodos();
|
|
1605
1750
|
const focus = this.clampedTodoFocus();
|
|
1606
1751
|
const todo = todos[focus];
|
|
1607
1752
|
if (!todo) {
|
|
1608
|
-
this.startDraftCompose(
|
|
1753
|
+
this.startDraftCompose();
|
|
1609
1754
|
return;
|
|
1610
1755
|
}
|
|
1611
1756
|
this.openCompose('todo', todo.text, todo.id);
|
|
1612
1757
|
}
|
|
1613
1758
|
|
|
1759
|
+
editNextTodo() {
|
|
1760
|
+
if (!this.todoOpen) return;
|
|
1761
|
+
const last = this.todoRowCount() - 1;
|
|
1762
|
+
const next = this.clampedTodoFocus() + 1;
|
|
1763
|
+
if (next <= last) this.todoFocus = next;
|
|
1764
|
+
this.editFocusedTodo();
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
moveTodoCompose(delta) {
|
|
1768
|
+
if (!this.todoOpen) return;
|
|
1769
|
+
const last = this.todoRowCount() - 1;
|
|
1770
|
+
const next = this.clampedTodoFocus() + delta;
|
|
1771
|
+
if (next < 0 || next > last) return;
|
|
1772
|
+
this.commitCompose();
|
|
1773
|
+
this.flushReview();
|
|
1774
|
+
this.closeCompose();
|
|
1775
|
+
const max = Math.max(0, this.todoRowCount() - 1);
|
|
1776
|
+
this.todoFocus = Math.min(next, max);
|
|
1777
|
+
this.editFocusedTodo();
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
typeIntoTodo(key) {
|
|
1781
|
+
if (!this.todoOpen) return false;
|
|
1782
|
+
if (this.pane !== 'diff') return false;
|
|
1783
|
+
if (this.mode !== 'review') return false;
|
|
1784
|
+
if (!isTodoTypeKey(key)) return false;
|
|
1785
|
+
this.editFocusedTodo();
|
|
1786
|
+
this.editor.insert(key);
|
|
1787
|
+
this.resetBlink();
|
|
1788
|
+
return true;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1614
1791
|
removeFocusedTodo() {
|
|
1615
|
-
if (this.pane !== 'diff') return;
|
|
1616
|
-
const
|
|
1617
|
-
if (!item || item.origin !== 'todo') return;
|
|
1618
|
-
const file = itemPath(item);
|
|
1619
|
-
const todos = this.fileTodos(file);
|
|
1792
|
+
if (this.pane !== 'diff' || !this.todoOpen) return;
|
|
1793
|
+
const todos = this.listTodos();
|
|
1620
1794
|
const focus = this.clampedTodoFocus();
|
|
1621
1795
|
const todo = todos[focus];
|
|
1622
1796
|
if (!todo) return;
|
|
1623
1797
|
removeTodo(this.notes, todo.id);
|
|
1624
|
-
this.
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
this.clampIndex();
|
|
1628
|
-
} else if (this.todoFocus >= left.length) {
|
|
1629
|
-
this.todoFocus = left.length - 1;
|
|
1798
|
+
const left = this.listTodos();
|
|
1799
|
+
if (this.todoFocus >= left.length) {
|
|
1800
|
+
this.todoFocus = Math.max(0, left.length - 1);
|
|
1630
1801
|
}
|
|
1631
|
-
this.syncReviewPath();
|
|
1632
|
-
this.syncFileCursor();
|
|
1633
1802
|
this.flushReview(true);
|
|
1634
1803
|
}
|
|
1635
1804
|
|
|
1636
1805
|
onTodoHit(cursor) {
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
const todos = this.fileTodos(itemPath(item));
|
|
1806
|
+
if (!this.todoOpen) return;
|
|
1807
|
+
const todos = this.listTodos();
|
|
1640
1808
|
const draft = cursor === todos.length;
|
|
1641
1809
|
const target = todos[cursor];
|
|
1642
1810
|
if (!draft && !target) return;
|
|
@@ -1650,12 +1818,8 @@ class Session {
|
|
|
1650
1818
|
this.editFocusedTodo();
|
|
1651
1819
|
}
|
|
1652
1820
|
|
|
1653
|
-
createTodo(
|
|
1654
|
-
|
|
1655
|
-
this.pane = 'diff';
|
|
1656
|
-
this.scroll = 0;
|
|
1657
|
-
this.clearSelection();
|
|
1658
|
-
this.startDraftCompose(file);
|
|
1821
|
+
createTodo() {
|
|
1822
|
+
this.startDraftCompose();
|
|
1659
1823
|
}
|
|
1660
1824
|
|
|
1661
1825
|
openCompose(kind, text, todoId) {
|
|
@@ -1676,7 +1840,7 @@ class Session {
|
|
|
1676
1840
|
const text = this.editor.text;
|
|
1677
1841
|
if (this.composeKind === 'feedback') {
|
|
1678
1842
|
const item = this.current();
|
|
1679
|
-
if (!item || item
|
|
1843
|
+
if (!item || isTodoItem(item)) return;
|
|
1680
1844
|
const hunk = item.hunk;
|
|
1681
1845
|
setFeedback(this.notes, itemFeedKey(item), {
|
|
1682
1846
|
file: itemPath(item),
|
|
@@ -1719,24 +1883,13 @@ class Session {
|
|
|
1719
1883
|
const id = this.composeTodoId;
|
|
1720
1884
|
const todos = this.notes.todos;
|
|
1721
1885
|
const prev = id === null ? null : todos.find((entry) => entry.id === id);
|
|
1722
|
-
const current = this.current();
|
|
1723
|
-
const file = prev ? prev.file : itemPath(current) || this.todoDraftFile;
|
|
1724
1886
|
if (prev) {
|
|
1725
1887
|
setTodoText(this.notes, id, text);
|
|
1726
|
-
|
|
1727
|
-
const todo = addTodo(this.notes, file, text);
|
|
1728
|
-
this.composeTodoId = todo.id;
|
|
1729
|
-
}
|
|
1730
|
-
this.injectTodos();
|
|
1731
|
-
if (this.focusTodoPage(file, this.composeTodoId)) return;
|
|
1732
|
-
const hunkIdx = this.items.findIndex(
|
|
1733
|
-
(item) => item.origin !== 'todo' && itemPath(item) === file,
|
|
1734
|
-
);
|
|
1735
|
-
if (hunkIdx >= 0) {
|
|
1736
|
-
this.index = hunkIdx;
|
|
1737
|
-
} else if (this.index >= this.items.length) {
|
|
1738
|
-
this.index = Math.max(0, this.items.length - 1);
|
|
1888
|
+
return;
|
|
1739
1889
|
}
|
|
1890
|
+
if (!text.trim()) return;
|
|
1891
|
+
const todo = addTodo(this.notes, TODO_FILE, text);
|
|
1892
|
+
this.composeTodoId = todo.id;
|
|
1740
1893
|
}
|
|
1741
1894
|
|
|
1742
1895
|
closeCompose() {
|
|
@@ -1748,8 +1901,6 @@ class Session {
|
|
|
1748
1901
|
this.composeBaseline = '';
|
|
1749
1902
|
this.templateIndex = -1;
|
|
1750
1903
|
this.templateFocus = false;
|
|
1751
|
-
this.todoDraftFile = null;
|
|
1752
|
-
this.injectTodos();
|
|
1753
1904
|
this.clampIndex();
|
|
1754
1905
|
this.syncReviewPath();
|
|
1755
1906
|
this.syncFileCursor();
|
|
@@ -1846,23 +1997,17 @@ class Session {
|
|
|
1846
1997
|
this.applyTemplate();
|
|
1847
1998
|
}
|
|
1848
1999
|
|
|
1849
|
-
|
|
1850
|
-
if (!this.notes
|
|
1851
|
-
|
|
1852
|
-
for (const todo of this.notes.todos) {
|
|
1853
|
-
if (todo.file !== rel) continue;
|
|
1854
|
-
todos.push(todo);
|
|
1855
|
-
}
|
|
1856
|
-
return todos;
|
|
2000
|
+
listTodos() {
|
|
2001
|
+
if (!this.notes) return [];
|
|
2002
|
+
return this.notes.todos;
|
|
1857
2003
|
}
|
|
1858
2004
|
|
|
1859
2005
|
todoTexts() {
|
|
1860
|
-
|
|
1861
|
-
if (!item || item.origin !== 'todo') return [];
|
|
2006
|
+
if (!this.todoOpen) return [];
|
|
1862
2007
|
const texts = [];
|
|
1863
2008
|
const editingTodo = this.mode === 'compose' && this.composeKind === 'todo';
|
|
1864
2009
|
const editingId = editingTodo ? this.composeTodoId : null;
|
|
1865
|
-
for (const todo of this.
|
|
2010
|
+
for (const todo of this.listTodos()) {
|
|
1866
2011
|
const live = editingId === todo.id && this.editor;
|
|
1867
2012
|
const text = live ? this.editor.text : todo.text;
|
|
1868
2013
|
texts.push(checkLabel(text, todo.done));
|
|
@@ -1873,9 +2018,8 @@ class Session {
|
|
|
1873
2018
|
}
|
|
1874
2019
|
|
|
1875
2020
|
focusedTodo() {
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
const todos = this.fileTodos(itemPath(item));
|
|
2021
|
+
if (!this.todoOpen) return null;
|
|
2022
|
+
const todos = this.listTodos();
|
|
1879
2023
|
return todos[this.clampedTodoFocus()] ?? null;
|
|
1880
2024
|
}
|
|
1881
2025
|
|
|
@@ -1887,14 +2031,12 @@ class Session {
|
|
|
1887
2031
|
}
|
|
1888
2032
|
|
|
1889
2033
|
todoRowCount() {
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
return this.fileTodos(itemPath(item)).length + 1;
|
|
2034
|
+
if (!this.todoOpen) return 0;
|
|
2035
|
+
return this.listTodos().length + 1;
|
|
1893
2036
|
}
|
|
1894
2037
|
|
|
1895
2038
|
moveTodoFocus(delta) {
|
|
1896
|
-
|
|
1897
|
-
if (!item || item.origin !== 'todo') return;
|
|
2039
|
+
if (!this.todoOpen) return;
|
|
1898
2040
|
const n = this.todoRowCount();
|
|
1899
2041
|
if (!n) return;
|
|
1900
2042
|
const next = this.clampedTodoFocus() + delta;
|
|
@@ -1907,7 +2049,7 @@ class Session {
|
|
|
1907
2049
|
if (!this.notes) return '';
|
|
1908
2050
|
if (this.mode === 'compose') return '';
|
|
1909
2051
|
const item = this.current();
|
|
1910
|
-
if (!item || item
|
|
2052
|
+
if (!item || isTodoItem(item)) return '';
|
|
1911
2053
|
const note = this.notes.feedback.get(itemFeedKey(item));
|
|
1912
2054
|
if (!note || !note.text.trim()) return '';
|
|
1913
2055
|
return checkLabel(note.text, note.done);
|
|
@@ -1926,7 +2068,8 @@ class Session {
|
|
|
1926
2068
|
}
|
|
1927
2069
|
}
|
|
1928
2070
|
|
|
1929
|
-
saveCompose() {
|
|
2071
|
+
saveCompose(options) {
|
|
2072
|
+
const opts = options ?? {};
|
|
1930
2073
|
if (this.composeKind === 'commit') {
|
|
1931
2074
|
this.finishGitCommit();
|
|
1932
2075
|
return;
|
|
@@ -1935,6 +2078,7 @@ class Session {
|
|
|
1935
2078
|
this.finishCreateBranch();
|
|
1936
2079
|
return;
|
|
1937
2080
|
}
|
|
2081
|
+
const editNext = opts.editNext === true;
|
|
1938
2082
|
this.commitCompose();
|
|
1939
2083
|
if (this.composeKind === 'feedback') {
|
|
1940
2084
|
const next = this.editor ? this.editor.text : '';
|
|
@@ -1943,6 +2087,7 @@ class Session {
|
|
|
1943
2087
|
this.flushReview();
|
|
1944
2088
|
this.closeCompose();
|
|
1945
2089
|
this.status = 'saved';
|
|
2090
|
+
if (editNext) this.editNextTodo();
|
|
1946
2091
|
}
|
|
1947
2092
|
|
|
1948
2093
|
autosave() {
|
|
@@ -1954,26 +2099,10 @@ class Session {
|
|
|
1954
2099
|
if (this.notes && this.notes.dirty) this.flushReview();
|
|
1955
2100
|
}
|
|
1956
2101
|
|
|
1957
|
-
moveTodoLine(delta, inner) {
|
|
1958
|
-
const todo = this.focusedTodo();
|
|
1959
|
-
const done = todo ? todo.done === true : false;
|
|
1960
|
-
const label = checkLabel(this.editor.text, done);
|
|
1961
|
-
const prefix = checkLabel('', done).length;
|
|
1962
|
-
const col = this.editor.wantCol;
|
|
1963
|
-
const at = prefix + this.editor.cursor;
|
|
1964
|
-
const moved = wrapMove(label, at, inner, delta, col);
|
|
1965
|
-
const next = moved.cursor - prefix;
|
|
1966
|
-
const max = this.editor.text.length;
|
|
1967
|
-
this.editor.wantCol = moved.col;
|
|
1968
|
-
if (next < 0) this.editor.cursor = 0;
|
|
1969
|
-
else if (next > max) this.editor.cursor = max;
|
|
1970
|
-
else this.editor.cursor = next;
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
2102
|
moveComposeLine(delta) {
|
|
1974
2103
|
const size = this.lastSize ?? this.getSize();
|
|
1975
2104
|
if (this.composeKind === 'todo') {
|
|
1976
|
-
this.
|
|
2105
|
+
this.moveTodoCompose(delta);
|
|
1977
2106
|
return;
|
|
1978
2107
|
}
|
|
1979
2108
|
if (this.composeKind === 'code') {
|
|
@@ -2035,6 +2164,10 @@ class Session {
|
|
|
2035
2164
|
this.resetBlink();
|
|
2036
2165
|
return;
|
|
2037
2166
|
}
|
|
2167
|
+
if (this.composeKind === 'todo') {
|
|
2168
|
+
this.saveCompose({ editNext: true });
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2038
2171
|
this.saveCompose();
|
|
2039
2172
|
return;
|
|
2040
2173
|
}
|
|
@@ -2067,6 +2200,7 @@ class Session {
|
|
|
2067
2200
|
this.pane = 'files';
|
|
2068
2201
|
this.clearSelection();
|
|
2069
2202
|
this.followReviewPath();
|
|
2203
|
+
this.todoOpen = false;
|
|
2070
2204
|
}
|
|
2071
2205
|
|
|
2072
2206
|
onEscape() {
|
|
@@ -2086,6 +2220,7 @@ class Session {
|
|
|
2086
2220
|
if (!this.reviewPath) return;
|
|
2087
2221
|
this.pane = 'diff';
|
|
2088
2222
|
this.clearSelection();
|
|
2223
|
+
if (this.reviewPath === TODO_FILE) this.openTodoPage();
|
|
2089
2224
|
return;
|
|
2090
2225
|
}
|
|
2091
2226
|
this.showFiles();
|
|
@@ -2094,13 +2229,18 @@ class Session {
|
|
|
2094
2229
|
onOpenFile() {
|
|
2095
2230
|
if (this.pane === 'branches') return void this.onCheckoutBranch();
|
|
2096
2231
|
if (this.pane !== 'files') {
|
|
2097
|
-
|
|
2098
|
-
if (item && item.origin === 'todo') this.editFocusedTodo();
|
|
2232
|
+
if (this.todoOpen) this.editFocusedTodo();
|
|
2099
2233
|
return;
|
|
2100
2234
|
}
|
|
2101
2235
|
const files = this.fileList();
|
|
2102
2236
|
const entry = files[this.fileCursor];
|
|
2103
2237
|
if (!entry) return;
|
|
2238
|
+
if (isTodosEntry(entry)) {
|
|
2239
|
+
this.openTodoPage();
|
|
2240
|
+
this.status = '';
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
this.todoOpen = false;
|
|
2104
2244
|
this.index = entry.openIndex ?? entry.firstIndex;
|
|
2105
2245
|
this.pane = 'diff';
|
|
2106
2246
|
this.scroll = 0;
|
|
@@ -2122,8 +2262,12 @@ class Session {
|
|
|
2122
2262
|
if (next === this.fileCursor) return;
|
|
2123
2263
|
this.fileCursor = next;
|
|
2124
2264
|
const entry = files[next];
|
|
2125
|
-
|
|
2126
|
-
|
|
2265
|
+
if (isTodosEntry(entry)) {
|
|
2266
|
+
this.reviewPath = TODO_FILE;
|
|
2267
|
+
} else {
|
|
2268
|
+
this.reviewPath = entry.path;
|
|
2269
|
+
this.index = entry.firstIndex;
|
|
2270
|
+
}
|
|
2127
2271
|
this.status = '';
|
|
2128
2272
|
}
|
|
2129
2273
|
|
|
@@ -2146,8 +2290,7 @@ class Session {
|
|
|
2146
2290
|
this.onBranchMove(delta);
|
|
2147
2291
|
return;
|
|
2148
2292
|
}
|
|
2149
|
-
|
|
2150
|
-
if (item && item.origin === 'todo' && this.todoRowCount() > 1) {
|
|
2293
|
+
if (this.todoOpen && this.todoRowCount() > 1) {
|
|
2151
2294
|
this.moveTodoFocus(delta);
|
|
2152
2295
|
return;
|
|
2153
2296
|
}
|
|
@@ -2254,6 +2397,7 @@ class Session {
|
|
|
2254
2397
|
this.onEscape();
|
|
2255
2398
|
return;
|
|
2256
2399
|
}
|
|
2400
|
+
if (this.typeIntoTodo(event.key)) return;
|
|
2257
2401
|
const action = actionFromKey(event.key, this.pane);
|
|
2258
2402
|
if (action) this.dispatch(action);
|
|
2259
2403
|
return;
|
|
@@ -2291,7 +2435,9 @@ class Session {
|
|
|
2291
2435
|
}
|
|
2292
2436
|
|
|
2293
2437
|
async startUi() {
|
|
2294
|
-
|
|
2438
|
+
const empty = !this.items.length;
|
|
2439
|
+
const notes = this.notes && hasNotes(this.notes);
|
|
2440
|
+
if (this.didLoad && empty && !notes) {
|
|
2295
2441
|
this.emptyReview = true;
|
|
2296
2442
|
return 0;
|
|
2297
2443
|
}
|