reslop 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/LICENSE +1 -1
  3. package/README.md +12 -13
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +32 -19
  6. package/lib/capabilities.js +8 -12
  7. package/lib/cli.js +40 -74
  8. package/lib/clipboard.js +48 -18
  9. package/lib/dashboard/git.js +128 -0
  10. package/lib/dashboard/model.js +332 -0
  11. package/lib/dashboard/npm.js +193 -0
  12. package/lib/dashboard/tree.js +278 -0
  13. package/lib/deps-diff.js +196 -404
  14. package/lib/deps.js +240 -399
  15. package/lib/detect.js +8 -18
  16. package/lib/diff/align.js +41 -113
  17. package/lib/diff/diff.js +12 -43
  18. package/lib/diff/display.js +25 -36
  19. package/lib/diff/edit.js +6 -20
  20. package/lib/diff/inline.js +138 -141
  21. package/lib/diff/items.js +33 -47
  22. package/lib/diff/parse.js +28 -56
  23. package/lib/diff/patch.js +12 -33
  24. package/lib/diff/unit.js +90 -206
  25. package/lib/editor.js +98 -55
  26. package/lib/files.js +86 -103
  27. package/lib/git-branches.js +254 -416
  28. package/lib/git-deps.js +182 -344
  29. package/lib/git-reword-todo.js +5 -13
  30. package/lib/git-worktree.js +141 -212
  31. package/lib/git.js +21 -49
  32. package/lib/github.js +67 -181
  33. package/lib/gitlab.js +40 -90
  34. package/lib/highlight/bash.js +55 -177
  35. package/lib/highlight/core.js +157 -132
  36. package/lib/highlight/css.js +25 -58
  37. package/lib/highlight/csv.js +19 -37
  38. package/lib/highlight/dart.js +72 -213
  39. package/lib/highlight/dot.js +16 -26
  40. package/lib/highlight/html.js +42 -63
  41. package/lib/highlight/js.js +177 -506
  42. package/lib/highlight/json.js +17 -37
  43. package/lib/highlight/log.js +22 -35
  44. package/lib/highlight/txt.js +9 -22
  45. package/lib/highlight.js +17 -46
  46. package/lib/keys.js +62 -85
  47. package/lib/manifest.js +67 -130
  48. package/lib/npm-commands.js +119 -189
  49. package/lib/remote.js +141 -197
  50. package/lib/render/branches.js +66 -73
  51. package/lib/render/chrome.js +264 -323
  52. package/lib/render/commits.js +187 -500
  53. package/lib/render/dash-activity.js +212 -0
  54. package/lib/render/dash-blocks.js +245 -0
  55. package/lib/render/dash-table.js +290 -0
  56. package/lib/render/dashboard.js +62 -0
  57. package/lib/render/diff.js +123 -228
  58. package/lib/render/files.js +45 -99
  59. package/lib/render/frame.js +129 -183
  60. package/lib/render/notes.js +144 -160
  61. package/lib/render/npm.js +234 -112
  62. package/lib/render/present.js +4 -11
  63. package/lib/render/primitives.js +131 -170
  64. package/lib/render/render.js +5 -14
  65. package/lib/render/report.js +43 -0
  66. package/lib/render/tiles.js +260 -0
  67. package/lib/render/unit.js +15 -28
  68. package/lib/report-model.js +110 -33
  69. package/lib/report-parse.js +56 -38
  70. package/lib/report-render.js +102 -59
  71. package/lib/report-run.js +87 -78
  72. package/lib/report-test.js +256 -141
  73. package/lib/report-tools.js +96 -161
  74. package/lib/review-merge.js +114 -152
  75. package/lib/review.js +115 -189
  76. package/lib/runs.js +225 -0
  77. package/lib/select.js +89 -75
  78. package/lib/session/accessors.js +14 -21
  79. package/lib/session/actions.js +82 -63
  80. package/lib/session/branches.js +193 -247
  81. package/lib/session/change-actions.js +171 -214
  82. package/lib/session/commit.js +280 -342
  83. package/lib/session/compose.js +559 -674
  84. package/lib/session/dashboard.js +385 -0
  85. package/lib/session/dispatch.js +164 -157
  86. package/lib/session/edit-click.js +57 -0
  87. package/lib/session/files-pane.js +85 -53
  88. package/lib/session/items.js +73 -104
  89. package/lib/session/lifecycle.js +403 -541
  90. package/lib/session/load.js +48 -68
  91. package/lib/session/navigation.js +96 -87
  92. package/lib/session/npm.js +409 -345
  93. package/lib/session/ops.js +196 -63
  94. package/lib/session/pointer.js +23 -15
  95. package/lib/session/progress.js +33 -21
  96. package/lib/session/review.js +33 -54
  97. package/lib/session/tasks.js +333 -0
  98. package/lib/session/templates.js +51 -59
  99. package/lib/session/terminal.js +85 -136
  100. package/lib/session/unit.js +3 -4
  101. package/lib/session/update.js +76 -97
  102. package/lib/session/view.js +52 -83
  103. package/lib/session/watch.js +208 -204
  104. package/lib/session.js +112 -237
  105. package/lib/update.js +45 -116
  106. package/lib/utilities.js +67 -89
  107. package/lib/wrap.js +33 -44
  108. package/package.json +1 -1
  109. package/lib/session/todos.js +0 -293
package/lib/remote.js CHANGED
@@ -15,8 +15,6 @@ const RETRY_ATTEMPTS = 3;
15
15
  const RETRY_DELAY_MS = 200;
16
16
  const TRANSIENT_STATUS = [429, 502, 503, 504];
17
17
 
18
- const isRecord = isHashObject;
19
-
20
18
  const abortError = (signal) => {
21
19
  if (signal && signal.reason !== undefined) return signal.reason;
22
20
  const error = new Error('This operation was aborted');
@@ -26,110 +24,142 @@ const abortError = (signal) => {
26
24
 
27
25
  const isAbortError = (error, signal) => {
28
26
  if (signal && signal.aborted) return true;
29
- if (!error || typeof error !== 'object') return false;
30
- if (error.name === 'AbortError') return true;
31
- if (error.code === 'ABORT') return true;
32
- return false;
27
+ return error?.name === 'AbortError' || error?.code === 'ABORT';
33
28
  };
34
29
 
35
30
  const throwIfAborted = (signal) => {
36
31
  if (signal && signal.aborted) throw abortError(signal);
37
32
  };
38
33
 
39
- const wait = async (ms, abortSignal) => {
34
+ const wait = async (ms, signal) => {
40
35
  try {
41
- await delay(ms, abortSignal);
36
+ await delay(ms, signal);
42
37
  } catch (error) {
43
- throwIfAborted(abortSignal);
38
+ throwIfAborted(signal);
44
39
  throw error;
45
40
  }
46
41
  };
47
42
 
48
- const createTransport = (deps = {}) => {
49
- const fetchImpl = deps.fetch;
50
- const signal = deps.signal;
43
+ class RemoteClient {
44
+ constructor(options) {
45
+ this.name = options.name;
46
+ this.subject = options.subject;
47
+ this.prefix = options.name.toUpperCase();
48
+ this.headers = options.headers;
49
+ this.nextPage = options.nextPage;
50
+ this.errorFields = options.errorFields ?? ['message'];
51
+ this.limitStatus = options.limitStatus ?? 0;
52
+ }
53
+
54
+ error(code, message, extra = {}) {
55
+ const fields = { code: `${this.prefix}_${code}`, ...extra };
56
+ return Object.assign(new Error(message), fields);
57
+ }
51
58
 
52
- const retry = async (run, options = {}) => {
53
- const attempts = options.attempts ?? RETRY_ATTEMPTS;
54
- const delayMs = options.delayMs ?? RETRY_DELAY_MS;
55
- const isRetryable = options.isRetryable;
56
- for (let i = 0; ; i++) {
59
+ parseJson(text, label, validate = isHashObject) {
60
+ const data = jsonParse(text ?? '');
61
+ if (validate(data)) return data;
62
+ throw this.error('PARSE', `${this.name} API: invalid ${label}`);
63
+ }
64
+
65
+ httpError(status, body, token) {
66
+ const { name, prefix } = this;
67
+ const text = `${body ?? ''}`.trim();
68
+ const parsed = jsonParse(text);
69
+ const fields = this.errorFields;
70
+ const field = fields.find((key) => typeof parsed?.[key] === 'string');
71
+ const message =
72
+ isHashObject(parsed) && field ? parsed[field] : oneLine(text);
73
+ if (status === 404) {
74
+ const hint = token
75
+ ? ''
76
+ : ` (set ${prefix}_TOKEN for private repositories)`;
77
+ const notFound = `${name} ${this.subject} not found${hint}`;
78
+ return this.error('NOT_FOUND', notFound, { status });
79
+ }
80
+ if (status === 401) {
81
+ return this.error('AUTH', `${name} authentication failed`, { status });
82
+ }
83
+ const rateLimit = status === 403 && /rate limit/i.test(message);
84
+ if (rateLimit || status === this.limitStatus) {
85
+ const limited = `${name} API rate limit exceeded`;
86
+ return this.error('RATE_LIMIT', limited, { status });
87
+ }
88
+ const detail = message ? `: ${oneLine(message)}` : ` HTTP ${status}`;
89
+ return this.error('HTTP', `${name} API${detail}`, { status });
90
+ }
91
+
92
+ isRetryable(error) {
93
+ if (error.code === `${this.prefix}_NETWORK`) return true;
94
+ return TRANSIENT_STATUS.includes(error.status);
95
+ }
96
+
97
+ async fetchPage(url, options) {
98
+ const { signal, token = '' } = options;
99
+ const headers = this.headers(token, options.accept);
100
+ if (options.contentType) headers['Content-Type'] = options.contentType;
101
+ const init = { method: options.method || 'GET', headers };
102
+ if (options.body !== undefined) init.body = options.body;
103
+ if (signal) init.signal = signal;
104
+ let response;
105
+ try {
106
+ response = await options.fetch(url, init);
107
+ } catch (cause) {
108
+ if (isAbortError(cause, signal)) throw cause;
109
+ const detail = oneLine(cause && cause.message);
110
+ const suffix = detail ? `: ${detail}` : '';
111
+ const message = `${this.name} request failed${suffix}`;
112
+ throw this.error('NETWORK', message, { cause });
113
+ }
114
+ const text = await response.text();
115
+ if (!response.ok) throw this.httpError(response.status, text, token);
116
+ return { body: text, headers: response.headers };
117
+ }
118
+
119
+ async request(url, options) {
120
+ const { signal, retry = {} } = options;
121
+ throwIfAborted(signal);
122
+ if (typeof options.fetch !== 'function') {
123
+ throw this.error('FETCH', 'fetch is not available');
124
+ }
125
+ const attempts = retry.attempts ?? RETRY_ATTEMPTS;
126
+ const delayMs = retry.delayMs ?? RETRY_DELAY_MS;
127
+ for (let attempt = 1; ; attempt++) {
57
128
  try {
58
- return await run();
129
+ return await this.fetchPage(url, options);
59
130
  } catch (error) {
60
131
  if (isAbortError(error, signal)) throw error;
61
- const isLast = i === attempts - 1;
62
- if (isLast || !isRetryable(error)) throw error;
63
- throwIfAborted(signal);
64
- await wait(delayMs * (i + 1), signal);
132
+ if (attempt >= attempts || !this.isRetryable(error)) throw error;
133
+ await wait(delayMs * attempt, signal);
65
134
  }
66
135
  }
67
- };
68
-
69
- const request = async (url, options = {}) => {
70
- throwIfAborted(signal);
71
- if (typeof fetchImpl !== 'function') {
72
- throw options.missingFetch();
73
- }
74
- const headers = options.headers;
75
- const method = options.method || 'GET';
76
- const body = options.body;
77
- const wrapNetwork = options.wrapNetwork;
78
- const toHttpError = options.toHttpError;
79
- const retryOpts = options.retry ?? {};
80
- return retry(
81
- async () => {
82
- let response;
83
- try {
84
- const init = { method, headers };
85
- if (body !== undefined) init.body = body;
86
- if (signal) init.signal = signal;
87
- response = await fetchImpl(url, init);
88
- } catch (error) {
89
- if (isAbortError(error, signal)) throw error;
90
- throw wrapNetwork(error);
91
- }
92
- const text = await response.text();
93
- if (!response.ok) throw toHttpError(response.status, text);
94
- return { body: text, headers: response.headers };
95
- },
96
- { ...retryOpts, isRetryable: options.isRetryable },
97
- );
98
- };
136
+ }
99
137
 
100
- const listPages = async (url, options = {}) => {
138
+ async list(url, options) {
101
139
  const items = [];
102
140
  let next = url;
103
- const parsePage = options.parsePage;
104
- const nextPage = options.nextPage;
105
141
  while (next) {
106
- const page = await request(next, options);
107
- const entries = parsePage(page.body);
108
- for (const entry of entries) items.push(entry);
109
- next = nextPage(page.headers, next);
142
+ const page = await this.request(next, { ...options, method: 'GET' });
143
+ const entries = this.parseJson(page.body, 'list payload', Array.isArray);
144
+ items.push(...entries);
145
+ next = this.nextPage(page.headers, next);
110
146
  }
111
147
  return items;
112
- };
113
-
114
- return { retry, request, listPages };
115
- };
148
+ }
149
+ }
116
150
 
117
151
  const headerValue = (headers, name) => {
118
152
  if (!headers) return '';
119
- if (typeof headers.get === 'function') {
120
- return headers.get(name) || '';
121
- }
153
+ if (typeof headers.get === 'function') return headers.get(name) || '';
122
154
  const wanted = name.toLowerCase();
123
155
  for (const key of Object.keys(headers)) {
124
- const value = headers[key];
125
- if (key.toLowerCase() === wanted) return `${value}`;
156
+ if (key.toLowerCase() === wanted) return `${headers[key]}`;
126
157
  }
127
158
  return '';
128
159
  };
129
160
 
130
161
  const parseLinkNext = (headers) => {
131
162
  const link = headerValue(headers, 'link');
132
- if (!link) return '';
133
163
  for (const part of link.split(',')) {
134
164
  const match = /<([^>]+)>\s*;\s*rel="?next"?/i.exec(part.trim());
135
165
  if (match) return match[1];
@@ -153,106 +183,24 @@ const listUrl = (base) => {
153
183
  return url.toString();
154
184
  };
155
185
 
156
- const createClient = ({ name, subject, headers, nextPage }) => {
157
- const prefix = name.toUpperCase();
158
- const error = (code, message, extra = {}) =>
159
- Object.assign(new Error(message), { code: `${prefix}_${code}`, ...extra });
160
-
161
- const parseJson = (text, label, validate = isHashObject) => {
162
- const data = jsonParse(text ?? '');
163
- if (!validate(data)) throw error('PARSE', `${name} API: invalid ${label}`);
164
- return data;
165
- };
166
-
167
- const httpError = (status, body, token) => {
168
- const text = `${body ?? ''}`.trim();
169
- const parsed = jsonParse(text);
170
- const fields = name === 'GitLab' ? ['message', 'error'] : ['message'];
171
- const field = fields.find((key) => typeof parsed?.[key] === 'string');
172
- const message =
173
- isHashObject(parsed) && field ? parsed[field] : oneLine(text);
174
- if (status === 404) {
175
- const hint = token
176
- ? ''
177
- : ` (set ${prefix}_TOKEN for private repositories)`;
178
- return error('NOT_FOUND', `${name} ${subject} not found${hint}`, {
179
- status,
180
- });
181
- }
182
- if (status === 401) {
183
- return error('AUTH', `${name} authentication failed`, { status });
184
- }
185
- const rateLimit = status === 403 && /rate limit/i.test(message);
186
- if (rateLimit || (name === 'GitLab' && status === 429)) {
187
- return error('RATE_LIMIT', `${name} API rate limit exceeded`, { status });
188
- }
189
- const detail = message ? `: ${oneLine(message)}` : ` HTTP ${status}`;
190
- return error('HTTP', `${name} API${detail}`, { status });
191
- };
192
-
193
- const policy = (options) => ({
194
- retry: options.retry ?? {},
195
- isRetryable: (err) =>
196
- err.code === `${prefix}_NETWORK` || TRANSIENT_STATUS.includes(err.status),
197
- missingFetch: () => error('FETCH', 'fetch is not available'),
198
- toHttpError: (status, body) => httpError(status, body, options.token),
199
- wrapNetwork: (cause) => {
200
- const detail = oneLine(cause && cause.message);
201
- const suffix = detail ? `: ${detail}` : '';
202
- return error('NETWORK', `${name} request failed${suffix}`, { cause });
203
- },
204
- method: options.method || 'GET',
205
- headers: headers(options.token || '', options.accept),
206
- });
207
-
208
- const request = (url, options) => {
209
- const init = policy(options);
210
- if (options.contentType) init.headers['Content-Type'] = options.contentType;
211
- return createTransport(options).request(url, {
212
- ...init,
213
- body: options.body,
214
- });
215
- };
216
-
217
- const list = (url, options) =>
218
- createTransport(options).listPages(url, {
219
- ...policy(options),
220
- method: 'GET',
221
- parsePage: (text) => parseJson(text, 'list payload', Array.isArray),
222
- nextPage,
223
- });
224
-
225
- return { parseJson, request, list, subject };
226
- };
186
+ const commentBody = (value) => `${value ?? ''}`.trim();
227
187
 
228
188
  const sideLineNumber = (type, oldNo, newNo, side) => {
229
- if (side === 'LEFT') {
230
- if (type === 'add') return 0;
231
- return oldNo;
232
- }
233
- if (type === 'del') return 0;
234
- return newNo;
189
+ if (side === 'LEFT') return type === 'add' ? 0 : oldNo;
190
+ return type === 'del' ? 0 : newNo;
235
191
  };
236
192
 
237
193
  const locateLineInItem = (item, rel, line, side) => {
238
- const path = itemPath(item);
239
- if (path !== rel) return null;
240
194
  const hunk = item.hunk;
241
- if (!hunk || !line) return null;
195
+ if (itemPath(item) !== rel || !hunk || !line) return null;
242
196
  let oldNo = hunk.oldStart;
243
197
  let newNo = hunk.newStart;
244
198
  for (const entry of hunk.lines) {
245
199
  const at = sideLineNumber(entry.type, oldNo, newNo, side);
246
- if (at === line) {
247
- const inBlock = entry.blockId === item.blockId;
248
- return { inBlock };
249
- }
250
- if (isCtxType(entry.type) || entry.type === 'del') {
251
- oldNo += 1;
252
- }
253
- if (isCtxType(entry.type) || entry.type === 'add') {
254
- newNo += 1;
255
- }
200
+ if (at === line) return { inBlock: entry.blockId === item.blockId };
201
+ const isCtx = isCtxType(entry.type);
202
+ if (isCtx || entry.type === 'del') oldNo += 1;
203
+ if (isCtx || entry.type === 'add') newNo += 1;
256
204
  }
257
205
  return null;
258
206
  };
@@ -270,8 +218,7 @@ const findItemForLine = (items, rel, line, side) => {
270
218
 
271
219
  const importedText = (entry, host) => {
272
220
  const reviewer = entry.reviewer || 'unknown';
273
- const body = `${entry.body ?? ''}`.trim();
274
- return `@${reviewer} review at ${host}: ${body}`;
221
+ return `@${reviewer} review at ${host}: ${commentBody(entry.body)}`;
275
222
  };
276
223
 
277
224
  const importedTodo = (file, entry, host) => ({
@@ -298,44 +245,46 @@ const importedFeedback = (item, path, entry, host) => {
298
245
 
299
246
  const noteFromLocation = (items, loc, entry, host, fallbackFile) => {
300
247
  const path = loc.path || '';
301
- const line = loc.line;
302
- const side = loc.side;
303
- if (line && path) {
304
- const item = findItemForLine(items, path, line, side);
248
+ if (loc.line && path) {
249
+ const item = findItemForLine(items, path, loc.line, loc.side);
305
250
  if (item) return importedFeedback(item, path, entry, host);
306
251
  }
307
- if (path) return importedTodo(path, entry, host);
308
- return importedTodo(fallbackFile, entry, host);
252
+ return importedTodo(path || fallbackFile, entry, host);
309
253
  };
310
254
 
311
- const changePath = (file) => file.newPath || file.oldPath || '';
255
+ const splitNotes = (notes) => {
256
+ const feedback = [];
257
+ const tasks = [];
258
+ for (const note of notes) {
259
+ if (!note) continue;
260
+ if (note.kind === 'feedback') feedback.push(note);
261
+ else tasks.push(note);
262
+ }
263
+ return { feedback, tasks };
264
+ };
312
265
 
313
266
  const filterChangeFiles = (files, paths) => {
314
267
  if (!paths.length) return files;
315
- return files.filter((file) => isPathInScope(changePath(file), paths));
268
+ return files.filter((file) =>
269
+ isPathInScope(file.newPath || file.oldPath || '', paths),
270
+ );
316
271
  };
317
272
 
318
273
  const changeFiles = (items) => {
319
- const files = new Set();
274
+ const rels = new Set();
320
275
  for (const item of items) {
321
276
  const paths = item.dep ? item.dep.files : [itemPath(item)];
322
277
  for (const rel of paths ?? []) {
323
- if (rel) files.add(rel);
278
+ if (rel) rels.add(rel);
324
279
  }
325
280
  }
326
- return [...files];
281
+ return [...rels];
327
282
  };
328
283
 
329
284
  const loadChange = async (ref, options, source) => {
330
285
  const { client, url, diffUrl = url, diffAccept } = source;
331
- const {
332
- cwd,
333
- paths = [],
334
- fetch = globalThis.fetch,
335
- token = '',
336
- retry = {},
337
- signal,
338
- } = options;
286
+ const { cwd, paths = [], fetch = globalThis.fetch } = options;
287
+ const { token = '', retry = {}, signal } = options;
339
288
  const request = { fetch, token, retry, signal };
340
289
  const jsonPage = await client.request(url, request);
341
290
  const data = client.parseJson(jsonPage.body, `${client.subject} payload`);
@@ -345,37 +294,32 @@ const loadChange = async (ref, options, source) => {
345
294
  });
346
295
  const files = filterChangeFiles(parseDiff(diffPage.body), paths);
347
296
  const items = foldDepItems(itemsFromFiles(files, 'pr'));
348
- const change = source.toChange(ref, data, items);
297
+ const change = {
298
+ ...source.toChange(ref, data),
299
+ files: changeFiles(items),
300
+ status: 'reviewing',
301
+ };
349
302
  const sourceLabel = `${source.prefix}${change.number}`;
350
- let imported = { feedback: [], todos: [] };
303
+ let imported = { feedback: [], tasks: [] };
351
304
  try {
352
305
  const discussion = await source.loadDiscussion(ref, request);
353
306
  imported = source.discussionToNotes(discussion, items, paths);
354
307
  } catch (error) {
355
- if (isAbortError(error, request.signal)) throw error;
356
- // Discussion import is optional.
308
+ if (isAbortError(error, signal)) throw error;
357
309
  }
358
310
  return { top: cwd, items, sourceLabel, change, imported };
359
311
  };
360
312
 
361
313
  module.exports = {
362
- RETRY_ATTEMPTS,
363
- RETRY_DELAY_MS,
364
- TRANSIENT_STATUS,
365
- isRecord,
314
+ RemoteClient,
366
315
  isAbortError,
367
- createTransport,
368
- headerValue,
369
316
  parseLinkNext,
370
317
  parseNextPage,
371
- createClient,
372
318
  listUrl,
373
- findItemForLine,
374
- importedText,
319
+ commentBody,
375
320
  importedTodo,
376
321
  noteFromLocation,
377
- changePath,
322
+ splitNotes,
378
323
  filterChangeFiles,
379
- changeFiles,
380
324
  loadChange,
381
325
  };
@@ -3,14 +3,13 @@
3
3
  const ansi = require('../ansi.js');
4
4
  const files = require('../files.js');
5
5
  const primitives = require('./primitives.js');
6
+
6
7
  const { shortAge } = files;
8
+ const { THEME, paint, visibleWidth, trimVisible, seq, EL } = ansi;
9
+ const { MARK_W, markPrefix, branchLabel, editSpan } = primitives;
10
+ const { paintCursorList, fieldWindow } = primitives;
7
11
  const { measureColumns, padColumns, joinColumns, paintColumns } = primitives;
8
12
 
9
- const { THEME, paint, visibleWidth, truncateVisible, seq, EL } = ansi;
10
- const { FILE_MARK } = primitives;
11
- const { paintCursorList, paintListEdit } = primitives;
12
- const { branchLabel } = primitives;
13
-
14
13
  const BRANCH_COL_ALIGN = {
15
14
  sha: 'end',
16
15
  ahead: 'start',
@@ -28,88 +27,91 @@ const BRANCH_COL_FG = {
28
27
  };
29
28
 
30
29
  const BRANCH_CURRENT_COL_FG = {
30
+ ...BRANCH_COL_FG,
31
31
  sha: THEME.shaDarkFg,
32
32
  date: THEME.headerFg,
33
33
  };
34
34
 
35
35
  const BRANCH_NAME_MAX = 24;
36
36
  const BRANCH_TAIL = 1;
37
+
37
38
  const branchNameChip = (entry, rowBg) => {
38
39
  if (entry.current) {
39
- return {
40
- nameFg: THEME.mutedFg,
41
- nameBg: THEME.currentBg,
42
- nameBold: false,
43
- };
40
+ return { nameFg: THEME.mutedFg, nameBg: THEME.currentBg, nameBold: false };
44
41
  }
45
42
  if (entry.isDefault) {
46
- return {
47
- nameFg: THEME.warnFg,
48
- nameBg: rowBg,
49
- nameBold: true,
50
- };
43
+ return { nameFg: THEME.warnFg, nameBg: rowBg, nameBold: true };
51
44
  }
52
- return {
53
- nameFg: THEME.buttonHotFg,
54
- nameBg: rowBg,
55
- nameBold: false,
56
- };
45
+ return { nameFg: THEME.buttonHotFg, nameBg: rowBg, nameBold: false };
57
46
  };
58
47
 
59
- const branchRowFg = (entry, selected) => {
60
- if (selected) return THEME.chromeFg;
61
- return THEME.mutedFg;
62
- };
48
+ const branchRowFields = (entry) => ({
49
+ sha: entry.sha ?? '',
50
+ ahead: entry.ahead ? `⇡${entry.ahead}` : '',
51
+ behind: entry.behind ? `⇣${entry.behind}` : '',
52
+ gone: entry.gone ? 'gone' : '',
53
+ date: shortAge(entry.date),
54
+ });
63
55
 
64
- const branchRowFields = (entry) => {
65
- const sha = entry.sha ?? '';
66
- const date = shortAge(entry.date ?? '');
67
- const gone = entry.gone ? 'gone' : '';
68
- const ahead = entry.ahead ? `⇡${entry.ahead}` : '';
69
- const behind = entry.behind ? `⇣${entry.behind}` : '';
70
- return { sha, ahead, behind, gone, date };
56
+ const branchRowFg = (entry, selected) => {
57
+ if (entry.current) return THEME.headerFg;
58
+ return selected ? THEME.chromeFg : THEME.mutedFg;
71
59
  };
72
60
 
73
61
  const branchRowBg = (entry, selected) => {
74
62
  if (entry.current) return THEME.currentBg;
75
- if (selected) return THEME.buttonBg;
76
- return THEME.ctxBg;
63
+ return selected ? THEME.buttonBg : THEME.ctxBg;
77
64
  };
78
65
 
79
66
  const paintBranchRow = (entry, width, color, selected, cols) => {
80
67
  const inner = Math.max(1, width - BRANCH_TAIL);
81
- const mark = selected ? FILE_MARK : ' ';
82
- const prefix = ` ${mark} `;
68
+ const prefix = markPrefix(selected);
83
69
  const parts = padColumns(branchRowFields(entry), cols, BRANCH_COL_ALIGN);
84
70
  const suffix = joinColumns(parts, cols);
85
71
  const rest = Math.max(1, inner - visibleWidth(prefix + suffix));
86
72
  const nameW = Math.min(cols.name, rest);
87
- const label = truncateVisible(branchLabel(entry.name), nameW);
88
- const namePad = Math.max(0, nameW - visibleWidth(label));
73
+ const label = trimVisible(branchLabel(entry.name), nameW);
74
+ const namePad = ' '.repeat(Math.max(0, nameW - visibleWidth(label)));
89
75
  const canSubject = rest >= nameW + 3;
90
76
  const gap = canSubject ? ' ' : '';
91
- const subjectW = canSubject ? rest - nameW - visibleWidth(gap) : 0;
92
- const subject = truncateVisible(entry.subject ?? '', subjectW);
93
- const mid = `${label}${' '.repeat(namePad)}${gap}${subject}`;
94
- const pad = Math.max(0, rest - visibleWidth(mid));
95
- const after = `${' '.repeat(namePad)}${gap}${subject}${' '.repeat(pad)}`;
96
- const left = `${prefix}${label}${after}`;
97
- const fgRgb = branchRowFg(entry, selected);
98
- const rowBg = branchRowBg(entry, selected);
99
- const chip = branchNameChip(entry, rowBg);
100
- const toneFg = entry.current ? THEME.headerFg : fgRgb;
77
+ const subjectW = canSubject ? rest - nameW - gap.length : 0;
78
+ const subject = trimVisible(entry.subject ?? '', subjectW);
79
+ const mid = `${label}${namePad}${gap}${subject}`;
80
+ const pad = ' '.repeat(Math.max(0, rest - visibleWidth(mid)));
81
+ const after = `${namePad}${gap}${subject}${pad}`;
101
82
  const tail = ' '.repeat(BRANCH_TAIL);
102
- if (!color) return `${left}${suffix}${tail}`;
103
- let out = `${seq(toneFg, rowBg)}${EL}`;
104
- out += paint(prefix, toneFg, rowBg, color);
83
+ if (!color) return `${prefix}${label}${after}${suffix}${tail}`;
84
+ const fgRgb = branchRowFg(entry, selected);
85
+ const bgRgb = branchRowBg(entry, selected);
86
+ const chip = branchNameChip(entry, bgRgb);
87
+ const tones = entry.current ? BRANCH_CURRENT_COL_FG : BRANCH_COL_FG;
88
+ let out = `${seq(fgRgb, bgRgb)}${EL}`;
89
+ out += paint(prefix, fgRgb, bgRgb, color);
105
90
  out += paint(label, chip.nameFg, chip.nameBg, color, chip.nameBold);
106
- out += paint(after, toneFg, rowBg, color);
107
- const tones = entry.current
108
- ? { ...BRANCH_COL_FG, ...BRANCH_CURRENT_COL_FG }
109
- : BRANCH_COL_FG;
110
- out += paintColumns(parts, cols, toneFg, rowBg, color, tones);
111
- out += paint(tail, toneFg, rowBg, color);
112
- return out;
91
+ out += paint(after, fgRgb, bgRgb, color);
92
+ out += paintColumns(parts, cols, fgRgb, bgRgb, color, tones);
93
+ return out + paint(tail, fgRgb, bgRgb, color);
94
+ };
95
+
96
+ const paintBranchEdit = (compose, width, color) => {
97
+ const prefix = markPrefix(true);
98
+ const rest = Math.max(1, width - MARK_W);
99
+ const text = `${compose.text ?? ''}`.replaceAll('\n', ' ');
100
+ const win = fieldWindow(text, compose.cursor, compose.scrollCol, rest);
101
+ const gap = ' '.repeat(Math.max(0, rest - visibleWidth(win.text)));
102
+ const fgRgb = THEME.chromeFg;
103
+ const bgRgb = THEME.buttonBg;
104
+ let row = `${prefix}${win.text}${gap}`;
105
+ if (color) {
106
+ row = `${seq(fgRgb, bgRgb)}${EL}`;
107
+ row += paint(prefix, fgRgb, bgRgb, color);
108
+ row += paint(win.text, THEME.buttonHotFg, bgRgb, color);
109
+ row += paint(gap, fgRgb, bgRgb, color);
110
+ }
111
+ const textX = MARK_W + 1;
112
+ const edit = editSpan(1, textX, textX + rest, 0, text);
113
+ const cursor = { x: textX + win.col, scroll: win.scroll };
114
+ return { row, cursor, edits: [{ ...edit, scroll: win.scroll }] };
113
115
  };
114
116
 
115
117
  const paintBodyBranches = (view, width, color, bodyH, headerLines) => {
@@ -121,22 +123,13 @@ const paintBodyBranches = (view, width, color, bodyH, headerLines) => {
121
123
  }
122
124
  cols.name = Math.min(BRANCH_NAME_MAX, cols.name);
123
125
  const cursor = view.branchCursor ?? 0;
124
- const compose = view.compose;
125
- const editing = compose && compose.kind === 'branch';
126
- const tail = editing ? paintListEdit(compose, width, color) : null;
127
- const paintRow = (entry, rowWidth, rowColor, selected) =>
128
- paintBranchRow(entry, rowWidth, rowColor, selected, cols);
129
- return paintCursorList(
130
- branches,
131
- cursor,
132
- width,
133
- color,
134
- bodyH,
135
- headerLines,
136
- paintRow,
137
- tail,
138
- view.listScroll,
139
- );
126
+ const { compose } = view;
127
+ const edit =
128
+ compose?.kind === 'branch' ? paintBranchEdit(compose, width, color) : null;
129
+ const pane = { width, color, bodyH, headerLines, offset: view.listScroll };
130
+ const paintRow = (entry, selected) =>
131
+ paintBranchRow(entry, width, color, selected, cols);
132
+ return paintCursorList(branches, cursor, pane, paintRow, edit);
140
133
  };
141
134
 
142
135
  module.exports = { paintBodyBranches };