reslop 0.2.1 → 0.2.4
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 +27 -1
- package/README.md +19 -4
- package/bin/reslop.js +4 -2
- package/lib/ansi.js +120 -4
- package/lib/api.js +2 -1
- package/lib/cli.js +21 -9
- package/lib/clipboard.js +6 -3
- package/lib/deps-diff.js +61 -115
- package/lib/deps.js +57 -54
- package/lib/detect.js +4 -2
- package/lib/diff/align.js +2 -1
- package/lib/diff/diff.js +6 -3
- package/lib/diff/display.js +2 -1
- package/lib/diff/inline.js +2 -1
- package/lib/diff/patch.js +2 -1
- package/lib/diff/unit.js +4 -2
- package/lib/editor.js +22 -4
- package/lib/files.js +38 -12
- package/lib/git-branches.js +150 -165
- package/lib/git-deps.js +20 -60
- package/lib/git-worktree.js +508 -0
- package/lib/git.js +23 -580
- package/lib/github.js +2 -1
- package/lib/gitlab.js +2 -1
- package/lib/highlight/bash.js +2 -1
- package/lib/highlight/css.js +2 -1
- package/lib/highlight/csv.js +2 -1
- package/lib/highlight/dot.js +2 -1
- package/lib/highlight/html.js +2 -1
- package/lib/highlight/json.js +2 -1
- package/lib/highlight/log.js +2 -1
- package/lib/highlight/txt.js +2 -1
- package/lib/highlight.js +2 -1
- package/lib/keys.js +12 -6
- package/lib/manifest.js +4 -2
- package/lib/npm-commands.js +592 -0
- package/lib/remote.js +20 -13
- package/lib/render/branches.js +25 -41
- package/lib/render/chrome.js +163 -128
- package/lib/render/commits.js +727 -65
- package/lib/render/diff.js +130 -47
- package/lib/render/files.js +4 -12
- package/lib/render/frame.js +41 -13
- package/lib/render/notes.js +4 -11
- package/lib/render/npm.js +216 -0
- package/lib/render/present.js +2 -1
- package/lib/render/primitives.js +34 -50
- package/lib/render/render.js +2 -1
- package/lib/render/unit.js +14 -11
- package/lib/report-model.js +129 -0
- package/lib/report-parse.js +89 -0
- package/lib/report-render.js +104 -0
- package/lib/report-run.js +166 -0
- package/lib/report-test.js +507 -0
- package/lib/report-tools.js +337 -0
- package/lib/review-merge.js +2 -1
- package/lib/review.js +6 -2
- package/lib/session/actions.js +77 -44
- package/lib/session/branches.js +13 -49
- package/lib/session/change-actions.js +2 -1
- package/lib/session/commit.js +113 -53
- package/lib/session/compose.js +83 -14
- package/lib/session/dispatch.js +42 -22
- package/lib/session/files-pane.js +16 -21
- package/lib/session/items.js +6 -3
- package/lib/session/lifecycle.js +12 -5
- package/lib/session/load.js +4 -2
- package/lib/session/navigation.js +2 -0
- package/lib/session/npm.js +388 -0
- package/lib/session/ops.js +15 -1
- package/lib/session/pointer.js +30 -3
- package/lib/session/review.js +2 -1
- package/lib/session/terminal.js +4 -2
- package/lib/session/update.js +2 -1
- package/lib/session/view.js +41 -4
- package/lib/session.js +44 -22
- package/lib/source.js +6 -3
- package/lib/update.js +5 -9
- package/lib/utilities.js +4 -2
- package/lib/wrap.js +3 -1
- package/package.json +6 -6
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ansi = require('../ansi.js');
|
|
4
|
+
const chrome = require('./chrome.js');
|
|
5
|
+
const primitives = require('./primitives.js');
|
|
6
|
+
const npmCommands = require('../npm-commands.js');
|
|
7
|
+
|
|
8
|
+
const { THEME, paint, visibleWidth, truncateVisible, seq, EL } = ansi;
|
|
9
|
+
const { clipAnsi, stripAnsi, RESET, foregroundOn } = ansi;
|
|
10
|
+
const { formatBusyStatus } = chrome;
|
|
11
|
+
const { TABLE_KEY, TABLE_VAL } = npmCommands;
|
|
12
|
+
const { fill, FILE_MARK, paneResult, paintCursorList } = primitives;
|
|
13
|
+
const { paintListEdit } = primitives;
|
|
14
|
+
|
|
15
|
+
const EDGE = 2;
|
|
16
|
+
const TAIL = 1;
|
|
17
|
+
const LOG_PAD_Y = 1;
|
|
18
|
+
const LOG_PAD_X = 2;
|
|
19
|
+
const NPM_NAME_MAX = 24;
|
|
20
|
+
|
|
21
|
+
const logViewRows = (bodyH) => {
|
|
22
|
+
const height = Math.max(1, bodyH);
|
|
23
|
+
if (height >= LOG_PAD_Y * 2 + 1) return height - LOG_PAD_Y * 2;
|
|
24
|
+
return height;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const rowFg = (selected) => (selected ? THEME.chromeFg : THEME.mutedFg);
|
|
28
|
+
|
|
29
|
+
const npmNameWidth = (commands) => {
|
|
30
|
+
let width = 0;
|
|
31
|
+
for (const entry of commands) {
|
|
32
|
+
width = Math.max(width, visibleWidth(`${entry.name ?? ''}`));
|
|
33
|
+
}
|
|
34
|
+
return Math.min(NPM_NAME_MAX, width);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const paintNpmRow = (entry, width, color, selected, nameCol = 0) => {
|
|
38
|
+
const inner = Math.max(1, width - EDGE - TAIL);
|
|
39
|
+
const mark = selected ? FILE_MARK : ' ';
|
|
40
|
+
const prefix = ` ${mark} `;
|
|
41
|
+
const prefixW = visibleWidth(prefix);
|
|
42
|
+
const rest = Math.max(1, inner - prefixW);
|
|
43
|
+
const detail = entry.kind === 'bin' ? 'bin' : entry.command;
|
|
44
|
+
const nameW = Math.min(Math.max(0, nameCol), rest);
|
|
45
|
+
const name = truncateVisible(entry.name, nameW);
|
|
46
|
+
const gap = rest > nameW + 2 ? ' ' : '';
|
|
47
|
+
const detailW = Math.max(0, rest - nameW - visibleWidth(gap));
|
|
48
|
+
const shown = truncateVisible(detail, detailW);
|
|
49
|
+
const mid = `${name}${' '.repeat(Math.max(0, nameW - visibleWidth(name)))}`;
|
|
50
|
+
const after = `${gap}${shown}`;
|
|
51
|
+
const pad = Math.max(0, rest - visibleWidth(mid + after));
|
|
52
|
+
const left = `${prefix}${mid}${after}${' '.repeat(pad)}`;
|
|
53
|
+
const fgRgb = rowFg(selected);
|
|
54
|
+
const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
|
|
55
|
+
const tail = ' '.repeat(TAIL);
|
|
56
|
+
const edge = fill(EDGE, fgRgb, THEME.ctxBg, color);
|
|
57
|
+
if (!color) return `${left}${tail}${edge}`;
|
|
58
|
+
let out = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
59
|
+
out += paint(prefix, fgRgb, bgRgb, color);
|
|
60
|
+
const bin = entry.kind === 'bin';
|
|
61
|
+
const nameFg = bin ? THEME.shaFg : THEME.buttonHotFg;
|
|
62
|
+
out += paint(mid, nameFg, bgRgb, color, !bin);
|
|
63
|
+
out += paint(`${after}${' '.repeat(pad)}${tail}`, fgRgb, bgRgb, color);
|
|
64
|
+
return out + edge;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const paintNpmList = (view, width, color, bodyH, headerLines) => {
|
|
68
|
+
const commands = view.npmCommands ?? [];
|
|
69
|
+
const nameW = npmNameWidth(commands);
|
|
70
|
+
const cursor = view.npmCursor ?? 0;
|
|
71
|
+
const compose = view.compose;
|
|
72
|
+
const editing = compose && compose.kind === 'npm';
|
|
73
|
+
const paintRow = (entry, rowWidth, rowColor, selected) =>
|
|
74
|
+
paintNpmRow(entry, rowWidth, rowColor, selected, nameW);
|
|
75
|
+
let extra = null;
|
|
76
|
+
if (editing) {
|
|
77
|
+
const painted = paintListEdit(compose, width, color, EDGE);
|
|
78
|
+
const at = view.npmEditName ? 'replace' : 'end';
|
|
79
|
+
extra = { row: painted.row, cursor: painted.cursor, at, index: cursor };
|
|
80
|
+
}
|
|
81
|
+
return paintCursorList(
|
|
82
|
+
commands,
|
|
83
|
+
cursor,
|
|
84
|
+
width,
|
|
85
|
+
color,
|
|
86
|
+
bodyH,
|
|
87
|
+
headerLines,
|
|
88
|
+
paintRow,
|
|
89
|
+
extra,
|
|
90
|
+
view.listScroll,
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const logStart = (view, count, rows) => {
|
|
95
|
+
const maxStart = Math.max(0, count - rows);
|
|
96
|
+
let start = maxStart;
|
|
97
|
+
if (!view.npmFollow) start = view.npmScroll ?? 0;
|
|
98
|
+
if (start > maxStart) start = maxStart;
|
|
99
|
+
if (start < 0) start = 0;
|
|
100
|
+
return start;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const isTableLine = (line) => `${line ?? ''}`.startsWith(TABLE_KEY);
|
|
104
|
+
|
|
105
|
+
const mixRgb = (from, to, part) => {
|
|
106
|
+
const out = [0, 0, 0];
|
|
107
|
+
for (let i = 0; i < 3; i++) {
|
|
108
|
+
const next = from[i] + (to[i] - from[i]) * part;
|
|
109
|
+
out[i] = Math.round(next);
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const tableTone = (index) => {
|
|
115
|
+
const lift = index % 2 === 0 ? 0.035 : 0.07;
|
|
116
|
+
const valueBg = mixRgb(THEME.ctxBg, THEME.chromeFg, lift);
|
|
117
|
+
const keyBg = mixRgb(THEME.ctxBg, THEME.chromeFg, lift + 0.025);
|
|
118
|
+
const frame = mixRgb(THEME.ctxBg, THEME.chromeFg, lift * 0.4);
|
|
119
|
+
return { valueBg, keyBg, frame };
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const tableIndex = (lines, index) => {
|
|
123
|
+
let at = index;
|
|
124
|
+
let row = 0;
|
|
125
|
+
while (at > 0 && isTableLine(lines[at - 1])) {
|
|
126
|
+
at -= 1;
|
|
127
|
+
row += 1;
|
|
128
|
+
}
|
|
129
|
+
return row;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const paintTableLine = (line, width, index) => {
|
|
133
|
+
const raw = line.slice(TABLE_KEY.length);
|
|
134
|
+
const at = raw.indexOf(TABLE_VAL);
|
|
135
|
+
const key = at < 0 ? raw : raw.slice(0, at);
|
|
136
|
+
const rest = at < 0 ? '' : raw.slice(at + TABLE_VAL.length);
|
|
137
|
+
const gap = rest.startsWith(' ') ? ' ' : '';
|
|
138
|
+
const value = rest.slice(gap.length);
|
|
139
|
+
const edge = 1;
|
|
140
|
+
const inner = Math.max(1, width - edge * 2);
|
|
141
|
+
const keyW = visibleWidth(key);
|
|
142
|
+
const gapW = visibleWidth(gap);
|
|
143
|
+
const valueW = Math.max(0, inner - keyW - gapW);
|
|
144
|
+
const shown = truncateVisible(value, valueW);
|
|
145
|
+
const pad = ' '.repeat(Math.max(0, valueW - visibleWidth(shown)));
|
|
146
|
+
const tone = tableTone(index);
|
|
147
|
+
const ink = THEME.ctxFg;
|
|
148
|
+
let out = fill(edge, ink, tone.frame, true);
|
|
149
|
+
out += paint(key, ink, tone.keyBg, true);
|
|
150
|
+
out += paint(gap, ink, tone.frame, true);
|
|
151
|
+
out += paint(`${shown}${pad}`, ink, tone.valueBg, true);
|
|
152
|
+
return `${out}${fill(edge, ink, tone.frame, true)}`;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const paintLogLine = (line, width, color, fgRgb = THEME.ctxFg) => {
|
|
156
|
+
const inner = Math.max(1, width - LOG_PAD_X * 2);
|
|
157
|
+
const source = color ? `${line ?? ''}` : stripAnsi(line);
|
|
158
|
+
const clip = (value) =>
|
|
159
|
+
color ? clipAnsi(value, inner) : truncateVisible(value, inner);
|
|
160
|
+
const body = color ? foregroundOn(clip(source), THEME.ctxBg) : clip(source);
|
|
161
|
+
const lead = ' '.repeat(LOG_PAD_X);
|
|
162
|
+
const pad = Math.max(0, width - LOG_PAD_X - visibleWidth(body));
|
|
163
|
+
const tail = ' '.repeat(pad);
|
|
164
|
+
if (!color) return `${lead}${body}${tail}`;
|
|
165
|
+
const base = seq(fgRgb, THEME.ctxBg);
|
|
166
|
+
let out = `${base}${EL}${lead}${body}`;
|
|
167
|
+
if (pad) out += `${RESET}${base}${tail}`;
|
|
168
|
+
return `${out}${RESET}`;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const paintRunBar = (width, color, frame) => {
|
|
172
|
+
const label = formatBusyStatus('running', frame);
|
|
173
|
+
return paintLogLine(label, width, color, THEME.chromeFg);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const paintNpmLog = (view, width, color, bodyH) => {
|
|
177
|
+
const text = view.npmOutput ?? '';
|
|
178
|
+
const lines = text.split('\n');
|
|
179
|
+
if (lines.length && lines.at(-1) === '') lines.pop();
|
|
180
|
+
const running = view.npmRunning === true;
|
|
181
|
+
const count = lines.length + (running ? 1 : 0);
|
|
182
|
+
const rows = logViewRows(bodyH);
|
|
183
|
+
const padY = bodyH - rows;
|
|
184
|
+
const top = padY ? 1 : 0;
|
|
185
|
+
const start = logStart(view, count, rows);
|
|
186
|
+
const blank = fill(width, THEME.ctxFg, THEME.ctxBg, color);
|
|
187
|
+
const body = [];
|
|
188
|
+
for (let i = 0; i < top; i++) body.push(blank);
|
|
189
|
+
for (let i = 0; i < rows; i++) {
|
|
190
|
+
const index = start + i;
|
|
191
|
+
if (index < lines.length) {
|
|
192
|
+
const line = lines[index];
|
|
193
|
+
if (color && isTableLine(line)) {
|
|
194
|
+
const row = tableIndex(lines, index);
|
|
195
|
+
body.push(paintTableLine(line, width, row));
|
|
196
|
+
} else {
|
|
197
|
+
body.push(paintLogLine(line, width, color));
|
|
198
|
+
}
|
|
199
|
+
} else if (running && index === lines.length) {
|
|
200
|
+
body.push(paintRunBar(width, color, view.progressFrame ?? 0));
|
|
201
|
+
} else {
|
|
202
|
+
body.push(blank);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
for (let i = 0; i < padY - top; i++) body.push(blank);
|
|
206
|
+
return paneResult({
|
|
207
|
+
body,
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const paintBodyNpm = (view, width, color, bodyH, headerLines) => {
|
|
212
|
+
if (view.npmView) return paintNpmLog(view, width, color, bodyH);
|
|
213
|
+
return paintNpmList(view, width, color, bodyH, headerLines);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
module.exports = { paintBodyNpm, logViewRows };
|
package/lib/render/present.js
CHANGED
package/lib/render/primitives.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const ansi = require('../ansi.js');
|
|
4
|
+
const actions = require('../session/actions.js');
|
|
5
|
+
const { LIST_PANES } = actions;
|
|
4
6
|
|
|
5
7
|
const { THEME, paint, visibleWidth, truncateVisible, seq, EL, RESET } = ansi;
|
|
6
8
|
|
|
@@ -8,7 +10,6 @@ const GUTTER_W = 2;
|
|
|
8
10
|
const FILE_MARK = '▶';
|
|
9
11
|
const FILE_LIST_PAD = 1;
|
|
10
12
|
const CHECK_DONE = 'xX';
|
|
11
|
-
const LIST_PANES = ['files', 'branches', 'commits'];
|
|
12
13
|
const CHECK_TOKEN = /\[([ xX])\]/;
|
|
13
14
|
const CHECK_LEAD = /^\[[ xX]\] /;
|
|
14
15
|
|
|
@@ -34,14 +35,21 @@ const splitCheck = (text) => {
|
|
|
34
35
|
return { before, mark, token, after };
|
|
35
36
|
};
|
|
36
37
|
|
|
37
|
-
const paintCheckBar = (text, width, fgRgb, bgRgb, color) => {
|
|
38
|
+
const paintCheckBar = (text, width, fgRgb, bgRgb, color, plain = false) => {
|
|
38
39
|
const clipped = truncateVisible(text, width);
|
|
39
40
|
const parts = color ? splitCheck(clipped) : null;
|
|
40
41
|
if (!parts) return paintBar(text, width, fgRgb, bgRgb, color);
|
|
41
42
|
const pad = Math.max(0, width - visibleWidth(clipped));
|
|
42
43
|
const done = CHECK_DONE.includes(parts.mark);
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
let chipFg = THEME.checkFg;
|
|
45
|
+
let chipBg = THEME.checkBg;
|
|
46
|
+
if (plain) {
|
|
47
|
+
chipFg = fgRgb;
|
|
48
|
+
chipBg = bgRgb;
|
|
49
|
+
} else if (done) {
|
|
50
|
+
chipFg = THEME.checkDoneFg;
|
|
51
|
+
chipBg = THEME.checkDoneBg;
|
|
52
|
+
}
|
|
45
53
|
const after = `${parts.after}${' '.repeat(pad)}`;
|
|
46
54
|
let out = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
47
55
|
if (parts.before) out += paint(parts.before, fgRgb, bgRgb, color);
|
|
@@ -111,6 +119,16 @@ const extraIdx = (extraRow, cursor, count) => {
|
|
|
111
119
|
return count;
|
|
112
120
|
};
|
|
113
121
|
|
|
122
|
+
const paneResult = (fields = {}) => ({
|
|
123
|
+
body: fields.body ?? [],
|
|
124
|
+
fileHits: fields.fileHits ?? [],
|
|
125
|
+
todoOwners: fields.todoOwners ?? [],
|
|
126
|
+
todoChecks: fields.todoChecks ?? [],
|
|
127
|
+
cursor: fields.cursor ?? null,
|
|
128
|
+
splitBody: fields.splitBody === true,
|
|
129
|
+
listScroll: fields.listScroll ?? 0,
|
|
130
|
+
});
|
|
131
|
+
|
|
114
132
|
const paintCursorList = (
|
|
115
133
|
items,
|
|
116
134
|
cursor,
|
|
@@ -153,41 +171,23 @@ const paintCursorList = (
|
|
|
153
171
|
fileHits.push({ y, cursor: itemIdx });
|
|
154
172
|
}
|
|
155
173
|
if (pad) body.push(blank);
|
|
156
|
-
return {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
return `[${branch}]`;
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const splitBracketName = (label) => {
|
|
166
|
-
const text = `${label ?? ''}`;
|
|
167
|
-
const open = text.startsWith('[') ? '[' : '';
|
|
168
|
-
const rest = open ? text.slice(1) : text;
|
|
169
|
-
const close = rest.endsWith(']') ? ']' : '';
|
|
170
|
-
const name = close ? rest.slice(0, -1) : rest;
|
|
171
|
-
return { open, name, close };
|
|
174
|
+
return paneResult({
|
|
175
|
+
body,
|
|
176
|
+
fileHits,
|
|
177
|
+
cursor: textCursor,
|
|
178
|
+
listScroll: start,
|
|
179
|
+
});
|
|
172
180
|
};
|
|
173
181
|
|
|
174
|
-
const
|
|
175
|
-
const { open, name, close } = splitBracketName(label);
|
|
176
|
-
return (
|
|
177
|
-
paint(open, wrapFg, bgRgb, color, true) +
|
|
178
|
-
paint(name, nameFg, bgRgb, color, nameBold) +
|
|
179
|
-
paint(close, wrapFg, bgRgb, color, true)
|
|
180
|
-
);
|
|
181
|
-
};
|
|
182
|
+
const branchLabel = (name) => `${name ?? ''}`.trim();
|
|
182
183
|
|
|
183
|
-
const paintListEdit = (compose, width, color, edgeW) => {
|
|
184
|
+
const paintListEdit = (compose, width, color, edgeW = 0) => {
|
|
184
185
|
const inner = Math.max(1, width - edgeW);
|
|
185
186
|
const prefix = ` ${FILE_MARK} `;
|
|
186
187
|
const prefixW = visibleWidth(prefix);
|
|
187
188
|
const rest = Math.max(1, inner - prefixW);
|
|
188
189
|
const text = `${compose.text ?? ''}`.replaceAll('\n', ' ');
|
|
189
|
-
const
|
|
190
|
-
const clipped = truncateVisible(label, rest);
|
|
190
|
+
const clipped = truncateVisible(text, rest);
|
|
191
191
|
const pad = Math.max(0, rest - visibleWidth(clipped));
|
|
192
192
|
const left = `${prefix}${clipped}${' '.repeat(pad)}`;
|
|
193
193
|
const fgRgb = THEME.chromeFg;
|
|
@@ -198,30 +198,14 @@ const paintListEdit = (compose, width, color, edgeW) => {
|
|
|
198
198
|
const gap = ' '.repeat(pad);
|
|
199
199
|
row = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
200
200
|
row += paint(prefix, fgRgb, bgRgb, color);
|
|
201
|
-
row +=
|
|
202
|
-
clipped,
|
|
203
|
-
THEME.buttonHotFg,
|
|
204
|
-
THEME.mutedFg,
|
|
205
|
-
bgRgb,
|
|
206
|
-
color,
|
|
207
|
-
);
|
|
201
|
+
row += paint(clipped, THEME.buttonHotFg, bgRgb, color);
|
|
208
202
|
row += paint(gap, fgRgb, bgRgb, color);
|
|
209
203
|
row += edge;
|
|
210
204
|
}
|
|
211
|
-
const col = Math.min(
|
|
205
|
+
const col = Math.min(compose.cursor ?? 0, visibleWidth(clipped));
|
|
212
206
|
return { row, cursor: { x: prefixW + col + 1 } };
|
|
213
207
|
};
|
|
214
208
|
|
|
215
|
-
const paneResult = (fields) => ({
|
|
216
|
-
body: fields.body,
|
|
217
|
-
fileHits: fields.fileHits,
|
|
218
|
-
todoOwners: fields.todoOwners,
|
|
219
|
-
todoChecks: fields.todoChecks ?? [],
|
|
220
|
-
cursor: fields.cursor ?? null,
|
|
221
|
-
splitBody: fields.splitBody === true,
|
|
222
|
-
listScroll: fields.listScroll ?? 0,
|
|
223
|
-
});
|
|
224
|
-
|
|
225
209
|
const measureColumns = (entries, rowFields, align) => {
|
|
226
210
|
const keys = Object.keys(align);
|
|
227
211
|
const widths = Object.fromEntries(keys.map((key) => [key, 0]));
|
|
@@ -280,9 +264,9 @@ module.exports = {
|
|
|
280
264
|
paintSplitRule,
|
|
281
265
|
paintBodyFill,
|
|
282
266
|
windowRows,
|
|
267
|
+
listWindowStart,
|
|
283
268
|
paintCursorList,
|
|
284
269
|
paintListEdit,
|
|
285
270
|
branchLabel,
|
|
286
|
-
paintBracketName,
|
|
287
271
|
paneResult,
|
|
288
272
|
};
|
package/lib/render/render.js
CHANGED
|
@@ -4,7 +4,8 @@ const chrome = require('./chrome.js');
|
|
|
4
4
|
const notes = require('./notes.js');
|
|
5
5
|
const diff = require('./diff.js');
|
|
6
6
|
const present = require('./present.js');
|
|
7
|
-
const
|
|
7
|
+
const frame = require('./frame.js');
|
|
8
|
+
const { renderFrame } = frame;
|
|
8
9
|
|
|
9
10
|
const { QUIT_PROMPT, PUSH_PROMPT } = chrome;
|
|
10
11
|
const { updatePrompt, dropPrompt } = chrome;
|
package/lib/render/unit.js
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const detect = require('../detect.js');
|
|
4
|
+
const { detectLang } = detect;
|
|
4
5
|
const primitives = require('./primitives.js');
|
|
5
6
|
const diff = require('./diff.js');
|
|
6
7
|
|
|
7
8
|
const { paneResult } = primitives;
|
|
8
|
-
const { paintDiffLine, codeCursorInRows } = diff;
|
|
9
|
-
const
|
|
9
|
+
const { paintDiffLine, codeCursorInRows, expandSoftRows } = diff;
|
|
10
|
+
const diffLib = require('../diff/diff.js');
|
|
11
|
+
const { sameHunk } = diffLib;
|
|
10
12
|
|
|
11
13
|
const paintBodyUnit = (view, width, color) => {
|
|
12
14
|
const lines = view.unitLines ?? [];
|
|
13
15
|
const item = view.item;
|
|
14
16
|
const lang = detectLang(view.reviewPath ?? '');
|
|
15
17
|
const overlay = view.codeOverlay;
|
|
16
|
-
const editing =
|
|
17
|
-
|
|
18
|
+
const editing =
|
|
19
|
+
view.mode === 'compose' && overlay && typeof overlay.cursor === 'number';
|
|
20
|
+
const picked = editing ? overlay.scrollCol : 0;
|
|
21
|
+
const scrollCol = picked ?? 0;
|
|
22
|
+
const source = editing ? lines : expandSoftRows(lines, width, false, true);
|
|
23
|
+
const { length } = source;
|
|
18
24
|
const body = new Array(length);
|
|
19
25
|
for (let i = 0; i < length; i++) {
|
|
20
|
-
const line =
|
|
26
|
+
const line = source[i];
|
|
21
27
|
const isBlock = item && sameHunk(line.item, item);
|
|
22
28
|
const row = { ...line };
|
|
23
29
|
if (editing) {
|
|
@@ -25,16 +31,13 @@ const paintBodyUnit = (view, width, color) => {
|
|
|
25
31
|
} else if ((row.type === 'add' || row.type === 'del') && !isBlock) {
|
|
26
32
|
row.mark = ' ';
|
|
27
33
|
}
|
|
28
|
-
body[i] = paintDiffLine(row, width, color, row.origin, lang);
|
|
34
|
+
body[i] = paintDiffLine(row, width, color, row.origin, lang, scrollCol);
|
|
29
35
|
}
|
|
30
36
|
const edit = overlay ? overlay.cursor : null;
|
|
31
|
-
const cursor = codeCursorInRows(
|
|
37
|
+
const cursor = codeCursorInRows(source, false, width, edit, scrollCol);
|
|
32
38
|
return paneResult({
|
|
33
39
|
body,
|
|
34
|
-
fileHits: [],
|
|
35
|
-
todoOwners: [],
|
|
36
40
|
cursor,
|
|
37
|
-
splitBody: false,
|
|
38
41
|
});
|
|
39
42
|
};
|
|
40
43
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ansi = require('./ansi.js');
|
|
4
|
+
const { stripAnsi } = ansi;
|
|
5
|
+
const commands = require('./npm-commands.js');
|
|
6
|
+
|
|
7
|
+
const { withoutNodeFrame, indentStack, relativize } = commands;
|
|
8
|
+
|
|
9
|
+
const summary = () => ({
|
|
10
|
+
tests: null,
|
|
11
|
+
passed: null,
|
|
12
|
+
failed: null,
|
|
13
|
+
skipped: null,
|
|
14
|
+
errors: null,
|
|
15
|
+
warnings: null,
|
|
16
|
+
duration: '',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const problem = (fields = {}) => {
|
|
20
|
+
const trace = fields.trace || '';
|
|
21
|
+
const traces = fields.traces || (trace ? [trace] : []);
|
|
22
|
+
return {
|
|
23
|
+
severity: fields.severity || 'error',
|
|
24
|
+
name: fields.name || '',
|
|
25
|
+
message: fields.message || '',
|
|
26
|
+
trace,
|
|
27
|
+
traces,
|
|
28
|
+
duration: fields.duration || '',
|
|
29
|
+
code: fields.code || '',
|
|
30
|
+
operator: fields.operator || '',
|
|
31
|
+
expected: fields.expected || '',
|
|
32
|
+
actual: fields.actual || '',
|
|
33
|
+
extra: fields.extra || [],
|
|
34
|
+
count: fields.count === undefined ? 1 : fields.count,
|
|
35
|
+
omitted: fields.omitted || 0,
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const report = (tool, fields = {}) => ({
|
|
40
|
+
tool,
|
|
41
|
+
ok: fields.ok !== false,
|
|
42
|
+
summary: fields.summary || summary(),
|
|
43
|
+
problems: fields.problems || [],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const userTrace = (text, root) => {
|
|
47
|
+
const lines = stripAnsi(`${text ?? ''}`).split(/\r?\n/);
|
|
48
|
+
const kept = [];
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
const next = withoutNodeFrame(line);
|
|
51
|
+
if (next === null) continue;
|
|
52
|
+
kept.push(relativize(indentStack(next), root).trimEnd());
|
|
53
|
+
}
|
|
54
|
+
while (kept.length && kept[kept.length - 1] === '') kept.pop();
|
|
55
|
+
return kept.join('\n').trim();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const normalizeMessage = (message) => {
|
|
59
|
+
const text = `${message ?? ''}`.trim().replace(/\s+/g, ' ');
|
|
60
|
+
return text.replace(/\s+\S+:\d+:\d+$/, '');
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const groupKey = (item) =>
|
|
64
|
+
[
|
|
65
|
+
item.severity,
|
|
66
|
+
item.code,
|
|
67
|
+
item.operator,
|
|
68
|
+
item.expected,
|
|
69
|
+
item.actual,
|
|
70
|
+
normalizeMessage(item.message),
|
|
71
|
+
].join('\0');
|
|
72
|
+
|
|
73
|
+
const groupProblems = (problems) => {
|
|
74
|
+
const groups = new Map();
|
|
75
|
+
const order = [];
|
|
76
|
+
for (const item of problems) {
|
|
77
|
+
const key = groupKey(item);
|
|
78
|
+
let group = groups.get(key);
|
|
79
|
+
if (!group) {
|
|
80
|
+
group = problem({ ...item, count: 0, traces: [], omitted: 0 });
|
|
81
|
+
groups.set(key, group);
|
|
82
|
+
order.push(group);
|
|
83
|
+
}
|
|
84
|
+
group.count += 1;
|
|
85
|
+
if (item.trace && group.traces.length < 3) group.traces.push(item.trace);
|
|
86
|
+
}
|
|
87
|
+
for (const group of order) {
|
|
88
|
+
group.trace = group.traces[0] || '';
|
|
89
|
+
group.omitted = Math.max(0, group.count - group.traces.length);
|
|
90
|
+
}
|
|
91
|
+
return order;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const groupReport = (item) =>
|
|
95
|
+
report(item.tool, {
|
|
96
|
+
ok: item.ok,
|
|
97
|
+
summary: item.summary,
|
|
98
|
+
problems: groupProblems(item.problems),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const reduceStderr = (text, root) => userTrace(text, root);
|
|
102
|
+
|
|
103
|
+
const shortTail = (text) => {
|
|
104
|
+
const lines = `${text ?? ''}`.trim().split(/\r?\n/);
|
|
105
|
+
if (lines.length <= 20) return lines.join('\n');
|
|
106
|
+
return lines.slice(-20).join('\n');
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const envelope = (reports, exit, stderr) => {
|
|
110
|
+
const status = exit === null || exit === undefined ? 1 : exit;
|
|
111
|
+
const ok = status === 0;
|
|
112
|
+
const items = new Array(reports.length);
|
|
113
|
+
for (let i = 0; i < reports.length; i++) {
|
|
114
|
+
const item = reports[i];
|
|
115
|
+
items[i] = report(item.tool, { ...item, ok });
|
|
116
|
+
}
|
|
117
|
+
return { ok, exit: status, reports: items, stderr: stderr || '' };
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
summary,
|
|
122
|
+
problem,
|
|
123
|
+
report,
|
|
124
|
+
userTrace,
|
|
125
|
+
groupReport,
|
|
126
|
+
reduceStderr,
|
|
127
|
+
shortTail,
|
|
128
|
+
envelope,
|
|
129
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ansi = require('./ansi.js');
|
|
4
|
+
const { stripAnsi } = ansi;
|
|
5
|
+
const model = require('./report-model.js');
|
|
6
|
+
const testParse = require('./report-test.js');
|
|
7
|
+
const toolParse = require('./report-tools.js');
|
|
8
|
+
|
|
9
|
+
const { problem, report, groupReport } = model;
|
|
10
|
+
const { reduceStderr, shortTail, envelope } = model;
|
|
11
|
+
const { parseTap, parseNodeJsonl, parseTestJson, parseSpec } = testParse;
|
|
12
|
+
const { parseToolJson, parseToolText } = toolParse;
|
|
13
|
+
|
|
14
|
+
const KNOWN = ['test', 'tsc', 'eslint', 'prettier', 'audit', 'outdated', 'ls'];
|
|
15
|
+
|
|
16
|
+
const toolHint = (program, args) => {
|
|
17
|
+
const name = `${program ?? ''}`.split(/[/\\]/).pop();
|
|
18
|
+
const list = args || [];
|
|
19
|
+
const joined = [name, ...list].join(' ');
|
|
20
|
+
if (name === 'eslint') return 'eslint';
|
|
21
|
+
if (name === 'tsc') return 'tsc';
|
|
22
|
+
if (name === 'prettier') return 'prettier';
|
|
23
|
+
if (/\baudit\b/.test(joined)) return 'audit';
|
|
24
|
+
if (/\boutdated\b/.test(joined)) return 'outdated';
|
|
25
|
+
if (name === 'npm' || name === 'pnpm' || name === 'npm.cmd') {
|
|
26
|
+
if (/\b(ls|list)\b/.test(joined)) return 'ls';
|
|
27
|
+
}
|
|
28
|
+
if (name === 'node' && (args || []).includes('--test')) return 'test';
|
|
29
|
+
return '';
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const parseJson = (text) => {
|
|
33
|
+
const trimmed = `${text ?? ''}`.trim();
|
|
34
|
+
if (!trimmed || (trimmed[0] !== '{' && trimmed[0] !== '[')) return null;
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(trimmed);
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const parseJsonReports = (text, root) => {
|
|
43
|
+
const node = parseNodeJsonl(text, root);
|
|
44
|
+
if (node) return [node];
|
|
45
|
+
const data = parseJson(text);
|
|
46
|
+
if (!data) return null;
|
|
47
|
+
const test = parseTestJson(data, root);
|
|
48
|
+
if (test) return [test];
|
|
49
|
+
const tools = parseToolJson(data, root);
|
|
50
|
+
if (tools) return tools;
|
|
51
|
+
return [];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const parseReports = (text, root) => {
|
|
55
|
+
const json = parseJsonReports(text, root);
|
|
56
|
+
if (json) return json;
|
|
57
|
+
const reports = [];
|
|
58
|
+
const tap = parseTap(text, root);
|
|
59
|
+
if (tap) reports.push(tap);
|
|
60
|
+
const tools = parseToolText(text, root);
|
|
61
|
+
for (const item of tools) reports.push(item);
|
|
62
|
+
if (!tap) {
|
|
63
|
+
const spec = parseSpec(text, root);
|
|
64
|
+
if (spec) reports.push(spec);
|
|
65
|
+
}
|
|
66
|
+
return reports;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const withHint = (reports, text, exit, hint) => {
|
|
70
|
+
if (reports.length || !`${text ?? ''}`.trim() || exit === 0) return reports;
|
|
71
|
+
const tool = KNOWN.includes(hint) ? hint : 'test';
|
|
72
|
+
return [report(tool, { problems: [problem({ message: shortTail(text) })] })];
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const buildDocument = (stdout, stderr, root, exit, hint) => {
|
|
76
|
+
const text = stripAnsi(stdout);
|
|
77
|
+
const reports = withHint(parseReports(text, root), text, exit, hint);
|
|
78
|
+
const grouped = new Array(reports.length);
|
|
79
|
+
for (let i = 0; i < reports.length; i++) {
|
|
80
|
+
grouped[i] = groupReport(reports[i]);
|
|
81
|
+
}
|
|
82
|
+
return envelope(grouped, exit, reduceStderr(stderr, root));
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
toolHint,
|
|
87
|
+
parseReports,
|
|
88
|
+
buildDocument,
|
|
89
|
+
};
|