reslop 0.1.9 → 0.2.1

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 (96) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +3 -1
  3. package/bin/reslop.js +2 -2
  4. package/lib/ansi.js +148 -5
  5. package/lib/api.js +4 -2
  6. package/lib/capabilities.js +13 -14
  7. package/lib/cli.js +38 -49
  8. package/lib/clipboard.js +1 -1
  9. package/lib/{deps/changes.js → deps-diff.js} +309 -19
  10. package/lib/deps.js +498 -29
  11. package/lib/detect.js +1 -7
  12. package/lib/diff/align.js +7 -10
  13. package/lib/diff/diff.js +53 -0
  14. package/lib/diff/display.js +5 -4
  15. package/lib/diff/inline.js +26 -21
  16. package/lib/diff/items.js +6 -4
  17. package/lib/diff/patch.js +1 -3
  18. package/lib/diff/unit.js +392 -0
  19. package/lib/files.js +5 -8
  20. package/lib/{git/branches.js → git-branches.js} +191 -11
  21. package/lib/{git/dependencies.js → git-deps.js} +111 -38
  22. package/lib/git-reword-todo.js +20 -0
  23. package/lib/git.js +606 -68
  24. package/lib/github.js +28 -197
  25. package/lib/gitlab.js +24 -195
  26. package/lib/highlight/bash.js +1 -3
  27. package/lib/highlight/css.js +1 -3
  28. package/lib/highlight/js.js +11 -14
  29. package/lib/highlight.js +6 -9
  30. package/lib/keys.js +14 -8
  31. package/lib/manifest.js +330 -0
  32. package/lib/remote.js +374 -0
  33. package/lib/render/branches.js +18 -100
  34. package/lib/render/chrome.js +71 -14
  35. package/lib/render/commits.js +69 -60
  36. package/lib/render/diff.js +20 -20
  37. package/lib/render/files.js +6 -34
  38. package/lib/render/frame.js +36 -13
  39. package/lib/render/notes.js +26 -8
  40. package/lib/render/primitives.js +99 -11
  41. package/lib/{render.js → render/render.js} +8 -8
  42. package/lib/render/unit.js +41 -0
  43. package/lib/review-merge.js +270 -0
  44. package/lib/review.js +782 -14
  45. package/lib/select.js +5 -10
  46. package/lib/session/accessors.js +9 -1
  47. package/lib/session/actions.js +61 -18
  48. package/lib/session/branches.js +116 -128
  49. package/lib/session/change-actions.js +202 -123
  50. package/lib/session/commit.js +201 -115
  51. package/lib/session/compose.js +181 -15
  52. package/lib/session/dispatch.js +110 -60
  53. package/lib/session/files-pane.js +156 -148
  54. package/lib/session/items.js +22 -30
  55. package/lib/session/lifecycle.js +151 -54
  56. package/lib/session/load.js +19 -33
  57. package/lib/session/navigation.js +12 -1
  58. package/lib/session/ops.js +28 -41
  59. package/lib/session/pointer.js +57 -57
  60. package/lib/session/review.js +23 -37
  61. package/lib/session/terminal.js +6 -12
  62. package/lib/session/todos.js +37 -13
  63. package/lib/session/unit.js +208 -0
  64. package/lib/session/update.js +33 -46
  65. package/lib/session/view.js +78 -9
  66. package/lib/session/watch.js +128 -24
  67. package/lib/session.js +22 -246
  68. package/lib/source.js +3 -3
  69. package/lib/update.js +40 -75
  70. package/lib/{git/proc.js → utilities.js} +75 -11
  71. package/lib/wrap.js +1 -3
  72. package/package.json +2 -2
  73. package/lib/deps/items.js +0 -134
  74. package/lib/deps/manifest.js +0 -152
  75. package/lib/deps/merge.js +0 -120
  76. package/lib/deps/proposals.js +0 -164
  77. package/lib/deps/reports.js +0 -180
  78. package/lib/deps/rows.js +0 -176
  79. package/lib/deps/scan.js +0 -138
  80. package/lib/deps/usage.js +0 -68
  81. package/lib/deps/versions.js +0 -119
  82. package/lib/diff.js +0 -33
  83. package/lib/git/changes.js +0 -164
  84. package/lib/git/read.js +0 -253
  85. package/lib/git/run.js +0 -100
  86. package/lib/npm/commands.js +0 -100
  87. package/lib/remote/changes.js +0 -37
  88. package/lib/remote/headers.js +0 -36
  89. package/lib/remote/http.js +0 -122
  90. package/lib/remote/notes.js +0 -94
  91. package/lib/review/format.js +0 -434
  92. package/lib/review/storage.js +0 -150
  93. package/lib/review/store.js +0 -178
  94. package/lib/review/templates.js +0 -49
  95. package/lib/session/origins.js +0 -7
  96. package/lib/sys.js +0 -84
@@ -9,7 +9,7 @@ const { itemPath, TODO_FILE, isTodosEntry, fileTotals } = files;
9
9
  const { ACTION_IDS, BRANCH_ACTIONS, COMMIT_ACTIONS, actionLetter } = keys;
10
10
  const { buttonWord } = keys;
11
11
  const { FILES_HIDDEN, FILES_GIT_DISABLED, BRANCHES_DISABLED } = keys;
12
- const { COMMITS_DISABLED, TODO_DISABLED, DIFF_DISABLED } = keys;
12
+ const { COMMITS_DISABLED, TODO_DISABLED, DIFF_DISABLED, UNIT_DISABLED } = keys;
13
13
  const { diffGitDisabled, branchGitDisabled, commitGitDisabled } = keys;
14
14
  const { CONFIRM, confirmPrefix, promptFromChoices } = keys;
15
15
  const { THEME, paint, visibleWidth } = ansi;
@@ -39,6 +39,7 @@ const BUSY_PROGRESS = [
39
39
  'rebasing',
40
40
  'dropping',
41
41
  'committing',
42
+ 'rewording',
42
43
  'force pushing',
43
44
  ];
44
45
 
@@ -62,11 +63,16 @@ const INFO_STATUS = [
62
63
  'unified',
63
64
  'mixed',
64
65
  'side-by-side',
66
+ 'dark',
67
+ 'light',
65
68
  'saved',
66
69
  'reloaded',
67
70
  'not a diff block',
71
+ 'file scope',
72
+ 'diff mode',
68
73
  'committed',
69
74
  'amended',
75
+ 'reworded',
70
76
  'fixup',
71
77
  'updated',
72
78
  'updating reslop',
@@ -89,6 +95,7 @@ const INFO_STATUS = [
89
95
  'checked out',
90
96
  'created',
91
97
  'empty branch name',
98
+ 'empty commit message',
92
99
  ];
93
100
  const INFO_PREFIX = ['checked out ', 'created ', 'rebased onto ', 'dropped '];
94
101
 
@@ -113,8 +120,6 @@ const originLabel = (origin, view) => {
113
120
  return origin ?? 'unknown';
114
121
  };
115
122
 
116
- const headerRoleFg = (role) => HEADER_ROLE_FG[role] ?? THEME.headerChromeFg;
117
-
118
123
  const pathSegments = (rel) => {
119
124
  const segs = [];
120
125
  if (!rel) return segs;
@@ -162,7 +167,22 @@ const headerTarget = (view) => {
162
167
  return { path: entry.name, origin, n, total };
163
168
  }
164
169
  if (pane === 'commits') {
170
+ const compose = view.compose;
165
171
  const commits = view.commits ?? [];
172
+ if (compose && compose.kind === 'commit') {
173
+ const path = `${compose.text || ''}`.replaceAll('\n', ' ');
174
+ if (compose.commitKind === 'amend' || compose.commitKind === 'reword') {
175
+ const cursor = view.commitCursor ?? 0;
176
+ const entry = commits[cursor];
177
+ const total = commits.length;
178
+ const n = total ? cursor + 1 : 0;
179
+ if (!entry) return { path, origin: 'commit', n, total };
180
+ const origin = entry.head ? 'HEAD' : 'commit';
181
+ return { path, origin, n, total };
182
+ }
183
+ const total = commits.length + 1;
184
+ return { path, origin: 'new', n: 1, total };
185
+ }
166
186
  const cursor = view.commitCursor ?? 0;
167
187
  const entry = commits[cursor];
168
188
  const total = commits.length;
@@ -171,6 +191,14 @@ const headerTarget = (view) => {
171
191
  const origin = entry.head ? 'HEAD' : 'commit';
172
192
  return { path: entry.subject ?? '', origin, n, total };
173
193
  }
194
+ if (pane === 'unit') {
195
+ const path = view.reviewPath ?? '';
196
+ const lines = view.unitLines ?? [];
197
+ const top = Math.max(0, (view.scroll ?? 0) - 1);
198
+ const n = lines.length ? Math.min(lines.length, top + 1) : 0;
199
+ const origin = view.item ? originLabel(view.item.origin, view) : 'unit';
200
+ return { path, origin, n, total: lines.length };
201
+ }
174
202
  if (!view.item) return { path: '', origin: '', n: 0, total: view.total ?? 0 };
175
203
  if (view.item.origin === 'todo') {
176
204
  return { path: TODO_FILE, origin: 'todo', n: 1, total: 1 };
@@ -183,10 +211,29 @@ const headerTarget = (view) => {
183
211
  };
184
212
  };
185
213
 
214
+ const headerScreen = (view) => {
215
+ const pane = view.pane ?? 'diff';
216
+ if (pane === 'branches') return 'branches';
217
+ if (pane === 'commits') return 'commits';
218
+ if (pane === 'files') {
219
+ const files = view.files ?? [];
220
+ const entry = files[view.fileCursor ?? 0];
221
+ if (isTodosEntry(entry)) return TODO_FILE;
222
+ return '';
223
+ }
224
+ if (view.item && view.item.origin === 'todo') return TODO_FILE;
225
+ return '';
226
+ };
227
+
228
+ const headerLabel = (view, target) => headerScreen(view) || target.path;
229
+
230
+ const headerSep = (view) => (headerScreen(view) ? ': ' : '/');
231
+
186
232
  const headerPrefix = (view) => {
187
233
  const repo = view.repoName || '';
188
- const { path } = headerTarget(view);
189
- if (repo && path) return ` ${LOGO} reslop: ${repo}/`;
234
+ const target = headerTarget(view);
235
+ const label = headerLabel(view, target);
236
+ if (repo && label) return ` ${LOGO} reslop: ${repo}${headerSep(view)}`;
190
237
  if (repo) return ` ${LOGO} reslop: ${repo} `;
191
238
  return ` ${LOGO} reslop: `;
192
239
  };
@@ -197,13 +244,14 @@ const headerSuffix = ({ origin, n, total }) =>
197
244
  const headerText = (view) => {
198
245
  const prefix = headerPrefix(view);
199
246
  const target = headerTarget(view);
200
- return `${prefix}${target.path}${headerSuffix(target)}`;
247
+ const label = headerLabel(view, target);
248
+ return `${prefix}${label}${headerSuffix(target)}`;
201
249
  };
202
250
 
203
251
  const headerPieces = (view, width) => {
204
252
  const repo = view.repoName || '';
205
253
  const target = headerTarget(view);
206
- const path = target.path;
254
+ const path = headerLabel(view, target);
207
255
  const suffix = headerSuffix(target);
208
256
  const right = ' ';
209
257
  const chrome = ` ${LOGO} reslop: `;
@@ -216,7 +264,9 @@ const headerPieces = (view, width) => {
216
264
  if (repo) {
217
265
  pieces.push({ text: repo, role: 'repo' });
218
266
  if (path) {
219
- pieces.push({ text: '/', role: 'slash' });
267
+ const sep = headerSep(view);
268
+ const role = sep === '/' ? 'slash' : 'chrome';
269
+ pieces.push({ text: sep, role });
220
270
  } else {
221
271
  pieces.push({ text: ' ', role: 'chrome' });
222
272
  }
@@ -243,7 +293,8 @@ const paintHeader = (view, width, color) => {
243
293
  if (!piece.text) {
244
294
  continue;
245
295
  }
246
- out += `${fg(headerRoleFg(piece.role))}${piece.text}`;
296
+ const roleFg = HEADER_ROLE_FG[piece.role] ?? THEME.headerChromeFg;
297
+ out += `${fg(roleFg)}${piece.text}`;
247
298
  }
248
299
  return `${out}${RESET}`;
249
300
  };
@@ -428,12 +479,14 @@ const statusLayout = (view, status, width) => {
428
479
  };
429
480
 
430
481
  const choiceHits = (choices, prefix, y) => {
431
- const hits = [];
482
+ const { length } = choices;
483
+ const hits = new Array(length);
432
484
  let x = visibleWidth(prefix);
433
- for (const choice of choices) {
485
+ for (let i = 0; i < length; i++) {
486
+ const choice = choices[i];
434
487
  const piece = `${choice.lead}${choice.label}`;
435
488
  const w = visibleWidth(piece);
436
- hits.push({ id: choice.letter, y, x0: x, x1: x + w });
489
+ hits[i] = { id: choice.letter, y, x0: x, x1: x + w };
437
490
  x += w;
438
491
  }
439
492
  return hits;
@@ -543,6 +596,7 @@ const selectedCommit = (view) => (view.commits ?? [])[view.commitCursor ?? 0];
543
596
  const hiddenActions = (view) => {
544
597
  const pane = view.pane ?? 'diff';
545
598
  if (pane === 'files') return FILES_HIDDEN;
599
+ if (pane === 'unit') return UNIT_DISABLED;
546
600
  if (pane === 'branches') return BRANCHES_DISABLED;
547
601
  if (pane === 'commits') return COMMITS_DISABLED;
548
602
  if (view.item && view.item.origin === 'todo') return TODO_DISABLED;
@@ -557,7 +611,7 @@ const dimFooterActions = (view) => {
557
611
  if (!entry || entry.kind !== 'todos') return [];
558
612
  return FILES_GIT_DISABLED;
559
613
  }
560
- if (pane === 'diff') return diffGitDisabled(view.item);
614
+ if (pane === 'diff' || pane === 'unit') return diffGitDisabled(view.item);
561
615
  if (pane === 'branches') return branchGitDisabled(selectedBranch(view));
562
616
  if (pane === 'commits') {
563
617
  const entry = selectedCommit(view);
@@ -570,7 +624,10 @@ const dimFooterActions = (view) => {
570
624
 
571
625
  const extraFooterActions = (view) => {
572
626
  const pane = view.pane ?? 'diff';
573
- if (pane === 'files') return ['commit', 'pull', 'push'];
627
+ if (pane === 'files') {
628
+ const scope = view.fileScope === 'file' ? 'diff' : 'file';
629
+ return [scope, 'commit', 'pull', 'push'];
630
+ }
574
631
  if (pane === 'branches') return Object.keys(BRANCH_ACTIONS);
575
632
  if (pane === 'commits') return Object.keys(COMMIT_ACTIONS);
576
633
  return [];
@@ -2,11 +2,10 @@
2
2
 
3
3
  const ansi = require('../ansi.js');
4
4
  const primitives = require('./primitives.js');
5
+ const { measureColumns, padColumns, joinColumns, paintColumns } = primitives;
5
6
 
6
7
  const { THEME, paint, visibleWidth, truncateVisible, seq, EL } = ansi;
7
- const { fill, padVisible, FILE_MARK, paneResult, paintCursorList } = primitives;
8
-
9
- const COMMIT_COL_KEYS = ['sha', 'author', 'date'];
8
+ const { fill, FILE_MARK, paneResult, paintCursorList } = primitives;
10
9
 
11
10
  const COMMIT_COL_ALIGN = {
12
11
  sha: 'end',
@@ -22,71 +21,19 @@ const COMMIT_COL_FG = {
22
21
 
23
22
  const COMMIT_EDGE = 2;
24
23
  const COMMIT_TAIL = 1;
25
- const COMMIT_GAP = ' ';
26
- const COMMIT_DATE_GAP = ' ';
27
-
28
- const colGap = (key) => (key === 'date' ? COMMIT_DATE_GAP : COMMIT_GAP);
29
-
30
24
  const commitRowFields = (entry) => ({
31
25
  sha: entry.shortSha ?? '',
32
26
  author: entry.author ?? '',
33
27
  date: entry.date ?? '',
34
28
  });
35
29
 
36
- const commitColumns = (commits) => {
37
- const widths = {};
38
- for (const key of COMMIT_COL_KEYS) widths[key] = 0;
39
- for (const entry of commits) {
40
- const fields = commitRowFields(entry);
41
- for (const key of COMMIT_COL_KEYS) {
42
- const w = visibleWidth(fields[key]);
43
- if (w > widths[key]) widths[key] = w;
44
- }
45
- }
46
- return widths;
47
- };
48
-
49
- const commitRowParts = (entry, cols) => {
50
- const fields = commitRowFields(entry);
51
- const parts = {};
52
- for (const key of COMMIT_COL_KEYS) {
53
- const width = cols?.[key] ?? visibleWidth(fields[key]);
54
- parts[key] = padVisible(fields[key], width, COMMIT_COL_ALIGN[key]);
55
- }
56
- return parts;
57
- };
58
-
59
- const joinCommitSuffix = (parts, cols) => {
60
- const bits = [];
61
- for (const key of COMMIT_COL_KEYS) {
62
- if (!cols[key]) continue;
63
- bits.push(colGap(key) + parts[key]);
64
- }
65
- if (!bits.length) return '';
66
- return bits.join('');
67
- };
68
-
69
- const commitColFg = (key) => COMMIT_COL_FG[key];
70
-
71
- const paintCommitSuffix = (parts, cols, fgRgb, bgRgb, color) => {
72
- const bits = [];
73
- for (const key of COMMIT_COL_KEYS) {
74
- if (!cols[key]) continue;
75
- const tone = commitColFg(key) ?? fgRgb;
76
- const gap = paint(colGap(key), fgRgb, bgRgb, color);
77
- bits.push(gap + paint(parts[key], tone, bgRgb, color));
78
- }
79
- if (!bits.length) return '';
80
- return bits.join('');
81
- };
82
-
83
30
  const paintCommitRow = (entry, width, color, selected, cols) => {
84
31
  const inner = Math.max(1, width - COMMIT_EDGE - COMMIT_TAIL);
85
32
  const mark = selected ? FILE_MARK : ' ';
86
33
  const prefix = ` ${mark} `;
87
34
  const lead = ' ';
88
- const parts = commitRowParts(entry, cols);
89
- const suffix = joinCommitSuffix(parts, cols);
35
+ const parts = padColumns(commitRowFields(entry), cols, COMMIT_COL_ALIGN);
36
+ const suffix = joinColumns(parts, cols);
90
37
  const rest = Math.max(1, inner - visibleWidth(prefix + lead + suffix));
91
38
  const subject = truncateVisible(entry.subject ?? '', rest);
92
39
  const pad = Math.max(0, rest - visibleWidth(subject));
@@ -103,15 +50,77 @@ const paintCommitRow = (entry, width, color, selected, cols) => {
103
50
  out += paint(lead, nameFg, bgRgb, color);
104
51
  out += paint(subject, nameFg, bgRgb, color);
105
52
  out += paint(after, fgRgb, bgRgb, color);
106
- out += paintCommitSuffix(parts, cols, fgRgb, bgRgb, color);
53
+ out += paintColumns(parts, cols, fgRgb, bgRgb, color, COMMIT_COL_FG);
107
54
  out += paint(tail, fgRgb, bgRgb, color);
108
55
  return out + edge;
109
56
  };
110
57
 
58
+ const paintCommitEdit = (compose, width, color, entry, cols) => {
59
+ const text = `${compose.text ?? ''}`.replaceAll('\n', ' ');
60
+ if (entry && cols) {
61
+ const row = paintCommitRow(
62
+ { ...entry, subject: text },
63
+ width,
64
+ color,
65
+ true,
66
+ cols,
67
+ );
68
+ const inner = Math.max(1, width - COMMIT_EDGE - COMMIT_TAIL);
69
+ const prefix = ` ${FILE_MARK} `;
70
+ const lead = ' ';
71
+ const parts = padColumns(commitRowFields(entry), cols, COMMIT_COL_ALIGN);
72
+ const suffix = joinColumns(parts, cols);
73
+ const rest = Math.max(1, inner - visibleWidth(prefix + lead + suffix));
74
+ const clipped = truncateVisible(text, rest);
75
+ const prefixW = visibleWidth(prefix + lead);
76
+ const col = Math.min(compose.cursor ?? 0, visibleWidth(clipped));
77
+ return {
78
+ row,
79
+ cursor: { x: prefixW + col + 1 },
80
+ at: 'replace',
81
+ };
82
+ }
83
+ const inner = Math.max(1, width - COMMIT_EDGE);
84
+ const prefix = ` ${FILE_MARK} `;
85
+ const lead = ' ';
86
+ const prefixW = visibleWidth(prefix + lead);
87
+ const rest = Math.max(1, inner - prefixW);
88
+ const clipped = truncateVisible(text, rest);
89
+ const pad = Math.max(0, rest - visibleWidth(clipped));
90
+ const gap = ' '.repeat(pad);
91
+ const fgRgb = THEME.chromeFg;
92
+ const bgRgb = THEME.buttonBg;
93
+ const nameFg = THEME.buttonHotFg;
94
+ const edge = fill(COMMIT_EDGE, fgRgb, THEME.ctxBg, color);
95
+ let row = `${prefix}${lead}${clipped}${gap}${edge}`;
96
+ if (color) {
97
+ row = `${seq(fgRgb, bgRgb)}${EL}`;
98
+ row += paint(prefix, fgRgb, bgRgb, color);
99
+ row += paint(lead, nameFg, bgRgb, color);
100
+ row += paint(clipped, nameFg, bgRgb, color);
101
+ row += paint(gap, fgRgb, bgRgb, color);
102
+ row += edge;
103
+ }
104
+ const col = Math.min(compose.cursor ?? 0, visibleWidth(clipped));
105
+ return { row, cursor: { x: prefixW + col + 1 }, at: 'start' };
106
+ };
107
+
108
+ const commitEditRow = (compose, commits, cursor, width, color, cols) => {
109
+ if (!compose || compose.kind !== 'commit') return null;
110
+ if (compose.commitKind === 'amend' || compose.commitKind === 'reword') {
111
+ const entry = commits[cursor];
112
+ if (!entry) return null;
113
+ return paintCommitEdit(compose, width, color, entry, cols);
114
+ }
115
+ return paintCommitEdit(compose, width, color);
116
+ };
117
+
111
118
  const paintBodyCommits = (view, width, color, bodyH, headerLines) => {
112
119
  const commits = view.commits ?? [];
113
- const cols = commitColumns(commits);
120
+ const cols = measureColumns(commits, commitRowFields, COMMIT_COL_ALIGN);
114
121
  const cursor = view.commitCursor ?? 0;
122
+ const compose = view.compose;
123
+ const extra = commitEditRow(compose, commits, cursor, width, color, cols);
115
124
  const paintRow = (entry, rowWidth, rowColor, selected) =>
116
125
  paintCommitRow(entry, rowWidth, rowColor, selected, cols);
117
126
  const painted = paintCursorList(
@@ -122,7 +131,7 @@ const paintBodyCommits = (view, width, color, bodyH, headerLines) => {
122
131
  bodyH,
123
132
  headerLines,
124
133
  paintRow,
125
- null,
134
+ extra,
126
135
  view.listScroll,
127
136
  );
128
137
  return paneResult({
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const ansi = require('../ansi.js');
4
- const { displayLines } = require('../diff.js');
4
+ const { displayLines } = require('../diff/diff.js');
5
5
  const highlight = require('../highlight.js');
6
6
  const { detectLang } = require('../detect.js');
7
7
  const files = require('../files.js');
@@ -78,10 +78,8 @@ const STAGED_STYLES = {
78
78
  };
79
79
 
80
80
  const SOFT_WRAP = ['warn', 'note'];
81
- const DEL_CHAR_BG = ansi.bg(THEME.delCharBg);
82
- const ADD_CHAR_BG = ansi.bg(THEME.addCharBg);
83
-
84
- const lineStyles = (origin) => (origin === 'staged' ? STAGED_STYLES : STYLES);
81
+ const delCharBg = () => ansi.bg(THEME.delCharBg);
82
+ const addCharBg = () => ansi.bg(THEME.addCharBg);
85
83
 
86
84
  const paintSpans = (pieces, lineStyle, budget, color) => {
87
85
  let out = '';
@@ -129,13 +127,13 @@ const paintAuditRule = (width, color) => {
129
127
  const paintDiffLine = (line, width, color, origin, lang) => {
130
128
  if (line.type === 'noteSep') return paintAuditRule(width, color);
131
129
  if (line.type === 'note') return paintAuditNote(line, width, color);
132
- const styles = lineStyles(origin);
130
+ const styles = origin === 'staged' ? STAGED_STYLES : STYLES;
133
131
  const style = styles[line.type] ?? styles.ctx;
134
132
  const diffSpans = line.spans ?? [{ text: line.text, changed: false }];
135
133
  const tokens = tokenize(lang, line.text ?? '');
136
134
  const pieces = overlayTokens(tokens, diffSpans);
137
135
  const gutterW = Math.min(GUTTER_W, Math.max(0, width));
138
- const label = `${style.mark} `.slice(0, gutterW);
136
+ const label = `${line.mark ?? style.mark} `.slice(0, gutterW);
139
137
  const gutter = paint(label, CODE_FG.punct, style.baseBg, color);
140
138
  const budget = Math.max(0, width - gutterW);
141
139
  const rest = paintSpans(pieces, style, budget, color);
@@ -190,11 +188,12 @@ const wrapSoftLine = (line, width) => {
190
188
  const budget = Math.max(1, width - gutterW);
191
189
  const rows = wrapPlain(line.text ?? '', budget);
192
190
  const changed = line.wrap === true;
193
- const pieces = [];
191
+ const { length } = rows;
192
+ const pieces = new Array(length);
194
193
  let used = 0;
195
- for (let i = 0; i < rows.length; i++) {
194
+ for (let i = 0; i < length; i++) {
196
195
  const text = rows[i];
197
- const last = i === rows.length - 1;
196
+ const last = i === length - 1;
198
197
  const spans = [{ text, changed }];
199
198
  const next = { ...line, text, spans, editLast: last };
200
199
  if (line.editStart !== undefined) {
@@ -203,7 +202,7 @@ const wrapSoftLine = (line, width) => {
203
202
  else next.editEnd = next.editStart + text.length;
204
203
  }
205
204
  used += text.length;
206
- pieces.push(next);
205
+ pieces[i] = next;
207
206
  }
208
207
  return pieces;
209
208
  };
@@ -249,8 +248,7 @@ const expandSoftRows = (lines, width, split) => {
249
248
  const onCodePiece = (line, cursor) => {
250
249
  if (!line || line.editStart === undefined) return false;
251
250
  if (cursor < line.editStart) return false;
252
- if (line.editLast) return cursor <= line.editEnd;
253
- return cursor < line.editEnd;
251
+ return cursor <= line.editEnd;
254
252
  };
255
253
 
256
254
  const codeCursorInRows = (rows, split, width, cursor) => {
@@ -283,14 +281,15 @@ const paintBodyDiff = (view, width, color) => {
283
281
  const splitBody = layout === 'side' && !item.file.isBinary;
284
282
  const raw = dropLeadingEmpty(bodyLines(item, layout, overlay), splitBody);
285
283
  const lines = expandSoftRows(raw, width, splitBody);
286
- const body = [];
284
+ const { length } = lines;
285
+ const body = new Array(length);
287
286
  if (splitBody) {
288
- for (const row of lines) {
289
- body.push(paintSplitRow(row, width, color, item.origin, lang));
287
+ for (let i = 0; i < length; i++) {
288
+ body[i] = paintSplitRow(lines[i], width, color, item.origin, lang);
290
289
  }
291
290
  } else {
292
- for (const line of lines) {
293
- body.push(paintDiffLine(line, width, color, item.origin, lang));
291
+ for (let i = 0; i < length; i++) {
292
+ body[i] = paintDiffLine(lines[i], width, color, item.origin, lang);
294
293
  }
295
294
  }
296
295
  const edit = overlay ? overlay.cursor : null;
@@ -299,9 +298,10 @@ const paintBodyDiff = (view, width, color) => {
299
298
  };
300
299
 
301
300
  module.exports = {
302
- DEL_CHAR_BG,
303
- ADD_CHAR_BG,
301
+ delCharBg,
302
+ addCharBg,
304
303
  paintDiffLine,
305
304
  codeInnerWidth,
306
305
  paintBodyDiff,
306
+ codeCursorInRows,
307
307
  };
@@ -3,10 +3,11 @@
3
3
  const ansi = require('../ansi.js');
4
4
  const files = require('../files.js');
5
5
  const primitives = require('./primitives.js');
6
+ const { measureColumns, padColumns } = primitives;
6
7
 
7
8
  const { isTodosEntry } = files;
8
9
  const { THEME, paint, visibleWidth, truncateVisible, seq, EL } = ansi;
9
- const { fill, padVisible, FILE_MARK, paneResult, paintCursorList } = primitives;
10
+ const { fill, FILE_MARK, paneResult, paintCursorList } = primitives;
10
11
 
11
12
  const FILE_STATUS_FG = {
12
13
  unstaged: THEME.delLineFg,
@@ -15,8 +16,6 @@ const FILE_STATUS_FG = {
15
16
  partial: THEME.warnFg,
16
17
  };
17
18
 
18
- const FILE_COL_KEYS = ['add', 'del', 'status', 'n', 'm', 'date'];
19
-
20
19
  const FILE_COL_ALIGN = {
21
20
  add: 'start',
22
21
  del: 'start',
@@ -46,19 +45,6 @@ const fileRowFields = (entry) => {
46
45
  return { add, del, status, n, m, date };
47
46
  };
48
47
 
49
- const fileColumns = (files) => {
50
- const widths = {};
51
- for (const key of FILE_COL_KEYS) widths[key] = 0;
52
- for (const entry of files) {
53
- const fields = fileRowFields(entry);
54
- for (const key of FILE_COL_KEYS) {
55
- const w = visibleWidth(fields[key]);
56
- if (w > widths[key]) widths[key] = w;
57
- }
58
- }
59
- return widths;
60
- };
61
-
62
48
  const fileRowFg = (status, selected) => {
63
49
  const statusFg = FILE_STATUS_FG[status];
64
50
  if (statusFg) return statusFg;
@@ -66,17 +52,6 @@ const fileRowFg = (status, selected) => {
66
52
  return THEME.mutedFg;
67
53
  };
68
54
 
69
- const fileRowParts = (entry, cols) => {
70
- const fields = fileRowFields(entry);
71
- const parts = {};
72
- for (const key of FILE_COL_KEYS) {
73
- const width = cols?.[key] ?? visibleWidth(fields[key]);
74
- const align = FILE_COL_ALIGN[key];
75
- parts[key] = padVisible(fields[key], width, align);
76
- }
77
- return parts;
78
- };
79
-
80
55
  const joinFileDate = (parts, cols) => {
81
56
  if (!cols?.date) return '';
82
57
  return `${FILE_DATE_GAP}${parts.date}`;
@@ -87,9 +62,6 @@ const joinFileMeta = (parts) => {
87
62
  return ` ${add} ${del} ${status} ${n}/${m}`;
88
63
  };
89
64
 
90
- const joinFileSuffix = (parts, cols) =>
91
- `${joinFileMeta(parts)}${joinFileDate(parts, cols)}`;
92
-
93
65
  const paintFileSuffix = (parts, cols, fgRgb, bgRgb, color) => {
94
66
  const { add, del, status, n, m } = parts;
95
67
  const gap = paint(' ', fgRgb, bgRgb, color);
@@ -111,7 +83,7 @@ const paintTodosRow = (entry, width, color, selected, cols) => {
111
83
  const inner = Math.max(1, width - 1);
112
84
  const mark = selected ? FILE_MARK : ' ';
113
85
  const prefix = ` ${mark} `;
114
- const parts = fileRowParts(entry, cols);
86
+ const parts = padColumns(fileRowFields(entry), cols, FILE_COL_ALIGN);
115
87
  const meta = joinFileMeta(parts);
116
88
  const dated = joinFileDate(parts, cols);
117
89
  const suffix = `${meta}${dated}`;
@@ -139,8 +111,8 @@ const paintFileRow = (entry, width, color, selected, cols) => {
139
111
  const inner = Math.max(1, width - 1);
140
112
  const mark = selected ? FILE_MARK : ' ';
141
113
  const prefix = ` ${mark} `;
142
- const parts = fileRowParts(entry, cols);
143
- const suffix = joinFileSuffix(parts, cols);
114
+ const parts = padColumns(fileRowFields(entry), cols, FILE_COL_ALIGN);
115
+ const suffix = `${joinFileMeta(parts)}${joinFileDate(parts, cols)}`;
144
116
  const pathW = Math.max(1, inner - visibleWidth(prefix + suffix));
145
117
  const { path, status } = entry;
146
118
  const clipped = truncateVisible(path, pathW);
@@ -158,7 +130,7 @@ const paintFileRow = (entry, width, color, selected, cols) => {
158
130
 
159
131
  const paintBodyFiles = (view, width, color, bodyH, headerLines) => {
160
132
  const files = view.files ?? [];
161
- const cols = fileColumns(files);
133
+ const cols = measureColumns(files, fileRowFields, FILE_COL_ALIGN);
162
134
  const cursor = view.fileCursor ?? 0;
163
135
  const paintRow = (entry, rowWidth, rowColor, selected) =>
164
136
  paintFileRow(entry, rowWidth, rowColor, selected, cols);
@@ -9,6 +9,7 @@ const branches = require('./branches.js');
9
9
  const commits = require('./commits.js');
10
10
  const notes = require('./notes.js');
11
11
  const diff = require('./diff.js');
12
+ const unit = require('./unit.js');
12
13
 
13
14
  const { RESET } = ansi;
14
15
  const { LIST_PANES, paintBodyFill, paneResult } = primitives;
@@ -20,12 +21,14 @@ const { paintBodyFiles } = files;
20
21
  const { paintBodyBranches } = branches;
21
22
  const { paintBodyCommits } = commits;
22
23
  const { paintBodyDiff } = diff;
24
+ const { paintBodyUnit } = unit;
23
25
 
24
26
  const bodyKind = (view) => {
25
27
  const pane = view.pane ?? 'diff';
26
28
  if (pane === 'files' || pane === 'branches' || pane === 'commits') {
27
29
  return pane;
28
30
  }
31
+ if (pane === 'unit') return 'unit';
29
32
  if (!view.item) return 'empty';
30
33
  if (view.item.origin === 'todo') return 'todo';
31
34
  return 'diff';
@@ -47,6 +50,7 @@ const BODY_PAINT = {
47
50
  empty: paintBodyEmpty,
48
51
  todo: paintBodyTodo,
49
52
  diff: paintBodyDiff,
53
+ unit: paintBodyUnit,
50
54
  };
51
55
 
52
56
  const paintFrameBody = (view, width, color, bodyH, headerLines) => {
@@ -84,41 +88,60 @@ const mapBodyCursor = (cursor, shift, scroll, headerLines, shown) => {
84
88
  const windowBody = (view, width, color, bodyH, headerLines, paintedBody) => {
85
89
  const sourceBody = paintedBody.body;
86
90
  const sourceOwners = paintedBody.todoOwners;
91
+ const sourceChecks = paintedBody.todoChecks ?? [];
87
92
  const splitBody = paintedBody.splitBody === true;
88
93
  const pane = view.pane ?? 'diff';
89
94
  const listPane = LIST_PANES.includes(pane);
90
95
  const fillKind = bodyFillKind(listPane, splitBody);
91
96
  const filler = paintBodyFill(width, color, fillKind);
92
- const body = [];
93
- const todoOwners = [];
97
+ const lead = !listPane && sourceBody.length > 0;
98
+ const body = new Array(sourceBody.length + (lead ? 1 : 0));
99
+ let bodyAt = 0;
94
100
  let shift = 0;
95
- if (!listPane && sourceBody.length > 0) {
96
- body.push(filler);
97
- if (sourceOwners.length) todoOwners.push(null);
101
+ if (lead) {
102
+ body[bodyAt++] = filler;
98
103
  shift = 1;
99
104
  }
100
- for (const row of sourceBody) body.push(row);
101
- for (const owner of sourceOwners) todoOwners.push(owner);
105
+ for (let i = 0; i < sourceBody.length; i++) body[bodyAt++] = sourceBody[i];
106
+ const ownerLead = lead && sourceOwners.length;
107
+ const todoOwners = new Array(sourceOwners.length + (ownerLead ? 1 : 0));
108
+ let ownerAt = 0;
109
+ if (ownerLead) todoOwners[ownerAt++] = null;
110
+ for (let i = 0; i < sourceOwners.length; i++) {
111
+ todoOwners[ownerAt++] = sourceOwners[i];
112
+ }
113
+ const checkLead = lead && sourceChecks.length;
114
+ const todoChecks = new Array(sourceChecks.length + (checkLead ? 1 : 0));
115
+ let checkAt = 0;
116
+ if (checkLead) todoChecks[checkAt++] = false;
117
+ for (let i = 0; i < sourceChecks.length; i++) {
118
+ todoChecks[checkAt++] = sourceChecks[i];
119
+ }
102
120
  const scrollRaw = view.scroll ?? 0;
103
121
  const scrollMax = Math.max(0, body.length - bodyH);
104
122
  const scroll = listPane ? 0 : Math.min(scrollRaw, scrollMax);
105
- const slice = [];
106
123
  const end = Math.min(body.length, scroll + bodyH);
107
- for (let i = scroll; i < end; i++) slice.push(body[i]);
124
+ const copied = Math.max(0, end - scroll);
125
+ const slice = new Array(bodyH);
126
+ for (let i = 0; i < copied; i++) slice[i] = body[scroll + i];
108
127
  const todoHits = [];
109
- for (let i = 0; i < slice.length; i++) {
128
+ for (let i = 0; i < copied; i++) {
110
129
  const owner = todoOwners[scroll + i];
111
130
  if (owner === undefined || owner === null) continue;
112
- todoHits.push({ y: headerLines + i + 1, cursor: owner });
131
+ const y = headerLines + i + 1;
132
+ if (todoChecks[scroll + i]) {
133
+ todoHits.push({ y, cursor: owner, x0: 1, x1: 4, check: true });
134
+ }
135
+ todoHits.push({ y, cursor: owner });
113
136
  }
114
137
  const cursor = mapBodyCursor(
115
138
  paintedBody.cursor,
116
139
  shift,
117
140
  scroll,
118
141
  headerLines,
119
- slice.length,
142
+ copied,
120
143
  );
121
- while (slice.length < bodyH) slice.push(filler);
144
+ for (let i = copied; i < bodyH; i++) slice[i] = filler;
122
145
  const listScroll = paintedBody.listScroll ?? 0;
123
146
  const fileHits = paintedBody.fileHits;
124
147
  return { slice, scroll, scrollMax, listScroll, todoHits, fileHits, cursor };