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
@@ -1,8 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { stateView } = require('./accessors.js');
4
-
5
3
  const ASYNC_WITHOUT_UI = ['pull', 'push'];
4
+ const QUIET_KIND = new Set(['staged', 'unstaged']);
6
5
 
7
6
  const pickRepoWrite = (repo, name, uiOpen) => {
8
7
  const asyncFn = repo[`${name}Async`];
@@ -20,28 +19,176 @@ const pickRepoWrite = (repo, name, uiOpen) => {
20
19
  };
21
20
  };
22
21
 
23
- const createOpsRunner = (ui) => {
24
- const state = {
25
- gitBusy: false,
26
- busy: '',
27
- };
22
+ const showsBusy = (kind) => Boolean(kind) && !QUIET_KIND.has(kind);
28
23
 
29
- const reset = () => {
30
- state.gitBusy = false;
31
- state.busy = '';
32
- };
24
+ class ChangeQueue {
25
+ constructor(ui, runner) {
26
+ this.ui = ui;
27
+ this.runner = runner;
28
+ this.jobs = [];
29
+ this.head = 0;
30
+ this.epoch = 0;
31
+ this.draining = false;
32
+ this.pumpQueued = false;
33
+ this.holding = false;
34
+ this.resume = null;
35
+ this.reloading = false;
36
+ }
33
37
 
34
- const setBusy = (kind) => {
35
- state.busy = kind;
36
- ui.startProgress();
37
- };
38
+ noteFree() {
39
+ const resume = this.resume;
40
+ this.resume = null;
41
+ if (resume) resume();
42
+ }
38
43
 
39
- const clearBusy = () => {
40
- state.busy = '';
41
- ui.stopProgress();
42
- };
44
+ dropJobs() {
45
+ this.jobs = [];
46
+ this.head = 0;
47
+ }
48
+
49
+ reset() {
50
+ this.epoch += 1;
51
+ this.dropJobs();
52
+ this.draining = false;
53
+ this.pumpQueued = false;
54
+ this.holding = false;
55
+ this.reloading = false;
56
+ this.noteFree();
57
+ }
58
+
59
+ armBusy(kind) {
60
+ if (!showsBusy(kind)) return;
61
+ this.ui.status = '';
62
+ this.ui.progressFrame = 0;
63
+ this.runner.setBusy(kind);
64
+ }
65
+
66
+ waitTurn() {
67
+ if (!this.runner.gitBusy || this.holding) return Promise.resolve();
68
+ return new Promise((resolve) => {
69
+ this.resume = resolve;
70
+ });
71
+ }
72
+
73
+ hasPendingBusy() {
74
+ const { jobs, head } = this;
75
+ for (let i = head; i < jobs.length; i++) {
76
+ if (showsBusy(jobs[i].kind)) return true;
77
+ }
78
+ return false;
79
+ }
80
+
81
+ releaseHold() {
82
+ this.holding = false;
83
+ this.runner.gitBusy = false;
84
+ this.noteFree();
85
+ }
86
+
87
+ startReload() {
88
+ const { ui } = this;
89
+ if (typeof ui.reloadAfterChange !== 'function') return void ui.paint();
90
+ this.reloading = true;
91
+ ui.reloadAfterChange(() => {
92
+ this.reloading = false;
93
+ ui.paint();
94
+ });
95
+ }
96
+
97
+ async takeJob(stamp) {
98
+ const job = this.jobs[this.head];
99
+ this.head += 1;
100
+ this.armBusy(job.kind);
101
+ try {
102
+ await job.write(this.ui.top);
103
+ } catch (error) {
104
+ if (stamp !== this.epoch) return 'stale';
105
+ this.ui.status = error.message;
106
+ return 'fail';
107
+ }
108
+ if (stamp !== this.epoch) return 'stale';
109
+ if (job.done) this.ui.status = job.done;
110
+ if (!this.hasPendingBusy()) this.runner.clearBusy();
111
+ return job.reload ? 'reload' : 'ok';
112
+ }
113
+
114
+ schedule() {
115
+ if (this.draining || this.pumpQueued) return;
116
+ this.pumpQueued = true;
117
+ queueMicrotask(() => {
118
+ this.pumpQueued = false;
119
+ void this.run();
120
+ });
121
+ }
43
122
 
44
- const runRepo = (label, write) => {
123
+ finish(stamp, needReload) {
124
+ if (stamp !== this.epoch) return;
125
+ this.draining = false;
126
+ this.releaseHold();
127
+ if (this.head < this.jobs.length) return void this.schedule();
128
+ this.dropJobs();
129
+ if (needReload) this.startReload();
130
+ else this.ui.paint();
131
+ }
132
+
133
+ async run() {
134
+ const stamp = this.epoch;
135
+ this.draining = true;
136
+ if (this.runner.gitBusy && !this.holding) await this.waitTurn();
137
+ if (stamp !== this.epoch) return;
138
+ this.holding = true;
139
+ this.runner.gitBusy = true;
140
+ let needReload = false;
141
+ while (this.head < this.jobs.length) {
142
+ const result = await this.takeJob(stamp);
143
+ if (result === 'stale') return;
144
+ if (result === 'fail') {
145
+ needReload = true;
146
+ this.dropJobs();
147
+ break;
148
+ }
149
+ if (result === 'reload') needReload = true;
150
+ }
151
+ this.finish(stamp, needReload);
152
+ }
153
+
154
+ enqueue(job) {
155
+ if (this.reloading) {
156
+ this.ui.loader.bump();
157
+ this.reloading = false;
158
+ job.reload = true;
159
+ }
160
+ this.jobs.push(job);
161
+ this.armBusy(job.kind);
162
+ this.schedule();
163
+ }
164
+ }
165
+
166
+ class OpsRunner {
167
+ constructor(ui) {
168
+ this.ui = ui;
169
+ this.gitBusy = false;
170
+ this.busy = '';
171
+ this.queue = new ChangeQueue(ui, this);
172
+ }
173
+
174
+ setBusy(kind) {
175
+ this.busy = kind;
176
+ this.ui.startProgress();
177
+ }
178
+
179
+ clearBusy() {
180
+ this.busy = '';
181
+ this.ui.stopProgress();
182
+ }
183
+
184
+ reset() {
185
+ this.gitBusy = false;
186
+ this.busy = '';
187
+ this.queue.reset();
188
+ }
189
+
190
+ runRepo(label, write) {
191
+ const { ui } = this;
45
192
  try {
46
193
  write(ui.top);
47
194
  ui.status = label;
@@ -50,21 +197,23 @@ const createOpsRunner = (ui) => {
50
197
  ui.status = error.message;
51
198
  return false;
52
199
  }
53
- };
200
+ }
54
201
 
55
- const finishBusy = (done, after) => {
202
+ finishBusy(done, after) {
203
+ const { ui } = this;
56
204
  if (ui.done || ui.mode === 'confirmPush') return void ui.paint();
57
205
  if (!ui.status && after) after();
58
- if (!ui.status && !state.busy) ui.status = done;
206
+ if (!ui.status && !this.busy) ui.status = done;
59
207
  ui.paint();
60
- };
208
+ }
61
209
 
62
- const runBusy = async (kind, done, write, after) => {
63
- if (state.gitBusy) return;
64
- state.gitBusy = true;
210
+ async runBusy(kind, done, write, after) {
211
+ const { ui } = this;
212
+ if (this.gitBusy) return;
213
+ this.gitBusy = true;
65
214
  ui.status = '';
66
215
  ui.progressFrame = 0;
67
- setBusy(kind);
216
+ this.setBusy(kind);
68
217
  ui.paint();
69
218
  try {
70
219
  await write(ui.top);
@@ -75,23 +224,29 @@ const createOpsRunner = (ui) => {
75
224
  ui.status = error.message;
76
225
  }
77
226
  } finally {
78
- clearBusy();
79
- state.gitBusy = false;
227
+ this.clearBusy();
228
+ this.gitBusy = false;
229
+ this.queue.noteFree();
80
230
  }
81
- finishBusy(done, after);
82
- };
231
+ this.finishBusy(done, after);
232
+ }
83
233
 
84
- return stateView(
85
- state,
86
- {
87
- reset,
88
- setBusy,
89
- clearBusy,
90
- runRepo,
91
- runBusy,
92
- },
93
- ['gitBusy', 'busy'],
94
- );
95
- };
234
+ runWrite(name, args, kind, done, after) {
235
+ const { ui } = this;
236
+ const picked = pickRepoWrite(ui.repo, name, ui.uiOpen);
237
+ const write = (top) => picked.write(top, ...args);
238
+ if (picked.async) {
239
+ const refresh = () => after({ doneStatus: done });
240
+ return void this.runBusy(kind, done, write, refresh);
241
+ }
242
+ if (!this.runRepo(done, write)) return;
243
+ after({});
244
+ if (!ui.status) ui.status = done;
245
+ }
246
+
247
+ enqueueLazy(job) {
248
+ this.queue.enqueue(job);
249
+ }
250
+ }
96
251
 
97
- module.exports = { ASYNC_WITHOUT_UI, pickRepoWrite, createOpsRunner };
252
+ module.exports = { pickRepoWrite, OpsRunner };
@@ -1,8 +1,12 @@
1
1
  'use strict';
2
2
 
3
- const { extractText, isRange } = require('../select.js');
4
- const { hitAction } = require('../keys.js');
5
- const { copyText } = require('../clipboard.js');
3
+ const select = require('../select.js');
4
+ const editClick = require('./edit-click.js');
5
+ const { extractText, isRange } = select;
6
+ const keys = require('../keys.js');
7
+ const { hitAction } = keys;
8
+ const clipboard = require('../clipboard.js');
9
+ const { copyText } = clipboard;
6
10
 
7
11
  const mouseCell = (ui, event) => {
8
12
  const size = ui.lastSize ?? ui.getSize();
@@ -45,6 +49,20 @@ const clickButton = (ui, cell, clickId) => {
45
49
  return true;
46
50
  };
47
51
 
52
+ const DOUBLE_CLICK_MS = 500;
53
+
54
+ const armDoubleClick = (nav, pane, cursor) => {
55
+ const now = Date.now();
56
+ const prev = nav.lastClick;
57
+ const again =
58
+ prev !== null &&
59
+ prev.pane === pane &&
60
+ prev.cursor === cursor &&
61
+ now - prev.at <= DOUBLE_CLICK_MS;
62
+ nav.lastClick = { pane, cursor, at: now };
63
+ return again;
64
+ };
65
+
48
66
  const clickHit = (ui, hits, cell, onHit) => {
49
67
  const nav = ui.nav;
50
68
  if (!hits || isRange(nav.selection)) return false;
@@ -83,16 +101,18 @@ const RELEASE_TARGETS = [
83
101
  match: (ui) => ui.pane === 'files',
84
102
  hits: 'fileHits',
85
103
  onHit: (ui, hit) => {
104
+ const again = armDoubleClick(ui.nav, 'files', hit.cursor);
86
105
  ui.nav.fileCursor = hit.cursor;
87
- ui.filesPane.onOpenFile();
106
+ if (again) ui.filesPane.onOpenFile();
88
107
  },
89
108
  },
90
109
  {
91
110
  match: (ui) => ui.pane === 'branches',
92
111
  hits: 'fileHits',
93
112
  onHit: (ui, hit) => {
113
+ const again = armDoubleClick(ui.nav, 'branches', hit.cursor);
94
114
  ui.nav.branchCursor = hit.cursor;
95
- ui.gitBranches.onCheckoutBranch();
115
+ if (again) ui.gitBranches.onCheckoutBranch();
96
116
  },
97
117
  },
98
118
  {
@@ -100,22 +120,26 @@ const RELEASE_TARGETS = [
100
120
  hits: 'fileHits',
101
121
  onHit: (ui, hit) => {
102
122
  if (ui.mode === 'compose') return;
123
+ const again = armDoubleClick(ui.nav, 'commits', hit.cursor);
103
124
  ui.nav.commitCursor = hit.cursor;
125
+ if (again) ui.filesPane.onOpenFile();
104
126
  },
105
127
  },
106
128
  {
107
129
  match: (ui) => ui.pane === 'npm',
108
130
  hits: 'fileHits',
109
131
  onHit: (ui, hit) => {
132
+ const again = armDoubleClick(ui.nav, 'npm', hit.cursor);
110
133
  ui.nav.npmCursor = hit.cursor;
134
+ if (again && ui.mode === 'review') ui.npm.run();
111
135
  },
112
136
  },
113
137
  {
114
138
  match: (ui) => ui.pane === 'diff',
115
- hits: 'todoHits',
139
+ hits: 'taskHits',
116
140
  onHit: (ui, hit) => {
117
- if (hit.check) return void ui.composer.onTodoCheck(hit.cursor);
118
- ui.composer.onTodoHit(hit.cursor);
141
+ if (hit.check) return void ui.composer.onTaskCheck(hit.cursor);
142
+ ui.composer.onTaskHit(hit.cursor);
119
143
  },
120
144
  },
121
145
  {
@@ -135,6 +159,7 @@ const onRelease = (ui, event) => {
135
159
  nav.pendingClick = null;
136
160
  nav.mouseAnchor = null;
137
161
  if (clickButton(ui, cell, clickId)) return;
162
+ if (editClick.placeClick(ui, cell)) return;
138
163
  const frame = ui.lastFrame;
139
164
  for (const target of RELEASE_TARGETS) {
140
165
  if (!target.match(ui)) continue;
@@ -3,22 +3,25 @@
3
3
  const createProgress = (term, onTick, intervalMs) => {
4
4
  const active = new Set();
5
5
 
6
+ const tick = () => {
7
+ if (!active.size) return;
8
+ onTick();
9
+ term.later('progress', tick, intervalMs);
10
+ };
11
+
6
12
  const start = (id) => {
7
- const wasEmpty = active.size === 0;
8
13
  active.add(id);
9
- if (wasEmpty) {
10
- term.startTimer('progress', onTick, intervalMs);
11
- }
14
+ term.later('progress', tick, intervalMs);
12
15
  };
13
16
 
14
17
  const stop = (id) => {
15
18
  active.delete(id);
16
- if (active.size === 0) term.stopTimer('progress');
19
+ if (active.size === 0) term.cancel('progress');
17
20
  };
18
21
 
19
22
  const clear = () => {
20
23
  active.clear();
21
- term.stopTimer('progress');
24
+ term.cancel('progress');
22
25
  };
23
26
 
24
27
  const size = () => active.size;
@@ -1,74 +1,56 @@
1
1
  'use strict';
2
2
 
3
- const { stateView } = require('./accessors.js');
4
-
5
3
  const review = require('../review.js');
6
4
  const { createStore, applyImportedNotes, setStatus } = review;
7
5
  const { listReviewNames, loadTemplates, resolveReviewPath } = review;
8
6
  const { loadReview, flushReview } = review;
9
7
 
10
- const createReviewController = () => {
11
- const state = {
12
- store: null,
13
- didResume: false,
14
- importedRemote: false,
15
- };
8
+ class ReviewController {
9
+ constructor() {
10
+ this.store = null;
11
+ this.didResume = false;
12
+ this.importedRemote = false;
13
+ }
16
14
 
17
- const reset = () => {
18
- state.store = null;
19
- state.didResume = false;
20
- state.importedRemote = false;
21
- };
15
+ reset() {
16
+ this.store = null;
17
+ this.didResume = false;
18
+ this.importedRemote = false;
19
+ }
22
20
 
23
- const init = (dir, date, extra = {}) => {
21
+ init(dir, date, extra = {}) {
24
22
  const names = listReviewNames(dir);
25
23
  const templates = loadTemplates(dir);
26
24
  const resolved = resolveReviewPath(dir, date, names, extra);
27
- if (resolved.resume) {
28
- state.store = loadReview(resolved.reviewPath, templates);
29
- state.didResume = true;
30
- return state.store;
31
- }
32
- state.store = createStore(resolved.reviewPath, templates);
33
- state.didResume = false;
34
- return state.store;
35
- };
36
-
37
- const applyImported = (imported) => {
38
- if (!imported || !state.store) return false;
39
- if (state.didResume || state.importedRemote) return false;
40
- applyImportedNotes(state.store, imported);
41
- state.importedRemote = true;
25
+ this.didResume = resolved.resume === true;
26
+ this.store = this.didResume
27
+ ? loadReview(resolved.reviewPath, templates)
28
+ : createStore(resolved.reviewPath, templates);
29
+ return this.store;
30
+ }
31
+
32
+ applyImported(imported) {
33
+ if (!imported || !this.store) return false;
34
+ if (this.didResume || this.importedRemote) return false;
35
+ applyImportedNotes(this.store, imported);
36
+ this.importedRemote = true;
42
37
  return true;
43
- };
38
+ }
44
39
 
45
- const flush = (force = false) => {
46
- if (!state.store) return { ok: true, wrote: false };
40
+ flush(force = false) {
41
+ if (!this.store) return { ok: true, wrote: false };
47
42
  try {
48
- const wrote = flushReview(state.store, force);
43
+ const wrote = flushReview(this.store, force);
49
44
  return { ok: true, wrote };
50
45
  } catch (error) {
51
46
  return { ok: false, wrote: false, error };
52
47
  }
53
- };
54
-
55
- const finishQuit = (status) => {
56
- setStatus(state.store, status);
57
- return flush();
58
- };
48
+ }
59
49
 
60
- return stateView(
61
- state,
62
- {
63
- reset,
64
- init,
65
- applyImported,
66
- flush,
67
- finishQuit,
68
- },
69
- [],
70
- ['store', 'didResume', 'importedRemote'],
71
- );
72
- };
50
+ finishQuit(status) {
51
+ setStatus(this.store, status);
52
+ return this.flush();
53
+ }
54
+ }
73
55
 
74
- module.exports = { createReviewController };
56
+ module.exports = { ReviewController };