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/session/pointer.js
CHANGED
|
@@ -2,50 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
const { extractText, isRange } = require('../select.js');
|
|
4
4
|
const { hitAction } = require('../keys.js');
|
|
5
|
+
const { copyText } = require('../clipboard.js');
|
|
5
6
|
|
|
6
|
-
const mouseCell = (
|
|
7
|
-
const size =
|
|
7
|
+
const mouseCell = (ui, event) => {
|
|
8
|
+
const size = ui.lastSize ?? ui.getSize();
|
|
8
9
|
const x = Math.min(Math.max(1, event.x), size.width);
|
|
9
10
|
const y = Math.min(Math.max(1, event.y), size.height);
|
|
10
11
|
return { x, y };
|
|
11
12
|
};
|
|
12
13
|
|
|
13
|
-
const onPress = (
|
|
14
|
-
const nav =
|
|
15
|
-
const cell = mouseCell(
|
|
14
|
+
const onPress = (ui, event) => {
|
|
15
|
+
const nav = ui.nav;
|
|
16
|
+
const cell = mouseCell(ui, event);
|
|
16
17
|
nav.mouseAnchor = cell;
|
|
17
18
|
nav.pendingClick = null;
|
|
18
19
|
nav.selection = null;
|
|
19
|
-
const frame =
|
|
20
|
+
const frame = ui.lastFrame;
|
|
20
21
|
if (frame && cell.y === frame.height) {
|
|
21
22
|
nav.pendingClick = hitAction(frame.buttons, cell.x - 1);
|
|
22
23
|
}
|
|
23
24
|
};
|
|
24
25
|
|
|
25
|
-
const onDrag = (
|
|
26
|
-
const nav =
|
|
26
|
+
const onDrag = (ui, event) => {
|
|
27
|
+
const nav = ui.nav;
|
|
27
28
|
if (!nav.mouseAnchor) return;
|
|
28
29
|
nav.pendingClick = null;
|
|
29
30
|
nav.selection = {
|
|
30
31
|
start: nav.mouseAnchor,
|
|
31
|
-
end: mouseCell(
|
|
32
|
+
end: mouseCell(ui, event),
|
|
32
33
|
};
|
|
33
34
|
};
|
|
34
35
|
|
|
35
|
-
const clickButton = (
|
|
36
|
-
const nav =
|
|
37
|
-
const frame =
|
|
36
|
+
const clickButton = (ui, cell, clickId) => {
|
|
37
|
+
const nav = ui.nav;
|
|
38
|
+
const frame = ui.lastFrame;
|
|
38
39
|
if (!clickId || !frame || isRange(nav.selection)) return false;
|
|
39
40
|
const id = hitAction(frame.buttons, cell.x - 1);
|
|
40
41
|
if (id === clickId && cell.y === frame.height) {
|
|
41
|
-
|
|
42
|
+
ui.dispatch(id);
|
|
42
43
|
}
|
|
43
44
|
nav.selection = null;
|
|
44
45
|
return true;
|
|
45
46
|
};
|
|
46
47
|
|
|
47
|
-
const clickHit = (
|
|
48
|
-
const nav =
|
|
48
|
+
const clickHit = (ui, hits, cell, onHit) => {
|
|
49
|
+
const nav = ui.nav;
|
|
49
50
|
if (!hits || isRange(nav.selection)) return false;
|
|
50
51
|
const x = cell.x - 1;
|
|
51
52
|
const hit = hits.find((row) => {
|
|
@@ -54,108 +55,107 @@ const clickHit = (options, hits, cell, onHit) => {
|
|
|
54
55
|
return x >= row.x0 && x < row.x1;
|
|
55
56
|
});
|
|
56
57
|
if (!hit) return false;
|
|
57
|
-
onHit(
|
|
58
|
+
onHit(ui, hit);
|
|
58
59
|
nav.selection = null;
|
|
59
60
|
return true;
|
|
60
61
|
};
|
|
61
62
|
|
|
62
|
-
const copySelection = (
|
|
63
|
-
const nav =
|
|
64
|
-
const frame =
|
|
63
|
+
const copySelection = (ui) => {
|
|
64
|
+
const nav = ui.nav;
|
|
65
|
+
const frame = ui.lastFrame;
|
|
65
66
|
if (!isRange(nav.selection) || !frame) return;
|
|
66
67
|
const text = extractText(frame.rows, nav.selection);
|
|
67
68
|
if (!text) return;
|
|
68
|
-
const ok =
|
|
69
|
-
|
|
69
|
+
const ok = copyText(text, ui.stdout);
|
|
70
|
+
ui.status = ok ? 'copied' : 'copy failed';
|
|
70
71
|
};
|
|
71
72
|
|
|
72
73
|
const RELEASE_TARGETS = [
|
|
73
74
|
{
|
|
74
75
|
match: () => true,
|
|
75
76
|
hits: 'statusHits',
|
|
76
|
-
onHit: (
|
|
77
|
-
if (hit.id === 'branch')
|
|
78
|
-
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
options.confirmChoice(hit.id);
|
|
77
|
+
onHit: (ui, hit) => {
|
|
78
|
+
if (hit.id === 'branch') return void ui.gitBranches.onBranch();
|
|
79
|
+
ui.input.confirmChoice(hit.id);
|
|
82
80
|
},
|
|
83
81
|
},
|
|
84
82
|
{
|
|
85
|
-
match: (
|
|
83
|
+
match: (ui) => ui.pane === 'files',
|
|
86
84
|
hits: 'fileHits',
|
|
87
|
-
onHit: (
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
onHit: (ui, hit) => {
|
|
86
|
+
ui.nav.fileCursor = hit.cursor;
|
|
87
|
+
ui.filesPane.onOpenFile();
|
|
90
88
|
},
|
|
91
89
|
},
|
|
92
90
|
{
|
|
93
|
-
match: (
|
|
91
|
+
match: (ui) => ui.pane === 'branches',
|
|
94
92
|
hits: 'fileHits',
|
|
95
|
-
onHit: (
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
onHit: (ui, hit) => {
|
|
94
|
+
ui.nav.branchCursor = hit.cursor;
|
|
95
|
+
ui.gitBranches.onCheckoutBranch();
|
|
98
96
|
},
|
|
99
97
|
},
|
|
100
98
|
{
|
|
101
|
-
match: (
|
|
99
|
+
match: (ui) => ui.pane === 'commits',
|
|
102
100
|
hits: 'fileHits',
|
|
103
|
-
onHit: (
|
|
104
|
-
|
|
101
|
+
onHit: (ui, hit) => {
|
|
102
|
+
ui.nav.commitCursor = hit.cursor;
|
|
105
103
|
},
|
|
106
104
|
},
|
|
107
105
|
{
|
|
108
|
-
match: (
|
|
106
|
+
match: (ui) => ui.pane === 'diff',
|
|
109
107
|
hits: 'todoHits',
|
|
110
|
-
onHit: (
|
|
108
|
+
onHit: (ui, hit) => {
|
|
109
|
+
if (hit.check) return void ui.composer.onTodoCheck(hit.cursor);
|
|
110
|
+
ui.composer.onTodoHit(hit.cursor);
|
|
111
|
+
},
|
|
111
112
|
},
|
|
112
113
|
{
|
|
113
|
-
match: (
|
|
114
|
+
match: (ui) => ui.mode === 'compose',
|
|
114
115
|
hits: 'templateHits',
|
|
115
|
-
onHit: (
|
|
116
|
+
onHit: (ui, hit) => ui.selectTemplate(hit.cursor),
|
|
116
117
|
},
|
|
117
118
|
];
|
|
118
119
|
|
|
119
|
-
const onRelease = (
|
|
120
|
-
const nav =
|
|
121
|
-
const cell = mouseCell(
|
|
120
|
+
const onRelease = (ui, event) => {
|
|
121
|
+
const nav = ui.nav;
|
|
122
|
+
const cell = mouseCell(ui, event);
|
|
122
123
|
if (nav.mouseAnchor) {
|
|
123
124
|
nav.selection = { start: nav.mouseAnchor, end: cell };
|
|
124
125
|
}
|
|
125
126
|
const clickId = nav.pendingClick;
|
|
126
127
|
nav.pendingClick = null;
|
|
127
128
|
nav.mouseAnchor = null;
|
|
128
|
-
if (clickButton(
|
|
129
|
-
const frame =
|
|
129
|
+
if (clickButton(ui, cell, clickId)) return;
|
|
130
|
+
const frame = ui.lastFrame;
|
|
130
131
|
for (const target of RELEASE_TARGETS) {
|
|
131
|
-
if (!target.match(
|
|
132
|
+
if (!target.match(ui)) continue;
|
|
132
133
|
const hits = frame?.[target.hits];
|
|
133
|
-
const opened = clickHit(
|
|
134
|
+
const opened = clickHit(ui, hits, cell, target.onHit);
|
|
134
135
|
if (opened) return;
|
|
135
136
|
}
|
|
136
|
-
copySelection(
|
|
137
|
+
copySelection(ui);
|
|
137
138
|
};
|
|
138
139
|
|
|
139
140
|
const POINTER_HANDLERS = {
|
|
140
|
-
wheelUp: (
|
|
141
|
-
wheelDown: (
|
|
141
|
+
wheelUp: (ui) => ui.filesPane.onScroll(-1),
|
|
142
|
+
wheelDown: (ui) => ui.filesPane.onScroll(1),
|
|
142
143
|
press: onPress,
|
|
143
144
|
drag: onDrag,
|
|
144
145
|
release: onRelease,
|
|
145
146
|
};
|
|
146
147
|
|
|
147
|
-
const handle = (
|
|
148
|
+
const handle = (ui, event) => {
|
|
148
149
|
const kind = event.kind;
|
|
149
150
|
const handler = POINTER_HANDLERS[kind];
|
|
150
151
|
if (!handler) return;
|
|
151
152
|
const isWheel = kind === 'wheelUp' || kind === 'wheelDown';
|
|
152
153
|
if (!isWheel && event.btn !== 0 && event.btn !== undefined) return;
|
|
153
|
-
handler(
|
|
154
|
+
handler(ui, event);
|
|
154
155
|
};
|
|
155
156
|
|
|
156
|
-
const createPointer = (
|
|
157
|
-
handle: (event) => handle(
|
|
158
|
-
mouseCell: (event) => mouseCell(options, event),
|
|
157
|
+
const createPointer = (ui) => ({
|
|
158
|
+
handle: (event) => handle(ui, event),
|
|
159
159
|
});
|
|
160
160
|
|
|
161
161
|
module.exports = { createPointer };
|
package/lib/session/review.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const { createStore, applyImportedNotes, setStatus } = store;
|
|
5
|
-
const storage = require('../review/storage.js');
|
|
6
|
-
const { listReviewNames, loadTemplates, resolveReviewPath } = storage;
|
|
7
|
-
const { loadReview, flushReview } = storage;
|
|
3
|
+
const { stateView } = require('./accessors.js');
|
|
8
4
|
|
|
9
|
-
const
|
|
10
|
-
|
|
5
|
+
const review = require('../review.js');
|
|
6
|
+
const { createStore, applyImportedNotes, setStatus } = review;
|
|
7
|
+
const { listReviewNames, loadTemplates, resolveReviewPath } = review;
|
|
8
|
+
const { loadReview, flushReview } = review;
|
|
9
|
+
|
|
10
|
+
const createReviewController = () => {
|
|
11
11
|
const state = {
|
|
12
12
|
store: null,
|
|
13
13
|
didResume: false,
|
|
@@ -21,15 +21,11 @@ const createReviewController = (options) => {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
const init = (dir, date, extra = {}) => {
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const resolved = resolveReviewPath(dir, date, names, {
|
|
28
|
-
forceNew,
|
|
29
|
-
readFileSync: io.readFileSync,
|
|
30
|
-
});
|
|
24
|
+
const names = listReviewNames(dir);
|
|
25
|
+
const templates = loadTemplates(dir);
|
|
26
|
+
const resolved = resolveReviewPath(dir, date, names, extra);
|
|
31
27
|
if (resolved.resume) {
|
|
32
|
-
state.store = loadReview(resolved.reviewPath, templates
|
|
28
|
+
state.store = loadReview(resolved.reviewPath, templates);
|
|
33
29
|
state.didResume = true;
|
|
34
30
|
return state.store;
|
|
35
31
|
}
|
|
@@ -49,13 +45,7 @@ const createReviewController = (options) => {
|
|
|
49
45
|
const flush = (force = false) => {
|
|
50
46
|
if (!state.store) return { ok: true, wrote: false };
|
|
51
47
|
try {
|
|
52
|
-
const
|
|
53
|
-
const mkdirSync = io.mkdirSync;
|
|
54
|
-
const wrote = flushReview(state.store, {
|
|
55
|
-
writeFileSync,
|
|
56
|
-
mkdirSync,
|
|
57
|
-
force,
|
|
58
|
-
});
|
|
48
|
+
const wrote = flushReview(state.store, force);
|
|
59
49
|
return { ok: true, wrote };
|
|
60
50
|
} catch (error) {
|
|
61
51
|
return { ok: false, wrote: false, error };
|
|
@@ -67,22 +57,18 @@ const createReviewController = (options) => {
|
|
|
67
57
|
return flush();
|
|
68
58
|
};
|
|
69
59
|
|
|
70
|
-
return
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
60
|
+
return stateView(
|
|
61
|
+
state,
|
|
62
|
+
{
|
|
63
|
+
reset,
|
|
64
|
+
init,
|
|
65
|
+
applyImported,
|
|
66
|
+
flush,
|
|
67
|
+
finishQuit,
|
|
76
68
|
},
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
reset,
|
|
81
|
-
init,
|
|
82
|
-
applyImported,
|
|
83
|
-
flush,
|
|
84
|
-
finishQuit,
|
|
85
|
-
};
|
|
69
|
+
[],
|
|
70
|
+
['store', 'didResume', 'importedRemote'],
|
|
71
|
+
);
|
|
86
72
|
};
|
|
87
73
|
|
|
88
74
|
module.exports = { createReviewController };
|
package/lib/session/terminal.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { presentRows, presentCursor } = require('../render.js');
|
|
4
|
-
const { listen, watchResize } = require('../
|
|
3
|
+
const { presentRows, presentCursor } = require('../render/render.js');
|
|
4
|
+
const { listen, watchResize } = require('../utilities.js');
|
|
5
5
|
|
|
6
6
|
const ENTER_TERM = '\x1b[?1049h\x1b[?25l\x1b[?7l\x1b[?1002h\x1b[?1006h';
|
|
7
7
|
const LEAVE_TERM =
|
|
@@ -18,14 +18,16 @@ const hasTimer = (api, name) => api.timers.has(name);
|
|
|
18
18
|
const stopTimer = (api, name) => {
|
|
19
19
|
const id = api.timers.get(name);
|
|
20
20
|
if (id === undefined) return;
|
|
21
|
-
|
|
21
|
+
clearInterval(id);
|
|
22
22
|
api.timers.delete(name);
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
const startTimer = (api, name, fn, ms) => {
|
|
26
26
|
if (api.state.disposed) return;
|
|
27
27
|
if (api.timers.has(name)) return;
|
|
28
|
-
|
|
28
|
+
const id = setInterval(fn, ms);
|
|
29
|
+
if (id && typeof id.unref === 'function') id.unref();
|
|
30
|
+
api.timers.set(name, id);
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
const restartTimer = (api, name, fn, ms) => {
|
|
@@ -118,8 +120,6 @@ const createTerminal = (options) => {
|
|
|
118
120
|
stdin: options.stdin ?? null,
|
|
119
121
|
stdout: options.stdout,
|
|
120
122
|
proc: options.proc ?? process,
|
|
121
|
-
setIntervalFn: options.setInterval ?? setInterval,
|
|
122
|
-
clearIntervalFn: options.clearInterval ?? clearInterval,
|
|
123
123
|
timers: new Map(),
|
|
124
124
|
disposers: [],
|
|
125
125
|
state,
|
|
@@ -131,18 +131,12 @@ const createTerminal = (options) => {
|
|
|
131
131
|
get lastSize() {
|
|
132
132
|
return state.lastSize;
|
|
133
133
|
},
|
|
134
|
-
get lastPresentedCursor() {
|
|
135
|
-
return state.lastPresentedCursor;
|
|
136
|
-
},
|
|
137
134
|
get carry() {
|
|
138
135
|
return state.carry;
|
|
139
136
|
},
|
|
140
137
|
set carry(value) {
|
|
141
138
|
state.carry = value;
|
|
142
139
|
},
|
|
143
|
-
get isOpen() {
|
|
144
|
-
return state.open;
|
|
145
|
-
},
|
|
146
140
|
get disposed() {
|
|
147
141
|
return state.disposed;
|
|
148
142
|
},
|
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',
|
|
@@ -48,17 +48,19 @@ const isDraftCompose = (api) => {
|
|
|
48
48
|
const todoTexts = (api) => {
|
|
49
49
|
const nav = api.nav;
|
|
50
50
|
if (!nav.todoOpen) return [];
|
|
51
|
-
const
|
|
51
|
+
const todos = listTodos(api);
|
|
52
|
+
const texts = new Array(todos.length + 1);
|
|
52
53
|
const { state } = api;
|
|
53
54
|
const editingTodo = state.composeKind === 'todo';
|
|
54
55
|
const editingId = editingTodo ? state.composeTodoId : null;
|
|
55
|
-
for (
|
|
56
|
+
for (let i = 0; i < todos.length; i++) {
|
|
57
|
+
const todo = todos[i];
|
|
56
58
|
const live = editingId === todo.id && state.editor;
|
|
57
59
|
const text = live ? state.editor.text : todo.text;
|
|
58
|
-
texts
|
|
60
|
+
texts[i] = checkLabel(text, todo.done);
|
|
59
61
|
}
|
|
60
62
|
const draft = isDraftCompose(api) && state.editor ? state.editor.text : '';
|
|
61
|
-
texts.
|
|
63
|
+
texts[todos.length] = checkLabel(draft, false);
|
|
62
64
|
return texts;
|
|
63
65
|
};
|
|
64
66
|
|
|
@@ -99,6 +101,7 @@ const closeTodoPage = (api) => {
|
|
|
99
101
|
api.nav.todoOpen = false;
|
|
100
102
|
api.ui.syncReviewPath();
|
|
101
103
|
api.ui.syncFileCursor();
|
|
104
|
+
api.ui.lifecycle.catchUpWatch();
|
|
102
105
|
};
|
|
103
106
|
|
|
104
107
|
const focusTodoPage = (api, todoId) => {
|
|
@@ -126,10 +129,7 @@ const editFocusedTodo = (api) => {
|
|
|
126
129
|
if (!api.nav.todoOpen) return;
|
|
127
130
|
const todos = listTodos(api);
|
|
128
131
|
const todo = todos[clampedTodoFocus(api)];
|
|
129
|
-
if (!todo)
|
|
130
|
-
startDraftCompose(api);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
132
|
+
if (!todo) return void startDraftCompose(api);
|
|
133
133
|
api.open('todo', todo.text, todo.id);
|
|
134
134
|
};
|
|
135
135
|
|
|
@@ -190,6 +190,17 @@ const removeFocusedTodo = (api) => {
|
|
|
190
190
|
api.ui.flushReview(true);
|
|
191
191
|
};
|
|
192
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
|
+
|
|
193
204
|
const onTodoHit = (api, cursor) => {
|
|
194
205
|
if (!api.nav.todoOpen) return;
|
|
195
206
|
const todos = listTodos(api);
|
|
@@ -207,6 +218,20 @@ const onTodoHit = (api, cursor) => {
|
|
|
207
218
|
editFocusedTodo(api);
|
|
208
219
|
};
|
|
209
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
|
+
|
|
210
235
|
const createTodo = (api) => {
|
|
211
236
|
startDraftCompose(api);
|
|
212
237
|
};
|
|
@@ -234,10 +259,7 @@ const commitTodo = (api, text) => {
|
|
|
234
259
|
const id = api.state.composeTodoId;
|
|
235
260
|
const todos = notes.todos;
|
|
236
261
|
const prev = id === null ? null : todos.find((entry) => entry.id === id);
|
|
237
|
-
if (prev)
|
|
238
|
-
setTodoText(notes, id, text);
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
262
|
+
if (prev) return void setTodoText(notes, id, text);
|
|
241
263
|
if (!text.trim()) return;
|
|
242
264
|
const todo = addTodo(notes, TODO_FILE, text);
|
|
243
265
|
api.state.composeTodoId = todo.id;
|
|
@@ -257,6 +279,8 @@ const createTodoCompose = (api) => ({
|
|
|
257
279
|
typeIntoTodo: (key) => typeIntoTodo(api, key),
|
|
258
280
|
removeFocusedTodo: () => removeFocusedTodo(api),
|
|
259
281
|
onTodoHit: (cursor) => onTodoHit(api, cursor),
|
|
282
|
+
onTodoCheck: (cursor) => onTodoCheck(api, cursor),
|
|
283
|
+
toggleFocusedTodo: () => toggleFocusedTodo(api),
|
|
260
284
|
onTodo: () => onTodo(api),
|
|
261
285
|
todoEditView: () => todoEditView(api),
|
|
262
286
|
commitTodo: (text) => commitTodo(api, text),
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const files = require('../files.js');
|
|
4
|
+
const { listPath, isTodosEntry } = files;
|
|
5
|
+
const diff = require('../diff/diff.js');
|
|
6
|
+
const { unitLines, itemsForPath, blockLineIndex } = diff;
|
|
7
|
+
const { hunkLineRange, sameHunk, hunkKey } = diff;
|
|
8
|
+
const { fileOffset, splitEditor, mergeEditRows, fileLineIndex } = diff;
|
|
9
|
+
|
|
10
|
+
const fileText = (ui, rel) => {
|
|
11
|
+
if (!rel) return '';
|
|
12
|
+
const repo = ui.repo;
|
|
13
|
+
if (repo && typeof repo.fileText === 'function') {
|
|
14
|
+
return repo.fileText(ui.top, rel, ui.rev);
|
|
15
|
+
}
|
|
16
|
+
return '';
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const pathItems = (ui, rel) => itemsForPath(ui.items ?? [], rel);
|
|
20
|
+
|
|
21
|
+
const linesFor = (
|
|
22
|
+
ui,
|
|
23
|
+
rel,
|
|
24
|
+
overlay = ui.composer.liveFileText(),
|
|
25
|
+
base = null,
|
|
26
|
+
) => {
|
|
27
|
+
const text = overlay === null ? fileText(ui, rel) : overlay;
|
|
28
|
+
const group = pathItems(ui, rel);
|
|
29
|
+
if (overlay !== null && base && base.length) {
|
|
30
|
+
return mergeEditRows(base, splitEditor(text));
|
|
31
|
+
}
|
|
32
|
+
return unitLines(text, group);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const BODY_SHIFT = 1;
|
|
36
|
+
|
|
37
|
+
const clampUnitLine = (nav, length) => {
|
|
38
|
+
if (!length) {
|
|
39
|
+
nav.unitLine = 0;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (nav.unitLine < 0) nav.unitLine = 0;
|
|
43
|
+
if (nav.unitLine >= length) nav.unitLine = length - 1;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const CONTEXT = 1;
|
|
47
|
+
|
|
48
|
+
const frameBodyH = (frame, ui) => {
|
|
49
|
+
if (frame && frame.bodyH) return frame.bodyH;
|
|
50
|
+
const size = ui?.lastSize ?? ui?.getSize?.();
|
|
51
|
+
const height = size && size.height ? size.height : 24;
|
|
52
|
+
return Math.max(1, height - 4);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const ensureRangeVisible = (nav, firstLine, lastLine, frame, ui) => {
|
|
56
|
+
const bodyH = frameBodyH(frame, ui);
|
|
57
|
+
const diffFirst = firstLine + BODY_SHIFT;
|
|
58
|
+
const diffLast = lastLine + BODY_SHIFT;
|
|
59
|
+
const diffSpan = diffLast - diffFirst + 1;
|
|
60
|
+
let first = diffFirst;
|
|
61
|
+
let last = diffLast;
|
|
62
|
+
if (diffSpan <= bodyH) {
|
|
63
|
+
const padFirst = Math.max(0, firstLine - CONTEXT) + BODY_SHIFT;
|
|
64
|
+
const padLast = lastLine + CONTEXT + BODY_SHIFT;
|
|
65
|
+
if (padLast - padFirst + 1 <= bodyH) {
|
|
66
|
+
first = padFirst;
|
|
67
|
+
last = padLast;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const span = last - first + 1;
|
|
71
|
+
let scroll = nav.scroll ?? 0;
|
|
72
|
+
if (span > bodyH) {
|
|
73
|
+
scroll = first;
|
|
74
|
+
} else {
|
|
75
|
+
if (first < scroll) scroll = first;
|
|
76
|
+
const viewLast = scroll + bodyH - 1;
|
|
77
|
+
if (last > viewLast) scroll = last - bodyH + 1;
|
|
78
|
+
}
|
|
79
|
+
nav.scroll = Math.max(0, scroll);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const ensureVisible = (nav, length, frame) => {
|
|
83
|
+
clampUnitLine(nav, length);
|
|
84
|
+
ensureRangeVisible(nav, nav.unitLine, nav.unitLine, frame);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const followFileCursor = (nav, editor, frame, rows) => {
|
|
88
|
+
if (!editor) return;
|
|
89
|
+
const fileLine = editor.linePos().line;
|
|
90
|
+
if (rows && rows.length) {
|
|
91
|
+
nav.unitLine = fileLineIndex(rows, fileLine);
|
|
92
|
+
ensureVisible(nav, rows.length, frame);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const length = `${editor.text}`.split('\n').length;
|
|
96
|
+
nav.unitLine = fileLine;
|
|
97
|
+
ensureVisible(nav, length, frame);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const jumpToItem = (ui, item) => {
|
|
101
|
+
const nav = ui.nav;
|
|
102
|
+
const rel = nav.reviewPath;
|
|
103
|
+
const lines = linesFor(ui, rel);
|
|
104
|
+
const items = ui.items ?? [];
|
|
105
|
+
const idx = items.indexOf(item);
|
|
106
|
+
if (idx >= 0) nav.index = idx;
|
|
107
|
+
const range = hunkLineRange(lines, item);
|
|
108
|
+
nav.unitLine = range.start;
|
|
109
|
+
ensureRangeVisible(nav, range.start, range.end, ui.lastFrame, ui);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const viewLine = (nav, length) => {
|
|
113
|
+
if (!length) return 0;
|
|
114
|
+
const top = Math.max(0, (nav.scroll ?? 0) - 1);
|
|
115
|
+
return Math.min(top, length - 1);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const moveUnitBlock = (ui, step) => {
|
|
119
|
+
const nav = ui.nav;
|
|
120
|
+
const rel = nav.reviewPath;
|
|
121
|
+
if (!rel) return;
|
|
122
|
+
const items = ui.items ?? [];
|
|
123
|
+
const indexes = [];
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
for (let i = 0; i < items.length; i++) {
|
|
126
|
+
if (listPath(items[i]) !== rel) continue;
|
|
127
|
+
const key = hunkKey(items[i]);
|
|
128
|
+
if (seen.has(key)) continue;
|
|
129
|
+
seen.add(key);
|
|
130
|
+
indexes.push(i);
|
|
131
|
+
}
|
|
132
|
+
if (!indexes.length) {
|
|
133
|
+
ui.status = 'not a diff block';
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
let at = -1;
|
|
137
|
+
const current = items[nav.index];
|
|
138
|
+
for (let i = 0; i < indexes.length; i++) {
|
|
139
|
+
if (!sameHunk(items[indexes[i]], current)) continue;
|
|
140
|
+
at = i;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
if (at < 0) at = step > 0 ? -1 : 0;
|
|
144
|
+
const next = at + step;
|
|
145
|
+
if (next < 0 || next >= indexes.length) return;
|
|
146
|
+
const item = items[indexes[next]];
|
|
147
|
+
jumpToItem(ui, item);
|
|
148
|
+
ui.status = '';
|
|
149
|
+
nav.clearSelection();
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const openUnitFile = (ui, entry) => {
|
|
153
|
+
if (!entry || isTodosEntry(entry)) return;
|
|
154
|
+
const nav = ui.nav;
|
|
155
|
+
nav.reviewPath = entry.path;
|
|
156
|
+
nav.todoOpen = false;
|
|
157
|
+
nav.pane = 'unit';
|
|
158
|
+
nav.scroll = 0;
|
|
159
|
+
nav.unitLine = 0;
|
|
160
|
+
nav.clearSelection();
|
|
161
|
+
const items = ui.items ?? [];
|
|
162
|
+
const group = pathItems(ui, entry.path);
|
|
163
|
+
if (group.length) {
|
|
164
|
+
const want = items[entry.openIndex] ?? items[entry.firstIndex] ?? group[0];
|
|
165
|
+
const item = group.includes(want) ? want : group[0];
|
|
166
|
+
jumpToItem(ui, item);
|
|
167
|
+
} else {
|
|
168
|
+
nav.unitLine = 0;
|
|
169
|
+
}
|
|
170
|
+
ui.status = '';
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const setFileScope = (ui, next) => {
|
|
174
|
+
if (ui.nav.pane !== 'files') return;
|
|
175
|
+
if (ui.nav.fileScope === next) return;
|
|
176
|
+
ui.nav.fileScope = next;
|
|
177
|
+
ui.nav.restoreFileCursor(ui.fileList());
|
|
178
|
+
ui.status = next === 'file' ? 'file scope' : 'diff mode';
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const editCursor = (ui) => {
|
|
182
|
+
const rel = ui.nav.reviewPath;
|
|
183
|
+
const text = fileText(ui, rel);
|
|
184
|
+
const group = pathItems(ui, rel);
|
|
185
|
+
const lines = unitLines(text, group);
|
|
186
|
+
const items = ui.items ?? [];
|
|
187
|
+
const item = items[ui.nav.index];
|
|
188
|
+
const atBlock = item && listPath(item) === rel;
|
|
189
|
+
const at = atBlock
|
|
190
|
+
? blockLineIndex(lines, item)
|
|
191
|
+
: viewLine(ui.nav, lines.length);
|
|
192
|
+
return {
|
|
193
|
+
text,
|
|
194
|
+
cursor: fileOffset(lines, at),
|
|
195
|
+
rows: lines,
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
module.exports = {
|
|
200
|
+
fileText,
|
|
201
|
+
linesFor,
|
|
202
|
+
moveUnitBlock,
|
|
203
|
+
openUnitFile,
|
|
204
|
+
setFileScope,
|
|
205
|
+
editCursor,
|
|
206
|
+
ensureVisible,
|
|
207
|
+
followFileCursor,
|
|
208
|
+
};
|