reslop 0.2.3 → 0.2.5

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 (89) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +8 -4
  3. package/bin/reslop.js +4 -2
  4. package/lib/api.js +2 -1
  5. package/lib/capabilities.js +0 -4
  6. package/lib/cli.js +19 -10
  7. package/lib/clipboard.js +45 -4
  8. package/lib/deps-diff.js +61 -118
  9. package/lib/deps.js +166 -235
  10. package/lib/detect.js +4 -2
  11. package/lib/diff/align.js +2 -1
  12. package/lib/diff/diff.js +6 -3
  13. package/lib/diff/display.js +2 -1
  14. package/lib/diff/inline.js +68 -2
  15. package/lib/diff/patch.js +2 -1
  16. package/lib/diff/unit.js +15 -30
  17. package/lib/editor.js +91 -13
  18. package/lib/files.js +54 -27
  19. package/lib/git-branches.js +107 -154
  20. package/lib/git-deps.js +78 -155
  21. package/lib/git-worktree.js +508 -0
  22. package/lib/git.js +23 -580
  23. package/lib/github.js +9 -8
  24. package/lib/gitlab.js +5 -4
  25. package/lib/highlight/bash.js +2 -1
  26. package/lib/highlight/core.js +11 -0
  27. package/lib/highlight/css.js +2 -1
  28. package/lib/highlight/csv.js +2 -1
  29. package/lib/highlight/dot.js +3 -12
  30. package/lib/highlight/html.js +2 -1
  31. package/lib/highlight/json.js +2 -1
  32. package/lib/highlight/log.js +5 -11
  33. package/lib/highlight/txt.js +3 -12
  34. package/lib/highlight.js +2 -1
  35. package/lib/keys.js +22 -12
  36. package/lib/manifest.js +4 -6
  37. package/lib/npm-commands.js +82 -24
  38. package/lib/remote.js +21 -20
  39. package/lib/render/branches.js +9 -27
  40. package/lib/render/chrome.js +109 -35
  41. package/lib/render/commits.js +216 -216
  42. package/lib/render/diff.js +160 -47
  43. package/lib/render/files.js +9 -26
  44. package/lib/render/frame.js +97 -118
  45. package/lib/render/notes.js +74 -26
  46. package/lib/render/npm.js +104 -30
  47. package/lib/render/present.js +2 -1
  48. package/lib/render/primitives.js +128 -52
  49. package/lib/render/render.js +2 -1
  50. package/lib/render/unit.js +17 -11
  51. package/lib/report-model.js +6 -4
  52. package/lib/report-parse.js +8 -14
  53. package/lib/report-render.js +38 -21
  54. package/lib/report-run.js +12 -11
  55. package/lib/report-test.js +45 -51
  56. package/lib/report-tools.js +44 -25
  57. package/lib/review-merge.js +102 -131
  58. package/lib/review.js +69 -78
  59. package/lib/select.js +52 -0
  60. package/lib/session/actions.js +72 -87
  61. package/lib/session/branches.js +18 -52
  62. package/lib/session/change-actions.js +175 -147
  63. package/lib/session/commit.js +65 -61
  64. package/lib/session/compose.js +152 -83
  65. package/lib/session/dispatch.js +53 -46
  66. package/lib/session/edit-click.js +57 -0
  67. package/lib/session/files-pane.js +30 -51
  68. package/lib/session/items.js +73 -90
  69. package/lib/session/lifecycle.js +37 -98
  70. package/lib/session/load.js +52 -57
  71. package/lib/session/navigation.js +105 -95
  72. package/lib/session/npm.js +205 -71
  73. package/lib/session/ops.js +200 -45
  74. package/lib/session/pointer.js +33 -8
  75. package/lib/session/progress.js +9 -6
  76. package/lib/session/review.js +35 -53
  77. package/lib/session/tasks.js +292 -0
  78. package/lib/session/terminal.js +17 -19
  79. package/lib/session/unit.js +3 -4
  80. package/lib/session/update.js +76 -96
  81. package/lib/session/view.js +45 -16
  82. package/lib/session/watch.js +47 -27
  83. package/lib/session.js +66 -97
  84. package/lib/source.js +6 -3
  85. package/lib/update.js +5 -13
  86. package/lib/utilities.js +25 -3
  87. package/lib/wrap.js +18 -1
  88. package/package.json +1 -1
  89. package/lib/session/todos.js +0 -293
@@ -1,15 +1,17 @@
1
1
  'use strict';
2
2
 
3
3
  const compose = require('./compose.js');
4
- const { listedCommits } = require('./commit.js');
4
+ const commit = require('./commit.js');
5
+ const { hasStagedChanges, listedCommits } = commit;
5
6
  const { COMPOSE_LEAVE } = compose;
6
7
  const actions = require('./actions.js');
7
- const { CONFIRM_MODE, disabledActions, actionFromKey, applyConfirm } = actions;
8
-
9
- const targetHasStaged = (session) => {
10
- if (session.repo.hasStaged) return session.repo.hasStaged(session.top);
11
- return session.items.some((item) => item.origin === 'staged');
12
- };
8
+ const {
9
+ LIST_PANES,
10
+ CONFIRM_MODE,
11
+ disabledActions,
12
+ actionFromKey,
13
+ applyConfirm,
14
+ } = actions;
13
15
 
14
16
  const currentTarget = (ui) => {
15
17
  if (ui.pane === 'files') return ui.fileCursorEntry();
@@ -18,9 +20,9 @@ const currentTarget = (ui) => {
18
20
  }
19
21
  if (ui.pane === 'commits') {
20
22
  const entry = listedCommits(ui.commits.commits)[ui.commitCursor];
21
- const staged = targetHasStaged(ui);
22
- if (!entry) return { canCommit: staged };
23
- return { ...entry, canCommit: staged };
23
+ const staged = hasStagedChanges(ui);
24
+ const unstagedCurrent = !ui.rev && !ui.revShort;
25
+ return { ...entry, canCommit: staged, unstagedCurrent };
24
26
  }
25
27
  return ui.current();
26
28
  };
@@ -30,13 +32,14 @@ const createDispatcher = (ui) => {
30
32
  add: ui.changeActions.onAdd,
31
33
  unstage: ui.changeActions.onUnstage,
32
34
  revert: ui.changeActions.onRevert,
35
+ ignore: ui.changeActions.onIgnore,
33
36
  next: ui.filesPane.onNext,
34
37
  prev: ui.filesPane.onPrev,
35
38
  layout: ui.filesPane.onLayout,
36
39
  theme: ui.filesPane.onTheme,
37
40
  files: ui.filesPane.onFiles,
38
41
  feedback: ui.composer.onFeedback,
39
- todo: ui.composer.onTodo,
42
+ tasks: ui.composer.onTasks,
40
43
  code: ui.composer.onCode,
41
44
  reload: ui.lifecycle.onReload,
42
45
  commit: ui.commits.onCommit,
@@ -62,12 +65,16 @@ const createDispatcher = (ui) => {
62
65
  npm: ui.npm.open,
63
66
  npmEdit: ui.npm.edit,
64
67
  npmNew: ui.npm.create,
68
+ npmDrop: () => ui.npm.askDrop(),
69
+ npmLogs: () => ui.npm.askLogs(),
65
70
  npmStop: () => ui.npm.closeView(),
71
+ back: () => ui.filesPane.onEscape(),
72
+ npmVerbose: () => ui.npm.toggleVerbose(),
66
73
  npmRerun: () => ui.npm.rerun(),
67
- check: () => ui.composer.toggleFocusedTodo(),
68
- removeTodo: () => {
74
+ check: () => ui.composer.toggleFocusedTask(),
75
+ removeTask: () => {
69
76
  if (ui.pane === 'npm') return void ui.npm.askDrop();
70
- ui.composer.removeFocusedTodo();
77
+ ui.composer.removeFocusedTask();
71
78
  },
72
79
  quit: () => ui.onQuit(),
73
80
  scrollUp: () => ui.filesPane.onScroll(-1),
@@ -88,21 +95,9 @@ const createDispatcher = (ui) => {
88
95
  if (result && result.kind === 'error') return;
89
96
  }
90
97
  const pane = ui.pane;
91
- if (pane === 'files') {
92
- if (action === 'next') return void ui.filesPane.onFileMove(1);
93
- if (action === 'prev') return void ui.filesPane.onFileMove(-1);
94
- }
95
- if (pane === 'branches') {
96
- if (action === 'next') return void ui.gitBranches.onBranchMove(1);
97
- if (action === 'prev') return void ui.gitBranches.onBranchMove(-1);
98
- }
99
- if (pane === 'commits') {
100
- if (action === 'next') return void ui.commits.onCommitMove(1);
101
- if (action === 'prev') return void ui.commits.onCommitMove(-1);
102
- }
103
- if (pane === 'npm') {
104
- if (action === 'next') return void ui.npm.move(1);
105
- if (action === 'prev') return void ui.npm.move(-1);
98
+ if (LIST_PANES.includes(pane) && (action === 'next' || action === 'prev')) {
99
+ ui.filesPane.onScroll(action === 'next' ? 1 : -1);
100
+ return;
106
101
  }
107
102
  const hidden = disabledActions(pane, currentTarget(ui));
108
103
  if (hidden.includes(action)) return;
@@ -122,8 +117,8 @@ const createInput = (ui) => {
122
117
  cancel: () => ui.cancelQuit(),
123
118
  },
124
119
  confirmUpdate: {
125
- accept: ui.updater.accept,
126
- decline: ui.updater.decline,
120
+ accept: () => ui.updater.accept(),
121
+ decline: () => ui.updater.decline(),
127
122
  },
128
123
  confirmDrop: {
129
124
  confirm: () => {
@@ -153,30 +148,42 @@ const createInput = (ui) => {
153
148
  applyConfirm(mode, key, confirmations[mode]);
154
149
  };
155
150
 
151
+ const npmViewKeys = {
152
+ escape: () => ui.npm.closeView(),
153
+ r: () => ui.npm.rerun(),
154
+ v: () => ui.npm.toggleVerbose(),
155
+ e: () => {},
156
+ n: () => {},
157
+ };
158
+
159
+ const npmListKeys = {
160
+ 'ctrl-up': () => ui.npm.reorder(-1),
161
+ 'ctrl-down': () => ui.npm.reorder(1),
162
+ up: () => ui.npm.move(-1),
163
+ down: () => ui.npm.move(1),
164
+ };
165
+
166
+ const npmKey = (key) => {
167
+ if (ui.pane !== 'npm') return null;
168
+ if (ui.npm.viewing && Object.hasOwn(npmViewKeys, key)) {
169
+ return npmViewKeys[key];
170
+ }
171
+ return Object.hasOwn(npmListKeys, key) ? npmListKeys[key] : null;
172
+ };
173
+
156
174
  const handleKey = (key) => {
157
175
  const mode = ui.mode;
158
176
  if (CONFIRM_MODE.includes(mode)) return void confirmChoice(key);
159
177
  if (mode === 'compose') {
160
- if (key === 'ctrl-c') return void ui.onQuit();
161
178
  const command = ui.composer.handleKey(key);
162
179
  return void ui.runComposeCommand(command);
163
180
  }
164
- if (ui.pane === 'npm' && ui.npm.viewing) {
165
- if (key === 'escape') return void ui.npm.closeView();
166
- if (key === 'r') return void ui.npm.rerun();
167
- if (key === 'e' || key === 'n') return;
168
- }
169
- if (ui.pane === 'npm' && key === 'ctrl-up') return void ui.npm.reorder(-1);
170
- if (ui.pane === 'npm' && key === 'ctrl-down') {
171
- return void ui.npm.reorder(1);
172
- }
173
- if (ui.pane === 'npm' && (key === 'up' || key === 'down')) {
174
- return void ui.npm.move(key === 'up' ? -1 : 1);
175
- }
181
+ const onNpmKey = npmKey(key);
182
+ if (onNpmKey) return void onNpmKey();
176
183
  if (key === 'escape') return void ui.filesPane.onEscape();
177
184
  const toggle = key === 'x' || key === ' ';
178
- if (toggle && ui.composer.toggleFocusedTodo()) return;
179
- if (ui.composer.typeIntoTodo(key)) return;
185
+ if (toggle && ui.composer.toggleFocusedTask()) return;
186
+ if (ui.composer.typeIntoTask(key)) return;
180
187
  const action = actionFromKey(key, ui.pane, ui.fileScope);
181
188
  if (action) ui.dispatch(action);
182
189
  };
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const wrap = require('../wrap.js');
4
+ const diff = require('../render/diff.js');
5
+
6
+ const { indexAtRow } = wrap;
7
+ const { codeInnerWidth } = diff;
8
+
9
+ const hitAt = (hits, cell) => {
10
+ for (const hit of hits) {
11
+ if (hit.y !== cell.y) continue;
12
+ const x0 = hit.x0 ?? hit.textX;
13
+ if (cell.x < x0) continue;
14
+ if (hit.x1 !== undefined && cell.x >= hit.x1) continue;
15
+ return hit;
16
+ }
17
+ return null;
18
+ };
19
+
20
+ const offsetOf = (hit, cellX) => {
21
+ const shown = cellX - hit.textX;
22
+ const col = Math.max(0, shown) + (hit.scroll || 0);
23
+ const at = indexAtRow(hit.start || 0, hit.text, col);
24
+ if (hit.limit !== undefined && at > hit.limit) return hit.limit;
25
+ return at;
26
+ };
27
+
28
+ const revealCode = (ui) => {
29
+ const kind = ui.composeKind;
30
+ if (kind !== 'code' && kind !== 'file') return;
31
+ const size = ui.lastSize ?? ui.getSize();
32
+ ui.editor.reveal(codeInnerWidth(size.width, ui.layout));
33
+ };
34
+
35
+ const focusField = (ui, hit) => {
36
+ if (!hit.field || !ui.npm) return true;
37
+ if (hit.field === ui.npm.editField) return true;
38
+ return ui.npm.focusField(hit.field);
39
+ };
40
+
41
+ const placeClick = (ui, cell) => {
42
+ if (ui.mode !== 'compose' || !ui.editor) return false;
43
+ const hits = ui.lastFrame && ui.lastFrame.editHits;
44
+ if (!hits || !hits.length) return false;
45
+ const hit = hitAt(hits, cell);
46
+ if (!hit) return false;
47
+ if (!focusField(ui, hit)) {
48
+ ui.paint();
49
+ return true;
50
+ }
51
+ ui.editor.place(offsetOf(hit, cell.x));
52
+ revealCode(ui);
53
+ ui.paint();
54
+ return true;
55
+ };
56
+
57
+ module.exports = { placeClick };
@@ -3,14 +3,17 @@
3
3
  const ansi = require('../ansi.js');
4
4
  const files = require('../files.js');
5
5
  const { THEME_NAMES, setTheme, themeName } = ansi;
6
- const { isTodosEntry, TODO_FILE } = files;
7
- const { logViewRows } = require('../render/npm.js');
6
+ const { isTasksEntry, TASKS_FILE } = files;
7
+ const npm = require('../render/npm.js');
8
+ const { logViewRows } = npm;
8
9
  const unit = require('./unit.js');
10
+ const actions = require('./actions.js');
11
+ const { LIST_PANES } = actions;
9
12
  const { setFileScope, openUnitFile, moveUnitBlock } = unit;
10
13
 
11
14
  const LAYOUTS = ['unified', 'mixed', 'side'];
12
15
 
13
- const current = (ui) => ui.nav.current(ui.items, ui.todoItem);
16
+ const current = (ui) => ui.nav.current(ui.items, ui.tasksItem);
14
17
 
15
18
  const clampIndex = (ui) => ui.nav.clampIndex(ui.items.length);
16
19
 
@@ -29,25 +32,19 @@ const syncFileCursor = (ui) => {
29
32
  const showFiles = (ui) => {
30
33
  const nav = ui.nav;
31
34
  const held =
32
- nav.todoOpen === true ||
35
+ nav.tasksOpen === true ||
33
36
  nav.pane === 'branches' ||
34
37
  nav.pane === 'commits' ||
35
38
  nav.pane === 'npm';
36
39
  nav.pane = 'files';
40
+ nav.resetListScroll('files');
37
41
  nav.clearSelection();
38
42
  followReviewPath(ui);
39
- nav.todoOpen = false;
43
+ nav.tasksOpen = false;
40
44
  if (held) ui.lifecycle.catchUpWatch();
41
45
  };
42
46
 
43
47
  const onEscape = (ui) => {
44
- if (
45
- ui.nav.pane === 'branches' ||
46
- ui.nav.pane === 'commits' ||
47
- ui.nav.pane === 'npm'
48
- ) {
49
- return void showFiles(ui);
50
- }
51
48
  if (ui.nav.pane === 'files') return void ui.onQuit();
52
49
  showFiles(ui);
53
50
  };
@@ -58,12 +55,12 @@ const onFiles = (ui) => {
58
55
  if (ui.nav.fileScope === 'file') {
59
56
  const list = ui.fileList();
60
57
  const entry = list[ui.nav.fileCursor];
61
- if (entry && !isTodosEntry(entry)) openUnitFile(ui, entry);
58
+ if (entry && !isTasksEntry(entry)) openUnitFile(ui, entry);
62
59
  return;
63
60
  }
64
61
  ui.nav.pane = 'diff';
65
62
  ui.nav.clearSelection();
66
- if (ui.nav.reviewPath === TODO_FILE) ui.composer.openTodoPage();
63
+ if (ui.nav.reviewPath === TASKS_FILE) ui.composer.openTasksPage();
67
64
  return;
68
65
  }
69
66
  showFiles(ui);
@@ -76,18 +73,18 @@ const onOpenFile = (ui) => {
76
73
  if (ui.nav.pane === 'commits') return void ui.commits.onShowDiff();
77
74
  if (ui.nav.pane === 'npm') return void ui.npm.run();
78
75
  if (ui.nav.pane !== 'files') {
79
- if (ui.nav.todoOpen) ui.composer.editFocusedTodo();
76
+ if (ui.nav.tasksOpen) ui.composer.editFocusedTask();
80
77
  return;
81
78
  }
82
79
  const list = ui.fileList();
83
80
  const entry = list[ui.nav.fileCursor];
84
81
  if (!entry) return;
85
- if (isTodosEntry(entry)) {
86
- ui.composer.openTodoPage();
82
+ if (isTasksEntry(entry)) {
83
+ ui.composer.openTasksPage();
87
84
  ui.status = '';
88
85
  return;
89
86
  }
90
- ui.nav.todoOpen = false;
87
+ ui.nav.tasksOpen = false;
91
88
  ui.nav.index = entry.openIndex ?? entry.firstIndex;
92
89
  if (ui.nav.fileScope === 'file') {
93
90
  return void openUnitFile(ui, entry);
@@ -111,8 +108,8 @@ const onFileMove = (ui, delta) => {
111
108
  if (next === ui.nav.fileCursor) return;
112
109
  ui.nav.fileCursor = next;
113
110
  const entry = list[next];
114
- if (isTodosEntry(entry)) {
115
- ui.nav.reviewPath = TODO_FILE;
111
+ if (isTasksEntry(entry)) {
112
+ ui.nav.reviewPath = TASKS_FILE;
116
113
  } else {
117
114
  ui.nav.reviewPath = entry.path;
118
115
  ui.nav.index = entry.firstIndex;
@@ -137,8 +134,8 @@ const moveRemaining = (ui, step) => {
137
134
 
138
135
  const onNext = (ui) => {
139
136
  if (ui.nav.pane === 'unit') return void moveUnitBlock(ui, 1);
140
- if (ui.nav.todoOpen) {
141
- ui.composer.closeTodoPage();
137
+ if (ui.nav.tasksOpen) {
138
+ ui.composer.closeTasksPage();
142
139
  if (ui.items.length) ui.nav.index = 0;
143
140
  ui.nav.scroll = 0;
144
141
  syncReviewPath(ui);
@@ -150,8 +147,8 @@ const onNext = (ui) => {
150
147
 
151
148
  const onPrev = (ui) => {
152
149
  if (ui.nav.pane === 'unit') return void moveUnitBlock(ui, -1);
153
- if (ui.nav.todoOpen) return;
154
- if (ui.nav.index <= 0) return void ui.composer.openTodoPage();
150
+ if (ui.nav.tasksOpen) return;
151
+ if (ui.nav.index <= 0) return void ui.composer.openTasksPage();
155
152
  moveRemaining(ui, -1);
156
153
  };
157
154
 
@@ -179,8 +176,8 @@ const onScroll = (ui, delta) => {
179
176
  return void ui.commits.onCommitMove(delta);
180
177
  }
181
178
  if (ui.nav.pane === 'npm') return void ui.npm.move(delta);
182
- if (ui.nav.todoOpen && ui.composer.todoRowCount() > 1) {
183
- return void ui.composer.moveTodoFocus(delta);
179
+ if (ui.nav.tasksOpen && ui.composer.taskRowCount() > 1) {
180
+ return void ui.composer.moveTaskFocus(delta);
184
181
  }
185
182
  const frame = ui.lastFrame;
186
183
  const max = frame ? frame.scrollMax : null;
@@ -193,32 +190,15 @@ const onScroll = (ui, delta) => {
193
190
  };
194
191
 
195
192
  const onJump = (ui, toEnd) => {
196
- if (ui.nav.pane === 'files') {
197
- const list = ui.fileList();
198
- if (!list.length) {
199
- ui.status = 'nothing to review';
200
- return;
201
- }
202
- const target = toEnd ? list.length - 1 : 0;
203
- return void onFileMove(ui, target - ui.nav.fileCursor);
204
- }
205
- if (ui.nav.pane === 'branches') {
206
- const delta = toEnd ? Number.MAX_SAFE_INTEGER : -Number.MAX_SAFE_INTEGER;
207
- return void ui.gitBranches.onBranchMove(delta);
208
- }
209
- if (ui.nav.pane === 'commits') {
210
- const delta = toEnd ? Number.MAX_SAFE_INTEGER : -Number.MAX_SAFE_INTEGER;
211
- return void ui.commits.onCommitMove(delta);
212
- }
213
- if (ui.nav.pane === 'npm') {
193
+ if (LIST_PANES.includes(ui.nav.pane)) {
214
194
  const delta = toEnd ? Number.MAX_SAFE_INTEGER : -Number.MAX_SAFE_INTEGER;
215
- return void ui.npm.move(delta);
195
+ return void onScroll(ui, delta);
216
196
  }
217
- if (ui.nav.todoOpen && ui.composer.todoRowCount() > 0) {
218
- const last = ui.composer.todoRowCount() - 1;
197
+ if (ui.nav.tasksOpen && ui.composer.taskRowCount() > 0) {
198
+ const last = ui.composer.taskRowCount() - 1;
219
199
  const target = toEnd ? last : 0;
220
- const at = ui.composer.clampedTodoFocus();
221
- return void ui.composer.moveTodoFocus(target - at);
200
+ const at = ui.composer.clampedTaskFocus();
201
+ return void ui.composer.moveTaskFocus(target - at);
222
202
  }
223
203
  const frame = ui.lastFrame;
224
204
  const max = frame ? frame.scrollMax : 0;
@@ -251,7 +231,6 @@ const createFilesPane = (ui) => ({
251
231
  onEscape: () => onEscape(ui),
252
232
  onFiles: () => onFiles(ui),
253
233
  onOpenFile: () => onOpenFile(ui),
254
- onFileMove: (delta) => onFileMove(ui, delta),
255
234
  onNext: () => onNext(ui),
256
235
  onPrev: () => onPrev(ui),
257
236
  scrollStep: (fraction) => scrollStep(ui, fraction),
@@ -264,4 +243,4 @@ const createFilesPane = (ui) => ({
264
243
  onDiffScope: () => setFileScope(ui, 'diff'),
265
244
  });
266
245
 
267
- module.exports = { LAYOUTS, createFilesPane };
246
+ module.exports = { createFilesPane };
@@ -1,11 +1,11 @@
1
1
  'use strict';
2
2
 
3
- const { stateView } = require('./accessors.js');
4
-
5
3
  const files = require('../files.js');
6
- const { itemPath, TODO_FILE, isTodoItem } = files;
7
- const { feedbackKey } = require('../review.js');
8
- const { refreshIndexPatches } = require('../diff/diff.js');
4
+ const { itemPath, TASKS_FILE, isTaskItem } = files;
5
+ const review = require('../review.js');
6
+ const { feedbackKey } = review;
7
+ const diff = require('../diff/diff.js');
8
+ const { refreshIndexPatches } = diff;
9
9
 
10
10
  const depChangeKey = (item) => {
11
11
  const change = item.dep && item.dep.change;
@@ -26,7 +26,7 @@ const npmBusyKind = (item) => {
26
26
  const blockKey = (item) => {
27
27
  if (!item || !item.file) return '';
28
28
  const rel = itemPath(item);
29
- if (isTodoItem(item)) return `todo:${TODO_FILE}`;
29
+ if (isTaskItem(item)) return `todo:${TASKS_FILE}`;
30
30
  const depId = depChangeKey(item);
31
31
  if (depId) return `${rel}:dep:${depId}`;
32
32
  if (!item.hunk) {
@@ -41,13 +41,13 @@ const blockKey = (item) => {
41
41
 
42
42
  const itemKey = (item) => {
43
43
  const key = blockKey(item);
44
- if (!key || isTodoItem(item)) return key;
44
+ if (!key || isTaskItem(item)) return key;
45
45
  const origin = item.origin ?? '';
46
46
  return `${origin}:${key}`;
47
47
  };
48
48
 
49
49
  const blockLinesKey = (item) => {
50
- if (!item || !item.file || isTodoItem(item)) return '';
50
+ if (!item || !item.file || isTaskItem(item)) return '';
51
51
  if (!item.hunk) return '';
52
52
  const parts = [itemPath(item)];
53
53
  for (const line of item.hunk.lines) {
@@ -129,27 +129,25 @@ const alignLoadedItems = (prev, next) => {
129
129
  return ordered;
130
130
  };
131
131
 
132
- const restoredIndex = (items, item) => {
133
- if (!item || !item.file) return -1;
134
- const origin = item.origin ?? '';
132
+ const originOf = (item) => item.origin ?? '';
133
+
134
+ const hunkStart = (item) => (item.hunk ? item.hunk.newStart : 0);
135
+
136
+ const sameLinesIndex = (items, item) => {
135
137
  const lines = blockLinesKey(item);
136
- if (lines) {
137
- let any = -1;
138
- for (let i = 0; i < items.length; i++) {
139
- if (blockLinesKey(items[i]) !== lines) continue;
140
- if (any < 0) any = i;
141
- if ((items[i].origin ?? '') === origin) return i;
142
- }
143
- if (any >= 0) return any;
144
- }
145
- const key = itemKey(item);
146
- if (key) {
147
- const exact = items.findIndex((entry) => itemKey(entry) === key);
148
- if (exact >= 0) return exact;
138
+ if (!lines) return -1;
139
+ let fallback = -1;
140
+ for (let i = 0; i < items.length; i++) {
141
+ if (blockLinesKey(items[i]) !== lines) continue;
142
+ if (originOf(items[i]) === originOf(item)) return i;
143
+ if (fallback < 0) fallback = i;
149
144
  }
145
+ return fallback;
146
+ };
147
+
148
+ const nearestHunkIndex = (items, item) => {
150
149
  const rel = itemPath(item);
151
150
  if (!rel) return -1;
152
- const start = item.hunk ? item.hunk.newStart : 0;
153
151
  let fileMatch = -1;
154
152
  let best = -1;
155
153
  let bestDist = Infinity;
@@ -157,19 +155,27 @@ const restoredIndex = (items, item) => {
157
155
  const entry = items[i];
158
156
  if (itemPath(entry) !== rel) continue;
159
157
  if (fileMatch < 0) fileMatch = i;
160
- if ((entry.origin ?? '') !== origin) continue;
161
- const at = entry.hunk ? entry.hunk.newStart : 0;
162
- const dist = Math.abs(at - start);
158
+ if (originOf(entry) !== originOf(item)) continue;
159
+ const dist = Math.abs(hunkStart(entry) - hunkStart(item));
163
160
  if (dist >= bestDist) continue;
164
161
  bestDist = dist;
165
162
  best = i;
166
163
  }
167
- if (best >= 0) return best;
168
- return fileMatch;
164
+ return best >= 0 ? best : fileMatch;
165
+ };
166
+
167
+ const restoredIndex = (items, item) => {
168
+ if (!item || !item.file) return -1;
169
+ const byLines = sameLinesIndex(items, item);
170
+ if (byLines >= 0) return byLines;
171
+ const key = itemKey(item);
172
+ const exact = key ? items.findIndex((entry) => itemKey(entry) === key) : -1;
173
+ if (exact >= 0) return exact;
174
+ return nearestHunkIndex(items, item);
169
175
  };
170
176
 
171
177
  const itemFeedKey = (item) => {
172
- if (!item || !item.file || isTodoItem(item)) return '';
178
+ if (!item || !item.file || isTaskItem(item)) return '';
173
179
  const hunk = item.hunk;
174
180
  const depId = depChangeKey(item);
175
181
  const file = depId ? `${itemPath(item)}#${depId}` : itemPath(item);
@@ -187,88 +193,65 @@ const sameBlock = (left, right) => {
187
193
  };
188
194
 
189
195
  const canEditCode = (item) => {
190
- if (!item || isTodoItem(item)) return false;
196
+ if (!item || isTaskItem(item)) return false;
191
197
  if (!item.hunk || !item.file) return false;
192
198
  if (item.file.isBinary) return false;
193
199
  return true;
194
200
  };
195
201
 
196
- const createItemCollection = () => {
197
- const state = {
198
- items: [],
199
- dismissed: new Set(),
200
- };
202
+ class ItemCollection {
203
+ constructor() {
204
+ this.items = [];
205
+ this.dismissed = new Set();
206
+ }
201
207
 
202
- const reset = () => {
203
- state.items = [];
204
- state.dismissed = new Set();
205
- };
208
+ reset() {
209
+ this.items = [];
210
+ this.dismissed = new Set();
211
+ }
206
212
 
207
- const isDismissed = (item) => state.dismissed.has(itemKey(item));
213
+ isDismissed(item) {
214
+ return this.dismissed.has(itemKey(item));
215
+ }
208
216
 
209
- const replace = (items) => {
210
- const aligned = alignLoadedItems(state.items, items);
211
- const next = [];
212
- for (const item of aligned) {
213
- if (isDismissed(item)) continue;
214
- next.push(item);
215
- }
216
- state.items = next;
217
- return next;
218
- };
217
+ replace(items) {
218
+ const aligned = alignLoadedItems(this.items, items);
219
+ this.items = aligned.filter((item) => !this.isDismissed(item));
220
+ return this.items;
221
+ }
219
222
 
220
- const dismiss = (item) => {
221
- state.dismissed.add(itemKey(item));
222
- };
223
+ dismiss(item) {
224
+ this.dismissed.add(itemKey(item));
225
+ }
223
226
 
224
- const clearDismissed = () => {
225
- state.dismissed.clear();
226
- };
227
+ clearDismissed() {
228
+ this.dismissed.clear();
229
+ }
227
230
 
228
- const findRestoredIndex = (item) => restoredIndex(state.items, item);
231
+ findRestoredIndex(item) {
232
+ return restoredIndex(this.items, item);
233
+ }
229
234
 
230
- const keepOrigin = (item, origin) => {
235
+ keepOrigin(item, origin) {
231
236
  const next = { ...item, origin };
232
- const { length } = state.items;
233
- const replaced = new Array(length);
234
- for (let i = 0; i < length; i++) {
235
- const entry = state.items[i];
236
- replaced[i] = sameBlock(entry, item) ? next : entry;
237
- }
237
+ const replaced = this.items.map((entry) =>
238
+ sameBlock(entry, item) ? next : entry,
239
+ );
238
240
  if (next.dep) {
239
- state.items = replaced;
241
+ this.items = replaced;
240
242
  return;
241
243
  }
242
- state.items = refreshIndexPatches(replaced, next);
243
- };
244
-
245
- return stateView(
246
- state,
247
- {
248
- reset,
249
- replace,
250
- dismiss,
251
- clearDismissed,
252
- isDismissed,
253
- findRestoredIndex,
254
- keepOrigin,
255
- },
256
- ['items'],
257
- ['dismissed'],
258
- );
259
- };
244
+ this.items = refreshIndexPatches(replaced, next);
245
+ }
246
+ }
260
247
 
261
248
  module.exports = {
262
- depChangeKey,
263
249
  needsReload,
264
250
  npmBusyKind,
265
- blockKey,
266
- itemKey,
267
251
  itemFeedKey,
268
- blockLinesKey,
269
252
  alignLoadedItems,
270
253
  restoredIndex,
271
254
  sameBlock,
272
255
  canEditCode,
273
- createItemCollection,
256
+ ItemCollection,
274
257
  };