@involvex/fresh-editor 0.1.76 → 0.1.78
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/bin/CHANGELOG.md +1017 -0
- package/bin/LICENSE +117 -0
- package/bin/README.md +248 -0
- package/bin/fresh.exe +0 -0
- package/bin/plugins/README.md +71 -0
- package/bin/plugins/audit_mode.i18n.json +821 -0
- package/bin/plugins/audit_mode.ts +1810 -0
- package/bin/plugins/buffer_modified.i18n.json +67 -0
- package/bin/plugins/buffer_modified.ts +281 -0
- package/bin/plugins/calculator.i18n.json +93 -0
- package/bin/plugins/calculator.ts +770 -0
- package/bin/plugins/clangd-lsp.ts +168 -0
- package/bin/plugins/clangd_support.i18n.json +223 -0
- package/bin/plugins/clangd_support.md +20 -0
- package/bin/plugins/clangd_support.ts +325 -0
- package/bin/plugins/color_highlighter.i18n.json +145 -0
- package/bin/plugins/color_highlighter.ts +304 -0
- package/bin/plugins/config-schema.json +768 -0
- package/bin/plugins/csharp-lsp.ts +147 -0
- package/bin/plugins/csharp_support.i18n.json +80 -0
- package/bin/plugins/csharp_support.ts +170 -0
- package/bin/plugins/css-lsp.ts +143 -0
- package/bin/plugins/diagnostics_panel.i18n.json +236 -0
- package/bin/plugins/diagnostics_panel.ts +642 -0
- package/bin/plugins/examples/README.md +85 -0
- package/bin/plugins/examples/async_demo.ts +165 -0
- package/bin/plugins/examples/bookmarks.ts +329 -0
- package/bin/plugins/examples/buffer_query_demo.ts +110 -0
- package/bin/plugins/examples/git_grep.ts +262 -0
- package/bin/plugins/examples/hello_world.ts +93 -0
- package/bin/plugins/examples/virtual_buffer_demo.ts +116 -0
- package/bin/plugins/find_references.i18n.json +275 -0
- package/bin/plugins/find_references.ts +359 -0
- package/bin/plugins/git_blame.i18n.json +496 -0
- package/bin/plugins/git_blame.ts +707 -0
- package/bin/plugins/git_find_file.i18n.json +314 -0
- package/bin/plugins/git_find_file.ts +300 -0
- package/bin/plugins/git_grep.i18n.json +171 -0
- package/bin/plugins/git_grep.ts +191 -0
- package/bin/plugins/git_gutter.i18n.json +93 -0
- package/bin/plugins/git_gutter.ts +477 -0
- package/bin/plugins/git_log.i18n.json +481 -0
- package/bin/plugins/git_log.ts +1285 -0
- package/bin/plugins/go-lsp.ts +143 -0
- package/bin/plugins/html-lsp.ts +145 -0
- package/bin/plugins/json-lsp.ts +145 -0
- package/bin/plugins/lib/fresh.d.ts +1321 -0
- package/bin/plugins/lib/index.ts +24 -0
- package/bin/plugins/lib/navigation-controller.ts +214 -0
- package/bin/plugins/lib/panel-manager.ts +220 -0
- package/bin/plugins/lib/types.ts +72 -0
- package/bin/plugins/lib/virtual-buffer-factory.ts +130 -0
- package/bin/plugins/live_grep.i18n.json +171 -0
- package/bin/plugins/live_grep.ts +422 -0
- package/bin/plugins/markdown_compose.i18n.json +223 -0
- package/bin/plugins/markdown_compose.ts +630 -0
- package/bin/plugins/merge_conflict.i18n.json +821 -0
- package/bin/plugins/merge_conflict.ts +1810 -0
- package/bin/plugins/path_complete.i18n.json +80 -0
- package/bin/plugins/path_complete.ts +165 -0
- package/bin/plugins/python-lsp.ts +162 -0
- package/bin/plugins/rust-lsp.ts +166 -0
- package/bin/plugins/search_replace.i18n.json +405 -0
- package/bin/plugins/search_replace.ts +484 -0
- package/bin/plugins/test_i18n.i18n.json +67 -0
- package/bin/plugins/test_i18n.ts +18 -0
- package/bin/plugins/theme_editor.i18n.json +3746 -0
- package/bin/plugins/theme_editor.ts +2063 -0
- package/bin/plugins/todo_highlighter.i18n.json +184 -0
- package/bin/plugins/todo_highlighter.ts +206 -0
- package/bin/plugins/typescript-lsp.ts +167 -0
- package/bin/plugins/vi_mode.i18n.json +1549 -0
- package/bin/plugins/vi_mode.ts +2747 -0
- package/bin/plugins/welcome.i18n.json +236 -0
- package/bin/plugins/welcome.ts +76 -0
- package/bin/themes/dark.json +102 -0
- package/bin/themes/dracula.json +62 -0
- package/bin/themes/high-contrast.json +102 -0
- package/bin/themes/light.json +102 -0
- package/bin/themes/nord.json +62 -0
- package/bin/themes/nostalgia.json +102 -0
- package/bin/themes/solarized-dark.json +62 -0
- package/binary-install.js +1 -1
- package/dist/bin/fresh.js +9 -0
- package/dist/binary-install.js +149 -0
- package/dist/binary.js +30 -0
- package/dist/fresh-6yhknp07.exe +0 -0
- package/dist/install.js +158 -0
- package/dist/run-fresh.js +43 -0
- package/package.json +7 -2
|
@@ -0,0 +1,1285 @@
|
|
|
1
|
+
/// <reference path="../types/fresh.d.ts" />
|
|
2
|
+
const editor = getEditor();
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Git Log Plugin - Magit-style Git Log Interface
|
|
7
|
+
*
|
|
8
|
+
* Provides an interactive git log view with:
|
|
9
|
+
* - Syntax highlighting for hash, author, date, subject
|
|
10
|
+
* - Cursor navigation between commits
|
|
11
|
+
* - Enter to open commit details in a virtual buffer
|
|
12
|
+
*
|
|
13
|
+
* Architecture designed for future magit-style features.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Types and Interfaces
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
interface GitCommit {
|
|
21
|
+
hash: string;
|
|
22
|
+
shortHash: string;
|
|
23
|
+
author: string;
|
|
24
|
+
authorEmail: string;
|
|
25
|
+
date: string;
|
|
26
|
+
relativeDate: string;
|
|
27
|
+
subject: string;
|
|
28
|
+
body: string;
|
|
29
|
+
refs: string; // Branch/tag refs
|
|
30
|
+
graph: string; // Graph characters
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface GitLogOptions {
|
|
34
|
+
showGraph: boolean;
|
|
35
|
+
showRefs: boolean;
|
|
36
|
+
maxCommits: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface GitLogState {
|
|
40
|
+
isOpen: boolean;
|
|
41
|
+
bufferId: number | null;
|
|
42
|
+
splitId: number | null; // The split where git log is displayed
|
|
43
|
+
sourceBufferId: number | null; // The buffer that was open before git log (to restore on close)
|
|
44
|
+
commits: GitCommit[];
|
|
45
|
+
options: GitLogOptions;
|
|
46
|
+
cachedContent: string; // Store content for highlighting (getBufferText doesn't work for virtual buffers)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface GitCommitDetailState {
|
|
50
|
+
isOpen: boolean;
|
|
51
|
+
bufferId: number | null;
|
|
52
|
+
splitId: number | null;
|
|
53
|
+
commit: GitCommit | null;
|
|
54
|
+
cachedContent: string; // Store content for highlighting
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface GitFileViewState {
|
|
58
|
+
isOpen: boolean;
|
|
59
|
+
bufferId: number | null;
|
|
60
|
+
splitId: number | null;
|
|
61
|
+
filePath: string | null;
|
|
62
|
+
commitHash: string | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// State Management
|
|
67
|
+
// =============================================================================
|
|
68
|
+
|
|
69
|
+
const gitLogState: GitLogState = {
|
|
70
|
+
isOpen: false,
|
|
71
|
+
bufferId: null,
|
|
72
|
+
splitId: null,
|
|
73
|
+
sourceBufferId: null,
|
|
74
|
+
commits: [],
|
|
75
|
+
options: {
|
|
76
|
+
showGraph: false, // Disabled by default - graph interferes with format parsing
|
|
77
|
+
showRefs: true,
|
|
78
|
+
maxCommits: 100,
|
|
79
|
+
},
|
|
80
|
+
cachedContent: "",
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const commitDetailState: GitCommitDetailState = {
|
|
84
|
+
isOpen: false,
|
|
85
|
+
bufferId: null,
|
|
86
|
+
splitId: null,
|
|
87
|
+
commit: null,
|
|
88
|
+
cachedContent: "",
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const fileViewState: GitFileViewState = {
|
|
92
|
+
isOpen: false,
|
|
93
|
+
bufferId: null,
|
|
94
|
+
splitId: null,
|
|
95
|
+
filePath: null,
|
|
96
|
+
commitHash: null,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// =============================================================================
|
|
100
|
+
// Color Definitions (for syntax highlighting)
|
|
101
|
+
// =============================================================================
|
|
102
|
+
|
|
103
|
+
const colors = {
|
|
104
|
+
hash: [255, 180, 50] as [number, number, number], // Yellow/Orange
|
|
105
|
+
author: [100, 200, 255] as [number, number, number], // Cyan
|
|
106
|
+
date: [150, 255, 150] as [number, number, number], // Green
|
|
107
|
+
subject: [255, 255, 255] as [number, number, number], // White
|
|
108
|
+
header: [255, 200, 100] as [number, number, number], // Gold
|
|
109
|
+
separator: [100, 100, 100] as [number, number, number], // Gray
|
|
110
|
+
selected: [80, 80, 120] as [number, number, number], // Selection background
|
|
111
|
+
diffAdd: [100, 255, 100] as [number, number, number], // Green for additions
|
|
112
|
+
diffDel: [255, 100, 100] as [number, number, number], // Red for deletions
|
|
113
|
+
diffHunk: [150, 150, 255] as [number, number, number], // Blue for hunk headers
|
|
114
|
+
branch: [255, 150, 255] as [number, number, number], // Magenta for branches
|
|
115
|
+
tag: [255, 255, 100] as [number, number, number], // Yellow for tags
|
|
116
|
+
remote: [255, 130, 100] as [number, number, number], // Orange for remotes
|
|
117
|
+
graph: [150, 150, 150] as [number, number, number], // Gray for graph
|
|
118
|
+
// Syntax highlighting colors
|
|
119
|
+
syntaxKeyword: [200, 120, 220] as [number, number, number], // Purple for keywords
|
|
120
|
+
syntaxString: [180, 220, 140] as [number, number, number], // Light green for strings
|
|
121
|
+
syntaxComment: [120, 120, 120] as [number, number, number], // Gray for comments
|
|
122
|
+
syntaxNumber: [220, 180, 120] as [number, number, number], // Orange for numbers
|
|
123
|
+
syntaxFunction: [100, 180, 255] as [number, number, number], // Blue for functions
|
|
124
|
+
syntaxType: [80, 200, 180] as [number, number, number], // Teal for types
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// =============================================================================
|
|
128
|
+
// Mode Definitions
|
|
129
|
+
// =============================================================================
|
|
130
|
+
|
|
131
|
+
// Define git-log mode with minimal keybindings
|
|
132
|
+
// Navigation uses normal cursor movement (arrows, j/k work naturally via parent mode)
|
|
133
|
+
editor.defineMode(
|
|
134
|
+
"git-log",
|
|
135
|
+
"normal", // inherit from normal mode for cursor movement
|
|
136
|
+
[
|
|
137
|
+
["Return", "git_log_show_commit"],
|
|
138
|
+
["Tab", "git_log_show_commit"],
|
|
139
|
+
["q", "git_log_close"],
|
|
140
|
+
["Escape", "git_log_close"],
|
|
141
|
+
["r", "git_log_refresh"],
|
|
142
|
+
["y", "git_log_copy_hash"],
|
|
143
|
+
],
|
|
144
|
+
true // read-only
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Define git-commit-detail mode for viewing commit details
|
|
148
|
+
// Inherits from normal mode for natural cursor movement
|
|
149
|
+
editor.defineMode(
|
|
150
|
+
"git-commit-detail",
|
|
151
|
+
"normal", // inherit from normal mode for cursor movement
|
|
152
|
+
[
|
|
153
|
+
["Return", "git_commit_detail_open_file"],
|
|
154
|
+
["q", "git_commit_detail_close"],
|
|
155
|
+
["Escape", "git_commit_detail_close"],
|
|
156
|
+
],
|
|
157
|
+
true // read-only
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Define git-file-view mode for viewing files at a specific commit
|
|
161
|
+
editor.defineMode(
|
|
162
|
+
"git-file-view",
|
|
163
|
+
"normal", // inherit from normal mode for cursor movement
|
|
164
|
+
[
|
|
165
|
+
["q", "git_file_view_close"],
|
|
166
|
+
["Escape", "git_file_view_close"],
|
|
167
|
+
],
|
|
168
|
+
true // read-only
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// =============================================================================
|
|
172
|
+
// Git Command Execution
|
|
173
|
+
// =============================================================================
|
|
174
|
+
|
|
175
|
+
async function fetchGitLog(): Promise<GitCommit[]> {
|
|
176
|
+
// Use record separator to reliably split commits
|
|
177
|
+
// Format: hash, short hash, author, email, date, relative date, refs, subject, body
|
|
178
|
+
const format = "%H%x00%h%x00%an%x00%ae%x00%ai%x00%ar%x00%d%x00%s%x00%b%x1e";
|
|
179
|
+
|
|
180
|
+
const args = [
|
|
181
|
+
"log",
|
|
182
|
+
`--format=${format}`,
|
|
183
|
+
`-n${gitLogState.options.maxCommits}`,
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
const cwd = editor.getCwd();
|
|
187
|
+
const result = await editor.spawnProcess("git", args, cwd);
|
|
188
|
+
|
|
189
|
+
if (result.exit_code !== 0) {
|
|
190
|
+
editor.setStatus(editor.t("status.git_error", { error: result.stderr }));
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const commits: GitCommit[] = [];
|
|
195
|
+
// Split by record separator (0x1e)
|
|
196
|
+
const records = result.stdout.split("\x1e");
|
|
197
|
+
|
|
198
|
+
for (const record of records) {
|
|
199
|
+
if (!record.trim()) continue;
|
|
200
|
+
|
|
201
|
+
const parts = record.split("\x00");
|
|
202
|
+
if (parts.length >= 8) {
|
|
203
|
+
commits.push({
|
|
204
|
+
hash: parts[0].trim(),
|
|
205
|
+
shortHash: parts[1].trim(),
|
|
206
|
+
author: parts[2].trim(),
|
|
207
|
+
authorEmail: parts[3].trim(),
|
|
208
|
+
date: parts[4].trim(),
|
|
209
|
+
relativeDate: parts[5].trim(),
|
|
210
|
+
refs: parts[6].trim(),
|
|
211
|
+
subject: parts[7].trim(),
|
|
212
|
+
body: parts[8] ? parts[8].trim() : "",
|
|
213
|
+
graph: "", // Graph is handled separately if needed
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return commits;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function fetchCommitDiff(hash: string): Promise<string> {
|
|
222
|
+
const cwd = editor.getCwd();
|
|
223
|
+
const result = await editor.spawnProcess("git", [
|
|
224
|
+
"show",
|
|
225
|
+
"--stat",
|
|
226
|
+
"--patch",
|
|
227
|
+
hash,
|
|
228
|
+
], cwd);
|
|
229
|
+
|
|
230
|
+
if (result.exit_code !== 0) {
|
|
231
|
+
return editor.t("status.error_fetching_diff", { error: result.stderr });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return result.stdout;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// =============================================================================
|
|
238
|
+
// Git Log View
|
|
239
|
+
// =============================================================================
|
|
240
|
+
|
|
241
|
+
function formatCommitRow(commit: GitCommit): string {
|
|
242
|
+
// Build a structured line for consistent parsing and highlighting
|
|
243
|
+
// Format: shortHash (author, relativeDate) subject [refs]
|
|
244
|
+
let line = commit.shortHash;
|
|
245
|
+
|
|
246
|
+
// Add author in parentheses
|
|
247
|
+
line += " (" + commit.author + ", " + commit.relativeDate + ")";
|
|
248
|
+
|
|
249
|
+
// Add subject
|
|
250
|
+
line += " " + commit.subject;
|
|
251
|
+
|
|
252
|
+
// Add refs at the end if present and enabled
|
|
253
|
+
if (gitLogState.options.showRefs && commit.refs) {
|
|
254
|
+
line += " " + commit.refs;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return line + "\n";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Helper to extract content string from entries (for highlighting)
|
|
261
|
+
function entriesToContent(entries: TextPropertyEntry[]): string {
|
|
262
|
+
return entries.map(e => e.text).join("");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function buildGitLogEntries(): TextPropertyEntry[] {
|
|
266
|
+
const entries: TextPropertyEntry[] = [];
|
|
267
|
+
|
|
268
|
+
// Magit-style header
|
|
269
|
+
entries.push({
|
|
270
|
+
text: editor.t("panel.commits_header") + "\n",
|
|
271
|
+
properties: { type: "section-header" },
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (gitLogState.commits.length === 0) {
|
|
275
|
+
entries.push({
|
|
276
|
+
text: editor.t("panel.no_commits") + "\n",
|
|
277
|
+
properties: { type: "empty" },
|
|
278
|
+
});
|
|
279
|
+
} else {
|
|
280
|
+
// Add each commit
|
|
281
|
+
for (let i = 0; i < gitLogState.commits.length; i++) {
|
|
282
|
+
const commit = gitLogState.commits[i];
|
|
283
|
+
entries.push({
|
|
284
|
+
text: formatCommitRow(commit),
|
|
285
|
+
properties: {
|
|
286
|
+
type: "commit",
|
|
287
|
+
index: i,
|
|
288
|
+
hash: commit.hash,
|
|
289
|
+
shortHash: commit.shortHash,
|
|
290
|
+
author: commit.author,
|
|
291
|
+
date: commit.relativeDate,
|
|
292
|
+
subject: commit.subject,
|
|
293
|
+
refs: commit.refs,
|
|
294
|
+
graph: commit.graph,
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Footer with help
|
|
301
|
+
entries.push({
|
|
302
|
+
text: "\n",
|
|
303
|
+
properties: { type: "blank" },
|
|
304
|
+
});
|
|
305
|
+
entries.push({
|
|
306
|
+
text: editor.t("panel.log_footer", { count: String(gitLogState.commits.length) }) + "\n",
|
|
307
|
+
properties: { type: "footer" },
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
return entries;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function applyGitLogHighlighting(): void {
|
|
314
|
+
if (gitLogState.bufferId === null) return;
|
|
315
|
+
|
|
316
|
+
const bufferId = gitLogState.bufferId;
|
|
317
|
+
|
|
318
|
+
// Clear existing overlays
|
|
319
|
+
editor.clearNamespace(bufferId, "gitlog");
|
|
320
|
+
|
|
321
|
+
// Use cached content (getBufferText doesn't work for virtual buffers)
|
|
322
|
+
const content = gitLogState.cachedContent;
|
|
323
|
+
if (!content) return;
|
|
324
|
+
const lines = content.split("\n");
|
|
325
|
+
|
|
326
|
+
// Get cursor line to highlight current row (1-indexed from API)
|
|
327
|
+
const cursorLine = editor.getCursorLine();
|
|
328
|
+
const headerLines = 1; // Just "Commits:" header
|
|
329
|
+
|
|
330
|
+
let byteOffset = 0;
|
|
331
|
+
|
|
332
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
333
|
+
const line = lines[lineIdx];
|
|
334
|
+
const lineStart = byteOffset;
|
|
335
|
+
const lineEnd = byteOffset + line.length;
|
|
336
|
+
|
|
337
|
+
// Highlight section header
|
|
338
|
+
if (line === editor.t("panel.commits_header")) {
|
|
339
|
+
editor.addOverlay(
|
|
340
|
+
bufferId,
|
|
341
|
+
"gitlog",
|
|
342
|
+
lineStart,
|
|
343
|
+
lineEnd,
|
|
344
|
+
colors.header[0],
|
|
345
|
+
colors.header[1],
|
|
346
|
+
colors.header[2],
|
|
347
|
+
true, // underline
|
|
348
|
+
true, // bold
|
|
349
|
+
false // italic
|
|
350
|
+
);
|
|
351
|
+
byteOffset += line.length + 1;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const commitIndex = lineIdx - headerLines;
|
|
356
|
+
if (commitIndex < 0 || commitIndex >= gitLogState.commits.length) {
|
|
357
|
+
byteOffset += line.length + 1;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const commit = gitLogState.commits[commitIndex];
|
|
362
|
+
// cursorLine is 1-indexed, lineIdx is 0-indexed
|
|
363
|
+
const isCurrentLine = (lineIdx + 1) === cursorLine;
|
|
364
|
+
|
|
365
|
+
// Highlight entire line if cursor is on it (using selected color with underline)
|
|
366
|
+
if (isCurrentLine) {
|
|
367
|
+
editor.addOverlay(
|
|
368
|
+
bufferId,
|
|
369
|
+
"gitlog",
|
|
370
|
+
lineStart,
|
|
371
|
+
lineEnd,
|
|
372
|
+
colors.selected[0],
|
|
373
|
+
colors.selected[1],
|
|
374
|
+
colors.selected[2],
|
|
375
|
+
true, // underline to make it visible
|
|
376
|
+
true, // bold
|
|
377
|
+
false // italic
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Parse the line format: "shortHash (author, relativeDate) subject [refs]"
|
|
382
|
+
// Highlight hash (first 7+ chars until space)
|
|
383
|
+
const hashEnd = commit.shortHash.length;
|
|
384
|
+
editor.addOverlay(
|
|
385
|
+
bufferId,
|
|
386
|
+
"gitlog",
|
|
387
|
+
lineStart,
|
|
388
|
+
lineStart + hashEnd,
|
|
389
|
+
colors.hash[0],
|
|
390
|
+
colors.hash[1],
|
|
391
|
+
colors.hash[2],
|
|
392
|
+
false, // underline
|
|
393
|
+
false, // bold
|
|
394
|
+
false // italic
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
// Highlight author name (inside parentheses)
|
|
398
|
+
const authorPattern = "(" + commit.author + ",";
|
|
399
|
+
const authorStartInLine = line.indexOf(authorPattern);
|
|
400
|
+
if (authorStartInLine >= 0) {
|
|
401
|
+
const authorStart = lineStart + authorStartInLine + 1; // skip "("
|
|
402
|
+
const authorEnd = authorStart + commit.author.length;
|
|
403
|
+
editor.addOverlay(
|
|
404
|
+
bufferId,
|
|
405
|
+
"gitlog",
|
|
406
|
+
authorStart,
|
|
407
|
+
authorEnd,
|
|
408
|
+
colors.author[0],
|
|
409
|
+
colors.author[1],
|
|
410
|
+
colors.author[2],
|
|
411
|
+
false, // underline
|
|
412
|
+
false, // bold
|
|
413
|
+
false // italic
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Highlight relative date
|
|
418
|
+
const datePattern = ", " + commit.relativeDate + ")";
|
|
419
|
+
const dateStartInLine = line.indexOf(datePattern);
|
|
420
|
+
if (dateStartInLine >= 0) {
|
|
421
|
+
const dateStart = lineStart + dateStartInLine + 2; // skip ", "
|
|
422
|
+
const dateEnd = dateStart + commit.relativeDate.length;
|
|
423
|
+
editor.addOverlay(
|
|
424
|
+
bufferId,
|
|
425
|
+
"gitlog",
|
|
426
|
+
dateStart,
|
|
427
|
+
dateEnd,
|
|
428
|
+
colors.date[0],
|
|
429
|
+
colors.date[1],
|
|
430
|
+
colors.date[2],
|
|
431
|
+
false, // underline
|
|
432
|
+
false, // bold
|
|
433
|
+
false // italic
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Highlight refs (branches/tags) at end of line if present
|
|
438
|
+
if (gitLogState.options.showRefs && commit.refs) {
|
|
439
|
+
const refsStartInLine = line.lastIndexOf(commit.refs);
|
|
440
|
+
if (refsStartInLine >= 0) {
|
|
441
|
+
const refsStart = lineStart + refsStartInLine;
|
|
442
|
+
const refsEnd = refsStart + commit.refs.length;
|
|
443
|
+
|
|
444
|
+
// Determine color based on ref type
|
|
445
|
+
let refColor = colors.branch;
|
|
446
|
+
if (commit.refs.includes("tag:")) {
|
|
447
|
+
refColor = colors.tag;
|
|
448
|
+
} else if (commit.refs.includes("origin/") || commit.refs.includes("remote")) {
|
|
449
|
+
refColor = colors.remote;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
editor.addOverlay(
|
|
453
|
+
bufferId,
|
|
454
|
+
"gitlog",
|
|
455
|
+
refsStart,
|
|
456
|
+
refsEnd,
|
|
457
|
+
refColor[0],
|
|
458
|
+
refColor[1],
|
|
459
|
+
refColor[2],
|
|
460
|
+
false, // underline
|
|
461
|
+
true, // bold (make refs stand out)
|
|
462
|
+
false // italic
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
byteOffset += line.length + 1;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function updateGitLogView(): void {
|
|
472
|
+
if (gitLogState.bufferId !== null) {
|
|
473
|
+
const entries = buildGitLogEntries();
|
|
474
|
+
gitLogState.cachedContent = entriesToContent(entries);
|
|
475
|
+
editor.setVirtualBufferContent(gitLogState.bufferId, entries);
|
|
476
|
+
applyGitLogHighlighting();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// =============================================================================
|
|
481
|
+
// Commit Detail View
|
|
482
|
+
// =============================================================================
|
|
483
|
+
|
|
484
|
+
// Parse diff line to extract file and line information
|
|
485
|
+
interface DiffContext {
|
|
486
|
+
currentFile: string | null;
|
|
487
|
+
currentHunkNewStart: number;
|
|
488
|
+
currentHunkNewLine: number; // Current line within the new file
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function buildCommitDetailEntries(commit: GitCommit, showOutput: string): TextPropertyEntry[] {
|
|
492
|
+
const entries: TextPropertyEntry[] = [];
|
|
493
|
+
const lines = showOutput.split("\n");
|
|
494
|
+
|
|
495
|
+
// Track diff context for file/line navigation
|
|
496
|
+
const diffContext: DiffContext = {
|
|
497
|
+
currentFile: null,
|
|
498
|
+
currentHunkNewStart: 0,
|
|
499
|
+
currentHunkNewLine: 0,
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
for (const line of lines) {
|
|
503
|
+
let lineType = "text";
|
|
504
|
+
const properties: Record<string, unknown> = { type: lineType };
|
|
505
|
+
|
|
506
|
+
// Detect diff file header: diff --git a/path b/path
|
|
507
|
+
const diffHeaderMatch = line.match(/^diff --git a\/(.+) b\/(.+)$/);
|
|
508
|
+
if (diffHeaderMatch) {
|
|
509
|
+
diffContext.currentFile = diffHeaderMatch[2]; // Use the 'b' (new) file path
|
|
510
|
+
diffContext.currentHunkNewStart = 0;
|
|
511
|
+
diffContext.currentHunkNewLine = 0;
|
|
512
|
+
lineType = "diff-header";
|
|
513
|
+
properties.type = lineType;
|
|
514
|
+
properties.file = diffContext.currentFile;
|
|
515
|
+
}
|
|
516
|
+
// Detect +++ line (new file path)
|
|
517
|
+
else if (line.startsWith("+++ b/")) {
|
|
518
|
+
diffContext.currentFile = line.slice(6);
|
|
519
|
+
lineType = "diff-header";
|
|
520
|
+
properties.type = lineType;
|
|
521
|
+
properties.file = diffContext.currentFile;
|
|
522
|
+
}
|
|
523
|
+
// Detect hunk header: @@ -old,count +new,count @@
|
|
524
|
+
else if (line.startsWith("@@")) {
|
|
525
|
+
lineType = "diff-hunk";
|
|
526
|
+
const hunkMatch = line.match(/@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
527
|
+
if (hunkMatch) {
|
|
528
|
+
diffContext.currentHunkNewStart = parseInt(hunkMatch[1], 10);
|
|
529
|
+
diffContext.currentHunkNewLine = diffContext.currentHunkNewStart;
|
|
530
|
+
}
|
|
531
|
+
properties.type = lineType;
|
|
532
|
+
properties.file = diffContext.currentFile;
|
|
533
|
+
properties.line = diffContext.currentHunkNewStart;
|
|
534
|
+
}
|
|
535
|
+
// Addition line
|
|
536
|
+
else if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
537
|
+
lineType = "diff-add";
|
|
538
|
+
properties.type = lineType;
|
|
539
|
+
properties.file = diffContext.currentFile;
|
|
540
|
+
properties.line = diffContext.currentHunkNewLine;
|
|
541
|
+
diffContext.currentHunkNewLine++;
|
|
542
|
+
}
|
|
543
|
+
// Deletion line
|
|
544
|
+
else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
545
|
+
lineType = "diff-del";
|
|
546
|
+
properties.type = lineType;
|
|
547
|
+
properties.file = diffContext.currentFile;
|
|
548
|
+
// Deletion lines don't advance the new file line counter
|
|
549
|
+
}
|
|
550
|
+
// Context line (unchanged)
|
|
551
|
+
else if (line.startsWith(" ") && diffContext.currentFile && diffContext.currentHunkNewLine > 0) {
|
|
552
|
+
lineType = "diff-context";
|
|
553
|
+
properties.type = lineType;
|
|
554
|
+
properties.file = diffContext.currentFile;
|
|
555
|
+
properties.line = diffContext.currentHunkNewLine;
|
|
556
|
+
diffContext.currentHunkNewLine++;
|
|
557
|
+
}
|
|
558
|
+
// Other diff header lines
|
|
559
|
+
else if (line.startsWith("index ") || line.startsWith("--- ")) {
|
|
560
|
+
lineType = "diff-header";
|
|
561
|
+
properties.type = lineType;
|
|
562
|
+
}
|
|
563
|
+
// Commit header lines
|
|
564
|
+
else if (line.startsWith("commit ")) {
|
|
565
|
+
lineType = "header";
|
|
566
|
+
properties.type = lineType;
|
|
567
|
+
const hashMatch = line.match(/^commit ([a-f0-9]+)/);
|
|
568
|
+
if (hashMatch) {
|
|
569
|
+
properties.hash = hashMatch[1];
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
else if (line.startsWith("Author:")) {
|
|
573
|
+
lineType = "meta";
|
|
574
|
+
properties.type = lineType;
|
|
575
|
+
properties.field = "author";
|
|
576
|
+
}
|
|
577
|
+
else if (line.startsWith("Date:")) {
|
|
578
|
+
lineType = "meta";
|
|
579
|
+
properties.type = lineType;
|
|
580
|
+
properties.field = "date";
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
entries.push({
|
|
584
|
+
text: `${line}\n`,
|
|
585
|
+
properties: properties,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Footer with help
|
|
590
|
+
entries.push({
|
|
591
|
+
text: "\n",
|
|
592
|
+
properties: { type: "blank" },
|
|
593
|
+
});
|
|
594
|
+
entries.push({
|
|
595
|
+
text: editor.t("panel.detail_footer") + "\n",
|
|
596
|
+
properties: { type: "footer" },
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
return entries;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function applyCommitDetailHighlighting(): void {
|
|
603
|
+
if (commitDetailState.bufferId === null) return;
|
|
604
|
+
|
|
605
|
+
const bufferId = commitDetailState.bufferId;
|
|
606
|
+
|
|
607
|
+
// Clear existing overlays
|
|
608
|
+
editor.clearNamespace(bufferId, "gitdetail");
|
|
609
|
+
|
|
610
|
+
// Use cached content (getBufferText doesn't work for virtual buffers)
|
|
611
|
+
const content = commitDetailState.cachedContent;
|
|
612
|
+
if (!content) return;
|
|
613
|
+
const lines = content.split("\n");
|
|
614
|
+
|
|
615
|
+
let byteOffset = 0;
|
|
616
|
+
|
|
617
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
618
|
+
const line = lines[lineIdx];
|
|
619
|
+
const lineStart = byteOffset;
|
|
620
|
+
const lineEnd = byteOffset + line.length;
|
|
621
|
+
|
|
622
|
+
// Highlight diff additions (green)
|
|
623
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
624
|
+
editor.addOverlay(
|
|
625
|
+
bufferId,
|
|
626
|
+
"gitdetail",
|
|
627
|
+
lineStart,
|
|
628
|
+
lineEnd,
|
|
629
|
+
colors.diffAdd[0],
|
|
630
|
+
colors.diffAdd[1],
|
|
631
|
+
colors.diffAdd[2],
|
|
632
|
+
false, // underline
|
|
633
|
+
false, // bold
|
|
634
|
+
false // italic
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
// Highlight diff deletions (red)
|
|
638
|
+
else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
639
|
+
editor.addOverlay(
|
|
640
|
+
bufferId,
|
|
641
|
+
"gitdetail",
|
|
642
|
+
lineStart,
|
|
643
|
+
lineEnd,
|
|
644
|
+
colors.diffDel[0],
|
|
645
|
+
colors.diffDel[1],
|
|
646
|
+
colors.diffDel[2],
|
|
647
|
+
false, // underline
|
|
648
|
+
false, // bold
|
|
649
|
+
false // italic
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
// Highlight hunk headers (cyan/blue)
|
|
653
|
+
else if (line.startsWith("@@")) {
|
|
654
|
+
editor.addOverlay(
|
|
655
|
+
bufferId,
|
|
656
|
+
"gitdetail",
|
|
657
|
+
lineStart,
|
|
658
|
+
lineEnd,
|
|
659
|
+
colors.diffHunk[0],
|
|
660
|
+
colors.diffHunk[1],
|
|
661
|
+
colors.diffHunk[2],
|
|
662
|
+
false, // underline
|
|
663
|
+
true, // bold
|
|
664
|
+
false // italic
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
// Highlight commit hash in "commit <hash>" line (git show format)
|
|
668
|
+
else if (line.startsWith("commit ")) {
|
|
669
|
+
const hashMatch = line.match(/^commit ([a-f0-9]+)/);
|
|
670
|
+
if (hashMatch) {
|
|
671
|
+
const hashStart = lineStart + 7; // "commit " is 7 chars
|
|
672
|
+
editor.addOverlay(
|
|
673
|
+
bufferId,
|
|
674
|
+
"gitdetail",
|
|
675
|
+
hashStart,
|
|
676
|
+
hashStart + hashMatch[1].length,
|
|
677
|
+
colors.hash[0],
|
|
678
|
+
colors.hash[1],
|
|
679
|
+
colors.hash[2],
|
|
680
|
+
false, // underline
|
|
681
|
+
true, // bold
|
|
682
|
+
false // italic
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// Highlight author line
|
|
687
|
+
else if (line.startsWith("Author:")) {
|
|
688
|
+
editor.addOverlay(
|
|
689
|
+
bufferId,
|
|
690
|
+
"gitdetail",
|
|
691
|
+
lineStart + 8, // "Author: " is 8 chars
|
|
692
|
+
lineEnd,
|
|
693
|
+
colors.author[0],
|
|
694
|
+
colors.author[1],
|
|
695
|
+
colors.author[2],
|
|
696
|
+
false, // underline
|
|
697
|
+
false, // bold
|
|
698
|
+
false // italic
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
// Highlight date line
|
|
702
|
+
else if (line.startsWith("Date:")) {
|
|
703
|
+
editor.addOverlay(
|
|
704
|
+
bufferId,
|
|
705
|
+
"gitdetail",
|
|
706
|
+
lineStart + 6, // "Date: " is 6 chars (with trailing spaces it's 8)
|
|
707
|
+
lineEnd,
|
|
708
|
+
colors.date[0],
|
|
709
|
+
colors.date[1],
|
|
710
|
+
colors.date[2],
|
|
711
|
+
false, // underline
|
|
712
|
+
false, // bold
|
|
713
|
+
false // italic
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
// Highlight diff file headers
|
|
717
|
+
else if (line.startsWith("diff --git")) {
|
|
718
|
+
editor.addOverlay(
|
|
719
|
+
bufferId,
|
|
720
|
+
"gitdetail",
|
|
721
|
+
lineStart,
|
|
722
|
+
lineEnd,
|
|
723
|
+
colors.header[0],
|
|
724
|
+
colors.header[1],
|
|
725
|
+
colors.header[2],
|
|
726
|
+
false, // underline
|
|
727
|
+
true, // bold
|
|
728
|
+
false // italic
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
byteOffset += line.length + 1;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// =============================================================================
|
|
737
|
+
// Public Commands - Git Log
|
|
738
|
+
// =============================================================================
|
|
739
|
+
|
|
740
|
+
globalThis.show_git_log = async function(): Promise<void> {
|
|
741
|
+
if (gitLogState.isOpen) {
|
|
742
|
+
editor.setStatus(editor.t("status.already_open"));
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
editor.setStatus(editor.t("status.loading"));
|
|
747
|
+
|
|
748
|
+
// Store the current split ID and buffer ID before opening git log
|
|
749
|
+
gitLogState.splitId = editor.getActiveSplitId();
|
|
750
|
+
gitLogState.sourceBufferId = editor.getActiveBufferId();
|
|
751
|
+
|
|
752
|
+
// Fetch commits
|
|
753
|
+
gitLogState.commits = await fetchGitLog();
|
|
754
|
+
|
|
755
|
+
if (gitLogState.commits.length === 0) {
|
|
756
|
+
editor.setStatus(editor.t("status.no_commits"));
|
|
757
|
+
gitLogState.splitId = null;
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Build entries and cache content for highlighting
|
|
762
|
+
const entries = buildGitLogEntries();
|
|
763
|
+
gitLogState.cachedContent = entriesToContent(entries);
|
|
764
|
+
|
|
765
|
+
// Create virtual buffer in the current split (replacing current buffer)
|
|
766
|
+
const bufferId = await editor.createVirtualBufferInExistingSplit({
|
|
767
|
+
name: "*Git Log*",
|
|
768
|
+
mode: "git-log",
|
|
769
|
+
read_only: true,
|
|
770
|
+
entries: entries,
|
|
771
|
+
split_id: gitLogState.splitId!,
|
|
772
|
+
show_line_numbers: false,
|
|
773
|
+
show_cursors: true,
|
|
774
|
+
editing_disabled: true,
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
if (bufferId !== null) {
|
|
778
|
+
gitLogState.isOpen = true;
|
|
779
|
+
gitLogState.bufferId = bufferId;
|
|
780
|
+
|
|
781
|
+
// Apply syntax highlighting
|
|
782
|
+
applyGitLogHighlighting();
|
|
783
|
+
|
|
784
|
+
editor.setStatus(editor.t("status.log_ready", { count: String(gitLogState.commits.length) }));
|
|
785
|
+
editor.debug("Git log panel opened");
|
|
786
|
+
} else {
|
|
787
|
+
gitLogState.splitId = null;
|
|
788
|
+
editor.setStatus(editor.t("status.failed_open"));
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
globalThis.git_log_close = function(): void {
|
|
793
|
+
if (!gitLogState.isOpen) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Restore the original buffer in the split
|
|
798
|
+
if (gitLogState.splitId !== null && gitLogState.sourceBufferId !== null) {
|
|
799
|
+
editor.setSplitBuffer(gitLogState.splitId, gitLogState.sourceBufferId);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Close the git log buffer (it's no longer displayed)
|
|
803
|
+
if (gitLogState.bufferId !== null) {
|
|
804
|
+
editor.closeBuffer(gitLogState.bufferId);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
gitLogState.isOpen = false;
|
|
808
|
+
gitLogState.bufferId = null;
|
|
809
|
+
gitLogState.splitId = null;
|
|
810
|
+
gitLogState.sourceBufferId = null;
|
|
811
|
+
gitLogState.commits = [];
|
|
812
|
+
editor.setStatus(editor.t("status.closed"));
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
// Cursor moved handler for git log - update highlighting and status
|
|
816
|
+
globalThis.on_git_log_cursor_moved = function(data: {
|
|
817
|
+
buffer_id: number;
|
|
818
|
+
cursor_id: number;
|
|
819
|
+
old_position: number;
|
|
820
|
+
new_position: number;
|
|
821
|
+
}): void {
|
|
822
|
+
// Only handle cursor movement in our git log buffer
|
|
823
|
+
if (gitLogState.bufferId === null || data.buffer_id !== gitLogState.bufferId) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Re-apply highlighting to update cursor line highlight
|
|
828
|
+
applyGitLogHighlighting();
|
|
829
|
+
|
|
830
|
+
// Get cursor line to show status
|
|
831
|
+
const cursorLine = editor.getCursorLine();
|
|
832
|
+
const headerLines = 1;
|
|
833
|
+
const commitIndex = cursorLine - headerLines;
|
|
834
|
+
|
|
835
|
+
if (commitIndex >= 0 && commitIndex < gitLogState.commits.length) {
|
|
836
|
+
editor.setStatus(editor.t("status.commit_position", { current: String(commitIndex + 1), total: String(gitLogState.commits.length) }));
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
// Register cursor movement handler
|
|
841
|
+
editor.on("cursor_moved", "on_git_log_cursor_moved");
|
|
842
|
+
|
|
843
|
+
globalThis.git_log_refresh = async function(): Promise<void> {
|
|
844
|
+
if (!gitLogState.isOpen) return;
|
|
845
|
+
|
|
846
|
+
editor.setStatus(editor.t("status.refreshing"));
|
|
847
|
+
gitLogState.commits = await fetchGitLog();
|
|
848
|
+
updateGitLogView();
|
|
849
|
+
editor.setStatus(editor.t("status.refreshed", { count: String(gitLogState.commits.length) }));
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
// Helper function to get commit at current cursor position
|
|
853
|
+
function getCommitAtCursor(): GitCommit | null {
|
|
854
|
+
if (gitLogState.bufferId === null) return null;
|
|
855
|
+
|
|
856
|
+
// Use text properties to find which commit the cursor is on
|
|
857
|
+
// This is more reliable than line number calculation
|
|
858
|
+
const props = editor.getTextPropertiesAtCursor(gitLogState.bufferId);
|
|
859
|
+
|
|
860
|
+
if (props.length > 0) {
|
|
861
|
+
const prop = props[0];
|
|
862
|
+
// Check if cursor is on a commit line (has type "commit" and index)
|
|
863
|
+
if (prop.type === "commit" && typeof prop.index === "number") {
|
|
864
|
+
const index = prop.index as number;
|
|
865
|
+
if (index >= 0 && index < gitLogState.commits.length) {
|
|
866
|
+
return gitLogState.commits[index];
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
// Also support finding commit by hash (alternative lookup)
|
|
870
|
+
if (prop.hash && typeof prop.hash === "string") {
|
|
871
|
+
return gitLogState.commits.find(c => c.hash === prop.hash) || null;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
globalThis.git_log_show_commit = async function(): Promise<void> {
|
|
879
|
+
if (!gitLogState.isOpen || gitLogState.commits.length === 0) return;
|
|
880
|
+
if (gitLogState.splitId === null) return;
|
|
881
|
+
|
|
882
|
+
const commit = getCommitAtCursor();
|
|
883
|
+
if (!commit) {
|
|
884
|
+
editor.setStatus(editor.t("status.move_to_commit"));
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
editor.setStatus(editor.t("status.loading_commit", { hash: commit.shortHash }));
|
|
889
|
+
|
|
890
|
+
// Fetch full commit info using git show (includes header and diff)
|
|
891
|
+
const showOutput = await fetchCommitDiff(commit.hash);
|
|
892
|
+
|
|
893
|
+
// Build entries using raw git show output
|
|
894
|
+
const entries = buildCommitDetailEntries(commit, showOutput);
|
|
895
|
+
|
|
896
|
+
// Cache content for highlighting (getBufferText doesn't work for virtual buffers)
|
|
897
|
+
commitDetailState.cachedContent = entriesToContent(entries);
|
|
898
|
+
|
|
899
|
+
// Create virtual buffer in the current split (replacing git log view)
|
|
900
|
+
const bufferId = await editor.createVirtualBufferInExistingSplit({
|
|
901
|
+
name: `*Commit: ${commit.shortHash}*`,
|
|
902
|
+
mode: "git-commit-detail",
|
|
903
|
+
read_only: true,
|
|
904
|
+
entries: entries,
|
|
905
|
+
split_id: gitLogState.splitId!,
|
|
906
|
+
show_line_numbers: false, // Disable line numbers for cleaner diff view
|
|
907
|
+
show_cursors: true,
|
|
908
|
+
editing_disabled: true,
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
if (bufferId !== null) {
|
|
912
|
+
commitDetailState.isOpen = true;
|
|
913
|
+
commitDetailState.bufferId = bufferId;
|
|
914
|
+
commitDetailState.splitId = gitLogState.splitId;
|
|
915
|
+
commitDetailState.commit = commit;
|
|
916
|
+
|
|
917
|
+
// Apply syntax highlighting
|
|
918
|
+
applyCommitDetailHighlighting();
|
|
919
|
+
|
|
920
|
+
editor.setStatus(editor.t("status.commit_ready", { hash: commit.shortHash }));
|
|
921
|
+
} else {
|
|
922
|
+
editor.setStatus(editor.t("status.failed_open_details"));
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
globalThis.git_log_copy_hash = function(): void {
|
|
927
|
+
if (!gitLogState.isOpen || gitLogState.commits.length === 0) return;
|
|
928
|
+
|
|
929
|
+
const commit = getCommitAtCursor();
|
|
930
|
+
if (!commit) {
|
|
931
|
+
editor.setStatus(editor.t("status.move_to_commit"));
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Use spawn to copy to clipboard (works on most systems)
|
|
936
|
+
// Try xclip first (Linux), then pbcopy (macOS), then xsel
|
|
937
|
+
editor.spawnProcess("sh", ["-c", `echo -n "${commit.hash}" | xclip -selection clipboard 2>/dev/null || echo -n "${commit.hash}" | pbcopy 2>/dev/null || echo -n "${commit.hash}" | xsel --clipboard 2>/dev/null`])
|
|
938
|
+
.then(() => {
|
|
939
|
+
editor.setStatus(editor.t("status.hash_copied", { short: commit.shortHash, full: commit.hash }));
|
|
940
|
+
})
|
|
941
|
+
.catch(() => {
|
|
942
|
+
// If all clipboard commands fail, just show the hash
|
|
943
|
+
editor.setStatus(editor.t("status.hash_display", { hash: commit.hash }));
|
|
944
|
+
});
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
// =============================================================================
|
|
948
|
+
// Public Commands - Commit Detail
|
|
949
|
+
// =============================================================================
|
|
950
|
+
|
|
951
|
+
globalThis.git_commit_detail_close = function(): void {
|
|
952
|
+
if (!commitDetailState.isOpen) {
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Go back to the git log view by restoring the git log buffer
|
|
957
|
+
if (commitDetailState.splitId !== null && gitLogState.bufferId !== null) {
|
|
958
|
+
editor.setSplitBuffer(commitDetailState.splitId, gitLogState.bufferId);
|
|
959
|
+
// Re-apply highlighting since we're switching back
|
|
960
|
+
applyGitLogHighlighting();
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Close the commit detail buffer (it's no longer displayed)
|
|
964
|
+
if (commitDetailState.bufferId !== null) {
|
|
965
|
+
editor.closeBuffer(commitDetailState.bufferId);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
commitDetailState.isOpen = false;
|
|
969
|
+
commitDetailState.bufferId = null;
|
|
970
|
+
commitDetailState.splitId = null;
|
|
971
|
+
commitDetailState.commit = null;
|
|
972
|
+
|
|
973
|
+
editor.setStatus(editor.t("status.log_ready", { count: String(gitLogState.commits.length) }));
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
// Close file view and go back to commit detail
|
|
977
|
+
globalThis.git_file_view_close = function(): void {
|
|
978
|
+
if (!fileViewState.isOpen) {
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Go back to the commit detail view by restoring the commit detail buffer
|
|
983
|
+
if (fileViewState.splitId !== null && commitDetailState.bufferId !== null) {
|
|
984
|
+
editor.setSplitBuffer(fileViewState.splitId, commitDetailState.bufferId);
|
|
985
|
+
// Re-apply highlighting since we're switching back
|
|
986
|
+
applyCommitDetailHighlighting();
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Close the file view buffer (it's no longer displayed)
|
|
990
|
+
if (fileViewState.bufferId !== null) {
|
|
991
|
+
editor.closeBuffer(fileViewState.bufferId);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
fileViewState.isOpen = false;
|
|
995
|
+
fileViewState.bufferId = null;
|
|
996
|
+
fileViewState.splitId = null;
|
|
997
|
+
fileViewState.filePath = null;
|
|
998
|
+
fileViewState.commitHash = null;
|
|
999
|
+
|
|
1000
|
+
if (commitDetailState.commit) {
|
|
1001
|
+
editor.setStatus(editor.t("status.commit_ready", { hash: commitDetailState.commit.shortHash }));
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
// Fetch file content at a specific commit
|
|
1006
|
+
async function fetchFileAtCommit(commitHash: string, filePath: string): Promise<string | null> {
|
|
1007
|
+
const cwd = editor.getCwd();
|
|
1008
|
+
const result = await editor.spawnProcess("git", [
|
|
1009
|
+
"show",
|
|
1010
|
+
`${commitHash}:${filePath}`,
|
|
1011
|
+
], cwd);
|
|
1012
|
+
|
|
1013
|
+
if (result.exit_code !== 0) {
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
return result.stdout;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// Get language type from file extension
|
|
1021
|
+
function getLanguageFromPath(filePath: string): string {
|
|
1022
|
+
const ext = editor.pathExtname(filePath).toLowerCase();
|
|
1023
|
+
const extMap: Record<string, string> = {
|
|
1024
|
+
".rs": "rust",
|
|
1025
|
+
".ts": "typescript",
|
|
1026
|
+
".tsx": "typescript",
|
|
1027
|
+
".js": "javascript",
|
|
1028
|
+
".jsx": "javascript",
|
|
1029
|
+
".py": "python",
|
|
1030
|
+
".go": "go",
|
|
1031
|
+
".c": "c",
|
|
1032
|
+
".cpp": "cpp",
|
|
1033
|
+
".h": "c",
|
|
1034
|
+
".hpp": "cpp",
|
|
1035
|
+
".java": "java",
|
|
1036
|
+
".rb": "ruby",
|
|
1037
|
+
".sh": "shell",
|
|
1038
|
+
".bash": "shell",
|
|
1039
|
+
".zsh": "shell",
|
|
1040
|
+
".toml": "toml",
|
|
1041
|
+
".yaml": "yaml",
|
|
1042
|
+
".yml": "yaml",
|
|
1043
|
+
".json": "json",
|
|
1044
|
+
".md": "markdown",
|
|
1045
|
+
".css": "css",
|
|
1046
|
+
".html": "html",
|
|
1047
|
+
".xml": "xml",
|
|
1048
|
+
};
|
|
1049
|
+
return extMap[ext] || "text";
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Keywords for different languages
|
|
1053
|
+
const languageKeywords: Record<string, string[]> = {
|
|
1054
|
+
rust: ["fn", "let", "mut", "const", "pub", "use", "mod", "struct", "enum", "impl", "trait", "for", "while", "loop", "if", "else", "match", "return", "async", "await", "move", "self", "Self", "super", "crate", "where", "type", "static", "unsafe", "extern", "ref", "dyn", "as", "in", "true", "false"],
|
|
1055
|
+
typescript: ["function", "const", "let", "var", "class", "interface", "type", "extends", "implements", "import", "export", "from", "async", "await", "return", "if", "else", "for", "while", "do", "switch", "case", "break", "continue", "new", "this", "super", "null", "undefined", "true", "false", "try", "catch", "finally", "throw", "typeof", "instanceof", "void", "delete", "in", "of", "static", "readonly", "private", "public", "protected", "abstract", "enum"],
|
|
1056
|
+
javascript: ["function", "const", "let", "var", "class", "extends", "import", "export", "from", "async", "await", "return", "if", "else", "for", "while", "do", "switch", "case", "break", "continue", "new", "this", "super", "null", "undefined", "true", "false", "try", "catch", "finally", "throw", "typeof", "instanceof", "void", "delete", "in", "of", "static"],
|
|
1057
|
+
python: ["def", "class", "if", "elif", "else", "for", "while", "try", "except", "finally", "with", "as", "import", "from", "return", "yield", "raise", "pass", "break", "continue", "and", "or", "not", "in", "is", "lambda", "None", "True", "False", "global", "nonlocal", "async", "await", "self"],
|
|
1058
|
+
go: ["func", "var", "const", "type", "struct", "interface", "map", "chan", "if", "else", "for", "range", "switch", "case", "default", "break", "continue", "return", "go", "defer", "select", "import", "package", "nil", "true", "false", "make", "new", "len", "cap", "append", "copy", "delete", "panic", "recover"],
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
// Apply basic syntax highlighting to file view
|
|
1062
|
+
function applyFileViewHighlighting(bufferId: number, content: string, filePath: string): void {
|
|
1063
|
+
const language = getLanguageFromPath(filePath);
|
|
1064
|
+
const keywords = languageKeywords[language] || [];
|
|
1065
|
+
const lines = content.split("\n");
|
|
1066
|
+
|
|
1067
|
+
// Clear existing overlays
|
|
1068
|
+
editor.clearNamespace(bufferId, "syntax");
|
|
1069
|
+
|
|
1070
|
+
let byteOffset = 0;
|
|
1071
|
+
let inMultilineComment = false;
|
|
1072
|
+
let inMultilineString = false;
|
|
1073
|
+
|
|
1074
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
1075
|
+
const line = lines[lineIdx];
|
|
1076
|
+
const lineStart = byteOffset;
|
|
1077
|
+
|
|
1078
|
+
// Skip empty lines
|
|
1079
|
+
if (line.trim() === "") {
|
|
1080
|
+
byteOffset += line.length + 1;
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// Check for multiline comment start/end
|
|
1085
|
+
if (language === "rust" || language === "c" || language === "cpp" || language === "java" || language === "javascript" || language === "typescript" || language === "go") {
|
|
1086
|
+
if (line.includes("/*") && !line.includes("*/")) {
|
|
1087
|
+
inMultilineComment = true;
|
|
1088
|
+
}
|
|
1089
|
+
if (inMultilineComment) {
|
|
1090
|
+
editor.addOverlay(bufferId, "syntax", lineStart, lineStart + line.length, colors.syntaxComment[0], colors.syntaxComment[1], colors.syntaxComment[2], false, false, true);
|
|
1091
|
+
if (line.includes("*/")) {
|
|
1092
|
+
inMultilineComment = false;
|
|
1093
|
+
}
|
|
1094
|
+
byteOffset += line.length + 1;
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Python multiline strings
|
|
1100
|
+
if (language === "python" && (line.includes('"""') || line.includes("'''"))) {
|
|
1101
|
+
const tripleQuote = line.includes('"""') ? '"""' : "'''";
|
|
1102
|
+
const firstIdx = line.indexOf(tripleQuote);
|
|
1103
|
+
const secondIdx = line.indexOf(tripleQuote, firstIdx + 3);
|
|
1104
|
+
if (firstIdx >= 0 && secondIdx < 0) {
|
|
1105
|
+
inMultilineString = !inMultilineString;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
if (inMultilineString) {
|
|
1109
|
+
editor.addOverlay(bufferId, "syntax", lineStart, lineStart + line.length, colors.syntaxString[0], colors.syntaxString[1], colors.syntaxString[2], false, false, false);
|
|
1110
|
+
byteOffset += line.length + 1;
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Single-line comment detection
|
|
1115
|
+
let commentStart = -1;
|
|
1116
|
+
if (language === "rust" || language === "c" || language === "cpp" || language === "java" || language === "javascript" || language === "typescript" || language === "go") {
|
|
1117
|
+
commentStart = line.indexOf("//");
|
|
1118
|
+
} else if (language === "python" || language === "shell" || language === "ruby" || language === "yaml" || language === "toml") {
|
|
1119
|
+
commentStart = line.indexOf("#");
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
if (commentStart >= 0) {
|
|
1123
|
+
editor.addOverlay(bufferId, "syntax", lineStart + commentStart, lineStart + line.length, colors.syntaxComment[0], colors.syntaxComment[1], colors.syntaxComment[2], false, false, true);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// String highlighting (simple: find "..." and '...')
|
|
1127
|
+
let i = 0;
|
|
1128
|
+
let stringCount = 0;
|
|
1129
|
+
while (i < line.length) {
|
|
1130
|
+
const ch = line[i];
|
|
1131
|
+
if (ch === '"' || ch === "'") {
|
|
1132
|
+
const quote = ch;
|
|
1133
|
+
const start = i;
|
|
1134
|
+
i++;
|
|
1135
|
+
while (i < line.length && line[i] !== quote) {
|
|
1136
|
+
if (line[i] === '\\') i++; // Skip escaped chars
|
|
1137
|
+
i++;
|
|
1138
|
+
}
|
|
1139
|
+
if (i < line.length) i++; // Include closing quote
|
|
1140
|
+
const end = i;
|
|
1141
|
+
if (commentStart < 0 || start < commentStart) {
|
|
1142
|
+
editor.addOverlay(bufferId, "syntax", lineStart + start, lineStart + end, colors.syntaxString[0], colors.syntaxString[1], colors.syntaxString[2], false, false, false);
|
|
1143
|
+
}
|
|
1144
|
+
} else {
|
|
1145
|
+
i++;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Keyword highlighting
|
|
1150
|
+
for (const keyword of keywords) {
|
|
1151
|
+
const regex = new RegExp(`\\b${keyword}\\b`, "g");
|
|
1152
|
+
let match;
|
|
1153
|
+
while ((match = regex.exec(line)) !== null) {
|
|
1154
|
+
const kwStart = match.index;
|
|
1155
|
+
const kwEnd = kwStart + keyword.length;
|
|
1156
|
+
// Don't highlight if inside comment
|
|
1157
|
+
if (commentStart < 0 || kwStart < commentStart) {
|
|
1158
|
+
editor.addOverlay(bufferId, "syntax", lineStart + kwStart, lineStart + kwEnd, colors.syntaxKeyword[0], colors.syntaxKeyword[1], colors.syntaxKeyword[2], false, true, false);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Number highlighting
|
|
1164
|
+
const numberRegex = /\b\d+(\.\d+)?\b/g;
|
|
1165
|
+
let numMatch;
|
|
1166
|
+
while ((numMatch = numberRegex.exec(line)) !== null) {
|
|
1167
|
+
const numStart = numMatch.index;
|
|
1168
|
+
const numEnd = numStart + numMatch[0].length;
|
|
1169
|
+
if (commentStart < 0 || numStart < commentStart) {
|
|
1170
|
+
editor.addOverlay(bufferId, "syntax", lineStart + numStart, lineStart + numEnd, colors.syntaxNumber[0], colors.syntaxNumber[1], colors.syntaxNumber[2], false, false, false);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
byteOffset += line.length + 1;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// Open file at the current diff line position - shows file as it was at that commit
|
|
1179
|
+
globalThis.git_commit_detail_open_file = async function(): Promise<void> {
|
|
1180
|
+
if (!commitDetailState.isOpen || commitDetailState.bufferId === null) {
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const commit = commitDetailState.commit;
|
|
1185
|
+
if (!commit) {
|
|
1186
|
+
editor.setStatus(editor.t("status.move_to_commit"));
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// Get text properties at cursor position to find file/line info
|
|
1191
|
+
const props = editor.getTextPropertiesAtCursor(commitDetailState.bufferId);
|
|
1192
|
+
|
|
1193
|
+
if (props.length > 0) {
|
|
1194
|
+
const file = props[0].file as string | undefined;
|
|
1195
|
+
const line = props[0].line as number | undefined;
|
|
1196
|
+
|
|
1197
|
+
if (file) {
|
|
1198
|
+
editor.setStatus(editor.t("status.file_loading", { file, hash: commit.shortHash }));
|
|
1199
|
+
|
|
1200
|
+
// Fetch file content at this commit
|
|
1201
|
+
const content = await fetchFileAtCommit(commit.hash, file);
|
|
1202
|
+
|
|
1203
|
+
if (content === null) {
|
|
1204
|
+
editor.setStatus(editor.t("status.file_not_found", { file, hash: commit.shortHash }));
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Build entries for the virtual buffer - one entry per line for proper line tracking
|
|
1209
|
+
const lines = content.split("\n");
|
|
1210
|
+
const entries: TextPropertyEntry[] = [];
|
|
1211
|
+
|
|
1212
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1213
|
+
entries.push({
|
|
1214
|
+
text: lines[i] + (i < lines.length - 1 ? "\n" : ""),
|
|
1215
|
+
properties: { type: "content", line: i + 1 },
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Create a read-only virtual buffer with the file content
|
|
1220
|
+
const bufferId = await editor.createVirtualBufferInExistingSplit({
|
|
1221
|
+
name: `${file} @ ${commit.shortHash}`,
|
|
1222
|
+
mode: "git-file-view",
|
|
1223
|
+
read_only: true,
|
|
1224
|
+
entries: entries,
|
|
1225
|
+
split_id: commitDetailState.splitId!,
|
|
1226
|
+
show_line_numbers: true,
|
|
1227
|
+
show_cursors: true,
|
|
1228
|
+
editing_disabled: true,
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
if (bufferId !== null) {
|
|
1232
|
+
// Track file view state so we can navigate back
|
|
1233
|
+
fileViewState.isOpen = true;
|
|
1234
|
+
fileViewState.bufferId = bufferId;
|
|
1235
|
+
fileViewState.splitId = commitDetailState.splitId;
|
|
1236
|
+
fileViewState.filePath = file;
|
|
1237
|
+
fileViewState.commitHash = commit.hash;
|
|
1238
|
+
|
|
1239
|
+
// Apply syntax highlighting based on file type
|
|
1240
|
+
applyFileViewHighlighting(bufferId, content, file);
|
|
1241
|
+
|
|
1242
|
+
const targetLine = line || 1;
|
|
1243
|
+
editor.setStatus(editor.t("status.file_view_ready", { file, hash: commit.shortHash, line: String(targetLine) }));
|
|
1244
|
+
} else {
|
|
1245
|
+
editor.setStatus(editor.t("status.failed_open_file", { file }));
|
|
1246
|
+
}
|
|
1247
|
+
} else {
|
|
1248
|
+
editor.setStatus(editor.t("status.move_to_diff_with_context"));
|
|
1249
|
+
}
|
|
1250
|
+
} else {
|
|
1251
|
+
editor.setStatus(editor.t("status.move_to_diff"));
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
// =============================================================================
|
|
1256
|
+
// Command Registration
|
|
1257
|
+
// =============================================================================
|
|
1258
|
+
|
|
1259
|
+
editor.registerCommand(
|
|
1260
|
+
"%cmd.git_log",
|
|
1261
|
+
"%cmd.git_log_desc",
|
|
1262
|
+
"show_git_log",
|
|
1263
|
+
"normal"
|
|
1264
|
+
);
|
|
1265
|
+
|
|
1266
|
+
editor.registerCommand(
|
|
1267
|
+
"%cmd.git_log_close",
|
|
1268
|
+
"%cmd.git_log_close_desc",
|
|
1269
|
+
"git_log_close",
|
|
1270
|
+
"normal"
|
|
1271
|
+
);
|
|
1272
|
+
|
|
1273
|
+
editor.registerCommand(
|
|
1274
|
+
"%cmd.git_log_refresh",
|
|
1275
|
+
"%cmd.git_log_refresh_desc",
|
|
1276
|
+
"git_log_refresh",
|
|
1277
|
+
"normal"
|
|
1278
|
+
);
|
|
1279
|
+
|
|
1280
|
+
// =============================================================================
|
|
1281
|
+
// Plugin Initialization
|
|
1282
|
+
// =============================================================================
|
|
1283
|
+
|
|
1284
|
+
editor.setStatus(editor.t("status.ready", { count: "0" }));
|
|
1285
|
+
editor.debug("Git Log plugin initialized - Use 'Git Log' command to open");
|