reslop 0.2.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -1
- package/README.md +13 -2
- package/lib/ansi.js +263 -6
- package/lib/api.js +2 -0
- package/lib/capabilities.js +6 -0
- package/lib/cli.js +13 -2
- package/lib/deps.js +43 -3
- package/lib/git-branches.js +228 -17
- package/lib/git-reword-todo.js +20 -0
- package/lib/git.js +20 -3
- package/lib/keys.js +10 -4
- package/lib/npm-commands.js +513 -0
- package/lib/render/branches.js +23 -58
- package/lib/render/chrome.js +156 -99
- package/lib/render/commits.js +643 -31
- package/lib/render/diff.js +4 -4
- package/lib/render/frame.js +51 -5
- package/lib/render/notes.js +22 -5
- package/lib/render/npm.js +228 -0
- package/lib/render/primitives.js +48 -34
- package/lib/render/render.js +3 -3
- package/lib/report-model.js +127 -0
- package/lib/report-parse.js +94 -0
- package/lib/report-render.js +87 -0
- package/lib/report-run.js +164 -0
- package/lib/report-test.js +506 -0
- package/lib/report-tools.js +311 -0
- package/lib/review-merge.js +270 -0
- package/lib/review.js +38 -6
- package/lib/session/actions.js +81 -5
- package/lib/session/commit.js +187 -19
- package/lib/session/compose.js +96 -32
- package/lib/session/dispatch.js +36 -2
- package/lib/session/files-pane.js +36 -6
- package/lib/session/lifecycle.js +116 -16
- package/lib/session/navigation.js +1 -0
- package/lib/session/npm.js +335 -0
- package/lib/session/pointer.js +12 -1
- package/lib/session/todos.js +29 -1
- package/lib/session/view.js +14 -3
- package/lib/session/watch.js +120 -8
- package/lib/session.js +11 -2
- package/package.json +7 -7
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const commands = require('../npm-commands.js');
|
|
4
|
+
const { logViewRows } = require('../render/npm.js');
|
|
5
|
+
|
|
6
|
+
const { listCommands, reduceOutput, writeLog, parseScriptLine } = commands;
|
|
7
|
+
const { scriptLine, saveScript, removeScript, reorderScript } = commands;
|
|
8
|
+
const { startNpm } = commands;
|
|
9
|
+
|
|
10
|
+
const selected = (api) => {
|
|
11
|
+
const cursor = api.ui.nav.npmCursor;
|
|
12
|
+
return api.state.commands[cursor];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const clampCursor = (api) => {
|
|
16
|
+
const last = api.state.commands.length - 1;
|
|
17
|
+
const nav = api.ui.nav;
|
|
18
|
+
if (nav.npmCursor > last) nav.npmCursor = Math.max(0, last);
|
|
19
|
+
if (nav.npmCursor < 0) nav.npmCursor = 0;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const refresh = (api) => {
|
|
23
|
+
api.state.commands = listCommands(api.ui.top);
|
|
24
|
+
clampCursor(api);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const canEdit = (ui) => {
|
|
28
|
+
if (ui.capabilities && ui.capabilities.changes === false) return false;
|
|
29
|
+
if (ui.readOnly) return false;
|
|
30
|
+
return true;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const open = (api) => {
|
|
34
|
+
if (api.ui.mode === 'compose') return;
|
|
35
|
+
refresh(api);
|
|
36
|
+
api.state.viewing = false;
|
|
37
|
+
api.state.output = '';
|
|
38
|
+
api.ui.nav.pane = 'npm';
|
|
39
|
+
api.ui.status = '';
|
|
40
|
+
api.ui.nav.clearSelection();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const scrollLog = (api, delta) => {
|
|
44
|
+
const lines = api.state.output.split('\n');
|
|
45
|
+
let count = lines.at(-1) === '' ? lines.length - 1 : lines.length;
|
|
46
|
+
if (api.state.running) count += 1;
|
|
47
|
+
const frame = api.ui.lastFrame;
|
|
48
|
+
const bodyH = frame && frame.bodyH ? frame.bodyH : 1;
|
|
49
|
+
const max = Math.max(0, count - logViewRows(bodyH));
|
|
50
|
+
let start = api.state.followEnd ? max : api.state.logScroll;
|
|
51
|
+
start = Math.min(max, Math.max(0, start + delta));
|
|
52
|
+
api.state.followEnd = start === max;
|
|
53
|
+
api.state.logScroll = start;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const move = (api, delta) => {
|
|
57
|
+
if (api.state.viewing) return void scrollLog(api, delta);
|
|
58
|
+
if (!api.state.commands.length) return;
|
|
59
|
+
const nav = api.ui.nav;
|
|
60
|
+
const last = api.state.commands.length - 1;
|
|
61
|
+
const next = Math.min(last, Math.max(0, nav.npmCursor + delta));
|
|
62
|
+
if (next === nav.npmCursor) return;
|
|
63
|
+
nav.npmCursor = next;
|
|
64
|
+
api.ui.status = '';
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const stopRun = (api) => {
|
|
68
|
+
api.state.running = false;
|
|
69
|
+
api.state.child = null;
|
|
70
|
+
if (api.ui.progress) api.ui.progress.stop('npm');
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const showOutput = (api, text) => {
|
|
74
|
+
if (!api.state.viewing || !api.state.running) return;
|
|
75
|
+
if (api.state.stopping) return;
|
|
76
|
+
api.state.output = text;
|
|
77
|
+
api.ui.paint();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const withNotice = (text, notice) => {
|
|
81
|
+
const body = `${text ?? ''}`;
|
|
82
|
+
if (body.endsWith(`${notice}\n`)) return body;
|
|
83
|
+
if (!body) return `${notice}\n`;
|
|
84
|
+
if (body.endsWith('\n')) return `${body}${notice}\n`;
|
|
85
|
+
return `${body}\n${notice}\n`;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const finishRun = (api, entry, result) => {
|
|
89
|
+
const stopped = api.state.stopping === true;
|
|
90
|
+
const reduced = stopped
|
|
91
|
+
? withNotice(api.state.output, 'terminated')
|
|
92
|
+
: reduceOutput(result.text, api.ui.top, result.status);
|
|
93
|
+
api.state.output = reduced;
|
|
94
|
+
api.state.stopping = false;
|
|
95
|
+
stopRun(api);
|
|
96
|
+
try {
|
|
97
|
+
writeLog(api.ui.top, entry.name, reduced);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
api.ui.status = error.message;
|
|
100
|
+
api.ui.paint();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (stopped && api.state.viewing) api.ui.status = 'terminated';
|
|
104
|
+
api.ui.paint();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const startRun = (api, entry) => {
|
|
108
|
+
api.state.output = '';
|
|
109
|
+
api.state.viewing = true;
|
|
110
|
+
api.state.followEnd = true;
|
|
111
|
+
api.state.logScroll = 0;
|
|
112
|
+
api.state.running = true;
|
|
113
|
+
api.state.stopping = false;
|
|
114
|
+
api.ui.status = '';
|
|
115
|
+
if (api.ui.progress) api.ui.progress.start('npm');
|
|
116
|
+
const runner = api.ui.repo && api.ui.repo.runNpmCommand;
|
|
117
|
+
const start = typeof runner === 'function' ? runner : startNpm;
|
|
118
|
+
api.state.child = start(
|
|
119
|
+
api.ui.top,
|
|
120
|
+
entry,
|
|
121
|
+
(text) => showOutput(api, reduceOutput(text, api.ui.top, '')),
|
|
122
|
+
(result) => finishRun(api, entry, result),
|
|
123
|
+
);
|
|
124
|
+
api.ui.paint();
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const runSelected = (api) => {
|
|
128
|
+
if (api.state.viewing || api.state.child) return;
|
|
129
|
+
const entry = selected(api);
|
|
130
|
+
if (!entry) return;
|
|
131
|
+
startRun(api, entry);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const rerun = (api) => {
|
|
135
|
+
if (!api.state.viewing || api.state.running) return;
|
|
136
|
+
const entry = selected(api);
|
|
137
|
+
if (!entry) return;
|
|
138
|
+
startRun(api, entry);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const closeView = (api) => {
|
|
142
|
+
if (api.state.running) {
|
|
143
|
+
api.state.stopping = true;
|
|
144
|
+
if (api.state.child) api.state.child.kill();
|
|
145
|
+
if (!api.state.running) return;
|
|
146
|
+
api.state.output = withNotice(api.state.output, 'terminated');
|
|
147
|
+
stopRun(api);
|
|
148
|
+
api.ui.status = 'terminated';
|
|
149
|
+
api.ui.paint();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
api.state.viewing = false;
|
|
153
|
+
api.ui.status = '';
|
|
154
|
+
api.ui.paint();
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const editSelected = (api) => {
|
|
158
|
+
if (api.state.viewing) return;
|
|
159
|
+
if (!canEdit(api.ui)) {
|
|
160
|
+
api.ui.status = 'read only';
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const entry = selected(api);
|
|
164
|
+
if (!entry || entry.kind !== 'script') {
|
|
165
|
+
api.ui.status = 'not a script';
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
api.state.editName = entry.name;
|
|
169
|
+
api.ui.composer.openCompose('npm', scriptLine(entry));
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const newCommand = (api) => {
|
|
173
|
+
if (api.state.viewing) return;
|
|
174
|
+
if (!canEdit(api.ui)) {
|
|
175
|
+
api.ui.status = 'read only';
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
api.state.editName = '';
|
|
179
|
+
api.ui.composer.openCompose('npm', '');
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const finishEdit = (api) => {
|
|
183
|
+
const editor = api.ui.composer.state.editor;
|
|
184
|
+
const text = editor ? editor.text : '';
|
|
185
|
+
const parsed = parseScriptLine(text);
|
|
186
|
+
if (!parsed) {
|
|
187
|
+
api.ui.status = 'name: command';
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
api.ui.ignoreWatch();
|
|
192
|
+
saveScript(api.ui.top, api.state.editName, parsed);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
api.ui.status = error.message;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
api.ui.composer.closeCompose();
|
|
198
|
+
refresh(api);
|
|
199
|
+
const at = api.state.commands.findIndex(
|
|
200
|
+
(entry) => entry.name === parsed.name,
|
|
201
|
+
);
|
|
202
|
+
if (at >= 0) api.ui.nav.npmCursor = at;
|
|
203
|
+
api.ui.status = 'saved';
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const askDrop = (api) => {
|
|
207
|
+
if (api.state.viewing) return;
|
|
208
|
+
if (!canEdit(api.ui)) {
|
|
209
|
+
api.ui.status = 'read only';
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const entry = selected(api);
|
|
213
|
+
if (!entry || entry.kind !== 'script') {
|
|
214
|
+
api.ui.status = 'not a script';
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
api.state.dropName = entry.name;
|
|
218
|
+
api.ui.mode = 'confirmDrop';
|
|
219
|
+
api.ui.status = '';
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const confirmDrop = (api) => {
|
|
223
|
+
const name = api.state.dropName;
|
|
224
|
+
api.state.dropName = '';
|
|
225
|
+
api.ui.mode = 'review';
|
|
226
|
+
if (!name) return;
|
|
227
|
+
try {
|
|
228
|
+
api.ui.ignoreWatch();
|
|
229
|
+
removeScript(api.ui.top, name);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
api.ui.status = error.message;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
refresh(api);
|
|
235
|
+
api.ui.status = `dropped ${name}`;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const cancelDrop = (api) => {
|
|
239
|
+
api.state.dropName = '';
|
|
240
|
+
api.ui.mode = 'review';
|
|
241
|
+
api.ui.status = '';
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const reorder = (api, delta) => {
|
|
245
|
+
if (api.state.viewing) return;
|
|
246
|
+
if (!canEdit(api.ui)) {
|
|
247
|
+
api.ui.status = 'read only';
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const entry = selected(api);
|
|
251
|
+
if (!entry || entry.kind !== 'script') {
|
|
252
|
+
api.ui.status = 'not a script';
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
api.ui.ignoreWatch();
|
|
257
|
+
if (!reorderScript(api.ui.top, entry.name, delta)) return;
|
|
258
|
+
} catch (error) {
|
|
259
|
+
api.ui.status = error.message;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
refresh(api);
|
|
263
|
+
const at = api.state.commands.findIndex((item) => item.name === entry.name);
|
|
264
|
+
if (at >= 0) api.ui.nav.npmCursor = at;
|
|
265
|
+
api.ui.status = '';
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const createNpmController = (ui) => {
|
|
269
|
+
const state = {
|
|
270
|
+
commands: [],
|
|
271
|
+
viewing: false,
|
|
272
|
+
output: '',
|
|
273
|
+
followEnd: true,
|
|
274
|
+
logScroll: 0,
|
|
275
|
+
editName: '',
|
|
276
|
+
dropName: '',
|
|
277
|
+
running: false,
|
|
278
|
+
stopping: false,
|
|
279
|
+
child: null,
|
|
280
|
+
};
|
|
281
|
+
const api = { ui, state };
|
|
282
|
+
return {
|
|
283
|
+
state,
|
|
284
|
+
get commands() {
|
|
285
|
+
return state.commands;
|
|
286
|
+
},
|
|
287
|
+
get viewing() {
|
|
288
|
+
return state.viewing;
|
|
289
|
+
},
|
|
290
|
+
get output() {
|
|
291
|
+
return state.output;
|
|
292
|
+
},
|
|
293
|
+
get dropName() {
|
|
294
|
+
return state.dropName;
|
|
295
|
+
},
|
|
296
|
+
get followEnd() {
|
|
297
|
+
return state.followEnd;
|
|
298
|
+
},
|
|
299
|
+
get logScroll() {
|
|
300
|
+
return state.logScroll;
|
|
301
|
+
},
|
|
302
|
+
get editName() {
|
|
303
|
+
return state.editName;
|
|
304
|
+
},
|
|
305
|
+
get running() {
|
|
306
|
+
return state.running;
|
|
307
|
+
},
|
|
308
|
+
reset() {
|
|
309
|
+
if (state.child) state.child.kill();
|
|
310
|
+
stopRun(api);
|
|
311
|
+
state.commands = [];
|
|
312
|
+
state.viewing = false;
|
|
313
|
+
state.output = '';
|
|
314
|
+
state.followEnd = true;
|
|
315
|
+
state.logScroll = 0;
|
|
316
|
+
state.editName = '';
|
|
317
|
+
state.dropName = '';
|
|
318
|
+
state.stopping = false;
|
|
319
|
+
},
|
|
320
|
+
open: () => open(api),
|
|
321
|
+
move: (delta) => move(api, delta),
|
|
322
|
+
run: () => runSelected(api),
|
|
323
|
+
rerun: () => rerun(api),
|
|
324
|
+
closeView: () => closeView(api),
|
|
325
|
+
edit: () => editSelected(api),
|
|
326
|
+
create: () => newCommand(api),
|
|
327
|
+
finishEdit: () => finishEdit(api),
|
|
328
|
+
askDrop: () => askDrop(api),
|
|
329
|
+
confirmDrop: () => confirmDrop(api),
|
|
330
|
+
cancelDrop: () => cancelDrop(api),
|
|
331
|
+
reorder: (delta) => reorder(api, delta),
|
|
332
|
+
};
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
module.exports = { createNpmController };
|
package/lib/session/pointer.js
CHANGED
|
@@ -99,13 +99,24 @@ const RELEASE_TARGETS = [
|
|
|
99
99
|
match: (ui) => ui.pane === 'commits',
|
|
100
100
|
hits: 'fileHits',
|
|
101
101
|
onHit: (ui, hit) => {
|
|
102
|
+
if (ui.mode === 'compose') return;
|
|
102
103
|
ui.nav.commitCursor = hit.cursor;
|
|
103
104
|
},
|
|
104
105
|
},
|
|
106
|
+
{
|
|
107
|
+
match: (ui) => ui.pane === 'npm',
|
|
108
|
+
hits: 'fileHits',
|
|
109
|
+
onHit: (ui, hit) => {
|
|
110
|
+
ui.nav.npmCursor = hit.cursor;
|
|
111
|
+
},
|
|
112
|
+
},
|
|
105
113
|
{
|
|
106
114
|
match: (ui) => ui.pane === 'diff',
|
|
107
115
|
hits: 'todoHits',
|
|
108
|
-
onHit: (ui, hit) =>
|
|
116
|
+
onHit: (ui, hit) => {
|
|
117
|
+
if (hit.check) return void ui.composer.onTodoCheck(hit.cursor);
|
|
118
|
+
ui.composer.onTodoHit(hit.cursor);
|
|
119
|
+
},
|
|
109
120
|
},
|
|
110
121
|
{
|
|
111
122
|
match: (ui) => ui.mode === 'compose',
|
package/lib/session/todos.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const files = require('../files.js');
|
|
4
4
|
const { isTodosEntry, TODO_FILE } = files;
|
|
5
5
|
const review = require('../review.js');
|
|
6
|
-
const { addTodo, setTodoText, removeTodo, checkLabel } = review;
|
|
6
|
+
const { addTodo, setTodoText, setTodoDone, removeTodo, checkLabel } = review;
|
|
7
7
|
|
|
8
8
|
const TODO_TYPE_SKIP = [
|
|
9
9
|
'enter',
|
|
@@ -101,6 +101,7 @@ const closeTodoPage = (api) => {
|
|
|
101
101
|
api.nav.todoOpen = false;
|
|
102
102
|
api.ui.syncReviewPath();
|
|
103
103
|
api.ui.syncFileCursor();
|
|
104
|
+
api.ui.lifecycle.catchUpWatch();
|
|
104
105
|
};
|
|
105
106
|
|
|
106
107
|
const focusTodoPage = (api, todoId) => {
|
|
@@ -189,6 +190,17 @@ const removeFocusedTodo = (api) => {
|
|
|
189
190
|
api.ui.flushReview(true);
|
|
190
191
|
};
|
|
191
192
|
|
|
193
|
+
const toggleFocusedTodo = (api) => {
|
|
194
|
+
const nav = api.nav;
|
|
195
|
+
if (!nav.todoOpen || nav.pane !== 'diff') return false;
|
|
196
|
+
if (api.ui.mode !== 'review') return false;
|
|
197
|
+
const todo = listTodos(api)[clampedTodoFocus(api)];
|
|
198
|
+
if (!todo) return false;
|
|
199
|
+
setTodoDone(api.review.store, todo.id, !todo.done);
|
|
200
|
+
api.ui.flushReview(true);
|
|
201
|
+
return true;
|
|
202
|
+
};
|
|
203
|
+
|
|
192
204
|
const onTodoHit = (api, cursor) => {
|
|
193
205
|
if (!api.nav.todoOpen) return;
|
|
194
206
|
const todos = listTodos(api);
|
|
@@ -206,6 +218,20 @@ const onTodoHit = (api, cursor) => {
|
|
|
206
218
|
editFocusedTodo(api);
|
|
207
219
|
};
|
|
208
220
|
|
|
221
|
+
const onTodoCheck = (api, cursor) => {
|
|
222
|
+
if (!api.nav.todoOpen) return;
|
|
223
|
+
const todo = listTodos(api)[cursor];
|
|
224
|
+
if (!todo) return void onTodoHit(api, cursor);
|
|
225
|
+
const { state } = api;
|
|
226
|
+
if (state.composeKind === 'todo' && state.composeTodoId !== todo.id) {
|
|
227
|
+
api.commit();
|
|
228
|
+
api.close();
|
|
229
|
+
}
|
|
230
|
+
setTodoDone(api.review.store, todo.id, !todo.done);
|
|
231
|
+
api.nav.todoFocus = cursor;
|
|
232
|
+
api.ui.flushReview(true);
|
|
233
|
+
};
|
|
234
|
+
|
|
209
235
|
const createTodo = (api) => {
|
|
210
236
|
startDraftCompose(api);
|
|
211
237
|
};
|
|
@@ -253,6 +279,8 @@ const createTodoCompose = (api) => ({
|
|
|
253
279
|
typeIntoTodo: (key) => typeIntoTodo(api, key),
|
|
254
280
|
removeFocusedTodo: () => removeFocusedTodo(api),
|
|
255
281
|
onTodoHit: (cursor) => onTodoHit(api, cursor),
|
|
282
|
+
onTodoCheck: (cursor) => onTodoCheck(api, cursor),
|
|
283
|
+
toggleFocusedTodo: () => toggleFocusedTodo(api),
|
|
256
284
|
onTodo: () => onTodo(api),
|
|
257
285
|
todoEditView: () => todoEditView(api),
|
|
258
286
|
commitTodo: (text) => commitTodo(api, text),
|
package/lib/session/view.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
|
|
5
5
|
const files = require('../files.js');
|
|
6
|
+
const { listedCommits } = require('./commit.js');
|
|
6
7
|
const { fileEntries, REPO_TODOS_LABEL } = files;
|
|
7
8
|
const review = require('../review.js');
|
|
8
9
|
const { noteCounts } = review;
|
|
@@ -143,7 +144,7 @@ const unitBodyLines = (api) => {
|
|
|
143
144
|
};
|
|
144
145
|
|
|
145
146
|
const view = (api) => {
|
|
146
|
-
const { composer, nav, ui, gitBranches, commits, updater } = api;
|
|
147
|
+
const { composer, nav, ui, gitBranches, commits, npm, updater } = api;
|
|
147
148
|
const templates = composer.shownTemplates();
|
|
148
149
|
const change = ui.change;
|
|
149
150
|
const sourceKind = (change && change.source) || '';
|
|
@@ -169,9 +170,19 @@ const view = (api) => {
|
|
|
169
170
|
branch: ui.branch,
|
|
170
171
|
branches: gitBranches.branches,
|
|
171
172
|
branchCursor: nav.branchCursor,
|
|
172
|
-
commits: commits.commits,
|
|
173
|
+
commits: listedCommits(commits.commits),
|
|
173
174
|
commitCursor: nav.commitCursor,
|
|
175
|
+
commitView: commits.commitView,
|
|
176
|
+
npmCommands: npm.commands,
|
|
177
|
+
npmCursor: nav.npmCursor,
|
|
178
|
+
npmView: npm.viewing,
|
|
179
|
+
npmOutput: npm.output,
|
|
180
|
+
npmFollow: npm.followEnd,
|
|
181
|
+
npmScroll: npm.logScroll,
|
|
182
|
+
npmEditName: npm.editName,
|
|
183
|
+
npmRunning: npm.running,
|
|
174
184
|
repoName: repoNameOf(api),
|
|
185
|
+
rev: ui.rev || '',
|
|
175
186
|
revShort: ui.revShort || '',
|
|
176
187
|
sourceLabel: ui.sourceLabel || '',
|
|
177
188
|
sourceKind,
|
|
@@ -186,7 +197,7 @@ const view = (api) => {
|
|
|
186
197
|
templateIndex: composer.clampedTemplateIndex(templates),
|
|
187
198
|
updateFrom: updater.from,
|
|
188
199
|
updateTo: updater.to,
|
|
189
|
-
dropName: gitBranches.dropName || commits.dropName,
|
|
200
|
+
dropName: gitBranches.dropName || commits.dropName || npm.dropName,
|
|
190
201
|
};
|
|
191
202
|
};
|
|
192
203
|
|
package/lib/session/watch.js
CHANGED
|
@@ -72,26 +72,127 @@ const closeWatcher = (watcher) => {
|
|
|
72
72
|
}
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
const reviewMarkdown = (rel) => {
|
|
76
|
+
const norm = `${rel}`.replaceAll('\\', '/');
|
|
77
|
+
if (!norm.startsWith('.review/')) return false;
|
|
78
|
+
const rest = norm.slice('.review/'.length);
|
|
79
|
+
if (!rest || rest.includes('/')) return false;
|
|
80
|
+
return rest.endsWith('.md');
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const isReviewDir = (root, dir) => {
|
|
84
|
+
const rel = path.relative(root, dir).replaceAll('\\', '/');
|
|
85
|
+
return rel === '.review' || rel.startsWith('.review/');
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const hasFile = (full) => {
|
|
89
|
+
try {
|
|
90
|
+
fs.accessSync(full);
|
|
91
|
+
return true;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const isBareReviewName = (root, dir, rel) => {
|
|
98
|
+
if (!rel || rel.includes('/') || !rel.endsWith('.md')) return false;
|
|
99
|
+
const base = path.basename(rel);
|
|
100
|
+
if (hasFile(path.join(dir, base))) return false;
|
|
101
|
+
return hasFile(path.join(root, '.review', base));
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const isReviewEvent = (root, dir, name) => {
|
|
105
|
+
if (isReviewDir(root, dir)) return true;
|
|
106
|
+
if (!name) return false;
|
|
107
|
+
const rel = path.relative(root, path.join(dir, name));
|
|
108
|
+
const norm = rel.replaceAll('\\', '/');
|
|
109
|
+
if (reviewMarkdown(norm)) return true;
|
|
110
|
+
return isBareReviewName(root, dir, norm);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const isOwnDirEvent = (dir, name) => {
|
|
114
|
+
if (!name) return false;
|
|
115
|
+
const base = path.basename(dir);
|
|
116
|
+
const slash = `${name}`.replaceAll('\\', '/');
|
|
117
|
+
const bare = slash.startsWith('./') ? slash.slice(2) : slash;
|
|
118
|
+
const trimmed = bare.endsWith('/') ? bare.slice(0, -1) : bare;
|
|
119
|
+
if (trimmed === base) {
|
|
120
|
+
try {
|
|
121
|
+
return !fs.statSync(path.join(dir, trimmed)).isFile();
|
|
122
|
+
} catch {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const full = path.resolve(dir, name);
|
|
127
|
+
if (full === path.resolve(dir)) return true;
|
|
128
|
+
if (path.basename(full) !== base) return false;
|
|
129
|
+
try {
|
|
130
|
+
return fs.realpathSync(full) === fs.realpathSync(dir);
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const schedule = (state, timerKey, fire) => {
|
|
137
|
+
if (state.closed || !fire) return;
|
|
138
|
+
if (state[timerKey] !== null) clearTimeout(state[timerKey]);
|
|
139
|
+
state[timerKey] = setTimeout(() => {
|
|
140
|
+
state[timerKey] = null;
|
|
80
141
|
if (state.closed) return;
|
|
81
|
-
|
|
142
|
+
fire();
|
|
82
143
|
}, state.debounceMs);
|
|
83
|
-
|
|
144
|
+
const timer = state[timerKey];
|
|
145
|
+
if (timer && typeof timer.unref === 'function') timer.unref();
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const REVIEW_HOLD_MS = 200;
|
|
149
|
+
|
|
150
|
+
const reviewBurst = (state) => {
|
|
151
|
+
const seen = state.reviewSeenAt;
|
|
152
|
+
if (!seen) return false;
|
|
153
|
+
const hold = Math.max(state.debounceMs * 2, REVIEW_HOLD_MS);
|
|
154
|
+
return Date.now() - seen < hold;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const scheduleChange = (state) => {
|
|
158
|
+
schedule(state, 'timer', () => {
|
|
159
|
+
if (reviewBurst(state)) return;
|
|
160
|
+
if (state.onChange) state.onChange();
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const scheduleUnnamed = (state) => {
|
|
165
|
+
if (reviewBurst(state)) return;
|
|
166
|
+
schedule(state, 'looseTimer', () => {
|
|
167
|
+
if (reviewBurst(state)) return;
|
|
168
|
+
scheduleChange(state);
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const dropChange = (state) => {
|
|
173
|
+
if (state.timer !== null) clearTimeout(state.timer);
|
|
174
|
+
if (state.looseTimer !== null) clearTimeout(state.looseTimer);
|
|
175
|
+
state.timer = null;
|
|
176
|
+
state.looseTimer = null;
|
|
84
177
|
};
|
|
85
178
|
|
|
86
179
|
const handleWatchEvent = (state, dir, eventType, filename) => {
|
|
87
180
|
if (state.closed) return;
|
|
88
181
|
const name = filename ? `${filename}` : '';
|
|
89
182
|
const rel = name ? path.relative(state.root, path.join(dir, name)) : '';
|
|
183
|
+
if (isReviewEvent(state.root, dir, name)) {
|
|
184
|
+
state.reviewSeenAt = Date.now();
|
|
185
|
+
dropChange(state);
|
|
186
|
+
return void schedule(state, 'reviewTimer', state.onReview);
|
|
187
|
+
}
|
|
90
188
|
if (rel && ignoredRel(rel)) return;
|
|
189
|
+
if (reviewBurst(state)) return;
|
|
190
|
+
if (!name) return void scheduleUnnamed(state);
|
|
191
|
+
if (isOwnDirEvent(dir, name)) return;
|
|
91
192
|
if (eventType === 'rename' && name) {
|
|
92
193
|
state.watchNewDir(path.join(dir, name));
|
|
93
194
|
}
|
|
94
|
-
|
|
195
|
+
scheduleChange(state);
|
|
95
196
|
};
|
|
96
197
|
|
|
97
198
|
const watchOne = (state, target, extra = {}) => {
|
|
@@ -141,6 +242,7 @@ const maybeWatchNewDir = (state, full) => {
|
|
|
141
242
|
return;
|
|
142
243
|
}
|
|
143
244
|
if (!stat.isDirectory()) return;
|
|
245
|
+
if (path.basename(full) === '.review') return void watchOne(state, full);
|
|
144
246
|
if (skipDirName(path.basename(full))) return;
|
|
145
247
|
watchTree(state, full);
|
|
146
248
|
};
|
|
@@ -166,7 +268,11 @@ const watchGit = (state) => {
|
|
|
166
268
|
const stopState = (state) => {
|
|
167
269
|
state.closed = true;
|
|
168
270
|
if (state.timer !== null) clearTimeout(state.timer);
|
|
271
|
+
if (state.reviewTimer !== null) clearTimeout(state.reviewTimer);
|
|
272
|
+
if (state.looseTimer !== null) clearTimeout(state.looseTimer);
|
|
169
273
|
state.timer = null;
|
|
274
|
+
state.reviewTimer = null;
|
|
275
|
+
state.looseTimer = null;
|
|
170
276
|
while (state.watchers.length) closeWatcher(state.watchers.pop());
|
|
171
277
|
state.watched.clear();
|
|
172
278
|
};
|
|
@@ -189,13 +295,18 @@ const createDiskWatcher = (options) => {
|
|
|
189
295
|
dirWatchOpts: recursive ? { recursive: true } : {},
|
|
190
296
|
watchers: [],
|
|
191
297
|
watched: new Set(),
|
|
298
|
+
onReview: options.onReview,
|
|
192
299
|
timer: null,
|
|
300
|
+
reviewTimer: null,
|
|
301
|
+
looseTimer: null,
|
|
302
|
+
reviewSeenAt: 0,
|
|
193
303
|
closed: false,
|
|
194
304
|
watchNewDir: (full) => maybeWatchNewDir(state, full),
|
|
195
305
|
};
|
|
196
306
|
const scopes = options.paths && options.paths.length ? options.paths : ['.'];
|
|
197
307
|
for (const spec of scopes) watchScope(state, spec);
|
|
198
308
|
watchGit(state);
|
|
309
|
+
watchOne(state, path.resolve(state.root, '.review'));
|
|
199
310
|
return {
|
|
200
311
|
close: () => stopState(state),
|
|
201
312
|
watchFiles: (rels) => watchFiles(state, rels),
|
|
@@ -207,5 +318,6 @@ module.exports = {
|
|
|
207
318
|
POLL_MS,
|
|
208
319
|
canWatchRecursive,
|
|
209
320
|
ignoredRel,
|
|
321
|
+
isOwnDirEvent,
|
|
210
322
|
createDiskWatcher,
|
|
211
323
|
};
|