reslop 0.1.8 → 0.2.0

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 (95) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +27 -38
  3. package/bin/reslop.js +2 -2
  4. package/lib/ansi.js +6 -2
  5. package/lib/api.js +2 -2
  6. package/lib/capabilities.js +10 -14
  7. package/lib/cli.js +28 -47
  8. package/lib/clipboard.js +1 -1
  9. package/lib/deps-diff.js +712 -0
  10. package/lib/deps.js +556 -32
  11. package/lib/detect.js +1 -7
  12. package/lib/diff/align.js +7 -10
  13. package/lib/diff/diff.js +53 -0
  14. package/lib/diff/display.js +5 -4
  15. package/lib/diff/inline.js +26 -21
  16. package/lib/diff/items.js +6 -4
  17. package/lib/diff/patch.js +1 -3
  18. package/lib/diff/unit.js +392 -0
  19. package/lib/files.js +34 -8
  20. package/lib/{git/branches.js → git-branches.js} +103 -10
  21. package/lib/{git/dependencies.js → git-deps.js} +111 -38
  22. package/lib/git.js +592 -65
  23. package/lib/github.js +28 -197
  24. package/lib/gitlab.js +24 -195
  25. package/lib/highlight/bash.js +1 -3
  26. package/lib/highlight/css.js +1 -3
  27. package/lib/highlight/js.js +11 -14
  28. package/lib/highlight.js +6 -9
  29. package/lib/keys.js +22 -9
  30. package/lib/manifest.js +330 -0
  31. package/lib/remote.js +374 -0
  32. package/lib/render/branches.js +34 -66
  33. package/lib/render/chrome.js +57 -22
  34. package/lib/render/commits.js +85 -0
  35. package/lib/render/diff.js +16 -16
  36. package/lib/render/files.js +29 -37
  37. package/lib/render/frame.js +29 -13
  38. package/lib/render/notes.js +4 -3
  39. package/lib/render/primitives.js +46 -1
  40. package/lib/{render.js → render/render.js} +6 -7
  41. package/lib/render/unit.js +41 -0
  42. package/lib/review.js +750 -14
  43. package/lib/select.js +5 -10
  44. package/lib/session/accessors.js +9 -1
  45. package/lib/session/actions.js +93 -36
  46. package/lib/session/branches.js +130 -144
  47. package/lib/session/change-actions.js +206 -124
  48. package/lib/session/commit.js +217 -62
  49. package/lib/session/compose.js +158 -13
  50. package/lib/session/dispatch.js +109 -55
  51. package/lib/session/files-pane.js +145 -142
  52. package/lib/session/items.js +131 -34
  53. package/lib/session/lifecycle.js +82 -42
  54. package/lib/session/load.js +19 -33
  55. package/lib/session/navigation.js +15 -3
  56. package/lib/session/ops.js +28 -41
  57. package/lib/session/pointer.js +58 -54
  58. package/lib/session/review.js +23 -37
  59. package/lib/session/terminal.js +6 -12
  60. package/lib/session/todos.js +8 -12
  61. package/lib/session/unit.js +208 -0
  62. package/lib/session/update.js +33 -46
  63. package/lib/session/view.js +82 -11
  64. package/lib/session/watch.js +11 -19
  65. package/lib/session.js +29 -226
  66. package/lib/source.js +3 -3
  67. package/lib/update.js +40 -75
  68. package/lib/{git/proc.js → utilities.js} +75 -11
  69. package/lib/wrap.js +1 -3
  70. package/package.json +2 -2
  71. package/lib/deps/changes.js +0 -331
  72. package/lib/deps/items.js +0 -131
  73. package/lib/deps/manifest.js +0 -145
  74. package/lib/deps/merge.js +0 -120
  75. package/lib/deps/proposals.js +0 -164
  76. package/lib/deps/reports.js +0 -180
  77. package/lib/deps/rows.js +0 -177
  78. package/lib/deps/scan.js +0 -138
  79. package/lib/deps/usage.js +0 -68
  80. package/lib/deps/versions.js +0 -119
  81. package/lib/diff.js +0 -33
  82. package/lib/git/changes.js +0 -164
  83. package/lib/git/read.js +0 -253
  84. package/lib/git/run.js +0 -100
  85. package/lib/npm/commands.js +0 -100
  86. package/lib/remote/changes.js +0 -37
  87. package/lib/remote/headers.js +0 -36
  88. package/lib/remote/http.js +0 -122
  89. package/lib/remote/notes.js +0 -94
  90. package/lib/review/format.js +0 -434
  91. package/lib/review/storage.js +0 -150
  92. package/lib/review/store.js +0 -178
  93. package/lib/review/templates.js +0 -49
  94. package/lib/session/origins.js +0 -7
  95. package/lib/sys.js +0 -84
@@ -1,9 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ const { stateView } = require('./accessors.js');
4
+
3
5
  const files = require('../files.js');
4
6
  const { itemPath, TODO_FILE, isTodoItem } = files;
5
7
  const { feedbackKey } = require('../review.js');
6
- const { refreshIndexPatches } = require('../diff.js');
8
+ const { refreshIndexPatches } = require('../diff/diff.js');
7
9
 
8
10
  const depChangeKey = (item) => {
9
11
  const change = item.dep && item.dep.change;
@@ -21,25 +23,125 @@ const npmBusyKind = (item) => {
21
23
  return 'npm i';
22
24
  };
23
25
 
24
- const itemKey = (item) => {
26
+ const blockKey = (item) => {
25
27
  if (!item || !item.file) return '';
26
28
  const rel = itemPath(item);
27
29
  if (isTodoItem(item)) return `todo:${TODO_FILE}`;
28
- const origin = item.origin ?? '';
29
30
  const depId = depChangeKey(item);
30
- if (depId) return `${origin}:${rel}:dep:${depId}`;
31
+ if (depId) return `${rel}:dep:${depId}`;
31
32
  if (!item.hunk) {
32
33
  const block = item.blockId ?? '-';
33
- return `${origin}:${rel}:file:${block}`;
34
+ return `${rel}:file:${block}`;
34
35
  }
35
36
  const oldStart = item.hunk.oldStart;
36
37
  const newStart = item.hunk.newStart;
37
38
  const block = item.blockId ?? '-';
38
- return `${origin}:${rel}:${oldStart}:${newStart}:${block}`;
39
+ return `${rel}:${oldStart}:${newStart}:${block}`;
40
+ };
41
+
42
+ const itemKey = (item) => {
43
+ const key = blockKey(item);
44
+ if (!key || isTodoItem(item)) return key;
45
+ const origin = item.origin ?? '';
46
+ return `${origin}:${key}`;
47
+ };
48
+
49
+ const blockLinesKey = (item) => {
50
+ if (!item || !item.file || isTodoItem(item)) return '';
51
+ if (!item.hunk) return '';
52
+ const parts = [itemPath(item)];
53
+ for (const line of item.hunk.lines) {
54
+ if (line.blockId !== item.blockId) continue;
55
+ if (line.type === 'ctx') continue;
56
+ parts.push(`${line.type}:${line.text}`);
57
+ }
58
+ return parts.join('\0');
59
+ };
60
+
61
+ const pushBucket = (map, key, item) => {
62
+ if (!key) return;
63
+ const list = map.get(key) ?? [];
64
+ list.push(item);
65
+ map.set(key, list);
66
+ };
67
+
68
+ const takeBucket = (map, key, used) => {
69
+ if (!key) return null;
70
+ const list = map.get(key);
71
+ if (!list || !list.length) return null;
72
+ while (list.length) {
73
+ const item = list.shift();
74
+ if (!used.has(item)) return item;
75
+ }
76
+ return null;
77
+ };
78
+
79
+ const groupByPath = (items) => {
80
+ const groups = new Map();
81
+ for (const item of items) {
82
+ const rel = itemPath(item) || '';
83
+ const list = groups.get(rel) ?? [];
84
+ list.push(item);
85
+ groups.set(rel, list);
86
+ }
87
+ return groups;
88
+ };
89
+
90
+ const alignGroup = (prev, next) => {
91
+ if (!prev.length) return next;
92
+ if (!next.length) return [];
93
+ const byBlock = new Map();
94
+ const byLines = new Map();
95
+ for (const item of next) {
96
+ pushBucket(byBlock, blockKey(item), item);
97
+ pushBucket(byLines, blockLinesKey(item), item);
98
+ }
99
+ const used = new Set();
100
+ const ordered = [];
101
+ for (const item of prev) {
102
+ const byText = takeBucket(byLines, blockLinesKey(item), used);
103
+ const taken = byText ?? takeBucket(byBlock, blockKey(item), used);
104
+ if (!taken) continue;
105
+ used.add(taken);
106
+ ordered.push(taken);
107
+ }
108
+ for (const item of next) {
109
+ if (used.has(item)) continue;
110
+ ordered.push(item);
111
+ }
112
+ return ordered;
113
+ };
114
+
115
+ const alignLoadedItems = (prev, next) => {
116
+ if (!prev.length || !next.length) return next;
117
+ const nextGroups = groupByPath(next);
118
+ const seen = new Set();
119
+ const ordered = [];
120
+ for (const [rel, list] of groupByPath(prev)) {
121
+ seen.add(rel);
122
+ const aligned = alignGroup(list, nextGroups.get(rel) ?? []);
123
+ for (const item of aligned) ordered.push(item);
124
+ }
125
+ for (const [rel, list] of nextGroups) {
126
+ if (seen.has(rel)) continue;
127
+ for (const item of list) ordered.push(item);
128
+ }
129
+ return ordered;
39
130
  };
40
131
 
41
132
  const restoredIndex = (items, item) => {
42
133
  if (!item || !item.file) return -1;
134
+ const origin = item.origin ?? '';
135
+ const lines = blockLinesKey(item);
136
+ if (lines) {
137
+ let any = -1;
138
+ for (let i = 0; i < items.length; i++) {
139
+ if (blockLinesKey(items[i]) !== lines) continue;
140
+ if (any < 0) any = i;
141
+ if ((items[i].origin ?? '') === origin) return i;
142
+ }
143
+ if (any >= 0) return any;
144
+ }
43
145
  const key = itemKey(item);
44
146
  if (key) {
45
147
  const exact = items.findIndex((entry) => itemKey(entry) === key);
@@ -47,7 +149,6 @@ const restoredIndex = (items, item) => {
47
149
  }
48
150
  const rel = itemPath(item);
49
151
  if (!rel) return -1;
50
- const origin = item.origin ?? '';
51
152
  const start = item.hunk ? item.hunk.newStart : 0;
52
153
  let fileMatch = -1;
53
154
  let best = -1;
@@ -106,8 +207,9 @@ const createItemCollection = () => {
106
207
  const isDismissed = (item) => state.dismissed.has(itemKey(item));
107
208
 
108
209
  const replace = (items) => {
210
+ const aligned = alignLoadedItems(state.items, items);
109
211
  const next = [];
110
- for (const item of items) {
212
+ for (const item of aligned) {
111
213
  if (isDismissed(item)) continue;
112
214
  next.push(item);
113
215
  }
@@ -123,18 +225,15 @@ const createItemCollection = () => {
123
225
  state.dismissed.clear();
124
226
  };
125
227
 
126
- const findIndexByKey = (key) => {
127
- if (!key) return -1;
128
- return state.items.findIndex((item) => itemKey(item) === key);
129
- };
130
-
131
228
  const findRestoredIndex = (item) => restoredIndex(state.items, item);
132
229
 
133
230
  const keepOrigin = (item, origin) => {
134
231
  const next = { ...item, origin };
135
- const replaced = [];
136
- for (const entry of state.items) {
137
- replaced.push(sameBlock(entry, item) ? next : entry);
232
+ const { length } = state.items;
233
+ const replaced = new Array(length);
234
+ for (let i = 0; i < length; i++) {
235
+ const entry = state.items[i];
236
+ replaced[i] = sameBlock(entry, item) ? next : entry;
138
237
  }
139
238
  if (next.dep) {
140
239
  state.items = replaced;
@@ -143,33 +242,31 @@ const createItemCollection = () => {
143
242
  state.items = refreshIndexPatches(replaced, next);
144
243
  };
145
244
 
146
- return {
147
- get items() {
148
- return state.items;
149
- },
150
- set items(value) {
151
- state.items = value;
245
+ return stateView(
246
+ state,
247
+ {
248
+ reset,
249
+ replace,
250
+ dismiss,
251
+ clearDismissed,
252
+ isDismissed,
253
+ findRestoredIndex,
254
+ keepOrigin,
152
255
  },
153
- get dismissed() {
154
- return state.dismissed;
155
- },
156
- reset,
157
- replace,
158
- dismiss,
159
- clearDismissed,
160
- isDismissed,
161
- findIndexByKey,
162
- findRestoredIndex,
163
- keepOrigin,
164
- };
256
+ ['items'],
257
+ ['dismissed'],
258
+ );
165
259
  };
166
260
 
167
261
  module.exports = {
168
262
  depChangeKey,
169
263
  needsReload,
170
264
  npmBusyKind,
265
+ blockKey,
171
266
  itemKey,
172
267
  itemFeedKey,
268
+ blockLinesKey,
269
+ alignLoadedItems,
173
270
  restoredIndex,
174
271
  sameBlock,
175
272
  canEditCode,
@@ -4,10 +4,10 @@ const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
 
6
6
  const { isAbortError } = require('./load.js');
7
- const { createDiskWatcher, POLL_MS } = require('./watch.js');
7
+ const { createDiskWatcher, POLL_MS, DEBOUNCE_MS } = require('./watch.js');
8
8
  const files = require('../files.js');
9
- const { itemPath } = files;
10
- const { MANIFEST, LOCKFILE } = require('../deps/manifest.js');
9
+ const { itemPath, relativeAge } = files;
10
+ const { MANIFEST, LOCKFILE } = require('../deps.js');
11
11
  const review = require('../review.js');
12
12
  const { hasNotes } = review;
13
13
 
@@ -30,8 +30,6 @@ const shouldDeferExtras = (api) => {
30
30
  return audit && (waitingOutdated || waitingAudit);
31
31
  };
32
32
 
33
- const watchOpts = (api) => api.watch ?? {};
34
-
35
33
  const endReadLoop = (api) => {
36
34
  const finish = api.ui.finishReadLoop;
37
35
  if (finish) finish();
@@ -50,8 +48,6 @@ const kindFromExtra = (extra = {}) => {
50
48
  return 'reload';
51
49
  };
52
50
 
53
- const shouldResetNav = (kind) => kind !== 'open' && kind !== 'watch';
54
-
55
51
  const finishIfEmpty = (api, kind) => {
56
52
  const { collection, review: notes, ui, nav } = api;
57
53
  if (collection.items.length) return;
@@ -103,9 +99,47 @@ const itemRels = (collection) => {
103
99
  return rels;
104
100
  };
105
101
 
102
+ const fileDatesFromDisk = (top, rels) => {
103
+ const dates = new Map();
104
+ if (!top) return dates;
105
+ const now = Date.now();
106
+ for (const rel of rels) {
107
+ try {
108
+ const st = fs.statSync(path.join(top, rel));
109
+ dates.set(rel, relativeAge(st.mtimeMs, now));
110
+ } catch {
111
+ continue;
112
+ }
113
+ }
114
+ return dates;
115
+ };
116
+
117
+ const stampItemDates = (items, dates) => {
118
+ for (const item of items) {
119
+ const date = dates.get(itemPath(item));
120
+ if (date) item.date = date;
121
+ }
122
+ };
123
+
124
+ const syncUnitEditor = (api) => {
125
+ const composer = api.ui.composer;
126
+ if (!composer || typeof composer.applyDiskText !== 'function') return;
127
+ if (api.nav.pane !== 'unit') return;
128
+ const rel = api.nav.reviewPath;
129
+ if (!rel) return;
130
+ const repo = api.ui.repo;
131
+ if (!repo || typeof repo.fileText !== 'function') return;
132
+ const text = repo.fileText(api.ui.top || api.loadConfig.cwd, rel, api.ui.rev);
133
+ composer.applyDiskText(text);
134
+ };
135
+
106
136
  const syncItemWatches = (api) => {
107
137
  if (!api.diskWatcher) return;
108
- api.diskWatcher.watchFiles(itemRels(api.collection));
138
+ const rels = itemRels(api.collection);
139
+ if (api.nav.pane === 'unit' && api.nav.reviewPath) {
140
+ if (!rels.includes(api.nav.reviewPath)) rels.push(api.nav.reviewPath);
141
+ }
142
+ api.diskWatcher.watchFiles(rels);
109
143
  };
110
144
 
111
145
  const applyLoaded = (api, loaded) => {
@@ -117,6 +151,10 @@ const applyLoaded = (api, loaded) => {
117
151
  const here = ui.current();
118
152
  ui.top = loaded.top ?? api.loadConfig.cwd;
119
153
  collection.replace(loaded.items);
154
+ stampItemDates(
155
+ collection.items,
156
+ fileDatesFromDisk(ui.top, itemRels(collection)),
157
+ );
120
158
  if (loaded.rev) ui.rev = loaded.rev;
121
159
  if (loaded.revShort) {
122
160
  ui.revShort = loaded.revShort;
@@ -134,6 +172,7 @@ const applyLoaded = (api, loaded) => {
134
172
  markLoaded(api);
135
173
  loader.pendingExtras = loaded.pending === true;
136
174
  syncItemWatches(api);
175
+ syncUnitEditor(api);
137
176
  return true;
138
177
  };
139
178
 
@@ -147,10 +186,10 @@ const load = (api) => {
147
186
  applyLoaded(api, loaded);
148
187
  };
149
188
 
150
- const fileStamp = (top, rel, statSync) => {
189
+ const fileStamp = (top, rel) => {
151
190
  const full = path.join(top, rel);
152
191
  try {
153
- const stat = statSync(full);
192
+ const stat = fs.statSync(full);
154
193
  return `${stat.mtimeMs}:${stat.size}`;
155
194
  } catch {
156
195
  return 'missing';
@@ -158,11 +197,9 @@ const fileStamp = (top, rel, statSync) => {
158
197
  };
159
198
 
160
199
  const npmStamp = (api) => {
161
- const extra = watchOpts(api);
162
- const statSync = extra.statSync ?? fs.statSync;
163
200
  const top = api.ui.top || api.loadConfig.cwd;
164
- const pkg = fileStamp(top, MANIFEST, statSync);
165
- const lock = fileStamp(top, LOCKFILE, statSync);
201
+ const pkg = fileStamp(top, MANIFEST);
202
+ const lock = fileStamp(top, LOCKFILE);
166
203
  return `${pkg}|${lock}`;
167
204
  };
168
205
 
@@ -190,8 +227,7 @@ const followExtras = async (api, loaded, gen, kind) => {
190
227
  next = await api.loader.fetchExtras(api.loadConfig.repo, loaded, extra);
191
228
  } catch (error) {
192
229
  if (!api.loader.isCurrent(gen)) return;
193
- failOpen(api, error);
194
- return;
230
+ return void failOpen(api, error);
195
231
  }
196
232
  if (!api.loader.isCurrent(gen)) return;
197
233
  if (api.ui.done) {
@@ -241,9 +277,8 @@ const resetNavAfterLoad = (api, extra = {}) => {
241
277
  };
242
278
 
243
279
  const afterSnapshot = (api, kind, extra = {}) => {
244
- if (shouldResetNav(kind)) {
245
- resetNavAfterLoad(api, extra);
246
- return;
280
+ if (kind !== 'open' && kind !== 'watch') {
281
+ return void resetNavAfterLoad(api, extra);
247
282
  }
248
283
  if (extra.afterLoad) extra.afterLoad();
249
284
  };
@@ -270,8 +305,7 @@ const runSnapshot = async (api, gen, kind, extra = {}) => {
270
305
  );
271
306
  } catch (error) {
272
307
  if (!api.loader.isCurrent(gen)) return;
273
- failOpen(api, error);
274
- return;
308
+ return void failOpen(api, error);
275
309
  }
276
310
  if (!api.loader.isCurrent(gen)) return;
277
311
  if (api.ui.done) {
@@ -328,8 +362,7 @@ const refreshFromRepo = (api, extra = {}) => {
328
362
  afterSnapshot(api, kind, extra);
329
363
  if (loaded.pending) {
330
364
  api.ops.setBusy('checking npm');
331
- followExtras(api, loaded, gen, kind);
332
- return;
365
+ return void followExtras(api, loaded, gen, kind);
333
366
  }
334
367
  if (kind === 'keep') return;
335
368
  finishIfEmpty(api, kind);
@@ -340,17 +373,18 @@ const refreshFromRepo = (api, extra = {}) => {
340
373
  };
341
374
 
342
375
  const reloadAfterChange = (api, afterLoad) => {
343
- const here = api.nav.index;
376
+ const hereItem = api.ui.current();
377
+ const hereIndex = api.nav.index;
344
378
  const restore = () => {
345
- api.nav.index = here;
379
+ const idx = api.collection.findRestoredIndex(hereItem);
380
+ api.nav.index = idx >= 0 ? idx : hereIndex;
346
381
  api.ui.clampIndex();
347
382
  api.ui.syncReviewPath();
348
383
  api.ui.syncFileCursor();
349
384
  if (afterLoad) afterLoad();
350
385
  };
351
386
  if (api.ui.uiOpen && api.loadConfig.repo.loadAsync) {
352
- refreshFromRepo(api, { afterLoad: restore });
353
- return;
387
+ return void refreshFromRepo(api, { afterLoad: restore });
354
388
  }
355
389
  refreshFromRepo(api);
356
390
  restore();
@@ -364,9 +398,7 @@ const onReload = (api) => {
364
398
 
365
399
  const clearWatchRetry = (api) => {
366
400
  if (api.watchRetry === null) return;
367
- const extra = watchOpts(api);
368
- const clearTimeoutFn = extra.clearTimeout ?? clearTimeout;
369
- clearTimeoutFn(api.watchRetry);
401
+ clearTimeout(api.watchRetry);
370
402
  api.watchRetry = null;
371
403
  };
372
404
 
@@ -376,35 +408,43 @@ const shouldDeferWatch = (api) => {
376
408
  if (!ui.uiOpen) return true;
377
409
  if (!api.loader.didLoad) return true;
378
410
  if (ops.gitBusy) return true;
411
+ if (ui.mode === 'compose') {
412
+ const kind = ui.composer && ui.composer.state.composeKind;
413
+ if (kind === 'file') return false;
414
+ }
379
415
  if (ui.mode !== 'review') return true;
380
416
  return false;
381
417
  };
382
418
 
419
+ const WATCH_IGNORE_MS = 500;
420
+
421
+ const ignoreWatch = (api, ms = WATCH_IGNORE_MS) => {
422
+ api.ignoreWatchUntil = Date.now() + ms;
423
+ };
424
+
383
425
  const onDiskChange = (api) => {
426
+ if (Date.now() < (api.ignoreWatchUntil ?? 0)) return;
384
427
  if (shouldDeferWatch(api)) {
385
428
  if (api.watchRetry !== null) return;
386
- const extra = watchOpts(api);
387
- const setTimeoutFn = extra.setTimeout ?? setTimeout;
388
- const delay = extra.debounceMs ?? 200;
389
- api.watchRetry = setTimeoutFn(() => {
429
+ api.watchRetry = setTimeout(() => {
390
430
  api.watchRetry = null;
391
431
  onDiskChange(api);
392
- }, delay);
432
+ }, DEBOUNCE_MS);
433
+ if (typeof api.watchRetry.unref === 'function') api.watchRetry.unref();
393
434
  return;
394
435
  }
395
436
  refreshFromRepo(api, { keepView: true, quiet: true });
396
437
  };
397
438
 
398
439
  const currentFileStamp = (api) => {
399
- if (api.nav.pane !== 'diff') return '';
400
- const rel = itemPath(api.ui.current());
440
+ const pane = api.nav.pane;
441
+ if (pane !== 'diff' && pane !== 'unit') return '';
442
+ const rel = pane === 'unit' ? api.nav.reviewPath : itemPath(api.ui.current());
401
443
  if (!rel) return '';
402
- const extra = watchOpts(api);
403
- const statSync = extra.statSync ?? fs.statSync;
404
444
  const full = path.join(api.ui.top || api.loadConfig.cwd, rel);
405
445
  let stat;
406
446
  try {
407
- stat = statSync(full);
447
+ stat = fs.statSync(full);
408
448
  } catch {
409
449
  return `${rel}:missing`;
410
450
  }
@@ -440,12 +480,10 @@ const startWatch = (api) => {
440
480
  if (!isWorktree(api.ui)) return;
441
481
  const root = api.ui.top || api.loadConfig.cwd;
442
482
  if (!root) return;
443
- const extra = watchOpts(api);
444
483
  api.diskWatcher = createDiskWatcher({
445
484
  root,
446
485
  paths: api.loadConfig.paths,
447
486
  onChange: () => onDiskChange(api),
448
- ...extra,
449
487
  });
450
488
  syncItemWatches(api);
451
489
  if (!api.ui.term) return;
@@ -456,6 +494,7 @@ const createLifecycle = (api) => {
456
494
  api.diskWatcher = null;
457
495
  api.watchRetry = null;
458
496
  api.watchStamp = '';
497
+ api.ignoreWatchUntil = 0;
459
498
  api.npmExtras = null;
460
499
  return {
461
500
  applyLoaded: (loaded) => applyLoaded(api, loaded),
@@ -469,6 +508,7 @@ const createLifecycle = (api) => {
469
508
  pollCurrentFile: () => pollCurrentFile(api),
470
509
  startWatch: () => startWatch(api),
471
510
  stopWatch: () => stopWatch(api),
511
+ ignoreWatch: (ms) => ignoreWatch(api, ms),
472
512
  clearNpmExtras: () => clearNpmExtras(api),
473
513
  };
474
514
  };
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { stateView } = require('./accessors.js');
4
+
3
5
  const isAbortError = (error) => !!(error && error.code === 'ABORT');
4
6
 
5
7
  const createLoadCoordinator = () => {
@@ -46,40 +48,24 @@ const createLoadCoordinator = () => {
46
48
  return repo.loadExtras(loaded, extra);
47
49
  };
48
50
 
49
- return {
50
- get gen() {
51
- return state.gen;
52
- },
53
- get promise() {
54
- return state.promise;
55
- },
56
- set promise(value) {
57
- state.promise = value;
58
- },
59
- get pendingExtras() {
60
- return state.pendingExtras;
61
- },
62
- set pendingExtras(value) {
63
- state.pendingExtras = value;
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,
64
65
  },
65
- get didLoad() {
66
- return state.didLoad;
67
- },
68
- set didLoad(value) {
69
- state.didLoad = value;
70
- },
71
- get signal() {
72
- return state.abort?.signal;
73
- },
74
- bump,
75
- isCurrent,
76
- accept,
77
- abort,
78
- reset,
79
- tag,
80
- fetchSnapshot,
81
- fetchExtras,
82
- };
66
+ ['promise', 'pendingExtras', 'didLoad'],
67
+ ['gen'],
68
+ );
83
69
  };
84
70
 
85
71
  module.exports = { isAbortError, createLoadCoordinator };
@@ -1,16 +1,19 @@
1
1
  'use strict';
2
2
 
3
3
  const files = require('../files.js');
4
- const { itemPath, isTodoItem, isTodosEntry, TODO_FILE } = files;
4
+ const { listPath, isTodoItem, isTodosEntry, TODO_FILE } = files;
5
5
 
6
6
  const initialState = (options = {}) => ({
7
7
  pane: options.startPane ?? 'files',
8
8
  index: 0,
9
9
  fileCursor: 0,
10
10
  branchCursor: 0,
11
+ commitCursor: 0,
11
12
  scroll: 0,
12
13
  listScroll: 0,
13
14
  reviewPath: null,
15
+ fileScope: 'diff',
16
+ unitLine: 0,
14
17
  selection: null,
15
18
  mouseAnchor: null,
16
19
  pendingClick: null,
@@ -29,12 +32,21 @@ const clampIndex = (nav, length) => {
29
32
 
30
33
  const current = (nav, items, todoItem) => {
31
34
  if (nav.todoOpen) return todoItem;
35
+ if (nav.pane === 'unit' && nav.reviewPath) {
36
+ const item = items[nav.index];
37
+ if (item && listPath(item) === nav.reviewPath) return item;
38
+ for (const entry of items) {
39
+ if (listPath(entry) === nav.reviewPath) return entry;
40
+ }
41
+ return null;
42
+ }
32
43
  return items[nav.index] ?? null;
33
44
  };
34
45
 
35
46
  const syncReviewPath = (nav, item) => {
36
- if (nav.pane !== 'diff') return;
37
- nav.reviewPath = item ? itemPath(item) : null;
47
+ if (nav.pane !== 'diff' && nav.pane !== 'unit') return;
48
+ if (nav.pane === 'unit' && nav.reviewPath) return;
49
+ nav.reviewPath = item ? listPath(item) : null;
38
50
  };
39
51
 
40
52
  const clampFileCursor = (nav, length) => {