reslop 0.1.1 → 0.1.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.
- package/CHANGELOG.md +21 -6
- package/README.md +29 -26
- package/bin/reslop.js +2 -2
- package/lib/ansi.js +0 -1
- package/lib/cli.js +62 -47
- package/lib/clipboard.js +1 -1
- package/lib/deps.js +1440 -0
- package/lib/detect.js +6 -4
- package/lib/diff.js +31 -55
- package/lib/editor.js +1 -1
- package/lib/files.js +58 -15
- package/lib/git.js +649 -69
- package/lib/github.js +28 -34
- package/lib/highlight.js +282 -141
- package/lib/keys.js +35 -39
- package/lib/render.js +250 -153
- package/lib/review.js +22 -42
- package/lib/select.js +16 -18
- package/lib/session.js +301 -227
- package/lib/source.js +7 -3
- package/package.json +1 -1
package/lib/render.js
CHANGED
|
@@ -2,20 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
const ansi = require('./ansi.js');
|
|
4
4
|
const { displayLines } = require('./diff.js');
|
|
5
|
-
const
|
|
5
|
+
const highlight = require('./highlight.js');
|
|
6
6
|
const { detectLang } = require('./detect.js');
|
|
7
7
|
const keys = require('./keys.js');
|
|
8
|
-
const { ACTIONS, actionHot, actionLetter, buttonWord } = keys;
|
|
9
|
-
const { FILES_DISABLED } = keys;
|
|
10
8
|
const { overlayRows } = require('./select.js');
|
|
11
9
|
const { itemPath } = require('./files.js');
|
|
12
|
-
const { isDoneMark } = require('./review.js');
|
|
13
10
|
const wrap = require('./wrap.js');
|
|
14
|
-
const { wrapPlain, wrapDoc, wrapMultiline, cursorInWrap, wrapMove } = wrap;
|
|
15
11
|
|
|
16
|
-
const {
|
|
17
|
-
|
|
18
|
-
const {
|
|
12
|
+
const { tokenize, overlayTokens } = highlight;
|
|
13
|
+
const { ACTIONS, ACTION_IDS, actionLetter, buttonWord } = keys;
|
|
14
|
+
const { FILES_DISABLED } = keys;
|
|
15
|
+
const { wrapPlain, wrapMultiline, cursorInWrap } = wrap;
|
|
16
|
+
const { THEME, CODE_FG, codeFg, paint, visibleWidth } = ansi;
|
|
17
|
+
const { truncateVisible, fg, RESET, ESC, EL, seq } = ansi;
|
|
19
18
|
|
|
20
19
|
const STYLES = {
|
|
21
20
|
del: {
|
|
@@ -32,6 +31,27 @@ const STYLES = {
|
|
|
32
31
|
chBg: THEME.addCharBg,
|
|
33
32
|
mark: '+',
|
|
34
33
|
},
|
|
34
|
+
warn: {
|
|
35
|
+
baseFg: THEME.delLineFg,
|
|
36
|
+
baseBg: THEME.delLineBg,
|
|
37
|
+
chFg: THEME.delCharFg,
|
|
38
|
+
chBg: THEME.delCharBg,
|
|
39
|
+
mark: ' ',
|
|
40
|
+
},
|
|
41
|
+
note: {
|
|
42
|
+
baseFg: THEME.chromeFg,
|
|
43
|
+
baseBg: THEME.noteBg,
|
|
44
|
+
chFg: THEME.chromeFg,
|
|
45
|
+
chBg: THEME.noteBg,
|
|
46
|
+
mark: ' ',
|
|
47
|
+
},
|
|
48
|
+
noteSep: {
|
|
49
|
+
baseFg: THEME.mutedFg,
|
|
50
|
+
baseBg: THEME.chromeBg,
|
|
51
|
+
chFg: THEME.mutedFg,
|
|
52
|
+
chBg: THEME.chromeBg,
|
|
53
|
+
mark: ' ',
|
|
54
|
+
},
|
|
35
55
|
ctx: {
|
|
36
56
|
baseFg: THEME.ctxFg,
|
|
37
57
|
baseBg: THEME.ctxBg,
|
|
@@ -56,6 +76,15 @@ const STAGED_STYLES = {
|
|
|
56
76
|
chBg: THEME.stagedAddCharBg,
|
|
57
77
|
mark: '+',
|
|
58
78
|
},
|
|
79
|
+
warn: {
|
|
80
|
+
baseFg: THEME.stagedDelLineFg,
|
|
81
|
+
baseBg: THEME.stagedDelLineBg,
|
|
82
|
+
chFg: THEME.stagedDelCharFg,
|
|
83
|
+
chBg: THEME.stagedDelCharBg,
|
|
84
|
+
mark: ' ',
|
|
85
|
+
},
|
|
86
|
+
note: STYLES.note,
|
|
87
|
+
noteSep: STYLES.noteSep,
|
|
59
88
|
ctx: STYLES.ctx,
|
|
60
89
|
};
|
|
61
90
|
|
|
@@ -74,9 +103,18 @@ const FILE_STATUS_FG = {
|
|
|
74
103
|
partial: THEME.warnFg,
|
|
75
104
|
};
|
|
76
105
|
|
|
106
|
+
const FILE_COL_KEYS = ['add', 'del', 'status', 'n', 'm'];
|
|
107
|
+
|
|
108
|
+
const FILE_COL_ALIGN = {
|
|
109
|
+
add: 'start',
|
|
110
|
+
del: 'start',
|
|
111
|
+
status: 'start',
|
|
112
|
+
n: 'start',
|
|
113
|
+
m: 'end',
|
|
114
|
+
};
|
|
115
|
+
|
|
77
116
|
const INFO_STATUS = [
|
|
78
117
|
'copied',
|
|
79
|
-
'skipped',
|
|
80
118
|
'staged',
|
|
81
119
|
'unstaged',
|
|
82
120
|
'not staged',
|
|
@@ -85,7 +123,6 @@ const INFO_STATUS = [
|
|
|
85
123
|
'read only',
|
|
86
124
|
'last block',
|
|
87
125
|
'first block',
|
|
88
|
-
'all skipped',
|
|
89
126
|
'first file',
|
|
90
127
|
'last file',
|
|
91
128
|
'nothing to review',
|
|
@@ -107,10 +144,14 @@ const BUTTON_GAP = ' ';
|
|
|
107
144
|
const LOGO = '👁️';
|
|
108
145
|
const FILE_MARK = '▶';
|
|
109
146
|
const FILE_LIST_PAD = 1;
|
|
147
|
+
const CHECK_DONE = 'xX';
|
|
110
148
|
const TODO_CAPTION = 'TODO:';
|
|
111
149
|
const INPUT_MAX = 4;
|
|
112
150
|
const NOTE_MAX = 4;
|
|
113
151
|
const NOTE_PAD = 1;
|
|
152
|
+
const SOFT_WRAP = ['warn', 'note'];
|
|
153
|
+
const DEL_CHAR_BG = ansi.bg(THEME.delCharBg);
|
|
154
|
+
const ADD_CHAR_BG = ansi.bg(THEME.addCharBg);
|
|
114
155
|
|
|
115
156
|
const lineStyles = (origin) => (origin === 'staged' ? STAGED_STYLES : STYLES);
|
|
116
157
|
|
|
@@ -137,7 +178,32 @@ const paintSpans = (pieces, lineStyle, budget, color) => {
|
|
|
137
178
|
return { text: out, used };
|
|
138
179
|
};
|
|
139
180
|
|
|
181
|
+
const paintAuditNote = (line, width, color) => {
|
|
182
|
+
const style = STYLES.note;
|
|
183
|
+
const gutterW = Math.min(GUTTER_W, Math.max(0, width));
|
|
184
|
+
const label = `${style.mark} `.slice(0, gutterW);
|
|
185
|
+
const gutter = paint(label, style.baseFg, style.baseBg, color);
|
|
186
|
+
const budget = Math.max(0, width - gutterW);
|
|
187
|
+
const text = truncateVisible(line.text ?? '', budget);
|
|
188
|
+
const used = visibleWidth(text);
|
|
189
|
+
let out = gutter;
|
|
190
|
+
if (text) out += paint(text, style.baseFg, style.baseBg, color);
|
|
191
|
+
if (used < budget) {
|
|
192
|
+
out += fill(budget - used, style.baseFg, style.baseBg, color);
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const paintAuditRule = (width, color) => {
|
|
198
|
+
const style = STYLES.noteSep;
|
|
199
|
+
const n = Math.max(0, width);
|
|
200
|
+
const bar = '─'.repeat(n);
|
|
201
|
+
return paint(bar, style.baseFg, style.baseBg, color);
|
|
202
|
+
};
|
|
203
|
+
|
|
140
204
|
const paintDiffLine = (line, width, color, origin, lang) => {
|
|
205
|
+
if (line.type === 'noteSep') return paintAuditRule(width, color);
|
|
206
|
+
if (line.type === 'note') return paintAuditNote(line, width, color);
|
|
141
207
|
const styles = lineStyles(origin);
|
|
142
208
|
const style = styles[line.type] ?? styles.ctx;
|
|
143
209
|
const diffSpans = line.spans ?? [{ text: line.text, changed: false }];
|
|
@@ -183,19 +249,14 @@ const paintSplitBlank = (width, color) => {
|
|
|
183
249
|
);
|
|
184
250
|
};
|
|
185
251
|
|
|
186
|
-
const
|
|
187
|
-
|
|
252
|
+
const paintBodyFill = (width, color, kind) => {
|
|
253
|
+
if (kind === 'files') {
|
|
188
254
|
const inner = Math.max(1, width - 1);
|
|
189
255
|
const main = fill(inner, THEME.ctxFg, THEME.ctxBg, color);
|
|
190
256
|
const edge = fill(1, THEME.ctxFg, THEME.ctxBg, color);
|
|
191
257
|
return main + edge;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
const paintBodyFill = (width, color, kind) => {
|
|
197
|
-
const paint = FILL_KIND[kind];
|
|
198
|
-
if (paint) return paint(width, color);
|
|
258
|
+
}
|
|
259
|
+
if (kind === 'split') return paintSplitBlank(width, color);
|
|
199
260
|
return fill(width, THEME.ctxFg, THEME.ctxBg, color);
|
|
200
261
|
};
|
|
201
262
|
|
|
@@ -215,22 +276,18 @@ const splitCheck = (text) => {
|
|
|
215
276
|
if (!match) return null;
|
|
216
277
|
const start = match.index;
|
|
217
278
|
const token = match[0];
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
after: text.slice(start + token.length),
|
|
223
|
-
};
|
|
279
|
+
const before = text.slice(0, start);
|
|
280
|
+
const mark = match[1];
|
|
281
|
+
const after = text.slice(start + token.length);
|
|
282
|
+
return { before, mark, token, after };
|
|
224
283
|
};
|
|
225
284
|
|
|
226
285
|
const paintCheckBar = (text, width, fgRgb, bgRgb, color) => {
|
|
227
286
|
const clipped = truncateVisible(text, width);
|
|
287
|
+
const parts = color ? splitCheck(clipped) : null;
|
|
288
|
+
if (!parts) return paintBar(text, width, fgRgb, bgRgb, color);
|
|
228
289
|
const pad = Math.max(0, width - visibleWidth(clipped));
|
|
229
|
-
const
|
|
230
|
-
if (!color) return filled;
|
|
231
|
-
const parts = splitCheck(clipped);
|
|
232
|
-
if (!parts) return `${seq(fgRgb, bgRgb)}${EL}${filled}${RESET}`;
|
|
233
|
-
const done = isDoneMark(parts.mark);
|
|
290
|
+
const done = CHECK_DONE.includes(parts.mark);
|
|
234
291
|
const chipFg = done ? THEME.checkDoneFg : THEME.checkFg;
|
|
235
292
|
const chipBg = done ? THEME.checkDoneBg : THEME.checkBg;
|
|
236
293
|
const after = `${parts.after}${' '.repeat(pad)}`;
|
|
@@ -241,10 +298,15 @@ const paintCheckBar = (text, width, fgRgb, bgRgb, color) => {
|
|
|
241
298
|
return out;
|
|
242
299
|
};
|
|
243
300
|
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
301
|
+
const ORIGIN_LABEL = {
|
|
302
|
+
commit: (view) => view.revShort || 'commit',
|
|
303
|
+
pr: (view) => view.sourceLabel || 'pr',
|
|
304
|
+
todo: () => 'todo',
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const originLabel = (origin, view) => {
|
|
308
|
+
const named = ORIGIN_LABEL[origin];
|
|
309
|
+
if (named) return named(view);
|
|
248
310
|
return origin ?? 'unknown';
|
|
249
311
|
};
|
|
250
312
|
|
|
@@ -281,7 +343,7 @@ const headerTarget = (view) => {
|
|
|
281
343
|
if (!view.item) return { path: '', origin: '', n: 0, total: view.total ?? 0 };
|
|
282
344
|
return {
|
|
283
345
|
path: itemPath(view.item),
|
|
284
|
-
origin: originLabel(view.item.origin, view
|
|
346
|
+
origin: originLabel(view.item.origin, view),
|
|
285
347
|
n: view.index + 1,
|
|
286
348
|
total: view.total,
|
|
287
349
|
};
|
|
@@ -295,17 +357,20 @@ const headerPrefix = (view) => {
|
|
|
295
357
|
return ` ${LOGO} reslop: `;
|
|
296
358
|
};
|
|
297
359
|
|
|
360
|
+
const headerSuffix = ({ origin, n, total }) =>
|
|
361
|
+
origin ? ` ${origin} ${n}/${total}` : ` ${n}/${total}`;
|
|
362
|
+
|
|
298
363
|
const headerText = (view) => {
|
|
299
364
|
const prefix = headerPrefix(view);
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
return `${prefix}${path}${suffix}`;
|
|
365
|
+
const target = headerTarget(view);
|
|
366
|
+
return `${prefix}${target.path}${headerSuffix(target)}`;
|
|
303
367
|
};
|
|
304
368
|
|
|
305
369
|
const headerPieces = (view, width) => {
|
|
306
370
|
const repo = view.repoName || '';
|
|
307
|
-
const
|
|
308
|
-
const
|
|
371
|
+
const target = headerTarget(view);
|
|
372
|
+
const path = target.path;
|
|
373
|
+
const suffix = headerSuffix(target);
|
|
309
374
|
const right = ' ';
|
|
310
375
|
const chrome = ` ${LOGO} reslop: `;
|
|
311
376
|
const prefixW = visibleWidth(headerPrefix(view));
|
|
@@ -336,7 +401,7 @@ const headerPieces = (view, width) => {
|
|
|
336
401
|
const paintHeader = (view, width, color) => {
|
|
337
402
|
const pieces = headerPieces(view, width);
|
|
338
403
|
if (!color) {
|
|
339
|
-
return pieces.map((
|
|
404
|
+
return pieces.map((piece) => piece.text).join('');
|
|
340
405
|
}
|
|
341
406
|
const bgRgb = THEME.headerBg;
|
|
342
407
|
let out = `${seq(THEME.headerChromeFg, bgRgb)}${EL}`;
|
|
@@ -355,7 +420,8 @@ const bodyLines = (item, layout) => {
|
|
|
355
420
|
return [{ type: 'ctx', text, spans: [{ text, changed: false }] }];
|
|
356
421
|
}
|
|
357
422
|
if (!item.hunk) return [];
|
|
358
|
-
|
|
423
|
+
const radius = item.dep ? Number.MAX_SAFE_INTEGER : undefined;
|
|
424
|
+
return displayLines(item.hunk, item.blockId, layout, radius);
|
|
359
425
|
};
|
|
360
426
|
|
|
361
427
|
const sourceEmpty = (line) => !line || !(line.text ?? '').trim();
|
|
@@ -373,26 +439,6 @@ const dropLeadingEmpty = (lines, split) => {
|
|
|
373
439
|
return i ? lines.slice(i) : lines;
|
|
374
440
|
};
|
|
375
441
|
|
|
376
|
-
const helpLines = () => {
|
|
377
|
-
const lines = [' keys and buttons', ''];
|
|
378
|
-
for (const action of ACTIONS) {
|
|
379
|
-
const keys = actionHot(action);
|
|
380
|
-
let label = action.label;
|
|
381
|
-
if (action.id === 'layout') {
|
|
382
|
-
label = 'Mode (unified / mixed / side)';
|
|
383
|
-
}
|
|
384
|
-
lines.push(` ${keys.padEnd(16)} ${label}`);
|
|
385
|
-
}
|
|
386
|
-
lines.push(' enter open file / save note');
|
|
387
|
-
lines.push(' esc files, then quit');
|
|
388
|
-
lines.push(' del, backspace remove todo');
|
|
389
|
-
lines.push(' j, k, wheel scroll / move');
|
|
390
|
-
lines.push(' drag copy');
|
|
391
|
-
lines.push(' ctrl-z, ctrl-y undo / redo');
|
|
392
|
-
lines.push(' ?, h help');
|
|
393
|
-
return lines;
|
|
394
|
-
};
|
|
395
|
-
|
|
396
442
|
const BUTTON_MARK = {
|
|
397
443
|
bgRgb: THEME.buttonBg,
|
|
398
444
|
fgRgb: THEME.buttonFg,
|
|
@@ -429,15 +475,16 @@ const layoutButtons = (width, compact = false, disabled = []) => {
|
|
|
429
475
|
const hits = [];
|
|
430
476
|
const parts = [];
|
|
431
477
|
let x = 0;
|
|
432
|
-
for (const
|
|
478
|
+
for (const id of ACTION_IDS) {
|
|
479
|
+
const action = { id, ...ACTIONS[id] };
|
|
433
480
|
const letter = actionLetter(action);
|
|
434
481
|
const word = buttonWord(action);
|
|
435
482
|
const label = short ? letter : word;
|
|
436
483
|
const piece = `${BUTTON_GAP}${label}`;
|
|
437
484
|
const w = visibleWidth(piece);
|
|
438
485
|
if (x + w > width && parts.length) break;
|
|
439
|
-
const isDisabled = disabled.includes(
|
|
440
|
-
if (!isDisabled) hits.push({ id
|
|
486
|
+
const isDisabled = disabled.includes(id);
|
|
487
|
+
if (!isDisabled) hits.push({ id, x0: x, x1: x + w });
|
|
441
488
|
parts.push({ action, piece, letter, label, disabled: isDisabled });
|
|
442
489
|
x += w;
|
|
443
490
|
}
|
|
@@ -445,7 +492,7 @@ const layoutButtons = (width, compact = false, disabled = []) => {
|
|
|
445
492
|
};
|
|
446
493
|
if (compact) return build(true);
|
|
447
494
|
const words = build(false);
|
|
448
|
-
if (words.parts.length ===
|
|
495
|
+
if (words.parts.length === ACTION_IDS.length) return words;
|
|
449
496
|
return build(true);
|
|
450
497
|
};
|
|
451
498
|
|
|
@@ -467,26 +514,38 @@ const paintButtons = (layout, width, color) => {
|
|
|
467
514
|
return out;
|
|
468
515
|
};
|
|
469
516
|
|
|
517
|
+
const COUNTS_KIND = {
|
|
518
|
+
pr: ({ counts, notes, sourceLabel }) => {
|
|
519
|
+
const n = counts.pr ?? 0;
|
|
520
|
+
const label = sourceLabel || 'pr';
|
|
521
|
+
return ` pr ${label} ${n} ${notes}`;
|
|
522
|
+
},
|
|
523
|
+
commit: ({ counts, notes, revShort }) => {
|
|
524
|
+
const n = counts.commit ?? 0;
|
|
525
|
+
return ` commit ${revShort} ${n} ${notes}`;
|
|
526
|
+
},
|
|
527
|
+
worktree: ({ counts, notes }) => {
|
|
528
|
+
const staged = counts.staged ?? 0;
|
|
529
|
+
const unstaged = counts.unstaged ?? 0;
|
|
530
|
+
const untracked = counts.untracked ?? 0;
|
|
531
|
+
const gitCounts = `unstaged ${unstaged} untracked ${untracked}`;
|
|
532
|
+
const git = `staged ${staged} ${gitCounts}`;
|
|
533
|
+
return ` ${git} ${notes}`;
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const countsKind = (sourceKind, revShort) => {
|
|
538
|
+
if (sourceKind === 'pr') return 'pr';
|
|
539
|
+
if (revShort) return 'commit';
|
|
540
|
+
return 'worktree';
|
|
541
|
+
};
|
|
542
|
+
|
|
470
543
|
const countsLine = (counts, revShort, sourceKind, sourceLabel) => {
|
|
471
|
-
const skipped = counts.skipped ?? 0;
|
|
472
544
|
const feedback = counts.feedback ?? 0;
|
|
473
545
|
const todo = counts.todo ?? 0;
|
|
474
546
|
const notes = `feedback ${feedback} todo ${todo}`;
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
const n = counts.pr ?? 0;
|
|
478
|
-
const label = sourceLabel || 'pr';
|
|
479
|
-
return ` pr ${label} ${n} ${notes}${extra}`;
|
|
480
|
-
}
|
|
481
|
-
if (revShort) {
|
|
482
|
-
const n = counts.commit ?? 0;
|
|
483
|
-
return ` commit ${revShort} ${n} ${notes}${extra}`;
|
|
484
|
-
}
|
|
485
|
-
const staged = counts.staged ?? 0;
|
|
486
|
-
const unstaged = counts.unstaged ?? 0;
|
|
487
|
-
const untracked = counts.untracked ?? 0;
|
|
488
|
-
const git = `staged ${staged} unstaged ${unstaged} untracked ${untracked}`;
|
|
489
|
-
return ` ${git} ${notes}${extra}`;
|
|
547
|
+
const kind = countsKind(sourceKind, revShort);
|
|
548
|
+
return COUNTS_KIND[kind]({ counts, notes, sourceLabel, revShort });
|
|
490
549
|
};
|
|
491
550
|
|
|
492
551
|
const paintQuitPrompt = (width, color) => {
|
|
@@ -526,23 +585,36 @@ const paintStatusLine = (view, status, width, color) => {
|
|
|
526
585
|
);
|
|
527
586
|
};
|
|
528
587
|
|
|
588
|
+
const signedCount = (sign, n) => `${sign}${n ?? 0}`;
|
|
589
|
+
|
|
590
|
+
const fileRowFields = (entry) => {
|
|
591
|
+
const { added, removed, staged, remaining } = entry;
|
|
592
|
+
const add = signedCount('+', added);
|
|
593
|
+
const del = signedCount('-', removed);
|
|
594
|
+
const status = entry.status ?? '';
|
|
595
|
+
const n = `${staged ?? 0}`;
|
|
596
|
+
const m = `${remaining ?? 0}`;
|
|
597
|
+
return { add, del, status, n, m };
|
|
598
|
+
};
|
|
599
|
+
|
|
529
600
|
const fileColumns = (files) => {
|
|
530
|
-
|
|
531
|
-
|
|
601
|
+
const widths = {};
|
|
602
|
+
for (const key of FILE_COL_KEYS) widths[key] = 0;
|
|
532
603
|
for (const entry of files) {
|
|
533
|
-
const
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
if (cw > countW) countW = cw;
|
|
604
|
+
const fields = fileRowFields(entry);
|
|
605
|
+
for (const key of FILE_COL_KEYS) {
|
|
606
|
+
const w = visibleWidth(fields[key]);
|
|
607
|
+
if (w > widths[key]) widths[key] = w;
|
|
608
|
+
}
|
|
539
609
|
}
|
|
540
|
-
return
|
|
610
|
+
return widths;
|
|
541
611
|
};
|
|
542
612
|
|
|
543
|
-
const
|
|
613
|
+
const padVisible = (text, width, align) => {
|
|
544
614
|
const n = Math.max(0, width - visibleWidth(text));
|
|
545
|
-
|
|
615
|
+
const gap = ' '.repeat(n);
|
|
616
|
+
if (align === 'end') return `${text}${gap}`;
|
|
617
|
+
return `${gap}${text}`;
|
|
546
618
|
};
|
|
547
619
|
|
|
548
620
|
const fileRowFg = (status, selected) => {
|
|
@@ -552,25 +624,55 @@ const fileRowFg = (status, selected) => {
|
|
|
552
624
|
return THEME.mutedFg;
|
|
553
625
|
};
|
|
554
626
|
|
|
627
|
+
const fileRowParts = (entry, cols) => {
|
|
628
|
+
const fields = fileRowFields(entry);
|
|
629
|
+
const parts = {};
|
|
630
|
+
for (const key of FILE_COL_KEYS) {
|
|
631
|
+
const width = cols?.[key] ?? visibleWidth(fields[key]);
|
|
632
|
+
const align = FILE_COL_ALIGN[key];
|
|
633
|
+
parts[key] = padVisible(fields[key], width, align);
|
|
634
|
+
}
|
|
635
|
+
return parts;
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
const joinFileSuffix = (parts) => {
|
|
639
|
+
const { add, del, status, n, m } = parts;
|
|
640
|
+
return ` ${add} ${del} ${status} ${n}/${m}`;
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const paintFileSuffix = (parts, fgRgb, bgRgb, color) => {
|
|
644
|
+
const { add, del, status, n, m } = parts;
|
|
645
|
+
const gap = paint(' ', fgRgb, bgRgb, color);
|
|
646
|
+
const addText = paint(add, THEME.addLineFg, bgRgb, color);
|
|
647
|
+
const delText = paint(del, THEME.delLineFg, bgRgb, color);
|
|
648
|
+
const statusText = paint(status, fgRgb, bgRgb, color);
|
|
649
|
+
const nText = paint(n, fgRgb, bgRgb, color);
|
|
650
|
+
const slash = paint('/', fgRgb, bgRgb, color);
|
|
651
|
+
const mText = paint(m, fgRgb, bgRgb, color);
|
|
652
|
+
const counts = addText + gap + delText;
|
|
653
|
+
const meta = statusText + gap + nText + slash + mText;
|
|
654
|
+
return gap + counts + gap + meta;
|
|
655
|
+
};
|
|
656
|
+
|
|
555
657
|
const paintFileRow = (entry, width, color, selected, cols) => {
|
|
556
658
|
const inner = Math.max(1, width - 1);
|
|
557
659
|
const mark = selected ? FILE_MARK : ' ';
|
|
558
660
|
const prefix = ` ${mark} `;
|
|
559
|
-
const
|
|
560
|
-
const
|
|
561
|
-
const statusW = cols?.statusW ?? visibleWidth(status);
|
|
562
|
-
const countW = cols?.countW ?? visibleWidth(count);
|
|
563
|
-
const statusCol = padStartVisible(status, statusW);
|
|
564
|
-
const countCol = padStartVisible(count, countW);
|
|
565
|
-
const suffix = ` ${statusCol} ${countCol}`;
|
|
661
|
+
const parts = fileRowParts(entry, cols);
|
|
662
|
+
const suffix = joinFileSuffix(parts);
|
|
566
663
|
const pathW = Math.max(1, inner - visibleWidth(prefix + suffix));
|
|
567
|
-
const
|
|
664
|
+
const { path, status } = entry;
|
|
665
|
+
const clipped = truncateVisible(path, pathW);
|
|
568
666
|
const pad = Math.max(0, pathW - visibleWidth(clipped));
|
|
569
|
-
const
|
|
667
|
+
const left = `${prefix}${clipped}${' '.repeat(pad)}`;
|
|
570
668
|
const fgRgb = fileRowFg(status, selected);
|
|
571
669
|
const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
|
|
572
|
-
const
|
|
573
|
-
|
|
670
|
+
const edge = fill(1, fgRgb, bgRgb, color);
|
|
671
|
+
if (!color) return `${left}${suffix}${edge}`;
|
|
672
|
+
let out = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
673
|
+
out += paint(left, fgRgb, bgRgb, color);
|
|
674
|
+
out += paintFileSuffix(parts, fgRgb, bgRgb, color);
|
|
675
|
+
return out + edge;
|
|
574
676
|
};
|
|
575
677
|
|
|
576
678
|
const todoInnerWidth = (width) => Math.max(1, width - 2);
|
|
@@ -669,8 +771,7 @@ const paintIdleNote = (view, width, color) => {
|
|
|
669
771
|
const text = view.noteText;
|
|
670
772
|
if (!text) return empty;
|
|
671
773
|
const inner = noteInnerWidth(width);
|
|
672
|
-
const
|
|
673
|
-
const wrapped = wrapMultiline(`${prefix}${text}`, inner);
|
|
774
|
+
const wrapped = wrapMultiline(`feedback: ${text}`, inner);
|
|
674
775
|
const rows = [];
|
|
675
776
|
for (const line of wrapped.slice(0, NOTE_MAX)) {
|
|
676
777
|
const painted = paintNoteLine(
|
|
@@ -688,7 +789,7 @@ const paintIdleNote = (view, width, color) => {
|
|
|
688
789
|
|
|
689
790
|
const paintNotePanel = (view, width, color, cap) => {
|
|
690
791
|
const empty = { rows: [], cursor: null, templateHits: [] };
|
|
691
|
-
if (
|
|
792
|
+
if ((view.pane ?? 'diff') === 'files') return empty;
|
|
692
793
|
const compose = view.compose;
|
|
693
794
|
if (compose && compose.kind === 'feedback') {
|
|
694
795
|
return paintComposeNote(view, width, color, cap);
|
|
@@ -696,16 +797,6 @@ const paintNotePanel = (view, width, color, cap) => {
|
|
|
696
797
|
return paintIdleNote(view, width, color);
|
|
697
798
|
};
|
|
698
799
|
|
|
699
|
-
const paintBodyHelp = (width, color) => {
|
|
700
|
-
const body = [];
|
|
701
|
-
for (const line of helpLines()) {
|
|
702
|
-
for (const row of wrapPlain(line, width)) {
|
|
703
|
-
body.push(paintBar(row, width, THEME.chromeFg, THEME.ctxBg, color));
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
return body;
|
|
707
|
-
};
|
|
708
|
-
|
|
709
800
|
const paintBodyFiles = (view, width, color, bodyH, headerLines) => {
|
|
710
801
|
const files = view.files ?? [];
|
|
711
802
|
const cols = fileColumns(files);
|
|
@@ -753,10 +844,39 @@ const paintBodyTodo = (view, width, color) => {
|
|
|
753
844
|
return { body, todoOwners, cursor };
|
|
754
845
|
};
|
|
755
846
|
|
|
847
|
+
const wrapSoftLine = (line, width) => {
|
|
848
|
+
const gutterW = Math.min(GUTTER_W, Math.max(0, width));
|
|
849
|
+
const budget = Math.max(1, width - gutterW);
|
|
850
|
+
const rows = wrapPlain(line.text ?? '', budget);
|
|
851
|
+
return rows.map((text) => {
|
|
852
|
+
const spans = [{ text, changed: false }];
|
|
853
|
+
return { ...line, text, spans };
|
|
854
|
+
});
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
const expandSoftRows = (lines, width, split) => {
|
|
858
|
+
const wrapWidth = split ? splitWidths(width).leftW : width;
|
|
859
|
+
const out = [];
|
|
860
|
+
for (const row of lines) {
|
|
861
|
+
const line = split ? row.left || row.right : row;
|
|
862
|
+
if (!line || !SOFT_WRAP.includes(line.type)) {
|
|
863
|
+
out.push(row);
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
const parts = wrapSoftLine(line, wrapWidth);
|
|
867
|
+
for (const part of parts) {
|
|
868
|
+
if (split) out.push({ left: part, right: part });
|
|
869
|
+
else out.push(part);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return out;
|
|
873
|
+
};
|
|
874
|
+
|
|
756
875
|
const paintBodyDiff = (item, layout, width, color) => {
|
|
757
876
|
const lang = detectLang(itemPath(item));
|
|
758
877
|
const splitBody = layout === 'side' && !item.file.isBinary;
|
|
759
|
-
const
|
|
878
|
+
const raw = dropLeadingEmpty(bodyLines(item, layout), splitBody);
|
|
879
|
+
const lines = expandSoftRows(raw, width, splitBody);
|
|
760
880
|
const body = [];
|
|
761
881
|
if (splitBody) {
|
|
762
882
|
for (const row of lines) {
|
|
@@ -771,7 +891,6 @@ const paintBodyDiff = (item, layout, width, color) => {
|
|
|
771
891
|
};
|
|
772
892
|
|
|
773
893
|
const bodyKind = (view) => {
|
|
774
|
-
if (view.help) return 'help';
|
|
775
894
|
if ((view.pane ?? 'diff') === 'files') return 'files';
|
|
776
895
|
if (!view.item) return 'empty';
|
|
777
896
|
if (view.item.origin === 'todo') return 'todo';
|
|
@@ -779,11 +898,6 @@ const bodyKind = (view) => {
|
|
|
779
898
|
};
|
|
780
899
|
|
|
781
900
|
const BODY_PAINT = {
|
|
782
|
-
help: (view, width, color) => ({
|
|
783
|
-
body: paintBodyHelp(width, color),
|
|
784
|
-
fileHits: [],
|
|
785
|
-
todoOwners: [],
|
|
786
|
-
}),
|
|
787
901
|
files: (view, width, color, bodyH, headerLines) => ({
|
|
788
902
|
...paintBodyFiles(view, width, color, bodyH, headerLines),
|
|
789
903
|
todoOwners: [],
|
|
@@ -837,10 +951,10 @@ const windowBody = (view, width, color, bodyH, headerLines, paintedBody) => {
|
|
|
837
951
|
const todoOwners = paintedBody.todoOwners;
|
|
838
952
|
const splitBody = paintedBody.splitBody === true;
|
|
839
953
|
const pane = view.pane ?? 'diff';
|
|
840
|
-
const listPane = pane === 'files'
|
|
954
|
+
const listPane = pane === 'files';
|
|
841
955
|
const fillKind = bodyFillKind(listPane, splitBody);
|
|
842
956
|
let shift = 0;
|
|
843
|
-
if (!listPane &&
|
|
957
|
+
if (!listPane && body.length > 0) {
|
|
844
958
|
body.unshift(paintBodyFill(width, color, fillKind));
|
|
845
959
|
if (todoOwners.length) todoOwners.unshift(null);
|
|
846
960
|
shift = 1;
|
|
@@ -889,7 +1003,7 @@ const stackFrame = (view, width, color, rows, layout, windowed, notes) => {
|
|
|
889
1003
|
return overlayRows(rows, view.selection);
|
|
890
1004
|
};
|
|
891
1005
|
|
|
892
|
-
const frameResult = (text, painted, layout, windowed, hits) => {
|
|
1006
|
+
const frameResult = (text, painted, layout, windowed, hits, bodyH) => {
|
|
893
1007
|
const fileHits = windowed.fileHits;
|
|
894
1008
|
const todoHits = windowed.todoHits;
|
|
895
1009
|
const templateHits = hits.templateHits;
|
|
@@ -907,6 +1021,7 @@ const frameResult = (text, painted, layout, windowed, hits) => {
|
|
|
907
1021
|
height: painted.length,
|
|
908
1022
|
scroll,
|
|
909
1023
|
cursor,
|
|
1024
|
+
bodyH,
|
|
910
1025
|
};
|
|
911
1026
|
};
|
|
912
1027
|
|
|
@@ -938,7 +1053,7 @@ const renderFrame = (view, options = {}) => {
|
|
|
938
1053
|
notes.noteCursor,
|
|
939
1054
|
bodyH,
|
|
940
1055
|
);
|
|
941
|
-
return frameResult(text, painted, layout, windowed, hits);
|
|
1056
|
+
return frameResult(text, painted, layout, windowed, hits, bodyH);
|
|
942
1057
|
};
|
|
943
1058
|
|
|
944
1059
|
const presentCursor = (cursor) => {
|
|
@@ -961,36 +1076,18 @@ const presentRows = (rows, options = {}) => {
|
|
|
961
1076
|
return parts.join('');
|
|
962
1077
|
};
|
|
963
1078
|
|
|
964
|
-
const DEL_CHAR_BG = ansi.bg(THEME.delCharBg);
|
|
965
|
-
const ADD_CHAR_BG = ansi.bg(THEME.addCharBg);
|
|
966
|
-
const DEL_LINE_FG = ansi.fg(THEME.delLineFg);
|
|
967
|
-
const ADD_LINE_FG = ansi.fg(THEME.addLineFg);
|
|
968
|
-
|
|
969
1079
|
module.exports = {
|
|
970
|
-
LOGO,
|
|
971
|
-
FILE_MARK,
|
|
972
1080
|
QUIT_FINISH,
|
|
973
1081
|
QUIT_CONTINUE,
|
|
974
1082
|
QUIT_PROMPT,
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
renderFrame,
|
|
978
|
-
presentCursor,
|
|
979
|
-
presentRows,
|
|
980
|
-
layoutButtons,
|
|
1083
|
+
DEL_CHAR_BG,
|
|
1084
|
+
ADD_CHAR_BG,
|
|
981
1085
|
paintDiffLine,
|
|
982
|
-
paintSplitRow,
|
|
983
|
-
bodyLines,
|
|
984
1086
|
headerText,
|
|
985
|
-
|
|
986
|
-
wrapDoc,
|
|
987
|
-
wrapMultiline,
|
|
988
|
-
cursorInWrap,
|
|
989
|
-
wrapMove,
|
|
990
|
-
noteInnerWidth,
|
|
1087
|
+
layoutButtons,
|
|
991
1088
|
todoInnerWidth,
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1089
|
+
noteInnerWidth,
|
|
1090
|
+
renderFrame,
|
|
1091
|
+
presentCursor,
|
|
1092
|
+
presentRows,
|
|
996
1093
|
};
|