reslop 0.1.0 → 0.1.2

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,18 +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
- const { ACTIONS, actionHot, actionLetter, buttonWord } = require('./keys.js');
7
+ const keys = require('./keys.js');
8
8
  const { overlayRows } = require('./select.js');
9
9
  const { itemPath } = require('./files.js');
10
- const { isDoneMark } = require('./review.js');
11
10
  const wrap = require('./wrap.js');
12
- const { wrapPlain, wrapDoc, wrapMultiline, cursorInWrap, wrapMove } = wrap;
13
11
 
14
- const { THEME, CODE_FG, codeFg, paint, visibleWidth, truncateVisible, fg } =
15
- ansi;
16
- 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;
17
18
 
18
19
  const STYLES = {
19
20
  del: {
@@ -30,6 +31,27 @@ const STYLES = {
30
31
  chBg: THEME.addCharBg,
31
32
  mark: '+',
32
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
+ },
33
55
  ctx: {
34
56
  baseFg: THEME.ctxFg,
35
57
  baseBg: THEME.ctxBg,
@@ -54,20 +76,35 @@ const STAGED_STYLES = {
54
76
  chBg: THEME.stagedAddCharBg,
55
77
  mark: '+',
56
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,
57
88
  ctx: STYLES.ctx,
58
89
  };
59
90
 
60
- const HEADER_ROLE_FG = Object.assign(Object.create(null), {
91
+ const HEADER_ROLE_FG = {
61
92
  repo: THEME.headerRepoFg,
62
93
  dir: THEME.headerDirFg,
63
94
  slash: THEME.headerSlashFg,
64
95
  file: THEME.headerFileFg,
65
96
  meta: THEME.headerMetaFg,
66
- });
97
+ };
98
+
99
+ const FILE_STATUS_FG = {
100
+ unstaged: THEME.delLineFg,
101
+ staged: THEME.addLineFg,
102
+ untracked: THEME.mutedFg,
103
+ partial: THEME.warnFg,
104
+ };
67
105
 
68
106
  const INFO_STATUS = [
69
107
  'copied',
70
- 'skipped',
71
108
  'staged',
72
109
  'unstaged',
73
110
  'not staged',
@@ -76,7 +113,6 @@ const INFO_STATUS = [
76
113
  'read only',
77
114
  'last block',
78
115
  'first block',
79
- 'all skipped',
80
116
  'first file',
81
117
  'last file',
82
118
  'nothing to review',
@@ -96,12 +132,16 @@ const QUIT_PROMPT = ` ${QUIT_FINISH_LABEL} ${QUIT_CONTINUE_LABEL}`;
96
132
  const GUTTER_W = 2;
97
133
  const BUTTON_GAP = ' ';
98
134
  const LOGO = '👁️';
99
- const FILE_CURSOR = '▷';
100
- const FILE_CURRENT = '▶';
135
+ const FILE_MARK = '▶';
136
+ const FILE_LIST_PAD = 1;
137
+ const CHECK_DONE = 'xX';
101
138
  const TODO_CAPTION = 'TODO:';
102
139
  const INPUT_MAX = 4;
103
140
  const NOTE_MAX = 4;
104
141
  const NOTE_PAD = 1;
142
+ const SOFT_WRAP = ['warn', 'note'];
143
+ const DEL_CHAR_BG = ansi.bg(THEME.delCharBg);
144
+ const ADD_CHAR_BG = ansi.bg(THEME.addCharBg);
105
145
 
106
146
  const lineStyles = (origin) => (origin === 'staged' ? STAGED_STYLES : STYLES);
107
147
 
@@ -128,7 +168,32 @@ const paintSpans = (pieces, lineStyle, budget, color) => {
128
168
  return { text: out, used };
129
169
  };
130
170
 
171
+ const paintAuditNote = (line, width, color) => {
172
+ const style = STYLES.note;
173
+ const gutterW = Math.min(GUTTER_W, Math.max(0, width));
174
+ const label = `${style.mark} `.slice(0, gutterW);
175
+ const gutter = paint(label, style.baseFg, style.baseBg, color);
176
+ const budget = Math.max(0, width - gutterW);
177
+ const text = truncateVisible(line.text ?? '', budget);
178
+ const used = visibleWidth(text);
179
+ let out = gutter;
180
+ if (text) out += paint(text, style.baseFg, style.baseBg, color);
181
+ if (used < budget) {
182
+ out += fill(budget - used, style.baseFg, style.baseBg, color);
183
+ }
184
+ return out;
185
+ };
186
+
187
+ const paintAuditRule = (width, color) => {
188
+ const style = STYLES.noteSep;
189
+ const n = Math.max(0, width);
190
+ const bar = '─'.repeat(n);
191
+ return paint(bar, style.baseFg, style.baseBg, color);
192
+ };
193
+
131
194
  const paintDiffLine = (line, width, color, origin, lang) => {
195
+ if (line.type === 'noteSep') return paintAuditRule(width, color);
196
+ if (line.type === 'note') return paintAuditNote(line, width, color);
132
197
  const styles = lineStyles(origin);
133
198
  const style = styles[line.type] ?? styles.ctx;
134
199
  const diffSpans = line.spans ?? [{ text: line.text, changed: false }];
@@ -174,19 +239,14 @@ const paintSplitBlank = (width, color) => {
174
239
  );
175
240
  };
176
241
 
177
- const FILL_KIND = {
178
- files: (width, color) => {
242
+ const paintBodyFill = (width, color, kind) => {
243
+ if (kind === 'files') {
179
244
  const inner = Math.max(1, width - 1);
180
245
  const main = fill(inner, THEME.ctxFg, THEME.ctxBg, color);
181
246
  const edge = fill(1, THEME.ctxFg, THEME.ctxBg, color);
182
247
  return main + edge;
183
- },
184
- split: (width, color) => paintSplitBlank(width, color),
185
- };
186
-
187
- const paintBodyFill = (width, color, kind) => {
188
- const paint = FILL_KIND[kind];
189
- if (paint) return paint(width, color);
248
+ }
249
+ if (kind === 'split') return paintSplitBlank(width, color);
190
250
  return fill(width, THEME.ctxFg, THEME.ctxBg, color);
191
251
  };
192
252
 
@@ -206,22 +266,18 @@ const splitCheck = (text) => {
206
266
  if (!match) return null;
207
267
  const start = match.index;
208
268
  const token = match[0];
209
- return {
210
- before: text.slice(0, start),
211
- mark: match[1],
212
- token,
213
- after: text.slice(start + token.length),
214
- };
269
+ const before = text.slice(0, start);
270
+ const mark = match[1];
271
+ const after = text.slice(start + token.length);
272
+ return { before, mark, token, after };
215
273
  };
216
274
 
217
275
  const paintCheckBar = (text, width, fgRgb, bgRgb, color) => {
218
276
  const clipped = truncateVisible(text, width);
277
+ const parts = color ? splitCheck(clipped) : null;
278
+ if (!parts) return paintBar(text, width, fgRgb, bgRgb, color);
219
279
  const pad = Math.max(0, width - visibleWidth(clipped));
220
- const filled = clipped + ' '.repeat(pad);
221
- if (!color) return filled;
222
- const parts = splitCheck(clipped);
223
- if (!parts) return `${seq(fgRgb, bgRgb)}${EL}${filled}${RESET}`;
224
- const done = isDoneMark(parts.mark);
280
+ const done = CHECK_DONE.includes(parts.mark);
225
281
  const chipFg = done ? THEME.checkDoneFg : THEME.checkFg;
226
282
  const chipBg = done ? THEME.checkDoneBg : THEME.checkBg;
227
283
  const after = `${parts.after}${' '.repeat(pad)}`;
@@ -232,9 +288,15 @@ const paintCheckBar = (text, width, fgRgb, bgRgb, color) => {
232
288
  return out;
233
289
  };
234
290
 
235
- const originLabel = (origin, revShort) => {
236
- if (origin === 'commit') return revShort || 'commit';
237
- if (origin === 'todo') return 'todo';
291
+ const ORIGIN_LABEL = {
292
+ commit: (view) => view.revShort || 'commit',
293
+ pr: (view) => view.sourceLabel || 'pr',
294
+ todo: () => 'todo',
295
+ };
296
+
297
+ const originLabel = (origin, view) => {
298
+ const named = ORIGIN_LABEL[origin];
299
+ if (named) return named(view);
238
300
  return origin ?? 'unknown';
239
301
  };
240
302
 
@@ -271,7 +333,7 @@ const headerTarget = (view) => {
271
333
  if (!view.item) return { path: '', origin: '', n: 0, total: view.total ?? 0 };
272
334
  return {
273
335
  path: itemPath(view.item),
274
- origin: originLabel(view.item.origin, view.revShort),
336
+ origin: originLabel(view.item.origin, view),
275
337
  n: view.index + 1,
276
338
  total: view.total,
277
339
  };
@@ -285,17 +347,20 @@ const headerPrefix = (view) => {
285
347
  return ` ${LOGO} reslop: `;
286
348
  };
287
349
 
350
+ const headerSuffix = ({ origin, n, total }) =>
351
+ origin ? ` ${origin} ${n}/${total}` : ` ${n}/${total}`;
352
+
288
353
  const headerText = (view) => {
289
354
  const prefix = headerPrefix(view);
290
- const { path, origin, n, total } = headerTarget(view);
291
- const suffix = origin ? ` ${origin} ${n}/${total}` : ` ${n}/${total}`;
292
- return `${prefix}${path}${suffix}`;
355
+ const target = headerTarget(view);
356
+ return `${prefix}${target.path}${headerSuffix(target)}`;
293
357
  };
294
358
 
295
359
  const headerPieces = (view, width) => {
296
360
  const repo = view.repoName || '';
297
- const { path, origin, n, total } = headerTarget(view);
298
- const suffix = origin ? ` ${origin} ${n}/${total}` : ` ${n}/${total}`;
361
+ const target = headerTarget(view);
362
+ const path = target.path;
363
+ const suffix = headerSuffix(target);
299
364
  const right = ' ';
300
365
  const chrome = ` ${LOGO} reslop: `;
301
366
  const prefixW = visibleWidth(headerPrefix(view));
@@ -326,7 +391,7 @@ const headerPieces = (view, width) => {
326
391
  const paintHeader = (view, width, color) => {
327
392
  const pieces = headerPieces(view, width);
328
393
  if (!color) {
329
- return pieces.map((p) => p.text).join('');
394
+ return pieces.map((piece) => piece.text).join('');
330
395
  }
331
396
  const bgRgb = THEME.headerBg;
332
397
  let out = `${seq(THEME.headerChromeFg, bgRgb)}${EL}`;
@@ -345,7 +410,8 @@ const bodyLines = (item, layout) => {
345
410
  return [{ type: 'ctx', text, spans: [{ text, changed: false }] }];
346
411
  }
347
412
  if (!item.hunk) return [];
348
- return displayLines(item.hunk, item.blockId, layout);
413
+ const radius = item.dep ? Number.MAX_SAFE_INTEGER : undefined;
414
+ return displayLines(item.hunk, item.blockId, layout, radius);
349
415
  };
350
416
 
351
417
  const sourceEmpty = (line) => !line || !(line.text ?? '').trim();
@@ -363,26 +429,6 @@ const dropLeadingEmpty = (lines, split) => {
363
429
  return i ? lines.slice(i) : lines;
364
430
  };
365
431
 
366
- const helpLines = () => {
367
- const lines = [' keys and buttons', ''];
368
- for (const action of ACTIONS) {
369
- const keys = actionHot(action);
370
- let label = action.label;
371
- if (action.id === 'layout') {
372
- label = 'Mode (unified / mixed / side)';
373
- }
374
- lines.push(` ${keys.padEnd(16)} ${label}`);
375
- }
376
- lines.push(' enter open file / save note');
377
- lines.push(' esc files, then quit');
378
- lines.push(' del, backspace remove todo');
379
- lines.push(' j, k, wheel scroll / move');
380
- lines.push(' drag copy');
381
- lines.push(' ctrl-z, ctrl-y undo / redo');
382
- lines.push(' ?, h help');
383
- return lines;
384
- };
385
-
386
432
  const BUTTON_MARK = {
387
433
  bgRgb: THEME.buttonBg,
388
434
  fgRgb: THEME.buttonFg,
@@ -414,27 +460,29 @@ const paintMarkedWord = (label, letter, color, mark) => {
414
460
  return out;
415
461
  };
416
462
 
417
- const layoutButtons = (width, compact = false) => {
463
+ const layoutButtons = (width, compact = false, disabled = []) => {
418
464
  const build = (short) => {
419
465
  const hits = [];
420
466
  const parts = [];
421
467
  let x = 0;
422
- for (const action of ACTIONS) {
468
+ for (const id of ACTION_IDS) {
469
+ const action = { id, ...ACTIONS[id] };
423
470
  const letter = actionLetter(action);
424
471
  const word = buttonWord(action);
425
472
  const label = short ? letter : word;
426
473
  const piece = `${BUTTON_GAP}${label}`;
427
474
  const w = visibleWidth(piece);
428
475
  if (x + w > width && parts.length) break;
429
- hits.push({ id: action.id, x0: x, x1: x + w });
430
- parts.push({ action, piece, letter, label });
476
+ const isDisabled = disabled.includes(id);
477
+ if (!isDisabled) hits.push({ id, x0: x, x1: x + w });
478
+ parts.push({ action, piece, letter, label, disabled: isDisabled });
431
479
  x += w;
432
480
  }
433
481
  return { hits, parts, width: x };
434
482
  };
435
483
  if (compact) return build(true);
436
484
  const words = build(false);
437
- if (words.parts.length === ACTIONS.length) return words;
485
+ if (words.parts.length === ACTION_IDS.length) return words;
438
486
  return build(true);
439
487
  };
440
488
 
@@ -443,7 +491,11 @@ const paintButtons = (layout, width, color) => {
443
491
  let used = 0;
444
492
  for (const part of layout.parts) {
445
493
  out += paint(BUTTON_GAP, THEME.buttonFg, THEME.buttonBg, color);
446
- out += paintMarkedWord(part.label, part.letter, color, BUTTON_MARK);
494
+ if (part.disabled) {
495
+ out += paint(part.label, THEME.buttonFg, THEME.buttonBg, color);
496
+ } else {
497
+ out += paintMarkedWord(part.label, part.letter, color, BUTTON_MARK);
498
+ }
447
499
  used += visibleWidth(part.piece);
448
500
  }
449
501
  if (used < width) {
@@ -452,21 +504,38 @@ const paintButtons = (layout, width, color) => {
452
504
  return out;
453
505
  };
454
506
 
455
- const countsLine = (counts, revShort) => {
456
- const skipped = counts.skipped ?? 0;
507
+ const COUNTS_KIND = {
508
+ pr: ({ counts, notes, sourceLabel }) => {
509
+ const n = counts.pr ?? 0;
510
+ const label = sourceLabel || 'pr';
511
+ return ` pr ${label} ${n} ${notes}`;
512
+ },
513
+ commit: ({ counts, notes, revShort }) => {
514
+ const n = counts.commit ?? 0;
515
+ return ` commit ${revShort} ${n} ${notes}`;
516
+ },
517
+ worktree: ({ counts, notes }) => {
518
+ const staged = counts.staged ?? 0;
519
+ const unstaged = counts.unstaged ?? 0;
520
+ const untracked = counts.untracked ?? 0;
521
+ const gitCounts = `unstaged ${unstaged} untracked ${untracked}`;
522
+ const git = `staged ${staged} ${gitCounts}`;
523
+ return ` ${git} ${notes}`;
524
+ },
525
+ };
526
+
527
+ const countsKind = (sourceKind, revShort) => {
528
+ if (sourceKind === 'pr') return 'pr';
529
+ if (revShort) return 'commit';
530
+ return 'worktree';
531
+ };
532
+
533
+ const countsLine = (counts, revShort, sourceKind, sourceLabel) => {
457
534
  const feedback = counts.feedback ?? 0;
458
535
  const todo = counts.todo ?? 0;
459
536
  const notes = `feedback ${feedback} todo ${todo}`;
460
- const extra = skipped ? ` skipped ${skipped}` : '';
461
- if (revShort) {
462
- const n = counts.commit ?? 0;
463
- return ` commit ${revShort} ${n} ${notes}${extra}`;
464
- }
465
- const staged = counts.staged ?? 0;
466
- const unstaged = counts.unstaged ?? 0;
467
- const untracked = counts.untracked ?? 0;
468
- const git = `staged ${staged} unstaged ${unstaged} untracked ${untracked}`;
469
- return ` ${git} ${notes}${extra}`;
537
+ const kind = countsKind(sourceKind, revShort);
538
+ return COUNTS_KIND[kind]({ counts, notes, sourceLabel, revShort });
470
539
  };
471
540
 
472
541
  const paintQuitPrompt = (width, color) => {
@@ -484,8 +553,9 @@ const paintQuitPrompt = (width, color) => {
484
553
  };
485
554
 
486
555
  const paintStatusLine = (view, status, width, color) => {
487
- if (view.mode === 'confirmQuit') return paintQuitPrompt(width, color);
488
- const left = countsLine(view.counts ?? {}, view.revShort);
556
+ const { mode, counts, revShort, sourceKind, sourceLabel } = view;
557
+ if (mode === 'confirmQuit') return paintQuitPrompt(width, color);
558
+ const left = countsLine(counts ?? {}, revShort, sourceKind, sourceLabel);
489
559
  let right = '';
490
560
  let msgFg = THEME.chromeFg;
491
561
  if (status) {
@@ -524,11 +594,16 @@ const padStartVisible = (text, width) => {
524
594
  return `${' '.repeat(n)}${text}`;
525
595
  };
526
596
 
527
- const paintFileRow = (entry, width, color, selected, here, cols) => {
597
+ const fileRowFg = (status, selected) => {
598
+ const statusFg = FILE_STATUS_FG[status];
599
+ if (statusFg) return statusFg;
600
+ if (selected) return THEME.chromeFg;
601
+ return THEME.mutedFg;
602
+ };
603
+
604
+ const paintFileRow = (entry, width, color, selected, cols) => {
528
605
  const inner = Math.max(1, width - 1);
529
- let mark = ' ';
530
- if (here) mark = FILE_CURRENT;
531
- else if (selected) mark = FILE_CURSOR;
606
+ const mark = selected ? FILE_MARK : ' ';
532
607
  const prefix = ` ${mark} `;
533
608
  const status = entry.status ?? '';
534
609
  const count = `${entry.remaining ?? 0}`;
@@ -541,7 +616,7 @@ const paintFileRow = (entry, width, color, selected, here, cols) => {
541
616
  const clipped = truncateVisible(entry.path, pathW);
542
617
  const pad = Math.max(0, pathW - visibleWidth(clipped));
543
618
  const line = `${prefix}${clipped}${' '.repeat(pad)}${suffix}`;
544
- const fgRgb = selected ? THEME.chromeFg : THEME.mutedFg;
619
+ const fgRgb = fileRowFg(status, selected);
545
620
  const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
546
621
  const row = paintBar(line, inner, fgRgb, bgRgb, color);
547
622
  return row + fill(1, fgRgb, bgRgb, color);
@@ -643,8 +718,7 @@ const paintIdleNote = (view, width, color) => {
643
718
  const text = view.noteText;
644
719
  if (!text) return empty;
645
720
  const inner = noteInnerWidth(width);
646
- const prefix = view.noteKind ? `${view.noteKind}: ` : '';
647
- const wrapped = wrapMultiline(`${prefix}${text}`, inner);
721
+ const wrapped = wrapMultiline(`feedback: ${text}`, inner);
648
722
  const rows = [];
649
723
  for (const line of wrapped.slice(0, NOTE_MAX)) {
650
724
  const painted = paintNoteLine(
@@ -662,7 +736,7 @@ const paintIdleNote = (view, width, color) => {
662
736
 
663
737
  const paintNotePanel = (view, width, color, cap) => {
664
738
  const empty = { rows: [], cursor: null, templateHits: [] };
665
- if (view.help || (view.pane ?? 'diff') === 'files') return empty;
739
+ if ((view.pane ?? 'diff') === 'files') return empty;
666
740
  const compose = view.compose;
667
741
  if (compose && compose.kind === 'feedback') {
668
742
  return paintComposeNote(view, width, color, cap);
@@ -670,32 +744,27 @@ const paintNotePanel = (view, width, color, cap) => {
670
744
  return paintIdleNote(view, width, color);
671
745
  };
672
746
 
673
- const paintBodyHelp = (width, color) => {
674
- const body = [];
675
- for (const line of helpLines()) {
676
- for (const row of wrapPlain(line, width)) {
677
- body.push(paintBar(row, width, THEME.chromeFg, THEME.ctxBg, color));
678
- }
679
- }
680
- return body;
681
- };
682
-
683
747
  const paintBodyFiles = (view, width, color, bodyH, headerLines) => {
684
748
  const files = view.files ?? [];
685
749
  const cols = fileColumns(files);
686
750
  const cursor = view.fileCursor ?? 0;
687
- const start = cursor >= bodyH ? cursor - bodyH + 1 : 0;
751
+ const pad = bodyH >= FILE_LIST_PAD * 2 + 1 ? FILE_LIST_PAD : 0;
752
+ const listH = bodyH - pad * 2;
753
+ const start = cursor >= listH ? cursor - listH + 1 : 0;
754
+ const blank = paintBodyFill(width, color, 'files');
688
755
  const body = [];
689
756
  const fileHits = [];
690
- for (let i = 0; i < bodyH; i++) {
757
+ if (pad) body.push(blank);
758
+ for (let i = 0; i < listH; i++) {
691
759
  const idx = start + i;
692
760
  const entry = files[idx];
693
761
  if (!entry) break;
694
762
  const selected = idx === cursor;
695
- const here = view.reviewPath === entry.path;
696
- body.push(paintFileRow(entry, width, color, selected, here, cols));
697
- fileHits.push({ y: headerLines + i + 1, cursor: idx });
763
+ body.push(paintFileRow(entry, width, color, selected, cols));
764
+ const y = headerLines + pad + i + 1;
765
+ fileHits.push({ y, cursor: idx });
698
766
  }
767
+ if (pad) body.push(blank);
699
768
  return { body, fileHits };
700
769
  };
701
770
 
@@ -722,10 +791,39 @@ const paintBodyTodo = (view, width, color) => {
722
791
  return { body, todoOwners, cursor };
723
792
  };
724
793
 
794
+ const wrapSoftLine = (line, width) => {
795
+ const gutterW = Math.min(GUTTER_W, Math.max(0, width));
796
+ const budget = Math.max(1, width - gutterW);
797
+ const rows = wrapPlain(line.text ?? '', budget);
798
+ return rows.map((text) => {
799
+ const spans = [{ text, changed: false }];
800
+ return { ...line, text, spans };
801
+ });
802
+ };
803
+
804
+ const expandSoftRows = (lines, width, split) => {
805
+ const wrapWidth = split ? splitWidths(width).leftW : width;
806
+ const out = [];
807
+ for (const row of lines) {
808
+ const line = split ? row.left || row.right : row;
809
+ if (!line || !SOFT_WRAP.includes(line.type)) {
810
+ out.push(row);
811
+ continue;
812
+ }
813
+ const parts = wrapSoftLine(line, wrapWidth);
814
+ for (const part of parts) {
815
+ if (split) out.push({ left: part, right: part });
816
+ else out.push(part);
817
+ }
818
+ }
819
+ return out;
820
+ };
821
+
725
822
  const paintBodyDiff = (item, layout, width, color) => {
726
823
  const lang = detectLang(itemPath(item));
727
824
  const splitBody = layout === 'side' && !item.file.isBinary;
728
- const lines = dropLeadingEmpty(bodyLines(item, layout), splitBody);
825
+ const raw = dropLeadingEmpty(bodyLines(item, layout), splitBody);
826
+ const lines = expandSoftRows(raw, width, splitBody);
729
827
  const body = [];
730
828
  if (splitBody) {
731
829
  for (const row of lines) {
@@ -740,7 +838,6 @@ const paintBodyDiff = (item, layout, width, color) => {
740
838
  };
741
839
 
742
840
  const bodyKind = (view) => {
743
- if (view.help) return 'help';
744
841
  if ((view.pane ?? 'diff') === 'files') return 'files';
745
842
  if (!view.item) return 'empty';
746
843
  if (view.item.origin === 'todo') return 'todo';
@@ -748,11 +845,6 @@ const bodyKind = (view) => {
748
845
  };
749
846
 
750
847
  const BODY_PAINT = {
751
- help: (view, width, color) => ({
752
- body: paintBodyHelp(width, color),
753
- fileHits: [],
754
- todoOwners: [],
755
- }),
756
848
  files: (view, width, color, bodyH, headerLines) => ({
757
849
  ...paintBodyFiles(view, width, color, bodyH, headerLines),
758
850
  todoOwners: [],
@@ -806,10 +898,10 @@ const windowBody = (view, width, color, bodyH, headerLines, paintedBody) => {
806
898
  const todoOwners = paintedBody.todoOwners;
807
899
  const splitBody = paintedBody.splitBody === true;
808
900
  const pane = view.pane ?? 'diff';
809
- const listPane = pane === 'files' && !view.help;
901
+ const listPane = pane === 'files';
810
902
  const fillKind = bodyFillKind(listPane, splitBody);
811
903
  let shift = 0;
812
- if (!listPane && !view.help && body.length > 0) {
904
+ if (!listPane && body.length > 0) {
813
905
  body.unshift(paintBodyFill(width, color, fillKind));
814
906
  if (todoOwners.length) todoOwners.unshift(null);
815
907
  shift = 1;
@@ -884,7 +976,9 @@ const renderFrame = (view, options = {}) => {
884
976
  const height = Math.max(6, options.height ?? 24);
885
977
  const color = options.color ?? true;
886
978
  const rows = [paintHeader(view, width, color)];
887
- const layout = layoutButtons(width);
979
+ const pane = view.pane ?? 'diff';
980
+ const disabled = pane === 'files' ? FILES_DISABLED : [];
981
+ const layout = layoutButtons(width, false, disabled);
888
982
  const notes = frameNotes(view, width, height, color);
889
983
  const headerLines = rows.length;
890
984
  const bodyH = notes.bodyH;
@@ -928,37 +1022,18 @@ const presentRows = (rows, options = {}) => {
928
1022
  return parts.join('');
929
1023
  };
930
1024
 
931
- const DEL_CHAR_BG = ansi.bg(THEME.delCharBg);
932
- const ADD_CHAR_BG = ansi.bg(THEME.addCharBg);
933
- const DEL_LINE_FG = ansi.fg(THEME.delLineFg);
934
- const ADD_LINE_FG = ansi.fg(THEME.addLineFg);
935
-
936
1025
  module.exports = {
937
- LOGO,
938
- FILE_CURSOR,
939
- FILE_CURRENT,
940
1026
  QUIT_FINISH,
941
1027
  QUIT_CONTINUE,
942
1028
  QUIT_PROMPT,
943
- STYLES,
944
- STAGED_STYLES,
945
- renderFrame,
946
- presentCursor,
947
- presentRows,
948
- layoutButtons,
1029
+ DEL_CHAR_BG,
1030
+ ADD_CHAR_BG,
949
1031
  paintDiffLine,
950
- paintSplitRow,
951
- bodyLines,
952
1032
  headerText,
953
- wrapPlain,
954
- wrapDoc,
955
- wrapMultiline,
956
- cursorInWrap,
957
- wrapMove,
958
- noteInnerWidth,
1033
+ layoutButtons,
959
1034
  todoInnerWidth,
960
- DEL_CHAR_BG,
961
- ADD_CHAR_BG,
962
- DEL_LINE_FG,
963
- ADD_LINE_FG,
1035
+ noteInnerWidth,
1036
+ renderFrame,
1037
+ presentCursor,
1038
+ presentRows,
964
1039
  };