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.
Files changed (96) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +3 -1
  3. package/bin/reslop.js +2 -2
  4. package/lib/ansi.js +148 -5
  5. package/lib/api.js +4 -2
  6. package/lib/capabilities.js +13 -14
  7. package/lib/cli.js +38 -49
  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} +191 -11
  21. package/lib/{git/dependencies.js → git-deps.js} +111 -38
  22. package/lib/git-reword-todo.js +20 -0
  23. package/lib/git.js +606 -68
  24. package/lib/github.js +28 -197
  25. package/lib/gitlab.js +24 -195
  26. package/lib/highlight/bash.js +1 -3
  27. package/lib/highlight/css.js +1 -3
  28. package/lib/highlight/js.js +11 -14
  29. package/lib/highlight.js +6 -9
  30. package/lib/keys.js +14 -8
  31. package/lib/manifest.js +330 -0
  32. package/lib/remote.js +374 -0
  33. package/lib/render/branches.js +18 -100
  34. package/lib/render/chrome.js +71 -14
  35. package/lib/render/commits.js +69 -60
  36. package/lib/render/diff.js +20 -20
  37. package/lib/render/files.js +6 -34
  38. package/lib/render/frame.js +36 -13
  39. package/lib/render/notes.js +26 -8
  40. package/lib/render/primitives.js +99 -11
  41. package/lib/{render.js → render/render.js} +8 -8
  42. package/lib/render/unit.js +41 -0
  43. package/lib/review-merge.js +270 -0
  44. package/lib/review.js +782 -14
  45. package/lib/select.js +5 -10
  46. package/lib/session/accessors.js +9 -1
  47. package/lib/session/actions.js +61 -18
  48. package/lib/session/branches.js +116 -128
  49. package/lib/session/change-actions.js +202 -123
  50. package/lib/session/commit.js +201 -115
  51. package/lib/session/compose.js +181 -15
  52. package/lib/session/dispatch.js +110 -60
  53. package/lib/session/files-pane.js +156 -148
  54. package/lib/session/items.js +22 -30
  55. package/lib/session/lifecycle.js +151 -54
  56. package/lib/session/load.js +19 -33
  57. package/lib/session/navigation.js +12 -1
  58. package/lib/session/ops.js +28 -41
  59. package/lib/session/pointer.js +57 -57
  60. package/lib/session/review.js +23 -37
  61. package/lib/session/terminal.js +6 -12
  62. package/lib/session/todos.js +37 -13
  63. package/lib/session/unit.js +208 -0
  64. package/lib/session/update.js +33 -46
  65. package/lib/session/view.js +78 -9
  66. package/lib/session/watch.js +128 -24
  67. package/lib/session.js +22 -246
  68. package/lib/source.js +3 -3
  69. package/lib/update.js +40 -75
  70. package/lib/{git/proc.js → utilities.js} +75 -11
  71. package/lib/wrap.js +1 -3
  72. package/package.json +2 -2
  73. package/lib/deps/items.js +0 -134
  74. package/lib/deps/manifest.js +0 -152
  75. package/lib/deps/merge.js +0 -120
  76. package/lib/deps/proposals.js +0 -164
  77. package/lib/deps/reports.js +0 -180
  78. package/lib/deps/rows.js +0 -176
  79. package/lib/deps/scan.js +0 -138
  80. package/lib/deps/usage.js +0 -68
  81. package/lib/deps/versions.js +0 -119
  82. package/lib/diff.js +0 -33
  83. package/lib/git/changes.js +0 -164
  84. package/lib/git/read.js +0 -253
  85. package/lib/git/run.js +0 -100
  86. package/lib/npm/commands.js +0 -100
  87. package/lib/remote/changes.js +0 -37
  88. package/lib/remote/headers.js +0 -36
  89. package/lib/remote/http.js +0 -122
  90. package/lib/remote/notes.js +0 -94
  91. package/lib/review/format.js +0 -434
  92. package/lib/review/storage.js +0 -150
  93. package/lib/review/store.js +0 -178
  94. package/lib/review/templates.js +0 -49
  95. package/lib/session/origins.js +0 -7
  96. package/lib/sys.js +0 -84
@@ -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 = (options, event) => {
7
- const size = options.getSize();
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 = (options, event) => {
14
- const nav = options.nav;
15
- const cell = mouseCell(options, event);
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 = options.lastFrame();
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 = (options, event) => {
26
- const nav = options.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(options, event),
32
+ end: mouseCell(ui, event),
32
33
  };
33
34
  };
34
35
 
35
- const clickButton = (options, cell, clickId) => {
36
- const nav = options.nav;
37
- const frame = options.lastFrame();
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
- options.dispatch(id);
42
+ ui.dispatch(id);
42
43
  }
43
44
  nav.selection = null;
44
45
  return true;
45
46
  };
46
47
 
47
- const clickHit = (options, hits, cell, onHit) => {
48
- const nav = options.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(options, hit);
58
+ onHit(ui, hit);
58
59
  nav.selection = null;
59
60
  return true;
60
61
  };
61
62
 
62
- const copySelection = (options) => {
63
- const nav = options.nav;
64
- const frame = options.lastFrame();
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 = options.copyText(text, options.stdout());
69
- options.setStatus(ok ? 'copied' : 'copy failed');
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: (options, hit) => {
77
- if (hit.id === 'branch') {
78
- options.onBranch();
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: (options) => options.getPane() === 'files',
83
+ match: (ui) => ui.pane === 'files',
86
84
  hits: 'fileHits',
87
- onHit: (options, hit) => {
88
- options.nav.fileCursor = hit.cursor;
89
- options.onOpenFile();
85
+ onHit: (ui, hit) => {
86
+ ui.nav.fileCursor = hit.cursor;
87
+ ui.filesPane.onOpenFile();
90
88
  },
91
89
  },
92
90
  {
93
- match: (options) => options.getPane() === 'branches',
91
+ match: (ui) => ui.pane === 'branches',
94
92
  hits: 'fileHits',
95
- onHit: (options, hit) => {
96
- options.nav.branchCursor = hit.cursor;
97
- options.onCheckoutBranch();
93
+ onHit: (ui, hit) => {
94
+ ui.nav.branchCursor = hit.cursor;
95
+ ui.gitBranches.onCheckoutBranch();
98
96
  },
99
97
  },
100
98
  {
101
- match: (options) => options.getPane() === 'commits',
99
+ match: (ui) => ui.pane === 'commits',
102
100
  hits: 'fileHits',
103
- onHit: (options, hit) => {
104
- options.nav.commitCursor = hit.cursor;
101
+ onHit: (ui, hit) => {
102
+ ui.nav.commitCursor = hit.cursor;
105
103
  },
106
104
  },
107
105
  {
108
- match: (options) => options.getPane() === 'diff',
106
+ match: (ui) => ui.pane === 'diff',
109
107
  hits: 'todoHits',
110
- onHit: (options, hit) => options.onTodoHit(hit.cursor),
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: (options) => options.getMode() === 'compose',
114
+ match: (ui) => ui.mode === 'compose',
114
115
  hits: 'templateHits',
115
- onHit: (options, hit) => options.selectTemplate(hit.cursor),
116
+ onHit: (ui, hit) => ui.selectTemplate(hit.cursor),
116
117
  },
117
118
  ];
118
119
 
119
- const onRelease = (options, event) => {
120
- const nav = options.nav;
121
- const cell = mouseCell(options, event);
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(options, cell, clickId)) return;
129
- const frame = options.lastFrame();
129
+ if (clickButton(ui, cell, clickId)) return;
130
+ const frame = ui.lastFrame;
130
131
  for (const target of RELEASE_TARGETS) {
131
- if (!target.match(options)) continue;
132
+ if (!target.match(ui)) continue;
132
133
  const hits = frame?.[target.hits];
133
- const opened = clickHit(options, hits, cell, target.onHit);
134
+ const opened = clickHit(ui, hits, cell, target.onHit);
134
135
  if (opened) return;
135
136
  }
136
- copySelection(options);
137
+ copySelection(ui);
137
138
  };
138
139
 
139
140
  const POINTER_HANDLERS = {
140
- wheelUp: (options) => options.onScroll(-1),
141
- wheelDown: (options) => options.onScroll(1),
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 = (options, event) => {
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(options, event);
154
+ handler(ui, event);
154
155
  };
155
156
 
156
- const createPointer = (options) => ({
157
- handle: (event) => handle(options, event),
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 };
@@ -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
  },
@@ -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 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
 
@@ -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
+ };