@fresh-editor/fresh-editor 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitignore +2 -0
- package/LICENSE +117 -0
- package/README.md +54 -0
- package/binary-install.js +212 -0
- package/binary.js +126 -0
- package/install.js +4 -0
- package/npm-shrinkwrap.json +900 -0
- package/package.json +100 -0
- package/plugins/README.md +121 -0
- package/plugins/clangd_support.md +20 -0
- package/plugins/clangd_support.ts +323 -0
- package/plugins/color_highlighter.ts +302 -0
- package/plugins/diagnostics_panel.ts +308 -0
- package/plugins/examples/README.md +245 -0
- package/plugins/examples/async_demo.ts +165 -0
- package/plugins/examples/bookmarks.ts +329 -0
- package/plugins/examples/buffer_query_demo.ts +110 -0
- package/plugins/examples/git_grep.ts +262 -0
- package/plugins/examples/hello_world.ts +93 -0
- package/plugins/examples/virtual_buffer_demo.ts +116 -0
- package/plugins/find_references.ts +357 -0
- package/plugins/git_find_file.ts +298 -0
- package/plugins/git_grep.ts +188 -0
- package/plugins/git_log.ts +1283 -0
- package/plugins/lib/fresh.d.ts +849 -0
- package/plugins/lib/index.ts +24 -0
- package/plugins/lib/navigation-controller.ts +214 -0
- package/plugins/lib/panel-manager.ts +218 -0
- package/plugins/lib/types.ts +72 -0
- package/plugins/lib/virtual-buffer-factory.ts +158 -0
- package/plugins/manual_help.ts +243 -0
- package/plugins/markdown_compose.ts +1207 -0
- package/plugins/merge_conflict.ts +1811 -0
- package/plugins/path_complete.ts +163 -0
- package/plugins/search_replace.ts +481 -0
- package/plugins/todo_highlighter.ts +204 -0
- package/plugins/welcome.ts +74 -0
- package/run-fresh.js +4 -0
|
@@ -0,0 +1,1207 @@
|
|
|
1
|
+
// Markdown Compose Mode Plugin
|
|
2
|
+
// Provides beautiful, semi-WYSIWYG rendering of Markdown documents
|
|
3
|
+
// - Highlighting: automatically enabled for all markdown files
|
|
4
|
+
// - Compose mode: explicitly toggled, adds margins, soft-wrapping, different editing
|
|
5
|
+
|
|
6
|
+
interface MarkdownConfig {
|
|
7
|
+
composeWidth: number;
|
|
8
|
+
maxWidth: number;
|
|
9
|
+
hideLineNumbers: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const config: MarkdownConfig = {
|
|
13
|
+
composeWidth: 80,
|
|
14
|
+
maxWidth: 100,
|
|
15
|
+
hideLineNumbers: true,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Track buffers with highlighting enabled (auto for markdown files)
|
|
19
|
+
const highlightingBuffers = new Set<number>();
|
|
20
|
+
|
|
21
|
+
// Track buffers in compose mode (explicit toggle)
|
|
22
|
+
const composeBuffers = new Set<number>();
|
|
23
|
+
|
|
24
|
+
// Track which buffers need their overlays refreshed (content changed)
|
|
25
|
+
const dirtyBuffers = new Set<number>();
|
|
26
|
+
|
|
27
|
+
// Markdown token types for parsing
|
|
28
|
+
enum TokenType {
|
|
29
|
+
Header1,
|
|
30
|
+
Header2,
|
|
31
|
+
Header3,
|
|
32
|
+
Header4,
|
|
33
|
+
Header5,
|
|
34
|
+
Header6,
|
|
35
|
+
ListItem,
|
|
36
|
+
OrderedListItem,
|
|
37
|
+
Checkbox,
|
|
38
|
+
CodeBlockFence,
|
|
39
|
+
CodeBlockContent,
|
|
40
|
+
BlockQuote,
|
|
41
|
+
HorizontalRule,
|
|
42
|
+
Paragraph,
|
|
43
|
+
HardBreak,
|
|
44
|
+
Image, // Images should have hard breaks (not soft breaks)
|
|
45
|
+
InlineCode,
|
|
46
|
+
Bold,
|
|
47
|
+
Italic,
|
|
48
|
+
Strikethrough,
|
|
49
|
+
Link,
|
|
50
|
+
LinkText,
|
|
51
|
+
LinkUrl,
|
|
52
|
+
Text,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface Token {
|
|
56
|
+
type: TokenType;
|
|
57
|
+
start: number; // byte offset
|
|
58
|
+
end: number; // byte offset
|
|
59
|
+
text: string;
|
|
60
|
+
level?: number; // For headers, list indentation
|
|
61
|
+
checked?: boolean; // For checkboxes
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Types match the Rust ViewTokenWire structure
|
|
65
|
+
interface ViewTokenWire {
|
|
66
|
+
source_offset: number | null;
|
|
67
|
+
kind: ViewTokenWireKind;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type ViewTokenWireKind =
|
|
71
|
+
| { Text: string }
|
|
72
|
+
| "Newline"
|
|
73
|
+
| "Space"
|
|
74
|
+
| "Break";
|
|
75
|
+
|
|
76
|
+
interface LayoutHints {
|
|
77
|
+
compose_width?: number | null;
|
|
78
|
+
column_guides?: number[] | null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Colors for styling (RGB tuples)
|
|
82
|
+
const COLORS = {
|
|
83
|
+
header: [100, 149, 237] as [number, number, number], // Cornflower blue
|
|
84
|
+
code: [152, 195, 121] as [number, number, number], // Green
|
|
85
|
+
codeBlock: [152, 195, 121] as [number, number, number],
|
|
86
|
+
fence: [80, 80, 80] as [number, number, number], // Subdued gray for ```
|
|
87
|
+
link: [86, 156, 214] as [number, number, number], // Light blue
|
|
88
|
+
linkUrl: [80, 80, 80] as [number, number, number], // Subdued gray
|
|
89
|
+
bold: [255, 255, 220] as [number, number, number], // Bright for bold text
|
|
90
|
+
boldMarker: [80, 80, 80] as [number, number, number], // Subdued for ** markers
|
|
91
|
+
italic: [198, 180, 221] as [number, number, number], // Light purple for italic
|
|
92
|
+
italicMarker: [80, 80, 80] as [number, number, number], // Subdued for * markers
|
|
93
|
+
quote: [128, 128, 128] as [number, number, number], // Gray
|
|
94
|
+
checkbox: [152, 195, 121] as [number, number, number], // Green
|
|
95
|
+
listBullet: [86, 156, 214] as [number, number, number], // Light blue
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Simple Markdown parser
|
|
99
|
+
class MarkdownParser {
|
|
100
|
+
private text: string;
|
|
101
|
+
private tokens: Token[] = [];
|
|
102
|
+
|
|
103
|
+
constructor(text: string) {
|
|
104
|
+
this.text = text;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
parse(): Token[] {
|
|
108
|
+
const lines = this.text.split('\n');
|
|
109
|
+
let byteOffset = 0;
|
|
110
|
+
let inCodeBlock = false;
|
|
111
|
+
let codeFenceStart = -1;
|
|
112
|
+
|
|
113
|
+
for (let i = 0; i < lines.length; i++) {
|
|
114
|
+
const line = lines[i];
|
|
115
|
+
const lineStart = byteOffset;
|
|
116
|
+
const lineEnd = byteOffset + line.length;
|
|
117
|
+
|
|
118
|
+
// Code block detection
|
|
119
|
+
if (line.trim().startsWith('```')) {
|
|
120
|
+
if (!inCodeBlock) {
|
|
121
|
+
inCodeBlock = true;
|
|
122
|
+
codeFenceStart = lineStart;
|
|
123
|
+
this.tokens.push({
|
|
124
|
+
type: TokenType.CodeBlockFence,
|
|
125
|
+
start: lineStart,
|
|
126
|
+
end: lineEnd,
|
|
127
|
+
text: line,
|
|
128
|
+
});
|
|
129
|
+
} else {
|
|
130
|
+
this.tokens.push({
|
|
131
|
+
type: TokenType.CodeBlockFence,
|
|
132
|
+
start: lineStart,
|
|
133
|
+
end: lineEnd,
|
|
134
|
+
text: line,
|
|
135
|
+
});
|
|
136
|
+
inCodeBlock = false;
|
|
137
|
+
}
|
|
138
|
+
} else if (inCodeBlock) {
|
|
139
|
+
this.tokens.push({
|
|
140
|
+
type: TokenType.CodeBlockContent,
|
|
141
|
+
start: lineStart,
|
|
142
|
+
end: lineEnd,
|
|
143
|
+
text: line,
|
|
144
|
+
});
|
|
145
|
+
} else {
|
|
146
|
+
// Parse line structure
|
|
147
|
+
this.parseLine(line, lineStart, lineEnd);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
byteOffset = lineEnd + 1; // +1 for newline
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Parse inline styles after structure
|
|
154
|
+
this.parseInlineStyles();
|
|
155
|
+
|
|
156
|
+
return this.tokens;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private parseLine(line: string, start: number, end: number): void {
|
|
160
|
+
const trimmed = line.trim();
|
|
161
|
+
|
|
162
|
+
// Headers
|
|
163
|
+
const headerMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
164
|
+
if (headerMatch) {
|
|
165
|
+
const level = headerMatch[1].length;
|
|
166
|
+
const type = [
|
|
167
|
+
TokenType.Header1,
|
|
168
|
+
TokenType.Header2,
|
|
169
|
+
TokenType.Header3,
|
|
170
|
+
TokenType.Header4,
|
|
171
|
+
TokenType.Header5,
|
|
172
|
+
TokenType.Header6,
|
|
173
|
+
][level - 1];
|
|
174
|
+
this.tokens.push({
|
|
175
|
+
type,
|
|
176
|
+
start,
|
|
177
|
+
end,
|
|
178
|
+
text: line,
|
|
179
|
+
level,
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Horizontal rule
|
|
185
|
+
if (trimmed.match(/^(-{3,}|\*{3,}|_{3,})$/)) {
|
|
186
|
+
this.tokens.push({
|
|
187
|
+
type: TokenType.HorizontalRule,
|
|
188
|
+
start,
|
|
189
|
+
end,
|
|
190
|
+
text: line,
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// List items
|
|
196
|
+
const bulletMatch = line.match(/^(\s*)([-*+])\s+(.*)$/);
|
|
197
|
+
if (bulletMatch) {
|
|
198
|
+
const indent = bulletMatch[1].length;
|
|
199
|
+
const hasCheckbox = bulletMatch[3].match(/^\[([ x])\]\s+/);
|
|
200
|
+
|
|
201
|
+
if (hasCheckbox) {
|
|
202
|
+
this.tokens.push({
|
|
203
|
+
type: TokenType.Checkbox,
|
|
204
|
+
start,
|
|
205
|
+
end,
|
|
206
|
+
text: line,
|
|
207
|
+
level: indent,
|
|
208
|
+
checked: hasCheckbox[1] === 'x',
|
|
209
|
+
});
|
|
210
|
+
} else {
|
|
211
|
+
this.tokens.push({
|
|
212
|
+
type: TokenType.ListItem,
|
|
213
|
+
start,
|
|
214
|
+
end,
|
|
215
|
+
text: line,
|
|
216
|
+
level: indent,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Ordered list
|
|
223
|
+
const orderedMatch = line.match(/^(\s*)(\d+\.)\s+(.*)$/);
|
|
224
|
+
if (orderedMatch) {
|
|
225
|
+
const indent = orderedMatch[1].length;
|
|
226
|
+
this.tokens.push({
|
|
227
|
+
type: TokenType.OrderedListItem,
|
|
228
|
+
start,
|
|
229
|
+
end,
|
|
230
|
+
text: line,
|
|
231
|
+
level: indent,
|
|
232
|
+
});
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Block quote
|
|
237
|
+
if (trimmed.startsWith('>')) {
|
|
238
|
+
this.tokens.push({
|
|
239
|
+
type: TokenType.BlockQuote,
|
|
240
|
+
start,
|
|
241
|
+
end,
|
|
242
|
+
text: line,
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Hard breaks (two spaces + newline, or backslash + newline)
|
|
248
|
+
if (line.endsWith(' ') || line.endsWith('\\')) {
|
|
249
|
+
this.tokens.push({
|
|
250
|
+
type: TokenType.HardBreak,
|
|
251
|
+
start,
|
|
252
|
+
end,
|
|
253
|
+
text: line,
|
|
254
|
+
});
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Images:  - these should have hard breaks to keep each on its own line
|
|
259
|
+
if (trimmed.match(/^!\[.*\]\(.*\)$/)) {
|
|
260
|
+
this.tokens.push({
|
|
261
|
+
type: TokenType.Image,
|
|
262
|
+
start,
|
|
263
|
+
end,
|
|
264
|
+
text: line,
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Default: paragraph
|
|
270
|
+
if (trimmed.length > 0) {
|
|
271
|
+
this.tokens.push({
|
|
272
|
+
type: TokenType.Paragraph,
|
|
273
|
+
start,
|
|
274
|
+
end,
|
|
275
|
+
text: line,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private parseInlineStyles(): void {
|
|
281
|
+
// Parse inline markdown (bold, italic, code, links) within text
|
|
282
|
+
// This is a simplified parser - a full implementation would use a proper MD parser
|
|
283
|
+
|
|
284
|
+
for (const token of this.tokens) {
|
|
285
|
+
if (token.type === TokenType.Paragraph ||
|
|
286
|
+
token.type === TokenType.ListItem ||
|
|
287
|
+
token.type === TokenType.OrderedListItem) {
|
|
288
|
+
// Find inline code
|
|
289
|
+
this.findInlineCode(token);
|
|
290
|
+
// Find bold/italic
|
|
291
|
+
this.findEmphasis(token);
|
|
292
|
+
// Find links
|
|
293
|
+
this.findLinks(token);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private findInlineCode(token: Token): void {
|
|
299
|
+
const regex = /`([^`]+)`/g;
|
|
300
|
+
let match;
|
|
301
|
+
while ((match = regex.exec(token.text)) !== null) {
|
|
302
|
+
this.tokens.push({
|
|
303
|
+
type: TokenType.InlineCode,
|
|
304
|
+
start: token.start + match.index,
|
|
305
|
+
end: token.start + match.index + match[0].length,
|
|
306
|
+
text: match[0],
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private findEmphasis(token: Token): void {
|
|
312
|
+
// Bold: **text** or __text__
|
|
313
|
+
const boldRegex = /(\*\*|__)([^*_]+)\1/g;
|
|
314
|
+
let match;
|
|
315
|
+
while ((match = boldRegex.exec(token.text)) !== null) {
|
|
316
|
+
this.tokens.push({
|
|
317
|
+
type: TokenType.Bold,
|
|
318
|
+
start: token.start + match.index,
|
|
319
|
+
end: token.start + match.index + match[0].length,
|
|
320
|
+
text: match[0],
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Italic: *text* or _text_
|
|
325
|
+
const italicRegex = /(\*|_)([^*_]+)\1/g;
|
|
326
|
+
while ((match = italicRegex.exec(token.text)) !== null) {
|
|
327
|
+
// Skip if it's part of bold
|
|
328
|
+
const isBold = this.tokens.some(t =>
|
|
329
|
+
t.type === TokenType.Bold &&
|
|
330
|
+
t.start <= token.start + match.index &&
|
|
331
|
+
t.end >= token.start + match.index + match[0].length
|
|
332
|
+
);
|
|
333
|
+
if (!isBold) {
|
|
334
|
+
this.tokens.push({
|
|
335
|
+
type: TokenType.Italic,
|
|
336
|
+
start: token.start + match.index,
|
|
337
|
+
end: token.start + match.index + match[0].length,
|
|
338
|
+
text: match[0],
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Strikethrough: ~~text~~
|
|
344
|
+
const strikeRegex = /~~([^~]+)~~/g;
|
|
345
|
+
while ((match = strikeRegex.exec(token.text)) !== null) {
|
|
346
|
+
this.tokens.push({
|
|
347
|
+
type: TokenType.Strikethrough,
|
|
348
|
+
start: token.start + match.index,
|
|
349
|
+
end: token.start + match.index + match[0].length,
|
|
350
|
+
text: match[0],
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private findLinks(token: Token): void {
|
|
356
|
+
// Links: [text](url)
|
|
357
|
+
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
358
|
+
let match;
|
|
359
|
+
while ((match = linkRegex.exec(token.text)) !== null) {
|
|
360
|
+
const fullStart = token.start + match.index;
|
|
361
|
+
const textStart = fullStart + 1; // After [
|
|
362
|
+
const textEnd = textStart + match[1].length;
|
|
363
|
+
const urlStart = textEnd + 2; // After ](
|
|
364
|
+
const urlEnd = urlStart + match[2].length;
|
|
365
|
+
|
|
366
|
+
this.tokens.push({
|
|
367
|
+
type: TokenType.Link,
|
|
368
|
+
start: fullStart,
|
|
369
|
+
end: fullStart + match[0].length,
|
|
370
|
+
text: match[0],
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
this.tokens.push({
|
|
374
|
+
type: TokenType.LinkText,
|
|
375
|
+
start: textStart,
|
|
376
|
+
end: textEnd,
|
|
377
|
+
text: match[1],
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
this.tokens.push({
|
|
381
|
+
type: TokenType.LinkUrl,
|
|
382
|
+
start: urlStart,
|
|
383
|
+
end: urlEnd,
|
|
384
|
+
text: match[2],
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Apply styling overlays based on parsed tokens
|
|
391
|
+
function applyMarkdownStyling(bufferId: number, tokens: Token[]): void {
|
|
392
|
+
// Clear existing markdown overlays
|
|
393
|
+
editor.clearNamespace(bufferId, "md");
|
|
394
|
+
|
|
395
|
+
for (const token of tokens) {
|
|
396
|
+
let color: [number, number, number] | null = null;
|
|
397
|
+
let underline = false;
|
|
398
|
+
let overlayId = "md";
|
|
399
|
+
|
|
400
|
+
switch (token.type) {
|
|
401
|
+
case TokenType.Header1:
|
|
402
|
+
case TokenType.Header2:
|
|
403
|
+
case TokenType.Header3:
|
|
404
|
+
case TokenType.Header4:
|
|
405
|
+
case TokenType.Header5:
|
|
406
|
+
case TokenType.Header6:
|
|
407
|
+
color = COLORS.header;
|
|
408
|
+
underline = true;
|
|
409
|
+
break;
|
|
410
|
+
|
|
411
|
+
case TokenType.InlineCode:
|
|
412
|
+
color = COLORS.code;
|
|
413
|
+
break;
|
|
414
|
+
|
|
415
|
+
case TokenType.CodeBlockFence:
|
|
416
|
+
color = COLORS.fence;
|
|
417
|
+
break;
|
|
418
|
+
|
|
419
|
+
case TokenType.CodeBlockContent:
|
|
420
|
+
color = COLORS.codeBlock;
|
|
421
|
+
break;
|
|
422
|
+
|
|
423
|
+
case TokenType.BlockQuote:
|
|
424
|
+
color = COLORS.quote;
|
|
425
|
+
break;
|
|
426
|
+
|
|
427
|
+
case TokenType.Bold:
|
|
428
|
+
// Style bold markers (** or __) subdued, content bold
|
|
429
|
+
const boldMatch = token.text.match(/^(\*\*|__)(.*)(\*\*|__)$/);
|
|
430
|
+
if (boldMatch) {
|
|
431
|
+
const markerLen = boldMatch[1].length;
|
|
432
|
+
// Subdued markers
|
|
433
|
+
editor.addOverlay(bufferId, "md",
|
|
434
|
+
token.start, token.start + markerLen,
|
|
435
|
+
COLORS.boldMarker[0], COLORS.boldMarker[1], COLORS.boldMarker[2], false, false, false);
|
|
436
|
+
editor.addOverlay(bufferId, "md",
|
|
437
|
+
token.end - markerLen, token.end,
|
|
438
|
+
COLORS.boldMarker[0], COLORS.boldMarker[1], COLORS.boldMarker[2], false, false, false);
|
|
439
|
+
// Bold content with bold=true
|
|
440
|
+
editor.addOverlay(bufferId, "md",
|
|
441
|
+
token.start + markerLen, token.end - markerLen,
|
|
442
|
+
COLORS.bold[0], COLORS.bold[1], COLORS.bold[2], false, true, false);
|
|
443
|
+
} else {
|
|
444
|
+
color = COLORS.bold;
|
|
445
|
+
}
|
|
446
|
+
break;
|
|
447
|
+
|
|
448
|
+
case TokenType.Italic:
|
|
449
|
+
// Style italic markers (* or _) subdued, content italic
|
|
450
|
+
const italicMatch = token.text.match(/^(\*|_)(.*)(\*|_)$/);
|
|
451
|
+
if (italicMatch) {
|
|
452
|
+
const markerLen = 1;
|
|
453
|
+
// Subdued markers
|
|
454
|
+
editor.addOverlay(bufferId, "md",
|
|
455
|
+
token.start, token.start + markerLen,
|
|
456
|
+
COLORS.italicMarker[0], COLORS.italicMarker[1], COLORS.italicMarker[2], false, false, false);
|
|
457
|
+
editor.addOverlay(bufferId, "md",
|
|
458
|
+
token.end - markerLen, token.end,
|
|
459
|
+
COLORS.italicMarker[0], COLORS.italicMarker[1], COLORS.italicMarker[2], false, false, false);
|
|
460
|
+
// Italic content with italic=true
|
|
461
|
+
editor.addOverlay(bufferId, "md",
|
|
462
|
+
token.start + markerLen, token.end - markerLen,
|
|
463
|
+
COLORS.italic[0], COLORS.italic[1], COLORS.italic[2], false, false, true);
|
|
464
|
+
} else {
|
|
465
|
+
color = COLORS.italic;
|
|
466
|
+
}
|
|
467
|
+
break;
|
|
468
|
+
|
|
469
|
+
case TokenType.LinkText:
|
|
470
|
+
color = COLORS.link;
|
|
471
|
+
underline = true;
|
|
472
|
+
break;
|
|
473
|
+
|
|
474
|
+
case TokenType.LinkUrl:
|
|
475
|
+
color = COLORS.linkUrl;
|
|
476
|
+
break;
|
|
477
|
+
|
|
478
|
+
case TokenType.ListItem:
|
|
479
|
+
case TokenType.OrderedListItem:
|
|
480
|
+
// Style just the bullet/number
|
|
481
|
+
const bulletMatch = token.text.match(/^(\s*)([-*+]|\d+\.)/);
|
|
482
|
+
if (bulletMatch) {
|
|
483
|
+
const bulletEnd = token.start + bulletMatch[0].length;
|
|
484
|
+
editor.addOverlay(
|
|
485
|
+
bufferId,
|
|
486
|
+
"md",
|
|
487
|
+
token.start,
|
|
488
|
+
bulletEnd,
|
|
489
|
+
COLORS.listBullet[0],
|
|
490
|
+
COLORS.listBullet[1],
|
|
491
|
+
COLORS.listBullet[2],
|
|
492
|
+
false
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
break;
|
|
496
|
+
|
|
497
|
+
case TokenType.Checkbox:
|
|
498
|
+
// Style checkbox and bullet
|
|
499
|
+
const checkboxMatch = token.text.match(/^(\s*[-*+]\s+\[[ x]\])/);
|
|
500
|
+
if (checkboxMatch) {
|
|
501
|
+
const checkboxEnd = token.start + checkboxMatch[0].length;
|
|
502
|
+
editor.addOverlay(
|
|
503
|
+
bufferId,
|
|
504
|
+
"md",
|
|
505
|
+
token.start,
|
|
506
|
+
checkboxEnd,
|
|
507
|
+
COLORS.checkbox[0],
|
|
508
|
+
COLORS.checkbox[1],
|
|
509
|
+
COLORS.checkbox[2],
|
|
510
|
+
false
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (color) {
|
|
517
|
+
editor.addOverlay(
|
|
518
|
+
bufferId,
|
|
519
|
+
overlayId,
|
|
520
|
+
token.start,
|
|
521
|
+
token.end,
|
|
522
|
+
color[0],
|
|
523
|
+
color[1],
|
|
524
|
+
color[2],
|
|
525
|
+
underline
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Highlight a single line for markdown (used with lines_changed event)
|
|
532
|
+
function highlightLine(
|
|
533
|
+
bufferId: number,
|
|
534
|
+
lineNumber: number,
|
|
535
|
+
byteStart: number,
|
|
536
|
+
content: string
|
|
537
|
+
): void {
|
|
538
|
+
const trimmed = content.trim();
|
|
539
|
+
if (trimmed.length === 0) return;
|
|
540
|
+
|
|
541
|
+
// Headers
|
|
542
|
+
const headerMatch = trimmed.match(/^(#{1,6})\s/);
|
|
543
|
+
if (headerMatch) {
|
|
544
|
+
editor.addOverlay(
|
|
545
|
+
bufferId,
|
|
546
|
+
"md",
|
|
547
|
+
byteStart,
|
|
548
|
+
byteStart + content.length,
|
|
549
|
+
COLORS.header[0], COLORS.header[1], COLORS.header[2],
|
|
550
|
+
false, true, false // bold
|
|
551
|
+
);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Code block fences
|
|
556
|
+
if (trimmed.startsWith('```')) {
|
|
557
|
+
editor.addOverlay(
|
|
558
|
+
bufferId,
|
|
559
|
+
"md",
|
|
560
|
+
byteStart,
|
|
561
|
+
byteStart + content.length,
|
|
562
|
+
COLORS.fence[0], COLORS.fence[1], COLORS.fence[2],
|
|
563
|
+
false
|
|
564
|
+
);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Block quotes
|
|
569
|
+
if (trimmed.startsWith('>')) {
|
|
570
|
+
editor.addOverlay(
|
|
571
|
+
bufferId,
|
|
572
|
+
"md",
|
|
573
|
+
byteStart,
|
|
574
|
+
byteStart + content.length,
|
|
575
|
+
COLORS.quote[0], COLORS.quote[1], COLORS.quote[2],
|
|
576
|
+
false
|
|
577
|
+
);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Horizontal rules
|
|
582
|
+
if (trimmed.match(/^[-*_]{3,}$/)) {
|
|
583
|
+
editor.addOverlay(
|
|
584
|
+
bufferId,
|
|
585
|
+
"md",
|
|
586
|
+
byteStart,
|
|
587
|
+
byteStart + content.length,
|
|
588
|
+
COLORS.quote[0], COLORS.quote[1], COLORS.quote[2],
|
|
589
|
+
false
|
|
590
|
+
);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// List items (unordered)
|
|
595
|
+
const listMatch = content.match(/^(\s*)([-*+])\s/);
|
|
596
|
+
if (listMatch) {
|
|
597
|
+
const bulletStart = byteStart + listMatch[1].length;
|
|
598
|
+
const bulletEnd = bulletStart + 1;
|
|
599
|
+
editor.addOverlay(
|
|
600
|
+
bufferId,
|
|
601
|
+
"md",
|
|
602
|
+
bulletStart,
|
|
603
|
+
bulletEnd,
|
|
604
|
+
COLORS.listBullet[0], COLORS.listBullet[1], COLORS.listBullet[2],
|
|
605
|
+
false
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Ordered list items
|
|
610
|
+
const orderedMatch = content.match(/^(\s*)(\d+\.)\s/);
|
|
611
|
+
if (orderedMatch) {
|
|
612
|
+
const numStart = byteStart + orderedMatch[1].length;
|
|
613
|
+
const numEnd = numStart + orderedMatch[2].length;
|
|
614
|
+
editor.addOverlay(
|
|
615
|
+
bufferId,
|
|
616
|
+
"md",
|
|
617
|
+
numStart,
|
|
618
|
+
numEnd,
|
|
619
|
+
COLORS.listBullet[0], COLORS.listBullet[1], COLORS.listBullet[2],
|
|
620
|
+
false
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Checkboxes
|
|
625
|
+
const checkMatch = content.match(/^(\s*[-*+]\s+)(\[[ x]\])/);
|
|
626
|
+
if (checkMatch) {
|
|
627
|
+
const checkStart = byteStart + checkMatch[1].length;
|
|
628
|
+
const checkEnd = checkStart + checkMatch[2].length;
|
|
629
|
+
editor.addOverlay(
|
|
630
|
+
bufferId,
|
|
631
|
+
"md",
|
|
632
|
+
checkStart,
|
|
633
|
+
checkEnd,
|
|
634
|
+
COLORS.checkbox[0], COLORS.checkbox[1], COLORS.checkbox[2],
|
|
635
|
+
false
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Inline elements
|
|
640
|
+
|
|
641
|
+
// Inline code: `code`
|
|
642
|
+
const codeRegex = /`([^`]+)`/g;
|
|
643
|
+
let match;
|
|
644
|
+
while ((match = codeRegex.exec(content)) !== null) {
|
|
645
|
+
editor.addOverlay(
|
|
646
|
+
bufferId,
|
|
647
|
+
"md",
|
|
648
|
+
byteStart + match.index,
|
|
649
|
+
byteStart + match.index + match[0].length,
|
|
650
|
+
COLORS.code[0], COLORS.code[1], COLORS.code[2],
|
|
651
|
+
false
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Bold: **text** or __text__
|
|
656
|
+
const boldRegex = /(\*\*|__)([^*_]+)\1/g;
|
|
657
|
+
while ((match = boldRegex.exec(content)) !== null) {
|
|
658
|
+
const markerLen = match[1].length;
|
|
659
|
+
const fullStart = byteStart + match.index;
|
|
660
|
+
const fullEnd = fullStart + match[0].length;
|
|
661
|
+
// Subdued markers
|
|
662
|
+
editor.addOverlay(
|
|
663
|
+
bufferId,
|
|
664
|
+
"md",
|
|
665
|
+
fullStart, fullStart + markerLen,
|
|
666
|
+
COLORS.boldMarker[0], COLORS.boldMarker[1], COLORS.boldMarker[2],
|
|
667
|
+
false, false, false
|
|
668
|
+
);
|
|
669
|
+
editor.addOverlay(
|
|
670
|
+
bufferId,
|
|
671
|
+
"md",
|
|
672
|
+
fullEnd - markerLen, fullEnd,
|
|
673
|
+
COLORS.boldMarker[0], COLORS.boldMarker[1], COLORS.boldMarker[2],
|
|
674
|
+
false, false, false
|
|
675
|
+
);
|
|
676
|
+
// Bold content
|
|
677
|
+
editor.addOverlay(
|
|
678
|
+
bufferId,
|
|
679
|
+
"md",
|
|
680
|
+
fullStart + markerLen, fullEnd - markerLen,
|
|
681
|
+
COLORS.bold[0], COLORS.bold[1], COLORS.bold[2],
|
|
682
|
+
false, true, false
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Italic: *text* or _text_ (but not inside bold)
|
|
687
|
+
const italicRegex = /(?<!\*|\w)(\*|_)(?!\*|_)([^*_\n]+)(?<!\*|_)\1(?!\*|\w)/g;
|
|
688
|
+
while ((match = italicRegex.exec(content)) !== null) {
|
|
689
|
+
const fullStart = byteStart + match.index;
|
|
690
|
+
const fullEnd = fullStart + match[0].length;
|
|
691
|
+
// Subdued markers
|
|
692
|
+
editor.addOverlay(
|
|
693
|
+
bufferId,
|
|
694
|
+
"md",
|
|
695
|
+
fullStart, fullStart + 1,
|
|
696
|
+
COLORS.italicMarker[0], COLORS.italicMarker[1], COLORS.italicMarker[2],
|
|
697
|
+
false, false, false
|
|
698
|
+
);
|
|
699
|
+
editor.addOverlay(
|
|
700
|
+
bufferId,
|
|
701
|
+
"md",
|
|
702
|
+
fullEnd - 1, fullEnd,
|
|
703
|
+
COLORS.italicMarker[0], COLORS.italicMarker[1], COLORS.italicMarker[2],
|
|
704
|
+
false, false, false
|
|
705
|
+
);
|
|
706
|
+
// Italic content
|
|
707
|
+
editor.addOverlay(
|
|
708
|
+
bufferId,
|
|
709
|
+
"md",
|
|
710
|
+
fullStart + 1, fullEnd - 1,
|
|
711
|
+
COLORS.italic[0], COLORS.italic[1], COLORS.italic[2],
|
|
712
|
+
false, false, true
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Links: [text](url)
|
|
717
|
+
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
718
|
+
while ((match = linkRegex.exec(content)) !== null) {
|
|
719
|
+
const fullStart = byteStart + match.index;
|
|
720
|
+
const textStart = fullStart + 1;
|
|
721
|
+
const textEnd = textStart + match[1].length;
|
|
722
|
+
const urlStart = textEnd + 2;
|
|
723
|
+
const urlEnd = urlStart + match[2].length;
|
|
724
|
+
|
|
725
|
+
// Link text (underlined)
|
|
726
|
+
editor.addOverlay(
|
|
727
|
+
bufferId,
|
|
728
|
+
"md",
|
|
729
|
+
textStart, textEnd,
|
|
730
|
+
COLORS.link[0], COLORS.link[1], COLORS.link[2],
|
|
731
|
+
true // underline
|
|
732
|
+
);
|
|
733
|
+
// Link URL (subdued)
|
|
734
|
+
editor.addOverlay(
|
|
735
|
+
bufferId,
|
|
736
|
+
"md",
|
|
737
|
+
urlStart, urlEnd,
|
|
738
|
+
COLORS.linkUrl[0], COLORS.linkUrl[1], COLORS.linkUrl[2],
|
|
739
|
+
false
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Clear highlights for a buffer
|
|
745
|
+
function clearHighlights(bufferId: number): void {
|
|
746
|
+
editor.clearNamespace(bufferId, "md");
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Build view transform with soft breaks
|
|
750
|
+
function buildViewTransform(
|
|
751
|
+
bufferId: number,
|
|
752
|
+
splitId: number | null,
|
|
753
|
+
text: string,
|
|
754
|
+
viewportStart: number,
|
|
755
|
+
viewportEnd: number,
|
|
756
|
+
tokens: Token[]
|
|
757
|
+
): void {
|
|
758
|
+
const viewTokens: ViewTokenWire[] = [];
|
|
759
|
+
|
|
760
|
+
// Get the relevant portion of text
|
|
761
|
+
const viewportText = text.substring(viewportStart, viewportEnd);
|
|
762
|
+
|
|
763
|
+
// Track which lines should have hard breaks
|
|
764
|
+
let lineStart = viewportStart;
|
|
765
|
+
let i = 0;
|
|
766
|
+
|
|
767
|
+
while (i < viewportText.length) {
|
|
768
|
+
const absOffset = viewportStart + i;
|
|
769
|
+
const ch = viewportText[i];
|
|
770
|
+
|
|
771
|
+
if (ch === '\n') {
|
|
772
|
+
// Check if this line should have a hard break
|
|
773
|
+
const hasHardBreak = tokens.some(t =>
|
|
774
|
+
(t.type === TokenType.HardBreak ||
|
|
775
|
+
t.type === TokenType.Header1 ||
|
|
776
|
+
t.type === TokenType.Header2 ||
|
|
777
|
+
t.type === TokenType.Header3 ||
|
|
778
|
+
t.type === TokenType.Header4 ||
|
|
779
|
+
t.type === TokenType.Header5 ||
|
|
780
|
+
t.type === TokenType.Header6 ||
|
|
781
|
+
t.type === TokenType.ListItem ||
|
|
782
|
+
t.type === TokenType.OrderedListItem ||
|
|
783
|
+
t.type === TokenType.Checkbox ||
|
|
784
|
+
t.type === TokenType.BlockQuote ||
|
|
785
|
+
t.type === TokenType.CodeBlockFence ||
|
|
786
|
+
t.type === TokenType.CodeBlockContent ||
|
|
787
|
+
t.type === TokenType.HorizontalRule ||
|
|
788
|
+
t.type === TokenType.Image) &&
|
|
789
|
+
t.start <= lineStart && t.end >= lineStart
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
// Empty lines are also hard breaks
|
|
793
|
+
const lineContent = viewportText.substring(lineStart - viewportStart, i).trim();
|
|
794
|
+
const isEmptyLine = lineContent.length === 0;
|
|
795
|
+
|
|
796
|
+
if (hasHardBreak || isEmptyLine) {
|
|
797
|
+
// Hard break - keep newline
|
|
798
|
+
viewTokens.push({
|
|
799
|
+
source_offset: absOffset,
|
|
800
|
+
kind: "Newline",
|
|
801
|
+
});
|
|
802
|
+
} else {
|
|
803
|
+
// Soft break - replace with space
|
|
804
|
+
viewTokens.push({
|
|
805
|
+
source_offset: absOffset,
|
|
806
|
+
kind: "Space",
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
lineStart = absOffset + 1;
|
|
811
|
+
i++;
|
|
812
|
+
} else if (ch === ' ') {
|
|
813
|
+
viewTokens.push({
|
|
814
|
+
source_offset: absOffset,
|
|
815
|
+
kind: "Space",
|
|
816
|
+
});
|
|
817
|
+
i++;
|
|
818
|
+
} else {
|
|
819
|
+
// Accumulate consecutive text characters
|
|
820
|
+
let textStart = i;
|
|
821
|
+
let textContent = '';
|
|
822
|
+
while (i < viewportText.length) {
|
|
823
|
+
const c = viewportText[i];
|
|
824
|
+
if (c === '\n' || c === ' ') {
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
textContent += c;
|
|
828
|
+
i++;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
viewTokens.push({
|
|
832
|
+
source_offset: viewportStart + textStart,
|
|
833
|
+
kind: { Text: textContent },
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Submit the view transform with layout hints
|
|
839
|
+
const layoutHints: LayoutHints = {
|
|
840
|
+
compose_width: config.composeWidth,
|
|
841
|
+
column_guides: null,
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
editor.debug(`buildViewTransform: submitting ${viewTokens.length} tokens, compose_width=${config.composeWidth}`);
|
|
845
|
+
if (viewTokens.length > 0 && viewTokens.length < 10) {
|
|
846
|
+
editor.debug(`buildViewTransform: first tokens: ${JSON.stringify(viewTokens.slice(0, 5))}`);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const success = editor.submitViewTransform(
|
|
850
|
+
bufferId,
|
|
851
|
+
splitId,
|
|
852
|
+
viewportStart,
|
|
853
|
+
viewportEnd,
|
|
854
|
+
viewTokens,
|
|
855
|
+
layoutHints
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
editor.debug(`buildViewTransform: submit result = ${success}`);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Check if a file is a markdown file
|
|
862
|
+
function isMarkdownFile(path: string): boolean {
|
|
863
|
+
return path.endsWith('.md') || path.endsWith('.markdown');
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Process a buffer in compose mode (highlighting + view transform)
|
|
867
|
+
function processBuffer(bufferId: number, splitId?: number): void {
|
|
868
|
+
if (!composeBuffers.has(bufferId)) return;
|
|
869
|
+
|
|
870
|
+
const info = editor.getBufferInfo(bufferId);
|
|
871
|
+
if (!info || !isMarkdownFile(info.path)) return;
|
|
872
|
+
|
|
873
|
+
editor.debug(`processBuffer: processing ${info.path}, buffer_id=${bufferId}`);
|
|
874
|
+
|
|
875
|
+
const bufferLength = editor.getBufferLength(bufferId);
|
|
876
|
+
const text = editor.getBufferText(bufferId, 0, bufferLength);
|
|
877
|
+
const parser = new MarkdownParser(text);
|
|
878
|
+
const tokens = parser.parse();
|
|
879
|
+
|
|
880
|
+
// Apply styling with overlays
|
|
881
|
+
applyMarkdownStyling(bufferId, tokens);
|
|
882
|
+
|
|
883
|
+
// Get viewport info and build view transform
|
|
884
|
+
const viewport = editor.getViewport();
|
|
885
|
+
if (!viewport) {
|
|
886
|
+
const viewportStart = 0;
|
|
887
|
+
const viewportEnd = text.length;
|
|
888
|
+
buildViewTransform(bufferId, splitId || null, text, viewportStart, viewportEnd, tokens);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const viewportStart = Math.max(0, viewport.top_byte - 500);
|
|
893
|
+
const viewportEnd = Math.min(text.length, viewport.top_byte + (viewport.height * 200));
|
|
894
|
+
buildViewTransform(bufferId, splitId || null, text, viewportStart, viewportEnd, tokens);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Enable highlighting for a markdown buffer (auto on file open)
|
|
898
|
+
function enableHighlighting(bufferId: number): void {
|
|
899
|
+
const info = editor.getBufferInfo(bufferId);
|
|
900
|
+
if (!info || !isMarkdownFile(info.path)) return;
|
|
901
|
+
|
|
902
|
+
if (!highlightingBuffers.has(bufferId)) {
|
|
903
|
+
highlightingBuffers.add(bufferId);
|
|
904
|
+
// Trigger a refresh so lines_changed will process visible lines
|
|
905
|
+
editor.refreshLines(bufferId);
|
|
906
|
+
editor.debug(`Markdown highlighting enabled for buffer ${bufferId}`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Enable full compose mode for a buffer (explicit toggle)
|
|
911
|
+
function enableMarkdownCompose(bufferId: number): void {
|
|
912
|
+
const info = editor.getBufferInfo(bufferId);
|
|
913
|
+
if (!info || !isMarkdownFile(info.path)) return;
|
|
914
|
+
|
|
915
|
+
if (!composeBuffers.has(bufferId)) {
|
|
916
|
+
composeBuffers.add(bufferId);
|
|
917
|
+
highlightingBuffers.add(bufferId); // Also ensure highlighting is on
|
|
918
|
+
|
|
919
|
+
// Hide line numbers in compose mode
|
|
920
|
+
editor.setLineNumbers(bufferId, false);
|
|
921
|
+
|
|
922
|
+
processBuffer(bufferId);
|
|
923
|
+
editor.debug(`Markdown compose enabled for buffer ${bufferId}`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Disable compose mode for a buffer (but keep highlighting)
|
|
928
|
+
function disableMarkdownCompose(bufferId: number): void {
|
|
929
|
+
if (composeBuffers.has(bufferId)) {
|
|
930
|
+
composeBuffers.delete(bufferId);
|
|
931
|
+
|
|
932
|
+
// Re-enable line numbers
|
|
933
|
+
editor.setLineNumbers(bufferId, true);
|
|
934
|
+
|
|
935
|
+
// Clear view transform to return to normal rendering
|
|
936
|
+
editor.clearViewTransform(bufferId);
|
|
937
|
+
|
|
938
|
+
// Keep highlighting on, just clear the view transform
|
|
939
|
+
editor.refreshLines(bufferId);
|
|
940
|
+
editor.debug(`Markdown compose disabled for buffer ${bufferId}`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Toggle markdown compose mode for current buffer
|
|
945
|
+
globalThis.markdownToggleCompose = function(): void {
|
|
946
|
+
const bufferId = editor.getActiveBufferId();
|
|
947
|
+
const info = editor.getBufferInfo(bufferId);
|
|
948
|
+
|
|
949
|
+
if (!info) return;
|
|
950
|
+
|
|
951
|
+
// Only work with markdown files
|
|
952
|
+
if (!info.path.endsWith('.md') && !info.path.endsWith('.markdown')) {
|
|
953
|
+
editor.setStatus("Not a Markdown file");
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (composeBuffers.has(bufferId)) {
|
|
958
|
+
disableMarkdownCompose(bufferId);
|
|
959
|
+
editor.setStatus("Markdown Compose: OFF");
|
|
960
|
+
} else {
|
|
961
|
+
enableMarkdownCompose(bufferId);
|
|
962
|
+
// Trigger a re-render to apply the transform
|
|
963
|
+
editor.refreshLines(bufferId);
|
|
964
|
+
editor.setStatus("Markdown Compose: ON (soft breaks, styled)");
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
// Handle view transform request - receives tokens from core for transformation
|
|
969
|
+
// Only applies transforms when in compose mode (not just highlighting)
|
|
970
|
+
globalThis.onMarkdownViewTransform = function(data: {
|
|
971
|
+
buffer_id: number;
|
|
972
|
+
split_id: number;
|
|
973
|
+
viewport_start: number;
|
|
974
|
+
viewport_end: number;
|
|
975
|
+
tokens: ViewTokenWire[];
|
|
976
|
+
}): void {
|
|
977
|
+
// Only transform when in compose mode (view transforms change line wrapping etc)
|
|
978
|
+
if (!composeBuffers.has(data.buffer_id)) return;
|
|
979
|
+
|
|
980
|
+
const info = editor.getBufferInfo(data.buffer_id);
|
|
981
|
+
if (!info || !isMarkdownFile(info.path)) return;
|
|
982
|
+
|
|
983
|
+
editor.debug(`onMarkdownViewTransform: buffer=${data.buffer_id}, split=${data.split_id}, tokens=${data.tokens.length}`);
|
|
984
|
+
|
|
985
|
+
// Reconstruct text from tokens for parsing (we need text for markdown parsing)
|
|
986
|
+
let reconstructedText = '';
|
|
987
|
+
for (const token of data.tokens) {
|
|
988
|
+
if (typeof token.kind === 'object' && 'Text' in token.kind) {
|
|
989
|
+
reconstructedText += token.kind.Text;
|
|
990
|
+
} else if (token.kind === 'Newline') {
|
|
991
|
+
reconstructedText += '\n';
|
|
992
|
+
} else if (token.kind === 'Space') {
|
|
993
|
+
reconstructedText += ' ';
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Parse markdown from reconstructed text
|
|
998
|
+
const parser = new MarkdownParser(reconstructedText);
|
|
999
|
+
const mdTokens = parser.parse();
|
|
1000
|
+
|
|
1001
|
+
// Apply overlays for styling (this still works via the existing overlay API)
|
|
1002
|
+
// Offset the markdown tokens by viewport_start for correct positioning
|
|
1003
|
+
const offsetTokens = mdTokens.map(t => ({
|
|
1004
|
+
...t,
|
|
1005
|
+
start: t.start + data.viewport_start,
|
|
1006
|
+
end: t.end + data.viewport_start,
|
|
1007
|
+
}));
|
|
1008
|
+
applyMarkdownStyling(data.buffer_id, offsetTokens);
|
|
1009
|
+
|
|
1010
|
+
// Transform the view tokens based on markdown structure
|
|
1011
|
+
// Convert newlines to spaces for soft breaks (paragraphs)
|
|
1012
|
+
const transformedTokens = transformTokensForMarkdown(data.tokens, mdTokens, data.viewport_start);
|
|
1013
|
+
|
|
1014
|
+
// Submit the transformed tokens
|
|
1015
|
+
const layoutHints: LayoutHints = {
|
|
1016
|
+
compose_width: config.composeWidth,
|
|
1017
|
+
column_guides: null,
|
|
1018
|
+
};
|
|
1019
|
+
|
|
1020
|
+
editor.submitViewTransform(
|
|
1021
|
+
data.buffer_id,
|
|
1022
|
+
data.split_id,
|
|
1023
|
+
data.viewport_start,
|
|
1024
|
+
data.viewport_end,
|
|
1025
|
+
transformedTokens,
|
|
1026
|
+
layoutHints
|
|
1027
|
+
);
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
// Transform view tokens based on markdown structure
|
|
1031
|
+
function transformTokensForMarkdown(
|
|
1032
|
+
tokens: ViewTokenWire[],
|
|
1033
|
+
mdTokens: Token[],
|
|
1034
|
+
viewportStart: number
|
|
1035
|
+
): ViewTokenWire[] {
|
|
1036
|
+
const result: ViewTokenWire[] = [];
|
|
1037
|
+
|
|
1038
|
+
// Build a set of positions that should have hard breaks
|
|
1039
|
+
const hardBreakPositions = new Set<number>();
|
|
1040
|
+
for (const t of mdTokens) {
|
|
1041
|
+
if (t.type === TokenType.HardBreak ||
|
|
1042
|
+
t.type === TokenType.Header1 ||
|
|
1043
|
+
t.type === TokenType.Header2 ||
|
|
1044
|
+
t.type === TokenType.Header3 ||
|
|
1045
|
+
t.type === TokenType.Header4 ||
|
|
1046
|
+
t.type === TokenType.Header5 ||
|
|
1047
|
+
t.type === TokenType.Header6 ||
|
|
1048
|
+
t.type === TokenType.ListItem ||
|
|
1049
|
+
t.type === TokenType.OrderedListItem ||
|
|
1050
|
+
t.type === TokenType.Checkbox ||
|
|
1051
|
+
t.type === TokenType.CodeBlockFence ||
|
|
1052
|
+
t.type === TokenType.CodeBlockContent ||
|
|
1053
|
+
t.type === TokenType.BlockQuote ||
|
|
1054
|
+
t.type === TokenType.HorizontalRule ||
|
|
1055
|
+
t.type === TokenType.Image) {
|
|
1056
|
+
// Mark the end of these elements as hard breaks
|
|
1057
|
+
hardBreakPositions.add(t.end + viewportStart);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Also mark empty lines (two consecutive newlines) as hard breaks
|
|
1062
|
+
let lastWasNewline = false;
|
|
1063
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1064
|
+
const token = tokens[i];
|
|
1065
|
+
if (token.kind === 'Newline') {
|
|
1066
|
+
if (lastWasNewline && token.source_offset !== null) {
|
|
1067
|
+
hardBreakPositions.add(token.source_offset);
|
|
1068
|
+
}
|
|
1069
|
+
lastWasNewline = true;
|
|
1070
|
+
} else {
|
|
1071
|
+
lastWasNewline = false;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Transform tokens
|
|
1076
|
+
for (const token of tokens) {
|
|
1077
|
+
if (token.kind === 'Newline') {
|
|
1078
|
+
const pos = token.source_offset;
|
|
1079
|
+
if (pos !== null && hardBreakPositions.has(pos)) {
|
|
1080
|
+
// Keep as newline (hard break)
|
|
1081
|
+
result.push(token);
|
|
1082
|
+
} else {
|
|
1083
|
+
// Convert to space (soft break)
|
|
1084
|
+
result.push({
|
|
1085
|
+
source_offset: token.source_offset,
|
|
1086
|
+
kind: 'Space',
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
} else {
|
|
1090
|
+
// Keep other tokens as-is
|
|
1091
|
+
result.push(token);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
return result;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Handle render_start - enable highlighting for markdown files
|
|
1099
|
+
globalThis.onMarkdownRenderStart = function(data: { buffer_id: number }): void {
|
|
1100
|
+
// Auto-enable highlighting for markdown files on first render
|
|
1101
|
+
if (!highlightingBuffers.has(data.buffer_id)) {
|
|
1102
|
+
const info = editor.getBufferInfo(data.buffer_id);
|
|
1103
|
+
if (info && isMarkdownFile(info.path)) {
|
|
1104
|
+
highlightingBuffers.add(data.buffer_id);
|
|
1105
|
+
editor.debug(`Markdown highlighting auto-enabled for buffer ${data.buffer_id}`);
|
|
1106
|
+
} else {
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
// Note: Don't clear overlays here - the after-insert/after-delete handlers
|
|
1111
|
+
// already clear affected ranges via clearOverlaysInRange(). Clearing all
|
|
1112
|
+
// overlays here would cause flicker since lines_changed hasn't fired yet.
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1115
|
+
// Handle lines_changed - process visible lines incrementally
|
|
1116
|
+
globalThis.onMarkdownLinesChanged = function(data: {
|
|
1117
|
+
buffer_id: number;
|
|
1118
|
+
lines: Array<{
|
|
1119
|
+
line_number: number;
|
|
1120
|
+
byte_start: number;
|
|
1121
|
+
byte_end: number;
|
|
1122
|
+
content: string;
|
|
1123
|
+
}>;
|
|
1124
|
+
}): void {
|
|
1125
|
+
// Auto-enable highlighting for markdown files
|
|
1126
|
+
if (!highlightingBuffers.has(data.buffer_id)) {
|
|
1127
|
+
const info = editor.getBufferInfo(data.buffer_id);
|
|
1128
|
+
if (info && isMarkdownFile(info.path)) {
|
|
1129
|
+
highlightingBuffers.add(data.buffer_id);
|
|
1130
|
+
} else {
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Process all changed lines
|
|
1136
|
+
for (const line of data.lines) {
|
|
1137
|
+
highlightLine(data.buffer_id, line.line_number, line.byte_start, line.content);
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
// Handle buffer activation - auto-enable highlighting for markdown files
|
|
1142
|
+
globalThis.onMarkdownBufferActivated = function(data: { buffer_id: number }): void {
|
|
1143
|
+
enableHighlighting(data.buffer_id);
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
// Handle content changes - clear affected overlays for efficient updates
|
|
1147
|
+
globalThis.onMarkdownAfterInsert = function(data: {
|
|
1148
|
+
buffer_id: number;
|
|
1149
|
+
position: number;
|
|
1150
|
+
text: string;
|
|
1151
|
+
affected_start: number;
|
|
1152
|
+
affected_end: number;
|
|
1153
|
+
}): void {
|
|
1154
|
+
if (!highlightingBuffers.has(data.buffer_id)) return;
|
|
1155
|
+
|
|
1156
|
+
// Clear only overlays in the affected byte range
|
|
1157
|
+
// These overlays may now span incorrect content after the insertion
|
|
1158
|
+
// The affected lines will be re-processed via lines_changed with correct content
|
|
1159
|
+
editor.clearOverlaysInRange(data.buffer_id, data.affected_start, data.affected_end);
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
globalThis.onMarkdownAfterDelete = function(data: {
|
|
1163
|
+
buffer_id: number;
|
|
1164
|
+
start: number;
|
|
1165
|
+
end: number;
|
|
1166
|
+
deleted_text: string;
|
|
1167
|
+
affected_start: number;
|
|
1168
|
+
deleted_len: number;
|
|
1169
|
+
}): void {
|
|
1170
|
+
if (!highlightingBuffers.has(data.buffer_id)) return;
|
|
1171
|
+
|
|
1172
|
+
// Clear overlays that overlapped with the deleted range
|
|
1173
|
+
// Overlays entirely within the deleted range are already gone (their markers were deleted)
|
|
1174
|
+
// But overlays spanning the deletion boundary may now be incorrect
|
|
1175
|
+
// Use a slightly expanded range to catch boundary cases
|
|
1176
|
+
const clearStart = data.affected_start > 0 ? data.affected_start - 1 : 0;
|
|
1177
|
+
const clearEnd = data.affected_start + data.deleted_len + 1;
|
|
1178
|
+
editor.clearOverlaysInRange(data.buffer_id, clearStart, clearEnd);
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
// Handle buffer close events
|
|
1182
|
+
globalThis.onMarkdownBufferClosed = function(data: { buffer_id: number }): void {
|
|
1183
|
+
highlightingBuffers.delete(data.buffer_id);
|
|
1184
|
+
composeBuffers.delete(data.buffer_id);
|
|
1185
|
+
dirtyBuffers.delete(data.buffer_id);
|
|
1186
|
+
};
|
|
1187
|
+
|
|
1188
|
+
// Register hooks
|
|
1189
|
+
editor.on("view_transform_request", "onMarkdownViewTransform");
|
|
1190
|
+
editor.on("render_start", "onMarkdownRenderStart");
|
|
1191
|
+
editor.on("lines_changed", "onMarkdownLinesChanged");
|
|
1192
|
+
editor.on("buffer_activated", "onMarkdownBufferActivated");
|
|
1193
|
+
editor.on("after-insert", "onMarkdownAfterInsert");
|
|
1194
|
+
editor.on("after-delete", "onMarkdownAfterDelete");
|
|
1195
|
+
editor.on("buffer_closed", "onMarkdownBufferClosed");
|
|
1196
|
+
|
|
1197
|
+
// Register command
|
|
1198
|
+
editor.registerCommand(
|
|
1199
|
+
"Markdown: Toggle Compose",
|
|
1200
|
+
"Toggle beautiful Markdown rendering (soft breaks, syntax highlighting)",
|
|
1201
|
+
"markdownToggleCompose",
|
|
1202
|
+
"normal"
|
|
1203
|
+
);
|
|
1204
|
+
|
|
1205
|
+
// Initialization
|
|
1206
|
+
editor.debug("Markdown Compose plugin loaded - use 'Markdown: Toggle Compose' command");
|
|
1207
|
+
editor.setStatus("Markdown plugin ready");
|