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
@@ -5,9 +5,9 @@ const wrap = require('../wrap.js');
5
5
  const files = require('../files.js');
6
6
  const primitives = require('./primitives.js');
7
7
 
8
- const { REPO_TODOS_LABEL } = files;
9
- const { wrapMultiline, cursorInWrap } = wrap;
10
- const { THEME, visibleWidth, truncateVisible, seq, EL, RESET, BOLD } = ansi;
8
+ const { REPO_TASKS_LABEL } = files;
9
+ const { wrapMultiline, wrapDoc, cursorInWrap } = wrap;
10
+ const { THEME, truncateVisible } = ansi;
11
11
  const { paintBar, paintCheckBar, windowRows } = primitives;
12
12
  const { LIST_PANES, CHECK_LEAD, paneResult } = primitives;
13
13
 
@@ -53,11 +53,35 @@ const todoCursor = (text, edit, inner, rowStart) => {
53
53
  return { x: pos.col + lead + 2, row: rowStart + pos.row };
54
54
  };
55
55
 
56
+ const taskEditHits = (text, inner, rowStart, width) => {
57
+ const lead = checkLeadLen(text);
58
+ const body = text.slice(lead);
59
+ const bodyWidth = Math.max(1, inner - lead);
60
+ const doc = wrapDoc(body, bodyWidth);
61
+ const textX = lead + 2;
62
+ const hits = [];
63
+ for (let line = 0; line < doc.length; line++) {
64
+ const piece = doc[line];
65
+ hits.push({
66
+ row: rowStart + line,
67
+ x0: lead + 1,
68
+ textX,
69
+ x1: width + 1,
70
+ start: piece.start,
71
+ text: piece.text,
72
+ limit: piece.start + piece.text.length,
73
+ live: true,
74
+ });
75
+ }
76
+ return hits;
77
+ };
78
+
56
79
  const paintTodoList = (texts, focus, width, color, edit) => {
57
80
  const inner = todoInnerWidth(width);
58
81
  const rows = [];
59
82
  const owners = [];
60
83
  const checks = [];
84
+ const editHits = [];
61
85
  let cursor = null;
62
86
  for (let i = 0; i < texts.length; i++) {
63
87
  const selected = i === focus;
@@ -73,6 +97,7 @@ const paintTodoList = (texts, focus, width, color, edit) => {
73
97
  fgRgb,
74
98
  bgRgb,
75
99
  color,
100
+ true,
76
101
  );
77
102
  rows.push(painted);
78
103
  owners.push(i);
@@ -80,8 +105,10 @@ const paintTodoList = (texts, focus, width, color, edit) => {
80
105
  }
81
106
  if (!edit || !selected) continue;
82
107
  cursor = todoCursor(text, edit, inner, rowStart);
108
+ const hits = taskEditHits(text, inner, rowStart, width);
109
+ for (const hit of hits) editHits.push(hit);
83
110
  }
84
- return { rows, owners, checks, cursor };
111
+ return { rows, owners, checks, cursor, editHits };
85
112
  };
86
113
 
87
114
  const noteInnerWidth = (width) => Math.max(1, width - NOTE_PAD * 2);
@@ -100,11 +127,23 @@ const paintTemplateRow = (text, selected, width, color) => {
100
127
  return paintNoteLine(text, width, fgRgb, bgRgb, color);
101
128
  };
102
129
 
130
+ const noteEditHit = (piece, row, width) => ({
131
+ row,
132
+ x0: 1,
133
+ textX: NOTE_PAD + 1,
134
+ x1: width + 1,
135
+ start: piece.start,
136
+ text: piece.text,
137
+ limit: piece.start + piece.text.length,
138
+ live: true,
139
+ });
140
+
103
141
  const paintComposeNote = (view, width, color, cap) => {
104
142
  const inner = noteInnerWidth(width);
105
143
  const bgRgb = THEME.noteBg;
106
144
  const compose = view.compose;
107
- const wrapped = wrapMultiline(compose.text, inner);
145
+ const doc = wrapDoc(compose.text ?? '', inner);
146
+ const wrapped = doc.map((piece) => piece.text);
108
147
  const { row, col } = cursorInWrap(compose.text, compose.cursor ?? 0, inner);
109
148
  const shown = windowRows(wrapped, row, INPUT_MAX);
110
149
  const rows = [];
@@ -123,8 +162,19 @@ const paintComposeNote = (view, width, color, cap) => {
123
162
  }
124
163
  }
125
164
  const templateCount = rows.length;
126
- for (const line of shown.rows) {
127
- rows.push(paintNoteLine(line, width, THEME.chromeFg, bgRgb, color));
165
+ const editHits = [];
166
+ for (let i = 0; i < shown.rows.length; i++) {
167
+ const piece = doc[shown.offset + i];
168
+ const painted = paintNoteLine(
169
+ shown.rows[i],
170
+ width,
171
+ THEME.chromeFg,
172
+ bgRgb,
173
+ color,
174
+ );
175
+ rows.push(painted);
176
+ if (!piece) continue;
177
+ editHits.push(noteEditHit(piece, templateCount + i, width));
128
178
  }
129
179
  return {
130
180
  rows,
@@ -133,11 +183,12 @@ const paintComposeNote = (view, width, color, cap) => {
133
183
  row: templateCount + row - shown.offset,
134
184
  },
135
185
  templateHits,
186
+ editHits,
136
187
  };
137
188
  };
138
189
 
139
190
  const paintIdleNote = (view, width, color) => {
140
- const empty = { rows: [], cursor: null, templateHits: [] };
191
+ const empty = { rows: [], cursor: null, templateHits: [], editHits: [] };
141
192
  const text = view.noteText;
142
193
  if (!text) return empty;
143
194
  const inner = noteInnerWidth(width);
@@ -158,7 +209,7 @@ const paintIdleNote = (view, width, color) => {
158
209
  };
159
210
 
160
211
  const paintNotePanel = (view, width, color, cap) => {
161
- const empty = { rows: [], cursor: null, templateHits: [] };
212
+ const empty = { rows: [], cursor: null, templateHits: [], editHits: [] };
162
213
  const compose = view.compose;
163
214
  if (compose && NOTE_COMPOSE.includes(compose.kind)) {
164
215
  return paintComposeNote(view, width, color, cap);
@@ -168,43 +219,40 @@ const paintNotePanel = (view, width, color, cap) => {
168
219
  return paintIdleNote(view, width, color);
169
220
  };
170
221
 
171
- const paintTodoCaption = (width, color) => {
172
- const text = ` ${REPO_TODOS_LABEL}`;
173
- const clipped = truncateVisible(text, width);
174
- const pad = Math.max(0, width - visibleWidth(clipped));
175
- const filled = clipped + ' '.repeat(pad);
176
- if (!color) return filled;
177
- const fgRgb = THEME.buttonHotFg;
178
- const bgRgb = THEME.ctxBg;
179
- return `${BOLD}${seq(fgRgb, bgRgb)}${EL}${filled}${RESET}`;
180
- };
222
+ const paintTodoCaption = (width, color) =>
223
+ paintBar(` ${REPO_TASKS_LABEL}`, width, THEME.mutedFg, THEME.ctxBg, color);
181
224
 
182
225
  const paintBodyTodo = (view, width, color) => {
183
- const texts = view.todos ?? [];
226
+ const texts = view.tasks ?? [];
184
227
  let lines = texts;
185
228
  if (!lines.length && view.noteText) lines = [view.noteText];
186
229
  const painted = paintTodoList(
187
230
  lines,
188
- view.todoFocus ?? 0,
231
+ view.tasksFocus ?? 0,
189
232
  width,
190
233
  color,
191
- view.todoEdit,
234
+ view.taskEdit,
192
235
  );
193
236
  const caption = paintTodoCaption(width, color);
194
237
  const body = [caption, ...painted.rows];
195
- const todoOwners = [null, ...painted.owners];
196
- const todoChecks = [false, ...painted.checks];
238
+ const taskOwners = [null, ...painted.owners];
239
+ const taskChecks = [false, ...painted.checks];
197
240
  let cursor = null;
198
241
  if (painted.cursor) {
199
242
  cursor = { x: painted.cursor.x, row: painted.cursor.row + 1 };
200
243
  }
244
+ const editHits = [];
245
+ for (const hit of painted.editHits) {
246
+ editHits.push({ ...hit, row: hit.row + 1 });
247
+ }
201
248
  const fileHits = [];
202
249
  const splitBody = false;
203
250
  return paneResult({
204
251
  body,
205
252
  fileHits,
206
- todoOwners,
207
- todoChecks,
253
+ editHits,
254
+ taskOwners,
255
+ taskChecks,
208
256
  cursor,
209
257
  splitBody,
210
258
  });
package/lib/render/npm.js CHANGED
@@ -10,7 +10,7 @@ const { clipAnsi, stripAnsi, RESET, foregroundOn } = ansi;
10
10
  const { formatBusyStatus } = chrome;
11
11
  const { TABLE_KEY, TABLE_VAL } = npmCommands;
12
12
  const { fill, FILE_MARK, paneResult, paintCursorList } = primitives;
13
- const { paintListEdit } = primitives;
13
+ const { fieldWindow } = primitives;
14
14
 
15
15
  const EDGE = 2;
16
16
  const TAIL = 1;
@@ -64,39 +64,117 @@ const paintNpmRow = (entry, width, color, selected, nameCol = 0) => {
64
64
  return out + edge;
65
65
  };
66
66
 
67
+ const editNameSource = (view, compose, field) => {
68
+ if (field === 'name') return compose.text;
69
+ return view.npmDraftName ?? '';
70
+ };
71
+
72
+ const editNameWidth = (commands, live) => {
73
+ const typed = visibleWidth(`${live ?? ''}`);
74
+ return Math.min(NPM_NAME_MAX, Math.max(npmNameWidth(commands), typed));
75
+ };
76
+
77
+ const paintNpmEdit = (view, width, color, nameW) => {
78
+ const compose = view.compose;
79
+ const field = view.npmEditField === 'command' ? 'command' : 'name';
80
+ const nameSource = editNameSource(view, compose, field);
81
+ const draft = view.npmDraftCommand ?? '';
82
+ const commandSource = field === 'command' ? compose.text : draft;
83
+ const inner = Math.max(1, width - EDGE - TAIL);
84
+ const prefix = ` ${FILE_MARK} `;
85
+ const prefixW = visibleWidth(prefix);
86
+ const rest = Math.max(1, inner - prefixW);
87
+ const colW = Math.min(Math.max(0, nameW), rest);
88
+ const flatName = `${nameSource ?? ''}`.replaceAll('\n', ' ');
89
+ const nameAt = field === 'name' ? compose.cursor : 0;
90
+ const nameScroll = field === 'name' ? compose.scrollCol : 0;
91
+ const nameWin = fieldWindow(flatName, nameAt, nameScroll, colW);
92
+ const name = nameWin.text;
93
+ const gap = rest > colW + 2 ? ' ' : '';
94
+ const gapW = visibleWidth(gap);
95
+ const detailW = Math.max(0, rest - colW - gapW);
96
+ const flatCommand = `${commandSource ?? ''}`.replaceAll('\n', ' ');
97
+ const commandAt = field === 'command' ? compose.cursor : 0;
98
+ const commandScroll = field === 'command' ? compose.scrollCol : 0;
99
+ const commandWin = fieldWindow(
100
+ flatCommand,
101
+ commandAt,
102
+ commandScroll,
103
+ detailW,
104
+ );
105
+ const command = commandWin.text;
106
+ const namePad = ' '.repeat(Math.max(0, colW - visibleWidth(name)));
107
+ const mid = `${name}${namePad}`;
108
+ const after = `${gap}${command}`;
109
+ const pad = Math.max(0, rest - visibleWidth(mid + after));
110
+ const tail = ' '.repeat(TAIL);
111
+ const left = `${prefix}${mid}${after}${' '.repeat(pad)}`;
112
+ const fgRgb = THEME.chromeFg;
113
+ const bgRgb = THEME.buttonBg;
114
+ const edge = fill(EDGE, fgRgb, THEME.ctxBg, color);
115
+ let row = `${left}${tail}${edge}`;
116
+ if (color) {
117
+ row = `${seq(fgRgb, bgRgb)}${EL}`;
118
+ row += paint(prefix, fgRgb, bgRgb, color);
119
+ row += paint(mid, THEME.buttonHotFg, bgRgb, color, true);
120
+ row += paint(`${after}${' '.repeat(pad)}${tail}`, fgRgb, bgRgb, color);
121
+ row += edge;
122
+ }
123
+ const active = field === 'name' ? nameWin : commandWin;
124
+ const lead = field === 'name' ? 0 : colW + gapW;
125
+ const nameX = prefixW + 1;
126
+ const commandX = prefixW + colW + gapW + 1;
127
+ const edits = [
128
+ {
129
+ field: 'name',
130
+ x0: 1,
131
+ textX: nameX,
132
+ x1: commandX,
133
+ start: 0,
134
+ text: flatName,
135
+ limit: flatName.length,
136
+ scroll: nameWin.scroll,
137
+ live: field === 'name',
138
+ },
139
+ {
140
+ field: 'command',
141
+ x0: commandX,
142
+ textX: commandX,
143
+ x1: width + 1,
144
+ start: 0,
145
+ text: flatCommand,
146
+ limit: flatCommand.length,
147
+ scroll: commandWin.scroll,
148
+ live: field === 'command',
149
+ },
150
+ ];
151
+ const cursor = {
152
+ x: prefixW + lead + active.col + 1,
153
+ scroll: active.scroll,
154
+ };
155
+ return { row, cursor, edits };
156
+ };
157
+
67
158
  const paintNpmList = (view, width, color, bodyH, headerLines) => {
68
159
  const commands = view.npmCommands ?? [];
69
- const nameW = npmNameWidth(commands);
70
- const cursor = view.npmCursor ?? 0;
71
160
  const compose = view.compose;
72
161
  const editing = compose && compose.kind === 'npm';
73
- const paintRow = (entry, rowWidth, rowColor, selected) =>
74
- paintNpmRow(entry, rowWidth, rowColor, selected, nameW);
162
+ const field = view.npmEditField === 'command' ? 'command' : 'name';
163
+ const live = editing ? editNameSource(view, compose, field) : '';
164
+ const nameW = editing
165
+ ? editNameWidth(commands, live)
166
+ : npmNameWidth(commands);
167
+ const cursor = view.npmCursor ?? 0;
168
+ const pane = { width, color, bodyH, headerLines, offset: view.listScroll };
169
+ const paintRow = (entry, selected) =>
170
+ paintNpmRow(entry, width, color, selected, nameW);
75
171
  let extra = null;
76
172
  if (editing) {
77
- const painted = paintListEdit(compose, width, color, EDGE);
173
+ const painted = paintNpmEdit(view, width, color, nameW);
78
174
  const at = view.npmEditName ? 'replace' : 'end';
79
- extra = { row: painted.row, cursor: painted.cursor, at, index: cursor };
175
+ extra = { ...painted, at, index: cursor };
80
176
  }
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
- });
177
+ return paintCursorList(commands, cursor, pane, paintRow, extra);
100
178
  };
101
179
 
102
180
  const logStart = (view, count, rows) => {
@@ -213,10 +291,6 @@ const paintNpmLog = (view, width, color, bodyH) => {
213
291
  for (let i = 0; i < padY - top; i++) body.push(blank);
214
292
  return paneResult({
215
293
  body,
216
- fileHits: [],
217
- todoOwners: [],
218
- cursor: null,
219
- splitBody: false,
220
294
  });
221
295
  };
222
296
 
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { ESC } = require('../ansi.js');
3
+ const ansi = require('../ansi.js');
4
+ const { ESC } = ansi;
4
5
 
5
6
  const presentCursor = (cursor) => {
6
7
  if (!cursor || cursor.x <= 0 || cursor.y <= 0) return `${ESC}[?25l`;
@@ -1,14 +1,16 @@
1
1
  'use strict';
2
2
 
3
3
  const ansi = require('../ansi.js');
4
+ const actions = require('../session/actions.js');
5
+ const { LIST_PANES } = actions;
4
6
 
5
7
  const { THEME, paint, visibleWidth, truncateVisible, seq, EL, RESET } = ansi;
8
+ const { graphemes, graphemeWidth } = ansi;
6
9
 
7
10
  const GUTTER_W = 2;
8
11
  const FILE_MARK = '▶';
9
- const FILE_LIST_PAD = 1;
12
+ const LIST_PAD = 1;
10
13
  const CHECK_DONE = 'xX';
11
- const LIST_PANES = ['files', 'branches', 'commits', 'npm'];
12
14
  const CHECK_TOKEN = /\[([ xX])\]/;
13
15
  const CHECK_LEAD = /^\[[ xX]\] /;
14
16
 
@@ -34,14 +36,21 @@ const splitCheck = (text) => {
34
36
  return { before, mark, token, after };
35
37
  };
36
38
 
37
- const paintCheckBar = (text, width, fgRgb, bgRgb, color) => {
39
+ const paintCheckBar = (text, width, fgRgb, bgRgb, color, plain = false) => {
38
40
  const clipped = truncateVisible(text, width);
39
41
  const parts = color ? splitCheck(clipped) : null;
40
42
  if (!parts) return paintBar(text, width, fgRgb, bgRgb, color);
41
43
  const pad = Math.max(0, width - visibleWidth(clipped));
42
44
  const done = CHECK_DONE.includes(parts.mark);
43
- const chipFg = done ? THEME.checkDoneFg : THEME.checkFg;
44
- const chipBg = done ? THEME.checkDoneBg : THEME.checkBg;
45
+ let chipFg = THEME.checkFg;
46
+ let chipBg = THEME.checkBg;
47
+ if (plain) {
48
+ chipFg = fgRgb;
49
+ chipBg = bgRgb;
50
+ } else if (done) {
51
+ chipFg = THEME.checkDoneFg;
52
+ chipBg = THEME.checkDoneBg;
53
+ }
45
54
  const after = `${parts.after}${' '.repeat(pad)}`;
46
55
  let out = `${seq(fgRgb, bgRgb)}${EL}`;
47
56
  if (parts.before) out += paint(parts.before, fgRgb, bgRgb, color);
@@ -111,49 +120,110 @@ const extraIdx = (extraRow, cursor, count) => {
111
120
  return count;
112
121
  };
113
122
 
114
- const paintCursorList = (
115
- items,
116
- cursor,
117
- width,
118
- color,
119
- bodyH,
120
- headerLines,
121
- paintRow,
122
- extraRow,
123
- offset = 0,
124
- ) => {
125
- const pad = bodyH >= FILE_LIST_PAD * 2 + 1 ? FILE_LIST_PAD : 0;
123
+ const paneResult = (fields = {}) => ({
124
+ body: fields.body ?? [],
125
+ fileHits: fields.fileHits ?? [],
126
+ taskOwners: fields.taskOwners ?? [],
127
+ taskChecks: fields.taskChecks ?? [],
128
+ cursor: fields.cursor ?? null,
129
+ splitBody: fields.splitBody === true,
130
+ listScroll: fields.listScroll ?? 0,
131
+ editHits: fields.editHits ?? [],
132
+ });
133
+
134
+ const NO_EDITS = [];
135
+
136
+ const listLine = (row, index, cursor = null, edits = NO_EDITS) => {
137
+ const hit = index >= 0;
138
+ return { row, index, hit, cursor, edits };
139
+ };
140
+
141
+ const rowCursor = (cursor, row) => {
142
+ const out = { x: cursor.x, row };
143
+ if (typeof cursor.scroll === 'number') out.scroll = cursor.scroll;
144
+ return out;
145
+ };
146
+
147
+ const paintListWindow = (count, focus, pane, lineAt) => {
148
+ const { width, color, bodyH, headerLines, offset } = pane;
149
+ const pad = bodyH >= LIST_PAD * 2 + 1 ? LIST_PAD : 0;
126
150
  const listH = bodyH - pad * 2;
127
- const inserting = extraRow && extraRow.at === 'start';
128
- const extra = extraRow && extraRow.at !== 'replace' ? 1 : 0;
129
- const count = items.length + extra;
130
- const extraAt = extraIdx(extraRow, cursor, items.length);
131
- const focus = extraRow ? extraAt : cursor;
132
151
  const start = listWindowStart(focus, offset, listH, count);
133
152
  const blank = paintBodyFill(width, color, 'files');
134
153
  const body = [];
135
154
  const fileHits = [];
136
- let textCursor = null;
155
+ const editHits = [];
156
+ let cursor = null;
137
157
  if (pad) body.push(blank);
138
158
  for (let i = 0; i < listH; i++) {
139
- const idx = start + i;
140
- if (extraRow && idx === extraAt) {
141
- body.push(extraRow.row);
142
- if (extraRow.cursor) {
143
- textCursor = { x: extraRow.cursor.x, row: body.length - 1 };
144
- }
145
- continue;
159
+ const line = lineAt(start + i);
160
+ if (!line) break;
161
+ const row = body.push(line.row) - 1;
162
+ if (line.cursor) cursor = rowCursor(line.cursor, row);
163
+ for (const edit of line.edits) editHits.push({ ...edit, row });
164
+ if (line.hit) {
165
+ fileHits.push({ y: headerLines + pad + i + 1, cursor: line.index });
146
166
  }
147
- const itemIdx = inserting ? idx - 1 : idx;
148
- const entry = items[itemIdx];
149
- if (!entry) break;
150
- const selected = extraRow ? false : itemIdx === cursor;
151
- body.push(paintRow(entry, width, color, selected));
152
- const y = headerLines + pad + i + 1;
153
- fileHits.push({ y, cursor: itemIdx });
154
167
  }
155
168
  if (pad) body.push(blank);
156
- return { body, fileHits, cursor: textCursor, offset: start };
169
+ return paneResult({ body, fileHits, editHits, cursor, listScroll: start });
170
+ };
171
+
172
+ const editLine = (extraRow) => {
173
+ const { row, cursor, edit, edits = [] } = extraRow;
174
+ return listLine(row, -1, cursor, edit ? [...edits, edit] : edits);
175
+ };
176
+
177
+ const paintCursorList = (items, cursor, pane, paintRow, extraRow = null) => {
178
+ const edit = extraRow ? editLine(extraRow) : null;
179
+ const inserting = edit !== null && extraRow.at === 'start';
180
+ const extra = edit && extraRow.at !== 'replace' ? 1 : 0;
181
+ const extraAt = extraIdx(extraRow, cursor, items.length);
182
+ const lineAt = (idx) => {
183
+ if (edit && idx === extraAt) return edit;
184
+ const index = inserting ? idx - 1 : idx;
185
+ const entry = items[index];
186
+ if (!entry) return null;
187
+ return listLine(paintRow(entry, !edit && index === cursor), index);
188
+ };
189
+ const focus = edit ? extraAt : cursor;
190
+ return paintListWindow(items.length + extra, focus, pane, lineAt);
191
+ };
192
+
193
+ const sliceVisible = (text, skip, width) => {
194
+ if (width <= 0) return '';
195
+ const parts = graphemes(`${text ?? ''}`);
196
+ let used = 0;
197
+ let i = 0;
198
+ const drop = Math.max(0, skip);
199
+ while (i < parts.length) {
200
+ const w = graphemeWidth(parts[i], used);
201
+ if (used + w > drop) break;
202
+ used += w;
203
+ i += 1;
204
+ }
205
+ let out = '';
206
+ let taken = 0;
207
+ for (; i < parts.length; i++) {
208
+ const w = graphemeWidth(parts[i], drop + taken);
209
+ if (taken + w > width) break;
210
+ out += parts[i];
211
+ taken += w;
212
+ }
213
+ return out;
214
+ };
215
+
216
+ const fieldWindow = (text, cursor, scroll, width) => {
217
+ const raw = `${text ?? ''}`;
218
+ if (width <= 0) return { text: '', col: 0, scroll: 0 };
219
+ const at = Math.max(0, Math.min(cursor ?? 0, raw.length));
220
+ const col = visibleWidth(raw.slice(0, at));
221
+ let next = scroll ?? 0;
222
+ if (next < 0) next = 0;
223
+ if (col < next) next = col;
224
+ if (col >= next + width) next = col - width + 1;
225
+ const shown = sliceVisible(raw, next, width);
226
+ return { text: shown, col: col - next, scroll: next };
157
227
  };
158
228
 
159
229
  const branchLabel = (name) => `${name ?? ''}`.trim();
@@ -164,7 +234,8 @@ const paintListEdit = (compose, width, color, edgeW = 0) => {
164
234
  const prefixW = visibleWidth(prefix);
165
235
  const rest = Math.max(1, inner - prefixW);
166
236
  const text = `${compose.text ?? ''}`.replaceAll('\n', ' ');
167
- const clipped = truncateVisible(text, rest);
237
+ const win = fieldWindow(text, compose.cursor, compose.scrollCol, rest);
238
+ const clipped = win.text;
168
239
  const pad = Math.max(0, rest - visibleWidth(clipped));
169
240
  const left = `${prefix}${clipped}${' '.repeat(pad)}`;
170
241
  const fgRgb = THEME.chromeFg;
@@ -179,20 +250,23 @@ const paintListEdit = (compose, width, color, edgeW = 0) => {
179
250
  row += paint(gap, fgRgb, bgRgb, color);
180
251
  row += edge;
181
252
  }
182
- const col = Math.min(compose.cursor ?? 0, visibleWidth(clipped));
183
- return { row, cursor: { x: prefixW + col + 1 } };
253
+ const flat = text;
254
+ return {
255
+ row,
256
+ cursor: { x: prefixW + win.col + 1, scroll: win.scroll },
257
+ edit: {
258
+ x0: 1,
259
+ textX: prefixW + 1,
260
+ x1: prefixW + 1 + rest,
261
+ start: 0,
262
+ text: flat,
263
+ limit: flat.length,
264
+ scroll: win.scroll,
265
+ live: true,
266
+ },
267
+ };
184
268
  };
185
269
 
186
- const paneResult = (fields) => ({
187
- body: fields.body,
188
- fileHits: fields.fileHits,
189
- todoOwners: fields.todoOwners,
190
- todoChecks: fields.todoChecks ?? [],
191
- cursor: fields.cursor ?? null,
192
- splitBody: fields.splitBody === true,
193
- listScroll: fields.listScroll ?? 0,
194
- });
195
-
196
270
  const measureColumns = (entries, rowFields, align) => {
197
271
  const keys = Object.keys(align);
198
272
  const widths = Object.fromEntries(keys.map((key) => [key, 0]));
@@ -251,9 +325,11 @@ module.exports = {
251
325
  paintSplitRule,
252
326
  paintBodyFill,
253
327
  windowRows,
254
- listWindowStart,
328
+ listLine,
329
+ paintListWindow,
255
330
  paintCursorList,
256
331
  paintListEdit,
332
+ fieldWindow,
257
333
  branchLabel,
258
334
  paneResult,
259
335
  };
@@ -4,7 +4,8 @@ const chrome = require('./chrome.js');
4
4
  const notes = require('./notes.js');
5
5
  const diff = require('./diff.js');
6
6
  const present = require('./present.js');
7
- const { renderFrame } = require('./frame.js');
7
+ const frame = require('./frame.js');
8
+ const { renderFrame } = frame;
8
9
 
9
10
  const { QUIT_PROMPT, PUSH_PROMPT } = chrome;
10
11
  const { updatePrompt, dropPrompt } = chrome;
@@ -1,23 +1,30 @@
1
1
  'use strict';
2
2
 
3
- const { detectLang } = require('../detect.js');
3
+ const detect = require('../detect.js');
4
+ const { detectLang } = detect;
4
5
  const primitives = require('./primitives.js');
5
6
  const diff = require('./diff.js');
6
7
 
7
8
  const { paneResult } = primitives;
8
- const { paintDiffLine, codeCursorInRows } = diff;
9
- const { sameHunk } = require('../diff/diff.js');
9
+ const { paintDiffLine, codeCursorInRows, codeEditHits } = diff;
10
+ const { expandSoftRows } = diff;
11
+ const diffLib = require('../diff/diff.js');
12
+ const { sameHunk } = diffLib;
10
13
 
11
14
  const paintBodyUnit = (view, width, color) => {
12
15
  const lines = view.unitLines ?? [];
13
16
  const item = view.item;
14
17
  const lang = detectLang(view.reviewPath ?? '');
15
18
  const overlay = view.codeOverlay;
16
- const editing = view.mode === 'compose' && overlay;
17
- const { length } = lines;
19
+ const editing =
20
+ view.mode === 'compose' && overlay && typeof overlay.cursor === 'number';
21
+ const picked = editing ? overlay.scrollCol : 0;
22
+ const scrollCol = picked ?? 0;
23
+ const source = editing ? lines : expandSoftRows(lines, width, false, true);
24
+ const { length } = source;
18
25
  const body = new Array(length);
19
26
  for (let i = 0; i < length; i++) {
20
- const line = lines[i];
27
+ const line = source[i];
21
28
  const isBlock = item && sameHunk(line.item, item);
22
29
  const row = { ...line };
23
30
  if (editing) {
@@ -25,16 +32,15 @@ const paintBodyUnit = (view, width, color) => {
25
32
  } else if ((row.type === 'add' || row.type === 'del') && !isBlock) {
26
33
  row.mark = ' ';
27
34
  }
28
- body[i] = paintDiffLine(row, width, color, row.origin, lang);
35
+ body[i] = paintDiffLine(row, width, color, row.origin, lang, scrollCol);
29
36
  }
30
37
  const edit = overlay ? overlay.cursor : null;
31
- const cursor = codeCursorInRows(lines, false, width, edit);
38
+ const cursor = codeCursorInRows(source, false, width, edit, scrollCol);
39
+ const editHits = editing ? codeEditHits(source, false, width, scrollCol) : [];
32
40
  return paneResult({
33
41
  body,
34
- fileHits: [],
35
- todoOwners: [],
36
42
  cursor,
37
- splitBody: false,
43
+ editHits,
38
44
  });
39
45
  };
40
46