@fresh-editor/fresh-editor 0.1.4 → 0.1.6
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/git_blame.ts +702 -0
- package/plugins/lib/fresh.d.ts +24 -0
- package/plugins/test_view_marker.ts +236 -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.6"
|
|
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.6"
|
|
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.6",
|
|
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.6",
|
|
96
96
|
"volta": {
|
|
97
97
|
"node": "18.14.1",
|
|
98
98
|
"npm": "9.5.0"
|
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
/// <reference path="../types/fresh.d.ts" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Git Blame Plugin - Magit-style Git Blame Interface
|
|
5
|
+
*
|
|
6
|
+
* Provides an interactive git blame view using Virtual Lines (Emacs-like model):
|
|
7
|
+
* - Virtual buffer contains pure file content (for syntax highlighting)
|
|
8
|
+
* - Virtual lines are added above each blame block using addVirtualLine API
|
|
9
|
+
* - Headers have dark gray background and no line numbers
|
|
10
|
+
* - Content lines preserve source line numbers and syntax highlighting
|
|
11
|
+
*
|
|
12
|
+
* This uses the persistent state model where:
|
|
13
|
+
* - Plugin adds virtual lines when blame data loads (async)
|
|
14
|
+
* - Render loop reads virtual lines synchronously from memory
|
|
15
|
+
* - No view transform hooks needed - eliminates frame lag issues
|
|
16
|
+
*
|
|
17
|
+
* Features:
|
|
18
|
+
* - 'b' to go back in history (show blame at parent commit)
|
|
19
|
+
* - 'q' to close the blame view
|
|
20
|
+
* - 'y' to yank (copy) the commit hash at cursor
|
|
21
|
+
*
|
|
22
|
+
* Inspired by magit's git-blame-additions feature.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// Types and Interfaces
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
interface BlameLine {
|
|
30
|
+
hash: string;
|
|
31
|
+
shortHash: string;
|
|
32
|
+
author: string;
|
|
33
|
+
authorTime: string; // Unix timestamp
|
|
34
|
+
relativeDate: string;
|
|
35
|
+
summary: string;
|
|
36
|
+
lineNumber: number; // Original line number
|
|
37
|
+
finalLineNumber: number; // Final line number in the file
|
|
38
|
+
content: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface BlameBlock {
|
|
42
|
+
hash: string;
|
|
43
|
+
shortHash: string;
|
|
44
|
+
author: string;
|
|
45
|
+
relativeDate: string;
|
|
46
|
+
summary: string;
|
|
47
|
+
lines: BlameLine[];
|
|
48
|
+
startLine: number; // First line number in block (1-indexed)
|
|
49
|
+
endLine: number; // Last line number in block (1-indexed)
|
|
50
|
+
startByte: number; // Start byte offset in the buffer
|
|
51
|
+
endByte: number; // End byte offset in the buffer
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface BlameState {
|
|
55
|
+
isOpen: boolean;
|
|
56
|
+
bufferId: number | null;
|
|
57
|
+
splitId: number | null;
|
|
58
|
+
sourceBufferId: number | null; // The buffer that was open before blame
|
|
59
|
+
sourceFilePath: string | null; // Path to the file being blamed
|
|
60
|
+
currentCommit: string | null; // Current commit being viewed (null = HEAD)
|
|
61
|
+
commitStack: string[]; // Stack of commits for navigation
|
|
62
|
+
blocks: BlameBlock[]; // Blame blocks with byte offsets
|
|
63
|
+
fileContent: string; // Pure file content (for virtual buffer)
|
|
64
|
+
lineByteOffsets: number[]; // Byte offset of each line start
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// State Management
|
|
69
|
+
// =============================================================================
|
|
70
|
+
|
|
71
|
+
const blameState: BlameState = {
|
|
72
|
+
isOpen: false,
|
|
73
|
+
bufferId: null,
|
|
74
|
+
splitId: null,
|
|
75
|
+
sourceBufferId: null,
|
|
76
|
+
sourceFilePath: null,
|
|
77
|
+
currentCommit: null,
|
|
78
|
+
commitStack: [],
|
|
79
|
+
blocks: [],
|
|
80
|
+
fileContent: "",
|
|
81
|
+
lineByteOffsets: [],
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// =============================================================================
|
|
85
|
+
// Color Definitions for Header Styling
|
|
86
|
+
// =============================================================================
|
|
87
|
+
|
|
88
|
+
const colors = {
|
|
89
|
+
headerFg: [0, 0, 0] as [number, number, number], // Black text
|
|
90
|
+
headerBg: [200, 200, 200] as [number, number, number], // Light gray background
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// =============================================================================
|
|
94
|
+
// Mode Definition
|
|
95
|
+
// =============================================================================
|
|
96
|
+
|
|
97
|
+
editor.defineMode(
|
|
98
|
+
"git-blame",
|
|
99
|
+
"normal", // inherit from normal mode for cursor movement
|
|
100
|
+
[
|
|
101
|
+
["b", "git_blame_go_back"],
|
|
102
|
+
["q", "git_blame_close"],
|
|
103
|
+
["Escape", "git_blame_close"],
|
|
104
|
+
["y", "git_blame_copy_hash"],
|
|
105
|
+
],
|
|
106
|
+
true // read-only
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// =============================================================================
|
|
110
|
+
// Git Blame Parsing
|
|
111
|
+
// =============================================================================
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Parse git blame --porcelain output
|
|
115
|
+
*/
|
|
116
|
+
async function fetchGitBlame(filePath: string, commit: string | null): Promise<BlameLine[]> {
|
|
117
|
+
const args = ["blame", "--porcelain"];
|
|
118
|
+
|
|
119
|
+
if (commit) {
|
|
120
|
+
args.push(commit);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
args.push("--", filePath);
|
|
124
|
+
|
|
125
|
+
const result = await editor.spawnProcess("git", args);
|
|
126
|
+
|
|
127
|
+
if (result.exit_code !== 0) {
|
|
128
|
+
editor.setStatus(`Git blame error: ${result.stderr}`);
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const lines: BlameLine[] = [];
|
|
133
|
+
const output = result.stdout;
|
|
134
|
+
const outputLines = output.split("\n");
|
|
135
|
+
|
|
136
|
+
let currentHash = "";
|
|
137
|
+
let currentAuthor = "";
|
|
138
|
+
let currentAuthorTime = "";
|
|
139
|
+
let currentSummary = "";
|
|
140
|
+
let currentOrigLine = 0;
|
|
141
|
+
let currentFinalLine = 0;
|
|
142
|
+
|
|
143
|
+
// Cache for commit info to avoid redundant parsing
|
|
144
|
+
const commitInfo: Map<string, { author: string; authorTime: string; summary: string }> = new Map();
|
|
145
|
+
|
|
146
|
+
for (let i = 0; i < outputLines.length; i++) {
|
|
147
|
+
const line = outputLines[i];
|
|
148
|
+
|
|
149
|
+
// Check for commit line: <hash> <orig-line> <final-line> [num-lines]
|
|
150
|
+
const commitMatch = line.match(/^([a-f0-9]{40}) (\d+) (\d+)/);
|
|
151
|
+
if (commitMatch) {
|
|
152
|
+
currentHash = commitMatch[1];
|
|
153
|
+
currentOrigLine = parseInt(commitMatch[2], 10);
|
|
154
|
+
currentFinalLine = parseInt(commitMatch[3], 10);
|
|
155
|
+
|
|
156
|
+
// Check cache for this commit's info
|
|
157
|
+
const cached = commitInfo.get(currentHash);
|
|
158
|
+
if (cached) {
|
|
159
|
+
currentAuthor = cached.author;
|
|
160
|
+
currentAuthorTime = cached.authorTime;
|
|
161
|
+
currentSummary = cached.summary;
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Parse header fields
|
|
167
|
+
if (line.startsWith("author ")) {
|
|
168
|
+
currentAuthor = line.slice(7);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (line.startsWith("author-time ")) {
|
|
172
|
+
currentAuthorTime = line.slice(12);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (line.startsWith("summary ")) {
|
|
176
|
+
currentSummary = line.slice(8);
|
|
177
|
+
// Cache this commit's info
|
|
178
|
+
commitInfo.set(currentHash, {
|
|
179
|
+
author: currentAuthor,
|
|
180
|
+
authorTime: currentAuthorTime,
|
|
181
|
+
summary: currentSummary,
|
|
182
|
+
});
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Content line (starts with tab)
|
|
187
|
+
if (line.startsWith("\t")) {
|
|
188
|
+
const content = line.slice(1);
|
|
189
|
+
|
|
190
|
+
// Calculate relative date from author-time
|
|
191
|
+
const relativeDate = formatRelativeDate(parseInt(currentAuthorTime, 10));
|
|
192
|
+
|
|
193
|
+
lines.push({
|
|
194
|
+
hash: currentHash,
|
|
195
|
+
shortHash: currentHash.slice(0, 7),
|
|
196
|
+
author: currentAuthor,
|
|
197
|
+
authorTime: currentAuthorTime,
|
|
198
|
+
relativeDate: relativeDate,
|
|
199
|
+
summary: currentSummary,
|
|
200
|
+
lineNumber: currentOrigLine,
|
|
201
|
+
finalLineNumber: currentFinalLine,
|
|
202
|
+
content: content,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return lines;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Format a unix timestamp as a relative date string
|
|
212
|
+
*/
|
|
213
|
+
function formatRelativeDate(timestamp: number): string {
|
|
214
|
+
const now = Math.floor(Date.now() / 1000);
|
|
215
|
+
const diff = now - timestamp;
|
|
216
|
+
|
|
217
|
+
if (diff < 60) {
|
|
218
|
+
return "just now";
|
|
219
|
+
} else if (diff < 3600) {
|
|
220
|
+
const mins = Math.floor(diff / 60);
|
|
221
|
+
return `${mins} minute${mins > 1 ? "s" : ""} ago`;
|
|
222
|
+
} else if (diff < 86400) {
|
|
223
|
+
const hours = Math.floor(diff / 3600);
|
|
224
|
+
return `${hours} hour${hours > 1 ? "s" : ""} ago`;
|
|
225
|
+
} else if (diff < 604800) {
|
|
226
|
+
const days = Math.floor(diff / 86400);
|
|
227
|
+
return `${days} day${days > 1 ? "s" : ""} ago`;
|
|
228
|
+
} else if (diff < 2592000) {
|
|
229
|
+
const weeks = Math.floor(diff / 604800);
|
|
230
|
+
return `${weeks} week${weeks > 1 ? "s" : ""} ago`;
|
|
231
|
+
} else if (diff < 31536000) {
|
|
232
|
+
const months = Math.floor(diff / 2592000);
|
|
233
|
+
return `${months} month${months > 1 ? "s" : ""} ago`;
|
|
234
|
+
} else {
|
|
235
|
+
const years = Math.floor(diff / 31536000);
|
|
236
|
+
return `${years} year${years > 1 ? "s" : ""} ago`;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Fetch file content at a specific commit (or HEAD)
|
|
242
|
+
*/
|
|
243
|
+
async function fetchFileContent(filePath: string, commit: string | null): Promise<string> {
|
|
244
|
+
if (commit) {
|
|
245
|
+
// Get historical file content
|
|
246
|
+
const result = await editor.spawnProcess("git", ["show", `${commit}:${filePath}`]);
|
|
247
|
+
if (result.exit_code === 0) {
|
|
248
|
+
return result.stdout;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Get current file content
|
|
253
|
+
const result = await editor.spawnProcess("cat", [filePath]);
|
|
254
|
+
return result.exit_code === 0 ? result.stdout : "";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Build line byte offset lookup table
|
|
259
|
+
*/
|
|
260
|
+
function buildLineByteOffsets(content: string): number[] {
|
|
261
|
+
const offsets: number[] = [0]; // Line 1 starts at byte 0
|
|
262
|
+
let byteOffset = 0;
|
|
263
|
+
|
|
264
|
+
for (const char of content) {
|
|
265
|
+
byteOffset += char.length; // In JS strings, each char is at least 1
|
|
266
|
+
if (char === '\n') {
|
|
267
|
+
offsets.push(byteOffset);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return offsets;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get byte offset for a given line number (1-indexed)
|
|
276
|
+
*/
|
|
277
|
+
function getLineByteOffset(lineNum: number): number {
|
|
278
|
+
if (lineNum <= 0) return 0;
|
|
279
|
+
const idx = lineNum - 1;
|
|
280
|
+
if (idx < blameState.lineByteOffsets.length) {
|
|
281
|
+
return blameState.lineByteOffsets[idx];
|
|
282
|
+
}
|
|
283
|
+
// Return end of file if line number is out of range
|
|
284
|
+
return blameState.fileContent.length;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Group blame lines into blocks by commit, with byte offset information
|
|
289
|
+
*/
|
|
290
|
+
function groupIntoBlocks(lines: BlameLine[]): BlameBlock[] {
|
|
291
|
+
const blocks: BlameBlock[] = [];
|
|
292
|
+
let currentBlock: BlameBlock | null = null;
|
|
293
|
+
|
|
294
|
+
for (const line of lines) {
|
|
295
|
+
// Check if we need to start a new block
|
|
296
|
+
if (!currentBlock || currentBlock.hash !== line.hash) {
|
|
297
|
+
// Save previous block
|
|
298
|
+
if (currentBlock && currentBlock.lines.length > 0) {
|
|
299
|
+
currentBlock.endByte = getLineByteOffset(currentBlock.endLine + 1);
|
|
300
|
+
blocks.push(currentBlock);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Start new block
|
|
304
|
+
currentBlock = {
|
|
305
|
+
hash: line.hash,
|
|
306
|
+
shortHash: line.shortHash,
|
|
307
|
+
author: line.author,
|
|
308
|
+
relativeDate: line.relativeDate,
|
|
309
|
+
summary: line.summary,
|
|
310
|
+
lines: [],
|
|
311
|
+
startLine: line.finalLineNumber,
|
|
312
|
+
endLine: line.finalLineNumber,
|
|
313
|
+
startByte: getLineByteOffset(line.finalLineNumber),
|
|
314
|
+
endByte: 0, // Will be set when block is complete
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
currentBlock.lines.push(line);
|
|
319
|
+
currentBlock.endLine = line.finalLineNumber;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Don't forget the last block
|
|
323
|
+
if (currentBlock && currentBlock.lines.length > 0) {
|
|
324
|
+
currentBlock.endByte = getLineByteOffset(currentBlock.endLine + 1);
|
|
325
|
+
blocks.push(currentBlock);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return blocks;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// =============================================================================
|
|
332
|
+
// Virtual Lines (Emacs-like persistent state model)
|
|
333
|
+
// =============================================================================
|
|
334
|
+
|
|
335
|
+
const BLAME_NAMESPACE = "git-blame";
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Format a header line for a blame block
|
|
339
|
+
*/
|
|
340
|
+
function formatBlockHeader(block: BlameBlock): string {
|
|
341
|
+
// Truncate summary if too long
|
|
342
|
+
const maxSummaryLen = 50;
|
|
343
|
+
const summary = block.summary.length > maxSummaryLen
|
|
344
|
+
? block.summary.slice(0, maxSummaryLen - 3) + "..."
|
|
345
|
+
: block.summary;
|
|
346
|
+
|
|
347
|
+
return `── ${block.shortHash} (${block.author}, ${block.relativeDate}) "${summary}" ──`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Find which block (if any) starts at or before the given byte offset
|
|
352
|
+
*/
|
|
353
|
+
function findBlockForByteOffset(byteOffset: number): BlameBlock | null {
|
|
354
|
+
for (const block of blameState.blocks) {
|
|
355
|
+
if (byteOffset >= block.startByte && byteOffset < block.endByte) {
|
|
356
|
+
return block;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Add virtual lines for all blame block headers
|
|
364
|
+
* Called when blame data is loaded or updated
|
|
365
|
+
*/
|
|
366
|
+
function addBlameHeaders(): void {
|
|
367
|
+
if (blameState.bufferId === null) return;
|
|
368
|
+
|
|
369
|
+
// Clear existing headers first
|
|
370
|
+
editor.clearVirtualTextNamespace(blameState.bufferId, BLAME_NAMESPACE);
|
|
371
|
+
|
|
372
|
+
// Add a virtual line above each block
|
|
373
|
+
for (const block of blameState.blocks) {
|
|
374
|
+
const headerText = formatBlockHeader(block);
|
|
375
|
+
|
|
376
|
+
editor.addVirtualLine(
|
|
377
|
+
blameState.bufferId,
|
|
378
|
+
block.startByte, // anchor position
|
|
379
|
+
headerText, // text content
|
|
380
|
+
colors.headerFg[0], // fg_r
|
|
381
|
+
colors.headerFg[1], // fg_g
|
|
382
|
+
colors.headerFg[2], // fg_b
|
|
383
|
+
colors.headerBg[0], // bg_r
|
|
384
|
+
colors.headerBg[1], // bg_g
|
|
385
|
+
colors.headerBg[2], // bg_b
|
|
386
|
+
true, // above (LineAbove)
|
|
387
|
+
BLAME_NAMESPACE, // namespace for bulk removal
|
|
388
|
+
0 // priority
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
editor.debug(`Added ${blameState.blocks.length} blame header virtual lines`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// =============================================================================
|
|
396
|
+
// Public Commands
|
|
397
|
+
// =============================================================================
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Show git blame for the current file
|
|
401
|
+
*/
|
|
402
|
+
globalThis.show_git_blame = async function(): Promise<void> {
|
|
403
|
+
if (blameState.isOpen) {
|
|
404
|
+
editor.setStatus("Git blame already open");
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Get current file path
|
|
409
|
+
const activeBufferId = editor.getActiveBufferId();
|
|
410
|
+
const filePath = editor.getBufferPath(activeBufferId);
|
|
411
|
+
if (!filePath || filePath === "") {
|
|
412
|
+
editor.setStatus("No file open to blame");
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
editor.setStatus("Loading git blame...");
|
|
417
|
+
|
|
418
|
+
// Store state before opening blame
|
|
419
|
+
blameState.splitId = editor.getActiveSplitId();
|
|
420
|
+
blameState.sourceBufferId = activeBufferId;
|
|
421
|
+
blameState.sourceFilePath = filePath;
|
|
422
|
+
blameState.currentCommit = null;
|
|
423
|
+
blameState.commitStack = [];
|
|
424
|
+
|
|
425
|
+
// Fetch file content and blame data in parallel
|
|
426
|
+
const [fileContent, blameLines] = await Promise.all([
|
|
427
|
+
fetchFileContent(filePath, null),
|
|
428
|
+
fetchGitBlame(filePath, null),
|
|
429
|
+
]);
|
|
430
|
+
|
|
431
|
+
if (blameLines.length === 0) {
|
|
432
|
+
editor.setStatus("No blame information available (not a git file or error)");
|
|
433
|
+
resetState();
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Store file content and build line offset table
|
|
438
|
+
blameState.fileContent = fileContent;
|
|
439
|
+
blameState.lineByteOffsets = buildLineByteOffsets(fileContent);
|
|
440
|
+
|
|
441
|
+
// Group into blocks with byte offsets
|
|
442
|
+
blameState.blocks = groupIntoBlocks(blameLines);
|
|
443
|
+
|
|
444
|
+
// Get file extension for language detection
|
|
445
|
+
const ext = filePath.includes('.') ? filePath.split('.').pop() : '';
|
|
446
|
+
const bufferName = `*blame:${editor.pathBasename(filePath)}*`;
|
|
447
|
+
|
|
448
|
+
// Create virtual buffer with PURE file content (for syntax highlighting)
|
|
449
|
+
// Virtual lines will be added after buffer creation
|
|
450
|
+
const entries: TextPropertyEntry[] = [];
|
|
451
|
+
|
|
452
|
+
// We need to track which line belongs to which block for text properties
|
|
453
|
+
let lineNum = 1;
|
|
454
|
+
const contentLines = fileContent.split('\n');
|
|
455
|
+
let byteOffset = 0;
|
|
456
|
+
|
|
457
|
+
for (const line of contentLines) {
|
|
458
|
+
// Find the block for this line
|
|
459
|
+
const block = findBlockForByteOffset(byteOffset);
|
|
460
|
+
|
|
461
|
+
entries.push({
|
|
462
|
+
text: line + (lineNum < contentLines.length || fileContent.endsWith('\n') ? '\n' : ''),
|
|
463
|
+
properties: {
|
|
464
|
+
type: "content",
|
|
465
|
+
hash: block?.hash ?? null,
|
|
466
|
+
shortHash: block?.shortHash ?? null,
|
|
467
|
+
lineNumber: lineNum,
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
byteOffset += line.length + 1; // +1 for newline
|
|
472
|
+
lineNum++;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Create virtual buffer with the file content
|
|
476
|
+
const bufferId = await editor.createVirtualBufferInExistingSplit({
|
|
477
|
+
name: bufferName,
|
|
478
|
+
mode: "git-blame",
|
|
479
|
+
read_only: true,
|
|
480
|
+
entries: entries,
|
|
481
|
+
split_id: blameState.splitId!,
|
|
482
|
+
show_line_numbers: true, // We DO want line numbers (headers won't have them due to source_offset: null)
|
|
483
|
+
show_cursors: true,
|
|
484
|
+
editing_disabled: true,
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
if (bufferId !== null) {
|
|
488
|
+
blameState.isOpen = true;
|
|
489
|
+
blameState.bufferId = bufferId;
|
|
490
|
+
|
|
491
|
+
// Add virtual lines for blame headers (persistent state model)
|
|
492
|
+
addBlameHeaders();
|
|
493
|
+
|
|
494
|
+
editor.setStatus(`Git blame: ${blameState.blocks.length} blocks | b: blame at parent | q: close`);
|
|
495
|
+
editor.debug("Git blame panel opened with virtual lines architecture");
|
|
496
|
+
} else {
|
|
497
|
+
resetState();
|
|
498
|
+
editor.setStatus("Failed to open git blame panel");
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Reset blame state
|
|
504
|
+
*/
|
|
505
|
+
function resetState(): void {
|
|
506
|
+
blameState.splitId = null;
|
|
507
|
+
blameState.sourceBufferId = null;
|
|
508
|
+
blameState.sourceFilePath = null;
|
|
509
|
+
blameState.currentCommit = null;
|
|
510
|
+
blameState.commitStack = [];
|
|
511
|
+
blameState.blocks = [];
|
|
512
|
+
blameState.fileContent = "";
|
|
513
|
+
blameState.lineByteOffsets = [];
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Close the git blame view
|
|
518
|
+
*/
|
|
519
|
+
globalThis.git_blame_close = function(): void {
|
|
520
|
+
if (!blameState.isOpen) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Restore the original buffer in the split
|
|
525
|
+
if (blameState.splitId !== null && blameState.sourceBufferId !== null) {
|
|
526
|
+
editor.setSplitBuffer(blameState.splitId, blameState.sourceBufferId);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Close the blame buffer
|
|
530
|
+
if (blameState.bufferId !== null) {
|
|
531
|
+
editor.closeBuffer(blameState.bufferId);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
blameState.isOpen = false;
|
|
535
|
+
blameState.bufferId = null;
|
|
536
|
+
resetState();
|
|
537
|
+
|
|
538
|
+
editor.setStatus("Git blame closed");
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Get the commit hash at the current cursor position
|
|
543
|
+
*/
|
|
544
|
+
function getCommitAtCursor(): string | null {
|
|
545
|
+
if (blameState.bufferId === null) return null;
|
|
546
|
+
|
|
547
|
+
const props = editor.getTextPropertiesAtCursor(blameState.bufferId);
|
|
548
|
+
|
|
549
|
+
if (props.length > 0) {
|
|
550
|
+
const hash = props[0].hash as string | undefined;
|
|
551
|
+
if (hash) {
|
|
552
|
+
return hash;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Navigate to blame at the parent commit of the current line's commit
|
|
561
|
+
*/
|
|
562
|
+
globalThis.git_blame_go_back = async function(): Promise<void> {
|
|
563
|
+
if (!blameState.isOpen || !blameState.sourceFilePath) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const currentHash = getCommitAtCursor();
|
|
568
|
+
if (!currentHash) {
|
|
569
|
+
editor.setStatus("Move cursor to a blame line first");
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Skip if this is the "not committed yet" hash (all zeros)
|
|
574
|
+
if (currentHash === "0000000000000000000000000000000000000000") {
|
|
575
|
+
editor.setStatus("This line is not yet committed");
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
editor.setStatus(`Loading blame at ${currentHash.slice(0, 7)}^...`);
|
|
580
|
+
|
|
581
|
+
// Get the parent commit
|
|
582
|
+
const parentCommit = `${currentHash}^`;
|
|
583
|
+
|
|
584
|
+
// Push current state to stack for potential future navigation
|
|
585
|
+
if (blameState.currentCommit) {
|
|
586
|
+
blameState.commitStack.push(blameState.currentCommit);
|
|
587
|
+
} else {
|
|
588
|
+
blameState.commitStack.push("HEAD");
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Fetch file content and blame at parent commit
|
|
592
|
+
const [fileContent, blameLines] = await Promise.all([
|
|
593
|
+
fetchFileContent(blameState.sourceFilePath, parentCommit),
|
|
594
|
+
fetchGitBlame(blameState.sourceFilePath, parentCommit),
|
|
595
|
+
]);
|
|
596
|
+
|
|
597
|
+
if (blameLines.length === 0) {
|
|
598
|
+
// Pop the stack since we couldn't navigate
|
|
599
|
+
blameState.commitStack.pop();
|
|
600
|
+
editor.setStatus(`Cannot get blame at ${currentHash.slice(0, 7)}^ (may be initial commit or file didn't exist)`);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Update state
|
|
605
|
+
blameState.currentCommit = parentCommit;
|
|
606
|
+
blameState.fileContent = fileContent;
|
|
607
|
+
blameState.lineByteOffsets = buildLineByteOffsets(fileContent);
|
|
608
|
+
blameState.blocks = groupIntoBlocks(blameLines);
|
|
609
|
+
|
|
610
|
+
// Update virtual buffer content
|
|
611
|
+
if (blameState.bufferId !== null) {
|
|
612
|
+
const entries: TextPropertyEntry[] = [];
|
|
613
|
+
let lineNum = 1;
|
|
614
|
+
const contentLines = fileContent.split('\n');
|
|
615
|
+
let byteOffset = 0;
|
|
616
|
+
|
|
617
|
+
for (const line of contentLines) {
|
|
618
|
+
const block = findBlockForByteOffset(byteOffset);
|
|
619
|
+
|
|
620
|
+
entries.push({
|
|
621
|
+
text: line + (lineNum < contentLines.length || fileContent.endsWith('\n') ? '\n' : ''),
|
|
622
|
+
properties: {
|
|
623
|
+
type: "content",
|
|
624
|
+
hash: block?.hash ?? null,
|
|
625
|
+
shortHash: block?.shortHash ?? null,
|
|
626
|
+
lineNumber: lineNum,
|
|
627
|
+
},
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
byteOffset += line.length + 1;
|
|
631
|
+
lineNum++;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
editor.setVirtualBufferContent(blameState.bufferId, entries);
|
|
635
|
+
|
|
636
|
+
// Re-add virtual lines for the new blame data
|
|
637
|
+
addBlameHeaders();
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const depth = blameState.commitStack.length;
|
|
641
|
+
editor.setStatus(`Git blame at ${currentHash.slice(0, 7)}^ | depth: ${depth} | b: go deeper | q: close`);
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Copy the commit hash at cursor to clipboard
|
|
646
|
+
*/
|
|
647
|
+
globalThis.git_blame_copy_hash = function(): void {
|
|
648
|
+
if (!blameState.isOpen) return;
|
|
649
|
+
|
|
650
|
+
const hash = getCommitAtCursor();
|
|
651
|
+
if (!hash) {
|
|
652
|
+
editor.setStatus("Move cursor to a blame line first");
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Skip if this is the "not committed yet" hash
|
|
657
|
+
if (hash === "0000000000000000000000000000000000000000") {
|
|
658
|
+
editor.setStatus("This line is not yet committed");
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Use spawn to copy to clipboard
|
|
663
|
+
editor.spawnProcess("sh", ["-c", `echo -n "${hash}" | xclip -selection clipboard 2>/dev/null || echo -n "${hash}" | pbcopy 2>/dev/null || echo -n "${hash}" | xsel --clipboard 2>/dev/null`])
|
|
664
|
+
.then(() => {
|
|
665
|
+
editor.setStatus(`Copied: ${hash.slice(0, 7)} (${hash})`);
|
|
666
|
+
})
|
|
667
|
+
.catch(() => {
|
|
668
|
+
editor.setStatus(`Hash: ${hash}`);
|
|
669
|
+
});
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
// =============================================================================
|
|
673
|
+
// Command Registration
|
|
674
|
+
// =============================================================================
|
|
675
|
+
|
|
676
|
+
editor.registerCommand(
|
|
677
|
+
"Git Blame",
|
|
678
|
+
"Show git blame for current file (magit-style)",
|
|
679
|
+
"show_git_blame",
|
|
680
|
+
"normal"
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
editor.registerCommand(
|
|
684
|
+
"Git Blame: Close",
|
|
685
|
+
"Close the git blame panel",
|
|
686
|
+
"git_blame_close",
|
|
687
|
+
"normal"
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
editor.registerCommand(
|
|
691
|
+
"Git Blame: Go Back",
|
|
692
|
+
"Show blame at parent commit of current line",
|
|
693
|
+
"git_blame_go_back",
|
|
694
|
+
"normal"
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
// =============================================================================
|
|
698
|
+
// Plugin Initialization
|
|
699
|
+
// =============================================================================
|
|
700
|
+
|
|
701
|
+
editor.setStatus("Git Blame plugin loaded (virtual lines architecture)");
|
|
702
|
+
editor.debug("Git Blame plugin initialized - Use 'Git Blame' command to open");
|
package/plugins/lib/fresh.d.ts
CHANGED
|
@@ -403,6 +403,23 @@ interface EditorAPI {
|
|
|
403
403
|
* @returns true if successful
|
|
404
404
|
*/
|
|
405
405
|
setLineNumbers(buffer_id: number, enabled: boolean): boolean;
|
|
406
|
+
/**
|
|
407
|
+
* Add a virtual line above or below a source line
|
|
408
|
+
* @param buffer_id - The buffer ID
|
|
409
|
+
* @param position - Byte position to anchor the virtual line to
|
|
410
|
+
* @param text - The text content of the virtual line
|
|
411
|
+
* @param fg_r - Foreground red color component (0-255)
|
|
412
|
+
* @param fg_g - Foreground green color component (0-255)
|
|
413
|
+
* @param fg_b - Foreground blue color component (0-255)
|
|
414
|
+
* @param bg_r - Background red color component (0-255), -1 for transparent
|
|
415
|
+
* @param bg_g - Background green color component (0-255), -1 for transparent
|
|
416
|
+
* @param bg_b - Background blue color component (0-255), -1 for transparent
|
|
417
|
+
* @param above - Whether to insert above (true) or below (false) the line
|
|
418
|
+
* @param namespace - Namespace for bulk removal (e.g., "git-blame")
|
|
419
|
+
* @param priority - Priority for ordering multiple lines at same position
|
|
420
|
+
* @returns true if virtual line was added
|
|
421
|
+
*/
|
|
422
|
+
addVirtualLine(buffer_id: number, position: number, text: string, fg_r: number, fg_g: number, fg_b: number, bg_r: number, bg_g: number, bg_b: number, above: boolean, namespace: string, priority: number): boolean;
|
|
406
423
|
/**
|
|
407
424
|
* Submit a transformed view stream for a viewport
|
|
408
425
|
* @param buffer_id - Buffer to apply the transform to
|
|
@@ -580,6 +597,13 @@ interface EditorAPI {
|
|
|
580
597
|
* @returns true if virtual texts were cleared
|
|
581
598
|
*/
|
|
582
599
|
clearVirtualTexts(buffer_id: number): boolean;
|
|
600
|
+
/**
|
|
601
|
+
* Clear all virtual texts in a namespace
|
|
602
|
+
* @param buffer_id - The buffer ID
|
|
603
|
+
* @param namespace - The namespace to clear (e.g., "git-blame")
|
|
604
|
+
* @returns true if namespace was cleared
|
|
605
|
+
*/
|
|
606
|
+
clearVirtualTextNamespace(buffer_id: number, namespace: string): boolean;
|
|
583
607
|
/**
|
|
584
608
|
* Force a refresh of line display for a buffer
|
|
585
609
|
* @param buffer_id - The buffer ID
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/// <reference path="../types/fresh.d.ts" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test plugin for virtual lines (Emacs-like persistent state model).
|
|
5
|
+
*
|
|
6
|
+
* This plugin demonstrates the virtual lines API by injecting header lines
|
|
7
|
+
* above content. Virtual lines are added to persistent state and rendered
|
|
8
|
+
* synchronously from memory - no view transform hooks needed.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let activeBufferId: number | null = null;
|
|
12
|
+
const padLinesByBuffer = new Map<number, number>();
|
|
13
|
+
const interleavedModeByBuffer = new Map<number, boolean>();
|
|
14
|
+
|
|
15
|
+
const TEST_NAMESPACE = "test-view-marker";
|
|
16
|
+
|
|
17
|
+
// Define a simple mode for testing
|
|
18
|
+
editor.defineMode(
|
|
19
|
+
"test-view-marker",
|
|
20
|
+
"normal",
|
|
21
|
+
[
|
|
22
|
+
["q", "test_view_marker_close"],
|
|
23
|
+
],
|
|
24
|
+
true // read-only
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Add virtual header lines for a buffer
|
|
29
|
+
*/
|
|
30
|
+
function addTestHeaders(bufferId: number): void {
|
|
31
|
+
const padLines = padLinesByBuffer.get(bufferId) ?? 0;
|
|
32
|
+
const interleavedMode = interleavedModeByBuffer.get(bufferId) ?? false;
|
|
33
|
+
|
|
34
|
+
// Clear existing headers first
|
|
35
|
+
editor.clearVirtualTextNamespace(bufferId, TEST_NAMESPACE);
|
|
36
|
+
|
|
37
|
+
if (interleavedMode) {
|
|
38
|
+
// Interleaved mode: add a header above each source line
|
|
39
|
+
const content = "Line 1\nLine 2\nLine 3\n";
|
|
40
|
+
let byteOffset = 0;
|
|
41
|
+
let lineNum = 1;
|
|
42
|
+
|
|
43
|
+
for (const line of content.split('\n')) {
|
|
44
|
+
if (line.length === 0 && byteOffset >= content.length - 1) break;
|
|
45
|
+
|
|
46
|
+
// Add header above this line
|
|
47
|
+
editor.addVirtualLine(
|
|
48
|
+
bufferId,
|
|
49
|
+
byteOffset,
|
|
50
|
+
`── Header before line ${lineNum} ──`,
|
|
51
|
+
200, 200, 100, // Yellow-ish fg
|
|
52
|
+
-1, -1, -1, // No background
|
|
53
|
+
true, // above
|
|
54
|
+
TEST_NAMESPACE,
|
|
55
|
+
0
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
byteOffset += line.length + 1; // +1 for newline
|
|
59
|
+
lineNum++;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Also add initial header at byte 0
|
|
63
|
+
editor.addVirtualLine(
|
|
64
|
+
bufferId,
|
|
65
|
+
0,
|
|
66
|
+
"== INTERLEAVED HEADER ==",
|
|
67
|
+
255, 255, 0, // Yellow fg
|
|
68
|
+
-1, -1, -1, // No background
|
|
69
|
+
true, // above
|
|
70
|
+
TEST_NAMESPACE,
|
|
71
|
+
-1 // lower priority to appear first
|
|
72
|
+
);
|
|
73
|
+
} else {
|
|
74
|
+
// Simple mode: just one header at byte 0
|
|
75
|
+
editor.addVirtualLine(
|
|
76
|
+
bufferId,
|
|
77
|
+
0,
|
|
78
|
+
"== HEADER AT BYTE 0 ==",
|
|
79
|
+
255, 255, 0, // Yellow fg
|
|
80
|
+
-1, -1, -1, // No background
|
|
81
|
+
true, // above
|
|
82
|
+
TEST_NAMESPACE,
|
|
83
|
+
0
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Optionally add many pad lines (for scroll stress tests)
|
|
87
|
+
for (let i = 0; i < padLines; i++) {
|
|
88
|
+
editor.addVirtualLine(
|
|
89
|
+
bufferId,
|
|
90
|
+
0,
|
|
91
|
+
`Virtual pad ${i + 1}`,
|
|
92
|
+
180, 180, 180, // Light gray fg
|
|
93
|
+
-1, -1, -1, // No background
|
|
94
|
+
true, // above
|
|
95
|
+
TEST_NAMESPACE,
|
|
96
|
+
i + 1 // increasing priority so they appear in order after header
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
editor.debug(`[test_view_marker] added ${interleavedMode ? 'interleaved' : 'simple'} headers with ${padLines} pad lines`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function open_test_view_marker(padLines: number, name: string): Promise<void> {
|
|
105
|
+
const splitId = editor.getActiveSplitId();
|
|
106
|
+
|
|
107
|
+
editor.debug(
|
|
108
|
+
`[test_view_marker] opening view marker in split ${splitId} with ${padLines} pad lines`
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Create virtual buffer with simple hardcoded content
|
|
112
|
+
const entries: TextPropertyEntry[] = [
|
|
113
|
+
{ text: "Line 1\n", properties: { type: "content", line: 1 } },
|
|
114
|
+
{ text: "Line 2\n", properties: { type: "content", line: 2 } },
|
|
115
|
+
{ text: "Line 3\n", properties: { type: "content", line: 3 } },
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
const bufferId = await editor.createVirtualBufferInExistingSplit({
|
|
119
|
+
name,
|
|
120
|
+
mode: "test-view-marker",
|
|
121
|
+
read_only: true,
|
|
122
|
+
entries,
|
|
123
|
+
split_id: splitId,
|
|
124
|
+
show_line_numbers: true,
|
|
125
|
+
show_cursors: true,
|
|
126
|
+
editing_disabled: true,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (bufferId !== null) {
|
|
130
|
+
activeBufferId = bufferId;
|
|
131
|
+
padLinesByBuffer.set(bufferId, padLines);
|
|
132
|
+
interleavedModeByBuffer.set(bufferId, false);
|
|
133
|
+
|
|
134
|
+
// Add virtual header lines
|
|
135
|
+
addTestHeaders(bufferId);
|
|
136
|
+
|
|
137
|
+
editor.debug(
|
|
138
|
+
`[test_view_marker] buffer created with id ${bufferId}, padLines=${padLines}`
|
|
139
|
+
);
|
|
140
|
+
editor.setStatus("Test view marker active - press q to close");
|
|
141
|
+
} else {
|
|
142
|
+
editor.debug(`[test_view_marker] failed to create buffer`);
|
|
143
|
+
editor.setStatus("Failed to create test view marker buffer");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function open_test_view_marker_interleaved(name: string): Promise<void> {
|
|
148
|
+
const splitId = editor.getActiveSplitId();
|
|
149
|
+
|
|
150
|
+
editor.debug(`[test_view_marker] opening interleaved view marker in split ${splitId}`);
|
|
151
|
+
|
|
152
|
+
// Create virtual buffer with simple hardcoded content
|
|
153
|
+
const entries: TextPropertyEntry[] = [
|
|
154
|
+
{ text: "Line 1\n", properties: { type: "content", line: 1 } },
|
|
155
|
+
{ text: "Line 2\n", properties: { type: "content", line: 2 } },
|
|
156
|
+
{ text: "Line 3\n", properties: { type: "content", line: 3 } },
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
const bufferId = await editor.createVirtualBufferInExistingSplit({
|
|
160
|
+
name,
|
|
161
|
+
mode: "test-view-marker",
|
|
162
|
+
read_only: true,
|
|
163
|
+
entries,
|
|
164
|
+
split_id: splitId,
|
|
165
|
+
show_line_numbers: true,
|
|
166
|
+
show_cursors: true,
|
|
167
|
+
editing_disabled: true,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (bufferId !== null) {
|
|
171
|
+
activeBufferId = bufferId;
|
|
172
|
+
padLinesByBuffer.set(bufferId, 0);
|
|
173
|
+
interleavedModeByBuffer.set(bufferId, true);
|
|
174
|
+
|
|
175
|
+
// Add virtual header lines in interleaved mode
|
|
176
|
+
addTestHeaders(bufferId);
|
|
177
|
+
|
|
178
|
+
editor.debug(`[test_view_marker] interleaved buffer created with id ${bufferId}`);
|
|
179
|
+
editor.setStatus("Test view marker (interleaved) active - press q to close");
|
|
180
|
+
} else {
|
|
181
|
+
editor.debug(`[test_view_marker] failed to create buffer`);
|
|
182
|
+
editor.setStatus("Failed to create test view marker buffer");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
globalThis.show_test_view_marker = async function(): Promise<void> {
|
|
187
|
+
await open_test_view_marker(0, "*test-view-marker*");
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
globalThis.show_test_view_marker_many_virtual_lines = async function(): Promise<void> {
|
|
191
|
+
await open_test_view_marker(120, "*test-view-marker-many*");
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
globalThis.show_test_view_marker_interleaved = async function(): Promise<void> {
|
|
195
|
+
await open_test_view_marker_interleaved("*test-view-marker-interleaved*");
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Close the test view marker
|
|
200
|
+
*/
|
|
201
|
+
globalThis.test_view_marker_close = function(): void {
|
|
202
|
+
if (activeBufferId !== null) {
|
|
203
|
+
// Clear virtual lines before closing
|
|
204
|
+
editor.clearVirtualTextNamespace(activeBufferId, TEST_NAMESPACE);
|
|
205
|
+
|
|
206
|
+
editor.closeBuffer(activeBufferId);
|
|
207
|
+
padLinesByBuffer.delete(activeBufferId);
|
|
208
|
+
interleavedModeByBuffer.delete(activeBufferId);
|
|
209
|
+
activeBufferId = null;
|
|
210
|
+
editor.setStatus("Test view marker closed");
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// Register command
|
|
215
|
+
editor.registerCommand(
|
|
216
|
+
"Test View Marker",
|
|
217
|
+
"Test virtual lines with header at byte 0",
|
|
218
|
+
"show_test_view_marker",
|
|
219
|
+
"normal"
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
editor.registerCommand(
|
|
223
|
+
"Test View Marker (Many Virtual Lines)",
|
|
224
|
+
"Test virtual lines with many header lines",
|
|
225
|
+
"show_test_view_marker_many_virtual_lines",
|
|
226
|
+
"normal"
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
editor.registerCommand(
|
|
230
|
+
"Test View Marker (Interleaved)",
|
|
231
|
+
"Test virtual lines with headers between each source line",
|
|
232
|
+
"show_test_view_marker_interleaved",
|
|
233
|
+
"normal"
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
editor.setStatus("Test View Marker plugin loaded (virtual lines)");
|