reslop 0.2.0 → 0.2.3

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 (43) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +13 -2
  3. package/lib/ansi.js +263 -6
  4. package/lib/api.js +2 -0
  5. package/lib/capabilities.js +6 -0
  6. package/lib/cli.js +13 -2
  7. package/lib/deps.js +43 -3
  8. package/lib/git-branches.js +228 -17
  9. package/lib/git-reword-todo.js +20 -0
  10. package/lib/git.js +20 -3
  11. package/lib/keys.js +10 -4
  12. package/lib/npm-commands.js +513 -0
  13. package/lib/render/branches.js +23 -58
  14. package/lib/render/chrome.js +156 -99
  15. package/lib/render/commits.js +643 -31
  16. package/lib/render/diff.js +4 -4
  17. package/lib/render/frame.js +51 -5
  18. package/lib/render/notes.js +22 -5
  19. package/lib/render/npm.js +228 -0
  20. package/lib/render/primitives.js +48 -34
  21. package/lib/render/render.js +3 -3
  22. package/lib/report-model.js +127 -0
  23. package/lib/report-parse.js +94 -0
  24. package/lib/report-render.js +87 -0
  25. package/lib/report-run.js +164 -0
  26. package/lib/report-test.js +506 -0
  27. package/lib/report-tools.js +311 -0
  28. package/lib/review-merge.js +270 -0
  29. package/lib/review.js +38 -6
  30. package/lib/session/actions.js +81 -5
  31. package/lib/session/commit.js +187 -19
  32. package/lib/session/compose.js +96 -32
  33. package/lib/session/dispatch.js +36 -2
  34. package/lib/session/files-pane.js +36 -6
  35. package/lib/session/lifecycle.js +116 -16
  36. package/lib/session/navigation.js +1 -0
  37. package/lib/session/npm.js +335 -0
  38. package/lib/session/pointer.js +12 -1
  39. package/lib/session/todos.js +29 -1
  40. package/lib/session/view.js +14 -3
  41. package/lib/session/watch.js +120 -8
  42. package/lib/session.js +11 -2
  43. package/package.json +7 -7
@@ -78,8 +78,8 @@ const STAGED_STYLES = {
78
78
  };
79
79
 
80
80
  const SOFT_WRAP = ['warn', 'note'];
81
- const DEL_CHAR_BG = ansi.bg(THEME.delCharBg);
82
- const ADD_CHAR_BG = ansi.bg(THEME.addCharBg);
81
+ const delCharBg = () => ansi.bg(THEME.delCharBg);
82
+ const addCharBg = () => ansi.bg(THEME.addCharBg);
83
83
 
84
84
  const paintSpans = (pieces, lineStyle, budget, color) => {
85
85
  let out = '';
@@ -298,8 +298,8 @@ const paintBodyDiff = (view, width, color) => {
298
298
  };
299
299
 
300
300
  module.exports = {
301
- DEL_CHAR_BG,
302
- ADD_CHAR_BG,
301
+ delCharBg,
302
+ addCharBg,
303
303
  paintDiffLine,
304
304
  codeInnerWidth,
305
305
  paintBodyDiff,
@@ -8,18 +8,20 @@ const files = require('./files.js');
8
8
  const branches = require('./branches.js');
9
9
  const commits = require('./commits.js');
10
10
  const notes = require('./notes.js');
11
+ const npm = require('./npm.js');
11
12
  const diff = require('./diff.js');
12
13
  const unit = require('./unit.js');
13
14
 
14
15
  const { RESET } = ansi;
15
16
  const { LIST_PANES, paintBodyFill, paneResult } = primitives;
16
- const { paintHeader, paintButtons, paintStatusLine } = chrome;
17
+ const { paintHeader, paintFooter, footerHits, paintStatusLine } = chrome;
17
18
  const { layoutButtons, statusBranchHit, statusChoiceHits } = chrome;
18
19
  const { hiddenActions, dimFooterActions, extraFooterActions } = chrome;
19
20
  const { paintNotePanel, paintBodyTodo } = notes;
20
21
  const { paintBodyFiles } = files;
21
22
  const { paintBodyBranches } = branches;
22
23
  const { paintBodyCommits } = commits;
24
+ const { paintBodyNpm } = npm;
23
25
  const { paintBodyDiff } = diff;
24
26
  const { paintBodyUnit } = unit;
25
27
 
@@ -28,6 +30,7 @@ const bodyKind = (view) => {
28
30
  if (pane === 'files' || pane === 'branches' || pane === 'commits') {
29
31
  return pane;
30
32
  }
33
+ if (pane === 'npm') return 'npm';
31
34
  if (pane === 'unit') return 'unit';
32
35
  if (!view.item) return 'empty';
33
36
  if (view.item.origin === 'todo') return 'todo';
@@ -47,6 +50,7 @@ const BODY_PAINT = {
47
50
  files: paintBodyFiles,
48
51
  branches: paintBodyBranches,
49
52
  commits: paintBodyCommits,
53
+ npm: paintBodyNpm,
50
54
  empty: paintBodyEmpty,
51
55
  todo: paintBodyTodo,
52
56
  diff: paintBodyDiff,
@@ -75,6 +79,33 @@ const bodyFillKind = (listPane, splitBody) => {
75
79
  return 'plain';
76
80
  };
77
81
 
82
+ const ownerSpan = (owners, focus) => {
83
+ let first = -1;
84
+ let last = -1;
85
+ for (let i = 0; i < owners.length; i++) {
86
+ if (owners[i] !== focus) continue;
87
+ if (first < 0) first = i;
88
+ last = i;
89
+ }
90
+ return { first, last };
91
+ };
92
+
93
+ const revealScroll = (owners, focus, height, prefer) => {
94
+ const span = ownerSpan(owners, focus);
95
+ if (span.first < 0) return prefer;
96
+ const room = Math.max(1, height);
97
+ let start = prefer;
98
+ if (span.first < start) start = span.first;
99
+ if (span.last >= start + room) {
100
+ const fits = span.last - span.first + 1 <= room;
101
+ start = fits ? span.last - room + 1 : span.first;
102
+ }
103
+ const maxStart = Math.max(0, owners.length - room);
104
+ if (start > maxStart) start = maxStart;
105
+ if (start < 0) start = 0;
106
+ return start;
107
+ };
108
+
78
109
  const mapBodyCursor = (cursor, shift, scroll, headerLines, shown) => {
79
110
  if (!cursor) return null;
80
111
  const row = cursor.row + shift;
@@ -88,6 +119,7 @@ const mapBodyCursor = (cursor, shift, scroll, headerLines, shown) => {
88
119
  const windowBody = (view, width, color, bodyH, headerLines, paintedBody) => {
89
120
  const sourceBody = paintedBody.body;
90
121
  const sourceOwners = paintedBody.todoOwners;
122
+ const sourceChecks = paintedBody.todoChecks ?? [];
91
123
  const splitBody = paintedBody.splitBody === true;
92
124
  const pane = view.pane ?? 'diff';
93
125
  const listPane = LIST_PANES.includes(pane);
@@ -109,9 +141,19 @@ const windowBody = (view, width, color, bodyH, headerLines, paintedBody) => {
109
141
  for (let i = 0; i < sourceOwners.length; i++) {
110
142
  todoOwners[ownerAt++] = sourceOwners[i];
111
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
+ }
112
151
  const scrollRaw = view.scroll ?? 0;
113
152
  const scrollMax = Math.max(0, body.length - bodyH);
114
- const scroll = listPane ? 0 : Math.min(scrollRaw, scrollMax);
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);
156
+ }
115
157
  const end = Math.min(body.length, scroll + bodyH);
116
158
  const copied = Math.max(0, end - scroll);
117
159
  const slice = new Array(bodyH);
@@ -120,7 +162,11 @@ const windowBody = (view, width, color, bodyH, headerLines, paintedBody) => {
120
162
  for (let i = 0; i < copied; i++) {
121
163
  const owner = todoOwners[scroll + i];
122
164
  if (owner === undefined || owner === null) continue;
123
- todoHits.push({ y: headerLines + i + 1, cursor: owner });
165
+ const y = headerLines + i + 1;
166
+ if (todoChecks[scroll + i]) {
167
+ todoHits.push({ y, cursor: owner, x0: 1, x1: 4, check: true });
168
+ }
169
+ todoHits.push({ y, cursor: owner });
124
170
  }
125
171
  const cursor = mapBodyCursor(
126
172
  paintedBody.cursor,
@@ -153,7 +199,7 @@ const stackFrame = (view, width, color, rows, layout, windowed, notes) => {
153
199
  for (const row of notes.noteRows) rows.push(row);
154
200
  const status = view.status ?? '';
155
201
  rows.push(paintStatusLine(view, status, width, color));
156
- rows.push(paintButtons(layout, width, color));
202
+ rows.push(paintFooter(view, layout, width, color));
157
203
  return overlayRows(rows, view.selection);
158
204
  };
159
205
 
@@ -195,7 +241,7 @@ const renderFrame = (view, options = {}) => {
195
241
  return {
196
242
  text,
197
243
  rows: painted,
198
- buttons: layout.hits,
244
+ buttons: footerHits(view, layout),
199
245
  fileHits,
200
246
  todoHits,
201
247
  templateHits,
@@ -11,7 +11,7 @@ const { THEME, visibleWidth, truncateVisible, seq, EL, RESET, BOLD } = ansi;
11
11
  const { paintBar, paintCheckBar, windowRows } = primitives;
12
12
  const { LIST_PANES, CHECK_LEAD, paneResult } = primitives;
13
13
 
14
- const NOTE_COMPOSE = ['feedback', 'commit'];
14
+ const NOTE_COMPOSE = ['feedback'];
15
15
  const INPUT_MAX = 4;
16
16
  const NOTE_MAX = 4;
17
17
  const NOTE_PAD = 1;
@@ -57,6 +57,7 @@ const paintTodoList = (texts, focus, width, color, edit) => {
57
57
  const inner = todoInnerWidth(width);
58
58
  const rows = [];
59
59
  const owners = [];
60
+ const checks = [];
60
61
  let cursor = null;
61
62
  for (let i = 0; i < texts.length; i++) {
62
63
  const selected = i === focus;
@@ -65,14 +66,22 @@ const paintTodoList = (texts, focus, width, color, edit) => {
65
66
  const text = texts[i];
66
67
  const wrapped = wrapTodoItem(text, inner);
67
68
  const rowStart = rows.length;
68
- for (const line of wrapped) {
69
- rows.push(paintCheckBar(` ${line}`, width, fgRgb, bgRgb, color));
69
+ for (let line = 0; line < wrapped.length; line++) {
70
+ const painted = paintCheckBar(
71
+ ` ${wrapped[line]}`,
72
+ width,
73
+ fgRgb,
74
+ bgRgb,
75
+ color,
76
+ );
77
+ rows.push(painted);
70
78
  owners.push(i);
79
+ checks.push(line === 0);
71
80
  }
72
81
  if (!edit || !selected) continue;
73
82
  cursor = todoCursor(text, edit, inner, rowStart);
74
83
  }
75
- return { rows, owners, cursor };
84
+ return { rows, owners, checks, cursor };
76
85
  };
77
86
 
78
87
  const noteInnerWidth = (width) => Math.max(1, width - NOTE_PAD * 2);
@@ -184,13 +193,21 @@ const paintBodyTodo = (view, width, color) => {
184
193
  const caption = paintTodoCaption(width, color);
185
194
  const body = [caption, ...painted.rows];
186
195
  const todoOwners = [null, ...painted.owners];
196
+ const todoChecks = [false, ...painted.checks];
187
197
  let cursor = null;
188
198
  if (painted.cursor) {
189
199
  cursor = { x: painted.cursor.x, row: painted.cursor.row + 1 };
190
200
  }
191
201
  const fileHits = [];
192
202
  const splitBody = false;
193
- return paneResult({ body, fileHits, todoOwners, cursor, splitBody });
203
+ return paneResult({
204
+ body,
205
+ fileHits,
206
+ todoOwners,
207
+ todoChecks,
208
+ cursor,
209
+ splitBody,
210
+ });
194
211
  };
195
212
 
196
213
  module.exports = {
@@ -0,0 +1,228 @@
1
+ 'use strict';
2
+
3
+ const ansi = require('../ansi.js');
4
+ const chrome = require('./chrome.js');
5
+ const primitives = require('./primitives.js');
6
+ const npmCommands = require('../npm-commands.js');
7
+
8
+ const { THEME, paint, visibleWidth, truncateVisible, seq, EL } = ansi;
9
+ const { clipAnsi, stripAnsi, RESET, foregroundOn } = ansi;
10
+ const { formatBusyStatus } = chrome;
11
+ const { TABLE_KEY, TABLE_VAL } = npmCommands;
12
+ const { fill, FILE_MARK, paneResult, paintCursorList } = primitives;
13
+ const { paintListEdit } = primitives;
14
+
15
+ const EDGE = 2;
16
+ const TAIL = 1;
17
+ const LOG_PAD_Y = 1;
18
+ const LOG_PAD_X = 2;
19
+ const NPM_NAME_MAX = 24;
20
+
21
+ const logViewRows = (bodyH) => {
22
+ const height = Math.max(1, bodyH);
23
+ if (height >= LOG_PAD_Y * 2 + 1) return height - LOG_PAD_Y * 2;
24
+ return height;
25
+ };
26
+
27
+ const rowFg = (selected) => (selected ? THEME.chromeFg : THEME.mutedFg);
28
+
29
+ const npmNameWidth = (commands) => {
30
+ let width = 0;
31
+ for (const entry of commands) {
32
+ width = Math.max(width, visibleWidth(`${entry.name ?? ''}`));
33
+ }
34
+ return Math.min(NPM_NAME_MAX, width);
35
+ };
36
+
37
+ const paintNpmRow = (entry, width, color, selected, nameCol = 0) => {
38
+ const inner = Math.max(1, width - EDGE - TAIL);
39
+ const mark = selected ? FILE_MARK : ' ';
40
+ const prefix = ` ${mark} `;
41
+ const prefixW = visibleWidth(prefix);
42
+ const rest = Math.max(1, inner - prefixW);
43
+ const detail = entry.kind === 'bin' ? 'bin' : entry.command;
44
+ const nameW = Math.min(Math.max(0, nameCol), rest);
45
+ const name = truncateVisible(entry.name, nameW);
46
+ const gap = rest > nameW + 2 ? ' ' : '';
47
+ const detailW = Math.max(0, rest - nameW - visibleWidth(gap));
48
+ const shown = truncateVisible(detail, detailW);
49
+ const mid = `${name}${' '.repeat(Math.max(0, nameW - visibleWidth(name)))}`;
50
+ const after = `${gap}${shown}`;
51
+ const pad = Math.max(0, rest - visibleWidth(mid + after));
52
+ const left = `${prefix}${mid}${after}${' '.repeat(pad)}`;
53
+ const fgRgb = rowFg(selected);
54
+ const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
55
+ const tail = ' '.repeat(TAIL);
56
+ const edge = fill(EDGE, fgRgb, THEME.ctxBg, color);
57
+ if (!color) return `${left}${tail}${edge}`;
58
+ let out = `${seq(fgRgb, bgRgb)}${EL}`;
59
+ out += paint(prefix, fgRgb, bgRgb, color);
60
+ const bin = entry.kind === 'bin';
61
+ const nameFg = bin ? THEME.shaFg : THEME.buttonHotFg;
62
+ out += paint(mid, nameFg, bgRgb, color, !bin);
63
+ out += paint(`${after}${' '.repeat(pad)}${tail}`, fgRgb, bgRgb, color);
64
+ return out + edge;
65
+ };
66
+
67
+ const paintNpmList = (view, width, color, bodyH, headerLines) => {
68
+ const commands = view.npmCommands ?? [];
69
+ const nameW = npmNameWidth(commands);
70
+ const cursor = view.npmCursor ?? 0;
71
+ const compose = view.compose;
72
+ const editing = compose && compose.kind === 'npm';
73
+ const paintRow = (entry, rowWidth, rowColor, selected) =>
74
+ paintNpmRow(entry, rowWidth, rowColor, selected, nameW);
75
+ let extra = null;
76
+ if (editing) {
77
+ const painted = paintListEdit(compose, width, color, EDGE);
78
+ const at = view.npmEditName ? 'replace' : 'end';
79
+ extra = { row: painted.row, cursor: painted.cursor, at, index: cursor };
80
+ }
81
+ const painted = paintCursorList(
82
+ commands,
83
+ cursor,
84
+ width,
85
+ color,
86
+ bodyH,
87
+ headerLines,
88
+ paintRow,
89
+ extra,
90
+ view.listScroll,
91
+ );
92
+ return paneResult({
93
+ body: painted.body,
94
+ fileHits: painted.fileHits,
95
+ todoOwners: [],
96
+ cursor: painted.cursor,
97
+ splitBody: false,
98
+ listScroll: painted.offset,
99
+ });
100
+ };
101
+
102
+ const logStart = (view, count, rows) => {
103
+ const maxStart = Math.max(0, count - rows);
104
+ let start = maxStart;
105
+ if (!view.npmFollow) start = view.npmScroll ?? 0;
106
+ if (start > maxStart) start = maxStart;
107
+ if (start < 0) start = 0;
108
+ return start;
109
+ };
110
+
111
+ const isTableLine = (line) => `${line ?? ''}`.startsWith(TABLE_KEY);
112
+
113
+ const mixRgb = (from, to, part) => {
114
+ const out = [0, 0, 0];
115
+ for (let i = 0; i < 3; i++) {
116
+ const next = from[i] + (to[i] - from[i]) * part;
117
+ out[i] = Math.round(next);
118
+ }
119
+ return out;
120
+ };
121
+
122
+ const tableTone = (index) => {
123
+ const lift = index % 2 === 0 ? 0.035 : 0.07;
124
+ const valueBg = mixRgb(THEME.ctxBg, THEME.chromeFg, lift);
125
+ const keyBg = mixRgb(THEME.ctxBg, THEME.chromeFg, lift + 0.025);
126
+ const frame = mixRgb(THEME.ctxBg, THEME.chromeFg, lift * 0.4);
127
+ return { valueBg, keyBg, frame };
128
+ };
129
+
130
+ const tableIndex = (lines, index) => {
131
+ let at = index;
132
+ let row = 0;
133
+ while (at > 0 && isTableLine(lines[at - 1])) {
134
+ at -= 1;
135
+ row += 1;
136
+ }
137
+ return row;
138
+ };
139
+
140
+ const paintTableLine = (line, width, index) => {
141
+ const raw = line.slice(TABLE_KEY.length);
142
+ const at = raw.indexOf(TABLE_VAL);
143
+ const key = at < 0 ? raw : raw.slice(0, at);
144
+ const rest = at < 0 ? '' : raw.slice(at + TABLE_VAL.length);
145
+ const gap = rest.startsWith(' ') ? ' ' : '';
146
+ const value = rest.slice(gap.length);
147
+ const edge = 1;
148
+ const inner = Math.max(1, width - edge * 2);
149
+ const keyW = visibleWidth(key);
150
+ const gapW = visibleWidth(gap);
151
+ const valueW = Math.max(0, inner - keyW - gapW);
152
+ const shown = truncateVisible(value, valueW);
153
+ const pad = ' '.repeat(Math.max(0, valueW - visibleWidth(shown)));
154
+ const tone = tableTone(index);
155
+ const ink = THEME.ctxFg;
156
+ let out = fill(edge, ink, tone.frame, true);
157
+ out += paint(key, ink, tone.keyBg, true);
158
+ out += paint(gap, ink, tone.frame, true);
159
+ out += paint(`${shown}${pad}`, ink, tone.valueBg, true);
160
+ return `${out}${fill(edge, ink, tone.frame, true)}`;
161
+ };
162
+
163
+ const paintLogLine = (line, width, color, fgRgb = THEME.ctxFg) => {
164
+ const inner = Math.max(1, width - LOG_PAD_X * 2);
165
+ const source = color ? `${line ?? ''}` : stripAnsi(line);
166
+ const clip = (value) =>
167
+ color ? clipAnsi(value, inner) : truncateVisible(value, inner);
168
+ const body = color ? foregroundOn(clip(source), THEME.ctxBg) : clip(source);
169
+ const lead = ' '.repeat(LOG_PAD_X);
170
+ const pad = Math.max(0, width - LOG_PAD_X - visibleWidth(body));
171
+ const tail = ' '.repeat(pad);
172
+ if (!color) return `${lead}${body}${tail}`;
173
+ const base = seq(fgRgb, THEME.ctxBg);
174
+ let out = `${base}${EL}${lead}${body}`;
175
+ if (pad) out += `${RESET}${base}${tail}`;
176
+ return `${out}${RESET}`;
177
+ };
178
+
179
+ const paintRunBar = (width, color, frame) => {
180
+ const label = formatBusyStatus('running', frame);
181
+ return paintLogLine(label, width, color, THEME.chromeFg);
182
+ };
183
+
184
+ const paintNpmLog = (view, width, color, bodyH) => {
185
+ const text = view.npmOutput ?? '';
186
+ const lines = text.split('\n');
187
+ if (lines.length && lines.at(-1) === '') lines.pop();
188
+ const running = view.npmRunning === true;
189
+ const count = lines.length + (running ? 1 : 0);
190
+ const rows = logViewRows(bodyH);
191
+ const padY = bodyH - rows;
192
+ const top = padY ? 1 : 0;
193
+ const start = logStart(view, count, rows);
194
+ const blank = fill(width, THEME.ctxFg, THEME.ctxBg, color);
195
+ const body = [];
196
+ for (let i = 0; i < top; i++) body.push(blank);
197
+ for (let i = 0; i < rows; i++) {
198
+ const index = start + i;
199
+ if (index < lines.length) {
200
+ const line = lines[index];
201
+ if (color && isTableLine(line)) {
202
+ const row = tableIndex(lines, index);
203
+ body.push(paintTableLine(line, width, row));
204
+ } else {
205
+ body.push(paintLogLine(line, width, color));
206
+ }
207
+ } else if (running && index === lines.length) {
208
+ body.push(paintRunBar(width, color, view.progressFrame ?? 0));
209
+ } else {
210
+ body.push(blank);
211
+ }
212
+ }
213
+ for (let i = 0; i < padY - top; i++) body.push(blank);
214
+ return paneResult({
215
+ body,
216
+ fileHits: [],
217
+ todoOwners: [],
218
+ cursor: null,
219
+ splitBody: false,
220
+ });
221
+ };
222
+
223
+ const paintBodyNpm = (view, width, color, bodyH, headerLines) => {
224
+ if (view.npmView) return paintNpmLog(view, width, color, bodyH);
225
+ return paintNpmList(view, width, color, bodyH, headerLines);
226
+ };
227
+
228
+ module.exports = { paintBodyNpm, logViewRows };
@@ -8,7 +8,7 @@ const GUTTER_W = 2;
8
8
  const FILE_MARK = '▶';
9
9
  const FILE_LIST_PAD = 1;
10
10
  const CHECK_DONE = 'xX';
11
- const LIST_PANES = ['files', 'branches', 'commits'];
11
+ const LIST_PANES = ['files', 'branches', 'commits', 'npm'];
12
12
  const CHECK_TOKEN = /\[([ xX])\]/;
13
13
  const CHECK_LEAD = /^\[[ xX]\] /;
14
14
 
@@ -104,6 +104,13 @@ const listWindowStart = (focus, offset, listH, count) => {
104
104
  return start;
105
105
  };
106
106
 
107
+ const extraIdx = (extraRow, cursor, count) => {
108
+ if (!extraRow) return -1;
109
+ if (extraRow.at === 'start') return 0;
110
+ if (extraRow.at === 'replace') return extraRow.index ?? cursor;
111
+ return count;
112
+ };
113
+
107
114
  const paintCursorList = (
108
115
  items,
109
116
  cursor,
@@ -112,15 +119,16 @@ const paintCursorList = (
112
119
  bodyH,
113
120
  headerLines,
114
121
  paintRow,
115
- tail,
122
+ extraRow,
116
123
  offset = 0,
117
124
  ) => {
118
125
  const pad = bodyH >= FILE_LIST_PAD * 2 + 1 ? FILE_LIST_PAD : 0;
119
126
  const listH = bodyH - pad * 2;
120
- const extra = tail ? 1 : 0;
127
+ const inserting = extraRow && extraRow.at === 'start';
128
+ const extra = extraRow && extraRow.at !== 'replace' ? 1 : 0;
121
129
  const count = items.length + extra;
122
- const last = count - 1;
123
- const focus = extra ? last : cursor;
130
+ const extraAt = extraIdx(extraRow, cursor, items.length);
131
+ const focus = extraRow ? extraAt : cursor;
124
132
  const start = listWindowStart(focus, offset, listH, count);
125
133
  const blank = paintBodyFill(width, color, 'files');
126
134
  const body = [];
@@ -129,52 +137,57 @@ const paintCursorList = (
129
137
  if (pad) body.push(blank);
130
138
  for (let i = 0; i < listH; i++) {
131
139
  const idx = start + i;
132
- if (tail && idx === items.length) {
133
- body.push(tail.row);
134
- if (tail.cursor) {
135
- textCursor = { x: tail.cursor.x, row: body.length - 1 };
140
+ if (extraRow && idx === extraAt) {
141
+ body.push(extraRow.row);
142
+ if (extraRow.cursor) {
143
+ textCursor = { x: extraRow.cursor.x, row: body.length - 1 };
136
144
  }
137
145
  continue;
138
146
  }
139
- const entry = items[idx];
147
+ const itemIdx = inserting ? idx - 1 : idx;
148
+ const entry = items[itemIdx];
140
149
  if (!entry) break;
141
- const selected = extra ? false : idx === cursor;
150
+ const selected = extraRow ? false : itemIdx === cursor;
142
151
  body.push(paintRow(entry, width, color, selected));
143
152
  const y = headerLines + pad + i + 1;
144
- fileHits.push({ y, cursor: idx });
153
+ fileHits.push({ y, cursor: itemIdx });
145
154
  }
146
155
  if (pad) body.push(blank);
147
156
  return { body, fileHits, cursor: textCursor, offset: start };
148
157
  };
149
158
 
150
- const branchLabel = (name) => {
151
- const branch = `${name ?? ''}`.trim();
152
- if (!branch) return '';
153
- return `[${branch}]`;
154
- };
155
-
156
- const splitBracketName = (label) => {
157
- const text = `${label ?? ''}`;
158
- const open = text.startsWith('[') ? '[' : '';
159
- const rest = open ? text.slice(1) : text;
160
- const close = rest.endsWith(']') ? ']' : '';
161
- const name = close ? rest.slice(0, -1) : rest;
162
- return { open, name, close };
163
- };
159
+ const branchLabel = (name) => `${name ?? ''}`.trim();
164
160
 
165
- const paintBracketName = (label, nameFg, wrapFg, bgRgb, color, nameBold) => {
166
- const { open, name, close } = splitBracketName(label);
167
- return (
168
- paint(open, wrapFg, bgRgb, color, true) +
169
- paint(name, nameFg, bgRgb, color, nameBold) +
170
- paint(close, wrapFg, bgRgb, color, true)
171
- );
161
+ const paintListEdit = (compose, width, color, edgeW = 0) => {
162
+ const inner = Math.max(1, width - edgeW);
163
+ const prefix = ` ${FILE_MARK} `;
164
+ const prefixW = visibleWidth(prefix);
165
+ const rest = Math.max(1, inner - prefixW);
166
+ const text = `${compose.text ?? ''}`.replaceAll('\n', ' ');
167
+ const clipped = truncateVisible(text, rest);
168
+ const pad = Math.max(0, rest - visibleWidth(clipped));
169
+ const left = `${prefix}${clipped}${' '.repeat(pad)}`;
170
+ const fgRgb = THEME.chromeFg;
171
+ const bgRgb = THEME.buttonBg;
172
+ const edge = fill(edgeW, fgRgb, THEME.ctxBg, color);
173
+ let row = `${left}${edge}`;
174
+ if (color) {
175
+ const gap = ' '.repeat(pad);
176
+ row = `${seq(fgRgb, bgRgb)}${EL}`;
177
+ row += paint(prefix, fgRgb, bgRgb, color);
178
+ row += paint(clipped, THEME.buttonHotFg, bgRgb, color);
179
+ row += paint(gap, fgRgb, bgRgb, color);
180
+ row += edge;
181
+ }
182
+ const col = Math.min(compose.cursor ?? 0, visibleWidth(clipped));
183
+ return { row, cursor: { x: prefixW + col + 1 } };
172
184
  };
173
185
 
174
186
  const paneResult = (fields) => ({
175
187
  body: fields.body,
176
188
  fileHits: fields.fileHits,
177
189
  todoOwners: fields.todoOwners,
190
+ todoChecks: fields.todoChecks ?? [],
178
191
  cursor: fields.cursor ?? null,
179
192
  splitBody: fields.splitBody === true,
180
193
  listScroll: fields.listScroll ?? 0,
@@ -238,8 +251,9 @@ module.exports = {
238
251
  paintSplitRule,
239
252
  paintBodyFill,
240
253
  windowRows,
254
+ listWindowStart,
241
255
  paintCursorList,
256
+ paintListEdit,
242
257
  branchLabel,
243
- paintBracketName,
244
258
  paneResult,
245
259
  };
@@ -9,7 +9,7 @@ const { renderFrame } = require('./frame.js');
9
9
  const { QUIT_PROMPT, PUSH_PROMPT } = chrome;
10
10
  const { updatePrompt, dropPrompt } = chrome;
11
11
  const { formatBusyStatus, headerText, layoutButtons } = chrome;
12
- const { DEL_CHAR_BG, ADD_CHAR_BG, paintDiffLine, codeInnerWidth } = diff;
12
+ const { delCharBg, addCharBg, paintDiffLine, codeInnerWidth } = diff;
13
13
  const { todoInnerWidth, todoBodyWidth, noteInnerWidth } = notes;
14
14
  const { presentCursor, presentRows } = present;
15
15
 
@@ -18,8 +18,8 @@ module.exports = {
18
18
  PUSH_PROMPT,
19
19
  updatePrompt,
20
20
  dropPrompt,
21
- DEL_CHAR_BG,
22
- ADD_CHAR_BG,
21
+ delCharBg,
22
+ addCharBg,
23
23
  paintDiffLine,
24
24
  headerText,
25
25
  layoutButtons,