reslop 0.2.5 → 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 (105) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +32 -19
  6. package/lib/capabilities.js +8 -8
  7. package/lib/cli.js +40 -74
  8. package/lib/clipboard.js +39 -47
  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 +197 -402
  14. package/lib/deps.js +113 -240
  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 +76 -144
  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 +79 -178
  25. package/lib/editor.js +52 -69
  26. package/lib/files.js +71 -89
  27. package/lib/git-branches.js +254 -416
  28. package/lib/git-deps.js +147 -272
  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 +155 -141
  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 +14 -14
  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 +19 -25
  44. package/lib/highlight/txt.js +7 -10
  45. package/lib/highlight.js +17 -46
  46. package/lib/keys.js +49 -82
  47. package/lib/manifest.js +67 -126
  48. package/lib/npm-commands.js +120 -169
  49. package/lib/remote.js +140 -190
  50. package/lib/render/branches.js +63 -61
  51. package/lib/render/chrome.js +220 -333
  52. package/lib/render/commits.js +160 -361
  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 -258
  58. package/lib/render/files.js +40 -85
  59. package/lib/render/frame.js +78 -117
  60. package/lib/render/notes.js +139 -210
  61. package/lib/render/npm.js +213 -177
  62. package/lib/render/present.js +4 -11
  63. package/lib/render/primitives.js +72 -174
  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 -31
  68. package/lib/report-model.js +110 -33
  69. package/lib/report-parse.js +56 -37
  70. package/lib/report-render.js +102 -59
  71. package/lib/report-run.js +87 -77
  72. package/lib/report-test.js +221 -99
  73. package/lib/report-tools.js +96 -154
  74. package/lib/review-merge.js +33 -41
  75. package/lib/review.js +68 -129
  76. package/lib/runs.js +225 -0
  77. package/lib/select.js +53 -91
  78. package/lib/session/accessors.js +14 -21
  79. package/lib/session/actions.js +61 -43
  80. package/lib/session/branches.js +166 -222
  81. package/lib/session/change-actions.js +49 -119
  82. package/lib/session/commit.js +274 -355
  83. package/lib/session/compose.js +556 -714
  84. package/lib/session/dashboard.js +385 -0
  85. package/lib/session/dispatch.js +162 -170
  86. package/lib/session/files-pane.js +76 -44
  87. package/lib/session/items.js +6 -17
  88. package/lib/session/lifecycle.js +393 -466
  89. package/lib/session/load.js +1 -14
  90. package/lib/session/navigation.js +2 -2
  91. package/lib/session/npm.js +403 -420
  92. package/lib/session/ops.js +5 -13
  93. package/lib/session/pointer.js +16 -14
  94. package/lib/session/progress.js +39 -30
  95. package/lib/session/review.js +1 -3
  96. package/lib/session/tasks.js +276 -235
  97. package/lib/session/templates.js +51 -59
  98. package/lib/session/terminal.js +85 -132
  99. package/lib/session/view.js +47 -81
  100. package/lib/session/watch.js +196 -212
  101. package/lib/session.js +81 -161
  102. package/lib/update.js +44 -111
  103. package/lib/utilities.js +46 -88
  104. package/lib/wrap.js +29 -55
  105. package/package.json +1 -1
@@ -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
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, paintCursorList, fieldWindow } = primitives;
14
- const { paintBodyFill, listLine, paintListWindow } = 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,6 +32,7 @@ const COMMIT_COL_FG = {
30
32
  };
31
33
 
32
34
  const COMMIT_CURRENT_COL_FG = {
35
+ ...COMMIT_COL_FG,
33
36
  sha: THEME.shaDarkFg,
34
37
  refs: THEME.shaDarkFg,
35
38
  date: THEME.headerFg,
@@ -37,63 +40,45 @@ const COMMIT_CURRENT_COL_FG = {
37
40
 
38
41
  const REFS_MAX = 28;
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,111 +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;
179
- };
180
-
181
- const lineEdit = (text, textX, span, scroll) => ({
182
- x0: 1,
183
- textX,
184
- x1: textX + span,
185
- start: 0,
186
- text,
187
- limit: text.length,
188
- scroll,
189
- live: true,
190
- });
191
-
192
- const subjectWindow = (compose, rest) => {
193
- const line = splitMessage(compose.text).line;
194
- const at = Math.min(compose.cursor ?? 0, line.length);
195
- return fieldWindow(line, at, compose.scrollCol, rest);
149
+ return out + paint(tail, tone.fgRgb, tone.bgRgb, color);
196
150
  };
197
151
 
198
- const paintCommitEdit = (compose, width, color, entry = null, cols = {}) => {
152
+ const paintCommitEdit = (compose, width, color, entry, cols, index) => {
199
153
  const text = splitMessage(compose.text).line;
200
- const prefix = ` ${FILE_MARK} `;
201
- const prefixW = visibleWidth(prefix);
154
+ const prefix = markPrefix(true);
202
155
  const fields = commitRowFields(entry ?? {});
203
156
  const suffix = joinColumns(padColumns(fields, cols, COMMIT_COL_ALIGN), cols);
204
157
  const rest = Math.max(1, contentWidth(width) - visibleWidth(prefix + suffix));
205
- const win = subjectWindow(compose, rest);
158
+ const win = fieldWindow(text, compose.cursor, compose.scrollCol, rest);
206
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);
207
162
  return {
208
163
  row: paintCommitRow(shown, width, color, true, cols),
209
- cursor: { x: prefixW + win.col + 1, scroll: win.scroll },
164
+ cursor: { x: textX + win.col, scroll: win.scroll },
210
165
  at: entry ? 'replace' : 'start',
211
- edit: lineEdit(text, prefixW + 1, rest, win.scroll),
166
+ index,
167
+ edits: [{ ...edit, scroll: win.scroll }],
212
168
  };
213
169
  };
214
170
 
215
- const pendingAt = (commits) => {
216
- for (let i = 0; i < commits.length; i++) {
217
- 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);
218
177
  }
219
178
  return -1;
220
179
  };
221
180
 
222
- const editsCursor = (compose) =>
223
- compose.commitKind === 'amend' || compose.commitKind === 'reword';
224
-
225
- const editsPending = (compose) =>
226
- compose.commitKind === 'commit' || compose.commitKind === 'fixup';
227
-
228
- const commitEditRow = (compose, commits, cursor, width, color, cols) => {
229
- if (!compose || compose.kind !== 'commit') return null;
230
- let at = -1;
231
- if (editsCursor(compose)) at = cursor;
232
- if (editsPending(compose)) at = pendingAt(commits);
233
- if (at >= 0) {
234
- const entry = commits[at];
235
- if (!entry) return null;
236
- const painted = paintCommitEdit(compose, width, color, entry, cols);
237
- painted.index = at;
238
- return painted;
239
- }
240
- 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);
241
187
  };
242
188
 
243
- const paintPieces = (pieces, width, color, selected, current) => {
244
- const tone = commitTone(selected, current);
245
- const inner = contentWidth(width);
189
+ const fitPieces = (pieces, width) => {
246
190
  const shown = [];
247
191
  let used = 0;
248
192
  for (const piece of pieces) {
249
- const room = inner - used;
250
- if (room <= 0) break;
251
- const text = truncateVisible(piece.text, room);
193
+ if (used >= width) break;
194
+ const text = trimVisible(piece.text, width - used);
252
195
  if (!text) continue;
253
196
  shown.push({ text, fg: piece.fg });
254
197
  used += visibleWidth(text);
255
198
  }
256
- 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));
257
204
  const tail = ' '.repeat(COMMIT_TAIL);
258
205
  if (!color) {
259
- let plain = '';
260
- for (const piece of shown) plain += piece.text;
206
+ const plain = shown.map((piece) => piece.text).join('');
261
207
  return `${plain}${pad}${tail}`;
262
208
  }
263
209
  let out = `${seq(tone.fgRgb, tone.bgRgb)}${EL}`;
@@ -265,41 +211,25 @@ const paintPieces = (pieces, width, color, selected, current) => {
265
211
  out += paint(piece.text, piece.fg, tone.bgRgb, color);
266
212
  }
267
213
  out += paint(pad, tone.fgRgb, tone.bgRgb, color);
268
- out += paint(tail, tone.fgRgb, tone.bgRgb, color);
269
- return out;
270
- };
271
-
272
- const COL_GAP = 2;
273
- const MIN_LEFT = 8;
274
-
275
- const messageText = (entry) => {
276
- const body = `${entry.body ?? ''}`.replace(/\s+$/, '');
277
- if (body) return body;
278
- return `${entry.subject ?? ''}`.replace(/\s+$/, '');
214
+ return out + paint(tail, tone.fgRgb, tone.bgRgb, color);
279
215
  };
280
216
 
281
- const wrapKind = (text, width, kind) => {
282
- const rows = wrapMultiline(text, Math.max(1, width));
283
- const lines = [];
284
- for (const row of rows) lines.push({ text: row, kind });
285
- return lines;
286
- };
217
+ const messageText = (entry) =>
218
+ `${entry.body ?? ''}`.trimEnd() || `${entry.subject ?? ''}`.trimEnd();
287
219
 
288
- const messagePartsFrom = (text, width) => {
289
- const raw = `${text ?? ''}`;
290
- if (!raw) return [{ text: '', kind: 'text' }];
291
- const split = raw.split('\n');
292
- const lines = wrapKind(split[0], width, 'text');
293
- for (let i = 1; i < split.length; i++) {
294
- const rest = wrapKind(split[i], width, 'body');
295
- 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
+ }
296
229
  }
297
- return lines;
230
+ return parts;
298
231
  };
299
232
 
300
- const messageParts = (entry, width) =>
301
- messagePartsFrom(messageText(entry), width);
302
-
303
233
  const authorParts = (entry) => {
304
234
  const name = `${entry.author ?? ''}`.trim();
305
235
  const email = `${entry.email ?? ''}`.trim();
@@ -314,24 +244,23 @@ const authorParts = (entry) => {
314
244
  return [];
315
245
  };
316
246
 
317
- const partsWidth = (parts) => {
318
- let width = 0;
319
- for (const part of parts) width += visibleWidth(part.text);
320
- return width;
321
- };
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
+ });
322
254
 
323
255
  const timeNeed = (entry) => {
324
- const when = `${entry.when ?? ''}`.trim();
325
- const ago = shortAge(entry.date ?? '');
256
+ const { when, ago } = timeFields(entry);
326
257
  if (when && ago) return visibleWidth(when) + COL_GAP + visibleWidth(ago);
327
258
  return visibleWidth(when || ago);
328
259
  };
329
260
 
330
- const refsText = (entry) => `${entry.refs ?? ''}`.trim();
331
-
332
261
  const diffLine = (entry, widths) => {
333
262
  const label = countLabel(entry);
334
- if (!label) return null;
263
+ if (!label.add) return null;
335
264
  const add = padVisible(label.add, widths.add, 'start');
336
265
  const del = padVisible(label.del, widths.del, 'start');
337
266
  const text = `${DIFF_LABEL}${DIFF_GAP}${add}${DIFF_GAP}${del}`;
@@ -352,7 +281,7 @@ const diffCountParts = (entry, widths, rightW) => {
352
281
  ];
353
282
  };
354
283
 
355
- const rightNeed = (entry, widths = entryCountWidth(entry)) => {
284
+ const rightNeed = (entry, widths) => {
356
285
  const line = diffLine(entry, widths);
357
286
  return Math.max(
358
287
  partsWidth(authorParts(entry)),
@@ -365,11 +294,11 @@ const rightNeed = (entry, widths = entryCountWidth(entry)) => {
365
294
  const spreadTime = (when, ago, width) => {
366
295
  const agoW = visibleWidth(ago);
367
296
  if (agoW >= width) {
368
- return [{ text: truncateVisible(ago, width), kind: 'meta' }];
297
+ return [{ text: trimVisible(ago, width), kind: 'meta' }];
369
298
  }
370
299
  const room = width - agoW;
371
300
  const minGap = Math.min(COL_GAP, room);
372
- const left = truncateVisible(when, room - minGap);
301
+ const left = trimVisible(when, room - minGap);
373
302
  const gap = width - visibleWidth(left) - agoW;
374
303
  return [
375
304
  { text: left, kind: 'meta' },
@@ -379,7 +308,7 @@ const spreadTime = (when, ago, width) => {
379
308
  };
380
309
 
381
310
  const alignEnd = (text, width, kind) => {
382
- const shown = truncateVisible(text, width);
311
+ const shown = trimVisible(text, width);
383
312
  const pad = Math.max(0, width - visibleWidth(shown));
384
313
  return [
385
314
  { text: ' '.repeat(pad), kind },
@@ -388,12 +317,11 @@ const alignEnd = (text, width, kind) => {
388
317
  };
389
318
 
390
319
  const timeParts = (entry, width) => {
391
- const when = `${entry.when ?? ''}`.trim();
392
- const ago = shortAge(entry.date ?? '');
393
320
  if (!width) return [];
321
+ const { when, ago } = timeFields(entry);
394
322
  if (when && ago) return spreadTime(when, ago, width);
395
323
  if (ago) return alignEnd(ago, width, 'meta');
396
- if (when) return [{ text: truncateVisible(when, width), kind: 'meta' }];
324
+ if (when) return [{ text: trimVisible(when, width), kind: 'meta' }];
397
325
  return [];
398
326
  };
399
327
 
@@ -411,23 +339,17 @@ const rightRows = (entry, rightW, widths) => {
411
339
  };
412
340
 
413
341
  const panelWidths = (need, leftNeed, inner) => {
414
- const indent = visibleWidth(FULL_INDENT);
415
- let rightW = need;
416
- const room = Math.max(1, inner - indent);
417
- if (!rightW || room <= MIN_LEFT) {
418
- return { rightW: 0, leftW: room, gapW: 0 };
419
- }
342
+ const room = Math.max(1, inner - MARK_W);
343
+ if (!need || room <= MIN_LEFT) return { rightW: 0, leftW: room, gapW: 0 };
420
344
  const reserve = Math.max(MIN_LEFT, leftNeed);
421
345
  const minLeft = Math.min(reserve, room - COL_GAP - 1);
422
346
  const maxRight = room - COL_GAP - minLeft;
423
- if (rightW > maxRight) rightW = Math.max(1, maxRight);
347
+ const rightW = need > maxRight ? Math.max(1, maxRight) : need;
424
348
  return { rightW, leftW: room - COL_GAP - rightW, gapW: COL_GAP };
425
349
  };
426
350
 
427
351
  const panelLeft = (entry, leftW, edit) => {
428
- const parts = edit
429
- ? messagePartsFrom(edit.text ?? '', leftW)
430
- : messageParts(entry, leftW);
352
+ const parts = messageParts(edit ? edit.text : messageText(entry), leftW);
431
353
  if (!entry.sha) return parts;
432
354
  return [{ text: entry.sha, kind: 'hash' }, ...parts];
433
355
  };
@@ -435,25 +357,12 @@ const panelLeft = (entry, leftW, edit) => {
435
357
  const commitMessageWidth = (entry, screenWidth) => {
436
358
  const width = Math.max(20, screenWidth || 80);
437
359
  const inner = contentWidth(width);
438
- const indent = visibleWidth(FULL_INDENT);
439
- if (!entry) return Math.max(1, inner - indent);
440
- const hashW = visibleWidth(entry.sha ?? '');
441
- 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);
442
363
  return Math.max(1, cols.leftW);
443
364
  };
444
365
 
445
- const prefixWidth = (first, selected) => {
446
- const mark = first && selected ? FILE_MARK : ' ';
447
- const prefix = first ? ` ${mark} ` : FULL_INDENT;
448
- return visibleWidth(prefix);
449
- };
450
-
451
- const messageCaret = (text, cursor, width, row, first, selected) => {
452
- const pos = cursorInWrap(`${text ?? ''}`, cursor ?? 0, width);
453
- if (pos.row !== row) return null;
454
- return { x: prefixWidth(first, selected) + pos.col + 1 };
455
- };
456
-
457
366
  const pieceFg = (kind, tone) => {
458
367
  if (kind === 'add') return THEME.addLineFg;
459
368
  if (kind === 'del') return THEME.delLineFg;
@@ -472,190 +381,85 @@ const pieceFg = (kind, tone) => {
472
381
  return tone.fgRgb;
473
382
  };
474
383
 
475
- const clipParts = (parts, width, fallbackKind, tone) => {
476
- const shown = [];
477
- let used = 0;
478
- for (const part of parts) {
479
- const room = width - used;
480
- if (room <= 0) break;
481
- const text = truncateVisible(part.text ?? '', room);
482
- if (!text) continue;
483
- const kind = part.kind ?? fallbackKind;
484
- shown.push({ text, fg: pieceFg(kind, tone) });
485
- used += visibleWidth(text);
486
- }
487
- const pad = Math.max(0, width - used);
488
- 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 });
489
391
  return shown;
490
392
  };
491
393
 
492
- const paintPanelLine = (spec) => {
394
+ const paintPanelLine = (left, right, cols, first, spec) => {
493
395
  const tone = commitTone(spec.selected, spec.current);
494
- const mark = spec.first && spec.selected ? FILE_MARK : ' ';
495
- const prefix = spec.first ? ` ${mark} ` : FULL_INDENT;
496
- const left = truncateVisible(spec.left, spec.leftW);
497
- const leftPad = Math.max(0, spec.leftW - visibleWidth(left));
498
- const gap = ' '.repeat(spec.gapW);
499
- const right = Array.isArray(spec.right)
500
- ? spec.right
501
- : [{ text: spec.right ?? '', kind: spec.rightKind }];
502
- return paintPieces(
503
- [
504
- { text: prefix, fg: tone.fgRgb },
505
- { text: left, fg: pieceFg(spec.leftKind, tone) },
506
- { text: `${' '.repeat(leftPad)}${gap}`, fg: tone.fgRgb },
507
- ...clipParts(right, spec.rightW, spec.rightKind, tone),
508
- ],
509
- spec.width,
510
- spec.color,
511
- spec.selected,
512
- spec.current,
513
- );
514
- };
515
-
516
- const editFromDoc = (doc, row, textX, span) => {
517
- if (row < 0 || row >= doc.length) return null;
518
- const piece = doc[row];
519
- return {
520
- x0: 1,
521
- textX,
522
- x1: textX + span,
523
- start: piece.start,
524
- text: piece.text,
525
- limit: piece.start + piece.text.length,
526
- live: true,
527
- };
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);
528
406
  };
529
407
 
530
- const editLines = (editHit) => (editHit ? [editHit] : undefined);
531
-
532
408
  const commitLines = (entry, index, spec) => {
533
- const { width, color, selected, edit, current, diffWidths } = spec;
534
- const inner = contentWidth(width);
409
+ const { width, edit, diffWidths } = spec;
410
+ const need = rightNeed(entry, diffWidths);
535
411
  const hashW = visibleWidth(entry.sha ?? '');
536
- const widths = diffWidths ?? entryCountWidth(entry);
537
- const cols = panelWidths(rightNeed(entry, widths), hashW, inner);
538
- const rights = rightRows(entry, cols.rightW, widths);
412
+ const cols = panelWidths(need, hashW, contentWidth(width));
413
+ const rights = rightRows(entry, cols.rightW, diffWidths);
539
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;
540
420
  const count = Math.max(left.length, rights.length);
541
- const doc = edit ? wrapDoc(`${edit.text ?? ''}`, cols.leftW) : null;
542
421
  const lines = [];
543
422
  for (let row = 0; row < count; row++) {
544
423
  const line = left[row] ?? { text: '', kind: 'text' };
545
- const painted = paintPanelLine({
546
- left: line.text,
547
- right: rights[row] ?? [],
548
- leftW: cols.leftW,
549
- rightW: cols.rightW,
550
- gapW: cols.gapW,
551
- width,
552
- color,
553
- selected,
554
- current,
555
- first: row === 0,
556
- leftKind: line.kind,
557
- rightKind: 'meta',
558
- });
559
- const messageRow = entry.sha ? row - 1 : row;
560
- const first = !entry.sha && row === 0;
561
- const caret = edit
562
- ? messageCaret(
563
- edit.text,
564
- edit.cursor,
565
- cols.leftW,
566
- messageRow,
567
- first,
568
- selected,
569
- )
570
- : null;
571
- const textX = prefixWidth(first, selected) + 1;
572
- const editHit = doc
573
- ? editFromDoc(doc, messageRow, textX, cols.leftW)
574
- : null;
575
- lines.push(listLine(painted, index, caret, editLines(editHit)));
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));
576
434
  }
577
435
  return lines;
578
436
  };
579
437
 
580
- const draftLines = (text, cursor, width, color) => {
581
- const inner = contentWidth(width);
582
- const leftW = Math.max(1, inner - visibleWidth(FULL_INDENT));
583
- const parts = messagePartsFrom(text, leftW);
584
- const doc = wrapDoc(`${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
- const textX = prefixWidth(row === 0, true) + 1;
603
- const editHit = editFromDoc(doc, row, textX, leftW);
604
- lines.push(listLine(painted, -1, caret, editLines(editHit)));
605
- }
606
- return lines;
607
- };
608
-
609
- const composeEditsIndex = (compose, commits, index, cursor) => {
610
- if (!compose || compose.kind !== 'commit') return false;
611
- if (editsCursor(compose)) return index === cursor;
612
- if (!editsPending(compose)) return false;
613
- const at = pendingAt(commits);
614
- return at >= 0 && index === at;
615
- };
616
-
617
- const composeShowsDraft = (compose, commits) => {
618
- if (!compose || compose.kind !== 'commit') return false;
619
- if (editsCursor(compose)) return false;
620
- if (editsPending(compose)) return pendingAt(commits) < 0;
621
- return true;
622
- };
623
-
624
- const flattenCommits = (view, commits, width, color) => {
438
+ const flattenCommits = (view, commits, target, width, color) => {
625
439
  const cursor = view.commitCursor ?? 0;
626
- const compose = view.compose ?? null;
627
- const editing = compose && compose.kind === 'commit';
628
- const diffWidths = listCountWidth(commits);
629
- const gap = listLine(paintBodyFill(width, color, 'files'), -1);
440
+ const { compose } = view;
441
+ const edit = { text: compose?.text ?? '', cursor: compose?.cursor ?? 0 };
442
+ const spec = { width, color, diffWidths: countWidths(commits) };
630
443
  const lines = [];
631
- if (composeShowsDraft(compose, commits)) {
632
- const text = compose.text ?? '';
633
- lines.push(...draftLines(text, compose.cursor ?? 0, width, color));
444
+ if (target === -1) {
445
+ lines.push(...commitLines({}, -1, { ...spec, selected: true, edit }));
634
446
  }
447
+ const gap = listLine(paintBodyFill(width, color, 'files'), -1);
635
448
  for (let index = 0; index < commits.length; index++) {
636
449
  if (lines.length) lines.push(gap);
637
450
  const entry = commits[index];
638
- const inplace = composeEditsIndex(compose, commits, index, cursor);
639
- const selected = inplace || (!editing && index === cursor);
640
- const text = compose?.text ?? '';
641
- const caret = compose?.cursor ?? 0;
642
- const edit = inplace ? { text, cursor: caret } : null;
451
+ const inplace = index === target;
452
+ const selected = inplace || (target === null && index === cursor);
643
453
  const current = viewedCommit(entry, view.rev, view.revShort);
644
- const block = commitLines(entry, index, {
645
- width,
646
- color,
647
- selected,
648
- edit,
649
- current,
650
- diffWidths,
651
- });
652
- lines.push(...block);
454
+ const entryEdit = inplace ? edit : null;
455
+ const entrySpec = { ...spec, selected, current, edit: entryEdit };
456
+ lines.push(...commitLines(entry, index, entrySpec));
653
457
  }
654
458
  return lines;
655
459
  };
656
460
 
657
- const focusLine = (lines, cursor, compose) => {
658
- if (compose && compose.kind === 'commit') {
461
+ const focusLine = (lines, cursor, editing) => {
462
+ if (editing) {
659
463
  const at = lines.findIndex((line) => line.cursor);
660
464
  if (at >= 0) return at;
661
465
  }
@@ -667,25 +471,20 @@ const paintBodyCommits = (view, width, color, bodyH, headerLines) => {
667
471
  const pane = { width, color, bodyH, headerLines, offset: view.listScroll };
668
472
  const commits = view.commits ?? [];
669
473
  const cursor = view.commitCursor ?? 0;
474
+ const { compose } = view;
475
+ const target = editTarget(compose, commits, cursor);
670
476
  if (view.commitView === 'full') {
671
- const lines = flattenCommits(view, commits, width, color);
672
- const focus = focusLine(lines, cursor, view.compose);
477
+ const lines = flattenCommits(view, commits, target, width, color);
478
+ const focus = focusLine(lines, cursor, target !== null);
673
479
  return paintListWindow(lines.length, focus, pane, (i) => lines[i]);
674
480
  }
675
481
  const cols = measureColumns(commits, commitRowFields, COMMIT_COL_ALIGN);
676
- const extra = commitEditRow(
677
- view.compose,
678
- commits,
679
- cursor,
680
- width,
681
- color,
682
- cols,
683
- );
482
+ const edit = commitEditRow(compose, commits, target, width, color, cols);
684
483
  const paintRow = (entry, selected) => {
685
484
  const current = viewedCommit(entry, view.rev, view.revShort);
686
485
  return paintCommitRow(entry, width, color, selected, cols, current);
687
486
  };
688
- return paintCursorList(commits, cursor, pane, paintRow, extra);
487
+ return paintCursorList(commits, cursor, pane, paintRow, edit);
689
488
  };
690
489
 
691
490
  module.exports = {