reslop 0.2.1 → 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.
@@ -2,77 +2,132 @@
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
119
  const paintCommitEdit = (compose, width, color, entry, cols) => {
59
- const text = `${compose.text ?? ''}`.replaceAll('\n', ' ');
120
+ const text = splitMessage(compose.text).line;
60
121
  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);
122
+ const shown = { ...entry, body: text, subject: text };
123
+ const row = paintCommitRow(shown, width, color, true, cols);
124
+ const inner = contentWidth(width);
69
125
  const prefix = ` ${FILE_MARK} `;
70
- const lead = ' ';
71
126
  const parts = padColumns(commitRowFields(entry), cols, COMMIT_COL_ALIGN);
72
127
  const suffix = joinColumns(parts, cols);
73
- const rest = Math.max(1, inner - visibleWidth(prefix + lead + suffix));
128
+ const rest = Math.max(1, inner - visibleWidth(prefix + suffix));
74
129
  const clipped = truncateVisible(text, rest);
75
- const prefixW = visibleWidth(prefix + lead);
130
+ const prefixW = visibleWidth(prefix);
76
131
  const col = Math.min(compose.cursor ?? 0, visibleWidth(clipped));
77
132
  return {
78
133
  row,
@@ -80,50 +135,514 @@ const paintCommitEdit = (compose, width, color, entry, cols) => {
80
135
  at: 'replace',
81
136
  };
82
137
  }
83
- const inner = Math.max(1, width - COMMIT_EDGE);
138
+ const inner = contentWidth(width);
84
139
  const prefix = ` ${FILE_MARK} `;
85
- const lead = ' ';
86
- const prefixW = visibleWidth(prefix + lead);
140
+ const prefixW = visibleWidth(prefix);
87
141
  const rest = Math.max(1, inner - prefixW);
88
142
  const clipped = truncateVisible(text, rest);
89
143
  const pad = Math.max(0, rest - visibleWidth(clipped));
90
144
  const gap = ' '.repeat(pad);
145
+ const tail = ' '.repeat(COMMIT_TAIL);
91
146
  const fgRgb = THEME.chromeFg;
92
147
  const bgRgb = THEME.buttonBg;
93
148
  const nameFg = THEME.buttonHotFg;
94
- const edge = fill(COMMIT_EDGE, fgRgb, THEME.ctxBg, color);
95
- let row = `${prefix}${lead}${clipped}${gap}${edge}`;
149
+ let row = `${prefix}${clipped}${gap}${tail}`;
96
150
  if (color) {
97
151
  row = `${seq(fgRgb, bgRgb)}${EL}`;
98
152
  row += paint(prefix, fgRgb, bgRgb, color);
99
- row += paint(lead, nameFg, bgRgb, color);
100
153
  row += paint(clipped, nameFg, bgRgb, color);
101
154
  row += paint(gap, fgRgb, bgRgb, color);
102
- row += edge;
155
+ row += paint(tail, fgRgb, bgRgb, color);
103
156
  }
104
157
  const col = Math.min(compose.cursor ?? 0, visibleWidth(clipped));
105
158
  return { row, cursor: { x: prefixW + col + 1 }, at: 'start' };
106
159
  };
107
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
+
108
174
  const commitEditRow = (compose, commits, cursor, width, color, cols) => {
109
175
  if (!compose || compose.kind !== 'commit') return null;
110
- if (compose.commitKind === 'amend' || compose.commitKind === 'reword') {
111
- const entry = commits[cursor];
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];
112
181
  if (!entry) return null;
113
- return paintCommitEdit(compose, width, color, entry, cols);
182
+ const painted = paintCommitEdit(compose, width, color, entry, cols);
183
+ painted.index = at;
184
+ return painted;
114
185
  }
115
186
  return paintCommitEdit(compose, width, color);
116
187
  };
117
188
 
118
- const paintBodyCommits = (view, width, color, bodyH, headerLines) => {
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) => {
119
599
  const commits = view.commits ?? [];
120
- const cols = measureColumns(commits, commitRowFields, COMMIT_COL_ALIGN);
121
600
  const cursor = view.commitCursor ?? 0;
122
- const compose = view.compose;
123
- const extra = commitEditRow(compose, commits, cursor, width, color, cols);
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
+ ) => {
124
636
  const paintRow = (entry, rowWidth, rowColor, selected) =>
125
- paintCommitRow(entry, rowWidth, rowColor, selected, cols);
126
- const painted = paintCursorList(
637
+ paintCommitRow(
638
+ entry,
639
+ rowWidth,
640
+ rowColor,
641
+ selected,
642
+ cols,
643
+ viewedCommit(entry, rev, revShort),
644
+ );
645
+ return paintCursorList(
127
646
  commits,
128
647
  cursor,
129
648
  width,
@@ -132,8 +651,33 @@ const paintBodyCommits = (view, width, color, bodyH, headerLines) => {
132
651
  headerLines,
133
652
  paintRow,
134
653
  extra,
135
- view.listScroll,
654
+ offset,
136
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
+ );
137
681
  return paneResult({
138
682
  body: painted.body,
139
683
  fileHits: painted.fileHits,
@@ -144,4 +688,10 @@ const paintBodyCommits = (view, width, color, bodyH, headerLines) => {
144
688
  });
145
689
  };
146
690
 
147
- module.exports = { paintBodyCommits };
691
+ module.exports = {
692
+ paintBodyCommits,
693
+ commitMessageWidth,
694
+ commitLine,
695
+ commitText,
696
+ splitMessage,
697
+ };