reslop 0.2.3 → 0.2.5

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 (89) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +8 -4
  3. package/bin/reslop.js +4 -2
  4. package/lib/api.js +2 -1
  5. package/lib/capabilities.js +0 -4
  6. package/lib/cli.js +19 -10
  7. package/lib/clipboard.js +45 -4
  8. package/lib/deps-diff.js +61 -118
  9. package/lib/deps.js +166 -235
  10. package/lib/detect.js +4 -2
  11. package/lib/diff/align.js +2 -1
  12. package/lib/diff/diff.js +6 -3
  13. package/lib/diff/display.js +2 -1
  14. package/lib/diff/inline.js +68 -2
  15. package/lib/diff/patch.js +2 -1
  16. package/lib/diff/unit.js +15 -30
  17. package/lib/editor.js +91 -13
  18. package/lib/files.js +54 -27
  19. package/lib/git-branches.js +107 -154
  20. package/lib/git-deps.js +78 -155
  21. package/lib/git-worktree.js +508 -0
  22. package/lib/git.js +23 -580
  23. package/lib/github.js +9 -8
  24. package/lib/gitlab.js +5 -4
  25. package/lib/highlight/bash.js +2 -1
  26. package/lib/highlight/core.js +11 -0
  27. package/lib/highlight/css.js +2 -1
  28. package/lib/highlight/csv.js +2 -1
  29. package/lib/highlight/dot.js +3 -12
  30. package/lib/highlight/html.js +2 -1
  31. package/lib/highlight/json.js +2 -1
  32. package/lib/highlight/log.js +5 -11
  33. package/lib/highlight/txt.js +3 -12
  34. package/lib/highlight.js +2 -1
  35. package/lib/keys.js +22 -12
  36. package/lib/manifest.js +4 -6
  37. package/lib/npm-commands.js +82 -24
  38. package/lib/remote.js +21 -20
  39. package/lib/render/branches.js +9 -27
  40. package/lib/render/chrome.js +109 -35
  41. package/lib/render/commits.js +216 -216
  42. package/lib/render/diff.js +160 -47
  43. package/lib/render/files.js +9 -26
  44. package/lib/render/frame.js +97 -118
  45. package/lib/render/notes.js +74 -26
  46. package/lib/render/npm.js +104 -30
  47. package/lib/render/present.js +2 -1
  48. package/lib/render/primitives.js +128 -52
  49. package/lib/render/render.js +2 -1
  50. package/lib/render/unit.js +17 -11
  51. package/lib/report-model.js +6 -4
  52. package/lib/report-parse.js +8 -14
  53. package/lib/report-render.js +38 -21
  54. package/lib/report-run.js +12 -11
  55. package/lib/report-test.js +45 -51
  56. package/lib/report-tools.js +44 -25
  57. package/lib/review-merge.js +102 -131
  58. package/lib/review.js +69 -78
  59. package/lib/select.js +52 -0
  60. package/lib/session/actions.js +72 -87
  61. package/lib/session/branches.js +18 -52
  62. package/lib/session/change-actions.js +175 -147
  63. package/lib/session/commit.js +65 -61
  64. package/lib/session/compose.js +152 -83
  65. package/lib/session/dispatch.js +53 -46
  66. package/lib/session/edit-click.js +57 -0
  67. package/lib/session/files-pane.js +30 -51
  68. package/lib/session/items.js +73 -90
  69. package/lib/session/lifecycle.js +37 -98
  70. package/lib/session/load.js +52 -57
  71. package/lib/session/navigation.js +105 -95
  72. package/lib/session/npm.js +205 -71
  73. package/lib/session/ops.js +200 -45
  74. package/lib/session/pointer.js +33 -8
  75. package/lib/session/progress.js +9 -6
  76. package/lib/session/review.js +35 -53
  77. package/lib/session/tasks.js +292 -0
  78. package/lib/session/terminal.js +17 -19
  79. package/lib/session/unit.js +3 -4
  80. package/lib/session/update.js +76 -96
  81. package/lib/session/view.js +45 -16
  82. package/lib/session/watch.js +47 -27
  83. package/lib/session.js +66 -97
  84. package/lib/source.js +6 -3
  85. package/lib/update.js +5 -13
  86. package/lib/utilities.js +25 -3
  87. package/lib/wrap.js +18 -1
  88. package/package.json +1 -1
  89. package/lib/session/todos.js +0 -293
@@ -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,12 +325,39 @@ 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
  };
269
334
 
335
+ const codeEditHits = (rows, split, width, scrollCol = 0) => {
336
+ const { leftW, rightW } = splitWidths(width);
337
+ const hits = [];
338
+ for (let i = 0; i < rows.length; i++) {
339
+ const line = split ? rows[i].right : rows[i];
340
+ if (!line || line.editStart === undefined) continue;
341
+ const lineW = split ? rightW : width;
342
+ const gutterW = Math.min(GUTTER_W, Math.max(0, lineW));
343
+ const pane = split ? leftW + 1 : 0;
344
+ const textX = pane + gutterW + 1;
345
+ const end = split ? pane + rightW : width;
346
+ hits.push({
347
+ row: i,
348
+ x0: pane + 1,
349
+ textX,
350
+ x1: end + 1,
351
+ scroll: scrollCol,
352
+ start: line.editStart,
353
+ text: line.text ?? '',
354
+ limit: line.editEnd,
355
+ live: true,
356
+ });
357
+ }
358
+ return hits;
359
+ };
360
+
270
361
  const codeInnerWidth = (width, layout = 'unified') => {
271
362
  const lineW = layout === 'side' ? splitWidths(width).rightW : width;
272
363
  const gutterW = Math.min(GUTTER_W, Math.max(0, lineW));
@@ -279,22 +370,42 @@ const paintBodyDiff = (view, width, color) => {
279
370
  const overlay = view.codeOverlay;
280
371
  const lang = detectLang(itemPath(item));
281
372
  const splitBody = layout === 'side' && !item.file.isBinary;
373
+ const editing = Boolean(overlay && typeof overlay.cursor === 'number');
374
+ const picked = editing ? overlay.scrollCol : 0;
375
+ const scrollCol = picked ?? 0;
282
376
  const raw = dropLeadingEmpty(bodyLines(item, layout, overlay), splitBody);
283
- const lines = expandSoftRows(raw, width, splitBody);
377
+ const lines = expandSoftRows(raw, width, splitBody, !editing);
284
378
  const { length } = lines;
285
379
  const body = new Array(length);
286
380
  if (splitBody) {
287
381
  for (let i = 0; i < length; i++) {
288
- body[i] = paintSplitRow(lines[i], width, color, item.origin, lang);
382
+ body[i] = paintSplitRow(
383
+ lines[i],
384
+ width,
385
+ color,
386
+ item.origin,
387
+ lang,
388
+ scrollCol,
389
+ );
289
390
  }
290
391
  } else {
291
392
  for (let i = 0; i < length; i++) {
292
- body[i] = paintDiffLine(lines[i], width, color, item.origin, lang);
393
+ body[i] = paintDiffLine(
394
+ lines[i],
395
+ width,
396
+ color,
397
+ item.origin,
398
+ lang,
399
+ scrollCol,
400
+ );
293
401
  }
294
402
  }
295
403
  const edit = overlay ? overlay.cursor : null;
296
- const cursor = codeCursorInRows(lines, splitBody, width, edit);
297
- return paneResult({ body, fileHits: [], todoOwners: [], cursor, splitBody });
404
+ const cursor = codeCursorInRows(lines, splitBody, width, edit, scrollCol);
405
+ const editHits = editing
406
+ ? codeEditHits(lines, splitBody, width, scrollCol)
407
+ : [];
408
+ return paneResult({ body, cursor, splitBody, editHits });
298
409
  };
299
410
 
300
411
  module.exports = {
@@ -304,4 +415,6 @@ module.exports = {
304
415
  codeInnerWidth,
305
416
  paintBodyDiff,
306
417
  codeCursorInRows,
418
+ codeEditHits,
419
+ expandSoftRows,
307
420
  };
@@ -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 { isTasksEntry, 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,
@@ -30,7 +30,7 @@ const FILE_DATE_GAP = ' ';
30
30
  const signedCount = (sign, n) => `${sign}${n ?? 0}`;
31
31
 
32
32
  const fileRowFields = (entry) => {
33
- if (isTodosEntry(entry)) {
33
+ if (isTasksEntry(entry)) {
34
34
  const n = `${entry.staged ?? 0}`;
35
35
  const m = `${entry.remaining ?? 0}`;
36
36
  return { add: '', del: '', status: '', n, m, date: '' };
@@ -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
 
@@ -105,7 +105,7 @@ const paintTodosRow = (entry, width, color, selected, cols) => {
105
105
  };
106
106
 
107
107
  const paintFileRow = (entry, width, color, selected, cols) => {
108
- if (isTodosEntry(entry)) {
108
+ if (isTasksEntry(entry)) {
109
109
  return paintTodosRow(entry, width, color, selected, cols);
110
110
  }
111
111
  const inner = Math.max(1, width - 1);
@@ -132,27 +132,10 @@ const paintBodyFiles = (view, width, color, bodyH, headerLines) => {
132
132
  const files = view.files ?? [];
133
133
  const cols = measureColumns(files, fileRowFields, FILE_COL_ALIGN);
134
134
  const cursor = view.fileCursor ?? 0;
135
- const paintRow = (entry, rowWidth, rowColor, selected) =>
136
- paintFileRow(entry, rowWidth, rowColor, selected, cols);
137
- const painted = paintCursorList(
138
- files,
139
- cursor,
140
- width,
141
- color,
142
- bodyH,
143
- headerLines,
144
- paintRow,
145
- null,
146
- view.listScroll,
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
- });
135
+ const pane = { width, color, bodyH, headerLines, offset: view.listScroll };
136
+ const paintRow = (entry, selected) =>
137
+ paintFileRow(entry, width, color, selected, cols);
138
+ return paintCursorList(files, cursor, pane, paintRow);
156
139
  };
157
140
 
158
141
  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, markEditorSelection } = select;
5
6
  const primitives = require('./primitives.js');
6
7
  const chrome = require('./chrome.js');
7
8
  const files = require('./files.js');
@@ -15,8 +16,7 @@ const unit = require('./unit.js');
15
16
  const { RESET } = ansi;
16
17
  const { LIST_PANES, paintBodyFill, paneResult } = primitives;
17
18
  const { paintHeader, paintFooter, footerHits, paintStatusLine } = chrome;
18
- const { layoutButtons, statusBranchHit, statusChoiceHits } = chrome;
19
- const { hiddenActions, dimFooterActions, extraFooterActions } = chrome;
19
+ const { footerLayout, statusBranchHit, statusChoiceHits } = chrome;
20
20
  const { paintNotePanel, paintBodyTodo } = notes;
21
21
  const { paintBodyFiles } = files;
22
22
  const { paintBodyBranches } = branches;
@@ -33,18 +33,11 @@ const bodyKind = (view) => {
33
33
  if (pane === 'npm') return 'npm';
34
34
  if (pane === 'unit') return 'unit';
35
35
  if (!view.item) return 'empty';
36
- if (view.item.origin === 'todo') return 'todo';
36
+ if (view.item.origin === 'task') return 'tasks';
37
37
  return 'diff';
38
38
  };
39
39
 
40
- const paintBodyEmpty = () =>
41
- paneResult({
42
- body: [],
43
- fileHits: [],
44
- todoOwners: [],
45
- cursor: null,
46
- splitBody: false,
47
- });
40
+ const paintBodyEmpty = () => paneResult();
48
41
 
49
42
  const BODY_PAINT = {
50
43
  files: paintBodyFiles,
@@ -52,7 +45,7 @@ const BODY_PAINT = {
52
45
  commits: paintBodyCommits,
53
46
  npm: paintBodyNpm,
54
47
  empty: paintBodyEmpty,
55
- todo: paintBodyTodo,
48
+ tasks: paintBodyTodo,
56
49
  diff: paintBodyDiff,
57
50
  unit: paintBodyUnit,
58
51
  };
@@ -106,145 +99,131 @@ const revealScroll = (owners, focus, height, prefer) => {
106
99
  return start;
107
100
  };
108
101
 
109
- const mapBodyCursor = (cursor, shift, scroll, headerLines, shown) => {
102
+ const windowY = (win, row) => {
103
+ const at = row + win.shift - win.scroll;
104
+ if (at < 0 || at >= win.shown) return -1;
105
+ return win.headerLines + at + 1;
106
+ };
107
+
108
+ const mapEditHits = (hits, win) => {
109
+ const out = [];
110
+ for (const hit of hits) {
111
+ const y = windowY(win, hit.row);
112
+ if (y >= 0) out.push({ ...hit, y });
113
+ }
114
+ return out;
115
+ };
116
+
117
+ const mapBodyCursor = (cursor, win) => {
110
118
  if (!cursor) return null;
111
- const row = cursor.row + shift;
112
- if (row < scroll || row >= scroll + shown) return null;
113
- return {
114
- x: cursor.x,
115
- y: headerLines + (row - scroll) + 1,
116
- };
119
+ const y = windowY(win, cursor.row);
120
+ if (y < 0) return null;
121
+ const point = { x: cursor.x, y };
122
+ if (typeof cursor.scroll === 'number') point.scroll = cursor.scroll;
123
+ return point;
117
124
  };
118
125
 
119
- const windowBody = (view, width, color, bodyH, headerLines, paintedBody) => {
120
- const sourceBody = paintedBody.body;
121
- const sourceOwners = paintedBody.todoOwners;
122
- const sourceChecks = paintedBody.todoChecks ?? [];
123
- const splitBody = paintedBody.splitBody === true;
124
- const pane = view.pane ?? 'diff';
125
- const listPane = LIST_PANES.includes(pane);
126
- const fillKind = bodyFillKind(listPane, splitBody);
126
+ const windowBody = (view, paintedBody, frame) => {
127
+ const { width, color, bodyH, headerLines } = frame;
128
+ const listPane = LIST_PANES.includes(view.pane ?? 'diff');
129
+ const fillKind = bodyFillKind(listPane, paintedBody.splitBody);
127
130
  const filler = paintBodyFill(width, color, fillKind);
128
- const lead = !listPane && sourceBody.length > 0;
129
- const body = new Array(sourceBody.length + (lead ? 1 : 0));
130
- let bodyAt = 0;
131
- let shift = 0;
132
- if (lead) {
133
- body[bodyAt++] = filler;
134
- shift = 1;
135
- }
136
- for (let i = 0; i < sourceBody.length; i++) body[bodyAt++] = sourceBody[i];
137
- const ownerLead = lead && sourceOwners.length;
138
- const todoOwners = new Array(sourceOwners.length + (ownerLead ? 1 : 0));
139
- let ownerAt = 0;
140
- if (ownerLead) todoOwners[ownerAt++] = null;
141
- for (let i = 0; i < sourceOwners.length; i++) {
142
- todoOwners[ownerAt++] = sourceOwners[i];
143
- }
144
- const checkLead = lead && sourceChecks.length;
145
- const todoChecks = new Array(sourceChecks.length + (checkLead ? 1 : 0));
146
- let checkAt = 0;
147
- if (checkLead) todoChecks[checkAt++] = false;
148
- for (let i = 0; i < sourceChecks.length; i++) {
149
- todoChecks[checkAt++] = sourceChecks[i];
150
- }
151
- const scrollRaw = view.scroll ?? 0;
131
+ const shift = !listPane && paintedBody.body.length > 0 ? 1 : 0;
132
+ const lead = (items, first) =>
133
+ shift && items.length ? [first, ...items] : items;
134
+ const body = lead(paintedBody.body, filler);
135
+ const taskOwners = lead(paintedBody.taskOwners, null);
136
+ const taskChecks = lead(paintedBody.taskChecks, false);
152
137
  const scrollMax = Math.max(0, body.length - bodyH);
153
- let scroll = listPane ? 0 : Math.min(scrollRaw, scrollMax);
154
- if (view.item && view.item.origin === 'todo') {
155
- scroll = revealScroll(todoOwners, view.todoFocus ?? 0, bodyH, scroll);
138
+ let scroll = listPane ? 0 : Math.min(view.scroll ?? 0, scrollMax);
139
+ if (view.item && view.item.origin === 'task') {
140
+ scroll = revealScroll(taskOwners, view.tasksFocus ?? 0, bodyH, scroll);
156
141
  }
157
- const end = Math.min(body.length, scroll + bodyH);
158
- const copied = Math.max(0, end - scroll);
159
- const slice = new Array(bodyH);
160
- for (let i = 0; i < copied; i++) slice[i] = body[scroll + i];
161
- const todoHits = [];
162
- for (let i = 0; i < copied; i++) {
163
- const owner = todoOwners[scroll + i];
142
+ const slice = body.slice(scroll, scroll + bodyH);
143
+ const win = { shift, scroll, shown: slice.length, headerLines };
144
+ const taskHits = [];
145
+ for (let i = 0; i < slice.length; i++) {
146
+ const owner = taskOwners[scroll + i];
164
147
  if (owner === undefined || owner === null) continue;
165
148
  const y = headerLines + i + 1;
166
- if (todoChecks[scroll + i]) {
167
- todoHits.push({ y, cursor: owner, x0: 1, x1: 4, check: true });
149
+ if (taskChecks[scroll + i]) {
150
+ taskHits.push({ y, cursor: owner, x0: 1, x1: 4, check: true });
168
151
  }
169
- todoHits.push({ y, cursor: owner });
152
+ taskHits.push({ y, cursor: owner });
170
153
  }
171
- const cursor = mapBodyCursor(
172
- paintedBody.cursor,
173
- shift,
154
+ const cursor = mapBodyCursor(paintedBody.cursor, win);
155
+ const editHits = mapEditHits(paintedBody.editHits, win);
156
+ while (slice.length < bodyH) slice.push(filler);
157
+ const { listScroll, fileHits } = paintedBody;
158
+ return {
159
+ slice,
174
160
  scroll,
175
- headerLines,
176
- copied,
177
- );
178
- for (let i = copied; i < bodyH; i++) slice[i] = filler;
179
- const listScroll = paintedBody.listScroll ?? 0;
180
- const fileHits = paintedBody.fileHits;
181
- return { slice, scroll, scrollMax, listScroll, todoHits, fileHits, cursor };
161
+ scrollMax,
162
+ listScroll,
163
+ taskHits,
164
+ fileHits,
165
+ editHits,
166
+ cursor,
167
+ };
182
168
  };
183
169
 
184
- const notePointerHits = (notesWanted, noteRows, noteCursor, bodyH) => {
185
- let cursor = null;
186
- if (noteCursor) {
187
- cursor = { x: noteCursor.x, y: 2 + bodyH + noteCursor.row };
188
- }
170
+ const notePointerHits = (notes, bodyH) => {
171
+ const { notesWanted, noteRows, noteCursor } = notes;
172
+ const top = 2 + bodyH;
173
+ const x = noteCursor ? noteCursor.x : 0;
174
+ const y = noteCursor ? top + noteCursor.row : 0;
175
+ const cursor = noteCursor ? { x, y } : null;
189
176
  const templateHits = [];
190
177
  for (const hit of notesWanted.templateHits ?? []) {
191
178
  if (hit.row >= noteRows.length) continue;
192
- templateHits.push({ y: 2 + bodyH + hit.row, cursor: hit.cursor });
179
+ templateHits.push({ y: top + hit.row, cursor: hit.cursor });
193
180
  }
194
- return { cursor, templateHits };
195
- };
196
-
197
- const stackFrame = (view, width, color, rows, layout, windowed, notes) => {
198
- for (const row of windowed.slice) rows.push(row);
199
- for (const row of notes.noteRows) rows.push(row);
200
- const status = view.status ?? '';
201
- rows.push(paintStatusLine(view, status, width, color));
202
- rows.push(paintFooter(view, layout, width, color));
203
- return overlayRows(rows, view.selection);
181
+ const editHits = [];
182
+ for (const hit of notesWanted.editHits ?? []) {
183
+ if (hit.row >= noteRows.length) continue;
184
+ editHits.push({ ...hit, y: top + hit.row });
185
+ }
186
+ return { cursor, templateHits, editHits };
204
187
  };
205
188
 
206
189
  const renderFrame = (view, options = {}) => {
207
190
  const width = Math.max(20, options.width ?? 80);
208
191
  const height = Math.max(6, options.height ?? 24);
209
192
  const color = options.color ?? true;
210
- const rows = [paintHeader(view, width, color)];
211
- const extra = extraFooterActions(view);
212
- const hidden = hiddenActions(view);
213
- const dimmed = dimFooterActions(view);
214
- const layout = layoutButtons(width, false, hidden, extra, dimmed);
193
+ const header = paintHeader(view, width, color);
194
+ const layout = footerLayout(view, width);
215
195
  const notes = frameNotes(view, width, height, color);
216
- const headerLines = rows.length;
217
- const bodyH = notes.bodyH;
196
+ const headerLines = 1;
197
+ const { bodyH } = notes;
218
198
  const paintedBody = paintFrameBody(view, width, color, bodyH, headerLines);
219
- const windowed = windowBody(
220
- view,
221
- width,
222
- color,
223
- bodyH,
224
- headerLines,
225
- paintedBody,
226
- );
227
- const painted = stackFrame(view, width, color, rows, layout, windowed, notes);
228
- const text = painted.map((row) => (color ? row + RESET : row)).join('\n');
229
- const hits = notePointerHits(
230
- notes.notesWanted,
231
- notes.noteRows,
232
- notes.noteCursor,
233
- bodyH,
234
- );
199
+ const frame = { width, color, bodyH, headerLines };
200
+ const windowed = windowBody(view, paintedBody, frame);
201
+ const rows = [
202
+ header,
203
+ ...windowed.slice,
204
+ ...notes.noteRows,
205
+ paintStatusLine(view, view.status ?? '', width, color),
206
+ paintFooter(view, layout, width, color),
207
+ ];
208
+ const painted = overlayRows(rows, view.selection);
209
+ const hits = notePointerHits(notes, bodyH);
235
210
  const statusY = painted.length - 1;
236
211
  const branchHit = statusBranchHit(view, width, statusY);
237
212
  const statusHits = branchHit ? [branchHit] : statusChoiceHits(view, statusY);
238
- const { fileHits, todoHits, scroll, scrollMax, listScroll } = windowed;
213
+ const { fileHits, taskHits, scroll, scrollMax, listScroll } = windowed;
239
214
  const { templateHits } = hits;
215
+ const editHits = [...windowed.editHits, ...hits.editHits];
216
+ const shown = markEditorSelection(painted, view, editHits, color);
217
+ const text = shown.map((row) => (color ? row + RESET : row)).join('\n');
240
218
  const footerY = painted.length;
241
219
  return {
242
220
  text,
243
- rows: painted,
221
+ rows: shown,
244
222
  buttons: footerHits(view, layout),
245
223
  fileHits,
246
- todoHits,
224
+ taskHits,
247
225
  templateHits,
226
+ editHits,
248
227
  statusHits,
249
228
  footerY,
250
229
  height: footerY,
@@ -256,4 +235,4 @@ const renderFrame = (view, options = {}) => {
256
235
  };
257
236
  };
258
237
 
259
- module.exports = { BODY_PAINT, renderFrame };
238
+ module.exports = { renderFrame };