reslop 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/session.js CHANGED
@@ -16,43 +16,51 @@ const { refreshIndexPatches } = require('./diff.js');
16
16
  const { renderFrame, presentRows, presentCursor } = render;
17
17
  const { noteInnerWidth, todoInnerWidth } = render;
18
18
  const { QUIT_FINISH, QUIT_CONTINUE } = render;
19
- const { actionFromKey, hitAction, decodeChunk, mouseKind } = keys;
19
+ const { actionFromKey, hitAction, decodeChunk } = keys;
20
+ const { FILES_DISABLED } = keys;
20
21
  const { cursorInWrap, wrapDoc, wrapMove } = wrap;
21
22
  const { AUTOSAVE_MS, listReviewNames, loadTemplates } = review;
22
23
  const { resolveReviewPath, loadReview, createStore } = review;
23
24
  const { noteCounts, hasNotes, setFeedback, addTodo } = review;
25
+ const { applyImportedNotes } = review;
24
26
  const { setTodoText, removeTodo, flushReview, mergeTodos } = review;
25
27
  const { feedbackKey, checkLabel, rankedTemplates } = review;
26
28
  const { prefixTemplates, TEMPLATE_SHOW, rememberTemplate } = review;
27
29
 
28
30
  const LAYOUTS = ['unified', 'mixed', 'side'];
29
- const FILES_IDLE = ['add', 'unstage', 'revert', 'skip'];
30
- const FILES_MOVE = Object.assign(Object.create(null), {
31
- next: 1,
32
- prev: -1,
33
- });
34
- const ADD_STATUS = Object.assign(Object.create(null), {
35
- staged: 'already staged',
36
- commit: 'read only',
37
- });
38
- const UNSTAGE_STATUS = Object.assign(Object.create(null), {
39
- commit: 'read only',
40
- });
41
- const REVERT_STATUS = Object.assign(Object.create(null), {
42
- commit: 'read only',
43
- });
31
+ const READ_ONLY_ORIGINS = ['commit', 'pr'];
44
32
  const ENTER_TERM = '\x1b[?1049h\x1b[?25l\x1b[?7l\x1b[?1002h\x1b[?1006h';
45
33
  const LEAVE_TERM =
46
34
  '\x1b[?1006l\x1b[?1002l\x1b[?7h\x1b[0 q\x1b[?12l\x1b[?25h\x1b[?1049l';
47
35
  const CURSOR_BLINK_MS = 500;
48
36
 
49
- const layoutStatus = (id) => (id === 'side' ? 'side-by-side' : id);
37
+ const systemNow = () => new Date();
38
+
39
+ const isReadOnlyOrigin = (origin) => READ_ONLY_ORIGINS.includes(origin);
40
+
41
+ const relabelStatus = (entries, from, to) => {
42
+ if (!to) return entries;
43
+ return entries.map((entry) => {
44
+ if (entry.status !== from) return entry;
45
+ return { ...entry, status: to };
46
+ });
47
+ };
48
+
49
+ const depChangeKey = (item) => {
50
+ const change = item.dep && item.dep.change;
51
+ if (!change) return '';
52
+ return `${change.section}/${change.name}`;
53
+ };
54
+
55
+ const needsReload = (item) => item.reload === true;
50
56
 
51
57
  const itemKey = (item) => {
52
58
  if (!item || !item.file) return '';
53
59
  const rel = itemPath(item);
54
60
  if (item.origin === 'todo') return `todo:${rel}`;
55
61
  const origin = item.origin ?? '';
62
+ const depId = depChangeKey(item);
63
+ if (depId) return `${origin}:${rel}:dep:${depId}`;
56
64
  if (!item.hunk) {
57
65
  const block = item.blockId ?? '-';
58
66
  return `${origin}:${rel}:file:${block}`;
@@ -66,12 +74,12 @@ const itemKey = (item) => {
66
74
  const itemFeedKey = (item) => {
67
75
  if (!item || !item.file || item.origin === 'todo') return '';
68
76
  const hunk = item.hunk;
69
- return feedbackKey({
70
- file: itemPath(item),
71
- oldStart: hunk ? hunk.oldStart : 0,
72
- newStart: hunk ? hunk.newStart : 0,
73
- blockId: item.blockId ?? 0,
74
- });
77
+ const depId = depChangeKey(item);
78
+ const file = depId ? `${itemPath(item)}#${depId}` : itemPath(item);
79
+ const oldStart = hunk ? hunk.oldStart : 0;
80
+ const newStart = hunk ? hunk.newStart : 0;
81
+ const blockId = item.blockId ?? 0;
82
+ return feedbackKey({ file, oldStart, newStart, blockId });
75
83
  };
76
84
 
77
85
  const sameCursor = (a, b) => {
@@ -80,16 +88,16 @@ const sameCursor = (a, b) => {
80
88
  return a.x === b.x && a.y === b.y;
81
89
  };
82
90
 
83
- const QUIT_CONFIRM = Object.assign(Object.create(null), {
91
+ const QUIT_CONFIRM = {
84
92
  [QUIT_FINISH]: (session) => session.finishQuit('ready'),
85
93
  [QUIT_CONTINUE]: (session) => session.finishQuit('editing'),
86
94
  'ctrl-c': (session) => session.finishQuit('ready'),
87
95
  escape: (session) => session.cancelQuit(),
88
- });
96
+ };
89
97
 
90
98
  const COMPOSE_EDITS = ['backspace', 'delete', 'ctrl-z', 'ctrl-y'];
91
99
 
92
- const COMPOSE_SPECIAL = Object.assign(Object.create(null), {
100
+ const COMPOSE_SPECIAL = {
93
101
  backspace: (session) => session.editor.backspace(),
94
102
  delete: (session) => session.editor.delete(),
95
103
  left: (session) => session.editor.move(-1),
@@ -103,13 +111,12 @@ const COMPOSE_SPECIAL = Object.assign(Object.create(null), {
103
111
  'ctrl-z': (session) => session.editor.undoEdit(),
104
112
  'ctrl-y': (session) => session.editor.redoEdit(),
105
113
  tab: (session) => session.applyTemplate(),
106
- });
114
+ };
107
115
 
108
116
  const HANDLERS = {
109
117
  add: (session) => session.onAdd(),
110
118
  unstage: (session) => session.onUnstage(),
111
119
  revert: (session) => session.onRevert(),
112
- skip: (session) => session.onSkip(),
113
120
  next: (session) => session.onNext(),
114
121
  prev: (session) => session.onPrev(),
115
122
  layout: (session) => session.onLayout(),
@@ -121,21 +128,20 @@ const HANDLERS = {
121
128
  quit: (session) => session.onQuit(),
122
129
  scrollUp: (session) => session.onScroll(-1),
123
130
  scrollDown: (session) => session.onScroll(1),
124
- help: (session) => session.onHelp(),
125
131
  };
126
132
 
127
- const KEY_MODE = Object.assign(Object.create(null), {
133
+ const KEY_MODE = {
128
134
  confirmQuit: (session, key) => session.onConfirmQuit(key),
129
135
  compose: (session, key) => session.handleComposeKey(key),
130
- });
136
+ };
131
137
 
132
- const MOUSE_KIND = Object.assign(Object.create(null), {
138
+ const MOUSE_KIND = {
133
139
  wheelUp: (session) => session.onScroll(-1),
134
140
  wheelDown: (session) => session.onScroll(1),
135
141
  press: (session, event) => session.onMousePress(event),
136
142
  drag: (session, event) => session.onMouseDrag(event),
137
143
  release: (session, event) => session.onMouseRelease(event),
138
- });
144
+ };
139
145
 
140
146
  const RELEASE_HITS = [
141
147
  {
@@ -167,35 +173,40 @@ class Session {
167
173
  this.stdout = options.stdout;
168
174
  this.color = options.color ?? true;
169
175
  this.copyText = options.copyText ?? copyText;
170
- this.getSize =
171
- options.getSize ??
172
- (() => ({
173
- width: this.stdout.columns ?? 80,
174
- height: this.stdout.rows ?? 24,
175
- }));
176
- this.pane = options.startPane ?? 'diff';
176
+ const termSize = () => {
177
+ const width = this.stdout.columns ?? 80;
178
+ const height = this.stdout.rows ?? 24;
179
+ return { width, height };
180
+ };
181
+ this.getSize = options.getSize ?? termSize;
182
+ this.pane = options.startPane ?? 'files';
177
183
  this.rev = options.rev ?? null;
178
- this.revShort = options.revShort ?? '';
184
+ this.revShort = '';
185
+ this.sourceLabel = options.sourceLabel ?? '';
186
+ this.repoName = options.repoName ?? '';
187
+ this.change = options.change ?? null;
179
188
  this.readdirSync = options.readdirSync ?? fs.readdirSync;
180
189
  this.readFileSync = options.readFileSync ?? fs.readFileSync;
181
190
  this.writeFileSync = options.writeFileSync ?? fs.writeFileSync;
182
191
  this.mkdirSync = options.mkdirSync ?? fs.mkdirSync;
183
- this.now = options.now ?? (() => new Date());
192
+ this.now = options.now ?? systemNow;
184
193
  this.forceNewReview = options.newReview === true;
194
+ this.audit = options.audit === true;
195
+ this.outdatedMap = options.outdatedMap;
196
+ this.auditMap = options.auditMap;
185
197
  this.setInterval = options.setInterval ?? setInterval;
186
198
  this.clearInterval = options.clearInterval ?? clearInterval;
187
199
  }
188
200
 
189
201
  resetState() {
190
202
  this.items = [];
203
+ this.dismissed = new Set();
191
204
  this.top = this.cwd;
192
205
  this.index = 0;
193
- this.skipped = new Set();
194
206
  this.fileCursor = 0;
195
207
  this.reviewPath = null;
196
208
  this.scroll = 0;
197
209
  this.status = '';
198
- this.help = false;
199
210
  this.layout = 'unified';
200
211
  this.done = false;
201
212
  this.exitCode = 0;
@@ -205,10 +216,11 @@ class Session {
205
216
  this.lastSize = null;
206
217
  this.selection = null;
207
218
  this.mouseAnchor = null;
208
- this.dragging = false;
209
219
  this.pendingClick = null;
210
220
  this.abortController = null;
211
221
  this.notes = null;
222
+ this.didResumeReview = false;
223
+ this.importedRemoteNotes = false;
212
224
  this.mode = 'review';
213
225
  this.editor = null;
214
226
  this.composeKind = null;
@@ -230,11 +242,15 @@ class Session {
230
242
 
231
243
  load() {
232
244
  const here = itemKey(this.items[this.index]);
233
- const loaded = this.repo.load(this.cwd, this.paths, {
234
- commit: this.rev,
235
- });
245
+ const commit = this.rev;
246
+ const audit = this.audit;
247
+ const outdatedMap = this.outdatedMap;
248
+ const auditMap = this.auditMap;
249
+ const options = { commit, audit, outdatedMap, auditMap };
250
+ const loaded = this.repo.load(this.cwd, this.paths, options);
236
251
  this.top = loaded.top ?? this.cwd;
237
- this.items = loaded.items;
252
+ const items = loaded.items;
253
+ this.items = items.filter((item) => !this.dismissed.has(itemKey(item)));
238
254
  if (loaded.rev) {
239
255
  this.rev = loaded.rev;
240
256
  }
@@ -243,19 +259,22 @@ class Session {
243
259
  } else if (this.rev && !this.revShort) {
244
260
  this.revShort = this.rev.slice(0, 7);
245
261
  }
262
+ if (loaded.sourceLabel) this.sourceLabel = loaded.sourceLabel;
263
+ if (loaded.change) this.change = loaded.change;
264
+ const repository = this.change ? this.change.repository : '';
265
+ if (!this.repoName && repository) this.repoName = repository;
246
266
  if (!this.notes) this.initReview();
267
+ const imported = loaded.imported;
268
+ if (imported && !this.didResumeReview && !this.importedRemoteNotes) {
269
+ applyImportedNotes(this.notes, imported);
270
+ this.importedRemoteNotes = true;
271
+ }
247
272
  this.injectTodos();
248
273
  if (here) {
249
274
  const idx = this.items.findIndex((item) => itemKey(item) === here);
250
275
  if (idx >= 0) this.index = idx;
251
276
  }
252
- this.pruneSkipped();
253
- if (this.index >= this.items.length) {
254
- this.index = Math.max(0, this.items.length - 1);
255
- }
256
- if (!this.isRemaining(this.items[this.index])) {
257
- this.focusRemaining(this.index, 1);
258
- }
277
+ this.clampIndex();
259
278
  this.syncReviewPath();
260
279
  this.syncFileCursor();
261
280
  }
@@ -273,6 +292,7 @@ class Session {
273
292
  templates,
274
293
  this.readFileSync,
275
294
  );
295
+ this.didResumeReview = true;
276
296
  return;
277
297
  }
278
298
  this.notes = createStore(resolved.reviewPath, templates);
@@ -284,62 +304,23 @@ class Session {
284
304
  this.items = mergeTodos(this.items, todos, extra);
285
305
  }
286
306
 
287
- pruneSkipped() {
288
- const live = new Set();
289
- for (const item of this.items) live.add(itemKey(item));
290
- for (const key of [...this.skipped]) {
291
- if (!live.has(key)) this.skipped.delete(key);
292
- }
293
- }
294
-
295
- isRemaining(item) {
296
- if (!item) return false;
297
- return !this.skipped.has(itemKey(item));
298
- }
299
-
300
- remainingIndices() {
301
- const indices = [];
302
- for (let i = 0; i < this.items.length; i++) {
303
- if (this.isRemaining(this.items[i])) indices.push(i);
304
- }
305
- return indices;
306
- }
307
-
308
- focusRemaining(from, direction) {
309
- const remaining = this.remainingIndices();
310
- if (!remaining.length) {
307
+ clampIndex() {
308
+ if (!this.items.length) {
311
309
  this.index = 0;
312
- return false;
313
- }
314
- if (direction >= 0) {
315
- for (const i of remaining) {
316
- if (i >= from) {
317
- this.index = i;
318
- return true;
319
- }
320
- }
321
- this.index = remaining[remaining.length - 1];
322
- return true;
310
+ return;
323
311
  }
324
- for (let k = remaining.length - 1; k >= 0; k--) {
325
- const i = remaining[k];
326
- if (i <= from) {
327
- this.index = i;
328
- return true;
329
- }
312
+ if (this.index < 0) this.index = 0;
313
+ if (this.index >= this.items.length) {
314
+ this.index = this.items.length - 1;
330
315
  }
331
- this.index = remaining[0];
332
- return true;
333
316
  }
334
317
 
335
318
  fileList() {
336
- const remaining = new Set(this.remainingIndices());
337
- const entries = fileEntries(this.items, remaining);
338
- if (!this.revShort) return entries;
339
- return entries.map((entry) => {
340
- if (entry.status !== 'commit') return entry;
341
- return { ...entry, status: this.revShort };
342
- });
319
+ const entries = fileEntries(this.items);
320
+ if (this.sourceLabel) {
321
+ return relabelStatus(entries, 'pr', this.sourceLabel);
322
+ }
323
+ return relabelStatus(entries, 'commit', this.revShort);
343
324
  }
344
325
 
345
326
  syncReviewPath() {
@@ -348,7 +329,18 @@ class Session {
348
329
  this.reviewPath = item ? itemPath(item) : null;
349
330
  }
350
331
 
351
- syncFileCursor() {
332
+ clampFileCursor() {
333
+ const files = this.fileList();
334
+ if (!files.length) {
335
+ this.fileCursor = 0;
336
+ return;
337
+ }
338
+ if (this.fileCursor >= files.length) {
339
+ this.fileCursor = files.length - 1;
340
+ }
341
+ }
342
+
343
+ followReviewPath() {
352
344
  const files = this.fileList();
353
345
  if (!files.length) {
354
346
  this.fileCursor = 0;
@@ -358,9 +350,17 @@ class Session {
358
350
  const idx = files.findIndex((entry) => entry.path === here);
359
351
  if (idx >= 0) {
360
352
  this.fileCursor = idx;
361
- } else if (this.fileCursor >= files.length) {
362
- this.fileCursor = files.length - 1;
353
+ return;
354
+ }
355
+ this.clampFileCursor();
356
+ }
357
+
358
+ syncFileCursor() {
359
+ if (this.pane === 'files') {
360
+ this.clampFileCursor();
361
+ return;
363
362
  }
363
+ this.followReviewPath();
364
364
  }
365
365
 
366
366
  counts() {
@@ -369,15 +369,11 @@ class Session {
369
369
  unstaged: 0,
370
370
  untracked: 0,
371
371
  commit: 0,
372
- skipped: 0,
372
+ pr: 0,
373
373
  todo: 0,
374
374
  feedback: 0,
375
375
  };
376
376
  for (const item of this.items) {
377
- if (!this.isRemaining(item)) {
378
- counts.skipped += 1;
379
- continue;
380
- }
381
377
  if (!Object.hasOwn(counts, item.origin)) continue;
382
378
  counts[item.origin] += 1;
383
379
  }
@@ -388,16 +384,16 @@ class Session {
388
384
  }
389
385
 
390
386
  view() {
391
- const remaining = this.remainingIndices();
392
- const pos = remaining.indexOf(this.index);
393
387
  const templates = this.shownTemplates();
388
+ const change = this.change;
389
+ const isGithubPr = change && change.source === 'github-pr';
390
+ const repoName = this.repoName || path.basename(this.top || this.cwd || '');
394
391
  return {
395
392
  item: this.current(),
396
- index: pos < 0 ? 0 : pos,
397
- total: remaining.length,
393
+ index: this.index,
394
+ total: this.items.length,
398
395
  scroll: this.scroll,
399
396
  status: this.status,
400
- help: this.help,
401
397
  layout: this.layout,
402
398
  counts: this.counts(),
403
399
  selection: this.selection,
@@ -405,12 +401,13 @@ class Session {
405
401
  files: this.fileList(),
406
402
  fileCursor: this.fileCursor,
407
403
  reviewPath: this.reviewPath,
408
- repoName: path.basename(this.top || this.cwd || ''),
404
+ repoName,
409
405
  revShort: this.revShort || '',
406
+ sourceLabel: this.sourceLabel || '',
407
+ sourceKind: isGithubPr ? 'pr' : '',
410
408
  mode: this.mode,
411
409
  compose: this.composeView(),
412
410
  noteText: this.idleNoteText(),
413
- noteKind: this.idleNoteKind(),
414
411
  todos: this.todoTexts(),
415
412
  todoFocus: this.clampedTodoFocus(),
416
413
  todoEdit: this.todoEditView(),
@@ -454,12 +451,9 @@ class Session {
454
451
  this.closeCompose();
455
452
  }
456
453
  if (this.pane === 'files') {
457
- const delta = FILES_MOVE[action];
458
- if (delta) {
459
- this.onFileMove(delta);
460
- return;
461
- }
462
- if (FILES_IDLE.includes(action)) return;
454
+ if (action === 'next') return void this.onFileMove(1);
455
+ if (action === 'prev') return void this.onFileMove(-1);
456
+ if (FILES_DISABLED.includes(action)) return;
463
457
  }
464
458
  const handler = HANDLERS[action];
465
459
  if (!handler) return;
@@ -467,9 +461,7 @@ class Session {
467
461
  }
468
462
 
469
463
  current() {
470
- const item = this.items[this.index] ?? null;
471
- if (!this.isRemaining(item)) return null;
472
- return item;
464
+ return this.items[this.index] ?? null;
473
465
  }
474
466
 
475
467
  reloadAfterChange() {
@@ -482,7 +474,8 @@ class Session {
482
474
  this.status = 'nothing to review';
483
475
  return;
484
476
  }
485
- this.focusRemaining(here, 1);
477
+ this.index = here;
478
+ this.clampIndex();
486
479
  this.syncReviewPath();
487
480
  this.syncFileCursor();
488
481
  this.scroll = 0;
@@ -508,7 +501,7 @@ class Session {
508
501
 
509
502
  keepOrigin(item, origin) {
510
503
  item.origin = origin;
511
- refreshIndexPatches(this.items, item);
504
+ if (!item.dep) refreshIndexPatches(this.items, item);
512
505
  this.clearSelection();
513
506
  this.syncReviewPath();
514
507
  this.syncFileCursor();
@@ -524,27 +517,160 @@ class Session {
524
517
  return item;
525
518
  }
526
519
 
520
+ fileCursorEntry() {
521
+ const files = this.fileList();
522
+ return files[this.fileCursor] ?? null;
523
+ }
524
+
525
+ fileActionItems() {
526
+ const entry = this.fileCursorEntry();
527
+ if (!entry) return [];
528
+ const rel = entry.path;
529
+ const items = [];
530
+ for (const item of this.items) {
531
+ if (item.origin === 'todo') continue;
532
+ if (itemPath(item) !== rel) continue;
533
+ items.push(item);
534
+ }
535
+ return items;
536
+ }
537
+
538
+ guardFileGit(items) {
539
+ if (!items.length) return false;
540
+ const first = items[0];
541
+ if (isReadOnlyOrigin(first.origin)) {
542
+ this.status = 'read only';
543
+ return false;
544
+ }
545
+ return true;
546
+ }
547
+
548
+ applyFileGit(items, label, write, afterItem) {
549
+ try {
550
+ for (const item of items) {
551
+ write(this.top, item);
552
+ afterItem(item);
553
+ }
554
+ this.status = label;
555
+ return true;
556
+ } catch (error) {
557
+ this.status = error.message;
558
+ return false;
559
+ }
560
+ }
561
+
562
+ selectNextFileAfter(rel) {
563
+ const files = this.fileList();
564
+ if (!files.length) {
565
+ this.fileCursor = 0;
566
+ this.reviewPath = null;
567
+ return;
568
+ }
569
+ const idx = files.findIndex((entry) => entry.path === rel);
570
+ let at = idx < 0 ? this.fileCursor : idx + 1;
571
+ if (at >= files.length) at = files.length - 1;
572
+ const entry = files[at];
573
+ this.fileCursor = at;
574
+ this.reviewPath = entry.path;
575
+ this.index = entry.firstIndex;
576
+ }
577
+
578
+ onFileAdd() {
579
+ const items = this.fileActionItems();
580
+ if (!this.guardFileGit(items)) return;
581
+ const targets = items.filter((item) => item.origin !== 'staged');
582
+ if (!targets.length) {
583
+ this.status = 'already staged';
584
+ return;
585
+ }
586
+ const rel = itemPath(targets[0]);
587
+ const proposed = targets.some((item) => needsReload(item));
588
+ const ok = this.applyFileGit(
589
+ targets,
590
+ 'staged',
591
+ (top, current) => this.repo.add(top, current),
592
+ (item) => this.keepOrigin(item, 'staged'),
593
+ );
594
+ if (ok && proposed) this.reloadAfterChange();
595
+ if (ok) this.selectNextFileAfter(rel);
596
+ }
597
+
598
+ onFileUnstage() {
599
+ const items = this.fileActionItems();
600
+ if (!this.guardFileGit(items)) return;
601
+ const targets = items.filter((item) => item.origin === 'staged');
602
+ if (!targets.length) {
603
+ this.status = 'not staged';
604
+ return;
605
+ }
606
+ const rel = itemPath(targets[0]);
607
+ const ok = this.applyFileGit(
608
+ targets,
609
+ 'unstaged',
610
+ (top, current) => this.repo.unstage(top, current),
611
+ (item) => {
612
+ const origin = item.file.isNew === true ? 'untracked' : 'unstaged';
613
+ this.keepOrigin(item, origin);
614
+ },
615
+ );
616
+ if (ok) this.selectNextFileAfter(rel);
617
+ }
618
+
619
+ onFileRevert() {
620
+ const items = this.fileActionItems();
621
+ if (!this.guardFileGit(items)) return;
622
+ const rel = itemPath(items[0]);
623
+ for (const item of items) {
624
+ if (needsReload(item)) this.dismissed.add(itemKey(item));
625
+ }
626
+ let ok = false;
627
+ try {
628
+ this.repo.revertFile(this.top, rel, items);
629
+ this.status = 'reverted';
630
+ ok = true;
631
+ } catch (error) {
632
+ this.status = error.message;
633
+ }
634
+ this.reloadAfterChange();
635
+ if (ok) this.selectNextFileAfter(rel);
636
+ }
637
+
527
638
  onAdd() {
639
+ if (this.pane === 'files') return void this.onFileAdd();
528
640
  const item = this.activeDiffItem();
529
641
  if (!item) return;
530
- const blocked = ADD_STATUS[item.origin];
531
- if (blocked) {
532
- this.status = blocked;
642
+ if (isReadOnlyOrigin(item.origin)) {
643
+ this.status = 'read only';
644
+ return;
645
+ }
646
+ if (item.origin === 'staged') {
647
+ this.status = 'already staged';
533
648
  return;
534
649
  }
535
650
  this.applyGit(
536
651
  item,
537
652
  'staged',
538
653
  (top, current) => this.repo.add(top, current),
539
- () => this.keepOrigin(item, 'staged'),
654
+ () => {
655
+ if (needsReload(item)) {
656
+ this.reloadAfterChange();
657
+ return;
658
+ }
659
+ this.keepOrigin(item, 'staged');
660
+ },
540
661
  );
541
662
  }
542
663
 
543
664
  onUnstage() {
665
+ if (this.pane === 'files') return void this.onFileUnstage();
544
666
  const item = this.activeDiffItem();
545
667
  if (!item) return;
668
+ if (isReadOnlyOrigin(item.origin)) {
669
+ this.status = 'read only';
670
+ return;
671
+ }
546
672
  if (item.origin !== 'staged') {
547
- this.status = UNSTAGE_STATUS[item.origin] ?? 'not staged';
673
+ this.status = 'not staged';
548
674
  return;
549
675
  }
550
676
  const origin = item.file.isNew === true ? 'untracked' : 'unstaged';
@@ -557,62 +683,39 @@ class Session {
557
683
  }
558
684
 
559
685
  onRevert() {
686
+ if (this.pane === 'files') return void this.onFileRevert();
560
687
  const item = this.activeDiffItem();
561
688
  if (!item) return;
562
- const blocked = REVERT_STATUS[item.origin];
563
- if (blocked) {
564
- this.status = blocked;
689
+ if (isReadOnlyOrigin(item.origin)) {
690
+ this.status = 'read only';
565
691
  return;
566
692
  }
567
693
  this.applyGit(
568
694
  item,
569
695
  'reverted',
570
696
  (top, current) => this.repo.revert(top, current),
571
- () => this.reloadAfterChange(),
697
+ () => {
698
+ if (needsReload(item)) this.dismissed.add(itemKey(item));
699
+ this.reloadAfterChange();
700
+ },
572
701
  );
573
702
  }
574
703
 
575
- onSkip() {
576
- const item = this.items[this.index];
577
- if (!this.isRemaining(item)) return;
578
- this.skipped.add(itemKey(item));
579
- this.scroll = 0;
580
- this.clearSelection();
581
- const remaining = this.remainingIndices();
582
- if (!remaining.length) {
583
- this.status = 'all skipped';
584
- return;
585
- }
586
- this.focusRemaining(this.index + 1, 1);
587
- this.syncReviewPath();
588
- this.status = 'skipped';
589
- }
590
-
591
704
  moveRemaining(step) {
592
- const remaining = this.remainingIndices();
593
- if (!remaining.length) {
594
- this.status = 'all skipped';
595
- return;
596
- }
597
- const pos = remaining.indexOf(this.index);
598
- if (pos < 0) {
599
- this.focusRemaining(this.index, step);
600
- this.scroll = 0;
601
- this.status = '';
602
- this.clearSelection();
603
- this.syncReviewPath();
705
+ if (!this.items.length) {
706
+ this.status = 'nothing to review';
604
707
  return;
605
708
  }
606
- const next = pos + step;
709
+ const next = this.index + step;
607
710
  if (next < 0) {
608
711
  this.status = 'first block';
609
712
  return;
610
713
  }
611
- if (next >= remaining.length) {
714
+ if (next >= this.items.length) {
612
715
  this.status = 'last block';
613
716
  return;
614
717
  }
615
- this.index = remaining[next];
718
+ this.index = next;
616
719
  this.scroll = 0;
617
720
  this.status = '';
618
721
  this.clearSelection();
@@ -620,18 +723,10 @@ class Session {
620
723
  }
621
724
 
622
725
  onNext() {
623
- if (this.mode === 'compose') {
624
- this.commitCompose();
625
- this.closeCompose();
626
- }
627
726
  this.moveRemaining(1);
628
727
  }
629
728
 
630
729
  onPrev() {
631
- if (this.mode === 'compose') {
632
- this.commitCompose();
633
- this.closeCompose();
634
- }
635
730
  this.moveRemaining(-1);
636
731
  }
637
732
 
@@ -680,7 +775,6 @@ class Session {
680
775
  this.status = 'not a diff block';
681
776
  return;
682
777
  }
683
- this.help = false;
684
778
  const prev = this.notes.feedback.get(itemFeedKey(item));
685
779
  const text = prev ? prev.text : '';
686
780
  this.openCompose('feedback', text);
@@ -688,8 +782,7 @@ class Session {
688
782
 
689
783
  todoFile() {
690
784
  if (this.pane === 'files') {
691
- const files = this.fileList();
692
- const entry = files[this.fileCursor];
785
+ const entry = this.fileCursorEntry();
693
786
  return entry ? entry.path : '';
694
787
  }
695
788
  const item = this.current();
@@ -768,9 +861,7 @@ class Session {
768
861
  this.injectTodos();
769
862
  const left = this.fileTodos(file);
770
863
  if (!left.length) {
771
- if (!this.isRemaining(this.items[this.index])) {
772
- this.focusRemaining(this.index, 1);
773
- }
864
+ this.clampIndex();
774
865
  } else if (this.todoFocus >= left.length) {
775
866
  this.todoFocus = left.length - 1;
776
867
  }
@@ -799,7 +890,6 @@ class Session {
799
890
  createTodo(file) {
800
891
  if (!file) return;
801
892
  this.pane = 'diff';
802
- this.help = false;
803
893
  this.scroll = 0;
804
894
  this.clearSelection();
805
895
  this.startDraftCompose(file);
@@ -841,8 +931,8 @@ class Session {
841
931
 
842
932
  commitTodo(text) {
843
933
  const id = this.composeTodoId;
844
- const prev =
845
- id === null ? null : this.notes.todos.find((entry) => entry.id === id);
934
+ const todos = this.notes.todos;
935
+ const prev = id === null ? null : todos.find((entry) => entry.id === id);
846
936
  const current = this.current();
847
937
  const file = prev ? prev.file : itemPath(current) || this.todoDraftFile;
848
938
  if (prev) {
@@ -873,9 +963,7 @@ class Session {
873
963
  this.templateFocus = false;
874
964
  this.todoDraftFile = null;
875
965
  this.injectTodos();
876
- if (!this.isRemaining(this.items[this.index])) {
877
- this.focusRemaining(this.index, 1);
878
- }
966
+ this.clampIndex();
879
967
  this.syncReviewPath();
880
968
  this.syncFileCursor();
881
969
  }
@@ -930,11 +1018,10 @@ class Session {
930
1018
  composeView() {
931
1019
  if (this.mode !== 'compose' || !this.editor) return null;
932
1020
  if (this.composeKind !== 'feedback') return null;
933
- return {
934
- kind: 'feedback',
935
- text: this.editor.text,
936
- cursor: this.editor.cursor,
937
- };
1021
+ const kind = this.composeKind;
1022
+ const text = this.editor.text;
1023
+ const cursor = this.editor.cursor;
1024
+ return { kind, text, cursor };
938
1025
  }
939
1026
 
940
1027
  todoEditView() {
@@ -945,8 +1032,6 @@ class Session {
945
1032
 
946
1033
  fileTodos(rel) {
947
1034
  if (!this.notes || !rel) return [];
948
- const page = { origin: 'todo', file: { newPath: rel, oldPath: rel } };
949
- if (!this.isRemaining(page)) return [];
950
1035
  const todos = [];
951
1036
  for (const todo of this.notes.todos) {
952
1037
  if (todo.file !== rel) continue;
@@ -959,10 +1044,8 @@ class Session {
959
1044
  const item = this.current();
960
1045
  if (!item || item.origin !== 'todo') return [];
961
1046
  const texts = [];
962
- const editingId =
963
- this.mode === 'compose' && this.composeKind === 'todo'
964
- ? this.composeTodoId
965
- : null;
1047
+ const editingTodo = this.mode === 'compose' && this.composeKind === 'todo';
1048
+ const editingId = editingTodo ? this.composeTodoId : null;
966
1049
  for (const todo of this.fileTodos(itemPath(item))) {
967
1050
  const live = editingId === todo.id && this.editor;
968
1051
  const text = live ? this.editor.text : todo.text;
@@ -981,7 +1064,7 @@ class Session {
981
1064
  }
982
1065
 
983
1066
  clampedTodoFocus() {
984
- const last = Math.max(0, this.todoTexts().length - 1);
1067
+ const last = Math.max(0, this.todoRowCount() - 1);
985
1068
  if (this.todoFocus < 0) return 0;
986
1069
  if (this.todoFocus > last) return last;
987
1070
  return this.todoFocus;
@@ -1004,12 +1087,6 @@ class Session {
1004
1087
  else this.todoFocus = next;
1005
1088
  }
1006
1089
 
1007
- idleNoteKind() {
1008
- const item = this.current();
1009
- if (!item || item.origin === 'todo') return '';
1010
- return 'feedback';
1011
- }
1012
-
1013
1090
  idleNoteText() {
1014
1091
  if (!this.notes) return '';
1015
1092
  if (this.mode === 'compose') return '';
@@ -1023,11 +1100,10 @@ class Session {
1023
1100
  flushReview(force) {
1024
1101
  if (!this.notes) return false;
1025
1102
  try {
1026
- return flushReview(this.notes, {
1027
- writeFileSync: this.writeFileSync,
1028
- mkdirSync: this.mkdirSync,
1029
- force: force === true,
1030
- });
1103
+ const writeFileSync = this.writeFileSync;
1104
+ const mkdirSync = this.mkdirSync;
1105
+ const io = { writeFileSync, mkdirSync, force: force === true };
1106
+ return flushReview(this.notes, io);
1031
1107
  } catch (error) {
1032
1108
  this.status = error.message;
1033
1109
  return false;
@@ -1146,9 +1222,8 @@ class Session {
1146
1222
 
1147
1223
  showFiles() {
1148
1224
  this.pane = 'files';
1149
- this.help = false;
1150
1225
  this.clearSelection();
1151
- this.syncFileCursor();
1226
+ this.followReviewPath();
1152
1227
  }
1153
1228
 
1154
1229
  onEscape() {
@@ -1163,7 +1238,6 @@ class Session {
1163
1238
  if (this.pane === 'files') {
1164
1239
  if (!this.reviewPath) return;
1165
1240
  this.pane = 'diff';
1166
- this.help = false;
1167
1241
  this.clearSelection();
1168
1242
  return;
1169
1243
  }
@@ -1183,7 +1257,6 @@ class Session {
1183
1257
  this.pane = 'diff';
1184
1258
  this.scroll = 0;
1185
1259
  this.status = '';
1186
- this.help = false;
1187
1260
  this.clearSelection();
1188
1261
  this.syncReviewPath();
1189
1262
  }
@@ -1191,7 +1264,7 @@ class Session {
1191
1264
  onFileMove(delta) {
1192
1265
  const files = this.fileList();
1193
1266
  if (!files.length) {
1194
- this.status = 'all skipped';
1267
+ this.status = 'nothing to review';
1195
1268
  return;
1196
1269
  }
1197
1270
  const next = this.fileCursor + delta;
@@ -1204,6 +1277,9 @@ class Session {
1204
1277
  return;
1205
1278
  }
1206
1279
  this.fileCursor = next;
1280
+ const entry = files[next];
1281
+ this.reviewPath = entry.path;
1282
+ this.index = entry.firstIndex;
1207
1283
  this.status = '';
1208
1284
  }
1209
1285
 
@@ -1221,16 +1297,11 @@ class Session {
1221
1297
  this.clearSelection();
1222
1298
  }
1223
1299
 
1224
- onHelp() {
1225
- this.help = !this.help;
1226
- this.clearSelection();
1227
- }
1228
-
1229
1300
  onLayout() {
1230
1301
  const i = LAYOUTS.indexOf(this.layout);
1231
1302
  const next = (i < 0 ? 0 : i + 1) % LAYOUTS.length;
1232
1303
  this.layout = LAYOUTS[next];
1233
- this.status = layoutStatus(this.layout);
1304
+ this.status = this.layout === 'side' ? 'side-by-side' : this.layout;
1234
1305
  this.scroll = 0;
1235
1306
  this.clearSelection();
1236
1307
  }
@@ -1245,7 +1316,6 @@ class Session {
1245
1316
  onMousePress(event) {
1246
1317
  const cell = this.mouseCell(event);
1247
1318
  this.mouseAnchor = cell;
1248
- this.dragging = false;
1249
1319
  this.pendingClick = null;
1250
1320
  this.selection = null;
1251
1321
  if (this.lastFrame && cell.y === this.lastFrame.height) {
@@ -1255,7 +1325,6 @@ class Session {
1255
1325
 
1256
1326
  onMouseDrag(event) {
1257
1327
  if (!this.mouseAnchor) return;
1258
- this.dragging = true;
1259
1328
  this.pendingClick = null;
1260
1329
  this.selection = {
1261
1330
  start: this.mouseAnchor,
@@ -1298,7 +1367,6 @@ class Session {
1298
1367
  const clickId = this.pendingClick;
1299
1368
  this.pendingClick = null;
1300
1369
  this.mouseAnchor = null;
1301
- this.dragging = false;
1302
1370
  if (this.clickButton(cell, clickId)) return;
1303
1371
  const frame = this.lastFrame;
1304
1372
  for (const target of RELEASE_HITS) {
@@ -1328,7 +1396,7 @@ class Session {
1328
1396
  return;
1329
1397
  }
1330
1398
  if (event.type !== 'mouse') return;
1331
- const kind = mouseKind(event);
1399
+ const kind = event.kind;
1332
1400
  const wheel = kind === 'wheelUp' || kind === 'wheelDown';
1333
1401
  if (!wheel && event.btn !== 0 && event.btn !== undefined) return;
1334
1402
  const handler = MOUSE_KIND[kind];
@@ -1406,11 +1474,6 @@ class Session {
1406
1474
  }
1407
1475
 
1408
1476
  module.exports = {
1409
- LAYOUTS,
1410
- ENTER_TERM,
1411
1477
  LEAVE_TERM,
1412
- CURSOR_BLINK_MS,
1413
- HANDLERS,
1414
- itemKey,
1415
1478
  Session,
1416
1479
  };