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
@@ -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
36
  if (!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 : 'backlog';
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('backlog').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 };