reslop 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/README.md +8 -3
  3. package/bin/reslop.js +4 -2
  4. package/lib/api.js +2 -1
  5. package/lib/cli.js +19 -10
  6. package/lib/clipboard.js +6 -3
  7. package/lib/deps-diff.js +61 -115
  8. package/lib/deps.js +24 -61
  9. package/lib/detect.js +4 -2
  10. package/lib/diff/align.js +2 -1
  11. package/lib/diff/diff.js +6 -3
  12. package/lib/diff/display.js +2 -1
  13. package/lib/diff/inline.js +2 -1
  14. package/lib/diff/patch.js +2 -1
  15. package/lib/diff/unit.js +4 -2
  16. package/lib/editor.js +22 -4
  17. package/lib/files.js +38 -12
  18. package/lib/git-branches.js +107 -154
  19. package/lib/git-deps.js +20 -60
  20. package/lib/git-worktree.js +508 -0
  21. package/lib/git.js +23 -580
  22. package/lib/github.js +2 -1
  23. package/lib/gitlab.js +2 -1
  24. package/lib/highlight/bash.js +2 -1
  25. package/lib/highlight/css.js +2 -1
  26. package/lib/highlight/csv.js +2 -1
  27. package/lib/highlight/dot.js +2 -1
  28. package/lib/highlight/html.js +2 -1
  29. package/lib/highlight/json.js +2 -1
  30. package/lib/highlight/log.js +2 -1
  31. package/lib/highlight/txt.js +2 -1
  32. package/lib/highlight.js +2 -1
  33. package/lib/keys.js +2 -2
  34. package/lib/manifest.js +4 -2
  35. package/lib/npm-commands.js +83 -4
  36. package/lib/remote.js +20 -13
  37. package/lib/render/branches.js +6 -15
  38. package/lib/render/chrome.js +27 -7
  39. package/lib/render/commits.js +158 -46
  40. package/lib/render/diff.js +130 -47
  41. package/lib/render/files.js +4 -12
  42. package/lib/render/frame.js +3 -9
  43. package/lib/render/notes.js +4 -11
  44. package/lib/render/npm.js +1 -13
  45. package/lib/render/present.js +2 -1
  46. package/lib/render/primitives.js +28 -15
  47. package/lib/render/render.js +2 -1
  48. package/lib/render/unit.js +14 -11
  49. package/lib/report-model.js +6 -4
  50. package/lib/report-parse.js +8 -13
  51. package/lib/report-render.js +38 -21
  52. package/lib/report-run.js +12 -10
  53. package/lib/report-test.js +5 -4
  54. package/lib/report-tools.js +44 -18
  55. package/lib/review-merge.js +2 -1
  56. package/lib/review.js +6 -2
  57. package/lib/session/actions.js +49 -65
  58. package/lib/session/branches.js +13 -49
  59. package/lib/session/change-actions.js +2 -1
  60. package/lib/session/commit.js +34 -49
  61. package/lib/session/compose.js +37 -11
  62. package/lib/session/dispatch.js +17 -25
  63. package/lib/session/files-pane.js +6 -27
  64. package/lib/session/items.js +6 -3
  65. package/lib/session/lifecycle.js +8 -4
  66. package/lib/session/load.js +4 -2
  67. package/lib/session/navigation.js +1 -0
  68. package/lib/session/npm.js +54 -1
  69. package/lib/session/ops.js +15 -1
  70. package/lib/session/pointer.js +22 -3
  71. package/lib/session/review.js +2 -1
  72. package/lib/session/terminal.js +4 -2
  73. package/lib/session/update.js +2 -1
  74. package/lib/session/view.js +29 -3
  75. package/lib/session.js +37 -23
  76. package/lib/source.js +6 -3
  77. package/lib/update.js +5 -9
  78. package/lib/utilities.js +4 -2
  79. package/lib/wrap.js +3 -1
  80. package/package.json +1 -1
@@ -1,9 +1,13 @@
1
1
  'use strict';
2
2
 
3
3
  const ansi = require('../ansi.js');
4
+ const files = require('../files.js');
4
5
  const primitives = require('./primitives.js');
5
- const { wrapMultiline, cursorInWrap } = require('../wrap.js');
6
+ const wrap = require('../wrap.js');
7
+ const { shortAge } = files;
8
+ const { wrapMultiline, cursorInWrap } = wrap;
6
9
  const { measureColumns, padColumns, joinColumns, paintColumns } = primitives;
10
+ const { padVisible } = primitives;
7
11
 
8
12
  const { THEME, paint, visibleWidth, truncateVisible, seq, EL } = ansi;
9
13
  const { FILE_MARK, paneResult, paintCursorList } = primitives;
@@ -12,12 +16,16 @@ const { paintBodyFill, listWindowStart } = primitives;
12
16
  const COMMIT_COL_ALIGN = {
13
17
  sha: 'end',
14
18
  refs: 'end',
19
+ add: 'start',
20
+ del: 'start',
15
21
  date: 'start',
16
22
  };
17
23
 
18
24
  const COMMIT_COL_FG = {
19
25
  sha: THEME.shaFg,
20
26
  refs: THEME.warnFg,
27
+ add: THEME.addLineFg,
28
+ del: THEME.delLineFg,
21
29
  date: THEME.mutedFg,
22
30
  };
23
31
 
@@ -29,11 +37,65 @@ const COMMIT_CURRENT_COL_FG = {
29
37
  const REFS_MAX = 28;
30
38
  const LIST_PAD = 1;
31
39
  const COMMIT_TAIL = 1;
32
- const commitRowFields = (entry) => ({
33
- sha: entry.shortSha ?? '',
34
- refs: truncateVisible(entry.refs ?? '', REFS_MAX),
35
- date: entry.date ?? '',
36
- });
40
+ const lineCounts = (entry) => {
41
+ const added = entry.added ?? 0;
42
+ const removed = entry.removed ?? 0;
43
+ if (!added && !removed) return null;
44
+ return { added, removed };
45
+ };
46
+
47
+ const countLabel = (entry) => {
48
+ if (entry && entry.pending) {
49
+ const stagedAdd = entry.stagedAdded ?? 0;
50
+ const unstagedAdd = entry.unstagedAdded ?? 0;
51
+ const stagedDel = entry.stagedRemoved ?? 0;
52
+ const unstagedDel = entry.unstagedRemoved ?? 0;
53
+ const any = stagedAdd || unstagedAdd || stagedDel || unstagedDel;
54
+ if (!any) return null;
55
+ return {
56
+ add: `+${stagedAdd}/${unstagedAdd}`,
57
+ del: `-${stagedDel}/${unstagedDel}`,
58
+ };
59
+ }
60
+ const counts = lineCounts(entry);
61
+ if (!counts) return null;
62
+ return { add: `+${counts.added}`, del: `-${counts.removed}` };
63
+ };
64
+
65
+ const DIFF_LABEL = 'Diffs:';
66
+ const DIFF_GAP = ' ';
67
+
68
+ const entryCountWidth = (entry) => {
69
+ const label = countLabel(entry);
70
+ if (!label) return { add: 0, del: 0 };
71
+ return {
72
+ add: visibleWidth(label.add),
73
+ del: visibleWidth(label.del),
74
+ };
75
+ };
76
+
77
+ const listCountWidth = (commits) => {
78
+ let add = 0;
79
+ let del = 0;
80
+ for (const entry of commits) {
81
+ const width = entryCountWidth(entry);
82
+ add = Math.max(add, width.add);
83
+ del = Math.max(del, width.del);
84
+ }
85
+ return { add, del };
86
+ };
87
+
88
+ const commitRowFields = (entry) => {
89
+ const refs = `${entry.refs ?? ''}`.trim();
90
+ const label = countLabel(entry);
91
+ return {
92
+ sha: entry.shortSha ?? '',
93
+ refs: refs ? truncateVisible(refs, REFS_MAX) : '',
94
+ add: label ? label.add : '',
95
+ del: label ? label.del : '',
96
+ date: shortAge(entry.date ?? ''),
97
+ };
98
+ };
37
99
 
38
100
  const contentWidth = (width) => Math.max(1, width - COMMIT_TAIL);
39
101
 
@@ -268,19 +330,45 @@ const partsWidth = (parts) => {
268
330
 
269
331
  const timeNeed = (entry) => {
270
332
  const when = `${entry.when ?? ''}`.trim();
271
- const ago = `${entry.date ?? ''}`.trim();
333
+ const ago = shortAge(entry.date ?? '');
272
334
  if (when && ago) return visibleWidth(when) + COL_GAP + visibleWidth(ago);
273
335
  return visibleWidth(when || ago);
274
336
  };
275
337
 
276
338
  const refsText = (entry) => `${entry.refs ?? ''}`.trim();
277
339
 
278
- const rightNeed = (entry) =>
279
- Math.max(
340
+ const diffLine = (entry, widths) => {
341
+ const label = countLabel(entry);
342
+ if (!label) return null;
343
+ const add = padVisible(label.add, widths.add, 'start');
344
+ const del = padVisible(label.del, widths.del, 'start');
345
+ const text = `${DIFF_LABEL}${DIFF_GAP}${add}${DIFF_GAP}${del}`;
346
+ return { add, del, text };
347
+ };
348
+
349
+ const diffCountParts = (entry, widths, rightW) => {
350
+ const line = diffLine(entry, widths);
351
+ if (!line || !rightW) return [];
352
+ const pad = Math.max(0, rightW - visibleWidth(line.text));
353
+ return [
354
+ { text: ' '.repeat(pad), kind: 'meta' },
355
+ { text: DIFF_LABEL, kind: 'meta' },
356
+ { text: DIFF_GAP, kind: 'meta' },
357
+ { text: line.add, kind: 'add' },
358
+ { text: DIFF_GAP, kind: 'meta' },
359
+ { text: line.del, kind: 'del' },
360
+ ];
361
+ };
362
+
363
+ const rightNeed = (entry, widths = entryCountWidth(entry)) => {
364
+ const line = diffLine(entry, widths);
365
+ return Math.max(
280
366
  partsWidth(authorParts(entry)),
281
367
  timeNeed(entry),
282
368
  visibleWidth(refsText(entry)),
369
+ line ? visibleWidth(line.text) : 0,
283
370
  );
371
+ };
284
372
 
285
373
  const spreadTime = (when, ago, width) => {
286
374
  const agoW = visibleWidth(ago);
@@ -309,7 +397,7 @@ const alignEnd = (text, width, kind) => {
309
397
 
310
398
  const timeParts = (entry, width) => {
311
399
  const when = `${entry.when ?? ''}`.trim();
312
- const ago = `${entry.date ?? ''}`.trim();
400
+ const ago = shortAge(entry.date ?? '');
313
401
  if (!width) return [];
314
402
  if (when && ago) return spreadTime(when, ago, width);
315
403
  if (ago) return alignEnd(ago, width, 'meta');
@@ -317,10 +405,16 @@ const timeParts = (entry, width) => {
317
405
  return [];
318
406
  };
319
407
 
320
- const rightRows = (entry, rightW) => {
321
- const rows = [authorParts(entry), timeParts(entry, rightW)];
408
+ const rightRows = (entry, rightW, widths) => {
409
+ const rows = [];
410
+ const author = authorParts(entry);
411
+ if (author.length) rows.push(author);
412
+ const time = timeParts(entry, rightW);
413
+ if (time.length) rows.push(time);
322
414
  const refs = refsText(entry);
323
415
  if (refs) rows.push([{ text: refs, kind: 'refs' }]);
416
+ const diff = diffCountParts(entry, widths, rightW);
417
+ if (diff.length) rows.push(diff);
324
418
  return rows;
325
419
  };
326
420
 
@@ -342,7 +436,8 @@ const panelLeft = (entry, leftW, edit) => {
342
436
  const parts = edit
343
437
  ? messagePartsFrom(edit.text ?? '', leftW)
344
438
  : messageParts(entry, leftW);
345
- return [{ text: entry.sha ?? '', kind: 'hash' }, ...parts];
439
+ if (!entry.sha) return parts;
440
+ return [{ text: entry.sha, kind: 'hash' }, ...parts];
346
441
  };
347
442
 
348
443
  const commitMessageWidth = (entry, screenWidth) => {
@@ -368,6 +463,8 @@ const messageCaret = (text, cursor, width, row, first, selected) => {
368
463
  };
369
464
 
370
465
  const pieceFg = (kind, tone) => {
466
+ if (kind === 'add') return THEME.addLineFg;
467
+ if (kind === 'del') return THEME.delLineFg;
371
468
  if (tone.current) {
372
469
  if (kind === 'hash') return THEME.shaDarkFg;
373
470
  if (kind === 'refs') return THEME.warnFg;
@@ -431,11 +528,21 @@ const lineRecord = (commit, row, hit, cursor = null) => ({
431
528
  cursor,
432
529
  });
433
530
 
434
- const commitLines = (entry, index, width, color, selected, edit, current) => {
531
+ const commitLines = (
532
+ entry,
533
+ index,
534
+ width,
535
+ color,
536
+ selected,
537
+ edit,
538
+ current,
539
+ diffWidths,
540
+ ) => {
435
541
  const inner = contentWidth(width);
436
542
  const hashW = visibleWidth(entry.sha ?? '');
437
- const cols = panelWidths(rightNeed(entry), hashW, inner);
438
- const rights = rightRows(entry, cols.rightW);
543
+ const widths = diffWidths ?? entryCountWidth(entry);
544
+ const cols = panelWidths(rightNeed(entry, widths), hashW, inner);
545
+ const rights = rightRows(entry, cols.rightW, widths);
439
546
  const left = panelLeft(entry, cols.leftW, edit);
440
547
  const count = Math.max(left.length, rights.length);
441
548
  const lines = [];
@@ -455,13 +562,14 @@ const commitLines = (entry, index, width, color, selected, edit, current) => {
455
562
  leftKind: line.kind,
456
563
  rightKind: 'meta',
457
564
  });
565
+ const messageRow = entry.sha ? row - 1 : row;
458
566
  const caret = edit
459
567
  ? messageCaret(
460
568
  edit.text,
461
569
  edit.cursor,
462
570
  cols.leftW,
463
- row - 1,
464
- false,
571
+ messageRow,
572
+ !entry.sha && row === 0,
465
573
  selected,
466
574
  )
467
575
  : null;
@@ -516,6 +624,7 @@ const gapLine = (width, color) =>
516
624
 
517
625
  const flattenCommits = (commits, cursor, width, color, compose, rev, short) => {
518
626
  const lines = [];
627
+ const diffWidths = listCountWidth(commits);
519
628
  const editing = compose && compose.kind === 'commit';
520
629
  const pushGap = () => {
521
630
  if (lines.length) lines.push(gapLine(width, color));
@@ -544,6 +653,7 @@ const flattenCommits = (commits, cursor, width, color, compose, rev, short) => {
544
653
  selected,
545
654
  edit,
546
655
  viewedCommit(commits[index], rev, short),
656
+ diffWidths,
547
657
  );
548
658
  for (const line of block) lines.push(line);
549
659
  }
@@ -592,7 +702,12 @@ const paintLineWindow = (
592
702
  fileHits.push({ y, cursor: line.commit });
593
703
  }
594
704
  if (pad) body.push(blank);
595
- return { body, fileHits, cursor: textCursor, offset: start };
705
+ return paneResult({
706
+ body,
707
+ fileHits,
708
+ cursor: textCursor,
709
+ listScroll: start,
710
+ });
596
711
  };
597
712
 
598
713
  const paintFullCommits = (view, width, color, bodyH, headerLines) => {
@@ -656,36 +771,33 @@ const paintBriefCommits = (
656
771
  };
657
772
 
658
773
  const paintBodyCommits = (view, width, color, bodyH, headerLines) => {
774
+ if (view.commitView === 'full') {
775
+ return paintFullCommits(view, width, color, bodyH, headerLines);
776
+ }
659
777
  const commits = view.commits ?? [];
660
778
  const cols = measureColumns(commits, commitRowFields, COMMIT_COL_ALIGN);
661
779
  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
- );
681
- return paneResult({
682
- body: painted.body,
683
- fileHits: painted.fileHits,
684
- todoOwners: [],
685
- cursor: painted.cursor,
686
- splitBody: false,
687
- listScroll: painted.offset,
688
- });
780
+ const extra = commitEditRow(
781
+ view.compose,
782
+ commits,
783
+ cursor,
784
+ width,
785
+ color,
786
+ cols,
787
+ );
788
+ return paintBriefCommits(
789
+ commits,
790
+ cursor,
791
+ width,
792
+ color,
793
+ bodyH,
794
+ headerLines,
795
+ extra,
796
+ view.listScroll,
797
+ cols,
798
+ view.rev,
799
+ view.revShort,
800
+ );
689
801
  };
690
802
 
691
803
  module.exports = {
@@ -1,9 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  const ansi = require('../ansi.js');
4
- const { displayLines } = require('../diff/diff.js');
4
+ const diff = require('../diff/diff.js');
5
+ const { displayLines } = diff;
5
6
  const highlight = require('../highlight.js');
6
- const { detectLang } = require('../detect.js');
7
+ const detect = require('../detect.js');
8
+ const { detectLang } = detect;
7
9
  const files = require('../files.js');
8
10
  const wrap = require('../wrap.js');
9
11
  const primitives = require('./primitives.js');
@@ -11,7 +13,8 @@ const primitives = require('./primitives.js');
11
13
  const { itemPath } = files;
12
14
  const { tokenize, overlayTokens } = highlight;
13
15
  const { wrapPlain } = wrap;
14
- const { THEME, CODE_FG, codeFg, paint, visibleWidth, truncateVisible } = ansi;
16
+ const { THEME, CODE_FG, codeFg, paint, visibleWidth } = ansi;
17
+ const { truncateVisible, graphemes, graphemeWidth } = ansi;
15
18
  const { GUTTER_W, fill, splitWidths, paintSplitRule, paneResult } = primitives;
16
19
 
17
20
  const STYLES = {
@@ -77,10 +80,43 @@ const STAGED_STYLES = {
77
80
  ctx: STYLES.ctx,
78
81
  };
79
82
 
80
- const SOFT_WRAP = ['warn', 'note'];
81
83
  const delCharBg = () => ansi.bg(THEME.delCharBg);
82
84
  const addCharBg = () => ansi.bg(THEME.addCharBg);
83
85
 
86
+ const skipText = (text, skip) => {
87
+ if (skip <= 0) return text ?? '';
88
+ const parts = graphemes(text ?? '');
89
+ let used = 0;
90
+ let i = 0;
91
+ while (i < parts.length) {
92
+ const w = graphemeWidth(parts[i], used);
93
+ if (used + w > skip) break;
94
+ used += w;
95
+ i += 1;
96
+ }
97
+ let out = '';
98
+ for (; i < parts.length; i++) out += parts[i];
99
+ return out;
100
+ };
101
+
102
+ const clipPieces = (pieces, skip) => {
103
+ if (skip <= 0) return pieces;
104
+ const out = [];
105
+ let drop = skip;
106
+ for (const piece of pieces) {
107
+ const text = piece.text ?? '';
108
+ const width = visibleWidth(text);
109
+ if (drop >= width) {
110
+ drop -= width;
111
+ continue;
112
+ }
113
+ const shown = drop > 0 ? skipText(text, drop) : text;
114
+ drop = 0;
115
+ if (shown) out.push({ ...piece, text: shown });
116
+ }
117
+ return out;
118
+ };
119
+
84
120
  const paintSpans = (pieces, lineStyle, budget, color) => {
85
121
  let out = '';
86
122
  let used = 0;
@@ -124,14 +160,14 @@ const paintAuditRule = (width, color) => {
124
160
  return paint(bar, style.baseFg, style.baseBg, color);
125
161
  };
126
162
 
127
- const paintDiffLine = (line, width, color, origin, lang) => {
163
+ const paintDiffLine = (line, width, color, origin, lang, scrollCol = 0) => {
128
164
  if (line.type === 'noteSep') return paintAuditRule(width, color);
129
165
  if (line.type === 'note') return paintAuditNote(line, width, color);
130
166
  const styles = origin === 'staged' ? STAGED_STYLES : STYLES;
131
167
  const style = styles[line.type] ?? styles.ctx;
132
168
  const diffSpans = line.spans ?? [{ text: line.text, changed: false }];
133
169
  const tokens = tokenize(lang, line.text ?? '');
134
- const pieces = overlayTokens(tokens, diffSpans);
170
+ const pieces = clipPieces(overlayTokens(tokens, diffSpans), scrollCol);
135
171
  const gutterW = Math.min(GUTTER_W, Math.max(0, width));
136
172
  const label = `${line.mark ?? style.mark} `.slice(0, gutterW);
137
173
  const gutter = paint(label, CODE_FG.punct, style.baseBg, color);
@@ -140,17 +176,24 @@ const paintDiffLine = (line, width, color, origin, lang) => {
140
176
  return gutter + rest;
141
177
  };
142
178
 
143
- const paintSplitHalf = (line, width, color, origin, lang) => {
179
+ const paintSplitHalf = (line, width, color, origin, lang, scrollCol) => {
144
180
  if (!line || width <= 0) {
145
181
  return fill(width, THEME.ctxFg, THEME.ctxBg, color);
146
182
  }
147
- return paintDiffLine(line, width, color, origin, lang);
183
+ return paintDiffLine(line, width, color, origin, lang, scrollCol);
148
184
  };
149
185
 
150
- const paintSplitRow = (row, width, color, origin, lang) => {
186
+ const paintSplitRow = (row, width, color, origin, lang, scrollCol) => {
151
187
  const { leftW, rightW } = splitWidths(width);
152
- const left = paintSplitHalf(row.left, leftW, color, origin, lang);
153
- const right = paintSplitHalf(row.right, rightW, color, origin, lang);
188
+ const left = paintSplitHalf(row.left, leftW, color, origin, lang, scrollCol);
189
+ const right = paintSplitHalf(
190
+ row.right,
191
+ rightW,
192
+ color,
193
+ origin,
194
+ lang,
195
+ scrollCol,
196
+ );
154
197
  return `${left}${paintSplitRule(color)}${right}`;
155
198
  };
156
199
 
@@ -183,6 +226,34 @@ const dropLeadingEmpty = (lines, split) => {
183
226
  return i ? lines.slice(i) : lines;
184
227
  };
185
228
 
229
+ const cutSpans = (spans, count) => {
230
+ const taken = [];
231
+ const rest = [];
232
+ let left = count;
233
+ let i = 0;
234
+ while (i < spans.length) {
235
+ const span = spans[i];
236
+ const text = span.text ?? '';
237
+ if (left <= 0) {
238
+ rest.push(span);
239
+ i += 1;
240
+ continue;
241
+ }
242
+ if (text.length <= left) {
243
+ taken.push(span);
244
+ left -= text.length;
245
+ i += 1;
246
+ continue;
247
+ }
248
+ const changed = span.changed === true;
249
+ taken.push({ text: text.slice(0, left), changed });
250
+ rest.push({ text: text.slice(left), changed });
251
+ left = 0;
252
+ i += 1;
253
+ }
254
+ return { taken, rest };
255
+ };
256
+
186
257
  const wrapSoftLine = (line, width) => {
187
258
  const gutterW = Math.min(GUTTER_W, Math.max(0, width));
188
259
  const budget = Math.max(1, width - gutterW);
@@ -191,11 +262,14 @@ const wrapSoftLine = (line, width) => {
191
262
  const { length } = rows;
192
263
  const pieces = new Array(length);
193
264
  let used = 0;
265
+ let spans = line.spans ?? [{ text: line.text ?? '', changed }];
194
266
  for (let i = 0; i < length; i++) {
195
267
  const text = rows[i];
196
268
  const last = i === length - 1;
197
- const spans = [{ text, changed }];
198
- const next = { ...line, text, spans, editLast: last };
269
+ const cut = cutSpans(spans, text.length);
270
+ spans = cut.rest;
271
+ const nextSpans = cut.taken.length ? cut.taken : [{ text, changed }];
272
+ const next = { ...line, text, spans: nextSpans, editLast: last };
199
273
  if (line.editStart !== undefined) {
200
274
  next.editStart = line.editStart + used;
201
275
  if (last) next.editEnd = line.editEnd;
@@ -207,40 +281,30 @@ const wrapSoftLine = (line, width) => {
207
281
  return pieces;
208
282
  };
209
283
 
210
- const shouldWrap = (line) => {
211
- if (!line) return false;
212
- if (SOFT_WRAP.includes(line.type)) return true;
213
- return line.wrap === true;
214
- };
215
-
216
- const expandSoftRows = (lines, width, split) => {
284
+ const expandSoftRows = (lines, width, split, wrapView) => {
217
285
  const { leftW, rightW } = splitWidths(width);
218
286
  const out = [];
287
+ const partsOf = (line, lineW) => {
288
+ if (!wrapView || !line || line.type === 'noteSep') return [line];
289
+ const gutterW = Math.min(GUTTER_W, Math.max(0, lineW));
290
+ const budget = Math.max(1, lineW - gutterW);
291
+ if (visibleWidth(line.text ?? '') <= budget) return [line];
292
+ return wrapSoftLine(line, lineW);
293
+ };
219
294
  for (const row of lines) {
220
295
  if (!split) {
221
- if (!shouldWrap(row)) {
222
- out.push(row);
223
- continue;
224
- }
225
- for (const part of wrapSoftLine(row, width)) out.push(part);
226
- continue;
227
- }
228
- const right = row.right;
229
- if (shouldWrap(right)) {
230
- const parts = wrapSoftLine(right, rightW);
231
- out.push({ left: row.left, right: parts[0] });
232
- for (let i = 1; i < parts.length; i++) {
233
- out.push({ left: null, right: parts[i] });
234
- }
296
+ for (const part of partsOf(row, width)) out.push(part);
235
297
  continue;
236
298
  }
237
- const left = row.left;
238
- if (shouldWrap(left)) {
239
- const parts = wrapSoftLine(left, leftW);
240
- for (const part of parts) out.push({ left: part, right: part });
241
- continue;
299
+ const leftParts = partsOf(row.left, leftW);
300
+ const rightParts = partsOf(row.right, rightW);
301
+ const n = Math.max(leftParts.length, rightParts.length);
302
+ for (let i = 0; i < n; i++) {
303
+ out.push({
304
+ left: leftParts[i] ?? null,
305
+ right: rightParts[i] ?? null,
306
+ });
242
307
  }
243
- out.push(row);
244
308
  }
245
309
  return out;
246
310
  };
@@ -251,7 +315,7 @@ const onCodePiece = (line, cursor) => {
251
315
  return cursor <= line.editEnd;
252
316
  };
253
317
 
254
- const codeCursorInRows = (rows, split, width, cursor) => {
318
+ const codeCursorInRows = (rows, split, width, cursor, scrollCol = 0) => {
255
319
  if (cursor === undefined || cursor === null) return null;
256
320
  const { leftW, rightW } = splitWidths(width);
257
321
  for (let i = 0; i < rows.length; i++) {
@@ -261,8 +325,9 @@ const codeCursorInRows = (rows, split, width, cursor) => {
261
325
  const gutterW = Math.min(GUTTER_W, Math.max(0, lineW));
262
326
  const take = Math.min(cursor - line.editStart, line.text.length);
263
327
  const col = visibleWidth(line.text.slice(0, take));
328
+ const shown = Math.max(0, col - scrollCol);
264
329
  const x0 = split ? leftW + 1 : 0;
265
- return { x: x0 + gutterW + col + 1, row: i };
330
+ return { x: x0 + gutterW + shown + 1, row: i };
266
331
  }
267
332
  return null;
268
333
  };
@@ -279,22 +344,39 @@ const paintBodyDiff = (view, width, color) => {
279
344
  const overlay = view.codeOverlay;
280
345
  const lang = detectLang(itemPath(item));
281
346
  const splitBody = layout === 'side' && !item.file.isBinary;
347
+ const editing = Boolean(overlay && typeof overlay.cursor === 'number');
348
+ const picked = editing ? overlay.scrollCol : 0;
349
+ const scrollCol = picked ?? 0;
282
350
  const raw = dropLeadingEmpty(bodyLines(item, layout, overlay), splitBody);
283
- const lines = expandSoftRows(raw, width, splitBody);
351
+ const lines = expandSoftRows(raw, width, splitBody, !editing);
284
352
  const { length } = lines;
285
353
  const body = new Array(length);
286
354
  if (splitBody) {
287
355
  for (let i = 0; i < length; i++) {
288
- body[i] = paintSplitRow(lines[i], width, color, item.origin, lang);
356
+ body[i] = paintSplitRow(
357
+ lines[i],
358
+ width,
359
+ color,
360
+ item.origin,
361
+ lang,
362
+ scrollCol,
363
+ );
289
364
  }
290
365
  } else {
291
366
  for (let i = 0; i < length; i++) {
292
- body[i] = paintDiffLine(lines[i], width, color, item.origin, lang);
367
+ body[i] = paintDiffLine(
368
+ lines[i],
369
+ width,
370
+ color,
371
+ item.origin,
372
+ lang,
373
+ scrollCol,
374
+ );
293
375
  }
294
376
  }
295
377
  const edit = overlay ? overlay.cursor : null;
296
- const cursor = codeCursorInRows(lines, splitBody, width, edit);
297
- return paneResult({ body, fileHits: [], todoOwners: [], cursor, splitBody });
378
+ const cursor = codeCursorInRows(lines, splitBody, width, edit, scrollCol);
379
+ return paneResult({ body, cursor, splitBody });
298
380
  };
299
381
 
300
382
  module.exports = {
@@ -304,4 +386,5 @@ module.exports = {
304
386
  codeInnerWidth,
305
387
  paintBodyDiff,
306
388
  codeCursorInRows,
389
+ expandSoftRows,
307
390
  };
@@ -5,9 +5,9 @@ const files = require('../files.js');
5
5
  const primitives = require('./primitives.js');
6
6
  const { measureColumns, padColumns } = primitives;
7
7
 
8
- const { isTodosEntry } = files;
8
+ const { isTodosEntry, shortAge } = files;
9
9
  const { THEME, paint, visibleWidth, truncateVisible, seq, EL } = ansi;
10
- const { fill, FILE_MARK, paneResult, paintCursorList } = primitives;
10
+ const { fill, FILE_MARK, paintCursorList } = primitives;
11
11
 
12
12
  const FILE_STATUS_FG = {
13
13
  unstaged: THEME.delLineFg,
@@ -41,7 +41,7 @@ const fileRowFields = (entry) => {
41
41
  const status = entry.status ?? '';
42
42
  const n = `${staged ?? 0}`;
43
43
  const m = `${remaining ?? 0}`;
44
- const date = entry.date ?? '';
44
+ const date = shortAge(entry.date ?? '');
45
45
  return { add, del, status, n, m, date };
46
46
  };
47
47
 
@@ -134,7 +134,7 @@ const paintBodyFiles = (view, width, color, bodyH, headerLines) => {
134
134
  const cursor = view.fileCursor ?? 0;
135
135
  const paintRow = (entry, rowWidth, rowColor, selected) =>
136
136
  paintFileRow(entry, rowWidth, rowColor, selected, cols);
137
- const painted = paintCursorList(
137
+ return paintCursorList(
138
138
  files,
139
139
  cursor,
140
140
  width,
@@ -145,14 +145,6 @@ const paintBodyFiles = (view, width, color, bodyH, headerLines) => {
145
145
  null,
146
146
  view.listScroll,
147
147
  );
148
- return paneResult({
149
- body: painted.body,
150
- fileHits: painted.fileHits,
151
- todoOwners: [],
152
- cursor: painted.cursor,
153
- splitBody: false,
154
- listScroll: painted.offset,
155
- });
156
148
  };
157
149
 
158
150
  module.exports = { paintBodyFiles };
@@ -1,7 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  const ansi = require('../ansi.js');
4
- const { overlayRows } = require('../select.js');
4
+ const select = require('../select.js');
5
+ const { overlayRows } = select;
5
6
  const primitives = require('./primitives.js');
6
7
  const chrome = require('./chrome.js');
7
8
  const files = require('./files.js');
@@ -37,14 +38,7 @@ const bodyKind = (view) => {
37
38
  return 'diff';
38
39
  };
39
40
 
40
- const paintBodyEmpty = () =>
41
- paneResult({
42
- body: [],
43
- fileHits: [],
44
- todoOwners: [],
45
- cursor: null,
46
- splitBody: false,
47
- });
41
+ const paintBodyEmpty = () => paneResult();
48
42
 
49
43
  const BODY_PAINT = {
50
44
  files: paintBodyFiles,