reslop 0.2.5 → 0.2.6

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 (105) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +32 -19
  6. package/lib/capabilities.js +8 -8
  7. package/lib/cli.js +40 -74
  8. package/lib/clipboard.js +39 -47
  9. package/lib/dashboard/git.js +128 -0
  10. package/lib/dashboard/model.js +332 -0
  11. package/lib/dashboard/npm.js +193 -0
  12. package/lib/dashboard/tree.js +278 -0
  13. package/lib/deps-diff.js +197 -402
  14. package/lib/deps.js +113 -240
  15. package/lib/detect.js +8 -18
  16. package/lib/diff/align.js +41 -113
  17. package/lib/diff/diff.js +12 -43
  18. package/lib/diff/display.js +25 -36
  19. package/lib/diff/edit.js +6 -20
  20. package/lib/diff/inline.js +76 -144
  21. package/lib/diff/items.js +33 -47
  22. package/lib/diff/parse.js +28 -56
  23. package/lib/diff/patch.js +12 -33
  24. package/lib/diff/unit.js +79 -178
  25. package/lib/editor.js +52 -69
  26. package/lib/files.js +71 -89
  27. package/lib/git-branches.js +254 -416
  28. package/lib/git-deps.js +147 -272
  29. package/lib/git-reword-todo.js +5 -13
  30. package/lib/git-worktree.js +141 -212
  31. package/lib/git.js +21 -49
  32. package/lib/github.js +67 -181
  33. package/lib/gitlab.js +40 -90
  34. package/lib/highlight/bash.js +55 -177
  35. package/lib/highlight/core.js +155 -141
  36. package/lib/highlight/css.js +25 -58
  37. package/lib/highlight/csv.js +19 -37
  38. package/lib/highlight/dart.js +72 -213
  39. package/lib/highlight/dot.js +14 -14
  40. package/lib/highlight/html.js +42 -63
  41. package/lib/highlight/js.js +177 -506
  42. package/lib/highlight/json.js +17 -37
  43. package/lib/highlight/log.js +19 -25
  44. package/lib/highlight/txt.js +7 -10
  45. package/lib/highlight.js +17 -46
  46. package/lib/keys.js +49 -82
  47. package/lib/manifest.js +67 -126
  48. package/lib/npm-commands.js +120 -169
  49. package/lib/remote.js +140 -190
  50. package/lib/render/branches.js +63 -61
  51. package/lib/render/chrome.js +220 -333
  52. package/lib/render/commits.js +160 -361
  53. package/lib/render/dash-activity.js +212 -0
  54. package/lib/render/dash-blocks.js +245 -0
  55. package/lib/render/dash-table.js +290 -0
  56. package/lib/render/dashboard.js +62 -0
  57. package/lib/render/diff.js +123 -258
  58. package/lib/render/files.js +40 -85
  59. package/lib/render/frame.js +78 -117
  60. package/lib/render/notes.js +139 -210
  61. package/lib/render/npm.js +213 -177
  62. package/lib/render/present.js +4 -11
  63. package/lib/render/primitives.js +72 -174
  64. package/lib/render/render.js +5 -14
  65. package/lib/render/report.js +43 -0
  66. package/lib/render/tiles.js +260 -0
  67. package/lib/render/unit.js +15 -31
  68. package/lib/report-model.js +110 -33
  69. package/lib/report-parse.js +56 -37
  70. package/lib/report-render.js +102 -59
  71. package/lib/report-run.js +87 -77
  72. package/lib/report-test.js +221 -99
  73. package/lib/report-tools.js +96 -154
  74. package/lib/review-merge.js +33 -41
  75. package/lib/review.js +68 -129
  76. package/lib/runs.js +225 -0
  77. package/lib/select.js +53 -91
  78. package/lib/session/accessors.js +14 -21
  79. package/lib/session/actions.js +61 -43
  80. package/lib/session/branches.js +166 -222
  81. package/lib/session/change-actions.js +49 -119
  82. package/lib/session/commit.js +274 -355
  83. package/lib/session/compose.js +556 -714
  84. package/lib/session/dashboard.js +385 -0
  85. package/lib/session/dispatch.js +162 -170
  86. package/lib/session/files-pane.js +76 -44
  87. package/lib/session/items.js +6 -17
  88. package/lib/session/lifecycle.js +393 -466
  89. package/lib/session/load.js +1 -14
  90. package/lib/session/navigation.js +2 -2
  91. package/lib/session/npm.js +403 -420
  92. package/lib/session/ops.js +5 -13
  93. package/lib/session/pointer.js +16 -14
  94. package/lib/session/progress.js +39 -30
  95. package/lib/session/review.js +1 -3
  96. package/lib/session/tasks.js +276 -235
  97. package/lib/session/templates.js +51 -59
  98. package/lib/session/terminal.js +85 -132
  99. package/lib/session/view.js +47 -81
  100. package/lib/session/watch.js +196 -212
  101. package/lib/session.js +81 -161
  102. package/lib/update.js +44 -111
  103. package/lib/utilities.js +46 -88
  104. package/lib/wrap.js +29 -55
  105. package/package.json +1 -1
@@ -3,10 +3,12 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
 
6
+ const utilities = require('../utilities.js');
7
+ const { isAbort } = utilities;
6
8
  const sessionLoad = require('./load.js');
7
- const { isAbortError, tagLoaded, fetchSnapshot, fetchExtras } = sessionLoad;
9
+ const { tagLoaded, fetchSnapshot, fetchExtras } = sessionLoad;
8
10
  const watch = require('./watch.js');
9
- const { createDiskWatcher, DEBOUNCE_MS } = watch;
11
+ const { DiskWatcher, DEBOUNCE_MS } = watch;
10
12
  const files = require('../files.js');
11
13
  const { itemPath, relativeAge } = files;
12
14
  const deps = require('../deps.js');
@@ -16,34 +18,7 @@ const { hasNotes, loadReview } = review;
16
18
  const reviewMerge = require('../review-merge.js');
17
19
  const { mergeReview } = reviewMerge;
18
20
 
19
- const loadOptions = (api, extra = {}) => {
20
- const { loadConfig, ui } = api;
21
- return {
22
- commit: ui.rev,
23
- audit: loadConfig.audit,
24
- outdatedMap: loadConfig.outdatedMap,
25
- auditMap: loadConfig.auditMap,
26
- ...extra,
27
- };
28
- };
29
-
30
- const shouldDeferExtras = (api) => {
31
- const { loadConfig } = api;
32
- const audit = !!loadConfig.audit;
33
- const waitingOutdated = loadConfig.outdatedMap === undefined;
34
- const waitingAudit = loadConfig.auditMap === undefined;
35
- return audit && (waitingOutdated || waitingAudit);
36
- };
37
-
38
- const endReadLoop = (api) => {
39
- const finish = api.ui.finishReadLoop;
40
- if (finish) finish();
41
- };
42
-
43
- const markLoaded = (api) => {
44
- api.loader.didLoad = true;
45
- api.updater.maybePrompt();
46
- };
21
+ const WATCH_IGNORE_MS = 500;
47
22
 
48
23
  const isWorktree = (ui) => !ui.rev && !ui.change;
49
24
 
@@ -53,503 +28,455 @@ const kindFromExtra = (extra = {}) => {
53
28
  return 'reload';
54
29
  };
55
30
 
56
- const finishIfEmpty = (api, kind) => {
57
- const { collection, review: notes, ui, nav } = api;
58
- if (collection.items.length) return;
59
- if (notes.store && hasNotes(notes.store)) return;
60
- if (kind === 'keep') return;
61
- if (isWorktree(ui)) {
62
- if (nav.pane === 'diff') nav.pane = 'files';
63
- return;
64
- }
65
- ui.done = true;
66
- ui.exitCode = 0;
67
- ui.status = 'nothing to review';
68
- ui.emptyReview = kind === 'open';
69
- endReadLoop(api);
70
- };
71
-
72
- const failOpen = (api, error) => {
73
- if (isAbortError(error)) return;
74
- const { ui, ops } = api;
75
- ui.loadError = error;
76
- ui.done = true;
77
- ui.exitCode = 1;
78
- ops.setBusy('');
79
- ui.stopProgress();
80
- endReadLoop(api);
81
- };
82
-
83
- const restoreAfterLoad = (api, here, wasTodo) => {
84
- const { collection, nav, ui } = api;
85
- nav.tasksOpen = wasTodo;
86
- if (here && !wasTodo) {
87
- const idx = collection.findRestoredIndex(here);
88
- if (idx >= 0) nav.index = idx;
89
- }
90
- ui.clampIndex();
91
- nav.restoreFileCursor(ui.fileList());
92
- ui.syncReviewPath();
93
- ui.syncFileCursor();
31
+ const statStamp = (file) => {
32
+ try {
33
+ const stat = fs.statSync(file);
34
+ return `${stat.mtimeMs}:${stat.size}`;
35
+ } catch {
36
+ return 'missing';
37
+ }
94
38
  };
95
39
 
96
- const itemRels = (collection) => {
97
- const rels = [];
98
- for (const item of collection.items) {
40
+ const itemRels = (items) => {
41
+ const rels = new Set();
42
+ for (const item of items) {
99
43
  const rel = itemPath(item);
100
- if (!rel) continue;
101
- if (rels.includes(rel)) continue;
102
- rels.push(rel);
44
+ if (rel) rels.add(rel);
103
45
  }
104
- return rels;
46
+ return [...rels];
105
47
  };
106
48
 
107
- const fileDatesFromDisk = (top, rels) => {
108
- const dates = new Map();
109
- if (!top) return dates;
49
+ const stampItemDates = (top, items) => {
50
+ if (!top) return;
110
51
  const now = Date.now();
111
- for (const rel of rels) {
52
+ const dates = new Map();
53
+ for (const rel of itemRels(items)) {
112
54
  try {
113
- const st = fs.statSync(path.join(top, rel));
114
- dates.set(rel, relativeAge(st.mtimeMs, now));
55
+ const stat = fs.statSync(path.join(top, rel));
56
+ dates.set(rel, relativeAge(stat.mtimeMs, now));
115
57
  } catch {
116
58
  continue;
117
59
  }
118
60
  }
119
- return dates;
120
- };
121
-
122
- const stampItemDates = (items, dates) => {
123
61
  for (const item of items) {
124
62
  const date = dates.get(itemPath(item));
125
63
  if (date) item.date = date;
126
64
  }
127
65
  };
128
66
 
129
- const syncUnitEditor = (api) => {
130
- const composer = api.ui.composer;
131
- if (!composer || typeof composer.applyDiskText !== 'function') return;
132
- if (api.nav.pane !== 'unit') return;
133
- const rel = api.nav.reviewPath;
134
- if (!rel) return;
135
- const repo = api.ui.repo;
136
- if (!repo || typeof repo.fileText !== 'function') return;
137
- const text = repo.fileText(api.ui.top || api.loadConfig.cwd, rel, api.ui.rev);
138
- composer.applyDiskText(text);
67
+ const npmRels = (items) => {
68
+ const rels = new Set([MANIFEST, LOCKFILE]);
69
+ for (const item of items) {
70
+ const rel = itemPath(item);
71
+ if (depFileMeta(rel)) rels.add(rel);
72
+ }
73
+ return [...rels].sort();
139
74
  };
140
75
 
141
- const syncItemWatches = (api) => {
142
- if (!api.diskWatcher) return;
143
- const rels = itemRels(api.collection);
144
- if (api.nav.pane === 'unit' && api.nav.reviewPath) {
145
- if (!rels.includes(api.nav.reviewPath)) rels.push(api.nav.reviewPath);
76
+ class Lifecycle {
77
+ constructor(ui) {
78
+ this.ui = ui;
79
+ this.diskWatcher = null;
80
+ this.watchRetry = null;
81
+ this.reviewStamp = '';
82
+ this.ignoreWatchUntil = 0;
83
+ this.cachedExtras = null;
146
84
  }
147
- api.diskWatcher.watchFiles(rels);
148
- };
149
85
 
150
- const applyLoaded = (api, loaded) => {
151
- const { loader, collection, review: notes, nav, ui } = api;
152
- if (loaded.generation !== undefined) {
153
- if (!loader.accept(loaded.generation, ui.done)) return false;
154
- }
155
- const wasTodo = nav.tasksOpen === true;
156
- const here = ui.current();
157
- ui.top = loaded.top ?? api.loadConfig.cwd;
158
- collection.replace(loaded.items);
159
- stampItemDates(
160
- collection.items,
161
- fileDatesFromDisk(ui.top, itemRels(collection)),
162
- );
163
- if (loaded.rev) ui.rev = loaded.rev;
164
- if (loaded.revShort) {
165
- ui.revShort = loaded.revShort;
166
- } else if (ui.rev && !ui.revShort) {
167
- ui.revShort = ui.rev.slice(0, 7);
168
- }
169
- if (loaded.sourceLabel) ui.sourceLabel = loaded.sourceLabel;
170
- if (loaded.change) ui.change = loaded.change;
171
- if (loaded.branch !== undefined) ui.branch = loaded.branch || '';
172
- const repository = ui.change ? ui.change.repository : '';
173
- if (!ui.repoName && repository) ui.repoName = repository;
174
- if (!notes.store) ui.initReview();
175
- notes.applyImported(loaded.imported);
176
- restoreAfterLoad(api, here, wasTodo);
177
- markLoaded(api);
178
- loader.pendingExtras = loaded.pending === true;
179
- syncItemWatches(api);
180
- syncUnitEditor(api);
181
- return true;
182
- };
86
+ get root() {
87
+ return this.ui.top || this.ui.cwd;
88
+ }
183
89
 
184
- const load = (api) => {
185
- const { loadConfig } = api;
186
- const loaded = loadConfig.repo.load(
187
- loadConfig.cwd,
188
- loadConfig.paths,
189
- loadOptions(api),
190
- );
191
- applyLoaded(api, loaded);
192
- };
90
+ loadOptions(extra = {}) {
91
+ const ui = this.ui;
92
+ return {
93
+ commit: ui.rev,
94
+ audit: ui.audit,
95
+ outdatedMap: ui.outdatedMap,
96
+ auditMap: ui.auditMap,
97
+ ...extra,
98
+ };
99
+ }
193
100
 
194
- const fileStamp = (top, rel) => {
195
- const full = path.join(top, rel);
196
- try {
197
- const stat = fs.statSync(full);
198
- return `${stat.mtimeMs}:${stat.size}`;
199
- } catch {
200
- return 'missing';
101
+ endReadLoop() {
102
+ const finish = this.ui.finishReadLoop;
103
+ if (finish) finish();
201
104
  }
202
- };
203
105
 
204
- const npmRels = (items) => {
205
- const rels = [MANIFEST, LOCKFILE];
206
- for (const item of items) {
207
- const rel = itemPath(item);
208
- if (!depFileMeta(rel)) continue;
209
- if (rels.includes(rel)) continue;
210
- rels.push(rel);
106
+ finishIfEmpty(kind) {
107
+ const ui = this.ui;
108
+ if (ui.items.length) return;
109
+ const store = ui.review.store;
110
+ if (store && hasNotes(store)) return;
111
+ if (kind === 'keep') return;
112
+ if (isWorktree(ui)) {
113
+ if (ui.nav.pane === 'diff') ui.nav.pane = 'files';
114
+ return;
115
+ }
116
+ ui.done = true;
117
+ ui.exitCode = 0;
118
+ ui.status = 'nothing to review';
119
+ ui.emptyReview = kind === 'open';
120
+ this.endReadLoop();
211
121
  }
212
- rels.sort();
213
- return rels;
214
- };
215
122
 
216
- const npmStamp = (api) => {
217
- const top = api.ui.top || api.loadConfig.cwd;
218
- const rels = npmRels(api.collection.items);
219
- const parts = new Array(rels.length);
220
- for (let i = 0; i < rels.length; i++) {
221
- parts[i] = `${rels[i]}:${fileStamp(top, rels[i])}`;
123
+ failOpen(error) {
124
+ if (isAbort(error)) return;
125
+ const ui = this.ui;
126
+ ui.loadError = error;
127
+ ui.done = true;
128
+ ui.exitCode = 1;
129
+ ui.ops.setBusy('');
130
+ ui.stopProgress();
131
+ this.endReadLoop();
222
132
  }
223
- return parts.join('|');
224
- };
225
133
 
226
- const npmExtrasStale = (api) => {
227
- const cached = api.npmExtras;
228
- if (!cached || !api.loadConfig.audit) return false;
229
- return cached.stamp !== npmStamp(api);
230
- };
134
+ restoreAfterLoad(here, wasTasks) {
135
+ const { nav, collection } = this.ui;
136
+ nav.tasksOpen = wasTasks;
137
+ if (here && !wasTasks) {
138
+ const idx = collection.findRestoredIndex(here);
139
+ if (idx >= 0) nav.index = idx;
140
+ }
141
+ this.ui.clampIndex();
142
+ nav.restoreFileCursor(this.ui.fileList());
143
+ this.ui.syncReviewPath();
144
+ this.ui.syncFileCursor();
145
+ }
231
146
 
232
- const cacheNpmExtras = (api, loaded) => {
233
- api.npmExtras = {
234
- stamp: npmStamp(api),
235
- auditMap: loaded.auditMap ?? null,
236
- outdatedMap: loaded.outdatedMap ?? null,
237
- usedNames: loaded.usedNames ?? null,
238
- exportEntries: loaded.exportEntries,
239
- };
240
- };
147
+ syncUnitEditor() {
148
+ const ui = this.ui;
149
+ const rel = ui.nav.reviewPath;
150
+ if (ui.nav.pane !== 'unit' || !rel || !ui.repo.fileText) return;
151
+ ui.composer.applyDiskText(ui.repo.fileText(this.root, rel, ui.rev));
152
+ }
241
153
 
242
- const clearNpmExtras = (api) => {
243
- api.npmExtras = null;
244
- };
154
+ syncItemWatches() {
155
+ if (!this.diskWatcher) return;
156
+ const nav = this.ui.nav;
157
+ const rels = itemRels(this.ui.items);
158
+ if (nav.pane === 'unit' && nav.reviewPath) {
159
+ if (!rels.includes(nav.reviewPath)) rels.push(nav.reviewPath);
160
+ }
161
+ this.diskWatcher.watchFiles(rels);
162
+ }
245
163
 
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
- };
164
+ applyLoaded(loaded) {
165
+ const ui = this.ui;
166
+ const { loader, collection, nav } = ui;
167
+ if (loaded.generation !== undefined) {
168
+ if (!loader.isCurrent(loaded.generation, ui.done)) return false;
169
+ }
170
+ const wasTasks = nav.tasksOpen === true;
171
+ const here = ui.current();
172
+ ui.top = loaded.top ?? ui.cwd;
173
+ collection.replace(loaded.items);
174
+ stampItemDates(ui.top, collection.items);
175
+ if (loaded.rev) ui.rev = loaded.rev;
176
+ if (loaded.revShort) ui.revShort = loaded.revShort;
177
+ else if (ui.rev && !ui.revShort) ui.revShort = ui.rev.slice(0, 7);
178
+ if (loaded.sourceLabel) ui.sourceLabel = loaded.sourceLabel;
179
+ if (loaded.change) ui.change = loaded.change;
180
+ if (loaded.branch !== undefined) ui.branch = loaded.branch || '';
181
+ const repository = ui.change ? ui.change.repository : '';
182
+ if (!ui.repoName && repository) ui.repoName = repository;
183
+ if (!ui.review.store) ui.initReview();
184
+ ui.review.applyImported(loaded.imported);
185
+ this.restoreAfterLoad(here, wasTasks);
186
+ loader.didLoad = true;
187
+ ui.updater.maybePrompt();
188
+ loader.pendingExtras = loaded.pending === true;
189
+ this.syncItemWatches();
190
+ this.syncUnitEditor();
191
+ ui.dashboard.noteLoaded();
192
+ return true;
193
+ }
252
194
 
253
- const followExtras = async (api, loaded, gen, kind) => {
254
- const extra = loadOptions(api, {
255
- paths: api.loadConfig.paths,
256
- signal: api.loader.signal,
257
- });
258
- let next;
259
- try {
260
- next = await fetchExtras(api.loadConfig.repo, loaded, extra);
261
- } catch (error) {
262
- if (api.loader.isCurrent(gen)) failOpen(api, error);
263
- return;
264
- }
265
- if (!isWanted(api, gen)) return;
266
- if (next) {
267
- applyLoaded(api, tagLoaded(next, gen));
268
- cacheNpmExtras(api, next);
269
- } else {
270
- api.loader.pendingExtras = false;
271
- }
272
- api.ops.clearBusy();
273
- api.ui.paint();
274
- finishIfEmpty(api, kind);
275
- };
195
+ load() {
196
+ const ui = this.ui;
197
+ const loaded = ui.repo.load(ui.cwd, ui.paths, this.loadOptions());
198
+ this.applyLoaded(loaded);
199
+ }
276
200
 
277
- const npmReuseOpts = (api, extra = {}) => {
278
- const opts = {};
279
- if (extra.signal) opts.signal = extra.signal;
280
- const cached = api.npmExtras;
281
- if (!cached) return opts;
282
- if (cached.stamp !== npmStamp(api)) return opts;
283
- opts.deferExtras = false;
284
- opts.collect = false;
285
- opts.auditMap = cached.auditMap;
286
- opts.outdatedMap = cached.outdatedMap;
287
- opts.usedNames = cached.usedNames;
288
- opts.exportEntries = cached.exportEntries;
289
- return opts;
290
- };
201
+ npmStamp() {
202
+ const root = this.root;
203
+ const rels = npmRels(this.ui.items);
204
+ const stamp = (rel) => `${rel}:${statStamp(path.join(root, rel))}`;
205
+ return rels.map(stamp).join('|');
206
+ }
291
207
 
292
- const snapshotOpts = (api, extra = {}) =>
293
- loadOptions(api, {
294
- deferExtras: shouldDeferExtras(api),
295
- ...npmReuseOpts(api, extra),
296
- });
208
+ npmExtrasStale() {
209
+ const cached = this.cachedExtras;
210
+ if (!cached || !this.ui.audit) return false;
211
+ return cached.stamp !== this.npmStamp();
212
+ }
297
213
 
298
- const resetNavAfterLoad = (api, extra = {}) => {
299
- api.nav.scroll = 0;
300
- api.nav.clearSelection();
301
- if (extra.afterLoad) extra.afterLoad();
302
- };
214
+ cacheNpmExtras(loaded) {
215
+ this.cachedExtras = {
216
+ stamp: this.npmStamp(),
217
+ auditMap: loaded.auditMap ?? null,
218
+ outdatedMap: loaded.outdatedMap ?? null,
219
+ usedNames: loaded.usedNames ?? null,
220
+ exportEntries: loaded.exportEntries,
221
+ };
222
+ }
303
223
 
304
- const refreshListsForPane = (api) => {
305
- const pane = api.ui.nav.pane;
306
- if (pane === 'commits' && api.ui.commits) api.ui.commits.refresh();
307
- };
224
+ clearNpmExtras() {
225
+ this.cachedExtras = null;
226
+ }
308
227
 
309
- const afterSnapshot = (api, kind, extra = {}) => {
310
- refreshListsForPane(api);
311
- if (kind !== 'open' && kind !== 'watch') {
312
- return void resetNavAfterLoad(api, extra);
228
+ npmExtras() {
229
+ if (this.cachedExtras) return this.cachedExtras;
230
+ const { auditMap, outdatedMap } = this.ui;
231
+ if (!auditMap && !outdatedMap) return null;
232
+ return { auditMap: auditMap ?? null, outdatedMap: outdatedMap ?? null };
313
233
  }
314
- if (extra.afterLoad) extra.afterLoad();
315
- };
316
234
 
317
- const beginSnapshot = (api, extra) => {
318
- if (extra.quiet === true) return;
319
- api.ui.progressFrame = 0;
320
- api.ops.setBusy('loading');
321
- api.ui.paint();
322
- };
235
+ isWanted(gen) {
236
+ if (!this.ui.loader.isCurrent(gen)) return false;
237
+ if (!this.ui.done) return true;
238
+ this.ui.ops.clearBusy();
239
+ return false;
240
+ }
323
241
 
324
- const runSnapshot = async (api, gen, kind, extra = {}) => {
325
- beginSnapshot(api, extra);
326
- let loaded;
327
- try {
328
- loaded = await fetchSnapshot(
329
- api.loadConfig.repo,
330
- api.loadConfig.cwd,
331
- api.loadConfig.paths,
332
- snapshotOpts(api, {
333
- signal: api.loader.signal,
334
- keepView: extra.keepView,
335
- }),
336
- );
337
- } catch (error) {
338
- if (api.loader.isCurrent(gen)) failOpen(api, error);
339
- return;
340
- }
341
- if (!isWanted(api, gen)) return;
342
- applyLoaded(api, tagLoaded(loaded, gen));
343
- afterSnapshot(api, kind, extra);
344
- if (loaded.pending === true || npmExtrasStale(api)) {
345
- api.ops.setBusy('checking npm');
346
- if (kind === 'open' || extra.quiet === true) api.ui.paint();
347
- await followExtras(api, loaded, gen, kind);
348
- return;
349
- }
350
- api.ops.clearBusy();
351
- finishIfEmpty(api, kind);
352
- if (extra.doneStatus && !api.ui.status && !api.ops.busy) {
353
- api.ui.status = extra.doneStatus;
354
- }
355
- api.ui.paint();
356
- };
242
+ async followExtras(loaded, gen, kind) {
243
+ const ui = this.ui;
244
+ const extra = this.loadOptions({
245
+ paths: ui.paths,
246
+ signal: ui.loader.signal,
247
+ });
248
+ let next;
249
+ try {
250
+ next = await fetchExtras(ui.repo, loaded, extra);
251
+ } catch (error) {
252
+ if (ui.loader.isCurrent(gen)) this.failOpen(error);
253
+ return;
254
+ }
255
+ if (!this.isWanted(gen)) return;
256
+ if (next) {
257
+ this.applyLoaded(tagLoaded(next, gen));
258
+ this.cacheNpmExtras(next);
259
+ } else {
260
+ ui.loader.pendingExtras = false;
261
+ }
262
+ ui.ops.clearBusy();
263
+ ui.paint();
264
+ this.finishIfEmpty(kind);
265
+ }
357
266
 
358
- const openLoad = (api) => {
359
- const { loader } = api;
360
- if (loader.promise) return loader.promise;
361
- if (loader.didLoad && !loader.pendingExtras) {
362
- loader.promise = Promise.resolve();
363
- return loader.promise;
267
+ snapshotOpts(extra = {}) {
268
+ const ui = this.ui;
269
+ const waiting = ui.outdatedMap === undefined || ui.auditMap === undefined;
270
+ const opts = { deferExtras: ui.audit && waiting };
271
+ if (extra.signal) opts.signal = extra.signal;
272
+ const cached = this.cachedExtras;
273
+ if (cached && cached.stamp === this.npmStamp()) {
274
+ opts.deferExtras = false;
275
+ opts.collect = false;
276
+ opts.auditMap = cached.auditMap;
277
+ opts.outdatedMap = cached.outdatedMap;
278
+ opts.usedNames = cached.usedNames;
279
+ opts.exportEntries = cached.exportEntries;
280
+ }
281
+ return this.loadOptions(opts);
364
282
  }
365
- const gen = loader.bump();
366
- loader.promise = runSnapshot(api, gen, 'open');
367
- return loader.promise;
368
- };
369
283
 
370
- const loadReady = async (api) => {
371
- api.ui.ensureRepo();
372
- await openLoad(api);
373
- if (api.ui.loadError) throw api.ui.loadError;
374
- };
284
+ afterSnapshot(kind, extra = {}) {
285
+ const nav = this.ui.nav;
286
+ if (nav.pane === 'commits') this.ui.commits.refresh();
287
+ if (kind !== 'open' && kind !== 'watch') {
288
+ nav.scroll = 0;
289
+ nav.clearSelection();
290
+ }
291
+ if (extra.afterLoad) extra.afterLoad();
292
+ }
375
293
 
376
- const refreshFromRepo = (api, extra = {}) => {
377
- api.loader.promise = null;
378
- const gen = api.loader.bump();
379
- const kind = kindFromExtra(extra);
380
- if (api.ui.uiOpen && api.loadConfig.repo.loadAsync) {
381
- return void runSnapshot(api, gen, kind, extra);
382
- }
383
- const loaded = api.loadConfig.repo.load(
384
- api.loadConfig.cwd,
385
- api.loadConfig.paths,
386
- snapshotOpts(api, extra),
387
- );
388
- applyLoaded(api, tagLoaded(loaded, gen));
389
- afterSnapshot(api, kind, extra);
390
- if (loaded.pending === true || npmExtrasStale(api)) {
391
- api.ops.setBusy('checking npm');
392
- return void followExtras(api, loaded, gen, kind);
393
- }
394
- if (kind === 'keep') return;
395
- finishIfEmpty(api, kind);
396
- if (extra.doneStatus && !api.ui.done && !api.loader.pendingExtras) {
397
- api.ui.status = extra.doneStatus;
398
- }
399
- if (kind === 'watch') api.ui.paint();
400
- };
294
+ async runSnapshot(gen, kind, extra = {}) {
295
+ const ui = this.ui;
296
+ if (extra.quiet !== true) {
297
+ ui.progressFrame = 0;
298
+ ui.ops.setBusy('loading');
299
+ ui.paint();
300
+ }
301
+ let loaded;
302
+ try {
303
+ const opts = this.snapshotOpts({ signal: ui.loader.signal });
304
+ loaded = await fetchSnapshot(ui.repo, ui.cwd, ui.paths, opts);
305
+ } catch (error) {
306
+ if (ui.loader.isCurrent(gen)) this.failOpen(error);
307
+ return;
308
+ }
309
+ if (!this.isWanted(gen)) return;
310
+ this.applyLoaded(tagLoaded(loaded, gen));
311
+ this.afterSnapshot(kind, extra);
312
+ if (loaded.pending === true || this.npmExtrasStale()) {
313
+ ui.ops.setBusy('checking npm');
314
+ if (kind === 'open' || extra.quiet === true) ui.paint();
315
+ await this.followExtras(loaded, gen, kind);
316
+ return;
317
+ }
318
+ ui.ops.clearBusy();
319
+ this.finishIfEmpty(kind);
320
+ if (extra.doneStatus && !ui.status && !ui.ops.busy) {
321
+ ui.status = extra.doneStatus;
322
+ }
323
+ ui.paint();
324
+ }
401
325
 
402
- const reloadAfterChange = (api, afterLoad) => {
403
- const hereItem = api.ui.current();
404
- const hereIndex = api.nav.index;
405
- const restore = () => {
406
- const idx = api.collection.findRestoredIndex(hereItem);
407
- api.nav.index = idx >= 0 ? idx : hereIndex;
408
- api.ui.clampIndex();
409
- api.ui.syncReviewPath();
410
- api.ui.syncFileCursor();
411
- if (afterLoad) afterLoad();
412
- };
413
- if (api.ui.uiOpen && api.loadConfig.repo.loadAsync) {
414
- return void refreshFromRepo(api, { afterLoad: restore });
415
- }
416
- refreshFromRepo(api);
417
- restore();
418
- };
326
+ openLoad() {
327
+ const loader = this.ui.loader;
328
+ if (loader.promise) return loader.promise;
329
+ if (loader.didLoad && !loader.pendingExtras) {
330
+ loader.promise = Promise.resolve();
331
+ } else {
332
+ loader.promise = this.runSnapshot(loader.bump(), 'open');
333
+ }
334
+ return loader.promise;
335
+ }
419
336
 
420
- const onReload = (api) => {
421
- api.collection.dismissed.clear();
422
- api.ui.status = '';
423
- refreshFromRepo(api, { doneStatus: 'reloaded' });
424
- };
337
+ async loadReady() {
338
+ this.ui.ensureRepo();
339
+ await this.openLoad();
340
+ if (this.ui.loadError) throw this.ui.loadError;
341
+ }
425
342
 
426
- const clearWatchRetry = (api) => {
427
- if (api.watchRetry === null) return;
428
- clearTimeout(api.watchRetry);
429
- api.watchRetry = null;
430
- };
343
+ refreshFromRepo(extra = {}) {
344
+ const ui = this.ui;
345
+ ui.loader.promise = null;
346
+ const gen = ui.loader.bump();
347
+ const kind = kindFromExtra(extra);
348
+ if (ui.uiOpen && ui.repo.loadAsync) {
349
+ return void this.runSnapshot(gen, kind, extra);
350
+ }
351
+ const loaded = ui.repo.load(ui.cwd, ui.paths, this.snapshotOpts(extra));
352
+ this.applyLoaded(tagLoaded(loaded, gen));
353
+ this.afterSnapshot(kind, extra);
354
+ if (loaded.pending === true || this.npmExtrasStale()) {
355
+ ui.ops.setBusy('checking npm');
356
+ return void this.followExtras(loaded, gen, kind);
357
+ }
358
+ if (kind === 'keep') return;
359
+ this.finishIfEmpty(kind);
360
+ if (extra.doneStatus && !ui.done && !ui.loader.pendingExtras) {
361
+ ui.status = extra.doneStatus;
362
+ }
363
+ if (kind === 'watch') ui.paint();
364
+ }
431
365
 
432
- const shouldDeferWatch = (api) => {
433
- const { ui, ops } = api;
434
- if (ui.done) return true;
435
- if (!ui.uiOpen) return true;
436
- if (!api.loader.didLoad) return true;
437
- if (ops.gitBusy) return true;
438
- if (ui.mode === 'compose') {
439
- const kind = ui.composer && ui.composer.state.composeKind;
440
- if (kind === 'file') return false;
441
- }
442
- if (ui.mode !== 'review') return true;
443
- return false;
444
- };
366
+ reloadAfterChange(afterLoad) {
367
+ const ui = this.ui;
368
+ const hereItem = ui.current();
369
+ const hereIndex = ui.nav.index;
370
+ const restore = () => {
371
+ const idx = ui.collection.findRestoredIndex(hereItem);
372
+ ui.nav.index = idx >= 0 ? idx : hereIndex;
373
+ ui.clampIndex();
374
+ ui.syncReviewPath();
375
+ ui.syncFileCursor();
376
+ if (afterLoad) afterLoad();
377
+ };
378
+ if (ui.uiOpen && ui.repo.loadAsync) {
379
+ return void this.refreshFromRepo({ afterLoad: restore });
380
+ }
381
+ this.refreshFromRepo();
382
+ restore();
383
+ }
445
384
 
446
- const WATCH_IGNORE_MS = 500;
385
+ onReload() {
386
+ this.ui.collection.dismissed.clear();
387
+ this.ui.status = '';
388
+ this.refreshFromRepo({ doneStatus: 'reloaded' });
389
+ }
447
390
 
448
- const ignoreWatch = (api, ms = WATCH_IGNORE_MS) => {
449
- api.ignoreWatchUntil = Date.now() + ms;
450
- };
391
+ clearWatchRetry() {
392
+ if (this.watchRetry === null) return;
393
+ clearTimeout(this.watchRetry);
394
+ this.watchRetry = null;
395
+ }
451
396
 
452
- const onDiskChange = (api) => {
453
- if (Date.now() < (api.ignoreWatchUntil ?? 0)) return;
454
- if (shouldDeferWatch(api)) {
455
- if (api.watchRetry !== null) return;
456
- api.watchRetry = setTimeout(() => {
457
- api.watchRetry = null;
458
- onDiskChange(api);
459
- }, DEBOUNCE_MS);
460
- if (typeof api.watchRetry.unref === 'function') api.watchRetry.unref();
461
- return;
397
+ shouldDeferWatch() {
398
+ const ui = this.ui;
399
+ if (ui.done || !ui.uiOpen || !ui.loader.didLoad) return true;
400
+ if (ui.ops.gitBusy) return true;
401
+ if (ui.mode === 'compose' && ui.composeKind === 'file') return false;
402
+ return ui.mode !== 'review';
462
403
  }
463
- refreshFromRepo(api, { keepView: true, quiet: true });
464
- };
465
404
 
466
- const catchUpWatch = (api) => {
467
- clearWatchRetry(api);
468
- if (shouldDeferWatch(api)) return void onDiskChange(api);
469
- refreshFromRepo(api, { keepView: true, quiet: true });
470
- };
405
+ ignoreWatch(ms = WATCH_IGNORE_MS) {
406
+ this.ignoreWatchUntil = Date.now() + ms;
407
+ }
471
408
 
472
- const reviewFileStamp = (api) => {
473
- const store = api.review && api.review.store;
474
- if (!store || !store.reviewPath) return '';
475
- try {
476
- const stat = fs.statSync(store.reviewPath);
477
- return `${stat.mtimeMs}:${stat.size}`;
478
- } catch {
479
- return 'missing';
409
+ onDiskChange() {
410
+ if (Date.now() < this.ignoreWatchUntil) return;
411
+ if (!this.shouldDeferWatch()) {
412
+ return void this.refreshFromRepo({ keepView: true, quiet: true });
413
+ }
414
+ if (this.watchRetry !== null) return;
415
+ this.watchRetry = setTimeout(() => {
416
+ this.watchRetry = null;
417
+ this.onDiskChange();
418
+ }, DEBOUNCE_MS);
419
+ this.watchRetry.unref();
480
420
  }
481
- };
482
421
 
483
- const noteReviewStamp = (api) => {
484
- api.reviewStamp = reviewFileStamp(api);
485
- };
422
+ catchUpWatch() {
423
+ this.clearWatchRetry();
424
+ if (this.shouldDeferWatch()) return void this.onDiskChange();
425
+ this.refreshFromRepo({ keepView: true, quiet: true });
426
+ }
486
427
 
487
- const applyReviewChange = (api) => {
488
- if (!api.ui || api.ui.done) return;
489
- const stamp = reviewFileStamp(api);
490
- if (!stamp || stamp === api.reviewStamp) return;
491
- if (stamp === 'missing') {
492
- api.reviewStamp = stamp;
493
- return;
494
- }
495
- const store = api.review && api.review.store;
496
- if (!store) return;
497
- let disk;
498
- try {
499
- disk = loadReview(store.reviewPath, store.templates);
500
- } catch {
501
- return;
428
+ reviewFileStamp() {
429
+ const store = this.ui.review.store;
430
+ if (!store || !store.reviewPath) return '';
431
+ return statStamp(store.reviewPath);
502
432
  }
503
- mergeReview(store, disk);
504
- api.reviewStamp = stamp;
505
- api.ui.paint();
506
- };
507
433
 
508
- const stopWatch = (api) => {
509
- clearWatchRetry(api);
510
- api.reviewStamp = '';
511
- if (!api.diskWatcher) return;
512
- api.diskWatcher.close();
513
- api.diskWatcher = null;
514
- };
434
+ noteReviewStamp() {
435
+ this.reviewStamp = this.reviewFileStamp();
436
+ }
515
437
 
516
- const startWatch = (api) => {
517
- stopWatch(api);
518
- if (!isWorktree(api.ui)) return;
519
- const root = api.ui.top || api.loadConfig.cwd;
520
- if (!root) return;
521
- api.diskWatcher = createDiskWatcher({
522
- root,
523
- paths: api.loadConfig.paths,
524
- onChange: () => onDiskChange(api),
525
- onReview: () => applyReviewChange(api),
526
- });
527
- syncItemWatches(api);
528
- noteReviewStamp(api);
529
- };
438
+ applyReviewChange() {
439
+ if (this.ui.done) return;
440
+ const stamp = this.reviewFileStamp();
441
+ if (!stamp || stamp === this.reviewStamp) return;
442
+ if (stamp === 'missing') {
443
+ this.reviewStamp = stamp;
444
+ return;
445
+ }
446
+ const store = this.ui.review.store;
447
+ let disk;
448
+ try {
449
+ disk = loadReview(store.reviewPath, store.templates);
450
+ } catch {
451
+ return;
452
+ }
453
+ mergeReview(store, disk);
454
+ this.reviewStamp = stamp;
455
+ this.ui.paint();
456
+ }
530
457
 
531
- const createLifecycle = (api) => {
532
- api.diskWatcher = null;
533
- api.watchRetry = null;
534
- api.reviewStamp = '';
535
- api.ignoreWatchUntil = 0;
536
- api.npmExtras = null;
537
- return {
538
- load: () => load(api),
539
- openLoad: () => openLoad(api),
540
- loadReady: () => loadReady(api),
541
- refreshFromRepo: (extra) => refreshFromRepo(api, extra),
542
- reloadAfterChange: (afterLoad) => reloadAfterChange(api, afterLoad),
543
- onReload: () => onReload(api),
544
- onDiskChange: () => onDiskChange(api),
545
- applyReviewChange: () => applyReviewChange(api),
546
- noteReviewStamp: () => noteReviewStamp(api),
547
- catchUpWatch: () => catchUpWatch(api),
548
- startWatch: () => startWatch(api),
549
- stopWatch: () => stopWatch(api),
550
- ignoreWatch: (ms) => ignoreWatch(api, ms),
551
- clearNpmExtras: () => clearNpmExtras(api),
552
- };
553
- };
458
+ stopWatch() {
459
+ this.clearWatchRetry();
460
+ this.reviewStamp = '';
461
+ if (!this.diskWatcher) return;
462
+ this.diskWatcher.close();
463
+ this.diskWatcher = null;
464
+ }
465
+
466
+ startWatch() {
467
+ this.stopWatch();
468
+ const root = this.root;
469
+ if (!isWorktree(this.ui) || !root) return;
470
+ this.diskWatcher = new DiskWatcher({
471
+ root,
472
+ paths: this.ui.paths,
473
+ onChange: () => this.onDiskChange(),
474
+ onReview: () => this.applyReviewChange(),
475
+ onPaths: (paths) => this.ui.dashboard.noteDisk(paths),
476
+ });
477
+ this.syncItemWatches();
478
+ this.noteReviewStamp();
479
+ }
480
+ }
554
481
 
555
- module.exports = { createLifecycle };
482
+ module.exports = { Lifecycle };