reslop 0.1.9 → 0.2.1
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 +3 -1
- package/bin/reslop.js +2 -2
- package/lib/ansi.js +148 -5
- package/lib/api.js +4 -2
- package/lib/capabilities.js +13 -14
- package/lib/cli.js +38 -49
- package/lib/clipboard.js +1 -1
- package/lib/{deps/changes.js → deps-diff.js} +309 -19
- package/lib/deps.js +498 -29
- 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 +5 -8
- package/lib/{git/branches.js → git-branches.js} +191 -11
- package/lib/{git/dependencies.js → git-deps.js} +111 -38
- package/lib/git-reword-todo.js +20 -0
- package/lib/git.js +606 -68
- 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 +14 -8
- package/lib/manifest.js +330 -0
- package/lib/remote.js +374 -0
- package/lib/render/branches.js +18 -100
- package/lib/render/chrome.js +71 -14
- package/lib/render/commits.js +69 -60
- package/lib/render/diff.js +20 -20
- package/lib/render/files.js +6 -34
- package/lib/render/frame.js +36 -13
- package/lib/render/notes.js +26 -8
- package/lib/render/primitives.js +99 -11
- package/lib/{render.js → render/render.js} +8 -8
- package/lib/render/unit.js +41 -0
- package/lib/review-merge.js +270 -0
- package/lib/review.js +782 -14
- package/lib/select.js +5 -10
- package/lib/session/accessors.js +9 -1
- package/lib/session/actions.js +61 -18
- package/lib/session/branches.js +116 -128
- package/lib/session/change-actions.js +202 -123
- package/lib/session/commit.js +201 -115
- package/lib/session/compose.js +181 -15
- package/lib/session/dispatch.js +110 -60
- package/lib/session/files-pane.js +156 -148
- package/lib/session/items.js +22 -30
- package/lib/session/lifecycle.js +151 -54
- package/lib/session/load.js +19 -33
- package/lib/session/navigation.js +12 -1
- package/lib/session/ops.js +28 -41
- package/lib/session/pointer.js +57 -57
- package/lib/session/review.js +23 -37
- package/lib/session/terminal.js +6 -12
- package/lib/session/todos.js +37 -13
- package/lib/session/unit.js +208 -0
- package/lib/session/update.js +33 -46
- package/lib/session/view.js +78 -9
- package/lib/session/watch.js +128 -24
- package/lib/session.js +22 -246
- 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/items.js +0 -134
- package/lib/deps/manifest.js +0 -152
- 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 -176
- 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
|
@@ -9,7 +9,7 @@ const { itemPath, TODO_FILE, isTodosEntry, fileTotals } = files;
|
|
|
9
9
|
const { ACTION_IDS, BRANCH_ACTIONS, COMMIT_ACTIONS, actionLetter } = keys;
|
|
10
10
|
const { buttonWord } = keys;
|
|
11
11
|
const { FILES_HIDDEN, FILES_GIT_DISABLED, BRANCHES_DISABLED } = keys;
|
|
12
|
-
const { COMMITS_DISABLED, TODO_DISABLED, DIFF_DISABLED } = keys;
|
|
12
|
+
const { COMMITS_DISABLED, TODO_DISABLED, DIFF_DISABLED, UNIT_DISABLED } = keys;
|
|
13
13
|
const { diffGitDisabled, branchGitDisabled, commitGitDisabled } = keys;
|
|
14
14
|
const { CONFIRM, confirmPrefix, promptFromChoices } = keys;
|
|
15
15
|
const { THEME, paint, visibleWidth } = ansi;
|
|
@@ -39,6 +39,7 @@ const BUSY_PROGRESS = [
|
|
|
39
39
|
'rebasing',
|
|
40
40
|
'dropping',
|
|
41
41
|
'committing',
|
|
42
|
+
'rewording',
|
|
42
43
|
'force pushing',
|
|
43
44
|
];
|
|
44
45
|
|
|
@@ -62,11 +63,16 @@ const INFO_STATUS = [
|
|
|
62
63
|
'unified',
|
|
63
64
|
'mixed',
|
|
64
65
|
'side-by-side',
|
|
66
|
+
'dark',
|
|
67
|
+
'light',
|
|
65
68
|
'saved',
|
|
66
69
|
'reloaded',
|
|
67
70
|
'not a diff block',
|
|
71
|
+
'file scope',
|
|
72
|
+
'diff mode',
|
|
68
73
|
'committed',
|
|
69
74
|
'amended',
|
|
75
|
+
'reworded',
|
|
70
76
|
'fixup',
|
|
71
77
|
'updated',
|
|
72
78
|
'updating reslop',
|
|
@@ -89,6 +95,7 @@ const INFO_STATUS = [
|
|
|
89
95
|
'checked out',
|
|
90
96
|
'created',
|
|
91
97
|
'empty branch name',
|
|
98
|
+
'empty commit message',
|
|
92
99
|
];
|
|
93
100
|
const INFO_PREFIX = ['checked out ', 'created ', 'rebased onto ', 'dropped '];
|
|
94
101
|
|
|
@@ -113,8 +120,6 @@ const originLabel = (origin, view) => {
|
|
|
113
120
|
return origin ?? 'unknown';
|
|
114
121
|
};
|
|
115
122
|
|
|
116
|
-
const headerRoleFg = (role) => HEADER_ROLE_FG[role] ?? THEME.headerChromeFg;
|
|
117
|
-
|
|
118
123
|
const pathSegments = (rel) => {
|
|
119
124
|
const segs = [];
|
|
120
125
|
if (!rel) return segs;
|
|
@@ -162,7 +167,22 @@ const headerTarget = (view) => {
|
|
|
162
167
|
return { path: entry.name, origin, n, total };
|
|
163
168
|
}
|
|
164
169
|
if (pane === 'commits') {
|
|
170
|
+
const compose = view.compose;
|
|
165
171
|
const commits = view.commits ?? [];
|
|
172
|
+
if (compose && compose.kind === 'commit') {
|
|
173
|
+
const path = `${compose.text || ''}`.replaceAll('\n', ' ');
|
|
174
|
+
if (compose.commitKind === 'amend' || compose.commitKind === 'reword') {
|
|
175
|
+
const cursor = view.commitCursor ?? 0;
|
|
176
|
+
const entry = commits[cursor];
|
|
177
|
+
const total = commits.length;
|
|
178
|
+
const n = total ? cursor + 1 : 0;
|
|
179
|
+
if (!entry) return { path, origin: 'commit', n, total };
|
|
180
|
+
const origin = entry.head ? 'HEAD' : 'commit';
|
|
181
|
+
return { path, origin, n, total };
|
|
182
|
+
}
|
|
183
|
+
const total = commits.length + 1;
|
|
184
|
+
return { path, origin: 'new', n: 1, total };
|
|
185
|
+
}
|
|
166
186
|
const cursor = view.commitCursor ?? 0;
|
|
167
187
|
const entry = commits[cursor];
|
|
168
188
|
const total = commits.length;
|
|
@@ -171,6 +191,14 @@ const headerTarget = (view) => {
|
|
|
171
191
|
const origin = entry.head ? 'HEAD' : 'commit';
|
|
172
192
|
return { path: entry.subject ?? '', origin, n, total };
|
|
173
193
|
}
|
|
194
|
+
if (pane === 'unit') {
|
|
195
|
+
const path = view.reviewPath ?? '';
|
|
196
|
+
const lines = view.unitLines ?? [];
|
|
197
|
+
const top = Math.max(0, (view.scroll ?? 0) - 1);
|
|
198
|
+
const n = lines.length ? Math.min(lines.length, top + 1) : 0;
|
|
199
|
+
const origin = view.item ? originLabel(view.item.origin, view) : 'unit';
|
|
200
|
+
return { path, origin, n, total: lines.length };
|
|
201
|
+
}
|
|
174
202
|
if (!view.item) return { path: '', origin: '', n: 0, total: view.total ?? 0 };
|
|
175
203
|
if (view.item.origin === 'todo') {
|
|
176
204
|
return { path: TODO_FILE, origin: 'todo', n: 1, total: 1 };
|
|
@@ -183,10 +211,29 @@ const headerTarget = (view) => {
|
|
|
183
211
|
};
|
|
184
212
|
};
|
|
185
213
|
|
|
214
|
+
const headerScreen = (view) => {
|
|
215
|
+
const pane = view.pane ?? 'diff';
|
|
216
|
+
if (pane === 'branches') return 'branches';
|
|
217
|
+
if (pane === 'commits') return 'commits';
|
|
218
|
+
if (pane === 'files') {
|
|
219
|
+
const files = view.files ?? [];
|
|
220
|
+
const entry = files[view.fileCursor ?? 0];
|
|
221
|
+
if (isTodosEntry(entry)) return TODO_FILE;
|
|
222
|
+
return '';
|
|
223
|
+
}
|
|
224
|
+
if (view.item && view.item.origin === 'todo') return TODO_FILE;
|
|
225
|
+
return '';
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const headerLabel = (view, target) => headerScreen(view) || target.path;
|
|
229
|
+
|
|
230
|
+
const headerSep = (view) => (headerScreen(view) ? ': ' : '/');
|
|
231
|
+
|
|
186
232
|
const headerPrefix = (view) => {
|
|
187
233
|
const repo = view.repoName || '';
|
|
188
|
-
const
|
|
189
|
-
|
|
234
|
+
const target = headerTarget(view);
|
|
235
|
+
const label = headerLabel(view, target);
|
|
236
|
+
if (repo && label) return ` ${LOGO} reslop: ${repo}${headerSep(view)}`;
|
|
190
237
|
if (repo) return ` ${LOGO} reslop: ${repo} `;
|
|
191
238
|
return ` ${LOGO} reslop: `;
|
|
192
239
|
};
|
|
@@ -197,13 +244,14 @@ const headerSuffix = ({ origin, n, total }) =>
|
|
|
197
244
|
const headerText = (view) => {
|
|
198
245
|
const prefix = headerPrefix(view);
|
|
199
246
|
const target = headerTarget(view);
|
|
200
|
-
|
|
247
|
+
const label = headerLabel(view, target);
|
|
248
|
+
return `${prefix}${label}${headerSuffix(target)}`;
|
|
201
249
|
};
|
|
202
250
|
|
|
203
251
|
const headerPieces = (view, width) => {
|
|
204
252
|
const repo = view.repoName || '';
|
|
205
253
|
const target = headerTarget(view);
|
|
206
|
-
const path = target
|
|
254
|
+
const path = headerLabel(view, target);
|
|
207
255
|
const suffix = headerSuffix(target);
|
|
208
256
|
const right = ' ';
|
|
209
257
|
const chrome = ` ${LOGO} reslop: `;
|
|
@@ -216,7 +264,9 @@ const headerPieces = (view, width) => {
|
|
|
216
264
|
if (repo) {
|
|
217
265
|
pieces.push({ text: repo, role: 'repo' });
|
|
218
266
|
if (path) {
|
|
219
|
-
|
|
267
|
+
const sep = headerSep(view);
|
|
268
|
+
const role = sep === '/' ? 'slash' : 'chrome';
|
|
269
|
+
pieces.push({ text: sep, role });
|
|
220
270
|
} else {
|
|
221
271
|
pieces.push({ text: ' ', role: 'chrome' });
|
|
222
272
|
}
|
|
@@ -243,7 +293,8 @@ const paintHeader = (view, width, color) => {
|
|
|
243
293
|
if (!piece.text) {
|
|
244
294
|
continue;
|
|
245
295
|
}
|
|
246
|
-
|
|
296
|
+
const roleFg = HEADER_ROLE_FG[piece.role] ?? THEME.headerChromeFg;
|
|
297
|
+
out += `${fg(roleFg)}${piece.text}`;
|
|
247
298
|
}
|
|
248
299
|
return `${out}${RESET}`;
|
|
249
300
|
};
|
|
@@ -428,12 +479,14 @@ const statusLayout = (view, status, width) => {
|
|
|
428
479
|
};
|
|
429
480
|
|
|
430
481
|
const choiceHits = (choices, prefix, y) => {
|
|
431
|
-
const
|
|
482
|
+
const { length } = choices;
|
|
483
|
+
const hits = new Array(length);
|
|
432
484
|
let x = visibleWidth(prefix);
|
|
433
|
-
for (
|
|
485
|
+
for (let i = 0; i < length; i++) {
|
|
486
|
+
const choice = choices[i];
|
|
434
487
|
const piece = `${choice.lead}${choice.label}`;
|
|
435
488
|
const w = visibleWidth(piece);
|
|
436
|
-
hits
|
|
489
|
+
hits[i] = { id: choice.letter, y, x0: x, x1: x + w };
|
|
437
490
|
x += w;
|
|
438
491
|
}
|
|
439
492
|
return hits;
|
|
@@ -543,6 +596,7 @@ const selectedCommit = (view) => (view.commits ?? [])[view.commitCursor ?? 0];
|
|
|
543
596
|
const hiddenActions = (view) => {
|
|
544
597
|
const pane = view.pane ?? 'diff';
|
|
545
598
|
if (pane === 'files') return FILES_HIDDEN;
|
|
599
|
+
if (pane === 'unit') return UNIT_DISABLED;
|
|
546
600
|
if (pane === 'branches') return BRANCHES_DISABLED;
|
|
547
601
|
if (pane === 'commits') return COMMITS_DISABLED;
|
|
548
602
|
if (view.item && view.item.origin === 'todo') return TODO_DISABLED;
|
|
@@ -557,7 +611,7 @@ const dimFooterActions = (view) => {
|
|
|
557
611
|
if (!entry || entry.kind !== 'todos') return [];
|
|
558
612
|
return FILES_GIT_DISABLED;
|
|
559
613
|
}
|
|
560
|
-
if (pane === 'diff') return diffGitDisabled(view.item);
|
|
614
|
+
if (pane === 'diff' || pane === 'unit') return diffGitDisabled(view.item);
|
|
561
615
|
if (pane === 'branches') return branchGitDisabled(selectedBranch(view));
|
|
562
616
|
if (pane === 'commits') {
|
|
563
617
|
const entry = selectedCommit(view);
|
|
@@ -570,7 +624,10 @@ const dimFooterActions = (view) => {
|
|
|
570
624
|
|
|
571
625
|
const extraFooterActions = (view) => {
|
|
572
626
|
const pane = view.pane ?? 'diff';
|
|
573
|
-
if (pane === 'files')
|
|
627
|
+
if (pane === 'files') {
|
|
628
|
+
const scope = view.fileScope === 'file' ? 'diff' : 'file';
|
|
629
|
+
return [scope, 'commit', 'pull', 'push'];
|
|
630
|
+
}
|
|
574
631
|
if (pane === 'branches') return Object.keys(BRANCH_ACTIONS);
|
|
575
632
|
if (pane === 'commits') return Object.keys(COMMIT_ACTIONS);
|
|
576
633
|
return [];
|
package/lib/render/commits.js
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const ansi = require('../ansi.js');
|
|
4
4
|
const primitives = require('./primitives.js');
|
|
5
|
+
const { measureColumns, padColumns, joinColumns, paintColumns } = primitives;
|
|
5
6
|
|
|
6
7
|
const { THEME, paint, visibleWidth, truncateVisible, seq, EL } = ansi;
|
|
7
|
-
const { fill,
|
|
8
|
-
|
|
9
|
-
const COMMIT_COL_KEYS = ['sha', 'author', 'date'];
|
|
8
|
+
const { fill, FILE_MARK, paneResult, paintCursorList } = primitives;
|
|
10
9
|
|
|
11
10
|
const COMMIT_COL_ALIGN = {
|
|
12
11
|
sha: 'end',
|
|
@@ -22,71 +21,19 @@ const COMMIT_COL_FG = {
|
|
|
22
21
|
|
|
23
22
|
const COMMIT_EDGE = 2;
|
|
24
23
|
const COMMIT_TAIL = 1;
|
|
25
|
-
const COMMIT_GAP = ' ';
|
|
26
|
-
const COMMIT_DATE_GAP = ' ';
|
|
27
|
-
|
|
28
|
-
const colGap = (key) => (key === 'date' ? COMMIT_DATE_GAP : COMMIT_GAP);
|
|
29
|
-
|
|
30
24
|
const commitRowFields = (entry) => ({
|
|
31
25
|
sha: entry.shortSha ?? '',
|
|
32
26
|
author: entry.author ?? '',
|
|
33
27
|
date: entry.date ?? '',
|
|
34
28
|
});
|
|
35
29
|
|
|
36
|
-
const commitColumns = (commits) => {
|
|
37
|
-
const widths = {};
|
|
38
|
-
for (const key of COMMIT_COL_KEYS) widths[key] = 0;
|
|
39
|
-
for (const entry of commits) {
|
|
40
|
-
const fields = commitRowFields(entry);
|
|
41
|
-
for (const key of COMMIT_COL_KEYS) {
|
|
42
|
-
const w = visibleWidth(fields[key]);
|
|
43
|
-
if (w > widths[key]) widths[key] = w;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return widths;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const commitRowParts = (entry, cols) => {
|
|
50
|
-
const fields = commitRowFields(entry);
|
|
51
|
-
const parts = {};
|
|
52
|
-
for (const key of COMMIT_COL_KEYS) {
|
|
53
|
-
const width = cols?.[key] ?? visibleWidth(fields[key]);
|
|
54
|
-
parts[key] = padVisible(fields[key], width, COMMIT_COL_ALIGN[key]);
|
|
55
|
-
}
|
|
56
|
-
return parts;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const joinCommitSuffix = (parts, cols) => {
|
|
60
|
-
const bits = [];
|
|
61
|
-
for (const key of COMMIT_COL_KEYS) {
|
|
62
|
-
if (!cols[key]) continue;
|
|
63
|
-
bits.push(colGap(key) + parts[key]);
|
|
64
|
-
}
|
|
65
|
-
if (!bits.length) return '';
|
|
66
|
-
return bits.join('');
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const commitColFg = (key) => COMMIT_COL_FG[key];
|
|
70
|
-
|
|
71
|
-
const paintCommitSuffix = (parts, cols, fgRgb, bgRgb, color) => {
|
|
72
|
-
const bits = [];
|
|
73
|
-
for (const key of COMMIT_COL_KEYS) {
|
|
74
|
-
if (!cols[key]) continue;
|
|
75
|
-
const tone = commitColFg(key) ?? fgRgb;
|
|
76
|
-
const gap = paint(colGap(key), fgRgb, bgRgb, color);
|
|
77
|
-
bits.push(gap + paint(parts[key], tone, bgRgb, color));
|
|
78
|
-
}
|
|
79
|
-
if (!bits.length) return '';
|
|
80
|
-
return bits.join('');
|
|
81
|
-
};
|
|
82
|
-
|
|
83
30
|
const paintCommitRow = (entry, width, color, selected, cols) => {
|
|
84
31
|
const inner = Math.max(1, width - COMMIT_EDGE - COMMIT_TAIL);
|
|
85
32
|
const mark = selected ? FILE_MARK : ' ';
|
|
86
33
|
const prefix = ` ${mark} `;
|
|
87
34
|
const lead = ' ';
|
|
88
|
-
const parts =
|
|
89
|
-
const suffix =
|
|
35
|
+
const parts = padColumns(commitRowFields(entry), cols, COMMIT_COL_ALIGN);
|
|
36
|
+
const suffix = joinColumns(parts, cols);
|
|
90
37
|
const rest = Math.max(1, inner - visibleWidth(prefix + lead + suffix));
|
|
91
38
|
const subject = truncateVisible(entry.subject ?? '', rest);
|
|
92
39
|
const pad = Math.max(0, rest - visibleWidth(subject));
|
|
@@ -103,15 +50,77 @@ const paintCommitRow = (entry, width, color, selected, cols) => {
|
|
|
103
50
|
out += paint(lead, nameFg, bgRgb, color);
|
|
104
51
|
out += paint(subject, nameFg, bgRgb, color);
|
|
105
52
|
out += paint(after, fgRgb, bgRgb, color);
|
|
106
|
-
out +=
|
|
53
|
+
out += paintColumns(parts, cols, fgRgb, bgRgb, color, COMMIT_COL_FG);
|
|
107
54
|
out += paint(tail, fgRgb, bgRgb, color);
|
|
108
55
|
return out + edge;
|
|
109
56
|
};
|
|
110
57
|
|
|
58
|
+
const paintCommitEdit = (compose, width, color, entry, cols) => {
|
|
59
|
+
const text = `${compose.text ?? ''}`.replaceAll('\n', ' ');
|
|
60
|
+
if (entry && cols) {
|
|
61
|
+
const row = paintCommitRow(
|
|
62
|
+
{ ...entry, subject: text },
|
|
63
|
+
width,
|
|
64
|
+
color,
|
|
65
|
+
true,
|
|
66
|
+
cols,
|
|
67
|
+
);
|
|
68
|
+
const inner = Math.max(1, width - COMMIT_EDGE - COMMIT_TAIL);
|
|
69
|
+
const prefix = ` ${FILE_MARK} `;
|
|
70
|
+
const lead = ' ';
|
|
71
|
+
const parts = padColumns(commitRowFields(entry), cols, COMMIT_COL_ALIGN);
|
|
72
|
+
const suffix = joinColumns(parts, cols);
|
|
73
|
+
const rest = Math.max(1, inner - visibleWidth(prefix + lead + suffix));
|
|
74
|
+
const clipped = truncateVisible(text, rest);
|
|
75
|
+
const prefixW = visibleWidth(prefix + lead);
|
|
76
|
+
const col = Math.min(compose.cursor ?? 0, visibleWidth(clipped));
|
|
77
|
+
return {
|
|
78
|
+
row,
|
|
79
|
+
cursor: { x: prefixW + col + 1 },
|
|
80
|
+
at: 'replace',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const inner = Math.max(1, width - COMMIT_EDGE);
|
|
84
|
+
const prefix = ` ${FILE_MARK} `;
|
|
85
|
+
const lead = ' ';
|
|
86
|
+
const prefixW = visibleWidth(prefix + lead);
|
|
87
|
+
const rest = Math.max(1, inner - prefixW);
|
|
88
|
+
const clipped = truncateVisible(text, rest);
|
|
89
|
+
const pad = Math.max(0, rest - visibleWidth(clipped));
|
|
90
|
+
const gap = ' '.repeat(pad);
|
|
91
|
+
const fgRgb = THEME.chromeFg;
|
|
92
|
+
const bgRgb = THEME.buttonBg;
|
|
93
|
+
const nameFg = THEME.buttonHotFg;
|
|
94
|
+
const edge = fill(COMMIT_EDGE, fgRgb, THEME.ctxBg, color);
|
|
95
|
+
let row = `${prefix}${lead}${clipped}${gap}${edge}`;
|
|
96
|
+
if (color) {
|
|
97
|
+
row = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
98
|
+
row += paint(prefix, fgRgb, bgRgb, color);
|
|
99
|
+
row += paint(lead, nameFg, bgRgb, color);
|
|
100
|
+
row += paint(clipped, nameFg, bgRgb, color);
|
|
101
|
+
row += paint(gap, fgRgb, bgRgb, color);
|
|
102
|
+
row += edge;
|
|
103
|
+
}
|
|
104
|
+
const col = Math.min(compose.cursor ?? 0, visibleWidth(clipped));
|
|
105
|
+
return { row, cursor: { x: prefixW + col + 1 }, at: 'start' };
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const commitEditRow = (compose, commits, cursor, width, color, cols) => {
|
|
109
|
+
if (!compose || compose.kind !== 'commit') return null;
|
|
110
|
+
if (compose.commitKind === 'amend' || compose.commitKind === 'reword') {
|
|
111
|
+
const entry = commits[cursor];
|
|
112
|
+
if (!entry) return null;
|
|
113
|
+
return paintCommitEdit(compose, width, color, entry, cols);
|
|
114
|
+
}
|
|
115
|
+
return paintCommitEdit(compose, width, color);
|
|
116
|
+
};
|
|
117
|
+
|
|
111
118
|
const paintBodyCommits = (view, width, color, bodyH, headerLines) => {
|
|
112
119
|
const commits = view.commits ?? [];
|
|
113
|
-
const cols =
|
|
120
|
+
const cols = measureColumns(commits, commitRowFields, COMMIT_COL_ALIGN);
|
|
114
121
|
const cursor = view.commitCursor ?? 0;
|
|
122
|
+
const compose = view.compose;
|
|
123
|
+
const extra = commitEditRow(compose, commits, cursor, width, color, cols);
|
|
115
124
|
const paintRow = (entry, rowWidth, rowColor, selected) =>
|
|
116
125
|
paintCommitRow(entry, rowWidth, rowColor, selected, cols);
|
|
117
126
|
const painted = paintCursorList(
|
|
@@ -122,7 +131,7 @@ const paintBodyCommits = (view, width, color, bodyH, headerLines) => {
|
|
|
122
131
|
bodyH,
|
|
123
132
|
headerLines,
|
|
124
133
|
paintRow,
|
|
125
|
-
|
|
134
|
+
extra,
|
|
126
135
|
view.listScroll,
|
|
127
136
|
);
|
|
128
137
|
return paneResult({
|
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');
|
|
@@ -78,10 +78,8 @@ const STAGED_STYLES = {
|
|
|
78
78
|
};
|
|
79
79
|
|
|
80
80
|
const SOFT_WRAP = ['warn', 'note'];
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
const lineStyles = (origin) => (origin === 'staged' ? STAGED_STYLES : STYLES);
|
|
81
|
+
const delCharBg = () => ansi.bg(THEME.delCharBg);
|
|
82
|
+
const addCharBg = () => ansi.bg(THEME.addCharBg);
|
|
85
83
|
|
|
86
84
|
const paintSpans = (pieces, lineStyle, budget, color) => {
|
|
87
85
|
let out = '';
|
|
@@ -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;
|
|
@@ -299,9 +298,10 @@ const paintBodyDiff = (view, width, color) => {
|
|
|
299
298
|
};
|
|
300
299
|
|
|
301
300
|
module.exports = {
|
|
302
|
-
|
|
303
|
-
|
|
301
|
+
delCharBg,
|
|
302
|
+
addCharBg,
|
|
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,8 +16,6 @@ const FILE_STATUS_FG = {
|
|
|
15
16
|
partial: THEME.warnFg,
|
|
16
17
|
};
|
|
17
18
|
|
|
18
|
-
const FILE_COL_KEYS = ['add', 'del', 'status', 'n', 'm', 'date'];
|
|
19
|
-
|
|
20
19
|
const FILE_COL_ALIGN = {
|
|
21
20
|
add: 'start',
|
|
22
21
|
del: 'start',
|
|
@@ -46,19 +45,6 @@ const fileRowFields = (entry) => {
|
|
|
46
45
|
return { add, del, status, n, m, date };
|
|
47
46
|
};
|
|
48
47
|
|
|
49
|
-
const fileColumns = (files) => {
|
|
50
|
-
const widths = {};
|
|
51
|
-
for (const key of FILE_COL_KEYS) widths[key] = 0;
|
|
52
|
-
for (const entry of files) {
|
|
53
|
-
const fields = fileRowFields(entry);
|
|
54
|
-
for (const key of FILE_COL_KEYS) {
|
|
55
|
-
const w = visibleWidth(fields[key]);
|
|
56
|
-
if (w > widths[key]) widths[key] = w;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return widths;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
48
|
const fileRowFg = (status, selected) => {
|
|
63
49
|
const statusFg = FILE_STATUS_FG[status];
|
|
64
50
|
if (statusFg) return statusFg;
|
|
@@ -66,17 +52,6 @@ const fileRowFg = (status, selected) => {
|
|
|
66
52
|
return THEME.mutedFg;
|
|
67
53
|
};
|
|
68
54
|
|
|
69
|
-
const fileRowParts = (entry, cols) => {
|
|
70
|
-
const fields = fileRowFields(entry);
|
|
71
|
-
const parts = {};
|
|
72
|
-
for (const key of FILE_COL_KEYS) {
|
|
73
|
-
const width = cols?.[key] ?? visibleWidth(fields[key]);
|
|
74
|
-
const align = FILE_COL_ALIGN[key];
|
|
75
|
-
parts[key] = padVisible(fields[key], width, align);
|
|
76
|
-
}
|
|
77
|
-
return parts;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
55
|
const joinFileDate = (parts, cols) => {
|
|
81
56
|
if (!cols?.date) return '';
|
|
82
57
|
return `${FILE_DATE_GAP}${parts.date}`;
|
|
@@ -87,9 +62,6 @@ const joinFileMeta = (parts) => {
|
|
|
87
62
|
return ` ${add} ${del} ${status} ${n}/${m}`;
|
|
88
63
|
};
|
|
89
64
|
|
|
90
|
-
const joinFileSuffix = (parts, cols) =>
|
|
91
|
-
`${joinFileMeta(parts)}${joinFileDate(parts, cols)}`;
|
|
92
|
-
|
|
93
65
|
const paintFileSuffix = (parts, cols, fgRgb, bgRgb, color) => {
|
|
94
66
|
const { add, del, status, n, m } = parts;
|
|
95
67
|
const gap = paint(' ', fgRgb, bgRgb, color);
|
|
@@ -111,7 +83,7 @@ const paintTodosRow = (entry, width, color, selected, cols) => {
|
|
|
111
83
|
const inner = Math.max(1, width - 1);
|
|
112
84
|
const mark = selected ? FILE_MARK : ' ';
|
|
113
85
|
const prefix = ` ${mark} `;
|
|
114
|
-
const parts =
|
|
86
|
+
const parts = padColumns(fileRowFields(entry), cols, FILE_COL_ALIGN);
|
|
115
87
|
const meta = joinFileMeta(parts);
|
|
116
88
|
const dated = joinFileDate(parts, cols);
|
|
117
89
|
const suffix = `${meta}${dated}`;
|
|
@@ -139,8 +111,8 @@ const paintFileRow = (entry, width, color, selected, cols) => {
|
|
|
139
111
|
const inner = Math.max(1, width - 1);
|
|
140
112
|
const mark = selected ? FILE_MARK : ' ';
|
|
141
113
|
const prefix = ` ${mark} `;
|
|
142
|
-
const parts =
|
|
143
|
-
const suffix =
|
|
114
|
+
const parts = padColumns(fileRowFields(entry), cols, FILE_COL_ALIGN);
|
|
115
|
+
const suffix = `${joinFileMeta(parts)}${joinFileDate(parts, cols)}`;
|
|
144
116
|
const pathW = Math.max(1, inner - visibleWidth(prefix + suffix));
|
|
145
117
|
const { path, status } = entry;
|
|
146
118
|
const clipped = truncateVisible(path, pathW);
|
|
@@ -158,7 +130,7 @@ const paintFileRow = (entry, width, color, selected, cols) => {
|
|
|
158
130
|
|
|
159
131
|
const paintBodyFiles = (view, width, color, bodyH, headerLines) => {
|
|
160
132
|
const files = view.files ?? [];
|
|
161
|
-
const cols =
|
|
133
|
+
const cols = measureColumns(files, fileRowFields, FILE_COL_ALIGN);
|
|
162
134
|
const cursor = view.fileCursor ?? 0;
|
|
163
135
|
const paintRow = (entry, rowWidth, rowColor, selected) =>
|
|
164
136
|
paintFileRow(entry, rowWidth, rowColor, selected, cols);
|
package/lib/render/frame.js
CHANGED
|
@@ -9,6 +9,7 @@ const branches = require('./branches.js');
|
|
|
9
9
|
const commits = require('./commits.js');
|
|
10
10
|
const notes = require('./notes.js');
|
|
11
11
|
const diff = require('./diff.js');
|
|
12
|
+
const unit = require('./unit.js');
|
|
12
13
|
|
|
13
14
|
const { RESET } = ansi;
|
|
14
15
|
const { LIST_PANES, paintBodyFill, paneResult } = primitives;
|
|
@@ -20,12 +21,14 @@ const { paintBodyFiles } = files;
|
|
|
20
21
|
const { paintBodyBranches } = branches;
|
|
21
22
|
const { paintBodyCommits } = commits;
|
|
22
23
|
const { paintBodyDiff } = diff;
|
|
24
|
+
const { paintBodyUnit } = unit;
|
|
23
25
|
|
|
24
26
|
const bodyKind = (view) => {
|
|
25
27
|
const pane = view.pane ?? 'diff';
|
|
26
28
|
if (pane === 'files' || pane === 'branches' || pane === 'commits') {
|
|
27
29
|
return pane;
|
|
28
30
|
}
|
|
31
|
+
if (pane === 'unit') return 'unit';
|
|
29
32
|
if (!view.item) return 'empty';
|
|
30
33
|
if (view.item.origin === 'todo') return 'todo';
|
|
31
34
|
return 'diff';
|
|
@@ -47,6 +50,7 @@ const BODY_PAINT = {
|
|
|
47
50
|
empty: paintBodyEmpty,
|
|
48
51
|
todo: paintBodyTodo,
|
|
49
52
|
diff: paintBodyDiff,
|
|
53
|
+
unit: paintBodyUnit,
|
|
50
54
|
};
|
|
51
55
|
|
|
52
56
|
const paintFrameBody = (view, width, color, bodyH, headerLines) => {
|
|
@@ -84,41 +88,60 @@ const mapBodyCursor = (cursor, shift, scroll, headerLines, shown) => {
|
|
|
84
88
|
const windowBody = (view, width, color, bodyH, headerLines, paintedBody) => {
|
|
85
89
|
const sourceBody = paintedBody.body;
|
|
86
90
|
const sourceOwners = paintedBody.todoOwners;
|
|
91
|
+
const sourceChecks = paintedBody.todoChecks ?? [];
|
|
87
92
|
const splitBody = paintedBody.splitBody === true;
|
|
88
93
|
const pane = view.pane ?? 'diff';
|
|
89
94
|
const listPane = LIST_PANES.includes(pane);
|
|
90
95
|
const fillKind = bodyFillKind(listPane, splitBody);
|
|
91
96
|
const filler = paintBodyFill(width, color, fillKind);
|
|
92
|
-
const
|
|
93
|
-
const
|
|
97
|
+
const lead = !listPane && sourceBody.length > 0;
|
|
98
|
+
const body = new Array(sourceBody.length + (lead ? 1 : 0));
|
|
99
|
+
let bodyAt = 0;
|
|
94
100
|
let shift = 0;
|
|
95
|
-
if (
|
|
96
|
-
body
|
|
97
|
-
if (sourceOwners.length) todoOwners.push(null);
|
|
101
|
+
if (lead) {
|
|
102
|
+
body[bodyAt++] = filler;
|
|
98
103
|
shift = 1;
|
|
99
104
|
}
|
|
100
|
-
for (
|
|
101
|
-
|
|
105
|
+
for (let i = 0; i < sourceBody.length; i++) body[bodyAt++] = sourceBody[i];
|
|
106
|
+
const ownerLead = lead && sourceOwners.length;
|
|
107
|
+
const todoOwners = new Array(sourceOwners.length + (ownerLead ? 1 : 0));
|
|
108
|
+
let ownerAt = 0;
|
|
109
|
+
if (ownerLead) todoOwners[ownerAt++] = null;
|
|
110
|
+
for (let i = 0; i < sourceOwners.length; i++) {
|
|
111
|
+
todoOwners[ownerAt++] = sourceOwners[i];
|
|
112
|
+
}
|
|
113
|
+
const checkLead = lead && sourceChecks.length;
|
|
114
|
+
const todoChecks = new Array(sourceChecks.length + (checkLead ? 1 : 0));
|
|
115
|
+
let checkAt = 0;
|
|
116
|
+
if (checkLead) todoChecks[checkAt++] = false;
|
|
117
|
+
for (let i = 0; i < sourceChecks.length; i++) {
|
|
118
|
+
todoChecks[checkAt++] = sourceChecks[i];
|
|
119
|
+
}
|
|
102
120
|
const scrollRaw = view.scroll ?? 0;
|
|
103
121
|
const scrollMax = Math.max(0, body.length - bodyH);
|
|
104
122
|
const scroll = listPane ? 0 : Math.min(scrollRaw, scrollMax);
|
|
105
|
-
const slice = [];
|
|
106
123
|
const end = Math.min(body.length, scroll + bodyH);
|
|
107
|
-
|
|
124
|
+
const copied = Math.max(0, end - scroll);
|
|
125
|
+
const slice = new Array(bodyH);
|
|
126
|
+
for (let i = 0; i < copied; i++) slice[i] = body[scroll + i];
|
|
108
127
|
const todoHits = [];
|
|
109
|
-
for (let i = 0; i <
|
|
128
|
+
for (let i = 0; i < copied; i++) {
|
|
110
129
|
const owner = todoOwners[scroll + i];
|
|
111
130
|
if (owner === undefined || owner === null) continue;
|
|
112
|
-
|
|
131
|
+
const y = headerLines + i + 1;
|
|
132
|
+
if (todoChecks[scroll + i]) {
|
|
133
|
+
todoHits.push({ y, cursor: owner, x0: 1, x1: 4, check: true });
|
|
134
|
+
}
|
|
135
|
+
todoHits.push({ y, cursor: owner });
|
|
113
136
|
}
|
|
114
137
|
const cursor = mapBodyCursor(
|
|
115
138
|
paintedBody.cursor,
|
|
116
139
|
shift,
|
|
117
140
|
scroll,
|
|
118
141
|
headerLines,
|
|
119
|
-
|
|
142
|
+
copied,
|
|
120
143
|
);
|
|
121
|
-
|
|
144
|
+
for (let i = copied; i < bodyH; i++) slice[i] = filler;
|
|
122
145
|
const listScroll = paintedBody.listScroll ?? 0;
|
|
123
146
|
const fileHits = paintedBody.fileHits;
|
|
124
147
|
return { slice, scroll, scrollMax, listScroll, todoHits, fileHits, cursor };
|