reslop 0.1.8 → 0.2.0

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 (95) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +27 -38
  3. package/bin/reslop.js +2 -2
  4. package/lib/ansi.js +6 -2
  5. package/lib/api.js +2 -2
  6. package/lib/capabilities.js +10 -14
  7. package/lib/cli.js +28 -47
  8. package/lib/clipboard.js +1 -1
  9. package/lib/deps-diff.js +712 -0
  10. package/lib/deps.js +556 -32
  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 +34 -8
  20. package/lib/{git/branches.js → git-branches.js} +103 -10
  21. package/lib/{git/dependencies.js → git-deps.js} +111 -38
  22. package/lib/git.js +592 -65
  23. package/lib/github.js +28 -197
  24. package/lib/gitlab.js +24 -195
  25. package/lib/highlight/bash.js +1 -3
  26. package/lib/highlight/css.js +1 -3
  27. package/lib/highlight/js.js +11 -14
  28. package/lib/highlight.js +6 -9
  29. package/lib/keys.js +22 -9
  30. package/lib/manifest.js +330 -0
  31. package/lib/remote.js +374 -0
  32. package/lib/render/branches.js +34 -66
  33. package/lib/render/chrome.js +57 -22
  34. package/lib/render/commits.js +85 -0
  35. package/lib/render/diff.js +16 -16
  36. package/lib/render/files.js +29 -37
  37. package/lib/render/frame.js +29 -13
  38. package/lib/render/notes.js +4 -3
  39. package/lib/render/primitives.js +46 -1
  40. package/lib/{render.js → render/render.js} +6 -7
  41. package/lib/render/unit.js +41 -0
  42. package/lib/review.js +750 -14
  43. package/lib/select.js +5 -10
  44. package/lib/session/accessors.js +9 -1
  45. package/lib/session/actions.js +93 -36
  46. package/lib/session/branches.js +130 -144
  47. package/lib/session/change-actions.js +206 -124
  48. package/lib/session/commit.js +217 -62
  49. package/lib/session/compose.js +158 -13
  50. package/lib/session/dispatch.js +109 -55
  51. package/lib/session/files-pane.js +145 -142
  52. package/lib/session/items.js +131 -34
  53. package/lib/session/lifecycle.js +82 -42
  54. package/lib/session/load.js +19 -33
  55. package/lib/session/navigation.js +15 -3
  56. package/lib/session/ops.js +28 -41
  57. package/lib/session/pointer.js +58 -54
  58. package/lib/session/review.js +23 -37
  59. package/lib/session/terminal.js +6 -12
  60. package/lib/session/todos.js +8 -12
  61. package/lib/session/unit.js +208 -0
  62. package/lib/session/update.js +33 -46
  63. package/lib/session/view.js +82 -11
  64. package/lib/session/watch.js +11 -19
  65. package/lib/session.js +29 -226
  66. package/lib/source.js +3 -3
  67. package/lib/update.js +40 -75
  68. package/lib/{git/proc.js → utilities.js} +75 -11
  69. package/lib/wrap.js +1 -3
  70. package/package.json +2 -2
  71. package/lib/deps/changes.js +0 -331
  72. package/lib/deps/items.js +0 -131
  73. package/lib/deps/manifest.js +0 -145
  74. package/lib/deps/merge.js +0 -120
  75. package/lib/deps/proposals.js +0 -164
  76. package/lib/deps/reports.js +0 -180
  77. package/lib/deps/rows.js +0 -177
  78. package/lib/deps/scan.js +0 -138
  79. package/lib/deps/usage.js +0 -68
  80. package/lib/deps/versions.js +0 -119
  81. package/lib/diff.js +0 -33
  82. package/lib/git/changes.js +0 -164
  83. package/lib/git/read.js +0 -253
  84. package/lib/git/run.js +0 -100
  85. package/lib/npm/commands.js +0 -100
  86. package/lib/remote/changes.js +0 -37
  87. package/lib/remote/headers.js +0 -36
  88. package/lib/remote/http.js +0 -122
  89. package/lib/remote/notes.js +0 -94
  90. package/lib/review/format.js +0 -434
  91. package/lib/review/storage.js +0 -150
  92. package/lib/review/store.js +0 -178
  93. package/lib/review/templates.js +0 -49
  94. package/lib/session/origins.js +0 -7
  95. package/lib/sys.js +0 -84
@@ -6,10 +6,12 @@ const files = require('../files.js');
6
6
  const primitives = require('./primitives.js');
7
7
 
8
8
  const { itemPath, TODO_FILE, isTodosEntry, fileTotals } = files;
9
- const { ACTION_IDS, BRANCH_ACTIONS, actionLetter, buttonWord } = keys;
10
- const { FILES_DISABLED, FILES_GIT_DISABLED, BRANCHES_DISABLED } = keys;
11
- const { TODO_DISABLED, DIFF_DISABLED, diffGitDisabled } = keys;
12
- const { branchGitDisabled, CONFIRM, confirmPrefix, promptFromChoices } = keys;
9
+ const { ACTION_IDS, BRANCH_ACTIONS, COMMIT_ACTIONS, actionLetter } = keys;
10
+ const { buttonWord } = keys;
11
+ const { FILES_HIDDEN, FILES_GIT_DISABLED, BRANCHES_DISABLED } = keys;
12
+ const { COMMITS_DISABLED, TODO_DISABLED, DIFF_DISABLED, UNIT_DISABLED } = keys;
13
+ const { diffGitDisabled, branchGitDisabled, commitGitDisabled } = keys;
14
+ const { CONFIRM, confirmPrefix, promptFromChoices } = keys;
13
15
  const { THEME, paint, visibleWidth } = ansi;
14
16
  const { truncateVisible, fg, RESET, EL, seq } = ansi;
15
17
  const { fill, branchLabel, paintBracketName } = primitives;
@@ -63,6 +65,8 @@ const INFO_STATUS = [
63
65
  'saved',
64
66
  'reloaded',
65
67
  'not a diff block',
68
+ 'file scope',
69
+ 'diff mode',
66
70
  'committed',
67
71
  'amended',
68
72
  'fixup',
@@ -91,7 +95,6 @@ const INFO_STATUS = [
91
95
  const INFO_PREFIX = ['checked out ', 'created ', 'rebased onto ', 'dropped '];
92
96
 
93
97
  const QUIT_PROMPT = promptFromChoices(CONFIRM.confirmQuit.choices);
94
- const COMMIT_PROMPT = promptFromChoices(CONFIRM.confirmCommit.choices);
95
98
  const updatePrompt = (from, to) => ` update reslop ${from} → ${to}? y/n`;
96
99
  const dropPrompt = (name) => ` drop ${name}? y/n`;
97
100
  const PUSH_REJECT_PREFIX = CONFIRM.confirmPush.prefix;
@@ -112,8 +115,6 @@ const originLabel = (origin, view) => {
112
115
  return origin ?? 'unknown';
113
116
  };
114
117
 
115
- const headerRoleFg = (role) => HEADER_ROLE_FG[role] ?? THEME.headerChromeFg;
116
-
117
118
  const pathSegments = (rel) => {
118
119
  const segs = [];
119
120
  if (!rel) return segs;
@@ -160,6 +161,24 @@ const headerTarget = (view) => {
160
161
  const origin = entry.current ? 'current' : 'branch';
161
162
  return { path: entry.name, origin, n, total };
162
163
  }
164
+ if (pane === 'commits') {
165
+ const commits = view.commits ?? [];
166
+ const cursor = view.commitCursor ?? 0;
167
+ const entry = commits[cursor];
168
+ const total = commits.length;
169
+ const n = total ? cursor + 1 : 0;
170
+ if (!entry) return { path: '', origin: 'commit', n, total };
171
+ const origin = entry.head ? 'HEAD' : 'commit';
172
+ return { path: entry.subject ?? '', origin, n, total };
173
+ }
174
+ if (pane === 'unit') {
175
+ const path = view.reviewPath ?? '';
176
+ const lines = view.unitLines ?? [];
177
+ const top = Math.max(0, (view.scroll ?? 0) - 1);
178
+ const n = lines.length ? Math.min(lines.length, top + 1) : 0;
179
+ const origin = view.item ? originLabel(view.item.origin, view) : 'unit';
180
+ return { path, origin, n, total: lines.length };
181
+ }
163
182
  if (!view.item) return { path: '', origin: '', n: 0, total: view.total ?? 0 };
164
183
  if (view.item.origin === 'todo') {
165
184
  return { path: TODO_FILE, origin: 'todo', n: 1, total: 1 };
@@ -232,7 +251,8 @@ const paintHeader = (view, width, color) => {
232
251
  if (!piece.text) {
233
252
  continue;
234
253
  }
235
- out += `${fg(headerRoleFg(piece.role))}${piece.text}`;
254
+ const roleFg = HEADER_ROLE_FG[piece.role] ?? THEME.headerChromeFg;
255
+ out += `${fg(roleFg)}${piece.text}`;
236
256
  }
237
257
  return `${out}${RESET}`;
238
258
  };
@@ -276,7 +296,7 @@ const layoutButtons = (width, compact, hidden, extra, disabled) => {
276
296
  for (const id of ACTION_IDS) {
277
297
  if (id === 'quit') {
278
298
  for (const add of extraIds) {
279
- if (!hiddenIds.includes(add)) ids.push(add);
299
+ if (!ids.includes(add)) ids.push(add);
280
300
  }
281
301
  }
282
302
  if (hiddenIds.includes(id)) continue;
@@ -380,8 +400,8 @@ const statusDelta = (total) => {
380
400
  const hasDelta =
381
401
  stagedAdd || unstagedAdd || stagedDel || unstagedDel || total.added;
382
402
  if (!hasDelta && !total.removed) return '';
383
- const plus = `+${stagedAdd}/${unstagedAdd}`;
384
- const minus = `-${stagedDel}/${unstagedDel}`;
403
+ const plus = `+${stagedAdd}/${stagedAdd + unstagedAdd}`;
404
+ const minus = `-${stagedDel}/${stagedDel + unstagedDel}`;
385
405
  return joinStatus([plus, minus]);
386
406
  };
387
407
 
@@ -417,12 +437,14 @@ const statusLayout = (view, status, width) => {
417
437
  };
418
438
 
419
439
  const choiceHits = (choices, prefix, y) => {
420
- const hits = [];
440
+ const { length } = choices;
441
+ const hits = new Array(length);
421
442
  let x = visibleWidth(prefix);
422
- for (const choice of choices) {
443
+ for (let i = 0; i < length; i++) {
444
+ const choice = choices[i];
423
445
  const piece = `${choice.lead}${choice.label}`;
424
446
  const w = visibleWidth(piece);
425
- hits.push({ id: choice.letter, y, x0: x, x1: x + w });
447
+ hits[i] = { id: choice.letter, y, x0: x, x1: x + w };
426
448
  x += w;
427
449
  }
428
450
  return hits;
@@ -525,10 +547,16 @@ const statusBranchHit = (view, width, y) => {
525
547
  return { id: 'branch', y, x0: open, x1: close + 1 };
526
548
  };
527
549
 
550
+ const selectedBranch = (view) => (view.branches ?? [])[view.branchCursor ?? 0];
551
+
552
+ const selectedCommit = (view) => (view.commits ?? [])[view.commitCursor ?? 0];
553
+
528
554
  const hiddenActions = (view) => {
529
555
  const pane = view.pane ?? 'diff';
530
- if (pane === 'files') return FILES_DISABLED;
556
+ if (pane === 'files') return FILES_HIDDEN;
557
+ if (pane === 'unit') return UNIT_DISABLED;
531
558
  if (pane === 'branches') return BRANCHES_DISABLED;
559
+ if (pane === 'commits') return COMMITS_DISABLED;
532
560
  if (view.item && view.item.origin === 'todo') return TODO_DISABLED;
533
561
  return DIFF_DISABLED;
534
562
  };
@@ -541,23 +569,30 @@ const dimFooterActions = (view) => {
541
569
  if (!entry || entry.kind !== 'todos') return [];
542
570
  return FILES_GIT_DISABLED;
543
571
  }
544
- if (pane === 'diff') return diffGitDisabled(view.item);
545
- if (pane === 'branches') {
546
- const branches = view.branches ?? [];
547
- const entry = branches[view.branchCursor ?? 0];
548
- return branchGitDisabled(entry);
572
+ if (pane === 'diff' || pane === 'unit') return diffGitDisabled(view.item);
573
+ if (pane === 'branches') return branchGitDisabled(selectedBranch(view));
574
+ if (pane === 'commits') {
575
+ const entry = selectedCommit(view);
576
+ const staged = (view.counts?.staged ?? 0) > 0;
577
+ if (!entry) return commitGitDisabled({ canCommit: staged });
578
+ return commitGitDisabled({ ...entry, canCommit: staged });
549
579
  }
550
580
  return [];
551
581
  };
552
582
 
553
583
  const extraFooterActions = (view) => {
554
- if ((view.pane ?? 'diff') === 'branches') return Object.keys(BRANCH_ACTIONS);
584
+ const pane = view.pane ?? 'diff';
585
+ if (pane === 'files') {
586
+ const scope = view.fileScope === 'file' ? 'diff' : 'file';
587
+ return [scope, 'commit', 'pull', 'push'];
588
+ }
589
+ if (pane === 'branches') return Object.keys(BRANCH_ACTIONS);
590
+ if (pane === 'commits') return Object.keys(COMMIT_ACTIONS);
555
591
  return [];
556
592
  };
557
593
 
558
594
  module.exports = {
559
595
  QUIT_PROMPT,
560
- COMMIT_PROMPT,
561
596
  PUSH_PROMPT,
562
597
  updatePrompt,
563
598
  dropPrompt,
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ const ansi = require('../ansi.js');
4
+ const primitives = require('./primitives.js');
5
+ const { measureColumns, padColumns, joinColumns, paintColumns } = primitives;
6
+
7
+ const { THEME, paint, visibleWidth, truncateVisible, seq, EL } = ansi;
8
+ const { fill, FILE_MARK, paneResult, paintCursorList } = primitives;
9
+
10
+ const COMMIT_COL_ALIGN = {
11
+ sha: 'end',
12
+ author: 'start',
13
+ date: 'start',
14
+ };
15
+
16
+ const COMMIT_COL_FG = {
17
+ sha: THEME.shaFg,
18
+ author: THEME.mutedFg,
19
+ date: THEME.mutedFg,
20
+ };
21
+
22
+ const COMMIT_EDGE = 2;
23
+ const COMMIT_TAIL = 1;
24
+ const commitRowFields = (entry) => ({
25
+ sha: entry.shortSha ?? '',
26
+ author: entry.author ?? '',
27
+ date: entry.date ?? '',
28
+ });
29
+
30
+ const paintCommitRow = (entry, width, color, selected, cols) => {
31
+ const inner = Math.max(1, width - COMMIT_EDGE - COMMIT_TAIL);
32
+ const mark = selected ? FILE_MARK : ' ';
33
+ const prefix = ` ${mark} `;
34
+ const lead = ' ';
35
+ const parts = padColumns(commitRowFields(entry), cols, COMMIT_COL_ALIGN);
36
+ const suffix = joinColumns(parts, cols);
37
+ const rest = Math.max(1, inner - visibleWidth(prefix + lead + suffix));
38
+ const subject = truncateVisible(entry.subject ?? '', rest);
39
+ const pad = Math.max(0, rest - visibleWidth(subject));
40
+ const after = `${' '.repeat(pad)}`;
41
+ const left = `${prefix}${lead}${subject}${after}`;
42
+ const fgRgb = selected ? THEME.chromeFg : THEME.mutedFg;
43
+ const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
44
+ const nameFg = selected ? THEME.buttonHotFg : THEME.chromeFg;
45
+ const tail = ' '.repeat(COMMIT_TAIL);
46
+ const edge = fill(COMMIT_EDGE, fgRgb, THEME.ctxBg, color);
47
+ if (!color) return `${left}${suffix}${tail}${edge}`;
48
+ let out = `${seq(fgRgb, bgRgb)}${EL}`;
49
+ out += paint(prefix, fgRgb, bgRgb, color);
50
+ out += paint(lead, nameFg, bgRgb, color);
51
+ out += paint(subject, nameFg, bgRgb, color);
52
+ out += paint(after, fgRgb, bgRgb, color);
53
+ out += paintColumns(parts, cols, fgRgb, bgRgb, color, COMMIT_COL_FG);
54
+ out += paint(tail, fgRgb, bgRgb, color);
55
+ return out + edge;
56
+ };
57
+
58
+ const paintBodyCommits = (view, width, color, bodyH, headerLines) => {
59
+ const commits = view.commits ?? [];
60
+ const cols = measureColumns(commits, commitRowFields, COMMIT_COL_ALIGN);
61
+ const cursor = view.commitCursor ?? 0;
62
+ const paintRow = (entry, rowWidth, rowColor, selected) =>
63
+ paintCommitRow(entry, rowWidth, rowColor, selected, cols);
64
+ const painted = paintCursorList(
65
+ commits,
66
+ cursor,
67
+ width,
68
+ color,
69
+ bodyH,
70
+ headerLines,
71
+ paintRow,
72
+ null,
73
+ view.listScroll,
74
+ );
75
+ return paneResult({
76
+ body: painted.body,
77
+ fileHits: painted.fileHits,
78
+ todoOwners: [],
79
+ cursor: painted.cursor,
80
+ splitBody: false,
81
+ listScroll: painted.offset,
82
+ });
83
+ };
84
+
85
+ module.exports = { paintBodyCommits };
@@ -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');
@@ -81,8 +81,6 @@ const SOFT_WRAP = ['warn', 'note'];
81
81
  const DEL_CHAR_BG = ansi.bg(THEME.delCharBg);
82
82
  const ADD_CHAR_BG = ansi.bg(THEME.addCharBg);
83
83
 
84
- const lineStyles = (origin) => (origin === 'staged' ? STAGED_STYLES : STYLES);
85
-
86
84
  const paintSpans = (pieces, lineStyle, budget, color) => {
87
85
  let out = '';
88
86
  let used = 0;
@@ -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;
@@ -304,4 +303,5 @@ module.exports = {
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,23 +16,24 @@ const FILE_STATUS_FG = {
15
16
  partial: THEME.warnFg,
16
17
  };
17
18
 
18
- const FILE_COL_KEYS = ['add', 'del', 'status', 'n', 'm'];
19
-
20
19
  const FILE_COL_ALIGN = {
21
20
  add: 'start',
22
21
  del: 'start',
23
22
  status: 'start',
24
23
  n: 'start',
25
24
  m: 'end',
25
+ date: 'start',
26
26
  };
27
27
 
28
+ const FILE_DATE_GAP = ' ';
29
+
28
30
  const signedCount = (sign, n) => `${sign}${n ?? 0}`;
29
31
 
30
32
  const fileRowFields = (entry) => {
31
33
  if (isTodosEntry(entry)) {
32
34
  const n = `${entry.staged ?? 0}`;
33
35
  const m = `${entry.remaining ?? 0}`;
34
- return { add: '', del: '', status: '', n, m };
36
+ return { add: '', del: '', status: '', n, m, date: '' };
35
37
  }
36
38
  const { added, removed, staged, remaining } = entry;
37
39
  const add = signedCount('+', added);
@@ -39,20 +41,8 @@ const fileRowFields = (entry) => {
39
41
  const status = entry.status ?? '';
40
42
  const n = `${staged ?? 0}`;
41
43
  const m = `${remaining ?? 0}`;
42
- return { add, del, status, n, m };
43
- };
44
-
45
- const fileColumns = (files) => {
46
- const widths = {};
47
- for (const key of FILE_COL_KEYS) widths[key] = 0;
48
- for (const entry of files) {
49
- const fields = fileRowFields(entry);
50
- for (const key of FILE_COL_KEYS) {
51
- const w = visibleWidth(fields[key]);
52
- if (w > widths[key]) widths[key] = w;
53
- }
54
- }
55
- return widths;
44
+ const date = entry.date ?? '';
45
+ return { add, del, status, n, m, date };
56
46
  };
57
47
 
58
48
  const fileRowFg = (status, selected) => {
@@ -62,23 +52,17 @@ const fileRowFg = (status, selected) => {
62
52
  return THEME.mutedFg;
63
53
  };
64
54
 
65
- const fileRowParts = (entry, cols) => {
66
- const fields = fileRowFields(entry);
67
- const parts = {};
68
- for (const key of FILE_COL_KEYS) {
69
- const width = cols?.[key] ?? visibleWidth(fields[key]);
70
- const align = FILE_COL_ALIGN[key];
71
- parts[key] = padVisible(fields[key], width, align);
72
- }
73
- return parts;
55
+ const joinFileDate = (parts, cols) => {
56
+ if (!cols?.date) return '';
57
+ return `${FILE_DATE_GAP}${parts.date}`;
74
58
  };
75
59
 
76
- const joinFileSuffix = (parts) => {
60
+ const joinFileMeta = (parts) => {
77
61
  const { add, del, status, n, m } = parts;
78
62
  return ` ${add} ${del} ${status} ${n}/${m}`;
79
63
  };
80
64
 
81
- const paintFileSuffix = (parts, fgRgb, bgRgb, color) => {
65
+ const paintFileSuffix = (parts, cols, fgRgb, bgRgb, color) => {
82
66
  const { add, del, status, n, m } = parts;
83
67
  const gap = paint(' ', fgRgb, bgRgb, color);
84
68
  const addText = paint(add, THEME.addLineFg, bgRgb, color);
@@ -89,15 +73,20 @@ const paintFileSuffix = (parts, fgRgb, bgRgb, color) => {
89
73
  const mText = paint(m, fgRgb, bgRgb, color);
90
74
  const counts = addText + gap + delText;
91
75
  const meta = statusText + gap + nText + slash + mText;
92
- return gap + counts + gap + meta;
76
+ if (!cols?.date) return gap + counts + gap + meta;
77
+ const dateGap = paint(FILE_DATE_GAP, fgRgb, bgRgb, color);
78
+ const age = paint(parts.date, THEME.mutedFg, bgRgb, color);
79
+ return gap + counts + gap + meta + dateGap + age;
93
80
  };
94
81
 
95
82
  const paintTodosRow = (entry, width, color, selected, cols) => {
96
83
  const inner = Math.max(1, width - 1);
97
84
  const mark = selected ? FILE_MARK : ' ';
98
85
  const prefix = ` ${mark} `;
99
- const parts = fileRowParts(entry, cols);
100
- const suffix = joinFileSuffix(parts);
86
+ const parts = padColumns(fileRowFields(entry), cols, FILE_COL_ALIGN);
87
+ const meta = joinFileMeta(parts);
88
+ const dated = joinFileDate(parts, cols);
89
+ const suffix = `${meta}${dated}`;
101
90
  const pathW = Math.max(1, inner - visibleWidth(prefix + suffix));
102
91
  const clipped = truncateVisible(entry.path, pathW);
103
92
  const pad = Math.max(0, pathW - visibleWidth(clipped));
@@ -108,7 +97,10 @@ const paintTodosRow = (entry, width, color, selected, cols) => {
108
97
  if (!color) return `${left}${suffix}${edge}`;
109
98
  let out = `${seq(fgRgb, bgRgb)}${EL}`;
110
99
  out += paint(left, fgRgb, bgRgb, color, true);
111
- out += paint(suffix, fgRgb, bgRgb, color, true);
100
+ out += paint(meta, fgRgb, bgRgb, color, true);
101
+ if (!dated) return out + edge;
102
+ out += paint(FILE_DATE_GAP, fgRgb, bgRgb, color);
103
+ out += paint(parts.date, THEME.mutedFg, bgRgb, color);
112
104
  return out + edge;
113
105
  };
114
106
 
@@ -119,8 +111,8 @@ const paintFileRow = (entry, width, color, selected, cols) => {
119
111
  const inner = Math.max(1, width - 1);
120
112
  const mark = selected ? FILE_MARK : ' ';
121
113
  const prefix = ` ${mark} `;
122
- const parts = fileRowParts(entry, cols);
123
- const suffix = joinFileSuffix(parts);
114
+ const parts = padColumns(fileRowFields(entry), cols, FILE_COL_ALIGN);
115
+ const suffix = `${joinFileMeta(parts)}${joinFileDate(parts, cols)}`;
124
116
  const pathW = Math.max(1, inner - visibleWidth(prefix + suffix));
125
117
  const { path, status } = entry;
126
118
  const clipped = truncateVisible(path, pathW);
@@ -132,13 +124,13 @@ const paintFileRow = (entry, width, color, selected, cols) => {
132
124
  if (!color) return `${left}${suffix}${edge}`;
133
125
  let out = `${seq(fgRgb, bgRgb)}${EL}`;
134
126
  out += paint(left, fgRgb, bgRgb, color);
135
- out += paintFileSuffix(parts, fgRgb, bgRgb, color);
127
+ out += paintFileSuffix(parts, cols, fgRgb, bgRgb, color);
136
128
  return out + edge;
137
129
  };
138
130
 
139
131
  const paintBodyFiles = (view, width, color, bodyH, headerLines) => {
140
132
  const files = view.files ?? [];
141
- const cols = fileColumns(files);
133
+ const cols = measureColumns(files, fileRowFields, FILE_COL_ALIGN);
142
134
  const cursor = view.fileCursor ?? 0;
143
135
  const paintRow = (entry, rowWidth, rowColor, selected) =>
144
136
  paintFileRow(entry, rowWidth, rowColor, selected, cols);
@@ -6,8 +6,10 @@ const primitives = require('./primitives.js');
6
6
  const chrome = require('./chrome.js');
7
7
  const files = require('./files.js');
8
8
  const branches = require('./branches.js');
9
+ const commits = require('./commits.js');
9
10
  const notes = require('./notes.js');
10
11
  const diff = require('./diff.js');
12
+ const unit = require('./unit.js');
11
13
 
12
14
  const { RESET } = ansi;
13
15
  const { LIST_PANES, paintBodyFill, paneResult } = primitives;
@@ -17,11 +19,16 @@ const { hiddenActions, dimFooterActions, extraFooterActions } = chrome;
17
19
  const { paintNotePanel, paintBodyTodo } = notes;
18
20
  const { paintBodyFiles } = files;
19
21
  const { paintBodyBranches } = branches;
22
+ const { paintBodyCommits } = commits;
20
23
  const { paintBodyDiff } = diff;
24
+ const { paintBodyUnit } = unit;
21
25
 
22
26
  const bodyKind = (view) => {
23
27
  const pane = view.pane ?? 'diff';
24
- if (pane === 'files' || pane === 'branches') return pane;
28
+ if (pane === 'files' || pane === 'branches' || pane === 'commits') {
29
+ return pane;
30
+ }
31
+ if (pane === 'unit') return 'unit';
25
32
  if (!view.item) return 'empty';
26
33
  if (view.item.origin === 'todo') return 'todo';
27
34
  return 'diff';
@@ -39,9 +46,11 @@ const paintBodyEmpty = () =>
39
46
  const BODY_PAINT = {
40
47
  files: paintBodyFiles,
41
48
  branches: paintBodyBranches,
49
+ commits: paintBodyCommits,
42
50
  empty: paintBodyEmpty,
43
51
  todo: paintBodyTodo,
44
52
  diff: paintBodyDiff,
53
+ unit: paintBodyUnit,
45
54
  };
46
55
 
47
56
  const paintFrameBody = (view, width, color, bodyH, headerLines) => {
@@ -84,24 +93,31 @@ const windowBody = (view, width, color, bodyH, headerLines, paintedBody) => {
84
93
  const listPane = LIST_PANES.includes(pane);
85
94
  const fillKind = bodyFillKind(listPane, splitBody);
86
95
  const filler = paintBodyFill(width, color, fillKind);
87
- const body = [];
88
- const todoOwners = [];
96
+ const lead = !listPane && sourceBody.length > 0;
97
+ const body = new Array(sourceBody.length + (lead ? 1 : 0));
98
+ let bodyAt = 0;
89
99
  let shift = 0;
90
- if (!listPane && sourceBody.length > 0) {
91
- body.push(filler);
92
- if (sourceOwners.length) todoOwners.push(null);
100
+ if (lead) {
101
+ body[bodyAt++] = filler;
93
102
  shift = 1;
94
103
  }
95
- for (const row of sourceBody) body.push(row);
96
- for (const owner of sourceOwners) todoOwners.push(owner);
104
+ for (let i = 0; i < sourceBody.length; i++) body[bodyAt++] = sourceBody[i];
105
+ const ownerLead = lead && sourceOwners.length;
106
+ const todoOwners = new Array(sourceOwners.length + (ownerLead ? 1 : 0));
107
+ let ownerAt = 0;
108
+ if (ownerLead) todoOwners[ownerAt++] = null;
109
+ for (let i = 0; i < sourceOwners.length; i++) {
110
+ todoOwners[ownerAt++] = sourceOwners[i];
111
+ }
97
112
  const scrollRaw = view.scroll ?? 0;
98
113
  const scrollMax = Math.max(0, body.length - bodyH);
99
114
  const scroll = listPane ? 0 : Math.min(scrollRaw, scrollMax);
100
- const slice = [];
101
115
  const end = Math.min(body.length, scroll + bodyH);
102
- for (let i = scroll; i < end; i++) slice.push(body[i]);
116
+ const copied = Math.max(0, end - scroll);
117
+ const slice = new Array(bodyH);
118
+ for (let i = 0; i < copied; i++) slice[i] = body[scroll + i];
103
119
  const todoHits = [];
104
- for (let i = 0; i < slice.length; i++) {
120
+ for (let i = 0; i < copied; i++) {
105
121
  const owner = todoOwners[scroll + i];
106
122
  if (owner === undefined || owner === null) continue;
107
123
  todoHits.push({ y: headerLines + i + 1, cursor: owner });
@@ -111,9 +127,9 @@ const windowBody = (view, width, color, bodyH, headerLines, paintedBody) => {
111
127
  shift,
112
128
  scroll,
113
129
  headerLines,
114
- slice.length,
130
+ copied,
115
131
  );
116
- while (slice.length < bodyH) slice.push(filler);
132
+ for (let i = copied; i < bodyH; i++) slice[i] = filler;
117
133
  const listScroll = paintedBody.listScroll ?? 0;
118
134
  const fileHits = paintedBody.fileHits;
119
135
  return { slice, scroll, scrollMax, listScroll, todoHits, fileHits, cursor };
@@ -36,10 +36,11 @@ const wrapTodoItem = (text, inner) => {
36
36
  const bodyWidth = Math.max(1, inner - lead);
37
37
  const hang = ' '.repeat(lead);
38
38
  const wrapped = wrapMultiline(body, bodyWidth);
39
- const lines = [];
40
- for (let i = 0; i < wrapped.length; i++) {
39
+ const { length } = wrapped;
40
+ const lines = new Array(length);
41
+ for (let i = 0; i < length; i++) {
41
42
  const prefix = i === 0 ? mark : hang;
42
- lines.push(`${prefix}${wrapped[i]}`);
43
+ lines[i] = `${prefix}${wrapped[i]}`;
43
44
  }
44
45
  return lines;
45
46
  };
@@ -8,7 +8,7 @@ const GUTTER_W = 2;
8
8
  const FILE_MARK = '▶';
9
9
  const FILE_LIST_PAD = 1;
10
10
  const CHECK_DONE = 'xX';
11
- const LIST_PANES = ['files', 'branches'];
11
+ const LIST_PANES = ['files', 'branches', 'commits'];
12
12
  const CHECK_TOKEN = /\[([ xX])\]/;
13
13
  const CHECK_LEAD = /^\[[ xX]\] /;
14
14
 
@@ -180,7 +180,52 @@ const paneResult = (fields) => ({
180
180
  listScroll: fields.listScroll ?? 0,
181
181
  });
182
182
 
183
+ const measureColumns = (entries, rowFields, align) => {
184
+ const keys = Object.keys(align);
185
+ const widths = Object.fromEntries(keys.map((key) => [key, 0]));
186
+ for (const entry of entries) {
187
+ const fields = rowFields(entry);
188
+ for (const key of keys) {
189
+ widths[key] = Math.max(widths[key], visibleWidth(fields[key]));
190
+ }
191
+ }
192
+ return widths;
193
+ };
194
+
195
+ const padColumns = (fields, widths, align) => {
196
+ const parts = {};
197
+ for (const key of Object.keys(align)) {
198
+ const width = widths?.[key] ?? visibleWidth(fields[key]);
199
+ parts[key] = padVisible(fields[key], width, align[key]);
200
+ }
201
+ return parts;
202
+ };
203
+
204
+ const columnGap = (key) => (key === 'date' ? ' ' : ' ');
205
+
206
+ const joinColumns = (parts, widths) => {
207
+ let text = '';
208
+ for (const key of Object.keys(parts)) {
209
+ if (widths[key]) text += columnGap(key) + parts[key];
210
+ }
211
+ return text;
212
+ };
213
+
214
+ const paintColumns = (parts, widths, fgRgb, bgRgb, color, tones) => {
215
+ let text = '';
216
+ for (const key of Object.keys(parts)) {
217
+ if (!widths[key]) continue;
218
+ text += paint(columnGap(key), fgRgb, bgRgb, color);
219
+ text += paint(parts[key], tones[key] ?? fgRgb, bgRgb, color);
220
+ }
221
+ return text;
222
+ };
223
+
183
224
  module.exports = {
225
+ measureColumns,
226
+ padColumns,
227
+ joinColumns,
228
+ paintColumns,
184
229
  GUTTER_W,
185
230
  FILE_MARK,
186
231
  LIST_PANES,