reslop 0.1.1 → 0.1.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.
package/lib/render.js CHANGED
@@ -2,20 +2,19 @@
2
2
 
3
3
  const ansi = require('./ansi.js');
4
4
  const { displayLines } = require('./diff.js');
5
- const { tokenize, overlayTokens } = require('./highlight.js');
5
+ const highlight = require('./highlight.js');
6
6
  const { detectLang } = require('./detect.js');
7
7
  const keys = require('./keys.js');
8
- const { ACTIONS, actionHot, actionLetter, buttonWord } = keys;
9
- const { FILES_DISABLED } = keys;
10
8
  const { overlayRows } = require('./select.js');
11
9
  const { itemPath } = require('./files.js');
12
- const { isDoneMark } = require('./review.js');
13
10
  const wrap = require('./wrap.js');
14
- const { wrapPlain, wrapDoc, wrapMultiline, cursorInWrap, wrapMove } = wrap;
15
11
 
16
- const { THEME, CODE_FG, codeFg, paint, visibleWidth, truncateVisible, fg } =
17
- ansi;
18
- const { RESET, ESC, EL, seq } = ansi;
12
+ const { tokenize, overlayTokens } = highlight;
13
+ const { ACTIONS, ACTION_IDS, actionLetter, buttonWord } = keys;
14
+ const { FILES_DISABLED } = keys;
15
+ const { wrapPlain, wrapMultiline, cursorInWrap } = wrap;
16
+ const { THEME, CODE_FG, codeFg, paint, visibleWidth } = ansi;
17
+ const { truncateVisible, fg, RESET, ESC, EL, seq } = ansi;
19
18
 
20
19
  const STYLES = {
21
20
  del: {
@@ -32,6 +31,27 @@ const STYLES = {
32
31
  chBg: THEME.addCharBg,
33
32
  mark: '+',
34
33
  },
34
+ warn: {
35
+ baseFg: THEME.delLineFg,
36
+ baseBg: THEME.delLineBg,
37
+ chFg: THEME.delCharFg,
38
+ chBg: THEME.delCharBg,
39
+ mark: ' ',
40
+ },
41
+ note: {
42
+ baseFg: THEME.chromeFg,
43
+ baseBg: THEME.noteBg,
44
+ chFg: THEME.chromeFg,
45
+ chBg: THEME.noteBg,
46
+ mark: ' ',
47
+ },
48
+ noteSep: {
49
+ baseFg: THEME.mutedFg,
50
+ baseBg: THEME.chromeBg,
51
+ chFg: THEME.mutedFg,
52
+ chBg: THEME.chromeBg,
53
+ mark: ' ',
54
+ },
35
55
  ctx: {
36
56
  baseFg: THEME.ctxFg,
37
57
  baseBg: THEME.ctxBg,
@@ -56,6 +76,15 @@ const STAGED_STYLES = {
56
76
  chBg: THEME.stagedAddCharBg,
57
77
  mark: '+',
58
78
  },
79
+ warn: {
80
+ baseFg: THEME.stagedDelLineFg,
81
+ baseBg: THEME.stagedDelLineBg,
82
+ chFg: THEME.stagedDelCharFg,
83
+ chBg: THEME.stagedDelCharBg,
84
+ mark: ' ',
85
+ },
86
+ note: STYLES.note,
87
+ noteSep: STYLES.noteSep,
59
88
  ctx: STYLES.ctx,
60
89
  };
61
90
 
@@ -74,9 +103,18 @@ const FILE_STATUS_FG = {
74
103
  partial: THEME.warnFg,
75
104
  };
76
105
 
106
+ const FILE_COL_KEYS = ['add', 'del', 'status', 'n', 'm'];
107
+
108
+ const FILE_COL_ALIGN = {
109
+ add: 'start',
110
+ del: 'start',
111
+ status: 'start',
112
+ n: 'start',
113
+ m: 'end',
114
+ };
115
+
77
116
  const INFO_STATUS = [
78
117
  'copied',
79
- 'skipped',
80
118
  'staged',
81
119
  'unstaged',
82
120
  'not staged',
@@ -85,7 +123,6 @@ const INFO_STATUS = [
85
123
  'read only',
86
124
  'last block',
87
125
  'first block',
88
- 'all skipped',
89
126
  'first file',
90
127
  'last file',
91
128
  'nothing to review',
@@ -107,10 +144,14 @@ const BUTTON_GAP = ' ';
107
144
  const LOGO = '👁️';
108
145
  const FILE_MARK = '▶';
109
146
  const FILE_LIST_PAD = 1;
147
+ const CHECK_DONE = 'xX';
110
148
  const TODO_CAPTION = 'TODO:';
111
149
  const INPUT_MAX = 4;
112
150
  const NOTE_MAX = 4;
113
151
  const NOTE_PAD = 1;
152
+ const SOFT_WRAP = ['warn', 'note'];
153
+ const DEL_CHAR_BG = ansi.bg(THEME.delCharBg);
154
+ const ADD_CHAR_BG = ansi.bg(THEME.addCharBg);
114
155
 
115
156
  const lineStyles = (origin) => (origin === 'staged' ? STAGED_STYLES : STYLES);
116
157
 
@@ -137,7 +178,32 @@ const paintSpans = (pieces, lineStyle, budget, color) => {
137
178
  return { text: out, used };
138
179
  };
139
180
 
181
+ const paintAuditNote = (line, width, color) => {
182
+ const style = STYLES.note;
183
+ const gutterW = Math.min(GUTTER_W, Math.max(0, width));
184
+ const label = `${style.mark} `.slice(0, gutterW);
185
+ const gutter = paint(label, style.baseFg, style.baseBg, color);
186
+ const budget = Math.max(0, width - gutterW);
187
+ const text = truncateVisible(line.text ?? '', budget);
188
+ const used = visibleWidth(text);
189
+ let out = gutter;
190
+ if (text) out += paint(text, style.baseFg, style.baseBg, color);
191
+ if (used < budget) {
192
+ out += fill(budget - used, style.baseFg, style.baseBg, color);
193
+ }
194
+ return out;
195
+ };
196
+
197
+ const paintAuditRule = (width, color) => {
198
+ const style = STYLES.noteSep;
199
+ const n = Math.max(0, width);
200
+ const bar = '─'.repeat(n);
201
+ return paint(bar, style.baseFg, style.baseBg, color);
202
+ };
203
+
140
204
  const paintDiffLine = (line, width, color, origin, lang) => {
205
+ if (line.type === 'noteSep') return paintAuditRule(width, color);
206
+ if (line.type === 'note') return paintAuditNote(line, width, color);
141
207
  const styles = lineStyles(origin);
142
208
  const style = styles[line.type] ?? styles.ctx;
143
209
  const diffSpans = line.spans ?? [{ text: line.text, changed: false }];
@@ -183,19 +249,14 @@ const paintSplitBlank = (width, color) => {
183
249
  );
184
250
  };
185
251
 
186
- const FILL_KIND = {
187
- files: (width, color) => {
252
+ const paintBodyFill = (width, color, kind) => {
253
+ if (kind === 'files') {
188
254
  const inner = Math.max(1, width - 1);
189
255
  const main = fill(inner, THEME.ctxFg, THEME.ctxBg, color);
190
256
  const edge = fill(1, THEME.ctxFg, THEME.ctxBg, color);
191
257
  return main + edge;
192
- },
193
- split: (width, color) => paintSplitBlank(width, color),
194
- };
195
-
196
- const paintBodyFill = (width, color, kind) => {
197
- const paint = FILL_KIND[kind];
198
- if (paint) return paint(width, color);
258
+ }
259
+ if (kind === 'split') return paintSplitBlank(width, color);
199
260
  return fill(width, THEME.ctxFg, THEME.ctxBg, color);
200
261
  };
201
262
 
@@ -215,22 +276,18 @@ const splitCheck = (text) => {
215
276
  if (!match) return null;
216
277
  const start = match.index;
217
278
  const token = match[0];
218
- return {
219
- before: text.slice(0, start),
220
- mark: match[1],
221
- token,
222
- after: text.slice(start + token.length),
223
- };
279
+ const before = text.slice(0, start);
280
+ const mark = match[1];
281
+ const after = text.slice(start + token.length);
282
+ return { before, mark, token, after };
224
283
  };
225
284
 
226
285
  const paintCheckBar = (text, width, fgRgb, bgRgb, color) => {
227
286
  const clipped = truncateVisible(text, width);
287
+ const parts = color ? splitCheck(clipped) : null;
288
+ if (!parts) return paintBar(text, width, fgRgb, bgRgb, color);
228
289
  const pad = Math.max(0, width - visibleWidth(clipped));
229
- const filled = clipped + ' '.repeat(pad);
230
- if (!color) return filled;
231
- const parts = splitCheck(clipped);
232
- if (!parts) return `${seq(fgRgb, bgRgb)}${EL}${filled}${RESET}`;
233
- const done = isDoneMark(parts.mark);
290
+ const done = CHECK_DONE.includes(parts.mark);
234
291
  const chipFg = done ? THEME.checkDoneFg : THEME.checkFg;
235
292
  const chipBg = done ? THEME.checkDoneBg : THEME.checkBg;
236
293
  const after = `${parts.after}${' '.repeat(pad)}`;
@@ -241,10 +298,15 @@ const paintCheckBar = (text, width, fgRgb, bgRgb, color) => {
241
298
  return out;
242
299
  };
243
300
 
244
- const originLabel = (origin, revShort, sourceLabel) => {
245
- if (origin === 'commit') return revShort || 'commit';
246
- if (origin === 'pr') return sourceLabel || 'pr';
247
- if (origin === 'todo') return 'todo';
301
+ const ORIGIN_LABEL = {
302
+ commit: (view) => view.revShort || 'commit',
303
+ pr: (view) => view.sourceLabel || 'pr',
304
+ todo: () => 'todo',
305
+ };
306
+
307
+ const originLabel = (origin, view) => {
308
+ const named = ORIGIN_LABEL[origin];
309
+ if (named) return named(view);
248
310
  return origin ?? 'unknown';
249
311
  };
250
312
 
@@ -281,7 +343,7 @@ const headerTarget = (view) => {
281
343
  if (!view.item) return { path: '', origin: '', n: 0, total: view.total ?? 0 };
282
344
  return {
283
345
  path: itemPath(view.item),
284
- origin: originLabel(view.item.origin, view.revShort, view.sourceLabel),
346
+ origin: originLabel(view.item.origin, view),
285
347
  n: view.index + 1,
286
348
  total: view.total,
287
349
  };
@@ -295,17 +357,20 @@ const headerPrefix = (view) => {
295
357
  return ` ${LOGO} reslop: `;
296
358
  };
297
359
 
360
+ const headerSuffix = ({ origin, n, total }) =>
361
+ origin ? ` ${origin} ${n}/${total}` : ` ${n}/${total}`;
362
+
298
363
  const headerText = (view) => {
299
364
  const prefix = headerPrefix(view);
300
- const { path, origin, n, total } = headerTarget(view);
301
- const suffix = origin ? ` ${origin} ${n}/${total}` : ` ${n}/${total}`;
302
- return `${prefix}${path}${suffix}`;
365
+ const target = headerTarget(view);
366
+ return `${prefix}${target.path}${headerSuffix(target)}`;
303
367
  };
304
368
 
305
369
  const headerPieces = (view, width) => {
306
370
  const repo = view.repoName || '';
307
- const { path, origin, n, total } = headerTarget(view);
308
- const suffix = origin ? ` ${origin} ${n}/${total}` : ` ${n}/${total}`;
371
+ const target = headerTarget(view);
372
+ const path = target.path;
373
+ const suffix = headerSuffix(target);
309
374
  const right = ' ';
310
375
  const chrome = ` ${LOGO} reslop: `;
311
376
  const prefixW = visibleWidth(headerPrefix(view));
@@ -336,7 +401,7 @@ const headerPieces = (view, width) => {
336
401
  const paintHeader = (view, width, color) => {
337
402
  const pieces = headerPieces(view, width);
338
403
  if (!color) {
339
- return pieces.map((p) => p.text).join('');
404
+ return pieces.map((piece) => piece.text).join('');
340
405
  }
341
406
  const bgRgb = THEME.headerBg;
342
407
  let out = `${seq(THEME.headerChromeFg, bgRgb)}${EL}`;
@@ -355,7 +420,8 @@ const bodyLines = (item, layout) => {
355
420
  return [{ type: 'ctx', text, spans: [{ text, changed: false }] }];
356
421
  }
357
422
  if (!item.hunk) return [];
358
- return displayLines(item.hunk, item.blockId, layout);
423
+ const radius = item.dep ? Number.MAX_SAFE_INTEGER : undefined;
424
+ return displayLines(item.hunk, item.blockId, layout, radius);
359
425
  };
360
426
 
361
427
  const sourceEmpty = (line) => !line || !(line.text ?? '').trim();
@@ -373,26 +439,6 @@ const dropLeadingEmpty = (lines, split) => {
373
439
  return i ? lines.slice(i) : lines;
374
440
  };
375
441
 
376
- const helpLines = () => {
377
- const lines = [' keys and buttons', ''];
378
- for (const action of ACTIONS) {
379
- const keys = actionHot(action);
380
- let label = action.label;
381
- if (action.id === 'layout') {
382
- label = 'Mode (unified / mixed / side)';
383
- }
384
- lines.push(` ${keys.padEnd(16)} ${label}`);
385
- }
386
- lines.push(' enter open file / save note');
387
- lines.push(' esc files, then quit');
388
- lines.push(' del, backspace remove todo');
389
- lines.push(' j, k, wheel scroll / move');
390
- lines.push(' drag copy');
391
- lines.push(' ctrl-z, ctrl-y undo / redo');
392
- lines.push(' ?, h help');
393
- return lines;
394
- };
395
-
396
442
  const BUTTON_MARK = {
397
443
  bgRgb: THEME.buttonBg,
398
444
  fgRgb: THEME.buttonFg,
@@ -429,15 +475,16 @@ const layoutButtons = (width, compact = false, disabled = []) => {
429
475
  const hits = [];
430
476
  const parts = [];
431
477
  let x = 0;
432
- for (const action of ACTIONS) {
478
+ for (const id of ACTION_IDS) {
479
+ const action = { id, ...ACTIONS[id] };
433
480
  const letter = actionLetter(action);
434
481
  const word = buttonWord(action);
435
482
  const label = short ? letter : word;
436
483
  const piece = `${BUTTON_GAP}${label}`;
437
484
  const w = visibleWidth(piece);
438
485
  if (x + w > width && parts.length) break;
439
- const isDisabled = disabled.includes(action.id);
440
- if (!isDisabled) hits.push({ id: action.id, x0: x, x1: x + w });
486
+ const isDisabled = disabled.includes(id);
487
+ if (!isDisabled) hits.push({ id, x0: x, x1: x + w });
441
488
  parts.push({ action, piece, letter, label, disabled: isDisabled });
442
489
  x += w;
443
490
  }
@@ -445,7 +492,7 @@ const layoutButtons = (width, compact = false, disabled = []) => {
445
492
  };
446
493
  if (compact) return build(true);
447
494
  const words = build(false);
448
- if (words.parts.length === ACTIONS.length) return words;
495
+ if (words.parts.length === ACTION_IDS.length) return words;
449
496
  return build(true);
450
497
  };
451
498
 
@@ -467,26 +514,38 @@ const paintButtons = (layout, width, color) => {
467
514
  return out;
468
515
  };
469
516
 
517
+ const COUNTS_KIND = {
518
+ pr: ({ counts, notes, sourceLabel }) => {
519
+ const n = counts.pr ?? 0;
520
+ const label = sourceLabel || 'pr';
521
+ return ` pr ${label} ${n} ${notes}`;
522
+ },
523
+ commit: ({ counts, notes, revShort }) => {
524
+ const n = counts.commit ?? 0;
525
+ return ` commit ${revShort} ${n} ${notes}`;
526
+ },
527
+ worktree: ({ counts, notes }) => {
528
+ const staged = counts.staged ?? 0;
529
+ const unstaged = counts.unstaged ?? 0;
530
+ const untracked = counts.untracked ?? 0;
531
+ const gitCounts = `unstaged ${unstaged} untracked ${untracked}`;
532
+ const git = `staged ${staged} ${gitCounts}`;
533
+ return ` ${git} ${notes}`;
534
+ },
535
+ };
536
+
537
+ const countsKind = (sourceKind, revShort) => {
538
+ if (sourceKind === 'pr') return 'pr';
539
+ if (revShort) return 'commit';
540
+ return 'worktree';
541
+ };
542
+
470
543
  const countsLine = (counts, revShort, sourceKind, sourceLabel) => {
471
- const skipped = counts.skipped ?? 0;
472
544
  const feedback = counts.feedback ?? 0;
473
545
  const todo = counts.todo ?? 0;
474
546
  const notes = `feedback ${feedback} todo ${todo}`;
475
- const extra = skipped ? ` skipped ${skipped}` : '';
476
- if (sourceKind === 'pr') {
477
- const n = counts.pr ?? 0;
478
- const label = sourceLabel || 'pr';
479
- return ` pr ${label} ${n} ${notes}${extra}`;
480
- }
481
- if (revShort) {
482
- const n = counts.commit ?? 0;
483
- return ` commit ${revShort} ${n} ${notes}${extra}`;
484
- }
485
- const staged = counts.staged ?? 0;
486
- const unstaged = counts.unstaged ?? 0;
487
- const untracked = counts.untracked ?? 0;
488
- const git = `staged ${staged} unstaged ${unstaged} untracked ${untracked}`;
489
- return ` ${git} ${notes}${extra}`;
547
+ const kind = countsKind(sourceKind, revShort);
548
+ return COUNTS_KIND[kind]({ counts, notes, sourceLabel, revShort });
490
549
  };
491
550
 
492
551
  const paintQuitPrompt = (width, color) => {
@@ -526,23 +585,36 @@ const paintStatusLine = (view, status, width, color) => {
526
585
  );
527
586
  };
528
587
 
588
+ const signedCount = (sign, n) => `${sign}${n ?? 0}`;
589
+
590
+ const fileRowFields = (entry) => {
591
+ const { added, removed, staged, remaining } = entry;
592
+ const add = signedCount('+', added);
593
+ const del = signedCount('-', removed);
594
+ const status = entry.status ?? '';
595
+ const n = `${staged ?? 0}`;
596
+ const m = `${remaining ?? 0}`;
597
+ return { add, del, status, n, m };
598
+ };
599
+
529
600
  const fileColumns = (files) => {
530
- let statusW = 0;
531
- let countW = 0;
601
+ const widths = {};
602
+ for (const key of FILE_COL_KEYS) widths[key] = 0;
532
603
  for (const entry of files) {
533
- const status = entry.status ?? '';
534
- const count = `${entry.remaining ?? 0}`;
535
- const sw = visibleWidth(status);
536
- const cw = visibleWidth(count);
537
- if (sw > statusW) statusW = sw;
538
- if (cw > countW) countW = cw;
604
+ const fields = fileRowFields(entry);
605
+ for (const key of FILE_COL_KEYS) {
606
+ const w = visibleWidth(fields[key]);
607
+ if (w > widths[key]) widths[key] = w;
608
+ }
539
609
  }
540
- return { statusW, countW };
610
+ return widths;
541
611
  };
542
612
 
543
- const padStartVisible = (text, width) => {
613
+ const padVisible = (text, width, align) => {
544
614
  const n = Math.max(0, width - visibleWidth(text));
545
- return `${' '.repeat(n)}${text}`;
615
+ const gap = ' '.repeat(n);
616
+ if (align === 'end') return `${text}${gap}`;
617
+ return `${gap}${text}`;
546
618
  };
547
619
 
548
620
  const fileRowFg = (status, selected) => {
@@ -552,25 +624,55 @@ const fileRowFg = (status, selected) => {
552
624
  return THEME.mutedFg;
553
625
  };
554
626
 
627
+ const fileRowParts = (entry, cols) => {
628
+ const fields = fileRowFields(entry);
629
+ const parts = {};
630
+ for (const key of FILE_COL_KEYS) {
631
+ const width = cols?.[key] ?? visibleWidth(fields[key]);
632
+ const align = FILE_COL_ALIGN[key];
633
+ parts[key] = padVisible(fields[key], width, align);
634
+ }
635
+ return parts;
636
+ };
637
+
638
+ const joinFileSuffix = (parts) => {
639
+ const { add, del, status, n, m } = parts;
640
+ return ` ${add} ${del} ${status} ${n}/${m}`;
641
+ };
642
+
643
+ const paintFileSuffix = (parts, fgRgb, bgRgb, color) => {
644
+ const { add, del, status, n, m } = parts;
645
+ const gap = paint(' ', fgRgb, bgRgb, color);
646
+ const addText = paint(add, THEME.addLineFg, bgRgb, color);
647
+ const delText = paint(del, THEME.delLineFg, bgRgb, color);
648
+ const statusText = paint(status, fgRgb, bgRgb, color);
649
+ const nText = paint(n, fgRgb, bgRgb, color);
650
+ const slash = paint('/', fgRgb, bgRgb, color);
651
+ const mText = paint(m, fgRgb, bgRgb, color);
652
+ const counts = addText + gap + delText;
653
+ const meta = statusText + gap + nText + slash + mText;
654
+ return gap + counts + gap + meta;
655
+ };
656
+
555
657
  const paintFileRow = (entry, width, color, selected, cols) => {
556
658
  const inner = Math.max(1, width - 1);
557
659
  const mark = selected ? FILE_MARK : ' ';
558
660
  const prefix = ` ${mark} `;
559
- const status = entry.status ?? '';
560
- const count = `${entry.remaining ?? 0}`;
561
- const statusW = cols?.statusW ?? visibleWidth(status);
562
- const countW = cols?.countW ?? visibleWidth(count);
563
- const statusCol = padStartVisible(status, statusW);
564
- const countCol = padStartVisible(count, countW);
565
- const suffix = ` ${statusCol} ${countCol}`;
661
+ const parts = fileRowParts(entry, cols);
662
+ const suffix = joinFileSuffix(parts);
566
663
  const pathW = Math.max(1, inner - visibleWidth(prefix + suffix));
567
- const clipped = truncateVisible(entry.path, pathW);
664
+ const { path, status } = entry;
665
+ const clipped = truncateVisible(path, pathW);
568
666
  const pad = Math.max(0, pathW - visibleWidth(clipped));
569
- const line = `${prefix}${clipped}${' '.repeat(pad)}${suffix}`;
667
+ const left = `${prefix}${clipped}${' '.repeat(pad)}`;
570
668
  const fgRgb = fileRowFg(status, selected);
571
669
  const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
572
- const row = paintBar(line, inner, fgRgb, bgRgb, color);
573
- return row + fill(1, fgRgb, bgRgb, color);
670
+ const edge = fill(1, fgRgb, bgRgb, color);
671
+ if (!color) return `${left}${suffix}${edge}`;
672
+ let out = `${seq(fgRgb, bgRgb)}${EL}`;
673
+ out += paint(left, fgRgb, bgRgb, color);
674
+ out += paintFileSuffix(parts, fgRgb, bgRgb, color);
675
+ return out + edge;
574
676
  };
575
677
 
576
678
  const todoInnerWidth = (width) => Math.max(1, width - 2);
@@ -669,8 +771,7 @@ const paintIdleNote = (view, width, color) => {
669
771
  const text = view.noteText;
670
772
  if (!text) return empty;
671
773
  const inner = noteInnerWidth(width);
672
- const prefix = view.noteKind ? `${view.noteKind}: ` : '';
673
- const wrapped = wrapMultiline(`${prefix}${text}`, inner);
774
+ const wrapped = wrapMultiline(`feedback: ${text}`, inner);
674
775
  const rows = [];
675
776
  for (const line of wrapped.slice(0, NOTE_MAX)) {
676
777
  const painted = paintNoteLine(
@@ -688,7 +789,7 @@ const paintIdleNote = (view, width, color) => {
688
789
 
689
790
  const paintNotePanel = (view, width, color, cap) => {
690
791
  const empty = { rows: [], cursor: null, templateHits: [] };
691
- if (view.help || (view.pane ?? 'diff') === 'files') return empty;
792
+ if ((view.pane ?? 'diff') === 'files') return empty;
692
793
  const compose = view.compose;
693
794
  if (compose && compose.kind === 'feedback') {
694
795
  return paintComposeNote(view, width, color, cap);
@@ -696,16 +797,6 @@ const paintNotePanel = (view, width, color, cap) => {
696
797
  return paintIdleNote(view, width, color);
697
798
  };
698
799
 
699
- const paintBodyHelp = (width, color) => {
700
- const body = [];
701
- for (const line of helpLines()) {
702
- for (const row of wrapPlain(line, width)) {
703
- body.push(paintBar(row, width, THEME.chromeFg, THEME.ctxBg, color));
704
- }
705
- }
706
- return body;
707
- };
708
-
709
800
  const paintBodyFiles = (view, width, color, bodyH, headerLines) => {
710
801
  const files = view.files ?? [];
711
802
  const cols = fileColumns(files);
@@ -753,10 +844,39 @@ const paintBodyTodo = (view, width, color) => {
753
844
  return { body, todoOwners, cursor };
754
845
  };
755
846
 
847
+ const wrapSoftLine = (line, width) => {
848
+ const gutterW = Math.min(GUTTER_W, Math.max(0, width));
849
+ const budget = Math.max(1, width - gutterW);
850
+ const rows = wrapPlain(line.text ?? '', budget);
851
+ return rows.map((text) => {
852
+ const spans = [{ text, changed: false }];
853
+ return { ...line, text, spans };
854
+ });
855
+ };
856
+
857
+ const expandSoftRows = (lines, width, split) => {
858
+ const wrapWidth = split ? splitWidths(width).leftW : width;
859
+ const out = [];
860
+ for (const row of lines) {
861
+ const line = split ? row.left || row.right : row;
862
+ if (!line || !SOFT_WRAP.includes(line.type)) {
863
+ out.push(row);
864
+ continue;
865
+ }
866
+ const parts = wrapSoftLine(line, wrapWidth);
867
+ for (const part of parts) {
868
+ if (split) out.push({ left: part, right: part });
869
+ else out.push(part);
870
+ }
871
+ }
872
+ return out;
873
+ };
874
+
756
875
  const paintBodyDiff = (item, layout, width, color) => {
757
876
  const lang = detectLang(itemPath(item));
758
877
  const splitBody = layout === 'side' && !item.file.isBinary;
759
- const lines = dropLeadingEmpty(bodyLines(item, layout), splitBody);
878
+ const raw = dropLeadingEmpty(bodyLines(item, layout), splitBody);
879
+ const lines = expandSoftRows(raw, width, splitBody);
760
880
  const body = [];
761
881
  if (splitBody) {
762
882
  for (const row of lines) {
@@ -771,7 +891,6 @@ const paintBodyDiff = (item, layout, width, color) => {
771
891
  };
772
892
 
773
893
  const bodyKind = (view) => {
774
- if (view.help) return 'help';
775
894
  if ((view.pane ?? 'diff') === 'files') return 'files';
776
895
  if (!view.item) return 'empty';
777
896
  if (view.item.origin === 'todo') return 'todo';
@@ -779,11 +898,6 @@ const bodyKind = (view) => {
779
898
  };
780
899
 
781
900
  const BODY_PAINT = {
782
- help: (view, width, color) => ({
783
- body: paintBodyHelp(width, color),
784
- fileHits: [],
785
- todoOwners: [],
786
- }),
787
901
  files: (view, width, color, bodyH, headerLines) => ({
788
902
  ...paintBodyFiles(view, width, color, bodyH, headerLines),
789
903
  todoOwners: [],
@@ -837,10 +951,10 @@ const windowBody = (view, width, color, bodyH, headerLines, paintedBody) => {
837
951
  const todoOwners = paintedBody.todoOwners;
838
952
  const splitBody = paintedBody.splitBody === true;
839
953
  const pane = view.pane ?? 'diff';
840
- const listPane = pane === 'files' && !view.help;
954
+ const listPane = pane === 'files';
841
955
  const fillKind = bodyFillKind(listPane, splitBody);
842
956
  let shift = 0;
843
- if (!listPane && !view.help && body.length > 0) {
957
+ if (!listPane && body.length > 0) {
844
958
  body.unshift(paintBodyFill(width, color, fillKind));
845
959
  if (todoOwners.length) todoOwners.unshift(null);
846
960
  shift = 1;
@@ -889,7 +1003,7 @@ const stackFrame = (view, width, color, rows, layout, windowed, notes) => {
889
1003
  return overlayRows(rows, view.selection);
890
1004
  };
891
1005
 
892
- const frameResult = (text, painted, layout, windowed, hits) => {
1006
+ const frameResult = (text, painted, layout, windowed, hits, bodyH) => {
893
1007
  const fileHits = windowed.fileHits;
894
1008
  const todoHits = windowed.todoHits;
895
1009
  const templateHits = hits.templateHits;
@@ -907,6 +1021,7 @@ const frameResult = (text, painted, layout, windowed, hits) => {
907
1021
  height: painted.length,
908
1022
  scroll,
909
1023
  cursor,
1024
+ bodyH,
910
1025
  };
911
1026
  };
912
1027
 
@@ -938,7 +1053,7 @@ const renderFrame = (view, options = {}) => {
938
1053
  notes.noteCursor,
939
1054
  bodyH,
940
1055
  );
941
- return frameResult(text, painted, layout, windowed, hits);
1056
+ return frameResult(text, painted, layout, windowed, hits, bodyH);
942
1057
  };
943
1058
 
944
1059
  const presentCursor = (cursor) => {
@@ -961,36 +1076,18 @@ const presentRows = (rows, options = {}) => {
961
1076
  return parts.join('');
962
1077
  };
963
1078
 
964
- const DEL_CHAR_BG = ansi.bg(THEME.delCharBg);
965
- const ADD_CHAR_BG = ansi.bg(THEME.addCharBg);
966
- const DEL_LINE_FG = ansi.fg(THEME.delLineFg);
967
- const ADD_LINE_FG = ansi.fg(THEME.addLineFg);
968
-
969
1079
  module.exports = {
970
- LOGO,
971
- FILE_MARK,
972
1080
  QUIT_FINISH,
973
1081
  QUIT_CONTINUE,
974
1082
  QUIT_PROMPT,
975
- STYLES,
976
- STAGED_STYLES,
977
- renderFrame,
978
- presentCursor,
979
- presentRows,
980
- layoutButtons,
1083
+ DEL_CHAR_BG,
1084
+ ADD_CHAR_BG,
981
1085
  paintDiffLine,
982
- paintSplitRow,
983
- bodyLines,
984
1086
  headerText,
985
- wrapPlain,
986
- wrapDoc,
987
- wrapMultiline,
988
- cursorInWrap,
989
- wrapMove,
990
- noteInnerWidth,
1087
+ layoutButtons,
991
1088
  todoInnerWidth,
992
- DEL_CHAR_BG,
993
- ADD_CHAR_BG,
994
- DEL_LINE_FG,
995
- ADD_LINE_FG,
1089
+ noteInnerWidth,
1090
+ renderFrame,
1091
+ presentCursor,
1092
+ presentRows,
996
1093
  };