reslop 0.1.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/README.md +2 -1
  3. package/bin/reslop.js +2 -2
  4. package/lib/ansi.js +4 -2
  5. package/lib/api.js +2 -2
  6. package/lib/capabilities.js +7 -14
  7. package/lib/cli.js +28 -47
  8. package/lib/clipboard.js +1 -1
  9. package/lib/{deps/changes.js → deps-diff.js} +309 -19
  10. package/lib/deps.js +498 -29
  11. package/lib/detect.js +1 -7
  12. package/lib/diff/align.js +7 -10
  13. package/lib/diff/diff.js +53 -0
  14. package/lib/diff/display.js +5 -4
  15. package/lib/diff/inline.js +26 -21
  16. package/lib/diff/items.js +6 -4
  17. package/lib/diff/patch.js +1 -3
  18. package/lib/diff/unit.js +392 -0
  19. package/lib/files.js +5 -8
  20. package/lib/{git/branches.js → git-branches.js} +6 -5
  21. package/lib/{git/dependencies.js → git-deps.js} +111 -38
  22. package/lib/git.js +589 -68
  23. package/lib/github.js +28 -197
  24. package/lib/gitlab.js +24 -195
  25. package/lib/highlight/bash.js +1 -3
  26. package/lib/highlight/css.js +1 -3
  27. package/lib/highlight/js.js +11 -14
  28. package/lib/highlight.js +6 -9
  29. package/lib/keys.js +14 -8
  30. package/lib/manifest.js +330 -0
  31. package/lib/remote.js +374 -0
  32. package/lib/render/branches.js +14 -68
  33. package/lib/render/chrome.js +24 -9
  34. package/lib/render/commits.js +6 -59
  35. package/lib/render/diff.js +16 -16
  36. package/lib/render/files.js +6 -34
  37. package/lib/render/frame.js +23 -12
  38. package/lib/render/notes.js +4 -3
  39. package/lib/render/primitives.js +45 -0
  40. package/lib/{render.js → render/render.js} +5 -5
  41. package/lib/render/unit.js +41 -0
  42. package/lib/review.js +750 -14
  43. package/lib/select.js +5 -10
  44. package/lib/session/accessors.js +9 -1
  45. package/lib/session/actions.js +32 -16
  46. package/lib/session/branches.js +116 -128
  47. package/lib/session/change-actions.js +202 -123
  48. package/lib/session/commit.js +105 -112
  49. package/lib/session/compose.js +158 -13
  50. package/lib/session/dispatch.js +104 -60
  51. package/lib/session/files-pane.js +142 -148
  52. package/lib/session/items.js +22 -30
  53. package/lib/session/lifecycle.js +44 -44
  54. package/lib/session/load.js +19 -33
  55. package/lib/session/navigation.js +12 -1
  56. package/lib/session/ops.js +28 -41
  57. package/lib/session/pointer.js +54 -57
  58. package/lib/session/review.js +23 -37
  59. package/lib/session/terminal.js +6 -12
  60. package/lib/session/todos.js +8 -12
  61. package/lib/session/unit.js +208 -0
  62. package/lib/session/update.js +33 -46
  63. package/lib/session/view.js +78 -9
  64. package/lib/session/watch.js +11 -19
  65. package/lib/session.js +21 -246
  66. package/lib/source.js +3 -3
  67. package/lib/update.js +40 -75
  68. package/lib/{git/proc.js → utilities.js} +75 -11
  69. package/lib/wrap.js +1 -3
  70. package/package.json +1 -1
  71. package/lib/deps/items.js +0 -134
  72. package/lib/deps/manifest.js +0 -152
  73. package/lib/deps/merge.js +0 -120
  74. package/lib/deps/proposals.js +0 -164
  75. package/lib/deps/reports.js +0 -180
  76. package/lib/deps/rows.js +0 -176
  77. package/lib/deps/scan.js +0 -138
  78. package/lib/deps/usage.js +0 -68
  79. package/lib/deps/versions.js +0 -119
  80. package/lib/diff.js +0 -33
  81. package/lib/git/changes.js +0 -164
  82. package/lib/git/read.js +0 -253
  83. package/lib/git/run.js +0 -100
  84. package/lib/npm/commands.js +0 -100
  85. package/lib/remote/changes.js +0 -37
  86. package/lib/remote/headers.js +0 -36
  87. package/lib/remote/http.js +0 -122
  88. package/lib/remote/notes.js +0 -94
  89. package/lib/review/format.js +0 -434
  90. package/lib/review/storage.js +0 -150
  91. package/lib/review/store.js +0 -178
  92. package/lib/review/templates.js +0 -49
  93. package/lib/session/origins.js +0 -7
  94. package/lib/sys.js +0 -84
@@ -1,13 +1,13 @@
1
1
  'use strict';
2
2
 
3
- const store = require('../review/store.js');
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 createReviewController = (options) => {
10
- const io = options.storage;
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 forceNew = extra.forceNew ?? false;
25
- const names = listReviewNames(dir, io.readdirSync);
26
- const templates = loadTemplates(dir, io.readFileSync);
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, io.readFileSync);
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 writeFileSync = io.writeFileSync;
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
- get store() {
72
- return state.store;
73
- },
74
- get didResume() {
75
- return state.didResume;
60
+ return stateView(
61
+ state,
62
+ {
63
+ reset,
64
+ init,
65
+ applyImported,
66
+ flush,
67
+ finishQuit,
76
68
  },
77
- get importedRemote() {
78
- return state.importedRemote;
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 };
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { presentRows, presentCursor } = require('../render.js');
4
- const { listen, watchResize } = require('../sys.js');
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
- api.clearIntervalFn(id);
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
- api.timers.set(name, api.setIntervalFn(fn, ms));
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
  },
@@ -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 texts = [];
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 (const todo of listTodos(api)) {
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.push(checkLabel(text, todo.done));
60
+ texts[i] = checkLabel(text, todo.done);
59
61
  }
60
62
  const draft = isDraftCompose(api) && state.editor ? state.editor.text : '';
61
- texts.push(checkLabel(draft, false));
63
+ texts[todos.length] = checkLabel(draft, false);
62
64
  return texts;
63
65
  };
64
66
 
@@ -126,10 +128,7 @@ const editFocusedTodo = (api) => {
126
128
  if (!api.nav.todoOpen) return;
127
129
  const todos = listTodos(api);
128
130
  const todo = todos[clampedTodoFocus(api)];
129
- if (!todo) {
130
- startDraftCompose(api);
131
- return;
132
- }
131
+ if (!todo) return void startDraftCompose(api);
133
132
  api.open('todo', todo.text, todo.id);
134
133
  };
135
134
 
@@ -234,10 +233,7 @@ const commitTodo = (api, text) => {
234
233
  const id = api.state.composeTodoId;
235
234
  const todos = notes.todos;
236
235
  const prev = id === null ? null : todos.find((entry) => entry.id === id);
237
- if (prev) {
238
- setTodoText(notes, id, text);
239
- return;
240
- }
236
+ if (prev) return void setTodoText(notes, id, text);
241
237
  if (!text.trim()) return;
242
238
  const todo = addTodo(notes, TODO_FILE, text);
243
239
  api.state.composeTodoId = todo.id;
@@ -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
+ };
@@ -1,10 +1,12 @@
1
1
  'use strict';
2
2
 
3
+ const { stateView } = require('./accessors.js');
4
+
3
5
  const update = require('../update.js');
4
6
  const { checkUpdate, installUpdate, markSkipped } = update;
5
7
 
6
8
  const optsOf = (api) => {
7
- const value = api.options.updateOpts();
9
+ const value = api.ui.updateOpts;
8
10
  if (!value || value === true) return {};
9
11
  return value;
10
12
  };
@@ -19,15 +21,16 @@ const reset = (state) => {
19
21
 
20
22
  const maybePrompt = (api) => {
21
23
  if (!api.state.offer) return;
22
- if (!api.options.didLoad()) return;
23
- if (api.options.getMode() !== 'review') return;
24
- api.options.setMode('confirmUpdate');
24
+ if (!api.ui.loader.didLoad) return;
25
+ if (api.ui.mode !== 'review') return;
26
+ api.ui.mode = 'confirmUpdate';
25
27
  };
26
28
 
27
29
  const runInstall = async (api, version) => {
28
- api.options.setStatus('updating reslop');
29
- api.options.startInstallProgress();
30
- api.options.paint();
30
+ api.ui.status = 'updating reslop';
31
+ api.ui.progressFrame = 0;
32
+ api.ui.progress.start('install');
33
+ api.ui.paint();
31
34
  let ok = false;
32
35
  try {
33
36
  await installUpdate(version, optsOf(api));
@@ -35,23 +38,23 @@ const runInstall = async (api, version) => {
35
38
  } catch {
36
39
  // ignore install errors
37
40
  }
38
- api.options.stopInstallProgress();
39
- if (api.options.isDone() || !api.options.uiOpen()) return;
40
- api.options.setStatus(ok ? 'updated' : 'update failed');
41
- api.options.paint();
41
+ api.ui.progress.stop('install');
42
+ if (api.ui.done || !api.ui.uiOpen) return;
43
+ api.ui.status = ok ? 'updated' : 'update failed';
44
+ api.ui.paint();
42
45
  };
43
46
 
44
47
  const acceptUpdate = (api) => {
45
- api.options.setMode('review');
48
+ api.ui.mode = 'review';
46
49
  api.state.offer = false;
47
50
  const version = api.state.to;
48
51
  api.state.installPromise = runInstall(api, version);
49
52
  };
50
53
 
51
54
  const declineUpdate = (api) => {
52
- api.options.setMode('review');
55
+ api.ui.mode = 'review';
53
56
  api.state.offer = false;
54
- api.options.setStatus('');
57
+ api.ui.status = '';
55
58
  markSkipped(api.state.to, optsOf(api));
56
59
  };
57
60
 
@@ -62,7 +65,7 @@ const runOpenUpdate = async (api) => {
62
65
  } catch {
63
66
  return;
64
67
  }
65
- if (api.options.isDone()) return;
68
+ if (api.ui.done) return;
66
69
  if (plan.action === 'install') {
67
70
  api.state.installPromise = runInstall(api, plan.latest);
68
71
  await api.state.installPromise;
@@ -73,17 +76,17 @@ const runOpenUpdate = async (api) => {
73
76
  api.state.to = plan.latest;
74
77
  api.state.offer = true;
75
78
  maybePrompt(api);
76
- api.options.paint();
79
+ api.ui.paint();
77
80
  };
78
81
 
79
82
  const openUpdate = (api) => {
80
- if (api.options.updateOpts() === false) return Promise.resolve();
83
+ if (api.ui.updateOpts === false) return Promise.resolve();
81
84
  if (api.state.checkPromise) return api.state.checkPromise;
82
85
  api.state.checkPromise = runOpenUpdate(api);
83
86
  return api.state.checkPromise;
84
87
  };
85
88
 
86
- const createUpdateCoordinator = (options) => {
89
+ const createUpdateCoordinator = (ui) => {
87
90
  const state = {
88
91
  offer: false,
89
92
  from: '',
@@ -91,35 +94,19 @@ const createUpdateCoordinator = (options) => {
91
94
  checkPromise: null,
92
95
  installPromise: null,
93
96
  };
94
- const api = { options, state };
95
- return {
96
- get offer() {
97
- return state.offer;
98
- },
99
- set offer(value) {
100
- state.offer = value;
101
- },
102
- get from() {
103
- return state.from;
104
- },
105
- get to() {
106
- return state.to;
97
+ const api = { ui, state };
98
+ return stateView(
99
+ state,
100
+ {
101
+ reset: () => reset(state),
102
+ maybePrompt: () => maybePrompt(api),
103
+ accept: () => acceptUpdate(api),
104
+ decline: () => declineUpdate(api),
105
+ open: () => openUpdate(api),
107
106
  },
108
- get checkPromise() {
109
- return state.checkPromise;
110
- },
111
- get installPromise() {
112
- return state.installPromise;
113
- },
114
- set installPromise(value) {
115
- state.installPromise = value;
116
- },
117
- reset: () => reset(state),
118
- maybePrompt: () => maybePrompt(api),
119
- accept: () => acceptUpdate(api),
120
- decline: () => declineUpdate(api),
121
- open: () => openUpdate(api),
122
- };
107
+ ['offer', 'installPromise'],
108
+ ['from', 'to', 'checkPromise'],
109
+ );
123
110
  };
124
111
 
125
112
  module.exports = { createUpdateCoordinator };
@@ -6,11 +6,8 @@ const files = require('../files.js');
6
6
  const { fileEntries, REPO_TODOS_LABEL } = files;
7
7
  const review = require('../review.js');
8
8
  const { noteCounts } = review;
9
-
10
- const SOURCE_KIND = {
11
- 'github-pr': 'pr',
12
- 'gitlab-mr': 'mr',
13
- };
9
+ const diff = require('../diff/diff.js');
10
+ const { unitLines, itemsForPath, splitEditor, mergeEditRows } = diff;
14
11
 
15
12
  const relabelStatus = (entries, from, to) => {
16
13
  if (!to) return entries;
@@ -43,14 +40,60 @@ const todosEntry = (counts) => ({
43
40
  unstaged: 0,
44
41
  });
45
42
 
43
+ const blankEntry = (rel) => ({
44
+ path: rel,
45
+ origins: [],
46
+ firstIndex: 0,
47
+ openIndex: 0,
48
+ remaining: 0,
49
+ added: 0,
50
+ removed: 0,
51
+ stagedAdded: 0,
52
+ stagedRemoved: 0,
53
+ unstagedAdded: 0,
54
+ unstagedRemoved: 0,
55
+ staged: 0,
56
+ unstaged: 0,
57
+ date: '',
58
+ status: '',
59
+ });
60
+
61
+ const listedNames = (api) => {
62
+ const repo = api.ui.repo;
63
+ if (!repo || typeof repo.listFiles !== 'function') return [];
64
+ try {
65
+ return repo.listFiles(api.ui.top || api.ui.cwd, api.ui.paths ?? [], {
66
+ commit: api.ui.rev,
67
+ });
68
+ } catch {
69
+ return [];
70
+ }
71
+ };
72
+
73
+ const mergeUnitFiles = (entries, names) => {
74
+ const have = new Set();
75
+ for (const entry of entries) have.add(entry.path);
76
+ const extra = [];
77
+ for (const rel of names) {
78
+ if (!rel || have.has(rel)) continue;
79
+ extra.push(blankEntry(rel));
80
+ }
81
+ const merged = [...entries, ...extra];
82
+ merged.sort((left, right) => left.path.localeCompare(right.path, 'en'));
83
+ return merged;
84
+ };
85
+
46
86
  const fileList = (api, notes = noteCounts(api.review.store)) => {
47
87
  const entries = fileEntries(api.collection.items);
48
88
  const sourceLabel = api.ui.sourceLabel;
49
89
  const from = sourceLabel ? 'pr' : 'commit';
50
90
  const to = sourceLabel || api.ui.revShort;
51
91
  const labeled = relabelStatus(entries, from, to);
52
- labeled.unshift(todosEntry(notes));
53
- return labeled;
92
+ const names = listedNames(api);
93
+ const scoped =
94
+ api.nav.fileScope === 'file' ? mergeUnitFiles(labeled, names) : labeled;
95
+ scoped.unshift(todosEntry(notes));
96
+ return scoped;
54
97
  };
55
98
 
56
99
  const counts = (api, notes = noteCounts(api.review.store)) => {
@@ -76,11 +119,34 @@ const counts = (api, notes = noteCounts(api.review.store)) => {
76
119
 
77
120
  const viewStatus = (api) => api.ui.status || api.ui.busy;
78
121
 
122
+ const unitFileText = (api) => {
123
+ const rel = api.nav.reviewPath;
124
+ if (!rel) return '';
125
+ const repo = api.ui.repo;
126
+ if (repo && typeof repo.fileText === 'function') {
127
+ return repo.fileText(api.ui.top, rel, api.ui.rev);
128
+ }
129
+ return '';
130
+ };
131
+
132
+ const unitBodyLines = (api) => {
133
+ if (api.nav.pane !== 'unit') return [];
134
+ const rel = api.nav.reviewPath;
135
+ const group = itemsForPath(api.collection.items, rel);
136
+ const live = api.composer.liveFileText();
137
+ if (live !== null) {
138
+ const base = api.composer.fileEditRows();
139
+ if (base && base.length) return mergeEditRows(base, splitEditor(live));
140
+ return mergeEditRows(unitLines(live, group), splitEditor(live));
141
+ }
142
+ return unitLines(unitFileText(api), group);
143
+ };
144
+
79
145
  const view = (api) => {
80
146
  const { composer, nav, ui, gitBranches, commits, updater } = api;
81
147
  const templates = composer.shownTemplates();
82
148
  const change = ui.change;
83
- const sourceKind = (change && SOURCE_KIND[change.source]) || '';
149
+ const sourceKind = (change && change.source) || '';
84
150
  const notes = noteCounts(api.review.store);
85
151
  return {
86
152
  item: ui.current(),
@@ -94,9 +160,12 @@ const view = (api) => {
94
160
  counts: counts(api, notes),
95
161
  selection: nav.selection,
96
162
  pane: nav.pane,
163
+ fileScope: nav.fileScope,
97
164
  files: fileList(api, notes),
98
165
  fileCursor: nav.fileCursor,
99
166
  reviewPath: nav.reviewPath,
167
+ unitLine: nav.unitLine,
168
+ unitLines: unitBodyLines(api),
100
169
  branch: ui.branch,
101
170
  branches: gitBranches.branches,
102
171
  branchCursor: nav.branchCursor,
@@ -128,4 +197,4 @@ const createView = (api) => ({
128
197
  view: () => view(api),
129
198
  });
130
199
 
131
- module.exports = { SOURCE_KIND, relabelStatus, createView };
200
+ module.exports = { relabelStatus, createView };