reslop 0.2.4 → 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 (109) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/LICENSE +1 -1
  3. package/README.md +12 -13
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +32 -19
  6. package/lib/capabilities.js +8 -12
  7. package/lib/cli.js +40 -74
  8. package/lib/clipboard.js +48 -18
  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 +196 -404
  14. package/lib/deps.js +240 -399
  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 +138 -141
  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 +90 -206
  25. package/lib/editor.js +98 -55
  26. package/lib/files.js +86 -103
  27. package/lib/git-branches.js +254 -416
  28. package/lib/git-deps.js +182 -344
  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 +157 -132
  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 +16 -26
  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 +22 -35
  44. package/lib/highlight/txt.js +9 -22
  45. package/lib/highlight.js +17 -46
  46. package/lib/keys.js +62 -85
  47. package/lib/manifest.js +67 -130
  48. package/lib/npm-commands.js +119 -189
  49. package/lib/remote.js +141 -197
  50. package/lib/render/branches.js +66 -73
  51. package/lib/render/chrome.js +264 -323
  52. package/lib/render/commits.js +187 -500
  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 -228
  58. package/lib/render/files.js +45 -99
  59. package/lib/render/frame.js +129 -183
  60. package/lib/render/notes.js +144 -160
  61. package/lib/render/npm.js +234 -112
  62. package/lib/render/present.js +4 -11
  63. package/lib/render/primitives.js +131 -170
  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 -28
  68. package/lib/report-model.js +110 -33
  69. package/lib/report-parse.js +56 -38
  70. package/lib/report-render.js +102 -59
  71. package/lib/report-run.js +87 -78
  72. package/lib/report-test.js +256 -141
  73. package/lib/report-tools.js +96 -161
  74. package/lib/review-merge.js +114 -152
  75. package/lib/review.js +115 -189
  76. package/lib/runs.js +225 -0
  77. package/lib/select.js +89 -75
  78. package/lib/session/accessors.js +14 -21
  79. package/lib/session/actions.js +82 -63
  80. package/lib/session/branches.js +193 -247
  81. package/lib/session/change-actions.js +171 -214
  82. package/lib/session/commit.js +280 -342
  83. package/lib/session/compose.js +559 -674
  84. package/lib/session/dashboard.js +385 -0
  85. package/lib/session/dispatch.js +164 -157
  86. package/lib/session/edit-click.js +57 -0
  87. package/lib/session/files-pane.js +85 -53
  88. package/lib/session/items.js +73 -104
  89. package/lib/session/lifecycle.js +403 -541
  90. package/lib/session/load.js +48 -68
  91. package/lib/session/navigation.js +96 -87
  92. package/lib/session/npm.js +409 -345
  93. package/lib/session/ops.js +196 -63
  94. package/lib/session/pointer.js +23 -15
  95. package/lib/session/progress.js +33 -21
  96. package/lib/session/review.js +33 -54
  97. package/lib/session/tasks.js +333 -0
  98. package/lib/session/templates.js +51 -59
  99. package/lib/session/terminal.js +85 -136
  100. package/lib/session/unit.js +3 -4
  101. package/lib/session/update.js +76 -97
  102. package/lib/session/view.js +52 -83
  103. package/lib/session/watch.js +208 -204
  104. package/lib/session.js +112 -237
  105. package/lib/update.js +45 -116
  106. package/lib/utilities.js +67 -89
  107. package/lib/wrap.js +33 -44
  108. package/package.json +1 -1
  109. package/lib/session/todos.js +0 -293
package/lib/editor.js CHANGED
@@ -6,6 +6,7 @@ const wrap = require('./wrap.js');
6
6
  const { wrapMove } = wrap;
7
7
 
8
8
  const UNDO_CAP = 100;
9
+ const WORD_RE = /[\p{L}\p{N}_]/u;
9
10
 
10
11
  const graphemeIndex = (text, cursor) => {
11
12
  const parts = graphemes(text);
@@ -24,10 +25,10 @@ const offsetAt = (parts, count) => {
24
25
  return pos;
25
26
  };
26
27
 
27
- const WORD_RE = /[\p{L}\p{N}_]/u;
28
-
29
28
  const isWordChar = (ch) => WORD_RE.test(ch);
30
29
 
30
+ const isSeparator = (ch) => !isWordChar(ch);
31
+
31
32
  const skipWhile = (parts, at, dir, pred) => {
32
33
  if (dir > 0) {
33
34
  while (at < parts.length && pred(parts[at])) at += 1;
@@ -38,20 +39,10 @@ const skipWhile = (parts, at, dir, pred) => {
38
39
  };
39
40
 
40
41
  const wordIndex = (parts, at, dir) => {
41
- const word = (ch) => isWordChar(ch);
42
- const sep = (ch) => !isWordChar(ch);
43
- if (dir > 0) {
44
- if (at < parts.length && word(parts[at])) {
45
- return skipWhile(parts, at, 1, word);
46
- }
47
- at = skipWhile(parts, at, 1, sep);
48
- return skipWhile(parts, at, 1, word);
49
- }
50
- if (at > 0 && word(parts[at - 1])) {
51
- return skipWhile(parts, at, -1, word);
52
- }
53
- at = skipWhile(parts, at, -1, sep);
54
- return skipWhile(parts, at, -1, word);
42
+ const ahead = dir > 0 ? parts[at] : parts[at - 1];
43
+ const inWord = ahead !== undefined && isWordChar(ahead);
44
+ const start = inWord ? at : skipWhile(parts, at, dir, isSeparator);
45
+ return skipWhile(parts, start, dir, isWordChar);
55
46
  };
56
47
 
57
48
  class Editor {
@@ -62,32 +53,70 @@ class Editor {
62
53
  this.redo = [];
63
54
  this.wantCol = null;
64
55
  this.scrollCol = 0;
56
+ this.anchor = null;
65
57
  }
66
58
 
67
59
  snapshot() {
68
- return { text: this.text, cursor: this.cursor };
60
+ return { text: this.text, cursor: this.cursor, anchor: this.anchor };
69
61
  }
70
62
 
71
63
  restore(snap) {
72
64
  this.text = snap.text;
73
65
  this.cursor = snap.cursor;
66
+ this.anchor = snap.anchor;
67
+ }
68
+
69
+ hasSelect() {
70
+ return this.anchor !== null && this.anchor !== this.cursor;
71
+ }
72
+
73
+ span() {
74
+ if (!this.hasSelect()) return { lo: this.cursor, hi: this.cursor };
75
+ const lo = Math.min(this.anchor, this.cursor);
76
+ const hi = Math.max(this.anchor, this.cursor);
77
+ return { lo, hi };
78
+ }
79
+
80
+ selectedText() {
81
+ const { lo, hi } = this.span();
82
+ return this.text.slice(lo, hi);
83
+ }
84
+
85
+ beginSelect() {
86
+ if (this.anchor === null) this.anchor = this.cursor;
87
+ }
88
+
89
+ clearSelect() {
90
+ this.anchor = null;
91
+ }
92
+
93
+ collapse(delta) {
94
+ const { lo, hi } = this.span();
95
+ this.anchor = null;
96
+ this.cursor = delta < 0 ? lo : hi;
97
+ }
98
+
99
+ removeSpan() {
100
+ if (!this.hasSelect()) return false;
101
+ const { lo, hi } = this.span();
102
+ this.edit(this.text.slice(0, lo) + this.text.slice(hi), lo);
103
+ return true;
74
104
  }
75
105
 
76
106
  edit(nextText, nextCursor) {
77
107
  this.undo.push(this.snapshot());
78
- if (this.undo.length > UNDO_CAP) {
79
- this.undo = this.undo.slice(this.undo.length - UNDO_CAP);
80
- }
108
+ if (this.undo.length > UNDO_CAP) this.undo.shift();
81
109
  this.redo = [];
82
110
  this.text = nextText;
83
111
  this.cursor = nextCursor;
112
+ this.anchor = null;
84
113
  }
85
114
 
86
115
  insert(chunk) {
87
116
  this.wantCol = null;
88
- const before = this.text.slice(0, this.cursor);
89
- const after = this.text.slice(this.cursor);
90
- this.edit(before + chunk + after, this.cursor + chunk.length);
117
+ const { lo, hi } = this.span();
118
+ const next = this.text.slice(0, lo) + chunk + this.text.slice(hi);
119
+ this.edit(next, lo + chunk.length);
91
120
  }
92
121
 
93
122
  replace(text) {
@@ -95,33 +124,46 @@ class Editor {
95
124
  this.edit(text, text.length);
96
125
  }
97
126
 
127
+ place(offset) {
128
+ this.wantCol = null;
129
+ this.anchor = null;
130
+ this.cursor = Math.max(0, Math.min(offset, this.text.length));
131
+ }
132
+
98
133
  backspace() {
99
134
  this.wantCol = null;
100
- const { parts, i } = graphemeIndex(this.text, this.cursor);
135
+ if (this.removeSpan()) return;
136
+ const { parts, i, pos } = graphemeIndex(this.text, this.cursor);
101
137
  if (i === 0) return;
102
- const drop = parts[i - 1];
103
- const nextParts = [...parts.slice(0, i - 1), ...parts.slice(i)];
104
- this.edit(nextParts.join(''), this.cursor - drop.length);
138
+ const size = parts[i - 1].length;
139
+ const next = this.text.slice(0, pos - size) + this.text.slice(pos);
140
+ this.edit(next, this.cursor - size);
105
141
  }
106
142
 
107
143
  delete() {
108
144
  this.wantCol = null;
109
- const { parts, i } = graphemeIndex(this.text, this.cursor);
145
+ if (this.removeSpan()) return;
146
+ const { parts, i, pos } = graphemeIndex(this.text, this.cursor);
110
147
  if (i >= parts.length) return;
111
- const nextParts = [...parts.slice(0, i), ...parts.slice(i + 1)];
112
- this.edit(nextParts.join(''), this.cursor);
148
+ const next =
149
+ this.text.slice(0, pos) + this.text.slice(pos + parts[i].length);
150
+ this.edit(next, this.cursor);
113
151
  }
114
152
 
115
- move(delta) {
153
+ move(delta, extend) {
116
154
  this.wantCol = null;
155
+ if (!extend && this.hasSelect()) return void this.collapse(delta);
156
+ if (extend) this.beginSelect();
117
157
  const { parts, i } = graphemeIndex(this.text, this.cursor);
118
158
  const next = i + delta;
119
159
  if (next < 0 || next > parts.length) return;
120
160
  this.cursor = offsetAt(parts, next);
121
161
  }
122
162
 
123
- moveWord(delta) {
163
+ moveWord(delta, extend) {
124
164
  this.wantCol = null;
165
+ if (!extend && this.hasSelect()) return void this.collapse(delta);
166
+ if (extend) this.beginSelect();
125
167
  const dir = delta < 0 ? -1 : 1;
126
168
  const { parts, i } = graphemeIndex(this.text, this.cursor);
127
169
  this.cursor = offsetAt(parts, wordIndex(parts, i, dir));
@@ -129,15 +171,14 @@ class Editor {
129
171
 
130
172
  linePos() {
131
173
  const before = this.text.slice(0, this.cursor);
132
- const at = before.lastIndexOf('\n');
133
174
  const line = before.split('\n').length - 1;
134
- const col = at < 0 ? before.length : before.length - at - 1;
175
+ const col = before.length - before.lastIndexOf('\n') - 1;
135
176
  return { line, col };
136
177
  }
137
178
 
138
179
  visibleLineCol() {
139
180
  const { col, line } = this.linePos();
140
- const row = this.text.split('\n')[line] ?? '';
181
+ const row = this.text.split('\n')[line];
141
182
  return visibleWidth(row.slice(0, col));
142
183
  }
143
184
 
@@ -148,55 +189,57 @@ class Editor {
148
189
  if (col >= this.scrollCol + room) this.scrollCol = col - room + 1;
149
190
  }
150
191
 
151
- moveLine(delta, width) {
192
+ moveLine(delta, width, extend) {
193
+ if (extend) this.beginSelect();
194
+ else this.clearSelect();
152
195
  if (width !== undefined) {
153
- const col = this.wantCol;
154
- const moved = wrapMove(this.text, this.cursor, width, delta, col);
196
+ const moved = wrapMove(
197
+ this.text,
198
+ this.cursor,
199
+ width,
200
+ delta,
201
+ this.wantCol,
202
+ );
155
203
  this.wantCol = moved.col;
156
204
  this.cursor = moved.cursor;
157
205
  return;
158
206
  }
159
207
  const rows = this.text.split('\n');
160
208
  const { line, col } = this.linePos();
161
- let next = line + delta;
162
- if (next < 0) next = 0;
163
- if (next >= rows.length) next = rows.length - 1;
209
+ const next = Math.min(Math.max(line + delta, 0), rows.length - 1);
164
210
  if (next === line) return;
165
- const row = rows[next];
166
- const c = Math.min(col, row.length);
167
211
  let cursor = 0;
168
212
  for (let i = 0; i < next; i++) cursor += rows[i].length + 1;
169
- this.cursor = cursor + c;
213
+ this.cursor = cursor + Math.min(col, rows[next].length);
170
214
  }
171
215
 
172
- home() {
216
+ home(extend) {
173
217
  this.wantCol = null;
174
- const { col } = this.linePos();
175
- this.cursor -= col;
218
+ if (extend) this.beginSelect();
219
+ else this.clearSelect();
220
+ this.cursor -= this.linePos().col;
176
221
  }
177
222
 
178
- end() {
223
+ end(extend) {
179
224
  this.wantCol = null;
225
+ if (extend) this.beginSelect();
226
+ else this.clearSelect();
180
227
  const { line, col } = this.linePos();
181
- const rows = this.text.split('\n');
182
- const row = rows[line] ?? '';
183
- this.cursor += row.length - col;
228
+ this.cursor += this.text.split('\n')[line].length - col;
184
229
  }
185
230
 
186
231
  undoEdit() {
187
232
  if (!this.undo.length) return;
188
233
  this.wantCol = null;
189
- const prev = this.undo.pop();
190
234
  this.redo.push(this.snapshot());
191
- this.restore(prev);
235
+ this.restore(this.undo.pop());
192
236
  }
193
237
 
194
238
  redoEdit() {
195
239
  if (!this.redo.length) return;
196
240
  this.wantCol = null;
197
- const next = this.redo.pop();
198
241
  this.undo.push(this.snapshot());
199
- this.restore(next);
242
+ this.restore(this.redo.pop());
200
243
  }
201
244
  }
202
245
 
package/lib/files.js CHANGED
@@ -5,9 +5,40 @@ const { listFilePath } = manifest;
5
5
 
6
6
  const REVIEW_DIR = '.review';
7
7
  const ORIGIN_ORDER = ['staged', 'unstaged', 'untracked', 'commit', 'pr'];
8
- const TODO_FILE = 'TODOs';
9
- const REPO_TODOS_LABEL = 'Project Backlog and Issues';
8
+ const TASKS_FILE = 'TODOs';
9
+ const REPO_TASKS_LABEL =
10
+ 'Project Tasks: Backlog, Issues, Bug Reports, Feature Requests';
11
+ const TASK_KINDS = [
12
+ { id: 'backlog', title: 'Backlog' },
13
+ { id: 'issues', title: 'Issues' },
14
+ { id: 'bugs', title: 'Bug Reports' },
15
+ { id: 'features', title: 'Feature Requests' },
16
+ ];
17
+ const TASK_KIND_IDS = new Set(TASK_KINDS.map((kind) => kind.id));
18
+
19
+ const taskKind = (todo) => {
20
+ const kind = todo && todo.kind;
21
+ return TASK_KIND_IDS.has(kind) ? kind : 'backlog';
22
+ };
23
+
24
+ const taskTitle = (id) => {
25
+ for (const kind of TASK_KINDS) {
26
+ if (kind.id === id) return kind.title;
27
+ }
28
+ return TASK_KINDS[0].title;
29
+ };
10
30
  const TOTAL_LABEL = 'total';
31
+ const AGE_UNIT = {
32
+ second: 's',
33
+ minute: 'm',
34
+ hour: 'h',
35
+ day: 'd',
36
+ week: 'w',
37
+ month: 'mo',
38
+ year: 'y',
39
+ };
40
+ const AGE_AGO = /^(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago$/i;
41
+ const AGE_SHORT = /^(\d+)(mo|[smhdwy])(?: ago)?$/i;
11
42
 
12
43
  const isPathInScope = (rel, paths) => {
13
44
  if (!paths.length) return true;
@@ -19,26 +50,23 @@ const isPathInScope = (rel, paths) => {
19
50
  return false;
20
51
  };
21
52
 
22
- const itemPath = (item) => {
23
- if (!item || !item.file) return '';
24
- return item.file.newPath || item.file.oldPath || '';
25
- };
53
+ const itemPath = (item) => item?.file?.newPath || item?.file?.oldPath || '';
26
54
 
27
55
  const listPath = (item) => listFilePath(itemPath(item));
28
56
 
29
- const isTodoItem = (item) => !!(item && item.origin === 'todo');
57
+ const isTaskItem = (item) => item?.origin === 'task';
30
58
 
31
59
  const isReadOnlyOrigin = (origin) => origin === 'commit' || origin === 'pr';
32
60
 
33
- const isTodosEntry = (entry) => !!(entry && entry.kind === 'todos');
61
+ const isTasksEntry = (entry) => entry?.kind === 'tasks';
34
62
 
35
- const isTotalEntry = (entry) => !!(entry && entry.kind === 'total');
63
+ const isTotalEntry = (entry) => entry?.kind === 'total';
36
64
 
37
- const makeTodoItem = () => ({
38
- origin: 'todo',
65
+ const makeTaskItem = () => ({
66
+ origin: 'task',
39
67
  file: {
40
- oldPath: TODO_FILE,
41
- newPath: TODO_FILE,
68
+ oldPath: TASKS_FILE,
69
+ newPath: TASKS_FILE,
42
70
  isNew: false,
43
71
  isDeleted: false,
44
72
  isBinary: false,
@@ -46,58 +74,36 @@ const makeTodoItem = () => ({
46
74
  hunks: [],
47
75
  },
48
76
  hunk: null,
49
- blockId: 'todo',
77
+ blockId: 'task',
50
78
  patchAdd: '',
51
79
  patchRevert: '',
52
80
  });
53
81
 
54
82
  const fileStatus = (origins) => {
55
- const present = new Set(origins);
56
- const seen = ORIGIN_ORDER.filter((origin) => present.has(origin));
83
+ const seen = ORIGIN_ORDER.filter((origin) => origins.includes(origin));
57
84
  const key = seen.join('+');
58
- if (!key) return '';
59
- if (key === 'staged+unstaged') return 'partial';
60
- return key;
85
+ return key === 'staged+unstaged' ? 'partial' : key;
61
86
  };
62
87
 
63
88
  const lineDelta = ({ hunk, blockId }) => {
64
89
  const delta = { added: 0, removed: 0 };
65
- if (!hunk) return delta;
66
- const { lines } = hunk;
67
- if (!lines) return delta;
90
+ if (!hunk || !hunk.lines) return delta;
68
91
  const scoped = typeof blockId === 'number';
69
- for (const { type, blockId: lineBlock } of lines) {
70
- if (scoped && lineBlock !== blockId) continue;
71
- if (type === 'add') delta.added += 1;
72
- else if (type === 'del') delta.removed += 1;
92
+ for (const line of hunk.lines) {
93
+ if (scoped && line.blockId !== blockId) continue;
94
+ if (line.type === 'add') delta.added += 1;
95
+ else if (line.type === 'del') delta.removed += 1;
73
96
  }
74
97
  return delta;
75
98
  };
76
99
 
77
- const AGE_UNIT = {
78
- second: 's',
79
- minute: 'm',
80
- hour: 'h',
81
- day: 'd',
82
- week: 'w',
83
- month: 'mo',
84
- year: 'y',
85
- };
86
-
87
- const AGE_AGO = /^(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago$/i;
88
- const AGE_SHORT = /^(\d+)(mo|[smhdwy])(?: ago)?$/i;
89
-
90
100
  const shortUnit = (n, unit) => `${n}${AGE_UNIT[unit]} ago`;
91
101
 
92
102
  const shortAge = (text) => {
93
103
  const raw = `${text ?? ''}`.trim();
94
- if (!raw) return '';
95
104
  if (/^yesterday$/i.test(raw)) return '1d ago';
96
105
  const match = AGE_AGO.exec(raw);
97
- if (match) {
98
- const unit = AGE_UNIT[match[2].toLowerCase()];
99
- if (unit) return `${match[1]}${unit} ago`;
100
- }
106
+ if (match) return shortUnit(match[1], match[2].toLowerCase());
101
107
  const brief = AGE_SHORT.exec(raw);
102
108
  if (brief) return `${brief[1]}${brief[2].toLowerCase()} ago`;
103
109
  return raw;
@@ -117,6 +123,18 @@ const relativeAge = (thenMs, nowMs = Date.now()) => {
117
123
  return shortUnit(Math.round(days / 365), 'year');
118
124
  };
119
125
 
126
+ const emptyCounts = () => ({
127
+ remaining: 0,
128
+ added: 0,
129
+ removed: 0,
130
+ stagedAdded: 0,
131
+ stagedRemoved: 0,
132
+ unstagedAdded: 0,
133
+ unstagedRemoved: 0,
134
+ staged: 0,
135
+ unstaged: 0,
136
+ });
137
+
120
138
  const addLineDelta = (group, origin, delta) => {
121
139
  group.added += delta.added;
122
140
  group.removed += delta.removed;
@@ -140,19 +158,12 @@ const originLineDelta = (entry) => {
140
158
  }
141
159
  const added = entry.added ?? 0;
142
160
  const removed = entry.removed ?? 0;
143
- if ((entry.status ?? '') === 'staged') {
144
- return {
145
- stagedAdded: added,
146
- stagedRemoved: removed,
147
- unstagedAdded: 0,
148
- unstagedRemoved: 0,
149
- };
150
- }
161
+ const staged = entry.status === 'staged';
151
162
  return {
152
- stagedAdded: 0,
153
- stagedRemoved: 0,
154
- unstagedAdded: added,
155
- unstagedRemoved: removed,
163
+ stagedAdded: staged ? added : 0,
164
+ stagedRemoved: staged ? removed : 0,
165
+ unstagedAdded: staged ? 0 : added,
166
+ unstagedRemoved: staged ? 0 : removed,
156
167
  };
157
168
  };
158
169
 
@@ -160,26 +171,12 @@ const fileEntries = (items) => {
160
171
  const groups = new Map();
161
172
  for (let i = 0; i < items.length; i++) {
162
173
  const item = items[i];
163
- if (isTodoItem(item)) continue;
174
+ if (isTaskItem(item)) continue;
164
175
  const rel = listPath(item);
165
176
  if (!rel) continue;
166
177
  if (!groups.has(rel)) {
167
- groups.set(rel, {
168
- path: rel,
169
- origins: [],
170
- firstIndex: i,
171
- openIndex: i,
172
- remaining: 0,
173
- added: 0,
174
- removed: 0,
175
- stagedAdded: 0,
176
- stagedRemoved: 0,
177
- unstagedAdded: 0,
178
- unstagedRemoved: 0,
179
- staged: 0,
180
- unstaged: 0,
181
- date: '',
182
- });
178
+ const fields = { origins: [], firstIndex: i, openIndex: i };
179
+ groups.set(rel, { path: rel, ...fields, ...emptyCounts(), date: '' });
183
180
  }
184
181
  const group = groups.get(rel);
185
182
  const { origin } = item;
@@ -194,64 +191,50 @@ const fileEntries = (items) => {
194
191
  }
195
192
  }
196
193
  const entries = [...groups.values()];
197
- for (const group of entries) {
198
- group.status = fileStatus(group.origins);
199
- }
194
+ for (const group of entries) group.status = fileStatus(group.origins);
200
195
  entries.sort((left, right) => left.path.localeCompare(right.path, 'en'));
201
196
  return entries;
202
197
  };
203
198
 
204
199
  const fileTotals = (files) => {
205
- const total = {
206
- path: TOTAL_LABEL,
207
- kind: 'total',
208
- status: '',
209
- remaining: 0,
210
- added: 0,
211
- removed: 0,
212
- stagedAdded: 0,
213
- stagedRemoved: 0,
214
- unstagedAdded: 0,
215
- unstagedRemoved: 0,
216
- staged: 0,
217
- unstaged: 0,
218
- };
200
+ const total = { path: TOTAL_LABEL, kind: 'total', status: '' };
201
+ Object.assign(total, emptyCounts());
219
202
  for (const entry of files) {
220
- if (isTodosEntry(entry) || isTotalEntry(entry)) continue;
203
+ if (isTasksEntry(entry) || isTotalEntry(entry)) continue;
221
204
  const remaining = entry.remaining ?? 0;
222
205
  const staged = entry.staged ?? 0;
223
- const added = entry.added ?? 0;
224
- const removed = entry.removed ?? 0;
225
206
  const lines = originLineDelta(entry);
226
207
  const listed = entry.unstaged ?? Math.max(0, remaining - staged);
227
- const untracked = (entry.status ?? '') === 'untracked';
228
- const unstaged = untracked && listed === 0 ? remaining : listed;
208
+ const untracked = entry.status === 'untracked';
229
209
  total.remaining += remaining;
230
- total.added += added;
231
- total.removed += removed;
210
+ total.added += entry.added ?? 0;
211
+ total.removed += entry.removed ?? 0;
232
212
  total.stagedAdded += lines.stagedAdded;
233
213
  total.stagedRemoved += lines.stagedRemoved;
234
214
  total.unstagedAdded += lines.unstagedAdded;
235
215
  total.unstagedRemoved += lines.unstagedRemoved;
236
216
  total.staged += staged;
237
- total.unstaged += unstaged;
217
+ total.unstaged += untracked && listed === 0 ? remaining : listed;
238
218
  }
239
219
  return total;
240
220
  };
241
221
 
242
222
  module.exports = {
243
223
  REVIEW_DIR,
244
- TODO_FILE,
245
- REPO_TODOS_LABEL,
224
+ TASKS_FILE,
225
+ REPO_TASKS_LABEL,
226
+ TASK_KINDS,
227
+ taskKind,
228
+ taskTitle,
246
229
  TOTAL_LABEL,
247
230
  isPathInScope,
248
231
  itemPath,
249
232
  listPath,
250
- isTodoItem,
233
+ isTaskItem,
251
234
  isReadOnlyOrigin,
252
- isTodosEntry,
235
+ isTasksEntry,
253
236
  isTotalEntry,
254
- makeTodoItem,
237
+ makeTaskItem,
255
238
  fileStatus,
256
239
  fileEntries,
257
240
  fileTotals,