reslop 0.2.5 → 0.2.7

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 (114) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +72 -19
  6. package/lib/api.js +1 -1
  7. package/lib/capabilities.js +8 -8
  8. package/lib/cli.js +61 -75
  9. package/lib/clipboard.js +39 -47
  10. package/lib/dashboard/git.js +128 -0
  11. package/lib/dashboard/model.js +357 -0
  12. package/lib/dashboard/npm.js +290 -0
  13. package/lib/dashboard/tree.js +278 -0
  14. package/lib/deps-diff.js +197 -402
  15. package/lib/deps.js +113 -240
  16. package/lib/detect.js +8 -18
  17. package/lib/diff/align.js +41 -113
  18. package/lib/diff/diff.js +12 -43
  19. package/lib/diff/display.js +25 -36
  20. package/lib/diff/edit.js +6 -20
  21. package/lib/diff/inline.js +76 -144
  22. package/lib/diff/items.js +33 -47
  23. package/lib/diff/parse.js +28 -56
  24. package/lib/diff/patch.js +12 -33
  25. package/lib/diff/unit.js +79 -178
  26. package/lib/editor.js +52 -69
  27. package/lib/files.js +79 -91
  28. package/lib/find.js +61 -0
  29. package/lib/git-branches.js +254 -416
  30. package/lib/git-deps.js +148 -272
  31. package/lib/git-reword-todo.js +5 -13
  32. package/lib/git-worktree.js +161 -214
  33. package/lib/git.js +21 -49
  34. package/lib/github.js +67 -181
  35. package/lib/gitlab.js +40 -90
  36. package/lib/highlight/bash.js +55 -177
  37. package/lib/highlight/core.js +155 -141
  38. package/lib/highlight/css.js +25 -58
  39. package/lib/highlight/csv.js +19 -37
  40. package/lib/highlight/dart.js +72 -213
  41. package/lib/highlight/dot.js +14 -14
  42. package/lib/highlight/html.js +42 -63
  43. package/lib/highlight/js.js +177 -506
  44. package/lib/highlight/json.js +17 -37
  45. package/lib/highlight/log.js +19 -25
  46. package/lib/highlight/txt.js +7 -10
  47. package/lib/highlight.js +17 -46
  48. package/lib/keys.js +75 -83
  49. package/lib/manifest.js +67 -126
  50. package/lib/npm-commands.js +120 -169
  51. package/lib/remote.js +140 -190
  52. package/lib/render/branches.js +63 -61
  53. package/lib/render/chrome.js +412 -347
  54. package/lib/render/commits.js +160 -361
  55. package/lib/render/dash-activity.js +212 -0
  56. package/lib/render/dash-blocks.js +496 -0
  57. package/lib/render/dash-table.js +295 -0
  58. package/lib/render/dashboard.js +62 -0
  59. package/lib/render/diff.js +123 -258
  60. package/lib/render/files.js +60 -86
  61. package/lib/render/frame.js +82 -117
  62. package/lib/render/notes.js +150 -210
  63. package/lib/render/npm.js +213 -177
  64. package/lib/render/packages.js +93 -0
  65. package/lib/render/present.js +4 -11
  66. package/lib/render/primitives.js +77 -172
  67. package/lib/render/render.js +5 -14
  68. package/lib/render/report.js +43 -0
  69. package/lib/render/repos.js +226 -0
  70. package/lib/render/search.js +25 -0
  71. package/lib/render/tiles.js +355 -0
  72. package/lib/render/unit.js +15 -31
  73. package/lib/report-model.js +110 -33
  74. package/lib/report-parse.js +56 -37
  75. package/lib/report-render.js +102 -59
  76. package/lib/report-run.js +87 -77
  77. package/lib/report-test.js +221 -99
  78. package/lib/report-tools.js +96 -154
  79. package/lib/review-merge.js +33 -41
  80. package/lib/review.js +69 -129
  81. package/lib/runs.js +225 -0
  82. package/lib/select.js +53 -91
  83. package/lib/session/accessors.js +14 -21
  84. package/lib/session/actions.js +189 -65
  85. package/lib/session/branches.js +164 -222
  86. package/lib/session/change-actions.js +49 -119
  87. package/lib/session/commit.js +298 -352
  88. package/lib/session/compose.js +572 -715
  89. package/lib/session/dashboard.js +530 -0
  90. package/lib/session/dispatch.js +197 -171
  91. package/lib/session/files-pane.js +92 -45
  92. package/lib/session/find.js +94 -0
  93. package/lib/session/items.js +6 -17
  94. package/lib/session/lifecycle.js +420 -466
  95. package/lib/session/load.js +1 -14
  96. package/lib/session/navigation.js +4 -2
  97. package/lib/session/npm.js +403 -420
  98. package/lib/session/ops.js +5 -13
  99. package/lib/session/packages.js +143 -0
  100. package/lib/session/pointer.js +30 -14
  101. package/lib/session/progress.js +39 -30
  102. package/lib/session/review.js +1 -3
  103. package/lib/session/tasks.js +277 -236
  104. package/lib/session/templates.js +51 -59
  105. package/lib/session/terminal.js +85 -132
  106. package/lib/session/view.js +59 -80
  107. package/lib/session/watch.js +201 -212
  108. package/lib/session/workspace.js +455 -0
  109. package/lib/session.js +95 -161
  110. package/lib/update.js +44 -111
  111. package/lib/utilities.js +46 -88
  112. package/lib/workspace.js +166 -0
  113. package/lib/wrap.js +29 -55
  114. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const files = require('../files.js');
4
- const { isTasksEntry, TASKS_FILE } = files;
4
+ const { isTasksEntry, TASKS_FILE, TASK_KINDS, taskKind } = files;
5
5
  const review = require('../review.js');
6
6
  const { addTask, setTaskText, setTaskDone, removeTask, checkLabel } = review;
7
7
 
@@ -24,269 +24,310 @@ const TASK_TYPE_SKIP = [
24
24
  'n',
25
25
  'p',
26
26
  'q',
27
+ 'f',
28
+ 'd',
29
+ 't',
30
+ 'b',
31
+ 'r',
32
+ 'c',
27
33
  ];
28
34
 
29
35
  const isTaskTypeKey = (key) => {
30
- if (!key) return false;
36
+ if (!key || key.length !== 1 || key === ' ') return false;
31
37
  if (TASK_TYPE_SKIP.includes(key)) return false;
32
- if (key.startsWith('ctrl-')) return false;
33
- if (key.startsWith('alt-')) return false;
38
+ if (key.startsWith('ctrl-') || key.startsWith('alt-')) return false;
34
39
  return key.charCodeAt(0) >= 32;
35
40
  };
36
41
 
37
- const listTasks = (api) => {
38
- const notes = api.review.store;
39
- if (!notes) return [];
40
- return notes.tasks;
41
- };
42
+ const clamp = (value, last) => Math.max(0, Math.min(value, last));
42
43
 
43
- const isDraftCompose = (api) => {
44
- const { state } = api;
45
- return state.composeKind === 'tasks' && state.composeTaskId === null;
46
- };
44
+ class TaskPage {
45
+ constructor(composer) {
46
+ this.composer = composer;
47
+ }
47
48
 
48
- const taskTexts = (api) => {
49
- const nav = api.nav;
50
- if (!nav.tasksOpen) return [];
51
- const todos = listTasks(api);
52
- const texts = new Array(todos.length + 1);
53
- const { state } = api;
54
- const editingTodo = state.composeKind === 'tasks';
55
- const editingId = editingTodo ? state.composeTaskId : null;
56
- for (let i = 0; i < todos.length; i++) {
57
- const todo = todos[i];
58
- const live = editingId === todo.id && state.editor;
59
- const text = live ? state.editor.text : todo.text;
60
- texts[i] = checkLabel(text, todo.done);
61
- }
62
- const draft = isDraftCompose(api) && state.editor ? state.editor.text : '';
63
- texts[todos.length] = checkLabel(draft, false);
64
- return texts;
65
- };
49
+ get ui() {
50
+ return this.composer.ui;
51
+ }
66
52
 
67
- const taskRowCount = (api) => {
68
- if (!api.nav.tasksOpen) return 0;
69
- return listTasks(api).length + 1;
70
- };
53
+ get nav() {
54
+ return this.composer.ui.nav;
55
+ }
71
56
 
72
- const clampedTaskFocus = (api) => {
73
- const last = Math.max(0, taskRowCount(api) - 1);
74
- const focus = api.nav.tasksFocus;
75
- if (focus < 0) return 0;
76
- if (focus > last) return last;
77
- return focus;
78
- };
57
+ isOpen() {
58
+ return this.nav.pane === 'tasks';
59
+ }
79
60
 
80
- const moveTaskFocus = (api, delta) => {
81
- const nav = api.nav;
82
- if (!nav.tasksOpen) return;
83
- const n = taskRowCount(api);
84
- if (!n) return;
85
- const next = clampedTaskFocus(api) + delta;
86
- if (next < 0) nav.tasksFocus = 0;
87
- else if (next >= n) nav.tasksFocus = n - 1;
88
- else nav.tasksFocus = next;
89
- };
61
+ list(kind) {
62
+ const notes = this.ui.review.store;
63
+ if (!notes) return [];
64
+ const rows = [];
65
+ for (const task of notes.tasks) {
66
+ if (taskKind(task) === kind) rows.push(task);
67
+ }
68
+ return rows;
69
+ }
90
70
 
91
- const openTasksPage = (api) => {
92
- const nav = api.nav;
93
- nav.tasksOpen = true;
94
- nav.pane = 'diff';
95
- nav.scroll = 0;
96
- nav.clearSelection();
97
- api.ui.syncReviewPath();
98
- };
71
+ rows() {
72
+ const rows = [];
73
+ for (const kind of TASK_KINDS) {
74
+ const tasks = this.list(kind.id);
75
+ for (const task of tasks) {
76
+ rows.push({ kind: kind.id, task, draft: false });
77
+ }
78
+ rows.push({ kind: kind.id, task: null, draft: true });
79
+ }
80
+ return rows;
81
+ }
99
82
 
100
- const closeTasksPage = (api) => {
101
- api.nav.tasksOpen = false;
102
- api.ui.syncReviewPath();
103
- api.ui.syncFileCursor();
104
- api.ui.lifecycle.catchUpWatch();
105
- };
83
+ rowAt(index) {
84
+ return this.rows()[index] ?? null;
85
+ }
106
86
 
107
- const focusTodoPage = (api, todoId) => {
108
- openTasksPage(api);
109
- const todos = listTasks(api);
110
- let focus = 0;
111
- if (todoId === null) {
112
- focus = todos.length;
113
- } else if (todoId !== undefined) {
114
- const at = todos.findIndex((todo) => todo.id === todoId);
115
- if (at >= 0) focus = at;
116
- else if (todos.length) focus = todos.length - 1;
117
- }
118
- api.nav.tasksFocus = focus;
119
- api.nav.scroll = 0;
120
- return true;
121
- };
87
+ rowIndex(dest) {
88
+ const rows = this.rows();
89
+ if (!dest) return 0;
90
+ for (let i = 0; i < rows.length; i++) {
91
+ const row = rows[i];
92
+ if (dest.draft) {
93
+ if (row.draft && row.kind === dest.kind) return i;
94
+ continue;
95
+ }
96
+ if (row.task && dest.task && row.task.id === dest.task.id) return i;
97
+ }
98
+ return clamp(0, rows.length - 1);
99
+ }
122
100
 
123
- const startDraftCompose = (api) => {
124
- focusTodoPage(api, null);
125
- api.open('tasks', '', null);
126
- };
101
+ editingId() {
102
+ const composer = this.composer;
103
+ return composer.composeKind === 'tasks' ? composer.composeTaskId : null;
104
+ }
127
105
 
128
- const editFocusedTask = (api) => {
129
- if (!api.nav.tasksOpen) return;
130
- const todos = listTasks(api);
131
- const todo = todos[clampedTaskFocus(api)];
132
- if (!todo) return void startDraftCompose(api);
133
- api.open('tasks', todo.text, todo.id);
134
- };
106
+ isDraftCompose() {
107
+ const composer = this.composer;
108
+ return composer.composeKind === 'tasks' && composer.composeTaskId === null;
109
+ }
135
110
 
136
- const editNextTask = (api) => {
137
- if (!api.nav.tasksOpen) return;
138
- const last = taskRowCount(api) - 1;
139
- const next = clampedTaskFocus(api) + 1;
140
- if (next <= last) api.nav.tasksFocus = next;
141
- editFocusedTask(api);
142
- };
111
+ canToggle() {
112
+ if (!this.isOpen()) return false;
113
+ return this.ui.mode === 'review';
114
+ }
143
115
 
144
- const moveTaskCompose = (api, delta, width, extend) => {
145
- if (!api.nav.tasksOpen) return;
146
- const editor = api.state.editor;
147
- if (editor && width) {
148
- const before = editor.cursor;
149
- editor.moveLine(delta, width, extend);
150
- if (editor.cursor !== before) return;
151
- if (extend) return;
152
- }
153
- const last = taskRowCount(api) - 1;
154
- const at = clampedTaskFocus(api);
155
- let next = at + delta;
156
- if (next < 0) next = 0;
157
- if (next > last) next = last;
158
- if (next === at) return;
159
- api.commit();
160
- api.ui.flushReview();
161
- api.close();
162
- const max = Math.max(0, taskRowCount(api) - 1);
163
- api.nav.tasksFocus = Math.min(next, max);
164
- editFocusedTask(api);
165
- };
116
+ focused() {
117
+ const row = this.rowAt(this.clampedTaskFocus());
118
+ return row && row.task ? row.task : null;
119
+ }
166
120
 
167
- const typeIntoTask = (api, key) => {
168
- const nav = api.nav;
169
- if (!nav.tasksOpen) return false;
170
- if (nav.pane !== 'diff') return false;
171
- if (api.ui.mode !== 'review') return false;
172
- if (!isTaskTypeKey(key)) return false;
173
- editFocusedTask(api);
174
- api.state.editor.insert(key);
175
- return true;
176
- };
121
+ focusedKind() {
122
+ const row = this.rowAt(this.clampedTaskFocus());
123
+ return row ? row.kind : TASK_KINDS[0].id;
124
+ }
177
125
 
178
- const removeFocusedTask = (api) => {
179
- const nav = api.nav;
180
- if (nav.pane !== 'diff' || !nav.tasksOpen) return;
181
- const todos = listTasks(api);
182
- const todo = todos[clampedTaskFocus(api)];
183
- if (!todo) return;
184
- const notes = api.review.store;
185
- removeTask(notes, todo.id);
186
- const left = listTasks(api);
187
- if (nav.tasksFocus >= left.length) {
188
- nav.tasksFocus = Math.max(0, left.length - 1);
189
- }
190
- api.ui.flushReview(true);
191
- };
126
+ closeEditor() {
127
+ this.composer.commitCompose();
128
+ this.composer.closeCompose();
129
+ }
192
130
 
193
- const toggleFocusedTask = (api) => {
194
- const nav = api.nav;
195
- if (!nav.tasksOpen || nav.pane !== 'diff') return false;
196
- if (api.ui.mode !== 'review') return false;
197
- const todo = listTasks(api)[clampedTaskFocus(api)];
198
- if (!todo) return false;
199
- setTaskDone(api.review.store, todo.id, !todo.done);
200
- api.ui.flushReview(true);
201
- return true;
202
- };
131
+ taskSections() {
132
+ if (!this.isOpen()) return [];
133
+ const editor = this.composer.editor;
134
+ const editingId = this.editingId();
135
+ const draft = this.isDraftCompose();
136
+ const focus = this.clampedTaskFocus();
137
+ let at = 0;
138
+ const sections = [];
139
+ for (const kind of TASK_KINDS) {
140
+ const tasks = this.list(kind.id);
141
+ const texts = [];
142
+ for (const task of tasks) {
143
+ const live = editingId === task.id && editor;
144
+ texts.push(checkLabel(live ? editor.text : task.text, task.done));
145
+ at += 1;
146
+ }
147
+ const drafting = draft && at === focus && editor;
148
+ texts.push(checkLabel(drafting ? editor.text : '', false));
149
+ at += 1;
150
+ sections.push({ title: kind.title, texts });
151
+ }
152
+ return sections;
153
+ }
203
154
 
204
- const onTaskHit = (api, cursor) => {
205
- if (!api.nav.tasksOpen) return;
206
- const todos = listTasks(api);
207
- const draft = cursor === todos.length;
208
- const target = todos[cursor];
209
- if (!draft && !target) return;
210
- const { state } = api;
211
- if (state.composeKind === 'tasks') {
212
- if (draft && state.composeTaskId === null) return;
213
- if (target && target.id === state.composeTaskId) return;
214
- api.commit();
215
- api.close();
216
- }
217
- api.nav.tasksFocus = cursor;
218
- editFocusedTask(api);
219
- };
155
+ taskTexts() {
156
+ const texts = [];
157
+ for (const section of this.taskSections()) texts.push(...section.texts);
158
+ return texts;
159
+ }
220
160
 
221
- const onTaskCheck = (api, cursor) => {
222
- if (!api.nav.tasksOpen) return;
223
- const todo = listTasks(api)[cursor];
224
- if (!todo) return void onTaskHit(api, cursor);
225
- const { state } = api;
226
- if (state.composeKind === 'tasks' && state.composeTaskId !== todo.id) {
227
- api.commit();
228
- api.close();
229
- }
230
- setTaskDone(api.review.store, todo.id, !todo.done);
231
- api.nav.tasksFocus = cursor;
232
- api.ui.flushReview(true);
233
- };
161
+ taskRowCount() {
162
+ if (!this.isOpen()) return 0;
163
+ return this.rows().length;
164
+ }
234
165
 
235
- const createTask = (api) => {
236
- startDraftCompose(api);
237
- };
166
+ clampedTaskFocus() {
167
+ return clamp(this.nav.tasksFocus, this.taskRowCount() - 1);
168
+ }
169
+
170
+ moveTaskFocus(delta) {
171
+ const count = this.taskRowCount();
172
+ if (!count) return;
173
+ this.nav.tasksFocus = clamp(this.clampedTaskFocus() + delta, count - 1);
174
+ }
238
175
 
239
- const onTasks = (api) => {
240
- const { nav, ui } = api;
241
- if (nav.pane === 'files') {
242
- const entry = ui.fileCursorEntry();
243
- if (entry && !isTasksEntry(entry)) {
244
- nav.index = entry.openIndex ?? entry.firstIndex;
176
+ openTasksPage() {
177
+ const nav = this.nav;
178
+ nav.tasksOpen = true;
179
+ nav.pane = 'tasks';
180
+ nav.scroll = 0;
181
+ nav.clearSelection();
182
+ this.ui.syncReviewPath();
183
+ }
184
+
185
+ closeTasksPage() {
186
+ this.nav.tasksOpen = false;
187
+ this.ui.syncReviewPath();
188
+ this.ui.syncFileCursor();
189
+ this.ui.lifecycle.catchUpWatch();
190
+ }
191
+
192
+ startDraftCompose() {
193
+ this.openTasksPage();
194
+ this.nav.tasksFocus = this.list(TASK_KINDS[0].id).length;
195
+ this.nav.scroll = 0;
196
+ this.composer.openCompose('tasks', '', null);
197
+ }
198
+
199
+ editFocusedTask() {
200
+ if (!this.isOpen()) return;
201
+ const row = this.rowAt(this.clampedTaskFocus());
202
+ if (!row || row.draft) {
203
+ this.composer.openCompose('tasks', '', null);
204
+ return;
245
205
  }
206
+ this.composer.openCompose('tasks', row.task.text, row.task.id);
246
207
  }
247
- createTask(api);
248
- };
249
208
 
250
- const taskEditView = (api) => {
251
- const { state } = api;
252
- if (state.composeKind !== 'tasks') return null;
253
- if (!state.editor) return null;
254
- return { cursor: state.editor.cursor };
255
- };
209
+ editNextTask() {
210
+ if (!this.isOpen()) return;
211
+ const next = this.clampedTaskFocus() + 1;
212
+ if (next < this.taskRowCount()) this.nav.tasksFocus = next;
213
+ this.editFocusedTask();
214
+ }
256
215
 
257
- const commitTask = (api, text) => {
258
- const notes = api.review.store;
259
- const id = api.state.composeTaskId;
260
- const todos = notes.tasks;
261
- const prev = id === null ? null : todos.find((entry) => entry.id === id);
262
- if (prev) return void setTaskText(notes, id, text);
263
- if (!text.trim()) return;
264
- const todo = addTask(notes, TASKS_FILE, text);
265
- api.state.composeTaskId = todo.id;
266
- };
216
+ moveTaskCompose(delta, width, extend) {
217
+ if (!this.isOpen()) return;
218
+ const editor = this.composer.editor;
219
+ if (editor && width) {
220
+ const before = editor.cursor;
221
+ editor.moveLine(delta, width, extend);
222
+ if (editor.cursor !== before || extend) return;
223
+ }
224
+ const rows = this.rows();
225
+ const at = this.clampedTaskFocus();
226
+ const next = clamp(at + delta, rows.length - 1);
227
+ if (next === at) return;
228
+ const dest = rows[next];
229
+ this.composer.commitCompose();
230
+ this.ui.flushReview();
231
+ this.composer.closeCompose();
232
+ this.nav.tasksFocus = this.rowIndex(dest);
233
+ this.editFocusedTask();
234
+ }
267
235
 
268
- const createTasksCompose = (api) => ({
269
- taskTexts: () => taskTexts(api),
270
- clampedTaskFocus: () => clampedTaskFocus(api),
271
- taskRowCount: () => taskRowCount(api),
272
- moveTaskFocus: (delta) => moveTaskFocus(api, delta),
273
- openTasksPage: () => openTasksPage(api),
274
- closeTasksPage: () => closeTasksPage(api),
275
- startDraftCompose: () => startDraftCompose(api),
276
- editFocusedTask: () => editFocusedTask(api),
277
- editNextTask: () => editNextTask(api),
278
- moveTaskCompose: (delta, width, extend) =>
279
- moveTaskCompose(api, delta, width, extend),
280
- typeIntoTask: (key) => typeIntoTask(api, key),
281
- removeFocusedTask: () => removeFocusedTask(api),
282
- onTaskHit: (cursor) => onTaskHit(api, cursor),
283
- onTaskCheck: (cursor) => onTaskCheck(api, cursor),
284
- toggleFocusedTask: () => toggleFocusedTask(api),
285
- onTasks: () => onTasks(api),
286
- taskEditView: () => taskEditView(api),
287
- commitTask: (text) => commitTask(api, text),
288
- });
289
-
290
- module.exports = {
291
- createTasksCompose,
292
- };
236
+ typeIntoTask(key) {
237
+ if (!this.canToggle() || !isTaskTypeKey(key)) return false;
238
+ this.editFocusedTask();
239
+ this.composer.editor.insert(key);
240
+ return true;
241
+ }
242
+
243
+ removeFocusedTask() {
244
+ const nav = this.nav;
245
+ if (!this.isOpen()) return;
246
+ const row = this.rowAt(this.clampedTaskFocus());
247
+ if (!row || !row.task) return;
248
+ const kind = row.kind;
249
+ removeTask(this.ui.review.store, row.task.id);
250
+ const rows = this.rows();
251
+ let focus = Math.min(nav.tasksFocus, rows.length - 1);
252
+ const at = rows[focus];
253
+ const prev = rows[focus - 1];
254
+ if (at && at.draft && prev && prev.task && prev.kind === kind) {
255
+ focus -= 1;
256
+ }
257
+ nav.tasksFocus = Math.max(0, focus);
258
+ this.ui.flushReview(true);
259
+ }
260
+
261
+ toggleFocusedTask() {
262
+ if (!this.canToggle()) return false;
263
+ const task = this.focused();
264
+ if (!task) return false;
265
+ setTaskDone(this.ui.review.store, task.id, !task.done);
266
+ this.ui.flushReview(true);
267
+ return true;
268
+ }
269
+
270
+ onTaskHit(cursor) {
271
+ if (!this.isOpen()) return;
272
+ const row = this.rowAt(cursor);
273
+ if (!row) return;
274
+ const composer = this.composer;
275
+ if (composer.composeKind === 'tasks') {
276
+ const same =
277
+ cursor === this.clampedTaskFocus() &&
278
+ ((row.draft && composer.composeTaskId === null) ||
279
+ (row.task && row.task.id === composer.composeTaskId));
280
+ if (same) return;
281
+ this.closeEditor();
282
+ }
283
+ this.nav.tasksFocus = cursor;
284
+ this.editFocusedTask();
285
+ }
286
+
287
+ onTaskCheck(cursor) {
288
+ if (!this.isOpen()) return;
289
+ const row = this.rowAt(cursor);
290
+ if (!row || !row.task) return void this.onTaskHit(cursor);
291
+ const composer = this.composer;
292
+ if (
293
+ composer.composeKind === 'tasks' &&
294
+ composer.composeTaskId !== row.task.id
295
+ ) {
296
+ this.closeEditor();
297
+ }
298
+ setTaskDone(this.ui.review.store, row.task.id, !row.task.done);
299
+ this.nav.tasksFocus = cursor;
300
+ this.ui.flushReview(true);
301
+ }
302
+
303
+ onTasks() {
304
+ const { nav } = this;
305
+ if (nav.pane === 'files') {
306
+ const entry = this.ui.fileCursorEntry();
307
+ if (entry && !isTasksEntry(entry)) {
308
+ nav.index = entry.openIndex ?? entry.firstIndex;
309
+ }
310
+ }
311
+ this.startDraftCompose();
312
+ }
313
+
314
+ taskEditView() {
315
+ const { composeKind, editor } = this.composer;
316
+ if (composeKind !== 'tasks' || !editor) return null;
317
+ return { cursor: editor.cursor };
318
+ }
319
+
320
+ commitTask(text) {
321
+ const notes = this.ui.review.store;
322
+ const id = this.composer.composeTaskId;
323
+ const prev =
324
+ id === null ? null : notes.tasks.find((task) => task.id === id);
325
+ if (prev) return void setTaskText(notes, id, text);
326
+ if (!text.trim()) return;
327
+ const kind = this.focusedKind();
328
+ const todo = addTask(notes, TASKS_FILE, text, false, kind);
329
+ this.composer.composeTaskId = todo.id;
330
+ }
331
+ }
332
+
333
+ module.exports = { TaskPage };
@@ -3,62 +3,54 @@
3
3
  const review = require('../review.js');
4
4
  const { rankedTemplates, prefixTemplates, TEMPLATE_SHOW } = review;
5
5
 
6
- const shownTemplates = (api) => {
7
- const notes = api.review.store;
8
- if (!notes) return [];
9
- const { state } = api;
10
- if (state.composeKind && state.composeKind !== 'feedback') return [];
11
- const ranked = rankedTemplates(notes.templates);
12
- const editing = state.composeKind && state.editor;
13
- const typed = editing ? state.editor.text : '';
14
- const exact = ranked.some((entry) => entry.text === typed);
15
- if (exact) return [];
16
- const matched = prefixTemplates(ranked, typed);
17
- return matched.slice(0, TEMPLATE_SHOW);
18
- };
19
-
20
- const clampedTemplateIndex = (api, shown) => {
21
- if (!shown.length) return -1;
22
- if (api.state.templateIndex >= shown.length) return shown.length - 1;
23
- return api.state.templateIndex;
24
- };
25
-
26
- const clearTemplatePick = (api) => {
27
- api.state.templateIndex = -1;
28
- api.state.templateFocus = false;
29
- };
30
-
31
- const focusTemplate = (api, index, apply) => {
32
- const shown = shownTemplates(api);
33
- if (!shown.length) return;
34
- let i = index;
35
- if (i < 0) i = shown.length - 1;
36
- if (i >= shown.length) i = 0;
37
- api.state.templateIndex = i;
38
- api.state.templateFocus = true;
39
- if (apply) api.state.editor.replace(shown[i].text);
40
- };
41
-
42
- const selectTemplate = (api, index) => {
43
- const shown = shownTemplates(api);
44
- if (!shown.length) return null;
45
- focusTemplate(api, index, true);
46
- return api.save();
47
- };
48
-
49
- const applyTemplate = (api) => {
50
- if (api.state.composeKind !== 'feedback') return null;
51
- const index = api.state.templateFocus ? api.state.templateIndex : 0;
52
- return selectTemplate(api, index);
53
- };
54
-
55
- const createTemplatePick = (api) => ({
56
- shownTemplates: () => shownTemplates(api),
57
- clampedTemplateIndex: (shown) => clampedTemplateIndex(api, shown),
58
- clearTemplatePick: () => clearTemplatePick(api),
59
- focusTemplate: (index, apply) => focusTemplate(api, index, apply),
60
- applyTemplate: () => applyTemplate(api),
61
- selectTemplate: (index) => selectTemplate(api, index),
62
- });
63
-
64
- module.exports = { createTemplatePick };
6
+ class TemplatePick {
7
+ constructor(composer) {
8
+ this.composer = composer;
9
+ }
10
+
11
+ shownTemplates() {
12
+ const notes = this.composer.ui.review.store;
13
+ if (!notes) return [];
14
+ const { composeKind, editor } = this.composer;
15
+ if (composeKind && composeKind !== 'feedback') return [];
16
+ const ranked = rankedTemplates(notes.templates);
17
+ const typed = composeKind && editor ? editor.text : '';
18
+ if (ranked.some((entry) => entry.text === typed)) return [];
19
+ return prefixTemplates(ranked, typed).slice(0, TEMPLATE_SHOW);
20
+ }
21
+
22
+ clampedTemplateIndex(shown) {
23
+ if (!shown.length) return -1;
24
+ return Math.min(this.composer.templateIndex, shown.length - 1);
25
+ }
26
+
27
+ clearTemplatePick() {
28
+ this.composer.templateIndex = -1;
29
+ this.composer.templateFocus = false;
30
+ }
31
+
32
+ focusTemplate(index, apply) {
33
+ const shown = this.shownTemplates();
34
+ if (!shown.length) return;
35
+ let i = index;
36
+ if (i < 0) i = shown.length - 1;
37
+ if (i >= shown.length) i = 0;
38
+ this.composer.templateIndex = i;
39
+ this.composer.templateFocus = true;
40
+ if (apply) this.composer.editor.replace(shown[i].text);
41
+ }
42
+
43
+ selectTemplate(index) {
44
+ if (!this.shownTemplates().length) return null;
45
+ this.focusTemplate(index, true);
46
+ return this.composer.saveCompose();
47
+ }
48
+
49
+ applyTemplate() {
50
+ const { composeKind, templateFocus, templateIndex } = this.composer;
51
+ if (composeKind !== 'feedback') return null;
52
+ return this.selectTemplate(templateFocus ? templateIndex : 0);
53
+ }
54
+ }
55
+
56
+ module.exports = { TemplatePick };