reslop 0.2.1 → 0.2.4

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 (81) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +19 -4
  3. package/bin/reslop.js +4 -2
  4. package/lib/ansi.js +120 -4
  5. package/lib/api.js +2 -1
  6. package/lib/cli.js +21 -9
  7. package/lib/clipboard.js +6 -3
  8. package/lib/deps-diff.js +61 -115
  9. package/lib/deps.js +57 -54
  10. package/lib/detect.js +4 -2
  11. package/lib/diff/align.js +2 -1
  12. package/lib/diff/diff.js +6 -3
  13. package/lib/diff/display.js +2 -1
  14. package/lib/diff/inline.js +2 -1
  15. package/lib/diff/patch.js +2 -1
  16. package/lib/diff/unit.js +4 -2
  17. package/lib/editor.js +22 -4
  18. package/lib/files.js +38 -12
  19. package/lib/git-branches.js +150 -165
  20. package/lib/git-deps.js +20 -60
  21. package/lib/git-worktree.js +508 -0
  22. package/lib/git.js +23 -580
  23. package/lib/github.js +2 -1
  24. package/lib/gitlab.js +2 -1
  25. package/lib/highlight/bash.js +2 -1
  26. package/lib/highlight/css.js +2 -1
  27. package/lib/highlight/csv.js +2 -1
  28. package/lib/highlight/dot.js +2 -1
  29. package/lib/highlight/html.js +2 -1
  30. package/lib/highlight/json.js +2 -1
  31. package/lib/highlight/log.js +2 -1
  32. package/lib/highlight/txt.js +2 -1
  33. package/lib/highlight.js +2 -1
  34. package/lib/keys.js +12 -6
  35. package/lib/manifest.js +4 -2
  36. package/lib/npm-commands.js +592 -0
  37. package/lib/remote.js +20 -13
  38. package/lib/render/branches.js +25 -41
  39. package/lib/render/chrome.js +163 -128
  40. package/lib/render/commits.js +727 -65
  41. package/lib/render/diff.js +130 -47
  42. package/lib/render/files.js +4 -12
  43. package/lib/render/frame.js +41 -13
  44. package/lib/render/notes.js +4 -11
  45. package/lib/render/npm.js +216 -0
  46. package/lib/render/present.js +2 -1
  47. package/lib/render/primitives.js +34 -50
  48. package/lib/render/render.js +2 -1
  49. package/lib/render/unit.js +14 -11
  50. package/lib/report-model.js +129 -0
  51. package/lib/report-parse.js +89 -0
  52. package/lib/report-render.js +104 -0
  53. package/lib/report-run.js +166 -0
  54. package/lib/report-test.js +507 -0
  55. package/lib/report-tools.js +337 -0
  56. package/lib/review-merge.js +2 -1
  57. package/lib/review.js +6 -2
  58. package/lib/session/actions.js +77 -44
  59. package/lib/session/branches.js +13 -49
  60. package/lib/session/change-actions.js +2 -1
  61. package/lib/session/commit.js +113 -53
  62. package/lib/session/compose.js +83 -14
  63. package/lib/session/dispatch.js +42 -22
  64. package/lib/session/files-pane.js +16 -21
  65. package/lib/session/items.js +6 -3
  66. package/lib/session/lifecycle.js +12 -5
  67. package/lib/session/load.js +4 -2
  68. package/lib/session/navigation.js +2 -0
  69. package/lib/session/npm.js +388 -0
  70. package/lib/session/ops.js +15 -1
  71. package/lib/session/pointer.js +30 -3
  72. package/lib/session/review.js +2 -1
  73. package/lib/session/terminal.js +4 -2
  74. package/lib/session/update.js +2 -1
  75. package/lib/session/view.js +41 -4
  76. package/lib/session.js +44 -22
  77. package/lib/source.js +6 -3
  78. package/lib/update.js +5 -9
  79. package/lib/utilities.js +4 -2
  80. package/lib/wrap.js +3 -1
  81. package/package.json +6 -6
@@ -1,78 +1,195 @@
1
1
  'use strict';
2
2
 
3
3
  const ansi = require('../ansi.js');
4
+ const files = require('../files.js');
4
5
  const primitives = require('./primitives.js');
6
+ const wrap = require('../wrap.js');
7
+ const { shortAge } = files;
8
+ const { wrapMultiline, cursorInWrap } = wrap;
5
9
  const { measureColumns, padColumns, joinColumns, paintColumns } = primitives;
10
+ const { padVisible } = primitives;
6
11
 
7
12
  const { THEME, paint, visibleWidth, truncateVisible, seq, EL } = ansi;
8
- const { fill, FILE_MARK, paneResult, paintCursorList } = primitives;
13
+ const { FILE_MARK, paneResult, paintCursorList } = primitives;
14
+ const { paintBodyFill, listWindowStart } = primitives;
9
15
 
10
16
  const COMMIT_COL_ALIGN = {
11
17
  sha: 'end',
12
- author: 'start',
18
+ refs: 'end',
19
+ add: 'start',
20
+ del: 'start',
13
21
  date: 'start',
14
22
  };
15
23
 
16
24
  const COMMIT_COL_FG = {
17
25
  sha: THEME.shaFg,
18
- author: THEME.mutedFg,
26
+ refs: THEME.warnFg,
27
+ add: THEME.addLineFg,
28
+ del: THEME.delLineFg,
19
29
  date: THEME.mutedFg,
20
30
  };
21
31
 
22
- const COMMIT_EDGE = 2;
32
+ const COMMIT_CURRENT_COL_FG = {
33
+ sha: THEME.shaDarkFg,
34
+ date: THEME.headerFg,
35
+ };
36
+
37
+ const REFS_MAX = 28;
38
+ const LIST_PAD = 1;
23
39
  const COMMIT_TAIL = 1;
24
- const commitRowFields = (entry) => ({
25
- sha: entry.shortSha ?? '',
26
- author: entry.author ?? '',
27
- date: entry.date ?? '',
28
- });
40
+ const lineCounts = (entry) => {
41
+ const added = entry.added ?? 0;
42
+ const removed = entry.removed ?? 0;
43
+ if (!added && !removed) return null;
44
+ return { added, removed };
45
+ };
46
+
47
+ const countLabel = (entry) => {
48
+ if (entry && entry.pending) {
49
+ const stagedAdd = entry.stagedAdded ?? 0;
50
+ const unstagedAdd = entry.unstagedAdded ?? 0;
51
+ const stagedDel = entry.stagedRemoved ?? 0;
52
+ const unstagedDel = entry.unstagedRemoved ?? 0;
53
+ const any = stagedAdd || unstagedAdd || stagedDel || unstagedDel;
54
+ if (!any) return null;
55
+ return {
56
+ add: `+${stagedAdd}/${unstagedAdd}`,
57
+ del: `-${stagedDel}/${unstagedDel}`,
58
+ };
59
+ }
60
+ const counts = lineCounts(entry);
61
+ if (!counts) return null;
62
+ return { add: `+${counts.added}`, del: `-${counts.removed}` };
63
+ };
64
+
65
+ const DIFF_LABEL = 'Diffs:';
66
+ const DIFF_GAP = ' ';
67
+
68
+ const entryCountWidth = (entry) => {
69
+ const label = countLabel(entry);
70
+ if (!label) return { add: 0, del: 0 };
71
+ return {
72
+ add: visibleWidth(label.add),
73
+ del: visibleWidth(label.del),
74
+ };
75
+ };
76
+
77
+ const listCountWidth = (commits) => {
78
+ let add = 0;
79
+ let del = 0;
80
+ for (const entry of commits) {
81
+ const width = entryCountWidth(entry);
82
+ add = Math.max(add, width.add);
83
+ del = Math.max(del, width.del);
84
+ }
85
+ return { add, del };
86
+ };
87
+
88
+ const commitRowFields = (entry) => {
89
+ const refs = `${entry.refs ?? ''}`.trim();
90
+ const label = countLabel(entry);
91
+ return {
92
+ sha: entry.shortSha ?? '',
93
+ refs: refs ? truncateVisible(refs, REFS_MAX) : '',
94
+ add: label ? label.add : '',
95
+ del: label ? label.del : '',
96
+ date: shortAge(entry.date ?? ''),
97
+ };
98
+ };
99
+
100
+ const contentWidth = (width) => Math.max(1, width - COMMIT_TAIL);
101
+
102
+ const splitMessage = (message) => {
103
+ const text = `${message ?? ''}`;
104
+ const at = text.indexOf('\n');
105
+ if (at < 0) return { line: text, rest: '' };
106
+ return { line: text.slice(0, at), rest: text.slice(at + 1) };
107
+ };
108
+
109
+ const commitText = (entry) => {
110
+ if (!entry) return '';
111
+ if (entry.body) return `${entry.body}`;
112
+ return `${entry.subject ?? ''}`;
113
+ };
114
+
115
+ const commitLine = (entry) => splitMessage(commitText(entry)).line;
29
116
 
30
- const paintCommitRow = (entry, width, color, selected, cols) => {
31
- const inner = Math.max(1, width - COMMIT_EDGE - COMMIT_TAIL);
117
+ const viewedCommit = (entry, rev, revShort) => {
118
+ const want = `${rev ?? ''}`;
119
+ const short = `${revShort ?? ''}`;
120
+ if (entry && entry.pending) return !want && !short;
121
+ const sha = entry && entry.sha ? `${entry.sha}` : '';
122
+ const brief = entry && entry.shortSha ? `${entry.shortSha}` : '';
123
+ if (want && (sha === want || brief === want)) return true;
124
+ if (short && (sha === short || brief === short)) return true;
125
+ return false;
126
+ };
127
+
128
+ const FULL_INDENT = ' '.repeat(visibleWidth(` ${FILE_MARK} `));
129
+
130
+ const commitTone = (selected, current) => {
131
+ if (current) {
132
+ return {
133
+ current: true,
134
+ fgRgb: THEME.headerFg,
135
+ bgRgb: THEME.currentBg,
136
+ nameFg: THEME.mutedFg,
137
+ };
138
+ }
139
+ if (selected) {
140
+ return {
141
+ current: false,
142
+ fgRgb: THEME.chromeFg,
143
+ bgRgb: THEME.buttonBg,
144
+ nameFg: THEME.buttonHotFg,
145
+ };
146
+ }
147
+ return {
148
+ current: false,
149
+ fgRgb: THEME.mutedFg,
150
+ bgRgb: THEME.ctxBg,
151
+ nameFg: THEME.chromeFg,
152
+ };
153
+ };
154
+
155
+ const paintCommitRow = (entry, width, color, selected, cols, current) => {
156
+ const inner = contentWidth(width);
32
157
  const mark = selected ? FILE_MARK : ' ';
33
158
  const prefix = ` ${mark} `;
34
- const lead = ' ';
35
159
  const parts = padColumns(commitRowFields(entry), cols, COMMIT_COL_ALIGN);
36
160
  const suffix = joinColumns(parts, cols);
37
- const rest = Math.max(1, inner - visibleWidth(prefix + lead + suffix));
38
- const subject = truncateVisible(entry.subject ?? '', rest);
161
+ const rest = Math.max(1, inner - visibleWidth(prefix + suffix));
162
+ const subject = truncateVisible(commitLine(entry), rest);
39
163
  const pad = Math.max(0, rest - visibleWidth(subject));
40
164
  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;
165
+ const left = `${prefix}${subject}${after}`;
45
166
  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;
167
+ const tone = commitTone(selected, current);
168
+ const tones = current
169
+ ? { ...COMMIT_COL_FG, ...COMMIT_CURRENT_COL_FG }
170
+ : COMMIT_COL_FG;
171
+ if (!color) return `${left}${suffix}${tail}`;
172
+ let out = `${seq(tone.fgRgb, tone.bgRgb)}${EL}`;
173
+ out += paint(prefix, tone.fgRgb, tone.bgRgb, color);
174
+ out += paint(subject, tone.nameFg, tone.bgRgb, color);
175
+ out += paint(after, tone.fgRgb, tone.bgRgb, color);
176
+ out += paintColumns(parts, cols, tone.fgRgb, tone.bgRgb, color, tones);
177
+ out += paint(tail, tone.fgRgb, tone.bgRgb, color);
178
+ return out;
56
179
  };
57
180
 
58
181
  const paintCommitEdit = (compose, width, color, entry, cols) => {
59
- const text = `${compose.text ?? ''}`.replaceAll('\n', ' ');
182
+ const text = splitMessage(compose.text).line;
60
183
  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);
184
+ const shown = { ...entry, body: text, subject: text };
185
+ const row = paintCommitRow(shown, width, color, true, cols);
186
+ const inner = contentWidth(width);
69
187
  const prefix = ` ${FILE_MARK} `;
70
- const lead = ' ';
71
188
  const parts = padColumns(commitRowFields(entry), cols, COMMIT_COL_ALIGN);
72
189
  const suffix = joinColumns(parts, cols);
73
- const rest = Math.max(1, inner - visibleWidth(prefix + lead + suffix));
190
+ const rest = Math.max(1, inner - visibleWidth(prefix + suffix));
74
191
  const clipped = truncateVisible(text, rest);
75
- const prefixW = visibleWidth(prefix + lead);
192
+ const prefixW = visibleWidth(prefix);
76
193
  const col = Math.min(compose.cursor ?? 0, visibleWidth(clipped));
77
194
  return {
78
195
  row,
@@ -80,50 +197,567 @@ const paintCommitEdit = (compose, width, color, entry, cols) => {
80
197
  at: 'replace',
81
198
  };
82
199
  }
83
- const inner = Math.max(1, width - COMMIT_EDGE);
200
+ const inner = contentWidth(width);
84
201
  const prefix = ` ${FILE_MARK} `;
85
- const lead = ' ';
86
- const prefixW = visibleWidth(prefix + lead);
202
+ const prefixW = visibleWidth(prefix);
87
203
  const rest = Math.max(1, inner - prefixW);
88
204
  const clipped = truncateVisible(text, rest);
89
205
  const pad = Math.max(0, rest - visibleWidth(clipped));
90
206
  const gap = ' '.repeat(pad);
207
+ const tail = ' '.repeat(COMMIT_TAIL);
91
208
  const fgRgb = THEME.chromeFg;
92
209
  const bgRgb = THEME.buttonBg;
93
210
  const nameFg = THEME.buttonHotFg;
94
- const edge = fill(COMMIT_EDGE, fgRgb, THEME.ctxBg, color);
95
- let row = `${prefix}${lead}${clipped}${gap}${edge}`;
211
+ let row = `${prefix}${clipped}${gap}${tail}`;
96
212
  if (color) {
97
213
  row = `${seq(fgRgb, bgRgb)}${EL}`;
98
214
  row += paint(prefix, fgRgb, bgRgb, color);
99
- row += paint(lead, nameFg, bgRgb, color);
100
215
  row += paint(clipped, nameFg, bgRgb, color);
101
216
  row += paint(gap, fgRgb, bgRgb, color);
102
- row += edge;
217
+ row += paint(tail, fgRgb, bgRgb, color);
103
218
  }
104
219
  const col = Math.min(compose.cursor ?? 0, visibleWidth(clipped));
105
220
  return { row, cursor: { x: prefixW + col + 1 }, at: 'start' };
106
221
  };
107
222
 
223
+ const pendingAt = (commits) => {
224
+ for (let i = 0; i < commits.length; i++) {
225
+ if (commits[i] && commits[i].pending) return i;
226
+ }
227
+ return -1;
228
+ };
229
+
230
+ const editsCursor = (compose) =>
231
+ compose.commitKind === 'amend' || compose.commitKind === 'reword';
232
+
233
+ const editsPending = (compose) =>
234
+ compose.commitKind === 'commit' || compose.commitKind === 'fixup';
235
+
108
236
  const commitEditRow = (compose, commits, cursor, width, color, cols) => {
109
237
  if (!compose || compose.kind !== 'commit') return null;
110
- if (compose.commitKind === 'amend' || compose.commitKind === 'reword') {
111
- const entry = commits[cursor];
238
+ let at = -1;
239
+ if (editsCursor(compose)) at = cursor;
240
+ if (editsPending(compose)) at = pendingAt(commits);
241
+ if (at >= 0) {
242
+ const entry = commits[at];
112
243
  if (!entry) return null;
113
- return paintCommitEdit(compose, width, color, entry, cols);
244
+ const painted = paintCommitEdit(compose, width, color, entry, cols);
245
+ painted.index = at;
246
+ return painted;
114
247
  }
115
248
  return paintCommitEdit(compose, width, color);
116
249
  };
117
250
 
118
- const paintBodyCommits = (view, width, color, bodyH, headerLines) => {
251
+ const paintPieces = (pieces, width, color, selected, current) => {
252
+ const tone = commitTone(selected, current);
253
+ const inner = contentWidth(width);
254
+ const shown = [];
255
+ let used = 0;
256
+ for (const piece of pieces) {
257
+ const room = inner - used;
258
+ if (room <= 0) break;
259
+ const text = truncateVisible(piece.text, room);
260
+ if (!text) continue;
261
+ shown.push({ text, fg: piece.fg });
262
+ used += visibleWidth(text);
263
+ }
264
+ const pad = ' '.repeat(Math.max(0, inner - used));
265
+ const tail = ' '.repeat(COMMIT_TAIL);
266
+ if (!color) {
267
+ let plain = '';
268
+ for (const piece of shown) plain += piece.text;
269
+ return `${plain}${pad}${tail}`;
270
+ }
271
+ let out = `${seq(tone.fgRgb, tone.bgRgb)}${EL}`;
272
+ for (const piece of shown) {
273
+ out += paint(piece.text, piece.fg, tone.bgRgb, color);
274
+ }
275
+ out += paint(pad, tone.fgRgb, tone.bgRgb, color);
276
+ out += paint(tail, tone.fgRgb, tone.bgRgb, color);
277
+ return out;
278
+ };
279
+
280
+ const COL_GAP = 2;
281
+ const MIN_LEFT = 8;
282
+
283
+ const messageText = (entry) => {
284
+ const body = `${entry.body ?? ''}`.replace(/\s+$/, '');
285
+ if (body) return body;
286
+ return `${entry.subject ?? ''}`.replace(/\s+$/, '');
287
+ };
288
+
289
+ const wrapKind = (text, width, kind) => {
290
+ const rows = wrapMultiline(text, Math.max(1, width));
291
+ const lines = [];
292
+ for (const row of rows) lines.push({ text: row, kind });
293
+ return lines;
294
+ };
295
+
296
+ const messagePartsFrom = (text, width) => {
297
+ const raw = `${text ?? ''}`;
298
+ if (!raw) return [{ text: '', kind: 'text' }];
299
+ const split = raw.split('\n');
300
+ const lines = wrapKind(split[0], width, 'text');
301
+ for (let i = 1; i < split.length; i++) {
302
+ const rest = wrapKind(split[i], width, 'body');
303
+ for (const line of rest) lines.push(line);
304
+ }
305
+ return lines;
306
+ };
307
+
308
+ const messageParts = (entry, width) =>
309
+ messagePartsFrom(messageText(entry), width);
310
+
311
+ const authorParts = (entry) => {
312
+ const name = `${entry.author ?? ''}`.trim();
313
+ const email = `${entry.email ?? ''}`.trim();
314
+ if (name && email) {
315
+ return [
316
+ { text: name, kind: 'author' },
317
+ { text: ` <${email}>`, kind: 'email' },
318
+ ];
319
+ }
320
+ if (email) return [{ text: email, kind: 'email' }];
321
+ if (name) return [{ text: name, kind: 'author' }];
322
+ return [];
323
+ };
324
+
325
+ const partsWidth = (parts) => {
326
+ let width = 0;
327
+ for (const part of parts) width += visibleWidth(part.text);
328
+ return width;
329
+ };
330
+
331
+ const timeNeed = (entry) => {
332
+ const when = `${entry.when ?? ''}`.trim();
333
+ const ago = shortAge(entry.date ?? '');
334
+ if (when && ago) return visibleWidth(when) + COL_GAP + visibleWidth(ago);
335
+ return visibleWidth(when || ago);
336
+ };
337
+
338
+ const refsText = (entry) => `${entry.refs ?? ''}`.trim();
339
+
340
+ const diffLine = (entry, widths) => {
341
+ const label = countLabel(entry);
342
+ if (!label) return null;
343
+ const add = padVisible(label.add, widths.add, 'start');
344
+ const del = padVisible(label.del, widths.del, 'start');
345
+ const text = `${DIFF_LABEL}${DIFF_GAP}${add}${DIFF_GAP}${del}`;
346
+ return { add, del, text };
347
+ };
348
+
349
+ const diffCountParts = (entry, widths, rightW) => {
350
+ const line = diffLine(entry, widths);
351
+ if (!line || !rightW) return [];
352
+ const pad = Math.max(0, rightW - visibleWidth(line.text));
353
+ return [
354
+ { text: ' '.repeat(pad), kind: 'meta' },
355
+ { text: DIFF_LABEL, kind: 'meta' },
356
+ { text: DIFF_GAP, kind: 'meta' },
357
+ { text: line.add, kind: 'add' },
358
+ { text: DIFF_GAP, kind: 'meta' },
359
+ { text: line.del, kind: 'del' },
360
+ ];
361
+ };
362
+
363
+ const rightNeed = (entry, widths = entryCountWidth(entry)) => {
364
+ const line = diffLine(entry, widths);
365
+ return Math.max(
366
+ partsWidth(authorParts(entry)),
367
+ timeNeed(entry),
368
+ visibleWidth(refsText(entry)),
369
+ line ? visibleWidth(line.text) : 0,
370
+ );
371
+ };
372
+
373
+ const spreadTime = (when, ago, width) => {
374
+ const agoW = visibleWidth(ago);
375
+ if (agoW >= width) {
376
+ return [{ text: truncateVisible(ago, width), kind: 'meta' }];
377
+ }
378
+ const room = width - agoW;
379
+ const minGap = Math.min(COL_GAP, room);
380
+ const left = truncateVisible(when, room - minGap);
381
+ const gap = width - visibleWidth(left) - agoW;
382
+ return [
383
+ { text: left, kind: 'meta' },
384
+ { text: ' '.repeat(gap), kind: 'meta' },
385
+ { text: ago, kind: 'meta' },
386
+ ];
387
+ };
388
+
389
+ const alignEnd = (text, width, kind) => {
390
+ const shown = truncateVisible(text, width);
391
+ const pad = Math.max(0, width - visibleWidth(shown));
392
+ return [
393
+ { text: ' '.repeat(pad), kind },
394
+ { text: shown, kind },
395
+ ];
396
+ };
397
+
398
+ const timeParts = (entry, width) => {
399
+ const when = `${entry.when ?? ''}`.trim();
400
+ const ago = shortAge(entry.date ?? '');
401
+ if (!width) return [];
402
+ if (when && ago) return spreadTime(when, ago, width);
403
+ if (ago) return alignEnd(ago, width, 'meta');
404
+ if (when) return [{ text: truncateVisible(when, width), kind: 'meta' }];
405
+ return [];
406
+ };
407
+
408
+ const rightRows = (entry, rightW, widths) => {
409
+ const rows = [];
410
+ const author = authorParts(entry);
411
+ if (author.length) rows.push(author);
412
+ const time = timeParts(entry, rightW);
413
+ if (time.length) rows.push(time);
414
+ const refs = refsText(entry);
415
+ if (refs) rows.push([{ text: refs, kind: 'refs' }]);
416
+ const diff = diffCountParts(entry, widths, rightW);
417
+ if (diff.length) rows.push(diff);
418
+ return rows;
419
+ };
420
+
421
+ const panelWidths = (need, leftNeed, inner) => {
422
+ const indent = visibleWidth(FULL_INDENT);
423
+ let rightW = need;
424
+ const room = Math.max(1, inner - indent);
425
+ if (!rightW || room <= MIN_LEFT) {
426
+ return { rightW: 0, leftW: room, gapW: 0 };
427
+ }
428
+ const reserve = Math.max(MIN_LEFT, leftNeed);
429
+ const minLeft = Math.min(reserve, room - COL_GAP - 1);
430
+ const maxRight = room - COL_GAP - minLeft;
431
+ if (rightW > maxRight) rightW = Math.max(1, maxRight);
432
+ return { rightW, leftW: room - COL_GAP - rightW, gapW: COL_GAP };
433
+ };
434
+
435
+ const panelLeft = (entry, leftW, edit) => {
436
+ const parts = edit
437
+ ? messagePartsFrom(edit.text ?? '', leftW)
438
+ : messageParts(entry, leftW);
439
+ if (!entry.sha) return parts;
440
+ return [{ text: entry.sha, kind: 'hash' }, ...parts];
441
+ };
442
+
443
+ const commitMessageWidth = (entry, screenWidth) => {
444
+ const width = Math.max(20, screenWidth || 80);
445
+ const inner = contentWidth(width);
446
+ const indent = visibleWidth(FULL_INDENT);
447
+ if (!entry) return Math.max(1, inner - indent);
448
+ const hashW = visibleWidth(entry.sha ?? '');
449
+ const cols = panelWidths(rightNeed(entry), hashW, inner);
450
+ return Math.max(1, cols.leftW);
451
+ };
452
+
453
+ const prefixWidth = (first, selected) => {
454
+ const mark = first && selected ? FILE_MARK : ' ';
455
+ const prefix = first ? ` ${mark} ` : FULL_INDENT;
456
+ return visibleWidth(prefix);
457
+ };
458
+
459
+ const messageCaret = (text, cursor, width, row, first, selected) => {
460
+ const pos = cursorInWrap(`${text ?? ''}`, cursor ?? 0, width);
461
+ if (pos.row !== row) return null;
462
+ return { x: prefixWidth(first, selected) + pos.col + 1 };
463
+ };
464
+
465
+ const pieceFg = (kind, tone) => {
466
+ if (kind === 'add') return THEME.addLineFg;
467
+ if (kind === 'del') return THEME.delLineFg;
468
+ if (tone.current) {
469
+ if (kind === 'hash') return THEME.shaDarkFg;
470
+ if (kind === 'refs') return THEME.warnFg;
471
+ if (kind === 'text' || kind === 'body') return THEME.mutedFg;
472
+ return tone.fgRgb;
473
+ }
474
+ if (kind === 'hash') return THEME.shaFg;
475
+ if (kind === 'author') return THEME.addLineFg;
476
+ if (kind === 'email') return THEME.mutedFg;
477
+ if (kind === 'refs') return THEME.warnFg;
478
+ if (kind === 'text') return tone.nameFg;
479
+ if (kind === 'body') return THEME.mutedFg;
480
+ return tone.fgRgb;
481
+ };
482
+
483
+ const clipParts = (parts, width, fallbackKind, tone) => {
484
+ const shown = [];
485
+ let used = 0;
486
+ for (const part of parts) {
487
+ const room = width - used;
488
+ if (room <= 0) break;
489
+ const text = truncateVisible(part.text ?? '', room);
490
+ if (!text) continue;
491
+ const kind = part.kind ?? fallbackKind;
492
+ shown.push({ text, fg: pieceFg(kind, tone) });
493
+ used += visibleWidth(text);
494
+ }
495
+ const pad = Math.max(0, width - used);
496
+ if (pad) shown.push({ text: ' '.repeat(pad), fg: tone.fgRgb });
497
+ return shown;
498
+ };
499
+
500
+ const paintPanelLine = (spec) => {
501
+ const tone = commitTone(spec.selected, spec.current);
502
+ const mark = spec.first && spec.selected ? FILE_MARK : ' ';
503
+ const prefix = spec.first ? ` ${mark} ` : FULL_INDENT;
504
+ const left = truncateVisible(spec.left, spec.leftW);
505
+ const leftPad = Math.max(0, spec.leftW - visibleWidth(left));
506
+ const gap = ' '.repeat(spec.gapW);
507
+ const right = Array.isArray(spec.right)
508
+ ? spec.right
509
+ : [{ text: spec.right ?? '', kind: spec.rightKind }];
510
+ return paintPieces(
511
+ [
512
+ { text: prefix, fg: tone.fgRgb },
513
+ { text: left, fg: pieceFg(spec.leftKind, tone) },
514
+ { text: `${' '.repeat(leftPad)}${gap}`, fg: tone.fgRgb },
515
+ ...clipParts(right, spec.rightW, spec.rightKind, tone),
516
+ ],
517
+ spec.width,
518
+ spec.color,
519
+ spec.selected,
520
+ spec.current,
521
+ );
522
+ };
523
+
524
+ const lineRecord = (commit, row, hit, cursor = null) => ({
525
+ commit,
526
+ row,
527
+ hit,
528
+ cursor,
529
+ });
530
+
531
+ const commitLines = (
532
+ entry,
533
+ index,
534
+ width,
535
+ color,
536
+ selected,
537
+ edit,
538
+ current,
539
+ diffWidths,
540
+ ) => {
541
+ const inner = contentWidth(width);
542
+ const hashW = visibleWidth(entry.sha ?? '');
543
+ const widths = diffWidths ?? entryCountWidth(entry);
544
+ const cols = panelWidths(rightNeed(entry, widths), hashW, inner);
545
+ const rights = rightRows(entry, cols.rightW, widths);
546
+ const left = panelLeft(entry, cols.leftW, edit);
547
+ const count = Math.max(left.length, rights.length);
548
+ const lines = [];
549
+ for (let row = 0; row < count; row++) {
550
+ const line = left[row] ?? { text: '', kind: 'text' };
551
+ const painted = paintPanelLine({
552
+ left: line.text,
553
+ right: rights[row] ?? [],
554
+ leftW: cols.leftW,
555
+ rightW: cols.rightW,
556
+ gapW: cols.gapW,
557
+ width,
558
+ color,
559
+ selected,
560
+ current,
561
+ first: row === 0,
562
+ leftKind: line.kind,
563
+ rightKind: 'meta',
564
+ });
565
+ const messageRow = entry.sha ? row - 1 : row;
566
+ const caret = edit
567
+ ? messageCaret(
568
+ edit.text,
569
+ edit.cursor,
570
+ cols.leftW,
571
+ messageRow,
572
+ !entry.sha && row === 0,
573
+ selected,
574
+ )
575
+ : null;
576
+ lines.push(lineRecord(index, painted, true, caret));
577
+ }
578
+ return lines;
579
+ };
580
+
581
+ const draftLines = (text, cursor, width, color) => {
582
+ const inner = contentWidth(width);
583
+ const leftW = Math.max(1, inner - visibleWidth(FULL_INDENT));
584
+ const parts = messagePartsFrom(text, leftW);
585
+ const lines = [];
586
+ for (let row = 0; row < parts.length; row++) {
587
+ const line = parts[row];
588
+ const painted = paintPanelLine({
589
+ left: line.text,
590
+ right: '',
591
+ leftW,
592
+ rightW: 0,
593
+ gapW: 0,
594
+ width,
595
+ color,
596
+ selected: true,
597
+ first: row === 0,
598
+ leftKind: line.kind,
599
+ rightKind: 'meta',
600
+ });
601
+ const caret = messageCaret(text, cursor, leftW, row, row === 0, true);
602
+ lines.push(lineRecord(-1, painted, false, caret));
603
+ }
604
+ return lines;
605
+ };
606
+
607
+ const composeEditsIndex = (compose, commits, index, cursor) => {
608
+ if (!compose || compose.kind !== 'commit') return false;
609
+ if (editsCursor(compose)) return index === cursor;
610
+ if (!editsPending(compose)) return false;
611
+ const at = pendingAt(commits);
612
+ return at >= 0 && index === at;
613
+ };
614
+
615
+ const composeShowsDraft = (compose, commits) => {
616
+ if (!compose || compose.kind !== 'commit') return false;
617
+ if (editsCursor(compose)) return false;
618
+ if (editsPending(compose)) return pendingAt(commits) < 0;
619
+ return true;
620
+ };
621
+
622
+ const gapLine = (width, color) =>
623
+ lineRecord(-1, paintBodyFill(width, color, 'files'), false);
624
+
625
+ const flattenCommits = (commits, cursor, width, color, compose, rev, short) => {
626
+ const lines = [];
627
+ const diffWidths = listCountWidth(commits);
628
+ const editing = compose && compose.kind === 'commit';
629
+ const pushGap = () => {
630
+ if (lines.length) lines.push(gapLine(width, color));
631
+ };
632
+ if (composeShowsDraft(compose, commits)) {
633
+ const draft = draftLines(
634
+ compose.text ?? '',
635
+ compose.cursor ?? 0,
636
+ width,
637
+ color,
638
+ );
639
+ for (const line of draft) lines.push(line);
640
+ }
641
+ for (let index = 0; index < commits.length; index++) {
642
+ pushGap();
643
+ const inplace = composeEditsIndex(compose, commits, index, cursor);
644
+ const selected = inplace || (!editing && index === cursor);
645
+ const edit = inplace
646
+ ? { text: compose.text ?? '', cursor: compose.cursor ?? 0 }
647
+ : null;
648
+ const block = commitLines(
649
+ commits[index],
650
+ index,
651
+ width,
652
+ color,
653
+ selected,
654
+ edit,
655
+ viewedCommit(commits[index], rev, short),
656
+ diffWidths,
657
+ );
658
+ for (const line of block) lines.push(line);
659
+ }
660
+ return lines;
661
+ };
662
+
663
+ const focusLine = (lines, cursor, compose) => {
664
+ if (compose && compose.kind === 'commit') {
665
+ for (let i = 0; i < lines.length; i++) {
666
+ if (lines[i].cursor) return i;
667
+ }
668
+ }
669
+ for (let i = 0; i < lines.length; i++) {
670
+ const line = lines[i];
671
+ if (line.hit && line.commit === cursor) return i;
672
+ }
673
+ return 0;
674
+ };
675
+
676
+ const paintLineWindow = (
677
+ lines,
678
+ focus,
679
+ width,
680
+ color,
681
+ bodyH,
682
+ headerLines,
683
+ offset,
684
+ ) => {
685
+ const pad = bodyH >= LIST_PAD * 2 + 1 ? LIST_PAD : 0;
686
+ const listH = bodyH - pad * 2;
687
+ const start = listWindowStart(focus, offset, listH, lines.length);
688
+ const blank = paintBodyFill(width, color, 'files');
689
+ const body = [];
690
+ const fileHits = [];
691
+ let textCursor = null;
692
+ if (pad) body.push(blank);
693
+ for (let i = 0; i < listH; i++) {
694
+ const line = lines[start + i];
695
+ if (!line) break;
696
+ body.push(line.row);
697
+ if (line.cursor) {
698
+ textCursor = { x: line.cursor.x, row: body.length - 1 };
699
+ }
700
+ if (!line.hit) continue;
701
+ const y = headerLines + pad + i + 1;
702
+ fileHits.push({ y, cursor: line.commit });
703
+ }
704
+ if (pad) body.push(blank);
705
+ return paneResult({
706
+ body,
707
+ fileHits,
708
+ cursor: textCursor,
709
+ listScroll: start,
710
+ });
711
+ };
712
+
713
+ const paintFullCommits = (view, width, color, bodyH, headerLines) => {
119
714
  const commits = view.commits ?? [];
120
- const cols = measureColumns(commits, commitRowFields, COMMIT_COL_ALIGN);
121
715
  const cursor = view.commitCursor ?? 0;
122
- const compose = view.compose;
123
- const extra = commitEditRow(compose, commits, cursor, width, color, cols);
716
+ const compose = view.compose ?? null;
717
+ const lines = flattenCommits(
718
+ commits,
719
+ cursor,
720
+ width,
721
+ color,
722
+ compose,
723
+ view.rev,
724
+ view.revShort,
725
+ );
726
+ const focus = focusLine(lines, cursor, compose);
727
+ return paintLineWindow(
728
+ lines,
729
+ focus,
730
+ width,
731
+ color,
732
+ bodyH,
733
+ headerLines,
734
+ view.listScroll,
735
+ );
736
+ };
737
+
738
+ const paintBriefCommits = (
739
+ commits,
740
+ cursor,
741
+ width,
742
+ color,
743
+ bodyH,
744
+ headerLines,
745
+ extra,
746
+ offset,
747
+ cols,
748
+ rev,
749
+ revShort,
750
+ ) => {
124
751
  const paintRow = (entry, rowWidth, rowColor, selected) =>
125
- paintCommitRow(entry, rowWidth, rowColor, selected, cols);
126
- const painted = paintCursorList(
752
+ paintCommitRow(
753
+ entry,
754
+ rowWidth,
755
+ rowColor,
756
+ selected,
757
+ cols,
758
+ viewedCommit(entry, rev, revShort),
759
+ );
760
+ return paintCursorList(
127
761
  commits,
128
762
  cursor,
129
763
  width,
@@ -132,16 +766,44 @@ const paintBodyCommits = (view, width, color, bodyH, headerLines) => {
132
766
  headerLines,
133
767
  paintRow,
134
768
  extra,
769
+ offset,
770
+ );
771
+ };
772
+
773
+ const paintBodyCommits = (view, width, color, bodyH, headerLines) => {
774
+ if (view.commitView === 'full') {
775
+ return paintFullCommits(view, width, color, bodyH, headerLines);
776
+ }
777
+ const commits = view.commits ?? [];
778
+ const cols = measureColumns(commits, commitRowFields, COMMIT_COL_ALIGN);
779
+ const cursor = view.commitCursor ?? 0;
780
+ const extra = commitEditRow(
781
+ view.compose,
782
+ commits,
783
+ cursor,
784
+ width,
785
+ color,
786
+ cols,
787
+ );
788
+ return paintBriefCommits(
789
+ commits,
790
+ cursor,
791
+ width,
792
+ color,
793
+ bodyH,
794
+ headerLines,
795
+ extra,
135
796
  view.listScroll,
797
+ cols,
798
+ view.rev,
799
+ view.revShort,
136
800
  );
137
- return paneResult({
138
- body: painted.body,
139
- fileHits: painted.fileHits,
140
- todoOwners: [],
141
- cursor: painted.cursor,
142
- splitBody: false,
143
- listScroll: painted.offset,
144
- });
145
801
  };
146
802
 
147
- module.exports = { paintBodyCommits };
803
+ module.exports = {
804
+ paintBodyCommits,
805
+ commitMessageWidth,
806
+ commitLine,
807
+ commitText,
808
+ splitMessage,
809
+ };