reslop 0.1.8 → 0.2.0
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 +25 -1
- package/README.md +27 -38
- package/bin/reslop.js +2 -2
- package/lib/ansi.js +6 -2
- package/lib/api.js +2 -2
- package/lib/capabilities.js +10 -14
- package/lib/cli.js +28 -47
- package/lib/clipboard.js +1 -1
- package/lib/deps-diff.js +712 -0
- package/lib/deps.js +556 -32
- package/lib/detect.js +1 -7
- package/lib/diff/align.js +7 -10
- package/lib/diff/diff.js +53 -0
- package/lib/diff/display.js +5 -4
- package/lib/diff/inline.js +26 -21
- package/lib/diff/items.js +6 -4
- package/lib/diff/patch.js +1 -3
- package/lib/diff/unit.js +392 -0
- package/lib/files.js +34 -8
- package/lib/{git/branches.js → git-branches.js} +103 -10
- package/lib/{git/dependencies.js → git-deps.js} +111 -38
- package/lib/git.js +592 -65
- package/lib/github.js +28 -197
- package/lib/gitlab.js +24 -195
- package/lib/highlight/bash.js +1 -3
- package/lib/highlight/css.js +1 -3
- package/lib/highlight/js.js +11 -14
- package/lib/highlight.js +6 -9
- package/lib/keys.js +22 -9
- package/lib/manifest.js +330 -0
- package/lib/remote.js +374 -0
- package/lib/render/branches.js +34 -66
- package/lib/render/chrome.js +57 -22
- package/lib/render/commits.js +85 -0
- package/lib/render/diff.js +16 -16
- package/lib/render/files.js +29 -37
- package/lib/render/frame.js +29 -13
- package/lib/render/notes.js +4 -3
- package/lib/render/primitives.js +46 -1
- package/lib/{render.js → render/render.js} +6 -7
- package/lib/render/unit.js +41 -0
- package/lib/review.js +750 -14
- package/lib/select.js +5 -10
- package/lib/session/accessors.js +9 -1
- package/lib/session/actions.js +93 -36
- package/lib/session/branches.js +130 -144
- package/lib/session/change-actions.js +206 -124
- package/lib/session/commit.js +217 -62
- package/lib/session/compose.js +158 -13
- package/lib/session/dispatch.js +109 -55
- package/lib/session/files-pane.js +145 -142
- package/lib/session/items.js +131 -34
- package/lib/session/lifecycle.js +82 -42
- package/lib/session/load.js +19 -33
- package/lib/session/navigation.js +15 -3
- package/lib/session/ops.js +28 -41
- package/lib/session/pointer.js +58 -54
- package/lib/session/review.js +23 -37
- package/lib/session/terminal.js +6 -12
- package/lib/session/todos.js +8 -12
- package/lib/session/unit.js +208 -0
- package/lib/session/update.js +33 -46
- package/lib/session/view.js +82 -11
- package/lib/session/watch.js +11 -19
- package/lib/session.js +29 -226
- package/lib/source.js +3 -3
- package/lib/update.js +40 -75
- package/lib/{git/proc.js → utilities.js} +75 -11
- package/lib/wrap.js +1 -3
- package/package.json +2 -2
- package/lib/deps/changes.js +0 -331
- package/lib/deps/items.js +0 -131
- package/lib/deps/manifest.js +0 -145
- package/lib/deps/merge.js +0 -120
- package/lib/deps/proposals.js +0 -164
- package/lib/deps/reports.js +0 -180
- package/lib/deps/rows.js +0 -177
- package/lib/deps/scan.js +0 -138
- package/lib/deps/usage.js +0 -68
- package/lib/deps/versions.js +0 -119
- package/lib/diff.js +0 -33
- package/lib/git/changes.js +0 -164
- package/lib/git/read.js +0 -253
- package/lib/git/run.js +0 -100
- package/lib/npm/commands.js +0 -100
- package/lib/remote/changes.js +0 -37
- package/lib/remote/headers.js +0 -36
- package/lib/remote/http.js +0 -122
- package/lib/remote/notes.js +0 -94
- package/lib/review/format.js +0 -434
- package/lib/review/storage.js +0 -150
- package/lib/review/store.js +0 -178
- package/lib/review/templates.js +0 -49
- package/lib/session/origins.js +0 -7
- package/lib/sys.js +0 -84
package/lib/render/chrome.js
CHANGED
|
@@ -6,10 +6,12 @@ const files = require('../files.js');
|
|
|
6
6
|
const primitives = require('./primitives.js');
|
|
7
7
|
|
|
8
8
|
const { itemPath, TODO_FILE, isTodosEntry, fileTotals } = files;
|
|
9
|
-
const { ACTION_IDS, BRANCH_ACTIONS,
|
|
10
|
-
const {
|
|
11
|
-
const {
|
|
12
|
-
const {
|
|
9
|
+
const { ACTION_IDS, BRANCH_ACTIONS, COMMIT_ACTIONS, actionLetter } = keys;
|
|
10
|
+
const { buttonWord } = keys;
|
|
11
|
+
const { FILES_HIDDEN, FILES_GIT_DISABLED, BRANCHES_DISABLED } = keys;
|
|
12
|
+
const { COMMITS_DISABLED, TODO_DISABLED, DIFF_DISABLED, UNIT_DISABLED } = keys;
|
|
13
|
+
const { diffGitDisabled, branchGitDisabled, commitGitDisabled } = keys;
|
|
14
|
+
const { CONFIRM, confirmPrefix, promptFromChoices } = keys;
|
|
13
15
|
const { THEME, paint, visibleWidth } = ansi;
|
|
14
16
|
const { truncateVisible, fg, RESET, EL, seq } = ansi;
|
|
15
17
|
const { fill, branchLabel, paintBracketName } = primitives;
|
|
@@ -63,6 +65,8 @@ const INFO_STATUS = [
|
|
|
63
65
|
'saved',
|
|
64
66
|
'reloaded',
|
|
65
67
|
'not a diff block',
|
|
68
|
+
'file scope',
|
|
69
|
+
'diff mode',
|
|
66
70
|
'committed',
|
|
67
71
|
'amended',
|
|
68
72
|
'fixup',
|
|
@@ -91,7 +95,6 @@ const INFO_STATUS = [
|
|
|
91
95
|
const INFO_PREFIX = ['checked out ', 'created ', 'rebased onto ', 'dropped '];
|
|
92
96
|
|
|
93
97
|
const QUIT_PROMPT = promptFromChoices(CONFIRM.confirmQuit.choices);
|
|
94
|
-
const COMMIT_PROMPT = promptFromChoices(CONFIRM.confirmCommit.choices);
|
|
95
98
|
const updatePrompt = (from, to) => ` update reslop ${from} → ${to}? y/n`;
|
|
96
99
|
const dropPrompt = (name) => ` drop ${name}? y/n`;
|
|
97
100
|
const PUSH_REJECT_PREFIX = CONFIRM.confirmPush.prefix;
|
|
@@ -112,8 +115,6 @@ const originLabel = (origin, view) => {
|
|
|
112
115
|
return origin ?? 'unknown';
|
|
113
116
|
};
|
|
114
117
|
|
|
115
|
-
const headerRoleFg = (role) => HEADER_ROLE_FG[role] ?? THEME.headerChromeFg;
|
|
116
|
-
|
|
117
118
|
const pathSegments = (rel) => {
|
|
118
119
|
const segs = [];
|
|
119
120
|
if (!rel) return segs;
|
|
@@ -160,6 +161,24 @@ const headerTarget = (view) => {
|
|
|
160
161
|
const origin = entry.current ? 'current' : 'branch';
|
|
161
162
|
return { path: entry.name, origin, n, total };
|
|
162
163
|
}
|
|
164
|
+
if (pane === 'commits') {
|
|
165
|
+
const commits = view.commits ?? [];
|
|
166
|
+
const cursor = view.commitCursor ?? 0;
|
|
167
|
+
const entry = commits[cursor];
|
|
168
|
+
const total = commits.length;
|
|
169
|
+
const n = total ? cursor + 1 : 0;
|
|
170
|
+
if (!entry) return { path: '', origin: 'commit', n, total };
|
|
171
|
+
const origin = entry.head ? 'HEAD' : 'commit';
|
|
172
|
+
return { path: entry.subject ?? '', origin, n, total };
|
|
173
|
+
}
|
|
174
|
+
if (pane === 'unit') {
|
|
175
|
+
const path = view.reviewPath ?? '';
|
|
176
|
+
const lines = view.unitLines ?? [];
|
|
177
|
+
const top = Math.max(0, (view.scroll ?? 0) - 1);
|
|
178
|
+
const n = lines.length ? Math.min(lines.length, top + 1) : 0;
|
|
179
|
+
const origin = view.item ? originLabel(view.item.origin, view) : 'unit';
|
|
180
|
+
return { path, origin, n, total: lines.length };
|
|
181
|
+
}
|
|
163
182
|
if (!view.item) return { path: '', origin: '', n: 0, total: view.total ?? 0 };
|
|
164
183
|
if (view.item.origin === 'todo') {
|
|
165
184
|
return { path: TODO_FILE, origin: 'todo', n: 1, total: 1 };
|
|
@@ -232,7 +251,8 @@ const paintHeader = (view, width, color) => {
|
|
|
232
251
|
if (!piece.text) {
|
|
233
252
|
continue;
|
|
234
253
|
}
|
|
235
|
-
|
|
254
|
+
const roleFg = HEADER_ROLE_FG[piece.role] ?? THEME.headerChromeFg;
|
|
255
|
+
out += `${fg(roleFg)}${piece.text}`;
|
|
236
256
|
}
|
|
237
257
|
return `${out}${RESET}`;
|
|
238
258
|
};
|
|
@@ -276,7 +296,7 @@ const layoutButtons = (width, compact, hidden, extra, disabled) => {
|
|
|
276
296
|
for (const id of ACTION_IDS) {
|
|
277
297
|
if (id === 'quit') {
|
|
278
298
|
for (const add of extraIds) {
|
|
279
|
-
if (!
|
|
299
|
+
if (!ids.includes(add)) ids.push(add);
|
|
280
300
|
}
|
|
281
301
|
}
|
|
282
302
|
if (hiddenIds.includes(id)) continue;
|
|
@@ -380,8 +400,8 @@ const statusDelta = (total) => {
|
|
|
380
400
|
const hasDelta =
|
|
381
401
|
stagedAdd || unstagedAdd || stagedDel || unstagedDel || total.added;
|
|
382
402
|
if (!hasDelta && !total.removed) return '';
|
|
383
|
-
const plus = `+${stagedAdd}/${unstagedAdd}`;
|
|
384
|
-
const minus = `-${stagedDel}/${unstagedDel}`;
|
|
403
|
+
const plus = `+${stagedAdd}/${stagedAdd + unstagedAdd}`;
|
|
404
|
+
const minus = `-${stagedDel}/${stagedDel + unstagedDel}`;
|
|
385
405
|
return joinStatus([plus, minus]);
|
|
386
406
|
};
|
|
387
407
|
|
|
@@ -417,12 +437,14 @@ const statusLayout = (view, status, width) => {
|
|
|
417
437
|
};
|
|
418
438
|
|
|
419
439
|
const choiceHits = (choices, prefix, y) => {
|
|
420
|
-
const
|
|
440
|
+
const { length } = choices;
|
|
441
|
+
const hits = new Array(length);
|
|
421
442
|
let x = visibleWidth(prefix);
|
|
422
|
-
for (
|
|
443
|
+
for (let i = 0; i < length; i++) {
|
|
444
|
+
const choice = choices[i];
|
|
423
445
|
const piece = `${choice.lead}${choice.label}`;
|
|
424
446
|
const w = visibleWidth(piece);
|
|
425
|
-
hits
|
|
447
|
+
hits[i] = { id: choice.letter, y, x0: x, x1: x + w };
|
|
426
448
|
x += w;
|
|
427
449
|
}
|
|
428
450
|
return hits;
|
|
@@ -525,10 +547,16 @@ const statusBranchHit = (view, width, y) => {
|
|
|
525
547
|
return { id: 'branch', y, x0: open, x1: close + 1 };
|
|
526
548
|
};
|
|
527
549
|
|
|
550
|
+
const selectedBranch = (view) => (view.branches ?? [])[view.branchCursor ?? 0];
|
|
551
|
+
|
|
552
|
+
const selectedCommit = (view) => (view.commits ?? [])[view.commitCursor ?? 0];
|
|
553
|
+
|
|
528
554
|
const hiddenActions = (view) => {
|
|
529
555
|
const pane = view.pane ?? 'diff';
|
|
530
|
-
if (pane === 'files') return
|
|
556
|
+
if (pane === 'files') return FILES_HIDDEN;
|
|
557
|
+
if (pane === 'unit') return UNIT_DISABLED;
|
|
531
558
|
if (pane === 'branches') return BRANCHES_DISABLED;
|
|
559
|
+
if (pane === 'commits') return COMMITS_DISABLED;
|
|
532
560
|
if (view.item && view.item.origin === 'todo') return TODO_DISABLED;
|
|
533
561
|
return DIFF_DISABLED;
|
|
534
562
|
};
|
|
@@ -541,23 +569,30 @@ const dimFooterActions = (view) => {
|
|
|
541
569
|
if (!entry || entry.kind !== 'todos') return [];
|
|
542
570
|
return FILES_GIT_DISABLED;
|
|
543
571
|
}
|
|
544
|
-
if (pane === 'diff') return diffGitDisabled(view.item);
|
|
545
|
-
if (pane === 'branches')
|
|
546
|
-
|
|
547
|
-
const entry =
|
|
548
|
-
|
|
572
|
+
if (pane === 'diff' || pane === 'unit') return diffGitDisabled(view.item);
|
|
573
|
+
if (pane === 'branches') return branchGitDisabled(selectedBranch(view));
|
|
574
|
+
if (pane === 'commits') {
|
|
575
|
+
const entry = selectedCommit(view);
|
|
576
|
+
const staged = (view.counts?.staged ?? 0) > 0;
|
|
577
|
+
if (!entry) return commitGitDisabled({ canCommit: staged });
|
|
578
|
+
return commitGitDisabled({ ...entry, canCommit: staged });
|
|
549
579
|
}
|
|
550
580
|
return [];
|
|
551
581
|
};
|
|
552
582
|
|
|
553
583
|
const extraFooterActions = (view) => {
|
|
554
|
-
|
|
584
|
+
const pane = view.pane ?? 'diff';
|
|
585
|
+
if (pane === 'files') {
|
|
586
|
+
const scope = view.fileScope === 'file' ? 'diff' : 'file';
|
|
587
|
+
return [scope, 'commit', 'pull', 'push'];
|
|
588
|
+
}
|
|
589
|
+
if (pane === 'branches') return Object.keys(BRANCH_ACTIONS);
|
|
590
|
+
if (pane === 'commits') return Object.keys(COMMIT_ACTIONS);
|
|
555
591
|
return [];
|
|
556
592
|
};
|
|
557
593
|
|
|
558
594
|
module.exports = {
|
|
559
595
|
QUIT_PROMPT,
|
|
560
|
-
COMMIT_PROMPT,
|
|
561
596
|
PUSH_PROMPT,
|
|
562
597
|
updatePrompt,
|
|
563
598
|
dropPrompt,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ansi = require('../ansi.js');
|
|
4
|
+
const primitives = require('./primitives.js');
|
|
5
|
+
const { measureColumns, padColumns, joinColumns, paintColumns } = primitives;
|
|
6
|
+
|
|
7
|
+
const { THEME, paint, visibleWidth, truncateVisible, seq, EL } = ansi;
|
|
8
|
+
const { fill, FILE_MARK, paneResult, paintCursorList } = primitives;
|
|
9
|
+
|
|
10
|
+
const COMMIT_COL_ALIGN = {
|
|
11
|
+
sha: 'end',
|
|
12
|
+
author: 'start',
|
|
13
|
+
date: 'start',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const COMMIT_COL_FG = {
|
|
17
|
+
sha: THEME.shaFg,
|
|
18
|
+
author: THEME.mutedFg,
|
|
19
|
+
date: THEME.mutedFg,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const COMMIT_EDGE = 2;
|
|
23
|
+
const COMMIT_TAIL = 1;
|
|
24
|
+
const commitRowFields = (entry) => ({
|
|
25
|
+
sha: entry.shortSha ?? '',
|
|
26
|
+
author: entry.author ?? '',
|
|
27
|
+
date: entry.date ?? '',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const paintCommitRow = (entry, width, color, selected, cols) => {
|
|
31
|
+
const inner = Math.max(1, width - COMMIT_EDGE - COMMIT_TAIL);
|
|
32
|
+
const mark = selected ? FILE_MARK : ' ';
|
|
33
|
+
const prefix = ` ${mark} `;
|
|
34
|
+
const lead = ' ';
|
|
35
|
+
const parts = padColumns(commitRowFields(entry), cols, COMMIT_COL_ALIGN);
|
|
36
|
+
const suffix = joinColumns(parts, cols);
|
|
37
|
+
const rest = Math.max(1, inner - visibleWidth(prefix + lead + suffix));
|
|
38
|
+
const subject = truncateVisible(entry.subject ?? '', rest);
|
|
39
|
+
const pad = Math.max(0, rest - visibleWidth(subject));
|
|
40
|
+
const after = `${' '.repeat(pad)}`;
|
|
41
|
+
const left = `${prefix}${lead}${subject}${after}`;
|
|
42
|
+
const fgRgb = selected ? THEME.chromeFg : THEME.mutedFg;
|
|
43
|
+
const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
|
|
44
|
+
const nameFg = selected ? THEME.buttonHotFg : THEME.chromeFg;
|
|
45
|
+
const tail = ' '.repeat(COMMIT_TAIL);
|
|
46
|
+
const edge = fill(COMMIT_EDGE, fgRgb, THEME.ctxBg, color);
|
|
47
|
+
if (!color) return `${left}${suffix}${tail}${edge}`;
|
|
48
|
+
let out = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
49
|
+
out += paint(prefix, fgRgb, bgRgb, color);
|
|
50
|
+
out += paint(lead, nameFg, bgRgb, color);
|
|
51
|
+
out += paint(subject, nameFg, bgRgb, color);
|
|
52
|
+
out += paint(after, fgRgb, bgRgb, color);
|
|
53
|
+
out += paintColumns(parts, cols, fgRgb, bgRgb, color, COMMIT_COL_FG);
|
|
54
|
+
out += paint(tail, fgRgb, bgRgb, color);
|
|
55
|
+
return out + edge;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const paintBodyCommits = (view, width, color, bodyH, headerLines) => {
|
|
59
|
+
const commits = view.commits ?? [];
|
|
60
|
+
const cols = measureColumns(commits, commitRowFields, COMMIT_COL_ALIGN);
|
|
61
|
+
const cursor = view.commitCursor ?? 0;
|
|
62
|
+
const paintRow = (entry, rowWidth, rowColor, selected) =>
|
|
63
|
+
paintCommitRow(entry, rowWidth, rowColor, selected, cols);
|
|
64
|
+
const painted = paintCursorList(
|
|
65
|
+
commits,
|
|
66
|
+
cursor,
|
|
67
|
+
width,
|
|
68
|
+
color,
|
|
69
|
+
bodyH,
|
|
70
|
+
headerLines,
|
|
71
|
+
paintRow,
|
|
72
|
+
null,
|
|
73
|
+
view.listScroll,
|
|
74
|
+
);
|
|
75
|
+
return paneResult({
|
|
76
|
+
body: painted.body,
|
|
77
|
+
fileHits: painted.fileHits,
|
|
78
|
+
todoOwners: [],
|
|
79
|
+
cursor: painted.cursor,
|
|
80
|
+
splitBody: false,
|
|
81
|
+
listScroll: painted.offset,
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
module.exports = { paintBodyCommits };
|
package/lib/render/diff.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const ansi = require('../ansi.js');
|
|
4
|
-
const { displayLines } = require('../diff.js');
|
|
4
|
+
const { displayLines } = require('../diff/diff.js');
|
|
5
5
|
const highlight = require('../highlight.js');
|
|
6
6
|
const { detectLang } = require('../detect.js');
|
|
7
7
|
const files = require('../files.js');
|
|
@@ -81,8 +81,6 @@ const SOFT_WRAP = ['warn', 'note'];
|
|
|
81
81
|
const DEL_CHAR_BG = ansi.bg(THEME.delCharBg);
|
|
82
82
|
const ADD_CHAR_BG = ansi.bg(THEME.addCharBg);
|
|
83
83
|
|
|
84
|
-
const lineStyles = (origin) => (origin === 'staged' ? STAGED_STYLES : STYLES);
|
|
85
|
-
|
|
86
84
|
const paintSpans = (pieces, lineStyle, budget, color) => {
|
|
87
85
|
let out = '';
|
|
88
86
|
let used = 0;
|
|
@@ -129,13 +127,13 @@ const paintAuditRule = (width, color) => {
|
|
|
129
127
|
const paintDiffLine = (line, width, color, origin, lang) => {
|
|
130
128
|
if (line.type === 'noteSep') return paintAuditRule(width, color);
|
|
131
129
|
if (line.type === 'note') return paintAuditNote(line, width, color);
|
|
132
|
-
const styles =
|
|
130
|
+
const styles = origin === 'staged' ? STAGED_STYLES : STYLES;
|
|
133
131
|
const style = styles[line.type] ?? styles.ctx;
|
|
134
132
|
const diffSpans = line.spans ?? [{ text: line.text, changed: false }];
|
|
135
133
|
const tokens = tokenize(lang, line.text ?? '');
|
|
136
134
|
const pieces = overlayTokens(tokens, diffSpans);
|
|
137
135
|
const gutterW = Math.min(GUTTER_W, Math.max(0, width));
|
|
138
|
-
const label = `${style.mark} `.slice(0, gutterW);
|
|
136
|
+
const label = `${line.mark ?? style.mark} `.slice(0, gutterW);
|
|
139
137
|
const gutter = paint(label, CODE_FG.punct, style.baseBg, color);
|
|
140
138
|
const budget = Math.max(0, width - gutterW);
|
|
141
139
|
const rest = paintSpans(pieces, style, budget, color);
|
|
@@ -190,11 +188,12 @@ const wrapSoftLine = (line, width) => {
|
|
|
190
188
|
const budget = Math.max(1, width - gutterW);
|
|
191
189
|
const rows = wrapPlain(line.text ?? '', budget);
|
|
192
190
|
const changed = line.wrap === true;
|
|
193
|
-
const
|
|
191
|
+
const { length } = rows;
|
|
192
|
+
const pieces = new Array(length);
|
|
194
193
|
let used = 0;
|
|
195
|
-
for (let i = 0; i <
|
|
194
|
+
for (let i = 0; i < length; i++) {
|
|
196
195
|
const text = rows[i];
|
|
197
|
-
const last = i ===
|
|
196
|
+
const last = i === length - 1;
|
|
198
197
|
const spans = [{ text, changed }];
|
|
199
198
|
const next = { ...line, text, spans, editLast: last };
|
|
200
199
|
if (line.editStart !== undefined) {
|
|
@@ -203,7 +202,7 @@ const wrapSoftLine = (line, width) => {
|
|
|
203
202
|
else next.editEnd = next.editStart + text.length;
|
|
204
203
|
}
|
|
205
204
|
used += text.length;
|
|
206
|
-
pieces
|
|
205
|
+
pieces[i] = next;
|
|
207
206
|
}
|
|
208
207
|
return pieces;
|
|
209
208
|
};
|
|
@@ -249,8 +248,7 @@ const expandSoftRows = (lines, width, split) => {
|
|
|
249
248
|
const onCodePiece = (line, cursor) => {
|
|
250
249
|
if (!line || line.editStart === undefined) return false;
|
|
251
250
|
if (cursor < line.editStart) return false;
|
|
252
|
-
|
|
253
|
-
return cursor < line.editEnd;
|
|
251
|
+
return cursor <= line.editEnd;
|
|
254
252
|
};
|
|
255
253
|
|
|
256
254
|
const codeCursorInRows = (rows, split, width, cursor) => {
|
|
@@ -283,14 +281,15 @@ const paintBodyDiff = (view, width, color) => {
|
|
|
283
281
|
const splitBody = layout === 'side' && !item.file.isBinary;
|
|
284
282
|
const raw = dropLeadingEmpty(bodyLines(item, layout, overlay), splitBody);
|
|
285
283
|
const lines = expandSoftRows(raw, width, splitBody);
|
|
286
|
-
const
|
|
284
|
+
const { length } = lines;
|
|
285
|
+
const body = new Array(length);
|
|
287
286
|
if (splitBody) {
|
|
288
|
-
for (
|
|
289
|
-
body
|
|
287
|
+
for (let i = 0; i < length; i++) {
|
|
288
|
+
body[i] = paintSplitRow(lines[i], width, color, item.origin, lang);
|
|
290
289
|
}
|
|
291
290
|
} else {
|
|
292
|
-
for (
|
|
293
|
-
body
|
|
291
|
+
for (let i = 0; i < length; i++) {
|
|
292
|
+
body[i] = paintDiffLine(lines[i], width, color, item.origin, lang);
|
|
294
293
|
}
|
|
295
294
|
}
|
|
296
295
|
const edit = overlay ? overlay.cursor : null;
|
|
@@ -304,4 +303,5 @@ module.exports = {
|
|
|
304
303
|
paintDiffLine,
|
|
305
304
|
codeInnerWidth,
|
|
306
305
|
paintBodyDiff,
|
|
306
|
+
codeCursorInRows,
|
|
307
307
|
};
|
package/lib/render/files.js
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
const ansi = require('../ansi.js');
|
|
4
4
|
const files = require('../files.js');
|
|
5
5
|
const primitives = require('./primitives.js');
|
|
6
|
+
const { measureColumns, padColumns } = primitives;
|
|
6
7
|
|
|
7
8
|
const { isTodosEntry } = files;
|
|
8
9
|
const { THEME, paint, visibleWidth, truncateVisible, seq, EL } = ansi;
|
|
9
|
-
const { fill,
|
|
10
|
+
const { fill, FILE_MARK, paneResult, paintCursorList } = primitives;
|
|
10
11
|
|
|
11
12
|
const FILE_STATUS_FG = {
|
|
12
13
|
unstaged: THEME.delLineFg,
|
|
@@ -15,23 +16,24 @@ const FILE_STATUS_FG = {
|
|
|
15
16
|
partial: THEME.warnFg,
|
|
16
17
|
};
|
|
17
18
|
|
|
18
|
-
const FILE_COL_KEYS = ['add', 'del', 'status', 'n', 'm'];
|
|
19
|
-
|
|
20
19
|
const FILE_COL_ALIGN = {
|
|
21
20
|
add: 'start',
|
|
22
21
|
del: 'start',
|
|
23
22
|
status: 'start',
|
|
24
23
|
n: 'start',
|
|
25
24
|
m: 'end',
|
|
25
|
+
date: 'start',
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
const FILE_DATE_GAP = ' ';
|
|
29
|
+
|
|
28
30
|
const signedCount = (sign, n) => `${sign}${n ?? 0}`;
|
|
29
31
|
|
|
30
32
|
const fileRowFields = (entry) => {
|
|
31
33
|
if (isTodosEntry(entry)) {
|
|
32
34
|
const n = `${entry.staged ?? 0}`;
|
|
33
35
|
const m = `${entry.remaining ?? 0}`;
|
|
34
|
-
return { add: '', del: '', status: '', n, m };
|
|
36
|
+
return { add: '', del: '', status: '', n, m, date: '' };
|
|
35
37
|
}
|
|
36
38
|
const { added, removed, staged, remaining } = entry;
|
|
37
39
|
const add = signedCount('+', added);
|
|
@@ -39,20 +41,8 @@ const fileRowFields = (entry) => {
|
|
|
39
41
|
const status = entry.status ?? '';
|
|
40
42
|
const n = `${staged ?? 0}`;
|
|
41
43
|
const m = `${remaining ?? 0}`;
|
|
42
|
-
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const fileColumns = (files) => {
|
|
46
|
-
const widths = {};
|
|
47
|
-
for (const key of FILE_COL_KEYS) widths[key] = 0;
|
|
48
|
-
for (const entry of files) {
|
|
49
|
-
const fields = fileRowFields(entry);
|
|
50
|
-
for (const key of FILE_COL_KEYS) {
|
|
51
|
-
const w = visibleWidth(fields[key]);
|
|
52
|
-
if (w > widths[key]) widths[key] = w;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return widths;
|
|
44
|
+
const date = entry.date ?? '';
|
|
45
|
+
return { add, del, status, n, m, date };
|
|
56
46
|
};
|
|
57
47
|
|
|
58
48
|
const fileRowFg = (status, selected) => {
|
|
@@ -62,23 +52,17 @@ const fileRowFg = (status, selected) => {
|
|
|
62
52
|
return THEME.mutedFg;
|
|
63
53
|
};
|
|
64
54
|
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
for (const key of FILE_COL_KEYS) {
|
|
69
|
-
const width = cols?.[key] ?? visibleWidth(fields[key]);
|
|
70
|
-
const align = FILE_COL_ALIGN[key];
|
|
71
|
-
parts[key] = padVisible(fields[key], width, align);
|
|
72
|
-
}
|
|
73
|
-
return parts;
|
|
55
|
+
const joinFileDate = (parts, cols) => {
|
|
56
|
+
if (!cols?.date) return '';
|
|
57
|
+
return `${FILE_DATE_GAP}${parts.date}`;
|
|
74
58
|
};
|
|
75
59
|
|
|
76
|
-
const
|
|
60
|
+
const joinFileMeta = (parts) => {
|
|
77
61
|
const { add, del, status, n, m } = parts;
|
|
78
62
|
return ` ${add} ${del} ${status} ${n}/${m}`;
|
|
79
63
|
};
|
|
80
64
|
|
|
81
|
-
const paintFileSuffix = (parts, fgRgb, bgRgb, color) => {
|
|
65
|
+
const paintFileSuffix = (parts, cols, fgRgb, bgRgb, color) => {
|
|
82
66
|
const { add, del, status, n, m } = parts;
|
|
83
67
|
const gap = paint(' ', fgRgb, bgRgb, color);
|
|
84
68
|
const addText = paint(add, THEME.addLineFg, bgRgb, color);
|
|
@@ -89,15 +73,20 @@ const paintFileSuffix = (parts, fgRgb, bgRgb, color) => {
|
|
|
89
73
|
const mText = paint(m, fgRgb, bgRgb, color);
|
|
90
74
|
const counts = addText + gap + delText;
|
|
91
75
|
const meta = statusText + gap + nText + slash + mText;
|
|
92
|
-
return gap + counts + gap + meta;
|
|
76
|
+
if (!cols?.date) return gap + counts + gap + meta;
|
|
77
|
+
const dateGap = paint(FILE_DATE_GAP, fgRgb, bgRgb, color);
|
|
78
|
+
const age = paint(parts.date, THEME.mutedFg, bgRgb, color);
|
|
79
|
+
return gap + counts + gap + meta + dateGap + age;
|
|
93
80
|
};
|
|
94
81
|
|
|
95
82
|
const paintTodosRow = (entry, width, color, selected, cols) => {
|
|
96
83
|
const inner = Math.max(1, width - 1);
|
|
97
84
|
const mark = selected ? FILE_MARK : ' ';
|
|
98
85
|
const prefix = ` ${mark} `;
|
|
99
|
-
const parts =
|
|
100
|
-
const
|
|
86
|
+
const parts = padColumns(fileRowFields(entry), cols, FILE_COL_ALIGN);
|
|
87
|
+
const meta = joinFileMeta(parts);
|
|
88
|
+
const dated = joinFileDate(parts, cols);
|
|
89
|
+
const suffix = `${meta}${dated}`;
|
|
101
90
|
const pathW = Math.max(1, inner - visibleWidth(prefix + suffix));
|
|
102
91
|
const clipped = truncateVisible(entry.path, pathW);
|
|
103
92
|
const pad = Math.max(0, pathW - visibleWidth(clipped));
|
|
@@ -108,7 +97,10 @@ const paintTodosRow = (entry, width, color, selected, cols) => {
|
|
|
108
97
|
if (!color) return `${left}${suffix}${edge}`;
|
|
109
98
|
let out = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
110
99
|
out += paint(left, fgRgb, bgRgb, color, true);
|
|
111
|
-
out += paint(
|
|
100
|
+
out += paint(meta, fgRgb, bgRgb, color, true);
|
|
101
|
+
if (!dated) return out + edge;
|
|
102
|
+
out += paint(FILE_DATE_GAP, fgRgb, bgRgb, color);
|
|
103
|
+
out += paint(parts.date, THEME.mutedFg, bgRgb, color);
|
|
112
104
|
return out + edge;
|
|
113
105
|
};
|
|
114
106
|
|
|
@@ -119,8 +111,8 @@ const paintFileRow = (entry, width, color, selected, cols) => {
|
|
|
119
111
|
const inner = Math.max(1, width - 1);
|
|
120
112
|
const mark = selected ? FILE_MARK : ' ';
|
|
121
113
|
const prefix = ` ${mark} `;
|
|
122
|
-
const parts =
|
|
123
|
-
const suffix =
|
|
114
|
+
const parts = padColumns(fileRowFields(entry), cols, FILE_COL_ALIGN);
|
|
115
|
+
const suffix = `${joinFileMeta(parts)}${joinFileDate(parts, cols)}`;
|
|
124
116
|
const pathW = Math.max(1, inner - visibleWidth(prefix + suffix));
|
|
125
117
|
const { path, status } = entry;
|
|
126
118
|
const clipped = truncateVisible(path, pathW);
|
|
@@ -132,13 +124,13 @@ const paintFileRow = (entry, width, color, selected, cols) => {
|
|
|
132
124
|
if (!color) return `${left}${suffix}${edge}`;
|
|
133
125
|
let out = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
134
126
|
out += paint(left, fgRgb, bgRgb, color);
|
|
135
|
-
out += paintFileSuffix(parts, fgRgb, bgRgb, color);
|
|
127
|
+
out += paintFileSuffix(parts, cols, fgRgb, bgRgb, color);
|
|
136
128
|
return out + edge;
|
|
137
129
|
};
|
|
138
130
|
|
|
139
131
|
const paintBodyFiles = (view, width, color, bodyH, headerLines) => {
|
|
140
132
|
const files = view.files ?? [];
|
|
141
|
-
const cols =
|
|
133
|
+
const cols = measureColumns(files, fileRowFields, FILE_COL_ALIGN);
|
|
142
134
|
const cursor = view.fileCursor ?? 0;
|
|
143
135
|
const paintRow = (entry, rowWidth, rowColor, selected) =>
|
|
144
136
|
paintFileRow(entry, rowWidth, rowColor, selected, cols);
|
package/lib/render/frame.js
CHANGED
|
@@ -6,8 +6,10 @@ const primitives = require('./primitives.js');
|
|
|
6
6
|
const chrome = require('./chrome.js');
|
|
7
7
|
const files = require('./files.js');
|
|
8
8
|
const branches = require('./branches.js');
|
|
9
|
+
const commits = require('./commits.js');
|
|
9
10
|
const notes = require('./notes.js');
|
|
10
11
|
const diff = require('./diff.js');
|
|
12
|
+
const unit = require('./unit.js');
|
|
11
13
|
|
|
12
14
|
const { RESET } = ansi;
|
|
13
15
|
const { LIST_PANES, paintBodyFill, paneResult } = primitives;
|
|
@@ -17,11 +19,16 @@ const { hiddenActions, dimFooterActions, extraFooterActions } = chrome;
|
|
|
17
19
|
const { paintNotePanel, paintBodyTodo } = notes;
|
|
18
20
|
const { paintBodyFiles } = files;
|
|
19
21
|
const { paintBodyBranches } = branches;
|
|
22
|
+
const { paintBodyCommits } = commits;
|
|
20
23
|
const { paintBodyDiff } = diff;
|
|
24
|
+
const { paintBodyUnit } = unit;
|
|
21
25
|
|
|
22
26
|
const bodyKind = (view) => {
|
|
23
27
|
const pane = view.pane ?? 'diff';
|
|
24
|
-
if (pane === 'files' || pane === 'branches'
|
|
28
|
+
if (pane === 'files' || pane === 'branches' || pane === 'commits') {
|
|
29
|
+
return pane;
|
|
30
|
+
}
|
|
31
|
+
if (pane === 'unit') return 'unit';
|
|
25
32
|
if (!view.item) return 'empty';
|
|
26
33
|
if (view.item.origin === 'todo') return 'todo';
|
|
27
34
|
return 'diff';
|
|
@@ -39,9 +46,11 @@ const paintBodyEmpty = () =>
|
|
|
39
46
|
const BODY_PAINT = {
|
|
40
47
|
files: paintBodyFiles,
|
|
41
48
|
branches: paintBodyBranches,
|
|
49
|
+
commits: paintBodyCommits,
|
|
42
50
|
empty: paintBodyEmpty,
|
|
43
51
|
todo: paintBodyTodo,
|
|
44
52
|
diff: paintBodyDiff,
|
|
53
|
+
unit: paintBodyUnit,
|
|
45
54
|
};
|
|
46
55
|
|
|
47
56
|
const paintFrameBody = (view, width, color, bodyH, headerLines) => {
|
|
@@ -84,24 +93,31 @@ const windowBody = (view, width, color, bodyH, headerLines, paintedBody) => {
|
|
|
84
93
|
const listPane = LIST_PANES.includes(pane);
|
|
85
94
|
const fillKind = bodyFillKind(listPane, splitBody);
|
|
86
95
|
const filler = paintBodyFill(width, color, fillKind);
|
|
87
|
-
const
|
|
88
|
-
const
|
|
96
|
+
const lead = !listPane && sourceBody.length > 0;
|
|
97
|
+
const body = new Array(sourceBody.length + (lead ? 1 : 0));
|
|
98
|
+
let bodyAt = 0;
|
|
89
99
|
let shift = 0;
|
|
90
|
-
if (
|
|
91
|
-
body
|
|
92
|
-
if (sourceOwners.length) todoOwners.push(null);
|
|
100
|
+
if (lead) {
|
|
101
|
+
body[bodyAt++] = filler;
|
|
93
102
|
shift = 1;
|
|
94
103
|
}
|
|
95
|
-
for (
|
|
96
|
-
|
|
104
|
+
for (let i = 0; i < sourceBody.length; i++) body[bodyAt++] = sourceBody[i];
|
|
105
|
+
const ownerLead = lead && sourceOwners.length;
|
|
106
|
+
const todoOwners = new Array(sourceOwners.length + (ownerLead ? 1 : 0));
|
|
107
|
+
let ownerAt = 0;
|
|
108
|
+
if (ownerLead) todoOwners[ownerAt++] = null;
|
|
109
|
+
for (let i = 0; i < sourceOwners.length; i++) {
|
|
110
|
+
todoOwners[ownerAt++] = sourceOwners[i];
|
|
111
|
+
}
|
|
97
112
|
const scrollRaw = view.scroll ?? 0;
|
|
98
113
|
const scrollMax = Math.max(0, body.length - bodyH);
|
|
99
114
|
const scroll = listPane ? 0 : Math.min(scrollRaw, scrollMax);
|
|
100
|
-
const slice = [];
|
|
101
115
|
const end = Math.min(body.length, scroll + bodyH);
|
|
102
|
-
|
|
116
|
+
const copied = Math.max(0, end - scroll);
|
|
117
|
+
const slice = new Array(bodyH);
|
|
118
|
+
for (let i = 0; i < copied; i++) slice[i] = body[scroll + i];
|
|
103
119
|
const todoHits = [];
|
|
104
|
-
for (let i = 0; i <
|
|
120
|
+
for (let i = 0; i < copied; i++) {
|
|
105
121
|
const owner = todoOwners[scroll + i];
|
|
106
122
|
if (owner === undefined || owner === null) continue;
|
|
107
123
|
todoHits.push({ y: headerLines + i + 1, cursor: owner });
|
|
@@ -111,9 +127,9 @@ const windowBody = (view, width, color, bodyH, headerLines, paintedBody) => {
|
|
|
111
127
|
shift,
|
|
112
128
|
scroll,
|
|
113
129
|
headerLines,
|
|
114
|
-
|
|
130
|
+
copied,
|
|
115
131
|
);
|
|
116
|
-
|
|
132
|
+
for (let i = copied; i < bodyH; i++) slice[i] = filler;
|
|
117
133
|
const listScroll = paintedBody.listScroll ?? 0;
|
|
118
134
|
const fileHits = paintedBody.fileHits;
|
|
119
135
|
return { slice, scroll, scrollMax, listScroll, todoHits, fileHits, cursor };
|
package/lib/render/notes.js
CHANGED
|
@@ -36,10 +36,11 @@ const wrapTodoItem = (text, inner) => {
|
|
|
36
36
|
const bodyWidth = Math.max(1, inner - lead);
|
|
37
37
|
const hang = ' '.repeat(lead);
|
|
38
38
|
const wrapped = wrapMultiline(body, bodyWidth);
|
|
39
|
-
const
|
|
40
|
-
|
|
39
|
+
const { length } = wrapped;
|
|
40
|
+
const lines = new Array(length);
|
|
41
|
+
for (let i = 0; i < length; i++) {
|
|
41
42
|
const prefix = i === 0 ? mark : hang;
|
|
42
|
-
lines
|
|
43
|
+
lines[i] = `${prefix}${wrapped[i]}`;
|
|
43
44
|
}
|
|
44
45
|
return lines;
|
|
45
46
|
};
|
package/lib/render/primitives.js
CHANGED
|
@@ -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'];
|
|
11
|
+
const LIST_PANES = ['files', 'branches', 'commits'];
|
|
12
12
|
const CHECK_TOKEN = /\[([ xX])\]/;
|
|
13
13
|
const CHECK_LEAD = /^\[[ xX]\] /;
|
|
14
14
|
|
|
@@ -180,7 +180,52 @@ const paneResult = (fields) => ({
|
|
|
180
180
|
listScroll: fields.listScroll ?? 0,
|
|
181
181
|
});
|
|
182
182
|
|
|
183
|
+
const measureColumns = (entries, rowFields, align) => {
|
|
184
|
+
const keys = Object.keys(align);
|
|
185
|
+
const widths = Object.fromEntries(keys.map((key) => [key, 0]));
|
|
186
|
+
for (const entry of entries) {
|
|
187
|
+
const fields = rowFields(entry);
|
|
188
|
+
for (const key of keys) {
|
|
189
|
+
widths[key] = Math.max(widths[key], visibleWidth(fields[key]));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return widths;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const padColumns = (fields, widths, align) => {
|
|
196
|
+
const parts = {};
|
|
197
|
+
for (const key of Object.keys(align)) {
|
|
198
|
+
const width = widths?.[key] ?? visibleWidth(fields[key]);
|
|
199
|
+
parts[key] = padVisible(fields[key], width, align[key]);
|
|
200
|
+
}
|
|
201
|
+
return parts;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const columnGap = (key) => (key === 'date' ? ' ' : ' ');
|
|
205
|
+
|
|
206
|
+
const joinColumns = (parts, widths) => {
|
|
207
|
+
let text = '';
|
|
208
|
+
for (const key of Object.keys(parts)) {
|
|
209
|
+
if (widths[key]) text += columnGap(key) + parts[key];
|
|
210
|
+
}
|
|
211
|
+
return text;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const paintColumns = (parts, widths, fgRgb, bgRgb, color, tones) => {
|
|
215
|
+
let text = '';
|
|
216
|
+
for (const key of Object.keys(parts)) {
|
|
217
|
+
if (!widths[key]) continue;
|
|
218
|
+
text += paint(columnGap(key), fgRgb, bgRgb, color);
|
|
219
|
+
text += paint(parts[key], tones[key] ?? fgRgb, bgRgb, color);
|
|
220
|
+
}
|
|
221
|
+
return text;
|
|
222
|
+
};
|
|
223
|
+
|
|
183
224
|
module.exports = {
|
|
225
|
+
measureColumns,
|
|
226
|
+
padColumns,
|
|
227
|
+
joinColumns,
|
|
228
|
+
paintColumns,
|
|
184
229
|
GUTTER_W,
|
|
185
230
|
FILE_MARK,
|
|
186
231
|
LIST_PANES,
|