diffstalker 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dependency-cruiser.cjs +67 -0
- package/.githooks/pre-commit +2 -0
- package/.githooks/pre-push +15 -0
- package/.github/workflows/release.yml +8 -0
- package/README.md +43 -35
- package/bun.lock +82 -3
- package/dist/App.js +555 -552
- package/dist/FollowMode.js +85 -0
- package/dist/KeyBindings.js +228 -0
- package/dist/MouseHandlers.js +192 -0
- package/dist/core/ExplorerStateManager.js +423 -78
- package/dist/core/GitStateManager.js +260 -119
- package/dist/git/diff.js +102 -17
- package/dist/git/status.js +16 -54
- package/dist/git/test-helpers.js +67 -0
- package/dist/index.js +60 -53
- package/dist/ipc/CommandClient.js +6 -7
- package/dist/state/UIState.js +39 -4
- package/dist/ui/PaneRenderers.js +76 -0
- package/dist/ui/modals/FileFinder.js +193 -0
- package/dist/ui/modals/HotkeysModal.js +12 -3
- package/dist/ui/modals/ThemePicker.js +1 -2
- package/dist/ui/widgets/CommitPanel.js +1 -1
- package/dist/ui/widgets/CompareListView.js +123 -80
- package/dist/ui/widgets/DiffView.js +228 -180
- package/dist/ui/widgets/ExplorerContent.js +15 -28
- package/dist/ui/widgets/ExplorerView.js +148 -43
- package/dist/ui/widgets/FileList.js +62 -95
- package/dist/ui/widgets/FlatFileList.js +65 -0
- package/dist/ui/widgets/Footer.js +25 -11
- package/dist/ui/widgets/Header.js +17 -52
- package/dist/ui/widgets/fileRowFormatters.js +73 -0
- package/dist/utils/ansiTruncate.js +0 -1
- package/dist/utils/displayRows.js +101 -21
- package/dist/utils/fileCategories.js +37 -0
- package/dist/utils/fileTree.js +148 -0
- package/dist/utils/flatFileList.js +67 -0
- package/dist/utils/layoutCalculations.js +5 -3
- package/eslint.metrics.js +15 -0
- package/metrics/.gitkeep +0 -0
- package/metrics/v0.2.1.json +268 -0
- package/metrics/v0.2.2.json +229 -0
- package/package.json +9 -2
- package/dist/utils/ansiToBlessed.js +0 -125
- package/dist/utils/mouseCoordinates.js +0 -165
- package/dist/utils/rowCalculations.js +0 -246
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { formatDate } from '../../utils/formatDate.js';
|
|
2
2
|
import { formatCommitDisplay } from '../../utils/commitFormat.js';
|
|
3
|
-
import {
|
|
3
|
+
import { buildFileTree, flattenTree, buildTreePrefix } from '../../utils/fileTree.js';
|
|
4
|
+
// ANSI escape codes for raw terminal output (avoids blessed tag escaping issues)
|
|
5
|
+
const ANSI_RESET = '\x1b[0m';
|
|
6
|
+
const ANSI_BOLD = '\x1b[1m';
|
|
7
|
+
const ANSI_GRAY = '\x1b[90m';
|
|
8
|
+
const ANSI_CYAN = '\x1b[36m';
|
|
9
|
+
const ANSI_YELLOW = '\x1b[33m';
|
|
10
|
+
const ANSI_GREEN = '\x1b[32m';
|
|
11
|
+
const ANSI_RED = '\x1b[31m';
|
|
12
|
+
const ANSI_BLUE = '\x1b[34m';
|
|
13
|
+
const ANSI_MAGENTA = '\x1b[35m';
|
|
14
|
+
const ANSI_INVERSE = '\x1b[7m';
|
|
4
15
|
/**
|
|
5
16
|
* Build the list of row items for the compare list view.
|
|
6
17
|
*/
|
|
@@ -15,26 +26,29 @@ export function buildCompareListRows(commits, files, commitsExpanded = true, fil
|
|
|
15
26
|
});
|
|
16
27
|
}
|
|
17
28
|
}
|
|
18
|
-
// Files section
|
|
29
|
+
// Files section with tree view
|
|
19
30
|
if (files.length > 0) {
|
|
20
31
|
if (commits.length > 0) {
|
|
21
32
|
result.push({ type: 'spacer' });
|
|
22
33
|
}
|
|
23
34
|
result.push({ type: 'section-header', sectionType: 'files' });
|
|
24
35
|
if (filesExpanded) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
// Build tree from files
|
|
37
|
+
const tree = buildFileTree(files);
|
|
38
|
+
const treeRows = flattenTree(tree);
|
|
39
|
+
for (const treeRow of treeRows) {
|
|
40
|
+
if (treeRow.type === 'directory') {
|
|
41
|
+
result.push({ type: 'directory', treeRow });
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const file = files[treeRow.fileIndex];
|
|
45
|
+
result.push({ type: 'file', fileIndex: treeRow.fileIndex, file, treeRow });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
28
48
|
}
|
|
29
49
|
}
|
|
30
50
|
return result;
|
|
31
51
|
}
|
|
32
|
-
/**
|
|
33
|
-
* Escape blessed tags in content.
|
|
34
|
-
*/
|
|
35
|
-
function escapeContent(content) {
|
|
36
|
-
return content.replace(/\{/g, '{{').replace(/\}/g, '}}');
|
|
37
|
-
}
|
|
38
52
|
/**
|
|
39
53
|
* Format a commit row.
|
|
40
54
|
*/
|
|
@@ -45,63 +59,122 @@ function formatCommitRow(commit, isSelected, isFocused, width) {
|
|
|
45
59
|
const baseWidth = 2 + 7 + 4 + dateStr.length + 2;
|
|
46
60
|
const remainingWidth = Math.max(10, width - baseWidth);
|
|
47
61
|
const { displayMessage, displayRefs } = formatCommitDisplay(commit.message, commit.refs, remainingWidth);
|
|
48
|
-
let line =
|
|
49
|
-
line += `{yellow-fg}${commit.shortHash}{/yellow-fg} `;
|
|
62
|
+
let line = ` ${ANSI_YELLOW}${commit.shortHash}${ANSI_RESET} `;
|
|
50
63
|
if (isHighlighted) {
|
|
51
|
-
line +=
|
|
64
|
+
line += `${ANSI_CYAN}${ANSI_INVERSE}${displayMessage}${ANSI_RESET}`;
|
|
52
65
|
}
|
|
53
66
|
else {
|
|
54
|
-
line +=
|
|
67
|
+
line += displayMessage;
|
|
55
68
|
}
|
|
56
|
-
line += ` {
|
|
69
|
+
line += ` ${ANSI_GRAY}(${dateStr})${ANSI_RESET}`;
|
|
57
70
|
if (displayRefs) {
|
|
58
|
-
line += ` {
|
|
71
|
+
line += ` ${ANSI_GREEN}${displayRefs}${ANSI_RESET}`;
|
|
59
72
|
}
|
|
60
|
-
return line
|
|
73
|
+
return `{escape}${line}{/escape}`;
|
|
61
74
|
}
|
|
62
75
|
/**
|
|
63
|
-
* Format a
|
|
76
|
+
* Format a directory row in tree view.
|
|
64
77
|
*/
|
|
65
|
-
function
|
|
78
|
+
function formatDirectoryRow(treeRow, width) {
|
|
79
|
+
const prefix = buildTreePrefix(treeRow);
|
|
80
|
+
const icon = '▸ '; // Collapsed folder icon (we don't support expanding individual folders yet)
|
|
81
|
+
// Truncate name if needed
|
|
82
|
+
const maxNameLen = width - prefix.length - icon.length - 2;
|
|
83
|
+
let name = treeRow.name;
|
|
84
|
+
if (name.length > maxNameLen) {
|
|
85
|
+
name = name.slice(0, maxNameLen - 1) + '…';
|
|
86
|
+
}
|
|
87
|
+
const line = `${ANSI_GRAY}${prefix}${ANSI_RESET}${ANSI_BLUE}${icon}${name}${ANSI_RESET}`;
|
|
88
|
+
return `{escape}${line}{/escape}`;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Format a file row in tree view.
|
|
92
|
+
*/
|
|
93
|
+
function formatFileRow(file, treeRow, isSelected, isFocused, width) {
|
|
66
94
|
const isHighlighted = isSelected && isFocused;
|
|
67
95
|
const isUncommitted = file.isUncommitted ?? false;
|
|
96
|
+
const prefix = buildTreePrefix(treeRow);
|
|
68
97
|
const statusColors = {
|
|
69
|
-
added:
|
|
70
|
-
modified:
|
|
71
|
-
deleted:
|
|
72
|
-
renamed:
|
|
98
|
+
added: ANSI_GREEN,
|
|
99
|
+
modified: ANSI_YELLOW,
|
|
100
|
+
deleted: ANSI_RED,
|
|
101
|
+
renamed: ANSI_BLUE,
|
|
73
102
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
103
|
+
// File icon based on status
|
|
104
|
+
const statusIcons = {
|
|
105
|
+
added: '+',
|
|
106
|
+
modified: '●',
|
|
107
|
+
deleted: '−',
|
|
108
|
+
renamed: '→',
|
|
79
109
|
};
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
110
|
+
const statusColor = isUncommitted ? ANSI_MAGENTA : statusColors[file.status];
|
|
111
|
+
const icon = statusIcons[file.status];
|
|
112
|
+
// Calculate available width for filename
|
|
113
|
+
const statsStr = `(+${file.additions} -${file.deletions})`;
|
|
114
|
+
const uncommittedStr = isUncommitted ? ' [uncommitted]' : '';
|
|
115
|
+
const fixedWidth = prefix.length + 2 + statsStr.length + uncommittedStr.length + 2;
|
|
116
|
+
const maxNameLen = Math.max(5, width - fixedWidth);
|
|
117
|
+
let name = treeRow.name;
|
|
118
|
+
if (name.length > maxNameLen) {
|
|
119
|
+
name = name.slice(0, maxNameLen - 1) + '…';
|
|
87
120
|
}
|
|
88
|
-
|
|
89
|
-
line +=
|
|
90
|
-
const displayPath = shortenPath(file.path, availableForPath);
|
|
121
|
+
let line = `${ANSI_GRAY}${prefix}${ANSI_RESET}`;
|
|
122
|
+
line += `${statusColor}${icon}${ANSI_RESET} `;
|
|
91
123
|
if (isHighlighted) {
|
|
92
|
-
line +=
|
|
124
|
+
line += `${ANSI_CYAN}${ANSI_INVERSE}${name}${ANSI_RESET}`;
|
|
93
125
|
}
|
|
94
126
|
else if (isUncommitted) {
|
|
95
|
-
line +=
|
|
127
|
+
line += `${ANSI_MAGENTA}${name}${ANSI_RESET}`;
|
|
96
128
|
}
|
|
97
129
|
else {
|
|
98
|
-
line +=
|
|
130
|
+
line += name;
|
|
99
131
|
}
|
|
100
|
-
line += ` {
|
|
132
|
+
line += ` ${ANSI_GRAY}(${ANSI_GREEN}+${file.additions}${ANSI_RESET} ${ANSI_RED}-${file.deletions}${ANSI_GRAY})${ANSI_RESET}`;
|
|
101
133
|
if (isUncommitted) {
|
|
102
|
-
line +=
|
|
134
|
+
line += ` ${ANSI_MAGENTA}[uncommitted]${ANSI_RESET}`;
|
|
103
135
|
}
|
|
104
|
-
return line
|
|
136
|
+
return `{escape}${line}{/escape}`;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Check if a row is currently selected.
|
|
140
|
+
*/
|
|
141
|
+
function isRowSelected(row, selectedItem) {
|
|
142
|
+
if (!selectedItem)
|
|
143
|
+
return false;
|
|
144
|
+
if (row.type === 'commit' && row.commitIndex !== undefined) {
|
|
145
|
+
return selectedItem.type === 'commit' && selectedItem.index === row.commitIndex;
|
|
146
|
+
}
|
|
147
|
+
if (row.type === 'file' && row.fileIndex !== undefined) {
|
|
148
|
+
return selectedItem.type === 'file' && selectedItem.index === row.fileIndex;
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Format a section header line (e.g. "▼ Commits (5)").
|
|
154
|
+
*/
|
|
155
|
+
function formatSectionHeader(label, count) {
|
|
156
|
+
return `{escape}${ANSI_CYAN}${ANSI_BOLD}▼ ${label}${ANSI_RESET} ${ANSI_GRAY}(${count})${ANSI_RESET}{/escape}`;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Format a single compare list row, returning null for unrenderable rows.
|
|
160
|
+
*/
|
|
161
|
+
function formatCompareRow(row, selectedItem, isFocused, commits, files, width) {
|
|
162
|
+
if (row.type === 'section-header') {
|
|
163
|
+
const isCommits = row.sectionType === 'commits';
|
|
164
|
+
return formatSectionHeader(isCommits ? 'Commits' : 'Files', isCommits ? commits.length : files.length);
|
|
165
|
+
}
|
|
166
|
+
if (row.type === 'spacer')
|
|
167
|
+
return '';
|
|
168
|
+
if (row.type === 'directory' && row.treeRow)
|
|
169
|
+
return formatDirectoryRow(row.treeRow, width);
|
|
170
|
+
const selected = isRowSelected(row, selectedItem);
|
|
171
|
+
if (row.type === 'commit' && row.commit && row.commitIndex !== undefined) {
|
|
172
|
+
return formatCommitRow(row.commit, selected, isFocused, width);
|
|
173
|
+
}
|
|
174
|
+
if (row.type === 'file' && row.file && row.fileIndex !== undefined && row.treeRow) {
|
|
175
|
+
return formatFileRow(row.file, row.treeRow, selected, isFocused, width);
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
105
178
|
}
|
|
106
179
|
/**
|
|
107
180
|
* Format the compare list view as blessed-compatible tagged string.
|
|
@@ -115,50 +188,20 @@ export function formatCompareListView(commits, files, selectedItem, isFocused, w
|
|
|
115
188
|
const visibleRows = maxHeight
|
|
116
189
|
? rows.slice(scrollOffset, scrollOffset + maxHeight)
|
|
117
190
|
: rows.slice(scrollOffset);
|
|
118
|
-
const lines =
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const isCommits = row.sectionType === 'commits';
|
|
122
|
-
const count = isCommits ? commits.length : files.length;
|
|
123
|
-
const label = isCommits ? 'Commits' : 'Files';
|
|
124
|
-
lines.push(`{cyan-fg}{bold}▼ ${label}{/bold}{/cyan-fg} {gray-fg}(${count}){/gray-fg}`);
|
|
125
|
-
}
|
|
126
|
-
else if (row.type === 'spacer') {
|
|
127
|
-
lines.push('');
|
|
128
|
-
}
|
|
129
|
-
else if (row.type === 'commit' && row.commit && row.commitIndex !== undefined) {
|
|
130
|
-
const isSelected = selectedItem?.type === 'commit' && selectedItem.index === row.commitIndex;
|
|
131
|
-
lines.push(formatCommitRow(row.commit, isSelected, isFocused, width));
|
|
132
|
-
}
|
|
133
|
-
else if (row.type === 'file' && row.file && row.fileIndex !== undefined) {
|
|
134
|
-
const isSelected = selectedItem?.type === 'file' && selectedItem.index === row.fileIndex;
|
|
135
|
-
lines.push(formatFileRow(row.file, isSelected, isFocused, width - 5));
|
|
136
|
-
}
|
|
137
|
-
}
|
|
191
|
+
const lines = visibleRows
|
|
192
|
+
.map((row) => formatCompareRow(row, selectedItem, isFocused, commits, files, width))
|
|
193
|
+
.filter((line) => line !== null);
|
|
138
194
|
return lines.join('\n');
|
|
139
195
|
}
|
|
140
196
|
/**
|
|
141
197
|
* Get the total number of rows in the compare list view (for scroll calculation).
|
|
142
198
|
*/
|
|
143
199
|
export function getCompareListTotalRows(commits, files, commitsExpanded = true, filesExpanded = true) {
|
|
144
|
-
|
|
145
|
-
if (commits.length > 0) {
|
|
146
|
-
count += 1; // header
|
|
147
|
-
if (commitsExpanded)
|
|
148
|
-
count += commits.length;
|
|
149
|
-
}
|
|
150
|
-
if (files.length > 0) {
|
|
151
|
-
if (commits.length > 0)
|
|
152
|
-
count += 1; // spacer
|
|
153
|
-
count += 1; // header
|
|
154
|
-
if (filesExpanded)
|
|
155
|
-
count += files.length;
|
|
156
|
-
}
|
|
157
|
-
return count;
|
|
200
|
+
return buildCompareListRows(commits, files, commitsExpanded, filesExpanded).length;
|
|
158
201
|
}
|
|
159
202
|
/**
|
|
160
203
|
* Map a row index to a selection.
|
|
161
|
-
* Returns null if the row is a header or
|
|
204
|
+
* Returns null if the row is a header, spacer, or directory.
|
|
162
205
|
*/
|
|
163
206
|
export function getCompareSelectionFromRow(rowIndex, commits, files, commitsExpanded = true, filesExpanded = true) {
|
|
164
207
|
const rows = buildCompareListRows(commits, files, commitsExpanded, filesExpanded);
|