@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.
Files changed (38) hide show
  1. package/.gitignore +2 -0
  2. package/LICENSE +117 -0
  3. package/README.md +54 -0
  4. package/binary-install.js +212 -0
  5. package/binary.js +126 -0
  6. package/install.js +4 -0
  7. package/npm-shrinkwrap.json +900 -0
  8. package/package.json +100 -0
  9. package/plugins/README.md +121 -0
  10. package/plugins/clangd_support.md +20 -0
  11. package/plugins/clangd_support.ts +323 -0
  12. package/plugins/color_highlighter.ts +302 -0
  13. package/plugins/diagnostics_panel.ts +308 -0
  14. package/plugins/examples/README.md +245 -0
  15. package/plugins/examples/async_demo.ts +165 -0
  16. package/plugins/examples/bookmarks.ts +329 -0
  17. package/plugins/examples/buffer_query_demo.ts +110 -0
  18. package/plugins/examples/git_grep.ts +262 -0
  19. package/plugins/examples/hello_world.ts +93 -0
  20. package/plugins/examples/virtual_buffer_demo.ts +116 -0
  21. package/plugins/find_references.ts +357 -0
  22. package/plugins/git_find_file.ts +298 -0
  23. package/plugins/git_grep.ts +188 -0
  24. package/plugins/git_log.ts +1283 -0
  25. package/plugins/lib/fresh.d.ts +849 -0
  26. package/plugins/lib/index.ts +24 -0
  27. package/plugins/lib/navigation-controller.ts +214 -0
  28. package/plugins/lib/panel-manager.ts +218 -0
  29. package/plugins/lib/types.ts +72 -0
  30. package/plugins/lib/virtual-buffer-factory.ts +158 -0
  31. package/plugins/manual_help.ts +243 -0
  32. package/plugins/markdown_compose.ts +1207 -0
  33. package/plugins/merge_conflict.ts +1811 -0
  34. package/plugins/path_complete.ts +163 -0
  35. package/plugins/search_replace.ts +481 -0
  36. package/plugins/todo_highlighter.ts +204 -0
  37. package/plugins/welcome.ts +74 -0
  38. 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: ![alt](url) - 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");