reslop 0.2.4 → 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 (67) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/README.md +1 -2
  3. package/lib/capabilities.js +0 -4
  4. package/lib/clipboard.js +39 -1
  5. package/lib/deps-diff.js +0 -3
  6. package/lib/deps.js +144 -176
  7. package/lib/diff/inline.js +66 -1
  8. package/lib/diff/unit.js +11 -28
  9. package/lib/editor.js +69 -9
  10. package/lib/files.js +17 -16
  11. package/lib/git-deps.js +58 -95
  12. package/lib/github.js +7 -7
  13. package/lib/gitlab.js +3 -3
  14. package/lib/highlight/core.js +11 -0
  15. package/lib/highlight/dot.js +2 -12
  16. package/lib/highlight/log.js +4 -11
  17. package/lib/highlight/txt.js +2 -12
  18. package/lib/keys.js +22 -12
  19. package/lib/manifest.js +0 -4
  20. package/lib/npm-commands.js +1 -22
  21. package/lib/remote.js +1 -7
  22. package/lib/render/branches.js +4 -13
  23. package/lib/render/chrome.js +85 -31
  24. package/lib/render/commits.js +95 -207
  25. package/lib/render/diff.js +31 -1
  26. package/lib/render/files.js +7 -16
  27. package/lib/render/frame.js +95 -110
  28. package/lib/render/notes.js +71 -16
  29. package/lib/render/npm.js +104 -18
  30. package/lib/render/primitives.js +108 -45
  31. package/lib/render/unit.js +4 -1
  32. package/lib/report-parse.js +0 -1
  33. package/lib/report-run.js +0 -1
  34. package/lib/report-test.js +40 -47
  35. package/lib/report-tools.js +0 -7
  36. package/lib/review-merge.js +101 -131
  37. package/lib/review.js +64 -77
  38. package/lib/select.js +52 -0
  39. package/lib/session/actions.js +23 -22
  40. package/lib/session/branches.js +5 -3
  41. package/lib/session/change-actions.js +173 -146
  42. package/lib/session/commit.js +32 -13
  43. package/lib/session/compose.js +120 -77
  44. package/lib/session/dispatch.js +36 -21
  45. package/lib/session/edit-click.js +57 -0
  46. package/lib/session/files-pane.js +24 -24
  47. package/lib/session/items.js +69 -89
  48. package/lib/session/lifecycle.js +31 -96
  49. package/lib/session/load.js +51 -58
  50. package/lib/session/navigation.js +105 -96
  51. package/lib/session/npm.js +158 -77
  52. package/lib/session/ops.js +192 -51
  53. package/lib/session/pointer.js +11 -5
  54. package/lib/session/progress.js +9 -6
  55. package/lib/session/review.js +35 -54
  56. package/lib/session/tasks.js +292 -0
  57. package/lib/session/terminal.js +13 -17
  58. package/lib/session/unit.js +3 -4
  59. package/lib/session/update.js +76 -97
  60. package/lib/session/view.js +17 -14
  61. package/lib/session/watch.js +47 -27
  62. package/lib/session.js +34 -79
  63. package/lib/update.js +1 -5
  64. package/lib/utilities.js +21 -1
  65. package/lib/wrap.js +15 -0
  66. package/package.json +1 -1
  67. package/lib/session/todos.js +0 -293
@@ -1,113 +1,92 @@
1
1
  'use strict';
2
2
 
3
- const accessors = require('./accessors.js');
4
- const { stateView } = accessors;
5
-
6
3
  const update = require('../update.js');
7
4
  const { checkUpdate, installUpdate, markSkipped } = update;
8
5
 
9
- const optsOf = (api) => {
10
- const value = api.ui.updateOpts;
11
- if (!value || value === true) return {};
12
- return value;
13
- };
14
-
15
- const reset = (state) => {
16
- state.offer = false;
17
- state.from = '';
18
- state.to = '';
19
- state.checkPromise = null;
20
- state.installPromise = null;
21
- };
6
+ class UpdateCoordinator {
7
+ constructor(ui) {
8
+ this.ui = ui;
9
+ this.reset();
10
+ }
22
11
 
23
- const maybePrompt = (api) => {
24
- if (!api.state.offer) return;
25
- if (!api.ui.loader.didLoad) return;
26
- if (api.ui.mode !== 'review') return;
27
- api.ui.mode = 'confirmUpdate';
28
- };
12
+ get opts() {
13
+ const value = this.ui.updateOpts;
14
+ if (!value || value === true) return {};
15
+ return value;
16
+ }
29
17
 
30
- const runInstall = async (api, version) => {
31
- api.ui.status = 'updating reslop';
32
- api.ui.progressFrame = 0;
33
- api.ui.progress.start('install');
34
- api.ui.paint();
35
- let ok = false;
36
- try {
37
- await installUpdate(version, optsOf(api));
38
- ok = true;
39
- } catch {
40
- // ignore install errors
18
+ reset() {
19
+ this.offer = false;
20
+ this.from = '';
21
+ this.to = '';
22
+ this.checkPromise = null;
23
+ this.installPromise = null;
41
24
  }
42
- api.ui.progress.stop('install');
43
- if (api.ui.done || !api.ui.uiOpen) return;
44
- api.ui.status = ok ? 'updated' : 'update failed';
45
- api.ui.paint();
46
- };
47
25
 
48
- const acceptUpdate = (api) => {
49
- api.ui.mode = 'review';
50
- api.state.offer = false;
51
- const version = api.state.to;
52
- api.state.installPromise = runInstall(api, version);
53
- };
26
+ maybePrompt() {
27
+ const { ui } = this;
28
+ if (!this.offer || !ui.loader.didLoad) return;
29
+ if (ui.mode === 'review') ui.mode = 'confirmUpdate';
30
+ }
54
31
 
55
- const declineUpdate = (api) => {
56
- api.ui.mode = 'review';
57
- api.state.offer = false;
58
- api.ui.status = '';
59
- markSkipped(api.state.to, optsOf(api));
60
- };
32
+ async install(version) {
33
+ const { ui } = this;
34
+ ui.status = 'updating reslop';
35
+ ui.progressFrame = 0;
36
+ ui.progress.start('install');
37
+ ui.paint();
38
+ let ok = false;
39
+ try {
40
+ await installUpdate(version, this.opts);
41
+ ok = true;
42
+ } catch {
43
+ // ignore install errors
44
+ }
45
+ ui.progress.stop('install');
46
+ if (ui.done || !ui.uiOpen) return;
47
+ ui.status = ok ? 'updated' : 'update failed';
48
+ ui.paint();
49
+ }
61
50
 
62
- const runOpenUpdate = async (api) => {
63
- let plan;
64
- try {
65
- plan = await checkUpdate(optsOf(api));
66
- } catch {
67
- return;
51
+ accept() {
52
+ this.ui.mode = 'review';
53
+ this.offer = false;
54
+ this.installPromise = this.install(this.to);
68
55
  }
69
- if (api.ui.done) return;
70
- if (plan.action === 'install') {
71
- api.state.installPromise = runInstall(api, plan.latest);
72
- await api.state.installPromise;
73
- return;
56
+
57
+ decline() {
58
+ this.ui.mode = 'review';
59
+ this.offer = false;
60
+ this.ui.status = '';
61
+ markSkipped(this.to, this.opts);
74
62
  }
75
- if (plan.action !== 'confirm') return;
76
- api.state.from = plan.current;
77
- api.state.to = plan.latest;
78
- api.state.offer = true;
79
- maybePrompt(api);
80
- api.ui.paint();
81
- };
82
63
 
83
- const openUpdate = (api) => {
84
- if (api.ui.updateOpts === false) return Promise.resolve();
85
- if (api.state.checkPromise) return api.state.checkPromise;
86
- api.state.checkPromise = runOpenUpdate(api);
87
- return api.state.checkPromise;
88
- };
64
+ async check() {
65
+ let plan;
66
+ try {
67
+ plan = await checkUpdate(this.opts);
68
+ } catch {
69
+ return;
70
+ }
71
+ if (this.ui.done) return;
72
+ if (plan.action === 'install') {
73
+ this.installPromise = this.install(plan.latest);
74
+ await this.installPromise;
75
+ return;
76
+ }
77
+ if (plan.action !== 'confirm') return;
78
+ this.from = plan.current;
79
+ this.to = plan.latest;
80
+ this.offer = true;
81
+ this.maybePrompt();
82
+ this.ui.paint();
83
+ }
89
84
 
90
- const createUpdateCoordinator = (ui) => {
91
- const state = {
92
- offer: false,
93
- from: '',
94
- to: '',
95
- checkPromise: null,
96
- installPromise: null,
97
- };
98
- const api = { ui, state };
99
- return stateView(
100
- state,
101
- {
102
- reset: () => reset(state),
103
- maybePrompt: () => maybePrompt(api),
104
- accept: () => acceptUpdate(api),
105
- decline: () => declineUpdate(api),
106
- open: () => openUpdate(api),
107
- },
108
- ['offer', 'installPromise'],
109
- ['from', 'to', 'checkPromise'],
110
- );
111
- };
85
+ open() {
86
+ if (this.ui.updateOpts === false) return Promise.resolve();
87
+ if (!this.checkPromise) this.checkPromise = this.check();
88
+ return this.checkPromise;
89
+ }
90
+ }
112
91
 
113
- module.exports = { createUpdateCoordinator };
92
+ module.exports = { UpdateCoordinator };
@@ -5,7 +5,7 @@ const path = require('node:path');
5
5
  const files = require('../files.js');
6
6
  const commit = require('./commit.js');
7
7
  const { listedCommits } = commit;
8
- const { fileEntries, fileTotals, REPO_TODOS_LABEL } = files;
8
+ const { fileEntries, fileTotals, REPO_TASKS_LABEL } = files;
9
9
  const review = require('../review.js');
10
10
  const { noteCounts } = review;
11
11
  const diff = require('../diff/diff.js');
@@ -25,12 +25,12 @@ const repoNameOf = (api) => {
25
25
  return path.basename(ui.top || ui.cwd || '');
26
26
  };
27
27
 
28
- const todosEntry = (counts) => ({
29
- path: REPO_TODOS_LABEL,
30
- kind: 'todos',
31
- status: 'todos',
32
- remaining: counts.todo,
33
- staged: counts.todoDone,
28
+ const tasksEntry = (counts) => ({
29
+ path: REPO_TASKS_LABEL,
30
+ kind: 'tasks',
31
+ status: 'tasks',
32
+ remaining: counts.tasks,
33
+ staged: counts.tasksDone,
34
34
  firstIndex: 0,
35
35
  openIndex: 0,
36
36
  added: 0,
@@ -94,7 +94,7 @@ const fileList = (api, notes = noteCounts(api.review.store)) => {
94
94
  const names = listedNames(api);
95
95
  const scoped =
96
96
  api.nav.fileScope === 'file' ? mergeUnitFiles(labeled, names) : labeled;
97
- scoped.unshift(todosEntry(notes));
97
+ scoped.unshift(tasksEntry(notes));
98
98
  return scoped;
99
99
  };
100
100
 
@@ -105,7 +105,7 @@ const counts = (api, notes = noteCounts(api.review.store)) => {
105
105
  untracked: 0,
106
106
  commit: 0,
107
107
  pr: 0,
108
- todo: 0,
108
+ tasks: 0,
109
109
  feedback: 0,
110
110
  code: 0,
111
111
  };
@@ -114,7 +114,7 @@ const counts = (api, notes = noteCounts(api.review.store)) => {
114
114
  next[item.origin] += 1;
115
115
  }
116
116
  next.feedback = notes.feedback;
117
- next.todo = notes.todo;
117
+ next.tasks = notes.tasks;
118
118
  next.code = notes.code;
119
119
  return next;
120
120
  };
@@ -205,6 +205,9 @@ const view = (api) => {
205
205
  npmFollow: npm.followEnd,
206
206
  npmScroll: npm.logScroll,
207
207
  npmEditName: npm.editName,
208
+ npmEditField: npm.editField,
209
+ npmDraftName: npm.draftName,
210
+ npmDraftCommand: npm.draftCommand,
208
211
  npmRunning: npm.running,
209
212
  npmLogs: nav.pane === 'npm' ? npm.logLabel : '',
210
213
  repoName: repoNameOf(api),
@@ -215,9 +218,9 @@ const view = (api) => {
215
218
  mode: ui.mode,
216
219
  compose: composer.composeView(),
217
220
  noteText: composer.idleNoteText(),
218
- todos: composer.todoTexts(),
219
- todoFocus: composer.clampedTodoFocus(),
220
- todoEdit: composer.todoEditView(),
221
+ tasks: composer.taskTexts(),
222
+ tasksFocus: composer.clampedTaskFocus(),
223
+ taskEdit: composer.taskEditView(),
221
224
  codeOverlay: composer.codeOverlayView(),
222
225
  templates,
223
226
  templateIndex: composer.clampedTemplateIndex(templates),
@@ -234,4 +237,4 @@ const createView = (api) => ({
234
237
  view: () => view(api),
235
238
  });
236
239
 
237
- module.exports = { relabelStatus, createView };
240
+ module.exports = { createView };
@@ -4,7 +4,6 @@ const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
 
6
6
  const DEBOUNCE_MS = 200;
7
- const POLL_MS = 250;
8
7
 
9
8
  const SKIP_DIRS = ['node_modules', '.review', 'coverage', 'dist', '.cache'];
10
9
 
@@ -45,14 +44,17 @@ const ignoredName = (name) => {
45
44
  };
46
45
 
47
46
  const ignoredGitRel = (parts) => {
47
+ if (parts[1] === 'logs' && parts[2] === 'HEAD') return false;
48
48
  const name = parts[1] ?? '';
49
49
  if (!name) return false;
50
50
  if (GIT_WATCH_NAMES.includes(name)) return false;
51
51
  return true;
52
52
  };
53
53
 
54
+ const slashRel = (rel) => `${rel}`.replaceAll('\\', '/');
55
+
54
56
  const ignoredRel = (rel) => {
55
- const norm = `${rel}`.replaceAll('\\', '/');
57
+ const norm = slashRel(rel);
56
58
  if (!norm || norm === '.') return false;
57
59
  const parts = norm.split('/');
58
60
  for (const part of parts) {
@@ -95,7 +97,7 @@ const hasFile = (full) => {
95
97
  };
96
98
 
97
99
  const isBareReviewName = (root, dir, rel) => {
98
- if (!rel || rel.includes('/') || !rel.endsWith('.md')) return false;
100
+ if (!rel || rel.includes('/')) return false;
99
101
  const base = path.basename(rel);
100
102
  if (hasFile(path.join(dir, base))) return false;
101
103
  return hasFile(path.join(root, '.review', base));
@@ -133,6 +135,15 @@ const isOwnDirEvent = (dir, name) => {
133
135
  }
134
136
  };
135
137
 
138
+ const unchangedSince = (full, since) => {
139
+ try {
140
+ const stat = fs.statSync(full);
141
+ return Math.max(stat.mtimeMs, stat.ctimeMs) < since;
142
+ } catch {
143
+ return false;
144
+ }
145
+ };
146
+
136
147
  const schedule = (state, timerKey, fire) => {
137
148
  if (state.closed || !fire) return;
138
149
  if (state[timerKey] !== null) clearTimeout(state[timerKey]);
@@ -154,9 +165,10 @@ const reviewBurst = (state) => {
154
165
  return Date.now() - seen < hold;
155
166
  };
156
167
 
157
- const scheduleChange = (state) => {
168
+ const scheduleChange = (state, loose = false) => {
169
+ state.looseChange = loose && (state.timer === null || state.looseChange);
158
170
  schedule(state, 'timer', () => {
159
- if (reviewBurst(state)) return;
171
+ if (state.looseChange && reviewBurst(state)) return;
160
172
  if (state.onChange) state.onChange();
161
173
  });
162
174
  };
@@ -164,34 +176,39 @@ const scheduleChange = (state) => {
164
176
  const scheduleUnnamed = (state) => {
165
177
  if (reviewBurst(state)) return;
166
178
  schedule(state, 'looseTimer', () => {
167
- if (reviewBurst(state)) return;
168
- scheduleChange(state);
179
+ if (!reviewBurst(state)) scheduleChange(state, true);
169
180
  });
170
181
  };
171
182
 
172
- const dropChange = (state) => {
173
- if (state.timer !== null) clearTimeout(state.timer);
174
- if (state.looseTimer !== null) clearTimeout(state.looseTimer);
175
- state.timer = null;
176
- state.looseTimer = null;
183
+ const clearTimer = (state, timerKey) => {
184
+ if (state[timerKey] !== null) clearTimeout(state[timerKey]);
185
+ state[timerKey] = null;
186
+ };
187
+
188
+ const dropLooseChange = (state) => {
189
+ clearTimer(state, 'looseTimer');
190
+ if (state.looseChange) clearTimer(state, 'timer');
177
191
  };
178
192
 
179
193
  const handleWatchEvent = (state, dir, eventType, filename) => {
180
194
  if (state.closed) return;
181
195
  const name = filename ? `${filename}` : '';
182
- const rel = name ? path.relative(state.root, path.join(dir, name)) : '';
196
+ const rel = name
197
+ ? slashRel(path.relative(state.root, path.join(dir, name)))
198
+ : '';
199
+ const wanted = state.files.has(rel);
183
200
  if (isReviewEvent(state.root, dir, name)) {
184
201
  state.reviewSeenAt = Date.now();
185
- dropChange(state);
186
- return void schedule(state, 'reviewTimer', state.onReview);
202
+ dropLooseChange(state);
203
+ schedule(state, 'reviewTimer', state.onReview);
204
+ if (!wanted) return;
187
205
  }
188
- if (rel && ignoredRel(rel)) return;
189
- if (reviewBurst(state)) return;
190
206
  if (!name) return void scheduleUnnamed(state);
207
+ if (!wanted && ignoredRel(rel)) return;
191
208
  if (isOwnDirEvent(dir, name)) return;
192
- if (eventType === 'rename' && name) {
193
- state.watchNewDir(path.join(dir, name));
194
- }
209
+ const full = path.join(dir, name);
210
+ if (unchangedSince(full, state.startedAt)) return;
211
+ if (eventType === 'rename') state.watchNewDir(full);
195
212
  scheduleChange(state);
196
213
  };
197
214
 
@@ -263,24 +280,24 @@ const watchGit = (state) => {
263
280
  const gitDir = path.resolve(state.root, '.git');
264
281
  watchOne(state, gitDir);
265
282
  watchOne(state, path.resolve(gitDir, 'refs'));
283
+ watchOne(state, path.resolve(gitDir, 'logs'));
266
284
  };
267
285
 
268
286
  const stopState = (state) => {
269
287
  state.closed = true;
270
- if (state.timer !== null) clearTimeout(state.timer);
271
- if (state.reviewTimer !== null) clearTimeout(state.reviewTimer);
272
- if (state.looseTimer !== null) clearTimeout(state.looseTimer);
273
- state.timer = null;
274
- state.reviewTimer = null;
275
- state.looseTimer = null;
288
+ clearTimer(state, 'timer');
289
+ clearTimer(state, 'reviewTimer');
290
+ clearTimer(state, 'looseTimer');
276
291
  while (state.watchers.length) closeWatcher(state.watchers.pop());
277
292
  state.watched.clear();
278
293
  };
279
294
 
280
295
  const watchFiles = (state, rels) => {
296
+ state.files = new Set();
281
297
  for (const rel of rels) {
282
298
  if (!rel) continue;
283
299
  const full = path.resolve(state.root, rel);
300
+ state.files.add(slashRel(path.relative(state.root, full)));
284
301
  watchOne(state, path.dirname(full));
285
302
  }
286
303
  };
@@ -295,11 +312,14 @@ const createDiskWatcher = (options) => {
295
312
  dirWatchOpts: recursive ? { recursive: true } : {},
296
313
  watchers: [],
297
314
  watched: new Set(),
315
+ files: new Set(),
298
316
  onReview: options.onReview,
299
317
  timer: null,
300
318
  reviewTimer: null,
301
319
  looseTimer: null,
320
+ looseChange: false,
302
321
  reviewSeenAt: 0,
322
+ startedAt: Date.now(),
303
323
  closed: false,
304
324
  watchNewDir: (full) => maybeWatchNewDir(state, full),
305
325
  };
@@ -315,9 +335,9 @@ const createDiskWatcher = (options) => {
315
335
 
316
336
  module.exports = {
317
337
  DEBOUNCE_MS,
318
- POLL_MS,
319
338
  canWatchRecursive,
320
339
  ignoredRel,
321
340
  isOwnDirEvent,
341
+ unchangedSince,
322
342
  createDiskWatcher,
323
343
  };
package/lib/session.js CHANGED
@@ -4,7 +4,7 @@ const render = require('./render/render.js');
4
4
  const keys = require('./keys.js');
5
5
  const review = require('./review.js');
6
6
  const files = require('./files.js');
7
- const { makeTodoItem } = files;
7
+ const { makeTaskItem } = files;
8
8
  const { renderFrame } = render;
9
9
  const { decodeChunk } = keys;
10
10
  const { AUTOSAVE_MS, hasNotes } = review;
@@ -15,21 +15,21 @@ const { sessionCapabilities } = capabilities;
15
15
  const terminal = require('./session/terminal.js');
16
16
  const { createTerminal, LEAVE_TERM } = terminal;
17
17
  const load = require('./session/load.js');
18
- const { createLoadCoordinator } = load;
18
+ const { LoadCoordinator } = load;
19
19
  const items = require('./session/items.js');
20
- const { createItemCollection } = items;
20
+ const { ItemCollection } = items;
21
21
  const navigation = require('./session/navigation.js');
22
- const { createNavigation } = navigation;
22
+ const { Navigation } = navigation;
23
23
  const sessionReview = require('./session/review.js');
24
- const { createReviewController } = sessionReview;
24
+ const { ReviewController } = sessionReview;
25
25
  const update = require('./session/update.js');
26
- const { createUpdateCoordinator } = update;
26
+ const { UpdateCoordinator } = update;
27
27
  const progress = require('./session/progress.js');
28
28
  const { createProgress } = progress;
29
29
  const compose = require('./session/compose.js');
30
30
  const { createComposer } = compose;
31
31
  const ops = require('./session/ops.js');
32
- const { createOpsRunner } = ops;
32
+ const { OpsRunner } = ops;
33
33
  const changeActions = require('./session/change-actions.js');
34
34
  const { createChangeActions } = changeActions;
35
35
  const commit = require('./session/commit.js');
@@ -51,7 +51,6 @@ const { createPointer } = pointer;
51
51
  const dispatch = require('./session/dispatch.js');
52
52
  const { createDispatcher, createInput } = dispatch;
53
53
 
54
- const CURSOR_BLINK_MS = 500;
55
54
  const PROGRESS_MS = 160;
56
55
 
57
56
  const NAV_FIELDS = [
@@ -69,19 +68,18 @@ const NAV_FIELDS = [
69
68
  'selection',
70
69
  'mouseAnchor',
71
70
  'pendingClick',
72
- 'todoOpen',
73
- 'todoFocus',
71
+ 'tasksOpen',
72
+ 'tasksFocus',
74
73
  ];
75
74
 
76
75
  const COMPOSER_FIELDS = [
77
76
  'editor',
78
77
  'composeKind',
79
78
  'commitKind',
80
- 'composeTodoId',
79
+ 'composeTaskId',
81
80
  'composeBaseline',
82
81
  'templateIndex',
83
82
  'templateFocus',
84
- 'blinkOn',
85
83
  ];
86
84
 
87
85
  const assignOptions = (session, options) => {
@@ -114,12 +112,10 @@ const assignOptions = (session, options) => {
114
112
  stdout: session.stdout,
115
113
  proc: session.proc,
116
114
  });
117
- session.loader = createLoadCoordinator();
118
- session.collection = createItemCollection();
119
- session.nav = createNavigation({
120
- startPane: options.startPane ?? 'files',
121
- });
122
- session.review = createReviewController();
115
+ session.loader = new LoadCoordinator();
116
+ session.collection = new ItemCollection();
117
+ session.nav = new Navigation({ startPane: options.startPane ?? 'files' });
118
+ session.review = new ReviewController();
123
119
  session.progress = createProgress(
124
120
  session.term,
125
121
  () => session.tickProgress(),
@@ -132,19 +128,11 @@ const assignComposer = (session) => {
132
128
  nav: session.nav,
133
129
  review: session.review,
134
130
  ui: session,
135
- restartBlink: () => {
136
- if (!session.term.hasTimer('blink')) return;
137
- session.term.restartTimer(
138
- 'blink',
139
- () => session.tickBlink(),
140
- CURSOR_BLINK_MS,
141
- );
142
- },
143
131
  });
144
132
  };
145
133
 
146
134
  const assignControllers = (session) => {
147
- session.ops = createOpsRunner(session);
135
+ session.ops = new OpsRunner(session);
148
136
  session.changeActions = createChangeActions(session);
149
137
  session.commits = createCommitController(session);
150
138
  session.gitBranches = createBranchController(session);
@@ -155,48 +143,14 @@ const bindSessionAccessors = (session) => {
155
143
  bindAccessors(session, session.nav, NAV_FIELDS);
156
144
  bindAccessors(session, session.composer.state, COMPOSER_FIELDS);
157
145
  bindAccessors(session, session.loader, ['pendingExtras']);
158
- bindAlias(
159
- session,
160
- 'loadPromise',
161
- () => session.loader.promise,
162
- (value) => {
163
- session.loader.promise = value;
164
- },
165
- );
146
+ bindGetters(session, session.loader, ['didLoad']);
166
147
  bindAccessors(session, session.collection, ['items']);
167
148
  bindGetters(session, session.collection, ['dismissed']);
168
- bindAccessors(session, session.updater, ['installPromise']);
149
+ bindGetters(session, session.updater, ['installPromise']);
169
150
  bindAccessors(session, session.ops, ['gitBusy', 'busy']);
170
151
  bindAccessors(session, session.gitBranches, ['branches', 'dropName']);
171
152
  bindGetters(session, session.term, ['lastFrame', 'lastSize']);
172
- bindAlias(session, 'loadGen', () => session.loader.gen);
173
- bindAlias(
174
- session,
175
- 'didLoad',
176
- () => session.loader.didLoad,
177
- (value) => {
178
- session.loader.didLoad = value;
179
- if (value) session.updater.maybePrompt();
180
- },
181
- );
182
153
  bindAlias(session, 'notes', () => session.review.store);
183
- bindAlias(session, 'didResumeReview', () => session.review.didResume);
184
- bindAlias(
185
- session,
186
- 'importedRemoteNotes',
187
- () => session.review.importedRemote,
188
- );
189
- bindAlias(
190
- session,
191
- 'updateOffer',
192
- () => session.updater.offer,
193
- (value) => {
194
- session.updater.offer = value;
195
- },
196
- );
197
- bindAlias(session, 'updateFrom', () => session.updater.from);
198
- bindAlias(session, 'updateTo', () => session.updater.to);
199
- bindAlias(session, 'updatePromise', () => session.updater.checkPromise);
200
154
  };
201
155
 
202
156
  const bindViewer = (session) => {
@@ -241,7 +195,7 @@ const bindDispatch = (session) => {
241
195
 
242
196
  const composeSession = (session, options) => {
243
197
  assignOptions(session, options);
244
- session.updater = createUpdateCoordinator(session);
198
+ session.updater = new UpdateCoordinator(session);
245
199
  assignComposer(session);
246
200
  assignControllers(session);
247
201
  bindSessionAccessors(session);
@@ -273,7 +227,7 @@ class Session {
273
227
  this.done = false;
274
228
  this.exitCode = 0;
275
229
  this.mode = 'review';
276
- this.todoItem = makeTodoItem();
230
+ this.tasksItem = makeTaskItem();
277
231
  this.progressFrame = 0;
278
232
  this.emptyReview = false;
279
233
  this.loadError = null;
@@ -316,10 +270,6 @@ class Session {
316
270
  this.lifecycle.ignoreWatch(ms);
317
271
  }
318
272
 
319
- applyLoaded(loaded) {
320
- return this.lifecycle.applyLoaded(loaded);
321
- }
322
-
323
273
  paint() {
324
274
  if (!this.uiOpen) return;
325
275
  this.draw();
@@ -336,10 +286,21 @@ class Session {
336
286
  if (LIST_PANES.includes(this.pane)) {
337
287
  this.nav.listScroll = frame.listScroll;
338
288
  }
339
- if (this.nav.todoOpen) this.nav.scroll = frame.scroll;
289
+ if (this.nav.tasksOpen) this.nav.scroll = frame.scroll;
290
+ const editScroll = frame.cursor && frame.cursor.scroll;
291
+ if (this.editor && typeof editScroll === 'number') {
292
+ this.editor.scrollCol = editScroll;
293
+ }
340
294
  const composing = this.mode === 'compose';
341
- const cursor = composing && this.blinkOn ? frame.cursor : null;
342
- this.term.paint(frame, size, cursor);
295
+ this.term.paint(frame, size, composing ? frame.cursor : null);
296
+ this.scheduleAutosave();
297
+ }
298
+
299
+ scheduleAutosave() {
300
+ if (!this.finishReadLoop) return;
301
+ const dirty = this.notes && this.notes.dirty;
302
+ if (!dirty && this.composeKind === null) return;
303
+ this.term.later('save', () => this.autosave(), AUTOSAVE_MS);
343
304
  }
344
305
 
345
306
  view() {
@@ -359,7 +320,7 @@ class Session {
359
320
  }
360
321
 
361
322
  current() {
362
- return this.nav.current(this.items, this.todoItem);
323
+ return this.nav.current(this.items, this.tasksItem);
363
324
  }
364
325
 
365
326
  fileCursorEntry() {
@@ -442,10 +403,6 @@ class Session {
442
403
  this.composer.startDraftCompose();
443
404
  }
444
405
 
445
- tickBlink() {
446
- this.composer.tickBlink();
447
- }
448
-
449
406
  autosave() {
450
407
  this.composer.autosave();
451
408
  }
@@ -550,8 +507,6 @@ class Session {
550
507
  onData,
551
508
  onResize: () => this.draw(),
552
509
  });
553
- this.term.startTimer('save', () => this.autosave(), AUTOSAVE_MS);
554
- this.term.startTimer('blink', () => this.tickBlink(), CURSOR_BLINK_MS);
555
510
  this.lifecycle.startWatch();
556
511
  });
557
512
  }