reslop 0.2.0 → 0.2.3

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