reslop 0.2.3 → 0.2.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.
Files changed (89) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +8 -4
  3. package/bin/reslop.js +4 -2
  4. package/lib/api.js +2 -1
  5. package/lib/capabilities.js +0 -4
  6. package/lib/cli.js +19 -10
  7. package/lib/clipboard.js +45 -4
  8. package/lib/deps-diff.js +61 -118
  9. package/lib/deps.js +166 -235
  10. package/lib/detect.js +4 -2
  11. package/lib/diff/align.js +2 -1
  12. package/lib/diff/diff.js +6 -3
  13. package/lib/diff/display.js +2 -1
  14. package/lib/diff/inline.js +68 -2
  15. package/lib/diff/patch.js +2 -1
  16. package/lib/diff/unit.js +15 -30
  17. package/lib/editor.js +91 -13
  18. package/lib/files.js +54 -27
  19. package/lib/git-branches.js +107 -154
  20. package/lib/git-deps.js +78 -155
  21. package/lib/git-worktree.js +508 -0
  22. package/lib/git.js +23 -580
  23. package/lib/github.js +9 -8
  24. package/lib/gitlab.js +5 -4
  25. package/lib/highlight/bash.js +2 -1
  26. package/lib/highlight/core.js +11 -0
  27. package/lib/highlight/css.js +2 -1
  28. package/lib/highlight/csv.js +2 -1
  29. package/lib/highlight/dot.js +3 -12
  30. package/lib/highlight/html.js +2 -1
  31. package/lib/highlight/json.js +2 -1
  32. package/lib/highlight/log.js +5 -11
  33. package/lib/highlight/txt.js +3 -12
  34. package/lib/highlight.js +2 -1
  35. package/lib/keys.js +22 -12
  36. package/lib/manifest.js +4 -6
  37. package/lib/npm-commands.js +82 -24
  38. package/lib/remote.js +21 -20
  39. package/lib/render/branches.js +9 -27
  40. package/lib/render/chrome.js +109 -35
  41. package/lib/render/commits.js +216 -216
  42. package/lib/render/diff.js +160 -47
  43. package/lib/render/files.js +9 -26
  44. package/lib/render/frame.js +97 -118
  45. package/lib/render/notes.js +74 -26
  46. package/lib/render/npm.js +104 -30
  47. package/lib/render/present.js +2 -1
  48. package/lib/render/primitives.js +128 -52
  49. package/lib/render/render.js +2 -1
  50. package/lib/render/unit.js +17 -11
  51. package/lib/report-model.js +6 -4
  52. package/lib/report-parse.js +8 -14
  53. package/lib/report-render.js +38 -21
  54. package/lib/report-run.js +12 -11
  55. package/lib/report-test.js +45 -51
  56. package/lib/report-tools.js +44 -25
  57. package/lib/review-merge.js +102 -131
  58. package/lib/review.js +69 -78
  59. package/lib/select.js +52 -0
  60. package/lib/session/actions.js +72 -87
  61. package/lib/session/branches.js +18 -52
  62. package/lib/session/change-actions.js +175 -147
  63. package/lib/session/commit.js +65 -61
  64. package/lib/session/compose.js +152 -83
  65. package/lib/session/dispatch.js +53 -46
  66. package/lib/session/edit-click.js +57 -0
  67. package/lib/session/files-pane.js +30 -51
  68. package/lib/session/items.js +73 -90
  69. package/lib/session/lifecycle.js +37 -98
  70. package/lib/session/load.js +52 -57
  71. package/lib/session/navigation.js +105 -95
  72. package/lib/session/npm.js +205 -71
  73. package/lib/session/ops.js +200 -45
  74. package/lib/session/pointer.js +33 -8
  75. package/lib/session/progress.js +9 -6
  76. package/lib/session/review.js +35 -53
  77. package/lib/session/tasks.js +292 -0
  78. package/lib/session/terminal.js +17 -19
  79. package/lib/session/unit.js +3 -4
  80. package/lib/session/update.js +76 -96
  81. package/lib/session/view.js +45 -16
  82. package/lib/session/watch.js +47 -27
  83. package/lib/session.js +66 -97
  84. package/lib/source.js +6 -3
  85. package/lib/update.js +5 -13
  86. package/lib/utilities.js +25 -3
  87. package/lib/wrap.js +18 -1
  88. package/package.json +1 -1
  89. package/lib/session/todos.js +0 -293
@@ -3,14 +3,18 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
 
6
- const { isAbortError } = require('./load.js');
7
- const { createDiskWatcher, POLL_MS, DEBOUNCE_MS } = require('./watch.js');
6
+ const sessionLoad = require('./load.js');
7
+ const { isAbortError, tagLoaded, fetchSnapshot, fetchExtras } = sessionLoad;
8
+ const watch = require('./watch.js');
9
+ const { createDiskWatcher, DEBOUNCE_MS } = watch;
8
10
  const files = require('../files.js');
9
- const { itemPath, relativeAge, TODO_FILE } = files;
10
- const { MANIFEST, LOCKFILE, depFileMeta } = require('../deps.js');
11
+ const { itemPath, relativeAge } = files;
12
+ const deps = require('../deps.js');
13
+ const { MANIFEST, LOCKFILE, depFileMeta } = deps;
11
14
  const review = require('../review.js');
12
15
  const { hasNotes, loadReview } = review;
13
- const { mergeReview } = require('../review-merge.js');
16
+ const reviewMerge = require('../review-merge.js');
17
+ const { mergeReview } = reviewMerge;
14
18
 
15
19
  const loadOptions = (api, extra = {}) => {
16
20
  const { loadConfig, ui } = api;
@@ -78,7 +82,7 @@ const failOpen = (api, error) => {
78
82
 
79
83
  const restoreAfterLoad = (api, here, wasTodo) => {
80
84
  const { collection, nav, ui } = api;
81
- nav.todoOpen = wasTodo;
85
+ nav.tasksOpen = wasTodo;
82
86
  if (here && !wasTodo) {
83
87
  const idx = collection.findRestoredIndex(here);
84
88
  if (idx >= 0) nav.index = idx;
@@ -148,7 +152,7 @@ const applyLoaded = (api, loaded) => {
148
152
  if (loaded.generation !== undefined) {
149
153
  if (!loader.accept(loaded.generation, ui.done)) return false;
150
154
  }
151
- const wasTodo = nav.todoOpen === true;
155
+ const wasTodo = nav.tasksOpen === true;
152
156
  const here = ui.current();
153
157
  ui.top = loaded.top ?? api.loadConfig.cwd;
154
158
  collection.replace(loaded.items);
@@ -239,6 +243,13 @@ const clearNpmExtras = (api) => {
239
243
  api.npmExtras = null;
240
244
  };
241
245
 
246
+ const isWanted = (api, gen) => {
247
+ if (!api.loader.isCurrent(gen)) return false;
248
+ if (!api.ui.done) return true;
249
+ api.ops.clearBusy();
250
+ return false;
251
+ };
252
+
242
253
  const followExtras = async (api, loaded, gen, kind) => {
243
254
  const extra = loadOptions(api, {
244
255
  paths: api.loadConfig.paths,
@@ -246,25 +257,18 @@ const followExtras = async (api, loaded, gen, kind) => {
246
257
  });
247
258
  let next;
248
259
  try {
249
- next = await api.loader.fetchExtras(api.loadConfig.repo, loaded, extra);
260
+ next = await fetchExtras(api.loadConfig.repo, loaded, extra);
250
261
  } catch (error) {
251
- if (!api.loader.isCurrent(gen)) return;
252
- return void failOpen(api, error);
253
- }
254
- if (!api.loader.isCurrent(gen)) return;
255
- if (api.ui.done) {
256
- api.ops.clearBusy();
262
+ if (api.loader.isCurrent(gen)) failOpen(api, error);
257
263
  return;
258
264
  }
259
- if (!next) {
260
- api.ops.clearBusy();
265
+ if (!isWanted(api, gen)) return;
266
+ if (next) {
267
+ applyLoaded(api, tagLoaded(next, gen));
268
+ cacheNpmExtras(api, next);
269
+ } else {
261
270
  api.loader.pendingExtras = false;
262
- api.ui.paint();
263
- finishIfEmpty(api, kind);
264
- return;
265
271
  }
266
- applyLoaded(api, api.loader.tag(next, gen));
267
- cacheNpmExtras(api, next);
268
272
  api.ops.clearBusy();
269
273
  api.ui.paint();
270
274
  finishIfEmpty(api, kind);
@@ -297,7 +301,13 @@ const resetNavAfterLoad = (api, extra = {}) => {
297
301
  if (extra.afterLoad) extra.afterLoad();
298
302
  };
299
303
 
304
+ const refreshListsForPane = (api) => {
305
+ const pane = api.ui.nav.pane;
306
+ if (pane === 'commits' && api.ui.commits) api.ui.commits.refresh();
307
+ };
308
+
300
309
  const afterSnapshot = (api, kind, extra = {}) => {
310
+ refreshListsForPane(api);
301
311
  if (kind !== 'open' && kind !== 'watch') {
302
312
  return void resetNavAfterLoad(api, extra);
303
313
  }
@@ -315,7 +325,7 @@ const runSnapshot = async (api, gen, kind, extra = {}) => {
315
325
  beginSnapshot(api, extra);
316
326
  let loaded;
317
327
  try {
318
- loaded = await api.loader.fetchSnapshot(
328
+ loaded = await fetchSnapshot(
319
329
  api.loadConfig.repo,
320
330
  api.loadConfig.cwd,
321
331
  api.loadConfig.paths,
@@ -325,15 +335,11 @@ const runSnapshot = async (api, gen, kind, extra = {}) => {
325
335
  }),
326
336
  );
327
337
  } catch (error) {
328
- if (!api.loader.isCurrent(gen)) return;
329
- return void failOpen(api, error);
330
- }
331
- if (!api.loader.isCurrent(gen)) return;
332
- if (api.ui.done) {
333
- api.ops.clearBusy();
338
+ if (api.loader.isCurrent(gen)) failOpen(api, error);
334
339
  return;
335
340
  }
336
- applyLoaded(api, api.loader.tag(loaded, gen));
341
+ if (!isWanted(api, gen)) return;
342
+ applyLoaded(api, tagLoaded(loaded, gen));
337
343
  afterSnapshot(api, kind, extra);
338
344
  if (loaded.pending === true || npmExtrasStale(api)) {
339
345
  api.ops.setBusy('checking npm');
@@ -379,7 +385,7 @@ const refreshFromRepo = (api, extra = {}) => {
379
385
  api.loadConfig.paths,
380
386
  snapshotOpts(api, extra),
381
387
  );
382
- applyLoaded(api, api.loader.tag(loaded, gen));
388
+ applyLoaded(api, tagLoaded(loaded, gen));
383
389
  afterSnapshot(api, kind, extra);
384
390
  if (loaded.pending === true || npmExtrasStale(api)) {
385
391
  api.ops.setBusy('checking npm');
@@ -463,59 +469,6 @@ const catchUpWatch = (api) => {
463
469
  refreshFromRepo(api, { keepView: true, quiet: true });
464
470
  };
465
471
 
466
- const stampFile = (root, rel) => {
467
- const full = path.join(root, rel);
468
- let stat;
469
- try {
470
- stat = fs.statSync(full);
471
- } catch {
472
- return `${rel}:missing`;
473
- }
474
- return `${rel}:${stat.mtimeMs}:${stat.size}`;
475
- };
476
-
477
- const worktreeStamp = (api) => {
478
- const root = api.ui.top || api.loadConfig.cwd;
479
- const parts = [];
480
- for (const rel of itemRels(api.collection)) {
481
- if (!rel || rel === TODO_FILE) continue;
482
- parts.push(stampFile(root, rel));
483
- }
484
- return parts.join('\n');
485
- };
486
-
487
- const watchesWorktree = (nav) =>
488
- nav.todoOpen === true ||
489
- nav.pane === 'branches' ||
490
- nav.pane === 'commits' ||
491
- nav.pane === 'npm';
492
-
493
- const currentFileStamp = (api) => {
494
- const { nav } = api;
495
- const root = api.ui.top || api.loadConfig.cwd;
496
- if (watchesWorktree(nav)) return worktreeStamp(api);
497
- const pane = nav.pane;
498
- if (pane !== 'diff' && pane !== 'unit') return '';
499
- const rel = pane === 'unit' ? nav.reviewPath : itemPath(api.ui.current());
500
- if (!rel) return '';
501
- return stampFile(root, rel);
502
- };
503
-
504
- const pollCurrentFile = (api) => {
505
- const stamp = currentFileStamp(api);
506
- if (!stamp) {
507
- api.watchStamp = '';
508
- return;
509
- }
510
- if (!api.watchStamp) {
511
- api.watchStamp = stamp;
512
- return;
513
- }
514
- if (stamp === api.watchStamp) return;
515
- api.watchStamp = stamp;
516
- onDiskChange(api);
517
- };
518
-
519
472
  const reviewFileStamp = (api) => {
520
473
  const store = api.review && api.review.store;
521
474
  if (!store || !store.reviewPath) return '';
@@ -549,14 +502,12 @@ const applyReviewChange = (api) => {
549
502
  }
550
503
  mergeReview(store, disk);
551
504
  api.reviewStamp = stamp;
552
- if (typeof api.ui.paint === 'function') api.ui.paint();
505
+ api.ui.paint();
553
506
  };
554
507
 
555
508
  const stopWatch = (api) => {
556
509
  clearWatchRetry(api);
557
- api.watchStamp = '';
558
510
  api.reviewStamp = '';
559
- if (api.ui.term) api.ui.term.stopTimer('watch');
560
511
  if (!api.diskWatcher) return;
561
512
  api.diskWatcher.close();
562
513
  api.diskWatcher = null;
@@ -575,26 +526,15 @@ const startWatch = (api) => {
575
526
  });
576
527
  syncItemWatches(api);
577
528
  noteReviewStamp(api);
578
- if (!api.ui.term) return;
579
- api.ui.term.startTimer(
580
- 'watch',
581
- () => {
582
- pollCurrentFile(api);
583
- applyReviewChange(api);
584
- },
585
- POLL_MS,
586
- );
587
529
  };
588
530
 
589
531
  const createLifecycle = (api) => {
590
532
  api.diskWatcher = null;
591
533
  api.watchRetry = null;
592
- api.watchStamp = '';
593
534
  api.reviewStamp = '';
594
535
  api.ignoreWatchUntil = 0;
595
536
  api.npmExtras = null;
596
537
  return {
597
- applyLoaded: (loaded) => applyLoaded(api, loaded),
598
538
  load: () => load(api),
599
539
  openLoad: () => openLoad(api),
600
540
  loadReady: () => loadReady(api),
@@ -605,7 +545,6 @@ const createLifecycle = (api) => {
605
545
  applyReviewChange: () => applyReviewChange(api),
606
546
  noteReviewStamp: () => noteReviewStamp(api),
607
547
  catchUpWatch: () => catchUpWatch(api),
608
- pollCurrentFile: () => pollCurrentFile(api),
609
548
  startWatch: () => startWatch(api),
610
549
  stopWatch: () => stopWatch(api),
611
550
  ignoreWatch: (ms) => ignoreWatch(api, ms),
@@ -1,71 +1,66 @@
1
1
  'use strict';
2
2
 
3
- const { stateView } = require('./accessors.js');
3
+ const utilities = require('../utilities.js');
4
+ const { isAbort: isAbortError } = utilities;
4
5
 
5
- const isAbortError = (error) => !!(error && error.code === 'ABORT');
6
+ class LoadCoordinator {
7
+ constructor() {
8
+ this.gen = 0;
9
+ this.abortController = null;
10
+ this.promise = null;
11
+ this.pendingExtras = false;
12
+ this.didLoad = false;
13
+ }
6
14
 
7
- const createLoadCoordinator = () => {
8
- const state = {
9
- gen: 0,
10
- abort: null,
11
- promise: null,
12
- pendingExtras: false,
13
- didLoad: false,
14
- };
15
+ get signal() {
16
+ return this.abortController?.signal;
17
+ }
15
18
 
16
- const bump = () => {
17
- state.gen += 1;
18
- state.abort?.abort();
19
- state.abort = new AbortController();
20
- return state.gen;
21
- };
19
+ bump() {
20
+ this.gen += 1;
21
+ this.abortController?.abort();
22
+ this.abortController = new AbortController();
23
+ return this.gen;
24
+ }
22
25
 
23
- const isCurrent = (gen, done = false) => gen === state.gen && !done;
24
- const accept = isCurrent;
26
+ isCurrent(gen, done = false) {
27
+ return gen === this.gen && !done;
28
+ }
25
29
 
26
- const abort = () => {
27
- state.abort?.abort();
28
- };
30
+ accept(gen, done = false) {
31
+ return this.isCurrent(gen, done);
32
+ }
29
33
 
30
- const reset = () => {
31
- abort();
32
- state.gen += 1;
33
- state.abort = null;
34
- state.promise = null;
35
- state.pendingExtras = false;
36
- state.didLoad = false;
37
- };
34
+ abort() {
35
+ this.abortController?.abort();
36
+ }
38
37
 
39
- const tag = (loaded, gen) => ({ ...loaded, generation: gen });
38
+ reset() {
39
+ this.abort();
40
+ this.gen += 1;
41
+ this.abortController = null;
42
+ this.promise = null;
43
+ this.pendingExtras = false;
44
+ this.didLoad = false;
45
+ }
46
+ }
40
47
 
41
- const fetchSnapshot = async (repo, cwd, paths, options) => {
42
- if (repo.loadAsync) return repo.loadAsync(cwd, paths, options);
43
- return repo.load(cwd, paths, options);
44
- };
48
+ const tagLoaded = (loaded, gen) => ({ ...loaded, generation: gen });
45
49
 
46
- const fetchExtras = async (repo, loaded, extra) => {
47
- if (!repo.loadExtras) return null;
48
- return repo.loadExtras(loaded, extra);
49
- };
50
+ const fetchSnapshot = async (repo, cwd, paths, options) => {
51
+ if (repo.loadAsync) return repo.loadAsync(cwd, paths, options);
52
+ return repo.load(cwd, paths, options);
53
+ };
50
54
 
51
- return stateView(
52
- state,
53
- {
54
- get signal() {
55
- return state.abort?.signal;
56
- },
57
- bump,
58
- isCurrent,
59
- accept,
60
- abort,
61
- reset,
62
- tag,
63
- fetchSnapshot,
64
- fetchExtras,
65
- },
66
- ['promise', 'pendingExtras', 'didLoad'],
67
- ['gen'],
68
- );
55
+ const fetchExtras = async (repo, loaded, extra) => {
56
+ if (!repo.loadExtras) return null;
57
+ return repo.loadExtras(loaded, extra);
69
58
  };
70
59
 
71
- module.exports = { isAbortError, createLoadCoordinator };
60
+ module.exports = {
61
+ isAbortError,
62
+ LoadCoordinator,
63
+ tagLoaded,
64
+ fetchSnapshot,
65
+ fetchExtras,
66
+ };
@@ -1,106 +1,116 @@
1
1
  'use strict';
2
2
 
3
3
  const files = require('../files.js');
4
- const { listPath, isTodoItem, isTodosEntry, TODO_FILE } = files;
5
-
6
- const initialState = (options = {}) => ({
7
- pane: options.startPane ?? 'files',
8
- index: 0,
9
- fileCursor: 0,
10
- branchCursor: 0,
11
- commitCursor: 0,
12
- npmCursor: 0,
13
- scroll: 0,
14
- listScroll: 0,
15
- reviewPath: null,
16
- fileScope: 'diff',
17
- unitLine: 0,
18
- selection: null,
19
- mouseAnchor: null,
20
- pendingClick: null,
21
- todoOpen: false,
22
- todoFocus: 0,
23
- });
24
-
25
- const clampIndex = (nav, length) => {
26
- if (!length) {
27
- nav.index = 0;
28
- return;
4
+ const { listPath, isTaskItem, isTasksEntry, TASKS_FILE } = files;
5
+
6
+ const LIST_PANES = ['files', 'branches', 'commits', 'npm'];
7
+
8
+ const freshListScroll = () =>
9
+ Object.fromEntries(LIST_PANES.map((pane) => [pane, 0]));
10
+
11
+ class Navigation {
12
+ constructor(options = {}) {
13
+ this.reset(options.startPane ?? 'files');
29
14
  }
30
- if (nav.index < 0) nav.index = 0;
31
- if (nav.index >= length) nav.index = length - 1;
32
- };
33
-
34
- const current = (nav, items, todoItem) => {
35
- if (nav.todoOpen) return todoItem;
36
- if (nav.pane === 'unit' && nav.reviewPath) {
37
- const item = items[nav.index];
38
- if (item && listPath(item) === nav.reviewPath) return item;
39
- for (const entry of items) {
40
- if (listPath(entry) === nav.reviewPath) return entry;
15
+
16
+ reset(startPane = this.pane) {
17
+ this.pane = startPane;
18
+ this.index = 0;
19
+ this.fileCursor = 0;
20
+ this.branchCursor = 0;
21
+ this.commitCursor = 0;
22
+ this.npmCursor = 0;
23
+ this.scroll = 0;
24
+ this.listScrollByPane = freshListScroll();
25
+ this.reviewPath = null;
26
+ this.fileScope = 'diff';
27
+ this.unitLine = 0;
28
+ this.selection = null;
29
+ this.mouseAnchor = null;
30
+ this.pendingClick = null;
31
+ this.lastClick = null;
32
+ this.tasksOpen = false;
33
+ this.tasksFocus = 0;
34
+ }
35
+
36
+ clampIndex(length) {
37
+ if (!length) {
38
+ this.index = 0;
39
+ return;
41
40
  }
42
- return null;
41
+ if (this.index < 0) this.index = 0;
42
+ if (this.index >= length) this.index = length - 1;
43
43
  }
44
- return items[nav.index] ?? null;
45
- };
46
-
47
- const syncReviewPath = (nav, item) => {
48
- if (nav.pane !== 'diff' && nav.pane !== 'unit') return;
49
- if (nav.pane === 'unit' && nav.reviewPath) return;
50
- nav.reviewPath = item ? listPath(item) : null;
51
- };
52
-
53
- const clampFileCursor = (nav, length) => {
54
- if (!length) {
55
- nav.fileCursor = 0;
56
- return;
44
+
45
+ current(items, tasksItem) {
46
+ if (this.tasksOpen) return tasksItem;
47
+ if (this.pane === 'unit' && this.reviewPath) {
48
+ const item = items[this.index];
49
+ if (item && listPath(item) === this.reviewPath) return item;
50
+ for (const entry of items) {
51
+ if (listPath(entry) === this.reviewPath) return entry;
52
+ }
53
+ return null;
54
+ }
55
+ return items[this.index] ?? null;
57
56
  }
58
- if (nav.fileCursor >= length) nav.fileCursor = length - 1;
59
- };
60
57
 
61
- const followReviewPath = (nav, fileList, item) => {
62
- if (!fileList.length) {
63
- nav.fileCursor = 0;
64
- return;
58
+ syncReviewPath(item) {
59
+ if (this.pane !== 'diff' && this.pane !== 'unit') return;
60
+ if (this.pane === 'unit' && this.reviewPath) return;
61
+ this.reviewPath = item ? listPath(item) : null;
65
62
  }
66
- let idx = fileList.findIndex(
67
- (entry) => !isTodosEntry(entry) && entry.path === nav.reviewPath,
68
- );
69
- if (nav.reviewPath === TODO_FILE || isTodoItem(item)) {
70
- idx = fileList.findIndex((entry) => isTodosEntry(entry));
63
+
64
+ clampFileCursor(length) {
65
+ if (!length) {
66
+ this.fileCursor = 0;
67
+ return;
68
+ }
69
+ if (this.fileCursor >= length) this.fileCursor = length - 1;
71
70
  }
72
- if (idx >= 0) {
73
- nav.fileCursor = idx;
74
- return;
71
+
72
+ followReviewPath(fileList, item) {
73
+ if (!fileList.length) {
74
+ this.fileCursor = 0;
75
+ return;
76
+ }
77
+ let idx = fileList.findIndex(
78
+ (entry) => !isTasksEntry(entry) && entry.path === this.reviewPath,
79
+ );
80
+ if (this.reviewPath === TASKS_FILE || isTaskItem(item)) {
81
+ idx = fileList.findIndex((entry) => isTasksEntry(entry));
82
+ }
83
+ if (idx >= 0) {
84
+ this.fileCursor = idx;
85
+ return;
86
+ }
87
+ this.clampFileCursor(fileList.length);
88
+ }
89
+
90
+ restoreFileCursor(fileList) {
91
+ if (this.pane !== 'files') return;
92
+ this.followReviewPath(fileList, null);
93
+ }
94
+
95
+ clearSelection() {
96
+ this.selection = null;
97
+ this.mouseAnchor = null;
98
+ this.pendingClick = null;
99
+ }
100
+
101
+ get listScroll() {
102
+ return this.listScrollByPane[this.pane] ?? 0;
103
+ }
104
+
105
+ set listScroll(value) {
106
+ if (!LIST_PANES.includes(this.pane)) return;
107
+ this.listScrollByPane[this.pane] = value ?? 0;
75
108
  }
76
- clampFileCursor(nav, fileList.length);
77
- };
78
-
79
- const restoreFileCursor = (nav, fileList) => {
80
- if (nav.pane !== 'files') return;
81
- followReviewPath(nav, fileList, null);
82
- };
83
-
84
- const clearSelection = (nav) => {
85
- nav.selection = null;
86
- nav.mouseAnchor = null;
87
- nav.pendingClick = null;
88
- };
89
-
90
- const createNavigation = (options = {}) => {
91
- const nav = initialState(options);
92
- nav.reset = (startPane) => {
93
- const pane = startPane ?? nav.pane;
94
- Object.assign(nav, initialState({ startPane: pane }));
95
- };
96
- nav.clampIndex = (length) => clampIndex(nav, length);
97
- nav.current = (items, todoItem) => current(nav, items, todoItem);
98
- nav.syncReviewPath = (item) => syncReviewPath(nav, item);
99
- nav.clampFileCursor = (length) => clampFileCursor(nav, length);
100
- nav.followReviewPath = (list, item) => followReviewPath(nav, list, item);
101
- nav.restoreFileCursor = (list) => restoreFileCursor(nav, list);
102
- nav.clearSelection = () => clearSelection(nav);
103
- return nav;
104
- };
105
-
106
- module.exports = { createNavigation };
109
+
110
+ resetListScroll(pane = this.pane) {
111
+ if (!LIST_PANES.includes(pane)) return;
112
+ this.listScrollByPane[pane] = 0;
113
+ }
114
+ }
115
+
116
+ module.exports = { Navigation };