reslop 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -1
- package/README.md +18 -17
- package/lib/cli.js +22 -8
- package/lib/detect.js +2 -0
- package/lib/files.js +36 -12
- package/lib/git.js +312 -56
- package/lib/highlight.js +160 -5
- package/lib/keys.js +8 -2
- package/lib/render.js +78 -24
- package/lib/session.js +207 -22
- package/package.json +1 -1
package/lib/session.js
CHANGED
|
@@ -82,6 +82,8 @@ const itemFeedKey = (item) => {
|
|
|
82
82
|
return feedbackKey({ file, oldStart, newStart, blockId });
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
+
const isAbortError = (error) => !!(error && error.code === 'ABORT');
|
|
86
|
+
|
|
85
87
|
const sameCursor = (a, b) => {
|
|
86
88
|
if (!a && !b) return true;
|
|
87
89
|
if (!a || !b) return false;
|
|
@@ -128,6 +130,10 @@ const HANDLERS = {
|
|
|
128
130
|
quit: (session) => session.onQuit(),
|
|
129
131
|
scrollUp: (session) => session.onScroll(-1),
|
|
130
132
|
scrollDown: (session) => session.onScroll(1),
|
|
133
|
+
pageUp: (session) => session.onScroll(session.scrollStep(-1)),
|
|
134
|
+
pageDown: (session) => session.onScroll(session.scrollStep(1)),
|
|
135
|
+
halfUp: (session) => session.onScroll(session.scrollStep(-0.5)),
|
|
136
|
+
halfDown: (session) => session.onScroll(session.scrollStep(0.5)),
|
|
131
137
|
};
|
|
132
138
|
|
|
133
139
|
const KEY_MODE = {
|
|
@@ -233,6 +239,16 @@ class Session {
|
|
|
233
239
|
this.saveTimer = null;
|
|
234
240
|
this.blinkTimer = null;
|
|
235
241
|
this.blinkOn = true;
|
|
242
|
+
this.loadGen = 0;
|
|
243
|
+
this.loadAbort = null;
|
|
244
|
+
this.loadPromise = null;
|
|
245
|
+
this.busy = '';
|
|
246
|
+
this.emptyReview = false;
|
|
247
|
+
this.loadError = null;
|
|
248
|
+
this.didLoad = false;
|
|
249
|
+
this.pendingExtras = false;
|
|
250
|
+
this.uiOpen = false;
|
|
251
|
+
this.finishReadLoop = null;
|
|
236
252
|
}
|
|
237
253
|
|
|
238
254
|
constructor(options) {
|
|
@@ -240,14 +256,35 @@ class Session {
|
|
|
240
256
|
this.resetState();
|
|
241
257
|
}
|
|
242
258
|
|
|
243
|
-
|
|
244
|
-
const here = itemKey(this.items[this.index]);
|
|
259
|
+
loadOptions(extra = {}) {
|
|
245
260
|
const commit = this.rev;
|
|
246
261
|
const audit = this.audit;
|
|
247
262
|
const outdatedMap = this.outdatedMap;
|
|
248
263
|
const auditMap = this.auditMap;
|
|
249
|
-
|
|
250
|
-
|
|
264
|
+
return { commit, audit, outdatedMap, auditMap, ...extra };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
shouldDeferExtras() {
|
|
268
|
+
if (!this.audit) return false;
|
|
269
|
+
if (this.outdatedMap !== undefined && this.auditMap !== undefined) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
bumpLoad() {
|
|
276
|
+
this.loadGen += 1;
|
|
277
|
+
if (this.loadAbort) this.loadAbort.abort();
|
|
278
|
+
this.loadAbort = new AbortController();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
ensureRepo() {
|
|
282
|
+
if (!this.repo.toplevel) return;
|
|
283
|
+
this.top = this.repo.toplevel(this.cwd);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
applyLoaded(loaded) {
|
|
287
|
+
const here = itemKey(this.items[this.index]);
|
|
251
288
|
this.top = loaded.top ?? this.cwd;
|
|
252
289
|
const items = loaded.items;
|
|
253
290
|
this.items = items.filter((item) => !this.dismissed.has(itemKey(item)));
|
|
@@ -277,6 +314,121 @@ class Session {
|
|
|
277
314
|
this.clampIndex();
|
|
278
315
|
this.syncReviewPath();
|
|
279
316
|
this.syncFileCursor();
|
|
317
|
+
this.didLoad = true;
|
|
318
|
+
this.pendingExtras = loaded.pending === true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
load() {
|
|
322
|
+
const loaded = this.repo.load(this.cwd, this.paths, this.loadOptions());
|
|
323
|
+
this.applyLoaded(loaded);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
paint() {
|
|
327
|
+
if (!this.uiOpen) return;
|
|
328
|
+
this.draw();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
viewStatus() {
|
|
332
|
+
if (this.status) return this.status;
|
|
333
|
+
return this.busy;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
finishIfEmpty(kind) {
|
|
337
|
+
if (this.items.length) return;
|
|
338
|
+
if (this.notes && hasNotes(this.notes)) this.flushReview();
|
|
339
|
+
this.done = true;
|
|
340
|
+
this.exitCode = 0;
|
|
341
|
+
this.status = 'nothing to review';
|
|
342
|
+
this.emptyReview = kind === 'open';
|
|
343
|
+
if (this.finishReadLoop) this.finishReadLoop();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
failOpen(error) {
|
|
347
|
+
if (isAbortError(error)) return;
|
|
348
|
+
this.loadError = error;
|
|
349
|
+
this.done = true;
|
|
350
|
+
this.exitCode = 1;
|
|
351
|
+
this.busy = '';
|
|
352
|
+
if (this.finishReadLoop) this.finishReadLoop();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async followExtras(loaded, gen, kind) {
|
|
356
|
+
if (!this.repo.loadExtras) {
|
|
357
|
+
this.busy = '';
|
|
358
|
+
this.pendingExtras = false;
|
|
359
|
+
this.paint();
|
|
360
|
+
this.finishIfEmpty(kind);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const signal = this.loadAbort ? this.loadAbort.signal : undefined;
|
|
364
|
+
const extra = this.loadOptions({
|
|
365
|
+
paths: this.paths,
|
|
366
|
+
signal,
|
|
367
|
+
});
|
|
368
|
+
let next;
|
|
369
|
+
try {
|
|
370
|
+
next = await this.repo.loadExtras(loaded, extra);
|
|
371
|
+
} catch (error) {
|
|
372
|
+
if (gen !== this.loadGen) return;
|
|
373
|
+
this.failOpen(error);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (gen !== this.loadGen || this.done) return;
|
|
377
|
+
this.applyLoaded(next);
|
|
378
|
+
this.busy = '';
|
|
379
|
+
this.paint();
|
|
380
|
+
this.finishIfEmpty(kind);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
openLoad() {
|
|
384
|
+
if (this.loadPromise) return this.loadPromise;
|
|
385
|
+
if (this.didLoad && !this.pendingExtras) {
|
|
386
|
+
this.loadPromise = Promise.resolve();
|
|
387
|
+
return this.loadPromise;
|
|
388
|
+
}
|
|
389
|
+
this.bumpLoad();
|
|
390
|
+
this.loadPromise = this.runOpenLoad();
|
|
391
|
+
return this.loadPromise;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async runOpenLoad() {
|
|
395
|
+
const gen = this.loadGen;
|
|
396
|
+
const signal = this.loadAbort.signal;
|
|
397
|
+
const options = this.loadOptions({
|
|
398
|
+
deferExtras: this.shouldDeferExtras(),
|
|
399
|
+
signal,
|
|
400
|
+
});
|
|
401
|
+
this.busy = 'loading';
|
|
402
|
+
this.paint();
|
|
403
|
+
let loaded;
|
|
404
|
+
try {
|
|
405
|
+
if (this.repo.loadAsync) {
|
|
406
|
+
loaded = await this.repo.loadAsync(this.cwd, this.paths, options);
|
|
407
|
+
} else {
|
|
408
|
+
loaded = this.repo.load(this.cwd, this.paths, options);
|
|
409
|
+
}
|
|
410
|
+
} catch (error) {
|
|
411
|
+
if (gen !== this.loadGen) return;
|
|
412
|
+
this.failOpen(error);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (gen !== this.loadGen || this.done) return;
|
|
416
|
+
this.applyLoaded(loaded);
|
|
417
|
+
if (loaded.pending) {
|
|
418
|
+
this.busy = 'checking npm';
|
|
419
|
+
this.paint();
|
|
420
|
+
await this.followExtras(loaded, gen, 'open');
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
this.busy = '';
|
|
424
|
+
this.paint();
|
|
425
|
+
this.finishIfEmpty('open');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async loadReady() {
|
|
429
|
+
this.ensureRepo();
|
|
430
|
+
await this.openLoad();
|
|
431
|
+
if (this.loadError) throw this.loadError;
|
|
280
432
|
}
|
|
281
433
|
|
|
282
434
|
initReview() {
|
|
@@ -393,7 +545,7 @@ class Session {
|
|
|
393
545
|
index: this.index,
|
|
394
546
|
total: this.items.length,
|
|
395
547
|
scroll: this.scroll,
|
|
396
|
-
status: this.
|
|
548
|
+
status: this.viewStatus(),
|
|
397
549
|
layout: this.layout,
|
|
398
550
|
counts: this.counts(),
|
|
399
551
|
selection: this.selection,
|
|
@@ -466,20 +618,25 @@ class Session {
|
|
|
466
618
|
|
|
467
619
|
reloadAfterChange() {
|
|
468
620
|
const here = this.index;
|
|
469
|
-
this.
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
this.
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
621
|
+
this.loadPromise = null;
|
|
622
|
+
this.bumpLoad();
|
|
623
|
+
const options = this.loadOptions({
|
|
624
|
+
deferExtras: this.shouldDeferExtras(),
|
|
625
|
+
});
|
|
626
|
+
const loaded = this.repo.load(this.cwd, this.paths, options);
|
|
627
|
+
this.applyLoaded(loaded);
|
|
477
628
|
this.index = here;
|
|
478
629
|
this.clampIndex();
|
|
479
630
|
this.syncReviewPath();
|
|
480
631
|
this.syncFileCursor();
|
|
481
632
|
this.scroll = 0;
|
|
482
633
|
this.clearSelection();
|
|
634
|
+
if (loaded.pending) {
|
|
635
|
+
this.busy = 'checking npm';
|
|
636
|
+
this.followExtras(loaded, this.loadGen, 'reload');
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
this.finishIfEmpty('reload');
|
|
483
640
|
}
|
|
484
641
|
|
|
485
642
|
clearSelection() {
|
|
@@ -578,12 +735,13 @@ class Session {
|
|
|
578
735
|
onFileAdd() {
|
|
579
736
|
const items = this.fileActionItems();
|
|
580
737
|
if (!this.guardFileGit(items)) return;
|
|
738
|
+
const rel = itemPath(items[0]);
|
|
581
739
|
const targets = items.filter((item) => item.origin !== 'staged');
|
|
582
740
|
if (!targets.length) {
|
|
583
741
|
this.status = 'already staged';
|
|
742
|
+
this.selectNextFileAfter(rel);
|
|
584
743
|
return;
|
|
585
744
|
}
|
|
586
|
-
const rel = itemPath(targets[0]);
|
|
587
745
|
const proposed = targets.some((item) => needsReload(item));
|
|
588
746
|
const ok = this.applyFileGit(
|
|
589
747
|
targets,
|
|
@@ -598,12 +756,13 @@ class Session {
|
|
|
598
756
|
onFileUnstage() {
|
|
599
757
|
const items = this.fileActionItems();
|
|
600
758
|
if (!this.guardFileGit(items)) return;
|
|
759
|
+
const rel = itemPath(items[0]);
|
|
601
760
|
const targets = items.filter((item) => item.origin === 'staged');
|
|
602
761
|
if (!targets.length) {
|
|
603
762
|
this.status = 'not staged';
|
|
763
|
+
this.selectNextFileAfter(rel);
|
|
604
764
|
return;
|
|
605
765
|
}
|
|
606
|
-
const rel = itemPath(targets[0]);
|
|
607
766
|
const ok = this.applyFileGit(
|
|
608
767
|
targets,
|
|
609
768
|
'unstaged',
|
|
@@ -1267,13 +1426,12 @@ class Session {
|
|
|
1267
1426
|
this.status = 'nothing to review';
|
|
1268
1427
|
return;
|
|
1269
1428
|
}
|
|
1270
|
-
const next =
|
|
1271
|
-
|
|
1272
|
-
this.
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
this.status = 'last file';
|
|
1429
|
+
const next = Math.min(
|
|
1430
|
+
files.length - 1,
|
|
1431
|
+
Math.max(0, this.fileCursor + delta),
|
|
1432
|
+
);
|
|
1433
|
+
if (next === this.fileCursor) {
|
|
1434
|
+
this.status = next <= 0 ? 'first file' : 'last file';
|
|
1277
1435
|
return;
|
|
1278
1436
|
}
|
|
1279
1437
|
this.fileCursor = next;
|
|
@@ -1283,6 +1441,16 @@ class Session {
|
|
|
1283
1441
|
this.status = '';
|
|
1284
1442
|
}
|
|
1285
1443
|
|
|
1444
|
+
scrollStep(fraction) {
|
|
1445
|
+
const bodyH = this.lastFrame?.bodyH;
|
|
1446
|
+
const size = this.lastSize ?? this.getSize();
|
|
1447
|
+
const fallback = Math.max(1, (size.height ?? 24) - 4);
|
|
1448
|
+
const height = Math.max(1, bodyH ?? fallback);
|
|
1449
|
+
const n = Math.max(1, Math.floor(height * Math.abs(fraction)));
|
|
1450
|
+
if (fraction < 0) return -n;
|
|
1451
|
+
return n;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1286
1454
|
onScroll(delta) {
|
|
1287
1455
|
if (this.pane === 'files') {
|
|
1288
1456
|
this.onFileMove(delta);
|
|
@@ -1428,12 +1596,20 @@ class Session {
|
|
|
1428
1596
|
}
|
|
1429
1597
|
|
|
1430
1598
|
async startUi() {
|
|
1599
|
+
if (this.didLoad && !this.items.length) {
|
|
1600
|
+
this.emptyReview = true;
|
|
1601
|
+
return 0;
|
|
1602
|
+
}
|
|
1603
|
+
this.uiOpen = true;
|
|
1431
1604
|
this.enterTerm();
|
|
1432
1605
|
try {
|
|
1606
|
+
this.openLoad();
|
|
1433
1607
|
this.draw();
|
|
1434
1608
|
await this.readLoop();
|
|
1435
1609
|
} finally {
|
|
1610
|
+
this.uiOpen = false;
|
|
1436
1611
|
this.leaveTerm();
|
|
1612
|
+
if (this.loadAbort) this.loadAbort.abort();
|
|
1437
1613
|
}
|
|
1438
1614
|
return this.exitCode;
|
|
1439
1615
|
}
|
|
@@ -1442,7 +1618,10 @@ class Session {
|
|
|
1442
1618
|
return new Promise((resolve) => {
|
|
1443
1619
|
this.abortController = new AbortController();
|
|
1444
1620
|
const { signal } = this.abortController;
|
|
1621
|
+
let settled = false;
|
|
1445
1622
|
const finish = () => {
|
|
1623
|
+
if (settled) return;
|
|
1624
|
+
settled = true;
|
|
1446
1625
|
if (this.saveTimer !== null) {
|
|
1447
1626
|
this.clearInterval(this.saveTimer);
|
|
1448
1627
|
this.saveTimer = null;
|
|
@@ -1452,8 +1631,14 @@ class Session {
|
|
|
1452
1631
|
this.blinkTimer = null;
|
|
1453
1632
|
}
|
|
1454
1633
|
this.abortController.abort();
|
|
1634
|
+
this.finishReadLoop = null;
|
|
1455
1635
|
resolve();
|
|
1456
1636
|
};
|
|
1637
|
+
this.finishReadLoop = finish;
|
|
1638
|
+
if (this.done) {
|
|
1639
|
+
finish();
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1457
1642
|
const onData = (chunk) => {
|
|
1458
1643
|
this.pushInput(chunk.toString('utf8'));
|
|
1459
1644
|
if (this.done) {
|