@fresh-editor/fresh-editor 0.1.12 → 0.1.13
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/npm-shrinkwrap.json +2 -2
- package/package.json +2 -2
- package/plugins/buffer_modified.ts +245 -0
- package/plugins/git_gutter.ts +475 -0
- package/plugins/lib/fresh.d.ts +20 -0
package/npm-shrinkwrap.json
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"hasInstallScript": true,
|
|
24
24
|
"license": "GPL-2.0",
|
|
25
25
|
"name": "@fresh-editor/fresh-editor",
|
|
26
|
-
"version": "0.1.
|
|
26
|
+
"version": "0.1.13"
|
|
27
27
|
},
|
|
28
28
|
"node_modules/@isaacs/balanced-match": {
|
|
29
29
|
"engines": {
|
|
@@ -896,5 +896,5 @@
|
|
|
896
896
|
}
|
|
897
897
|
},
|
|
898
898
|
"requires": true,
|
|
899
|
-
"version": "0.1.
|
|
899
|
+
"version": "0.1.13"
|
|
900
900
|
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"artifactDownloadUrl": "https://github.com/sinelaw/fresh/releases/download/v0.1.
|
|
2
|
+
"artifactDownloadUrl": "https://github.com/sinelaw/fresh/releases/download/v0.1.13",
|
|
3
3
|
"author": "Noam Lewis",
|
|
4
4
|
"bin": {
|
|
5
5
|
"fresh": "run-fresh.js"
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"zipExt": ".tar.xz"
|
|
93
93
|
}
|
|
94
94
|
},
|
|
95
|
-
"version": "0.1.
|
|
95
|
+
"version": "0.1.13",
|
|
96
96
|
"volta": {
|
|
97
97
|
"node": "18.14.1",
|
|
98
98
|
"npm": "9.5.0"
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/// <reference path="../types/fresh.d.ts" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Buffer Modified Plugin
|
|
5
|
+
*
|
|
6
|
+
* Shows indicators in the gutter for lines that have been modified since the last save.
|
|
7
|
+
* This tracks in-memory changes, not git changes.
|
|
8
|
+
*
|
|
9
|
+
* This plugin uses a simpler approach: it marks lines as modified when edits happen
|
|
10
|
+
* (after_insert/after_delete hooks), and clears all modified markers on save.
|
|
11
|
+
* It doesn't compare content - it just tracks which lines have been touched since save.
|
|
12
|
+
*
|
|
13
|
+
* Indicator symbols:
|
|
14
|
+
* - │ (blue): Line has been modified since last save
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Constants
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
const NAMESPACE = "buffer-modified";
|
|
22
|
+
const PRIORITY = 5; // Lower than git-gutter (10) and diagnostics
|
|
23
|
+
|
|
24
|
+
// Colors (RGB) - Blue to distinguish from git gutter (green/yellow/red)
|
|
25
|
+
const COLOR = [100, 149, 237] as [number, number, number]; // Cornflower blue
|
|
26
|
+
|
|
27
|
+
// Symbol
|
|
28
|
+
const SYMBOL = "│";
|
|
29
|
+
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Types
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
interface BufferState {
|
|
35
|
+
/** Whether we're tracking this buffer */
|
|
36
|
+
tracking: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// State
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
/** State per buffer */
|
|
44
|
+
const bufferStates: Map<number, BufferState> = new Map();
|
|
45
|
+
|
|
46
|
+
// =============================================================================
|
|
47
|
+
// Line Tracking
|
|
48
|
+
// =============================================================================
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Initialize state for a buffer (on file open)
|
|
52
|
+
* Starts with no modified lines since file was just loaded
|
|
53
|
+
*/
|
|
54
|
+
function initBufferState(bufferId: number): void {
|
|
55
|
+
bufferStates.set(bufferId, {
|
|
56
|
+
tracking: true,
|
|
57
|
+
});
|
|
58
|
+
// Clear any leftover indicators
|
|
59
|
+
editor.clearLineIndicators(bufferId, NAMESPACE);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Clear modified state for a buffer (on save)
|
|
64
|
+
* Removes all modified markers since buffer now matches disk
|
|
65
|
+
*/
|
|
66
|
+
function clearModifiedState(bufferId: number): void {
|
|
67
|
+
editor.clearLineIndicators(bufferId, NAMESPACE);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Mark a range of lines as modified and set indicators
|
|
72
|
+
*
|
|
73
|
+
* Note: The indicator markers automatically track their byte positions,
|
|
74
|
+
* so we don't need to manually track which lines are modified - we just
|
|
75
|
+
* set indicators and they'll stay on the correct lines as edits happen.
|
|
76
|
+
*/
|
|
77
|
+
function markLinesModified(bufferId: number, startLine: number, endLine: number): void {
|
|
78
|
+
const state = bufferStates.get(bufferId);
|
|
79
|
+
if (!state || !state.tracking) return;
|
|
80
|
+
|
|
81
|
+
// Add indicator for each affected line
|
|
82
|
+
// Note: If an indicator already exists at this position, it will be updated
|
|
83
|
+
for (let line = startLine; line <= endLine; line++) {
|
|
84
|
+
editor.setLineIndicator(
|
|
85
|
+
bufferId,
|
|
86
|
+
line,
|
|
87
|
+
NAMESPACE,
|
|
88
|
+
SYMBOL,
|
|
89
|
+
COLOR[0],
|
|
90
|
+
COLOR[1],
|
|
91
|
+
COLOR[2],
|
|
92
|
+
PRIORITY
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// =============================================================================
|
|
98
|
+
// Event Handlers
|
|
99
|
+
// =============================================================================
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Handle after file open - initialize state
|
|
103
|
+
*/
|
|
104
|
+
globalThis.onBufferModifiedAfterFileOpen = function (args: {
|
|
105
|
+
buffer_id: number;
|
|
106
|
+
path: string;
|
|
107
|
+
}): boolean {
|
|
108
|
+
const bufferId = args.buffer_id;
|
|
109
|
+
|
|
110
|
+
if (!args.path || args.path === "") {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Initialize tracking - file just loaded, no modifications yet
|
|
115
|
+
initBufferState(bufferId);
|
|
116
|
+
editor.debug(`Buffer Modified: initialized for ${args.path}`);
|
|
117
|
+
|
|
118
|
+
return true;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Handle buffer activation - ensure we're tracking
|
|
123
|
+
*/
|
|
124
|
+
globalThis.onBufferModifiedBufferActivated = function (args: {
|
|
125
|
+
buffer_id: number;
|
|
126
|
+
}): boolean {
|
|
127
|
+
const bufferId = args.buffer_id;
|
|
128
|
+
|
|
129
|
+
// If we don't have state yet, initialize it
|
|
130
|
+
if (!bufferStates.has(bufferId)) {
|
|
131
|
+
const filePath = editor.getBufferPath(bufferId);
|
|
132
|
+
if (filePath && filePath !== "") {
|
|
133
|
+
initBufferState(bufferId);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return true;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Handle after file save - clear modified state
|
|
142
|
+
*/
|
|
143
|
+
globalThis.onBufferModifiedAfterSave = function (args: {
|
|
144
|
+
buffer_id: number;
|
|
145
|
+
path: string;
|
|
146
|
+
}): boolean {
|
|
147
|
+
const bufferId = args.buffer_id;
|
|
148
|
+
|
|
149
|
+
// Clear all modified markers - buffer now matches disk
|
|
150
|
+
clearModifiedState(bufferId);
|
|
151
|
+
editor.debug("Buffer Modified: cleared on save");
|
|
152
|
+
|
|
153
|
+
return true;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Handle after insert - mark affected lines as modified
|
|
158
|
+
*
|
|
159
|
+
* Note: Line indicators automatically track position changes via byte-position markers.
|
|
160
|
+
* We only need to add new indicators for the modified lines; existing indicators
|
|
161
|
+
* will automatically shift to stay on the correct lines.
|
|
162
|
+
*/
|
|
163
|
+
globalThis.onBufferModifiedAfterInsert = function (args: {
|
|
164
|
+
buffer_id: number;
|
|
165
|
+
position: number;
|
|
166
|
+
text: string;
|
|
167
|
+
affected_start: number;
|
|
168
|
+
affected_end: number;
|
|
169
|
+
start_line: number;
|
|
170
|
+
end_line: number;
|
|
171
|
+
lines_added: number;
|
|
172
|
+
}): boolean {
|
|
173
|
+
const bufferId = args.buffer_id;
|
|
174
|
+
|
|
175
|
+
if (!bufferStates.has(bufferId)) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Mark all affected lines (from start_line to end_line inclusive)
|
|
180
|
+
// The indicator markers will automatically track their positions
|
|
181
|
+
markLinesModified(bufferId, args.start_line, args.end_line);
|
|
182
|
+
|
|
183
|
+
return true;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Handle after delete - mark affected line as modified
|
|
188
|
+
*
|
|
189
|
+
* Note: Line indicators automatically track position changes via byte-position markers.
|
|
190
|
+
* Markers within deleted ranges are automatically removed. We only need to mark the
|
|
191
|
+
* line where the deletion occurred.
|
|
192
|
+
*/
|
|
193
|
+
globalThis.onBufferModifiedAfterDelete = function (args: {
|
|
194
|
+
buffer_id: number;
|
|
195
|
+
range: { start: number; end: number };
|
|
196
|
+
deleted_text: string;
|
|
197
|
+
affected_start: number;
|
|
198
|
+
deleted_len: number;
|
|
199
|
+
start_line: number;
|
|
200
|
+
end_line: number;
|
|
201
|
+
lines_removed: number;
|
|
202
|
+
}): boolean {
|
|
203
|
+
const bufferId = args.buffer_id;
|
|
204
|
+
|
|
205
|
+
if (!bufferStates.has(bufferId)) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Mark the line where deletion occurred
|
|
210
|
+
// Markers for deleted lines are automatically cleaned up
|
|
211
|
+
markLinesModified(bufferId, args.start_line, args.start_line);
|
|
212
|
+
|
|
213
|
+
return true;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Handle buffer closed - cleanup state
|
|
218
|
+
*/
|
|
219
|
+
globalThis.onBufferModifiedBufferClosed = function (args: {
|
|
220
|
+
buffer_id: number;
|
|
221
|
+
}): boolean {
|
|
222
|
+
bufferStates.delete(args.buffer_id);
|
|
223
|
+
return true;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// =============================================================================
|
|
227
|
+
// Registration
|
|
228
|
+
// =============================================================================
|
|
229
|
+
|
|
230
|
+
// Register event handlers
|
|
231
|
+
editor.on("after_file_open", "onBufferModifiedAfterFileOpen");
|
|
232
|
+
editor.on("buffer_activated", "onBufferModifiedBufferActivated");
|
|
233
|
+
editor.on("after_file_save", "onBufferModifiedAfterSave");
|
|
234
|
+
editor.on("after-insert", "onBufferModifiedAfterInsert");
|
|
235
|
+
editor.on("after-delete", "onBufferModifiedAfterDelete");
|
|
236
|
+
editor.on("buffer_closed", "onBufferModifiedBufferClosed");
|
|
237
|
+
|
|
238
|
+
// Initialize for the current buffer
|
|
239
|
+
const initBufferId = editor.getActiveBufferId();
|
|
240
|
+
const initPath = editor.getBufferPath(initBufferId);
|
|
241
|
+
if (initPath && initPath !== "") {
|
|
242
|
+
initBufferState(initBufferId);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
editor.debug("Buffer Modified plugin loaded");
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/// <reference path="../types/fresh.d.ts" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Git Gutter Plugin
|
|
5
|
+
*
|
|
6
|
+
* Shows git diff indicators in the gutter for modified, added, and deleted lines.
|
|
7
|
+
* Uses `git diff` to compare the current buffer content against the index (staged changes)
|
|
8
|
+
* or HEAD if nothing is staged.
|
|
9
|
+
*
|
|
10
|
+
* Indicator symbols:
|
|
11
|
+
* - │ (green): Added line
|
|
12
|
+
* - │ (yellow): Modified line
|
|
13
|
+
* - ▾ (red): Deleted line(s) below
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Constants
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
const NAMESPACE = "git-gutter";
|
|
21
|
+
const PRIORITY = 10; // Lower than diagnostics
|
|
22
|
+
|
|
23
|
+
// Colors (RGB)
|
|
24
|
+
const COLORS = {
|
|
25
|
+
added: [80, 250, 123] as [number, number, number], // Green
|
|
26
|
+
modified: [255, 184, 108] as [number, number, number], // Orange/Yellow
|
|
27
|
+
deleted: [255, 85, 85] as [number, number, number], // Red
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Symbols
|
|
31
|
+
const SYMBOLS = {
|
|
32
|
+
added: "│",
|
|
33
|
+
modified: "│",
|
|
34
|
+
deleted: "▾",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// Types
|
|
39
|
+
// =============================================================================
|
|
40
|
+
|
|
41
|
+
interface DiffHunk {
|
|
42
|
+
/** Type of change */
|
|
43
|
+
type: "added" | "modified" | "deleted";
|
|
44
|
+
/** Starting line number in the new file (1-indexed) */
|
|
45
|
+
startLine: number;
|
|
46
|
+
/** Number of lines affected */
|
|
47
|
+
lineCount: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface BufferGitState {
|
|
51
|
+
/** File path for this buffer */
|
|
52
|
+
filePath: string;
|
|
53
|
+
/** Last known hunks for this buffer */
|
|
54
|
+
hunks: DiffHunk[];
|
|
55
|
+
/** Whether we're currently updating */
|
|
56
|
+
updating: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// =============================================================================
|
|
60
|
+
// State
|
|
61
|
+
// =============================================================================
|
|
62
|
+
|
|
63
|
+
/** Git state per buffer */
|
|
64
|
+
const bufferStates: Map<number, BufferGitState> = new Map();
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// Git Diff Parsing
|
|
69
|
+
// =============================================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse unified diff output to extract hunks
|
|
73
|
+
* Unified diff format:
|
|
74
|
+
* @@ -start,count +start,count @@
|
|
75
|
+
*/
|
|
76
|
+
function parseDiffOutput(diffOutput: string): DiffHunk[] {
|
|
77
|
+
const hunks: DiffHunk[] = [];
|
|
78
|
+
const lines = diffOutput.split("\n");
|
|
79
|
+
|
|
80
|
+
let currentOldLine = 0;
|
|
81
|
+
let currentNewLine = 0;
|
|
82
|
+
let inHunk = false;
|
|
83
|
+
let addedStart = 0;
|
|
84
|
+
let addedCount = 0;
|
|
85
|
+
let modifiedStart = 0;
|
|
86
|
+
let modifiedCount = 0;
|
|
87
|
+
let deletedAtLine = 0;
|
|
88
|
+
let deletedCount = 0;
|
|
89
|
+
|
|
90
|
+
const flushAdded = () => {
|
|
91
|
+
if (addedCount > 0) {
|
|
92
|
+
hunks.push({ type: "added", startLine: addedStart, lineCount: addedCount });
|
|
93
|
+
addedCount = 0;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const flushModified = () => {
|
|
98
|
+
if (modifiedCount > 0) {
|
|
99
|
+
hunks.push({ type: "modified", startLine: modifiedStart, lineCount: modifiedCount });
|
|
100
|
+
modifiedCount = 0;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const flushDeleted = () => {
|
|
105
|
+
if (deletedCount > 0) {
|
|
106
|
+
// Deleted lines are shown as a marker on the line after the deletion
|
|
107
|
+
hunks.push({ type: "deleted", startLine: deletedAtLine, lineCount: deletedCount });
|
|
108
|
+
deletedCount = 0;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
for (const line of lines) {
|
|
113
|
+
// Match hunk header: @@ -old_start,old_count +new_start,new_count @@
|
|
114
|
+
const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
|
115
|
+
if (hunkMatch) {
|
|
116
|
+
// Flush any pending changes from previous hunk
|
|
117
|
+
flushAdded();
|
|
118
|
+
flushModified();
|
|
119
|
+
flushDeleted();
|
|
120
|
+
|
|
121
|
+
currentOldLine = parseInt(hunkMatch[1], 10);
|
|
122
|
+
currentNewLine = parseInt(hunkMatch[3], 10);
|
|
123
|
+
inHunk = true;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!inHunk) continue;
|
|
128
|
+
|
|
129
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
130
|
+
// Added line
|
|
131
|
+
if (deletedCount > 0) {
|
|
132
|
+
// If there were deletions right before, this is a modification
|
|
133
|
+
if (modifiedCount === 0) {
|
|
134
|
+
modifiedStart = currentNewLine;
|
|
135
|
+
}
|
|
136
|
+
modifiedCount++;
|
|
137
|
+
deletedCount--;
|
|
138
|
+
} else {
|
|
139
|
+
// Pure addition
|
|
140
|
+
if (addedCount === 0) {
|
|
141
|
+
addedStart = currentNewLine;
|
|
142
|
+
}
|
|
143
|
+
addedCount++;
|
|
144
|
+
}
|
|
145
|
+
currentNewLine++;
|
|
146
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
147
|
+
// Deleted line - flush any pending additions first
|
|
148
|
+
flushAdded();
|
|
149
|
+
|
|
150
|
+
if (deletedCount === 0) {
|
|
151
|
+
deletedAtLine = currentNewLine;
|
|
152
|
+
}
|
|
153
|
+
deletedCount++;
|
|
154
|
+
currentOldLine++;
|
|
155
|
+
} else if (line.startsWith(" ")) {
|
|
156
|
+
// Context line (unchanged)
|
|
157
|
+
flushAdded();
|
|
158
|
+
flushModified();
|
|
159
|
+
flushDeleted();
|
|
160
|
+
currentOldLine++;
|
|
161
|
+
currentNewLine++;
|
|
162
|
+
} else if (line === "\") {
|
|
163
|
+
// Ignore this marker
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Flush any remaining changes
|
|
169
|
+
flushAdded();
|
|
170
|
+
flushModified();
|
|
171
|
+
flushDeleted();
|
|
172
|
+
|
|
173
|
+
return hunks;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// =============================================================================
|
|
177
|
+
// Git Operations
|
|
178
|
+
// =============================================================================
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get the directory containing a file
|
|
182
|
+
*/
|
|
183
|
+
function getFileDirectory(filePath: string): string {
|
|
184
|
+
const lastSlash = filePath.lastIndexOf("/");
|
|
185
|
+
if (lastSlash > 0) {
|
|
186
|
+
return filePath.substring(0, lastSlash);
|
|
187
|
+
}
|
|
188
|
+
return ".";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Check if a file is tracked by git
|
|
193
|
+
*/
|
|
194
|
+
async function isGitTracked(filePath: string): Promise<boolean> {
|
|
195
|
+
const cwd = getFileDirectory(filePath);
|
|
196
|
+
const result = await editor.spawnProcess("git", ["ls-files", "--error-unmatch", filePath], cwd);
|
|
197
|
+
return result.exit_code === 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get git diff for a file
|
|
202
|
+
* Compares working tree against HEAD to show all uncommitted changes
|
|
203
|
+
* (both staged and unstaged)
|
|
204
|
+
*/
|
|
205
|
+
async function getGitDiff(filePath: string): Promise<string> {
|
|
206
|
+
const cwd = getFileDirectory(filePath);
|
|
207
|
+
|
|
208
|
+
// Diff against HEAD to show all changes (staged + unstaged) vs last commit
|
|
209
|
+
const result = await editor.spawnProcess("git", [
|
|
210
|
+
"diff",
|
|
211
|
+
"HEAD",
|
|
212
|
+
"--no-color",
|
|
213
|
+
"--unified=0", // No context lines for cleaner parsing
|
|
214
|
+
"--",
|
|
215
|
+
filePath,
|
|
216
|
+
], cwd);
|
|
217
|
+
|
|
218
|
+
// Exit code 0 = no differences, 1 = differences found, >1 = error
|
|
219
|
+
if (result.exit_code <= 1) {
|
|
220
|
+
return result.stdout;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return "";
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// =============================================================================
|
|
227
|
+
// Indicator Management
|
|
228
|
+
// =============================================================================
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Update git gutter indicators for a buffer
|
|
232
|
+
*/
|
|
233
|
+
async function updateGitGutter(bufferId: number): Promise<void> {
|
|
234
|
+
const state = bufferStates.get(bufferId);
|
|
235
|
+
if (!state || state.updating) return;
|
|
236
|
+
|
|
237
|
+
state.updating = true;
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
editor.debug(`Git Gutter: updating for ${state.filePath}`);
|
|
241
|
+
|
|
242
|
+
// Check if file is git tracked
|
|
243
|
+
const tracked = await isGitTracked(state.filePath);
|
|
244
|
+
if (!tracked) {
|
|
245
|
+
// Clear indicators for non-tracked files
|
|
246
|
+
editor.debug("Git Gutter: file not tracked by git");
|
|
247
|
+
editor.clearLineIndicators(bufferId, NAMESPACE);
|
|
248
|
+
state.hunks = [];
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
editor.debug("Git Gutter: file is tracked, getting diff...");
|
|
253
|
+
|
|
254
|
+
// Get diff
|
|
255
|
+
const diffOutput = await getGitDiff(state.filePath);
|
|
256
|
+
editor.debug(`Git Gutter: diff output length = ${diffOutput.length}`);
|
|
257
|
+
if (diffOutput.length > 0 && diffOutput.length < 500) {
|
|
258
|
+
editor.debug(`Git Gutter: diff = ${diffOutput.replace(/\n/g, "\\n")}`);
|
|
259
|
+
}
|
|
260
|
+
const hunks = parseDiffOutput(diffOutput);
|
|
261
|
+
editor.debug(`Git Gutter: parsed ${hunks.length} hunks`);
|
|
262
|
+
|
|
263
|
+
// Clear existing indicators
|
|
264
|
+
editor.clearLineIndicators(bufferId, NAMESPACE);
|
|
265
|
+
|
|
266
|
+
// Apply new indicators
|
|
267
|
+
for (const hunk of hunks) {
|
|
268
|
+
const color = COLORS[hunk.type];
|
|
269
|
+
const symbol = SYMBOLS[hunk.type];
|
|
270
|
+
|
|
271
|
+
if (hunk.type === "deleted") {
|
|
272
|
+
// Deleted indicator shows on a single line
|
|
273
|
+
// Line numbers are 1-indexed in diff, but 0-indexed in editor
|
|
274
|
+
const line = Math.max(0, hunk.startLine - 1);
|
|
275
|
+
editor.setLineIndicator(
|
|
276
|
+
bufferId,
|
|
277
|
+
line,
|
|
278
|
+
NAMESPACE,
|
|
279
|
+
symbol,
|
|
280
|
+
color[0],
|
|
281
|
+
color[1],
|
|
282
|
+
color[2],
|
|
283
|
+
PRIORITY
|
|
284
|
+
);
|
|
285
|
+
} else {
|
|
286
|
+
// Added/modified indicators show on each affected line
|
|
287
|
+
for (let i = 0; i < hunk.lineCount; i++) {
|
|
288
|
+
// Line numbers are 1-indexed in diff, but 0-indexed in editor
|
|
289
|
+
const line = hunk.startLine - 1 + i;
|
|
290
|
+
editor.setLineIndicator(
|
|
291
|
+
bufferId,
|
|
292
|
+
line,
|
|
293
|
+
NAMESPACE,
|
|
294
|
+
symbol,
|
|
295
|
+
color[0],
|
|
296
|
+
color[1],
|
|
297
|
+
color[2],
|
|
298
|
+
PRIORITY
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
state.hunks = hunks;
|
|
305
|
+
} finally {
|
|
306
|
+
state.updating = false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
// =============================================================================
|
|
312
|
+
// Event Handlers
|
|
313
|
+
// =============================================================================
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Handle after file open - initialize git state and update indicators
|
|
317
|
+
*/
|
|
318
|
+
globalThis.onGitGutterAfterFileOpen = function (args: {
|
|
319
|
+
buffer_id: number;
|
|
320
|
+
path: string;
|
|
321
|
+
}): boolean {
|
|
322
|
+
const bufferId = args.buffer_id;
|
|
323
|
+
const filePath = args.path;
|
|
324
|
+
|
|
325
|
+
if (!filePath || filePath === "") {
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Initialize state for this buffer
|
|
330
|
+
bufferStates.set(bufferId, {
|
|
331
|
+
filePath,
|
|
332
|
+
hunks: [],
|
|
333
|
+
updating: false,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Update immediately (no debounce for file open)
|
|
337
|
+
updateGitGutter(bufferId);
|
|
338
|
+
|
|
339
|
+
return true;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Handle buffer activation - update if we have state but indicators might be stale
|
|
344
|
+
*/
|
|
345
|
+
globalThis.onGitGutterBufferActivated = function (args: {
|
|
346
|
+
buffer_id: number;
|
|
347
|
+
}): boolean {
|
|
348
|
+
const bufferId = args.buffer_id;
|
|
349
|
+
|
|
350
|
+
// If we don't have state yet, try to initialize from buffer path
|
|
351
|
+
if (!bufferStates.has(bufferId)) {
|
|
352
|
+
const filePath = editor.getBufferPath(bufferId);
|
|
353
|
+
if (filePath && filePath !== "") {
|
|
354
|
+
bufferStates.set(bufferId, {
|
|
355
|
+
filePath,
|
|
356
|
+
hunks: [],
|
|
357
|
+
updating: false,
|
|
358
|
+
});
|
|
359
|
+
updateGitGutter(bufferId);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// If we already have state, the indicators should be current
|
|
363
|
+
// (they update on file open and save)
|
|
364
|
+
|
|
365
|
+
return true;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Handle after file save - refresh indicators
|
|
370
|
+
*/
|
|
371
|
+
globalThis.onGitGutterAfterSave = function (args: {
|
|
372
|
+
buffer_id: number;
|
|
373
|
+
path: string;
|
|
374
|
+
}): boolean {
|
|
375
|
+
const bufferId = args.buffer_id;
|
|
376
|
+
|
|
377
|
+
// Update state with new path (in case of save-as)
|
|
378
|
+
const state = bufferStates.get(bufferId);
|
|
379
|
+
if (state) {
|
|
380
|
+
state.filePath = args.path;
|
|
381
|
+
} else {
|
|
382
|
+
bufferStates.set(bufferId, {
|
|
383
|
+
filePath: args.path,
|
|
384
|
+
hunks: [],
|
|
385
|
+
updating: false,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Update immediately after save (no debounce)
|
|
390
|
+
updateGitGutter(bufferId);
|
|
391
|
+
|
|
392
|
+
return true;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// Note: Git diff compares the file on disk, not the in-memory buffer.
|
|
396
|
+
// Line indicators automatically track position changes via byte-position markers.
|
|
397
|
+
// A full re-diff happens on save. For unsaved changes, see buffer_modified plugin.
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Handle buffer closed - cleanup state
|
|
401
|
+
*/
|
|
402
|
+
globalThis.onGitGutterBufferClosed = function (args: {
|
|
403
|
+
buffer_id: number;
|
|
404
|
+
}): boolean {
|
|
405
|
+
bufferStates.delete(args.buffer_id);
|
|
406
|
+
return true;
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
// =============================================================================
|
|
410
|
+
// Commands
|
|
411
|
+
// =============================================================================
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Manually refresh git gutter for the current buffer
|
|
415
|
+
*/
|
|
416
|
+
globalThis.git_gutter_refresh = function (): void {
|
|
417
|
+
const bufferId = editor.getActiveBufferId();
|
|
418
|
+
const filePath = editor.getBufferPath(bufferId);
|
|
419
|
+
|
|
420
|
+
if (!filePath || filePath === "") {
|
|
421
|
+
editor.setStatus("Git Gutter: No file open");
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Ensure state exists
|
|
426
|
+
if (!bufferStates.has(bufferId)) {
|
|
427
|
+
bufferStates.set(bufferId, {
|
|
428
|
+
filePath,
|
|
429
|
+
hunks: [],
|
|
430
|
+
updating: false,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Force immediate update
|
|
435
|
+
updateGitGutter(bufferId).then(() => {
|
|
436
|
+
const state = bufferStates.get(bufferId);
|
|
437
|
+
const count = state?.hunks.length || 0;
|
|
438
|
+
editor.setStatus(`Git Gutter: ${count} change${count !== 1 ? "s" : ""} detected`);
|
|
439
|
+
});
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
// =============================================================================
|
|
443
|
+
// Registration
|
|
444
|
+
// =============================================================================
|
|
445
|
+
|
|
446
|
+
// Register event handlers
|
|
447
|
+
// Note: No need to register after-insert/after-delete hooks - indicators
|
|
448
|
+
// automatically track position changes via byte-position markers in the editor.
|
|
449
|
+
editor.on("after_file_open", "onGitGutterAfterFileOpen");
|
|
450
|
+
editor.on("buffer_activated", "onGitGutterBufferActivated");
|
|
451
|
+
editor.on("after_file_save", "onGitGutterAfterSave");
|
|
452
|
+
editor.on("buffer_closed", "onGitGutterBufferClosed");
|
|
453
|
+
|
|
454
|
+
// Register commands
|
|
455
|
+
editor.registerCommand(
|
|
456
|
+
"Git Gutter: Refresh",
|
|
457
|
+
"Refresh git gutter indicators for the current buffer",
|
|
458
|
+
"git_gutter_refresh",
|
|
459
|
+
"normal"
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
// Initialize for the current buffer
|
|
463
|
+
const initBufferId = editor.getActiveBufferId();
|
|
464
|
+
const initPath = editor.getBufferPath(initBufferId);
|
|
465
|
+
if (initPath && initPath !== "") {
|
|
466
|
+
bufferStates.set(initBufferId, {
|
|
467
|
+
filePath: initPath,
|
|
468
|
+
hunks: [],
|
|
469
|
+
updating: false,
|
|
470
|
+
});
|
|
471
|
+
updateGitGutter(initBufferId);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
editor.debug("Git Gutter plugin loaded");
|
|
475
|
+
editor.setStatus("Git Gutter plugin ready");
|
package/plugins/lib/fresh.d.ts
CHANGED
|
@@ -447,6 +447,26 @@ interface EditorAPI {
|
|
|
447
447
|
* @returns true if virtual line was added
|
|
448
448
|
*/
|
|
449
449
|
addVirtualLine(buffer_id: number, position: number, text: string, fg_r: number, fg_g: number, fg_b: number, bg_r: i16, bg_g: i16, bg_b: i16, above: boolean, namespace: string, priority: number): boolean;
|
|
450
|
+
/**
|
|
451
|
+
* Set a line indicator in the gutter's indicator column
|
|
452
|
+
* @param buffer_id - The buffer ID
|
|
453
|
+
* @param line - Line number (0-indexed)
|
|
454
|
+
* @param namespace - Namespace for grouping (e.g., "git-gutter", "breakpoints")
|
|
455
|
+
* @param symbol - Symbol to display (e.g., "│", "●", "★")
|
|
456
|
+
* @param r - Red color component (0-255)
|
|
457
|
+
* @param g - Green color component (0-255)
|
|
458
|
+
* @param b - Blue color component (0-255)
|
|
459
|
+
* @param priority - Priority for display when multiple indicators exist (higher wins)
|
|
460
|
+
* @returns true if indicator was set
|
|
461
|
+
*/
|
|
462
|
+
setLineIndicator(buffer_id: number, line: number, namespace: string, symbol: string, r: number, g: number, b: number, priority: number): boolean;
|
|
463
|
+
/**
|
|
464
|
+
* Clear all line indicators for a specific namespace
|
|
465
|
+
* @param buffer_id - The buffer ID
|
|
466
|
+
* @param namespace - Namespace to clear (e.g., "git-gutter")
|
|
467
|
+
* @returns true if indicators were cleared
|
|
468
|
+
*/
|
|
469
|
+
clearLineIndicators(buffer_id: number, namespace: string): boolean;
|
|
450
470
|
/**
|
|
451
471
|
* Submit a transformed view stream for a viewport
|
|
452
472
|
* @param buffer_id - Buffer to apply the transform to
|