diffstalker 0.2.3 → 0.2.4

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 (46) hide show
  1. package/.dependency-cruiser.cjs +2 -2
  2. package/dist/App.js +278 -758
  3. package/dist/KeyBindings.js +103 -91
  4. package/dist/ModalController.js +166 -0
  5. package/dist/MouseHandlers.js +37 -30
  6. package/dist/NavigationController.js +290 -0
  7. package/dist/StagingOperations.js +199 -0
  8. package/dist/config.js +39 -0
  9. package/dist/core/CompareManager.js +134 -0
  10. package/dist/core/ExplorerStateManager.js +7 -3
  11. package/dist/core/GitStateManager.js +28 -771
  12. package/dist/core/HistoryManager.js +72 -0
  13. package/dist/core/RemoteOperationManager.js +109 -0
  14. package/dist/core/WorkingTreeManager.js +412 -0
  15. package/dist/index.js +57 -57
  16. package/dist/state/FocusRing.js +40 -0
  17. package/dist/state/UIState.js +82 -48
  18. package/dist/ui/PaneRenderers.js +3 -6
  19. package/dist/ui/modals/BaseBranchPicker.js +4 -7
  20. package/dist/ui/modals/CommitActionConfirm.js +4 -4
  21. package/dist/ui/modals/DiscardConfirm.js +4 -7
  22. package/dist/ui/modals/FileFinder.js +3 -6
  23. package/dist/ui/modals/HotkeysModal.js +17 -21
  24. package/dist/ui/modals/Modal.js +1 -0
  25. package/dist/ui/modals/RepoPicker.js +109 -0
  26. package/dist/ui/modals/ThemePicker.js +4 -7
  27. package/dist/ui/widgets/CommitPanel.js +26 -94
  28. package/dist/ui/widgets/CompareListView.js +1 -11
  29. package/dist/ui/widgets/DiffView.js +2 -27
  30. package/dist/ui/widgets/ExplorerContent.js +1 -4
  31. package/dist/ui/widgets/ExplorerView.js +1 -11
  32. package/dist/ui/widgets/FileList.js +2 -8
  33. package/dist/ui/widgets/Footer.js +1 -0
  34. package/dist/utils/ansi.js +38 -0
  35. package/dist/utils/ansiTruncate.js +1 -5
  36. package/dist/utils/displayRows.js +72 -59
  37. package/dist/utils/fileCategories.js +7 -0
  38. package/dist/utils/fileResolution.js +23 -0
  39. package/dist/utils/languageDetection.js +3 -2
  40. package/dist/utils/logger.js +32 -0
  41. package/metrics/v0.2.4.json +236 -0
  42. package/package.json +1 -1
  43. package/dist/ui/modals/BranchPicker.js +0 -157
  44. package/dist/ui/modals/SoftResetConfirm.js +0 -68
  45. package/dist/ui/modals/StashListModal.js +0 -98
  46. package/dist/utils/layoutCalculations.js +0 -100
@@ -0,0 +1,236 @@
1
+ {
2
+ "timestamp": "2026-03-06T05:12:44.045Z",
3
+ "gitRef": "v0.2.4",
4
+ "gitSha": "0677167",
5
+ "summary": {
6
+ "files": 91,
7
+ "lines": 16342,
8
+ "functions": 1163,
9
+ "avgCyclomaticComplexity": 4.9,
10
+ "maxCyclomaticComplexity": {
11
+ "value": 34,
12
+ "function": "reconcileSelectionAfterStateChange",
13
+ "file": "src/App.ts:538"
14
+ },
15
+ "avgCognitiveComplexity": 6.1,
16
+ "maxCognitiveComplexity": {
17
+ "value": 41,
18
+ "function": "(anonymous)",
19
+ "file": "src/git/diff.ts:517"
20
+ },
21
+ "smells": 0
22
+ },
23
+ "hotspots": [
24
+ {
25
+ "file": "src/App.ts",
26
+ "lines": 1004,
27
+ "cyclomaticMax": 34,
28
+ "cognitiveMax": 19,
29
+ "smells": 0
30
+ },
31
+ {
32
+ "file": "src/utils/flatFileList.ts",
33
+ "lines": 100,
34
+ "cyclomaticMax": 29,
35
+ "cognitiveMax": 14,
36
+ "smells": 0
37
+ },
38
+ {
39
+ "file": "src/git/diff.ts",
40
+ "lines": 656,
41
+ "cyclomaticMax": 26,
42
+ "cognitiveMax": 41,
43
+ "smells": 0
44
+ },
45
+ {
46
+ "file": "src/utils/displayRows.ts",
47
+ "lines": 614,
48
+ "cyclomaticMax": 25,
49
+ "cognitiveMax": 39,
50
+ "smells": 0
51
+ },
52
+ {
53
+ "file": "src/git/status.ts",
54
+ "lines": 392,
55
+ "cyclomaticMax": 22,
56
+ "cognitiveMax": 25,
57
+ "smells": 0
58
+ },
59
+ {
60
+ "file": "src/ui/PaneRenderers.ts",
61
+ "lines": 240,
62
+ "cyclomaticMax": 20,
63
+ "cognitiveMax": 8,
64
+ "smells": 0
65
+ },
66
+ {
67
+ "file": "src/ui/widgets/ExplorerContent.ts",
68
+ "lines": 126,
69
+ "cyclomaticMax": 20,
70
+ "cognitiveMax": 24,
71
+ "smells": 0
72
+ },
73
+ {
74
+ "file": "src/NavigationController.ts",
75
+ "lines": 352,
76
+ "cyclomaticMax": 19,
77
+ "cognitiveMax": 13,
78
+ "smells": 0
79
+ },
80
+ {
81
+ "file": "src/config.ts",
82
+ "lines": 168,
83
+ "cyclomaticMax": 18,
84
+ "cognitiveMax": 25,
85
+ "smells": 0
86
+ },
87
+ {
88
+ "file": "src/ui/widgets/CommitPanel.ts",
89
+ "lines": 133,
90
+ "cyclomaticMax": 18,
91
+ "cognitiveMax": 16,
92
+ "smells": 0
93
+ },
94
+ {
95
+ "file": "src/ui/widgets/Header.ts",
96
+ "lines": 134,
97
+ "cyclomaticMax": 18,
98
+ "cognitiveMax": 22,
99
+ "smells": 0
100
+ },
101
+ {
102
+ "file": "src/MouseHandlers.ts",
103
+ "lines": 276,
104
+ "cyclomaticMax": 17,
105
+ "cognitiveMax": 15,
106
+ "smells": 0
107
+ },
108
+ {
109
+ "file": "src/ipc/CommandServer.ts",
110
+ "lines": 266,
111
+ "cyclomaticMax": 17,
112
+ "cognitiveMax": 6,
113
+ "smells": 0
114
+ },
115
+ {
116
+ "file": "src/index.ts",
117
+ "lines": 180,
118
+ "cyclomaticMax": 16,
119
+ "cognitiveMax": 17,
120
+ "smells": 0
121
+ },
122
+ {
123
+ "file": "src/ui/widgets/ExplorerView.ts",
124
+ "lines": 245,
125
+ "cyclomaticMax": 16,
126
+ "cognitiveMax": 21,
127
+ "smells": 0
128
+ },
129
+ {
130
+ "file": "src/utils/ansiTruncate.ts",
131
+ "lines": 118,
132
+ "cyclomaticMax": 16,
133
+ "cognitiveMax": 24,
134
+ "smells": 0
135
+ },
136
+ {
137
+ "file": "src/ui/widgets/DiffView.ts",
138
+ "lines": 493,
139
+ "cyclomaticMax": 15,
140
+ "cognitiveMax": 12,
141
+ "smells": 0
142
+ },
143
+ {
144
+ "file": "src/ui/widgets/CompareListView.ts",
145
+ "lines": 383,
146
+ "cyclomaticMax": 14,
147
+ "cognitiveMax": 16,
148
+ "smells": 0
149
+ },
150
+ {
151
+ "file": "src/core/ExplorerStateManager.ts",
152
+ "lines": 735,
153
+ "cyclomaticMax": 13,
154
+ "cognitiveMax": 17,
155
+ "smells": 0
156
+ },
157
+ {
158
+ "file": "src/utils/diffRowCalculations.ts",
159
+ "lines": 136,
160
+ "cyclomaticMax": 13,
161
+ "cognitiveMax": 24,
162
+ "smells": 0
163
+ },
164
+ {
165
+ "file": "src/StagingOperations.ts",
166
+ "lines": 224,
167
+ "cyclomaticMax": 12,
168
+ "cognitiveMax": 10,
169
+ "smells": 0
170
+ },
171
+ {
172
+ "file": "src/ui/widgets/FileList.ts",
173
+ "lines": 209,
174
+ "cyclomaticMax": 12,
175
+ "cognitiveMax": 12,
176
+ "smells": 0
177
+ },
178
+ {
179
+ "file": "src/utils/lineBreaking.ts",
180
+ "lines": 114,
181
+ "cyclomaticMax": 12,
182
+ "cognitiveMax": 17,
183
+ "smells": 0
184
+ },
185
+ {
186
+ "file": "src/core/WorkingTreeManager.ts",
187
+ "lines": 515,
188
+ "cyclomaticMax": 10,
189
+ "cognitiveMax": 13,
190
+ "smells": 0
191
+ },
192
+ {
193
+ "file": "src/ui/widgets/HistoryView.ts",
194
+ "lines": 97,
195
+ "cyclomaticMax": 9,
196
+ "cognitiveMax": 12,
197
+ "smells": 0
198
+ },
199
+ {
200
+ "file": "src/utils/formatPath.ts",
201
+ "lines": 72,
202
+ "cyclomaticMax": 9,
203
+ "cognitiveMax": 11,
204
+ "smells": 0
205
+ },
206
+ {
207
+ "file": "src/utils/explorerDisplayRows.ts",
208
+ "lines": 206,
209
+ "cyclomaticMax": 8,
210
+ "cognitiveMax": 15,
211
+ "smells": 0
212
+ },
213
+ {
214
+ "file": "src/ui/modals/FileFinder.ts",
215
+ "lines": 236,
216
+ "cyclomaticMax": 7,
217
+ "cognitiveMax": 13,
218
+ "smells": 0
219
+ },
220
+ {
221
+ "file": "src/ui/modals/BaseBranchPicker.ts",
222
+ "lines": 132,
223
+ "cyclomaticMax": 6,
224
+ "cognitiveMax": 13,
225
+ "smells": 0
226
+ },
227
+ {
228
+ "file": "src/ui/modals/RepoPicker.ts",
229
+ "lines": 134,
230
+ "cyclomaticMax": 6,
231
+ "cognitiveMax": 13,
232
+ "smells": 0
233
+ }
234
+ ],
235
+ "smellsByRule": {}
236
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffstalker",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Terminal application that displays git diff/status for directories",
5
5
  "author": "yogh-io",
6
6
  "license": "MIT",
@@ -1,157 +0,0 @@
1
- import blessed from 'neo-blessed';
2
- /**
3
- * BranchPicker modal for switching or creating branches.
4
- * Text input at top for filtering; branch list below.
5
- * If typed name matches no existing branch, shows "Create: <name>" as first option.
6
- */
7
- export class BranchPicker {
8
- box;
9
- textbox;
10
- screen;
11
- branches;
12
- filteredBranches = [];
13
- selectedIndex = 0;
14
- query = '';
15
- showCreate = false;
16
- onSwitch;
17
- onCreate;
18
- onCancel;
19
- constructor(screen, branches, onSwitch, onCreate, onCancel) {
20
- this.screen = screen;
21
- this.branches = branches;
22
- this.onSwitch = onSwitch;
23
- this.onCreate = onCreate;
24
- this.onCancel = onCancel;
25
- this.filteredBranches = branches;
26
- const width = Math.min(60, screen.width - 6);
27
- const maxVisible = Math.min(branches.length + 1, 15);
28
- const height = maxVisible + 7;
29
- this.box = blessed.box({
30
- parent: screen,
31
- top: 'center',
32
- left: 'center',
33
- width,
34
- height,
35
- border: {
36
- type: 'line',
37
- },
38
- style: {
39
- border: {
40
- fg: 'cyan',
41
- },
42
- },
43
- tags: true,
44
- keys: false,
45
- });
46
- this.textbox = blessed.textarea({
47
- parent: this.box,
48
- top: 1,
49
- left: 1,
50
- width: width - 4,
51
- height: 1,
52
- inputOnFocus: true,
53
- style: {
54
- fg: 'white',
55
- bg: 'default',
56
- },
57
- });
58
- this.setupKeyHandlers();
59
- this.render();
60
- }
61
- setupKeyHandlers() {
62
- this.textbox.key(['escape'], () => {
63
- this.close();
64
- this.onCancel();
65
- });
66
- this.textbox.key(['enter'], () => {
67
- if (this.showCreate && this.selectedIndex === 0) {
68
- this.close();
69
- this.onCreate(this.query.trim());
70
- }
71
- else {
72
- const adjustedIndex = this.showCreate ? this.selectedIndex - 1 : this.selectedIndex;
73
- const branch = this.filteredBranches[adjustedIndex];
74
- if (branch && !branch.current) {
75
- this.close();
76
- this.onSwitch(branch.name);
77
- }
78
- }
79
- });
80
- this.textbox.key(['C-j', 'down'], () => {
81
- const maxIndex = this.filteredBranches.length + (this.showCreate ? 1 : 0) - 1;
82
- this.selectedIndex = Math.min(maxIndex, this.selectedIndex + 1);
83
- this.render();
84
- });
85
- this.textbox.key(['C-k', 'up'], () => {
86
- this.selectedIndex = Math.max(0, this.selectedIndex - 1);
87
- this.render();
88
- });
89
- this.textbox.on('keypress', () => {
90
- setImmediate(() => {
91
- const newQuery = this.textbox.getValue() || '';
92
- if (newQuery !== this.query) {
93
- this.query = newQuery;
94
- this.selectedIndex = 0;
95
- this.updateFilter();
96
- this.render();
97
- }
98
- });
99
- });
100
- }
101
- updateFilter() {
102
- const q = this.query.trim().toLowerCase();
103
- if (!q) {
104
- this.filteredBranches = this.branches;
105
- this.showCreate = false;
106
- }
107
- else {
108
- this.filteredBranches = this.branches.filter((b) => b.name.toLowerCase().includes(q));
109
- // Show create option if no exact match
110
- this.showCreate = !this.branches.some((b) => b.name === q);
111
- }
112
- }
113
- render() {
114
- const lines = [];
115
- lines.push('{bold}{cyan-fg}Switch / Create Branch{/cyan-fg}{/bold}');
116
- lines.push(''); // Space for input
117
- lines.push('');
118
- if (this.showCreate) {
119
- const isSelected = this.selectedIndex === 0;
120
- if (isSelected) {
121
- lines.push(`{green-fg}{bold}> Create: ${this.query.trim()}{/bold}{/green-fg}`);
122
- }
123
- else {
124
- lines.push(` {green-fg}Create: ${this.query.trim()}{/green-fg}`);
125
- }
126
- }
127
- for (let i = 0; i < this.filteredBranches.length; i++) {
128
- const branch = this.filteredBranches[i];
129
- const listIndex = this.showCreate ? i + 1 : i;
130
- const isSelected = listIndex === this.selectedIndex;
131
- let line = isSelected ? '{cyan-fg}{bold}> ' : ' ';
132
- if (branch.current) {
133
- line += '* ';
134
- }
135
- line += branch.name;
136
- if (isSelected)
137
- line += '{/bold}{/cyan-fg}';
138
- if (branch.current)
139
- line += ' {gray-fg}(current){/gray-fg}';
140
- lines.push(line);
141
- }
142
- if (this.filteredBranches.length === 0 && !this.showCreate) {
143
- lines.push('{gray-fg}No matching branches{/gray-fg}');
144
- }
145
- lines.push('');
146
- lines.push('{gray-fg}Enter: select | Esc: cancel | Ctrl+j/k: navigate{/gray-fg}');
147
- this.box.setContent(lines.join('\n'));
148
- this.screen.render();
149
- }
150
- close() {
151
- this.textbox.destroy();
152
- this.box.destroy();
153
- }
154
- focus() {
155
- this.textbox.focus();
156
- }
157
- }
@@ -1,68 +0,0 @@
1
- import blessed from 'neo-blessed';
2
- /**
3
- * SoftResetConfirm modal for confirming soft reset HEAD~1.
4
- */
5
- export class SoftResetConfirm {
6
- box;
7
- screen;
8
- onConfirm;
9
- onCancel;
10
- constructor(screen, headCommit, onConfirm, onCancel) {
11
- this.screen = screen;
12
- this.onConfirm = onConfirm;
13
- this.onCancel = onCancel;
14
- const width = Math.min(60, screen.width - 6);
15
- const height = 9;
16
- this.box = blessed.box({
17
- parent: screen,
18
- top: 'center',
19
- left: 'center',
20
- width,
21
- height,
22
- border: {
23
- type: 'line',
24
- },
25
- style: {
26
- border: {
27
- fg: 'yellow',
28
- },
29
- },
30
- tags: true,
31
- keys: true,
32
- });
33
- this.setupKeyHandlers();
34
- this.renderContent(headCommit, width);
35
- }
36
- setupKeyHandlers() {
37
- this.box.key(['y', 'Y'], () => {
38
- this.close();
39
- this.onConfirm();
40
- });
41
- this.box.key(['n', 'N', 'escape', 'q'], () => {
42
- this.close();
43
- this.onCancel();
44
- });
45
- }
46
- renderContent(commit, width) {
47
- const lines = [];
48
- const innerWidth = width - 6;
49
- lines.push('{bold}{yellow-fg} Soft Reset HEAD~1?{/yellow-fg}{/bold}');
50
- lines.push('');
51
- const msg = commit.message.length > innerWidth
52
- ? commit.message.slice(0, innerWidth - 3) + '\u2026'
53
- : commit.message;
54
- lines.push(`{yellow-fg}${commit.shortHash}{/yellow-fg} ${msg}`);
55
- lines.push('');
56
- lines.push('{gray-fg}Changes will return to staged state{/gray-fg}');
57
- lines.push('');
58
- lines.push('{gray-fg}Press {/gray-fg}{green-fg}y{/green-fg}{gray-fg} to confirm, {/gray-fg}{red-fg}n{/red-fg}{gray-fg} or Esc to cancel{/gray-fg}');
59
- this.box.setContent(lines.join('\n'));
60
- this.screen.render();
61
- }
62
- close() {
63
- this.box.destroy();
64
- }
65
- focus() {
66
- this.box.focus();
67
- }
68
- }
@@ -1,98 +0,0 @@
1
- import blessed from 'neo-blessed';
2
- /**
3
- * StashListModal shows stash entries and allows popping one.
4
- */
5
- export class StashListModal {
6
- box;
7
- screen;
8
- entries;
9
- selectedIndex = 0;
10
- onPop;
11
- onCancel;
12
- constructor(screen, entries, onPop, onCancel) {
13
- this.screen = screen;
14
- this.entries = entries;
15
- this.onPop = onPop;
16
- this.onCancel = onCancel;
17
- // Create modal box
18
- const width = Math.min(70, screen.width - 6);
19
- const maxVisible = Math.min(entries.length, 15);
20
- const height = maxVisible + 6;
21
- this.box = blessed.box({
22
- parent: screen,
23
- top: 'center',
24
- left: 'center',
25
- width,
26
- height,
27
- border: {
28
- type: 'line',
29
- },
30
- style: {
31
- border: {
32
- fg: 'cyan',
33
- },
34
- },
35
- tags: true,
36
- keys: true,
37
- scrollable: true,
38
- alwaysScroll: true,
39
- });
40
- this.setupKeyHandlers();
41
- this.render();
42
- }
43
- setupKeyHandlers() {
44
- this.box.key(['escape', 'q'], () => {
45
- this.close();
46
- this.onCancel();
47
- });
48
- this.box.key(['enter'], () => {
49
- if (this.entries.length > 0) {
50
- const index = this.entries[this.selectedIndex].index;
51
- this.close();
52
- this.onPop(index);
53
- }
54
- });
55
- this.box.key(['up', 'k'], () => {
56
- this.selectedIndex = Math.max(0, this.selectedIndex - 1);
57
- this.render();
58
- });
59
- this.box.key(['down', 'j'], () => {
60
- this.selectedIndex = Math.min(this.entries.length - 1, this.selectedIndex + 1);
61
- this.render();
62
- });
63
- }
64
- render() {
65
- const lines = [];
66
- const width = this.box.width - 4;
67
- lines.push('{bold}{cyan-fg} Stash List{/cyan-fg}{/bold}');
68
- lines.push('');
69
- if (this.entries.length === 0) {
70
- lines.push('{gray-fg}No stash entries{/gray-fg}');
71
- }
72
- else {
73
- for (let i = 0; i < this.entries.length; i++) {
74
- const entry = this.entries[i];
75
- const isSelected = i === this.selectedIndex;
76
- const msg = entry.message.length > width - 10
77
- ? entry.message.slice(0, width - 13) + '\u2026'
78
- : entry.message;
79
- if (isSelected) {
80
- lines.push(`{cyan-fg}{bold}> {${i}} ${msg}{/bold}{/cyan-fg}`);
81
- }
82
- else {
83
- lines.push(` {gray-fg}{${i}}{/gray-fg} ${msg}`);
84
- }
85
- }
86
- }
87
- lines.push('');
88
- lines.push('{gray-fg}j/k: navigate | Enter: pop | Esc: cancel{/gray-fg}');
89
- this.box.setContent(lines.join('\n'));
90
- this.screen.render();
91
- }
92
- close() {
93
- this.box.destroy();
94
- }
95
- focus() {
96
- this.box.focus();
97
- }
98
- }
@@ -1,100 +0,0 @@
1
- import { getFileListSectionCounts } from './fileCategories.js';
2
- // Re-export for backwards compatibility
3
- export { getFileListSectionCounts } from './fileCategories.js';
4
- /**
5
- * Calculate total rows for the FileList component.
6
- * Accounts for headers and spacers between sections.
7
- */
8
- export function getFileListTotalRows(files) {
9
- const { modifiedCount, untrackedCount, stagedCount } = getFileListSectionCounts(files);
10
- let rows = 0;
11
- // Modified section
12
- if (modifiedCount > 0) {
13
- rows += 1 + modifiedCount; // header + files
14
- }
15
- // Untracked section
16
- if (untrackedCount > 0) {
17
- if (modifiedCount > 0)
18
- rows += 1; // spacer
19
- rows += 1 + untrackedCount; // header + files
20
- }
21
- // Staged section
22
- if (stagedCount > 0) {
23
- if (modifiedCount > 0 || untrackedCount > 0)
24
- rows += 1; // spacer
25
- rows += 1 + stagedCount; // header + files
26
- }
27
- return rows;
28
- }
29
- /**
30
- * Calculate the heights of the top (file list) and bottom (diff/commit/etc) panes
31
- * based on the number of files and available content area.
32
- *
33
- * The top pane grows to fit files up to 40% of content height.
34
- * The bottom pane gets the remaining space.
35
- *
36
- * When flatRowCount is provided (flat view mode), uses that directly instead
37
- * of computing row count from categorized file list.
38
- */
39
- export function calculatePaneHeights(files, contentHeight, maxTopRatio = 0.4, flatRowCount) {
40
- // Calculate content rows needed for staging area
41
- const neededRows = flatRowCount !== undefined ? flatRowCount : getFileListTotalRows(files);
42
- // Minimum height of 3 (header + 2 lines for empty state)
43
- const minHeight = 3;
44
- // Maximum is maxTopRatio of content area
45
- const maxHeight = Math.floor(contentHeight * maxTopRatio);
46
- // Use the smaller of needed or max, but at least min
47
- const topHeight = Math.max(minHeight, Math.min(neededRows, maxHeight));
48
- const bottomHeight = contentHeight - topHeight;
49
- return { topPaneHeight: topHeight, bottomPaneHeight: bottomHeight };
50
- }
51
- /**
52
- * Calculate which row in the file list a file at a given index occupies.
53
- * This accounts for headers and spacers in the list.
54
- * File order: Modified → Untracked → Staged (matches FileList.tsx)
55
- */
56
- export function getRowForFileIndex(selectedIndex, modifiedCount, untrackedCount, _stagedCount) {
57
- let row = 0;
58
- // Modified section
59
- if (selectedIndex < modifiedCount) {
60
- // In modified section: header + file rows
61
- return 1 + selectedIndex;
62
- }
63
- if (modifiedCount > 0) {
64
- row += 1 + modifiedCount; // header + files
65
- }
66
- // Untracked section
67
- const untrackedStart = modifiedCount;
68
- if (selectedIndex < untrackedStart + untrackedCount) {
69
- // In untracked section
70
- const untrackedIdx = selectedIndex - untrackedStart;
71
- if (modifiedCount > 0)
72
- row += 1; // spacer
73
- return row + 1 + untrackedIdx; // header + file position
74
- }
75
- if (untrackedCount > 0) {
76
- if (modifiedCount > 0)
77
- row += 1; // spacer
78
- row += 1 + untrackedCount; // header + files
79
- }
80
- // Staged section
81
- const stagedStart = modifiedCount + untrackedCount;
82
- const stagedIdx = selectedIndex - stagedStart;
83
- if (modifiedCount > 0 || untrackedCount > 0)
84
- row += 1; // spacer
85
- return row + 1 + stagedIdx; // header + file position
86
- }
87
- /**
88
- * Calculate the scroll offset needed to keep a selected row visible.
89
- */
90
- export function calculateScrollOffset(selectedRow, currentScrollOffset, visibleHeight) {
91
- // Scroll up if selected is above visible area
92
- if (selectedRow < currentScrollOffset) {
93
- return Math.max(0, selectedRow - 1);
94
- }
95
- // Scroll down if selected is below visible area
96
- else if (selectedRow >= currentScrollOffset + visibleHeight) {
97
- return selectedRow - visibleHeight + 1;
98
- }
99
- return currentScrollOffset;
100
- }