@fresh-editor/fresh-editor 0.1.13 → 0.1.15

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.
@@ -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.13"
26
+ "version": "0.1.15"
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.13"
899
+ "version": "0.1.15"
900
900
  }
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "artifactDownloadUrl": "https://github.com/sinelaw/fresh/releases/download/v0.1.13",
2
+ "artifactDownloadUrl": "https://github.com/sinelaw/fresh/releases/download/v0.1.15",
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.13",
95
+ "version": "0.1.15",
96
96
  "volta": {
97
97
  "node": "18.14.1",
98
98
  "npm": "9.5.0"
@@ -94,6 +94,38 @@ function markLinesModified(bufferId: number, startLine: number, endLine: number)
94
94
  }
95
95
  }
96
96
 
97
+ function reapplyIndicatorsFromDiff(bufferId: number): void {
98
+ const diff = editor.getBufferSavedDiff(bufferId);
99
+ if (!diff) return;
100
+
101
+ // If buffer matches saved snapshot, clear everything.
102
+ if (diff.equal) {
103
+ editor.clearLineIndicators(bufferId, NAMESPACE);
104
+ return;
105
+ }
106
+
107
+ const ranges = diff.line_ranges;
108
+ // If line info is unavailable, leave existing indicators (best effort).
109
+ if (!ranges) return;
110
+
111
+ // Reset namespace to drop stale indicators outside the changed ranges.
112
+ editor.clearLineIndicators(bufferId, NAMESPACE);
113
+ for (const [start, end] of ranges) {
114
+ for (let line = start; line < end; line++) {
115
+ editor.setLineIndicator(
116
+ bufferId,
117
+ line,
118
+ NAMESPACE,
119
+ SYMBOL,
120
+ COLOR[0],
121
+ COLOR[1],
122
+ COLOR[2],
123
+ PRIORITY
124
+ );
125
+ }
126
+ }
127
+ }
128
+
97
129
  // =============================================================================
98
130
  // Event Handlers
99
131
  // =============================================================================
@@ -179,6 +211,7 @@ globalThis.onBufferModifiedAfterInsert = function (args: {
179
211
  // Mark all affected lines (from start_line to end_line inclusive)
180
212
  // The indicator markers will automatically track their positions
181
213
  markLinesModified(bufferId, args.start_line, args.end_line);
214
+ reapplyIndicatorsFromDiff(bufferId);
182
215
 
183
216
  return true;
184
217
  };
@@ -209,6 +242,7 @@ globalThis.onBufferModifiedAfterDelete = function (args: {
209
242
  // Mark the line where deletion occurred
210
243
  // Markers for deleted lines are automatically cleaned up
211
244
  markLinesModified(bufferId, args.start_line, args.start_line);
245
+ reapplyIndicatorsFromDiff(bufferId);
212
246
 
213
247
  return true;
214
248
  };
@@ -123,6 +123,13 @@ interface BufferInfo {
123
123
  length: number;
124
124
  }
125
125
 
126
+ /** Diff vs last save for a buffer */
127
+ interface TsBufferSavedDiff {
128
+ equal: boolean;
129
+ byte_ranges: [number, number][];
130
+ line_ranges?: [number, number][] | null;
131
+ }
132
+
126
133
  /** Selection range */
127
134
  interface SelectionRange {
128
135
  /** Start byte position */
@@ -341,6 +348,8 @@ interface EditorAPI {
341
348
  * @returns true if process is running, false if not found or exited
342
349
  */
343
350
  isProcessRunning(#[bigint] process_id: number): boolean;
351
+ /** Get diff vs last saved snapshot for a buffer */
352
+ getBufferSavedDiff(buffer_id: number): TsBufferSavedDiff | null;
344
353
 
345
354
  // === Buffer Info Queries ===
346
355
  /**
@@ -1,7 +1,11 @@
1
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
2
+ // Provides compose mode for Markdown documents with:
3
+ // - Soft wrapping at a configurable width
4
+ // - Hanging indents for lists and block quotes
5
+ // - Centered margins
6
+ //
7
+ // Syntax highlighting is handled by the TextMate grammar (built-in to the editor)
8
+ // This plugin only adds the compose mode layout features.
5
9
 
6
10
  interface MarkdownConfig {
7
11
  composeWidth: number;
@@ -15,52 +19,9 @@ const config: MarkdownConfig = {
15
19
  hideLineNumbers: true,
16
20
  };
17
21
 
18
- // Track buffers with highlighting enabled (auto for markdown files)
19
- const highlightingBuffers = new Set<number>();
20
-
21
22
  // Track buffers in compose mode (explicit toggle)
22
23
  const composeBuffers = new Set<number>();
23
24
 
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
25
  // Types match the Rust ViewTokenWire structure
65
26
  interface ViewTokenWire {
66
27
  source_offset: number | null;
@@ -343,669 +304,6 @@ function parseMarkdownBlocks(text: string): ParsedBlock[] {
343
304
  return blocks;
344
305
  }
345
306
 
346
- // Colors for styling (RGB tuples)
347
- const COLORS = {
348
- header: [100, 149, 237] as [number, number, number], // Cornflower blue
349
- code: [152, 195, 121] as [number, number, number], // Green
350
- codeBlock: [152, 195, 121] as [number, number, number],
351
- fence: [80, 80, 80] as [number, number, number], // Subdued gray for ```
352
- link: [86, 156, 214] as [number, number, number], // Light blue
353
- linkUrl: [80, 80, 80] as [number, number, number], // Subdued gray
354
- bold: [255, 255, 220] as [number, number, number], // Bright for bold text
355
- boldMarker: [80, 80, 80] as [number, number, number], // Subdued for ** markers
356
- italic: [198, 180, 221] as [number, number, number], // Light purple for italic
357
- italicMarker: [80, 80, 80] as [number, number, number], // Subdued for * markers
358
- quote: [128, 128, 128] as [number, number, number], // Gray
359
- checkbox: [152, 195, 121] as [number, number, number], // Green
360
- listBullet: [86, 156, 214] as [number, number, number], // Light blue
361
- };
362
-
363
- // Simple Markdown parser
364
- class MarkdownParser {
365
- private text: string;
366
- private tokens: Token[] = [];
367
-
368
- constructor(text: string) {
369
- this.text = text;
370
- }
371
-
372
- parse(): Token[] {
373
- const lines = this.text.split('\n');
374
- let byteOffset = 0;
375
- let inCodeBlock = false;
376
- let codeFenceStart = -1;
377
-
378
- for (let i = 0; i < lines.length; i++) {
379
- const line = lines[i];
380
- const lineStart = byteOffset;
381
- const lineEnd = byteOffset + line.length;
382
-
383
- // Code block detection
384
- if (line.trim().startsWith('```')) {
385
- if (!inCodeBlock) {
386
- inCodeBlock = true;
387
- codeFenceStart = lineStart;
388
- this.tokens.push({
389
- type: TokenType.CodeBlockFence,
390
- start: lineStart,
391
- end: lineEnd,
392
- text: line,
393
- });
394
- } else {
395
- this.tokens.push({
396
- type: TokenType.CodeBlockFence,
397
- start: lineStart,
398
- end: lineEnd,
399
- text: line,
400
- });
401
- inCodeBlock = false;
402
- }
403
- } else if (inCodeBlock) {
404
- this.tokens.push({
405
- type: TokenType.CodeBlockContent,
406
- start: lineStart,
407
- end: lineEnd,
408
- text: line,
409
- });
410
- } else {
411
- // Parse line structure
412
- this.parseLine(line, lineStart, lineEnd);
413
- }
414
-
415
- byteOffset = lineEnd + 1; // +1 for newline
416
- }
417
-
418
- // Parse inline styles after structure
419
- this.parseInlineStyles();
420
-
421
- return this.tokens;
422
- }
423
-
424
- private parseLine(line: string, start: number, end: number): void {
425
- const trimmed = line.trim();
426
-
427
- // Headers
428
- const headerMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
429
- if (headerMatch) {
430
- const level = headerMatch[1].length;
431
- const type = [
432
- TokenType.Header1,
433
- TokenType.Header2,
434
- TokenType.Header3,
435
- TokenType.Header4,
436
- TokenType.Header5,
437
- TokenType.Header6,
438
- ][level - 1];
439
- this.tokens.push({
440
- type,
441
- start,
442
- end,
443
- text: line,
444
- level,
445
- });
446
- return;
447
- }
448
-
449
- // Horizontal rule
450
- if (trimmed.match(/^(-{3,}|\*{3,}|_{3,})$/)) {
451
- this.tokens.push({
452
- type: TokenType.HorizontalRule,
453
- start,
454
- end,
455
- text: line,
456
- });
457
- return;
458
- }
459
-
460
- // List items
461
- const bulletMatch = line.match(/^(\s*)([-*+])\s+(.*)$/);
462
- if (bulletMatch) {
463
- const indent = bulletMatch[1].length;
464
- const hasCheckbox = bulletMatch[3].match(/^\[([ x])\]\s+/);
465
-
466
- if (hasCheckbox) {
467
- this.tokens.push({
468
- type: TokenType.Checkbox,
469
- start,
470
- end,
471
- text: line,
472
- level: indent,
473
- checked: hasCheckbox[1] === 'x',
474
- });
475
- } else {
476
- this.tokens.push({
477
- type: TokenType.ListItem,
478
- start,
479
- end,
480
- text: line,
481
- level: indent,
482
- });
483
- }
484
- return;
485
- }
486
-
487
- // Ordered list
488
- const orderedMatch = line.match(/^(\s*)(\d+\.)\s+(.*)$/);
489
- if (orderedMatch) {
490
- const indent = orderedMatch[1].length;
491
- this.tokens.push({
492
- type: TokenType.OrderedListItem,
493
- start,
494
- end,
495
- text: line,
496
- level: indent,
497
- });
498
- return;
499
- }
500
-
501
- // Block quote
502
- if (trimmed.startsWith('>')) {
503
- this.tokens.push({
504
- type: TokenType.BlockQuote,
505
- start,
506
- end,
507
- text: line,
508
- });
509
- return;
510
- }
511
-
512
- // Hard breaks (two spaces + newline, or backslash + newline)
513
- if (line.endsWith(' ') || line.endsWith('\\')) {
514
- this.tokens.push({
515
- type: TokenType.HardBreak,
516
- start,
517
- end,
518
- text: line,
519
- });
520
- return;
521
- }
522
-
523
- // Images: ![alt](url) - these should have hard breaks to keep each on its own line
524
- if (trimmed.match(/^!\[.*\]\(.*\)$/)) {
525
- this.tokens.push({
526
- type: TokenType.Image,
527
- start,
528
- end,
529
- text: line,
530
- });
531
- return;
532
- }
533
-
534
- // Default: paragraph
535
- if (trimmed.length > 0) {
536
- this.tokens.push({
537
- type: TokenType.Paragraph,
538
- start,
539
- end,
540
- text: line,
541
- });
542
- }
543
- }
544
-
545
- private parseInlineStyles(): void {
546
- // Parse inline markdown (bold, italic, code, links) within text
547
- // This is a simplified parser - a full implementation would use a proper MD parser
548
-
549
- for (const token of this.tokens) {
550
- if (token.type === TokenType.Paragraph ||
551
- token.type === TokenType.ListItem ||
552
- token.type === TokenType.OrderedListItem) {
553
- // Find inline code
554
- this.findInlineCode(token);
555
- // Find bold/italic
556
- this.findEmphasis(token);
557
- // Find links
558
- this.findLinks(token);
559
- }
560
- }
561
- }
562
-
563
- private findInlineCode(token: Token): void {
564
- const regex = /`([^`]+)`/g;
565
- let match;
566
- while ((match = regex.exec(token.text)) !== null) {
567
- this.tokens.push({
568
- type: TokenType.InlineCode,
569
- start: token.start + match.index,
570
- end: token.start + match.index + match[0].length,
571
- text: match[0],
572
- });
573
- }
574
- }
575
-
576
- private findEmphasis(token: Token): void {
577
- // Bold: **text** or __text__
578
- const boldRegex = /(\*\*|__)([^*_]+)\1/g;
579
- let match;
580
- while ((match = boldRegex.exec(token.text)) !== null) {
581
- this.tokens.push({
582
- type: TokenType.Bold,
583
- start: token.start + match.index,
584
- end: token.start + match.index + match[0].length,
585
- text: match[0],
586
- });
587
- }
588
-
589
- // Italic: *text* or _text_
590
- const italicRegex = /(\*|_)([^*_]+)\1/g;
591
- while ((match = italicRegex.exec(token.text)) !== null) {
592
- // Skip if it's part of bold
593
- const isBold = this.tokens.some(t =>
594
- t.type === TokenType.Bold &&
595
- t.start <= token.start + match.index &&
596
- t.end >= token.start + match.index + match[0].length
597
- );
598
- if (!isBold) {
599
- this.tokens.push({
600
- type: TokenType.Italic,
601
- start: token.start + match.index,
602
- end: token.start + match.index + match[0].length,
603
- text: match[0],
604
- });
605
- }
606
- }
607
-
608
- // Strikethrough: ~~text~~
609
- const strikeRegex = /~~([^~]+)~~/g;
610
- while ((match = strikeRegex.exec(token.text)) !== null) {
611
- this.tokens.push({
612
- type: TokenType.Strikethrough,
613
- start: token.start + match.index,
614
- end: token.start + match.index + match[0].length,
615
- text: match[0],
616
- });
617
- }
618
- }
619
-
620
- private findLinks(token: Token): void {
621
- // Links: [text](url)
622
- const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
623
- let match;
624
- while ((match = linkRegex.exec(token.text)) !== null) {
625
- const fullStart = token.start + match.index;
626
- const textStart = fullStart + 1; // After [
627
- const textEnd = textStart + match[1].length;
628
- const urlStart = textEnd + 2; // After ](
629
- const urlEnd = urlStart + match[2].length;
630
-
631
- this.tokens.push({
632
- type: TokenType.Link,
633
- start: fullStart,
634
- end: fullStart + match[0].length,
635
- text: match[0],
636
- });
637
-
638
- this.tokens.push({
639
- type: TokenType.LinkText,
640
- start: textStart,
641
- end: textEnd,
642
- text: match[1],
643
- });
644
-
645
- this.tokens.push({
646
- type: TokenType.LinkUrl,
647
- start: urlStart,
648
- end: urlEnd,
649
- text: match[2],
650
- });
651
- }
652
- }
653
- }
654
-
655
- // Apply styling overlays based on parsed tokens
656
- function applyMarkdownStyling(bufferId: number, tokens: Token[]): void {
657
- // Clear existing markdown overlays
658
- editor.clearNamespace(bufferId, "md");
659
-
660
- for (const token of tokens) {
661
- let color: [number, number, number] | null = null;
662
- let underline = false;
663
- let overlayId = "md";
664
-
665
- switch (token.type) {
666
- case TokenType.Header1:
667
- case TokenType.Header2:
668
- case TokenType.Header3:
669
- case TokenType.Header4:
670
- case TokenType.Header5:
671
- case TokenType.Header6:
672
- color = COLORS.header;
673
- underline = true;
674
- break;
675
-
676
- case TokenType.InlineCode:
677
- color = COLORS.code;
678
- break;
679
-
680
- case TokenType.CodeBlockFence:
681
- color = COLORS.fence;
682
- break;
683
-
684
- case TokenType.CodeBlockContent:
685
- color = COLORS.codeBlock;
686
- break;
687
-
688
- case TokenType.BlockQuote:
689
- color = COLORS.quote;
690
- break;
691
-
692
- case TokenType.Bold:
693
- // Style bold markers (** or __) subdued, content bold
694
- const boldMatch = token.text.match(/^(\*\*|__)(.*)(\*\*|__)$/);
695
- if (boldMatch) {
696
- const markerLen = boldMatch[1].length;
697
- // Subdued markers
698
- editor.addOverlay(bufferId, "md",
699
- token.start, token.start + markerLen,
700
- COLORS.boldMarker[0], COLORS.boldMarker[1], COLORS.boldMarker[2], false, false, false);
701
- editor.addOverlay(bufferId, "md",
702
- token.end - markerLen, token.end,
703
- COLORS.boldMarker[0], COLORS.boldMarker[1], COLORS.boldMarker[2], false, false, false);
704
- // Bold content with bold=true
705
- editor.addOverlay(bufferId, "md",
706
- token.start + markerLen, token.end - markerLen,
707
- COLORS.bold[0], COLORS.bold[1], COLORS.bold[2], false, true, false);
708
- } else {
709
- color = COLORS.bold;
710
- }
711
- break;
712
-
713
- case TokenType.Italic:
714
- // Style italic markers (* or _) subdued, content italic
715
- const italicMatch = token.text.match(/^(\*|_)(.*)(\*|_)$/);
716
- if (italicMatch) {
717
- const markerLen = 1;
718
- // Subdued markers
719
- editor.addOverlay(bufferId, "md",
720
- token.start, token.start + markerLen,
721
- COLORS.italicMarker[0], COLORS.italicMarker[1], COLORS.italicMarker[2], false, false, false);
722
- editor.addOverlay(bufferId, "md",
723
- token.end - markerLen, token.end,
724
- COLORS.italicMarker[0], COLORS.italicMarker[1], COLORS.italicMarker[2], false, false, false);
725
- // Italic content with italic=true
726
- editor.addOverlay(bufferId, "md",
727
- token.start + markerLen, token.end - markerLen,
728
- COLORS.italic[0], COLORS.italic[1], COLORS.italic[2], false, false, true);
729
- } else {
730
- color = COLORS.italic;
731
- }
732
- break;
733
-
734
- case TokenType.LinkText:
735
- color = COLORS.link;
736
- underline = true;
737
- break;
738
-
739
- case TokenType.LinkUrl:
740
- color = COLORS.linkUrl;
741
- break;
742
-
743
- case TokenType.ListItem:
744
- case TokenType.OrderedListItem:
745
- // Style just the bullet/number
746
- const bulletMatch = token.text.match(/^(\s*)([-*+]|\d+\.)/);
747
- if (bulletMatch) {
748
- const bulletEnd = token.start + bulletMatch[0].length;
749
- editor.addOverlay(
750
- bufferId,
751
- "md",
752
- token.start,
753
- bulletEnd,
754
- COLORS.listBullet[0],
755
- COLORS.listBullet[1],
756
- COLORS.listBullet[2],
757
- false
758
- );
759
- }
760
- break;
761
-
762
- case TokenType.Checkbox:
763
- // Style checkbox and bullet
764
- const checkboxMatch = token.text.match(/^(\s*[-*+]\s+\[[ x]\])/);
765
- if (checkboxMatch) {
766
- const checkboxEnd = token.start + checkboxMatch[0].length;
767
- editor.addOverlay(
768
- bufferId,
769
- "md",
770
- token.start,
771
- checkboxEnd,
772
- COLORS.checkbox[0],
773
- COLORS.checkbox[1],
774
- COLORS.checkbox[2],
775
- false
776
- );
777
- }
778
- break;
779
- }
780
-
781
- if (color) {
782
- editor.addOverlay(
783
- bufferId,
784
- overlayId,
785
- token.start,
786
- token.end,
787
- color[0],
788
- color[1],
789
- color[2],
790
- underline
791
- );
792
- }
793
- }
794
- }
795
-
796
- // Highlight a single line for markdown (used with lines_changed event)
797
- function highlightLine(
798
- bufferId: number,
799
- lineNumber: number,
800
- byteStart: number,
801
- content: string
802
- ): void {
803
- const trimmed = content.trim();
804
- if (trimmed.length === 0) return;
805
-
806
- // Headers
807
- const headerMatch = trimmed.match(/^(#{1,6})\s/);
808
- if (headerMatch) {
809
- editor.addOverlay(
810
- bufferId,
811
- "md",
812
- byteStart,
813
- byteStart + content.length,
814
- COLORS.header[0], COLORS.header[1], COLORS.header[2],
815
- false, true, false // bold
816
- );
817
- return;
818
- }
819
-
820
- // Code block fences
821
- if (trimmed.startsWith('```')) {
822
- editor.addOverlay(
823
- bufferId,
824
- "md",
825
- byteStart,
826
- byteStart + content.length,
827
- COLORS.fence[0], COLORS.fence[1], COLORS.fence[2],
828
- false
829
- );
830
- return;
831
- }
832
-
833
- // Block quotes
834
- if (trimmed.startsWith('>')) {
835
- editor.addOverlay(
836
- bufferId,
837
- "md",
838
- byteStart,
839
- byteStart + content.length,
840
- COLORS.quote[0], COLORS.quote[1], COLORS.quote[2],
841
- false
842
- );
843
- return;
844
- }
845
-
846
- // Horizontal rules
847
- if (trimmed.match(/^[-*_]{3,}$/)) {
848
- editor.addOverlay(
849
- bufferId,
850
- "md",
851
- byteStart,
852
- byteStart + content.length,
853
- COLORS.quote[0], COLORS.quote[1], COLORS.quote[2],
854
- false
855
- );
856
- return;
857
- }
858
-
859
- // List items (unordered)
860
- const listMatch = content.match(/^(\s*)([-*+])\s/);
861
- if (listMatch) {
862
- const bulletStart = byteStart + listMatch[1].length;
863
- const bulletEnd = bulletStart + 1;
864
- editor.addOverlay(
865
- bufferId,
866
- "md",
867
- bulletStart,
868
- bulletEnd,
869
- COLORS.listBullet[0], COLORS.listBullet[1], COLORS.listBullet[2],
870
- false
871
- );
872
- }
873
-
874
- // Ordered list items
875
- const orderedMatch = content.match(/^(\s*)(\d+\.)\s/);
876
- if (orderedMatch) {
877
- const numStart = byteStart + orderedMatch[1].length;
878
- const numEnd = numStart + orderedMatch[2].length;
879
- editor.addOverlay(
880
- bufferId,
881
- "md",
882
- numStart,
883
- numEnd,
884
- COLORS.listBullet[0], COLORS.listBullet[1], COLORS.listBullet[2],
885
- false
886
- );
887
- }
888
-
889
- // Checkboxes
890
- const checkMatch = content.match(/^(\s*[-*+]\s+)(\[[ x]\])/);
891
- if (checkMatch) {
892
- const checkStart = byteStart + checkMatch[1].length;
893
- const checkEnd = checkStart + checkMatch[2].length;
894
- editor.addOverlay(
895
- bufferId,
896
- "md",
897
- checkStart,
898
- checkEnd,
899
- COLORS.checkbox[0], COLORS.checkbox[1], COLORS.checkbox[2],
900
- false
901
- );
902
- }
903
-
904
- // Inline elements
905
-
906
- // Inline code: `code`
907
- const codeRegex = /`([^`]+)`/g;
908
- let match;
909
- while ((match = codeRegex.exec(content)) !== null) {
910
- editor.addOverlay(
911
- bufferId,
912
- "md",
913
- byteStart + match.index,
914
- byteStart + match.index + match[0].length,
915
- COLORS.code[0], COLORS.code[1], COLORS.code[2],
916
- false
917
- );
918
- }
919
-
920
- // Bold: **text** or __text__
921
- const boldRegex = /(\*\*|__)([^*_]+)\1/g;
922
- while ((match = boldRegex.exec(content)) !== null) {
923
- const markerLen = match[1].length;
924
- const fullStart = byteStart + match.index;
925
- const fullEnd = fullStart + match[0].length;
926
- // Subdued markers
927
- editor.addOverlay(
928
- bufferId,
929
- "md",
930
- fullStart, fullStart + markerLen,
931
- COLORS.boldMarker[0], COLORS.boldMarker[1], COLORS.boldMarker[2],
932
- false, false, false
933
- );
934
- editor.addOverlay(
935
- bufferId,
936
- "md",
937
- fullEnd - markerLen, fullEnd,
938
- COLORS.boldMarker[0], COLORS.boldMarker[1], COLORS.boldMarker[2],
939
- false, false, false
940
- );
941
- // Bold content
942
- editor.addOverlay(
943
- bufferId,
944
- "md",
945
- fullStart + markerLen, fullEnd - markerLen,
946
- COLORS.bold[0], COLORS.bold[1], COLORS.bold[2],
947
- false, true, false
948
- );
949
- }
950
-
951
- // Italic: *text* or _text_ (but not inside bold)
952
- const italicRegex = /(?<!\*|\w)(\*|_)(?!\*|_)([^*_\n]+)(?<!\*|_)\1(?!\*|\w)/g;
953
- while ((match = italicRegex.exec(content)) !== null) {
954
- const fullStart = byteStart + match.index;
955
- const fullEnd = fullStart + match[0].length;
956
- // Subdued markers
957
- editor.addOverlay(
958
- bufferId,
959
- "md",
960
- fullStart, fullStart + 1,
961
- COLORS.italicMarker[0], COLORS.italicMarker[1], COLORS.italicMarker[2],
962
- false, false, false
963
- );
964
- editor.addOverlay(
965
- bufferId,
966
- "md",
967
- fullEnd - 1, fullEnd,
968
- COLORS.italicMarker[0], COLORS.italicMarker[1], COLORS.italicMarker[2],
969
- false, false, false
970
- );
971
- // Italic content
972
- editor.addOverlay(
973
- bufferId,
974
- "md",
975
- fullStart + 1, fullEnd - 1,
976
- COLORS.italic[0], COLORS.italic[1], COLORS.italic[2],
977
- false, false, true
978
- );
979
- }
980
-
981
- // Links: [text](url)
982
- const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
983
- while ((match = linkRegex.exec(content)) !== null) {
984
- const fullStart = byteStart + match.index;
985
- const textStart = fullStart + 1;
986
- const textEnd = textStart + match[1].length;
987
- const urlStart = textEnd + 2;
988
- const urlEnd = urlStart + match[2].length;
989
-
990
- // Link text (underlined)
991
- editor.addOverlay(
992
- bufferId,
993
- "md",
994
- textStart, textEnd,
995
- COLORS.link[0], COLORS.link[1], COLORS.link[2],
996
- true // underline
997
- );
998
- // Link URL (subdued)
999
- editor.addOverlay(
1000
- bufferId,
1001
- "md",
1002
- urlStart, urlEnd,
1003
- COLORS.linkUrl[0], COLORS.linkUrl[1], COLORS.linkUrl[2],
1004
- false
1005
- );
1006
- }
1007
- }
1008
-
1009
307
  // Check if a file is a markdown file
1010
308
  function isMarkdownFile(path: string): boolean {
1011
309
  return path.endsWith('.md') || path.endsWith('.markdown');
@@ -1025,19 +323,6 @@ function processBuffer(bufferId: number, _splitId?: number): void {
1025
323
  editor.refreshLines(bufferId);
1026
324
  }
1027
325
 
1028
- // Enable highlighting for a markdown buffer (auto on file open)
1029
- function enableHighlighting(bufferId: number): void {
1030
- const info = editor.getBufferInfo(bufferId);
1031
- if (!info || !isMarkdownFile(info.path)) return;
1032
-
1033
- if (!highlightingBuffers.has(bufferId)) {
1034
- highlightingBuffers.add(bufferId);
1035
- // Trigger a refresh so lines_changed will process visible lines
1036
- editor.refreshLines(bufferId);
1037
- editor.debug(`Markdown highlighting enabled for buffer ${bufferId}`);
1038
- }
1039
- }
1040
-
1041
326
  // Enable full compose mode for a buffer (explicit toggle)
1042
327
  function enableMarkdownCompose(bufferId: number): void {
1043
328
  const info = editor.getBufferInfo(bufferId);
@@ -1045,7 +330,6 @@ function enableMarkdownCompose(bufferId: number): void {
1045
330
 
1046
331
  if (!composeBuffers.has(bufferId)) {
1047
332
  composeBuffers.add(bufferId);
1048
- highlightingBuffers.add(bufferId); // Also ensure highlighting is on
1049
333
 
1050
334
  // Hide line numbers in compose mode
1051
335
  editor.setLineNumbers(bufferId, false);
@@ -1055,7 +339,7 @@ function enableMarkdownCompose(bufferId: number): void {
1055
339
  }
1056
340
  }
1057
341
 
1058
- // Disable compose mode for a buffer (but keep highlighting)
342
+ // Disable compose mode for a buffer
1059
343
  function disableMarkdownCompose(bufferId: number): void {
1060
344
  if (composeBuffers.has(bufferId)) {
1061
345
  composeBuffers.delete(bufferId);
@@ -1066,7 +350,6 @@ function disableMarkdownCompose(bufferId: number): void {
1066
350
  // Clear view transform to return to normal rendering
1067
351
  editor.clearViewTransform(bufferId);
1068
352
 
1069
- // Keep highlighting on, just clear the view transform
1070
353
  editor.refreshLines(bufferId);
1071
354
  editor.debug(`Markdown compose disabled for buffer ${bufferId}`);
1072
355
  }
@@ -1092,7 +375,7 @@ globalThis.markdownToggleCompose = function(): void {
1092
375
  enableMarkdownCompose(bufferId);
1093
376
  // Trigger a re-render to apply the transform
1094
377
  editor.refreshLines(bufferId);
1095
- editor.setStatus("Markdown Compose: ON (soft breaks, styled)");
378
+ editor.setStatus("Markdown Compose: ON (soft breaks, centered)");
1096
379
  }
1097
380
  };
1098
381
 
@@ -1243,7 +526,7 @@ function transformMarkdownTokens(
1243
526
  }
1244
527
 
1245
528
  // Handle view transform request - receives tokens from core for transformation
1246
- // Only applies transforms when in compose mode (not just highlighting)
529
+ // Only applies transforms when in compose mode
1247
530
  globalThis.onMarkdownViewTransform = function(data: {
1248
531
  buffer_id: number;
1249
532
  split_id: number;
@@ -1251,7 +534,7 @@ globalThis.onMarkdownViewTransform = function(data: {
1251
534
  viewport_end: number;
1252
535
  tokens: ViewTokenWire[];
1253
536
  }): void {
1254
- // Only transform when in compose mode (view transforms change line wrapping etc)
537
+ // Only transform when in compose mode
1255
538
  if (!composeBuffers.has(data.buffer_id)) return;
1256
539
 
1257
540
  const info = editor.getBufferInfo(data.buffer_id);
@@ -1266,18 +549,6 @@ globalThis.onMarkdownViewTransform = function(data: {
1266
549
  data.viewport_start
1267
550
  );
1268
551
 
1269
- // Extract text for overlay styling
1270
- const text = extractTextFromTokens(data.tokens);
1271
- const parser = new MarkdownParser(text);
1272
- const mdTokens = parser.parse();
1273
-
1274
- // Adjust token offsets for viewport
1275
- for (const token of mdTokens) {
1276
- token.start += data.viewport_start;
1277
- token.end += data.viewport_start;
1278
- }
1279
- applyMarkdownStyling(data.buffer_id, mdTokens);
1280
-
1281
552
  // Submit the transformed tokens - keep compose_width for margins/centering
1282
553
  const layoutHints: LayoutHints = {
1283
554
  compose_width: config.composeWidth,
@@ -1294,103 +565,13 @@ globalThis.onMarkdownViewTransform = function(data: {
1294
565
  );
1295
566
  };
1296
567
 
1297
- // Handle render_start - enable highlighting for markdown files
1298
- globalThis.onMarkdownRenderStart = function(data: { buffer_id: number }): void {
1299
- // Auto-enable highlighting for markdown files on first render
1300
- if (!highlightingBuffers.has(data.buffer_id)) {
1301
- const info = editor.getBufferInfo(data.buffer_id);
1302
- if (info && isMarkdownFile(info.path)) {
1303
- highlightingBuffers.add(data.buffer_id);
1304
- editor.debug(`Markdown highlighting auto-enabled for buffer ${data.buffer_id}`);
1305
- } else {
1306
- return;
1307
- }
1308
- }
1309
- // Note: Don't clear overlays here - the after-insert/after-delete handlers
1310
- // already clear affected ranges via clearOverlaysInRange(). Clearing all
1311
- // overlays here would cause flicker since lines_changed hasn't fired yet.
1312
- };
1313
-
1314
- // Handle lines_changed - process visible lines incrementally
1315
- globalThis.onMarkdownLinesChanged = function(data: {
1316
- buffer_id: number;
1317
- lines: Array<{
1318
- line_number: number;
1319
- byte_start: number;
1320
- byte_end: number;
1321
- content: string;
1322
- }>;
1323
- }): void {
1324
- // Auto-enable highlighting for markdown files
1325
- if (!highlightingBuffers.has(data.buffer_id)) {
1326
- const info = editor.getBufferInfo(data.buffer_id);
1327
- if (info && isMarkdownFile(info.path)) {
1328
- highlightingBuffers.add(data.buffer_id);
1329
- } else {
1330
- return;
1331
- }
1332
- }
1333
-
1334
- // Process all changed lines
1335
- for (const line of data.lines) {
1336
- highlightLine(data.buffer_id, line.line_number, line.byte_start, line.content);
1337
- }
1338
- };
1339
-
1340
- // Handle buffer activation - auto-enable highlighting for markdown files
1341
- globalThis.onMarkdownBufferActivated = function(data: { buffer_id: number }): void {
1342
- enableHighlighting(data.buffer_id);
1343
- };
1344
-
1345
- // Handle content changes - clear affected overlays for efficient updates
1346
- globalThis.onMarkdownAfterInsert = function(data: {
1347
- buffer_id: number;
1348
- position: number;
1349
- text: string;
1350
- affected_start: number;
1351
- affected_end: number;
1352
- }): void {
1353
- if (!highlightingBuffers.has(data.buffer_id)) return;
1354
-
1355
- // Clear only overlays in the affected byte range
1356
- // These overlays may now span incorrect content after the insertion
1357
- // The affected lines will be re-processed via lines_changed with correct content
1358
- editor.clearOverlaysInRange(data.buffer_id, data.affected_start, data.affected_end);
1359
- };
1360
-
1361
- globalThis.onMarkdownAfterDelete = function(data: {
1362
- buffer_id: number;
1363
- start: number;
1364
- end: number;
1365
- deleted_text: string;
1366
- affected_start: number;
1367
- deleted_len: number;
1368
- }): void {
1369
- if (!highlightingBuffers.has(data.buffer_id)) return;
1370
-
1371
- // Clear overlays that overlapped with the deleted range
1372
- // Overlays entirely within the deleted range are already gone (their markers were deleted)
1373
- // But overlays spanning the deletion boundary may now be incorrect
1374
- // Use a slightly expanded range to catch boundary cases
1375
- const clearStart = data.affected_start > 0 ? data.affected_start - 1 : 0;
1376
- const clearEnd = data.affected_start + data.deleted_len + 1;
1377
- editor.clearOverlaysInRange(data.buffer_id, clearStart, clearEnd);
1378
- };
1379
-
1380
- // Handle buffer close events
568
+ // Handle buffer close events - clean up compose mode tracking
1381
569
  globalThis.onMarkdownBufferClosed = function(data: { buffer_id: number }): void {
1382
- highlightingBuffers.delete(data.buffer_id);
1383
570
  composeBuffers.delete(data.buffer_id);
1384
- dirtyBuffers.delete(data.buffer_id);
1385
571
  };
1386
572
 
1387
573
  // Register hooks
1388
574
  editor.on("view_transform_request", "onMarkdownViewTransform");
1389
- editor.on("render_start", "onMarkdownRenderStart");
1390
- editor.on("lines_changed", "onMarkdownLinesChanged");
1391
- editor.on("buffer_activated", "onMarkdownBufferActivated");
1392
- editor.on("after-insert", "onMarkdownAfterInsert");
1393
- editor.on("after-delete", "onMarkdownAfterDelete");
1394
575
  editor.on("buffer_closed", "onMarkdownBufferClosed");
1395
576
  editor.on("prompt_confirmed", "onMarkdownComposeWidthConfirmed");
1396
577
 
@@ -1430,7 +611,7 @@ globalThis.onMarkdownComposeWidthConfirmed = function(args: {
1430
611
  // Register commands
1431
612
  editor.registerCommand(
1432
613
  "Markdown: Toggle Compose",
1433
- "Toggle beautiful Markdown rendering (soft breaks, syntax highlighting)",
614
+ "Toggle compose mode (soft wrapping, centered margins)",
1434
615
  "markdownToggleCompose",
1435
616
  "normal"
1436
617
  );