reslop 0.2.4 → 0.2.6

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