fresh-editor 0.1.52

A lightweight, fast terminal-based text editor with LSP support and TypeScript plugins
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
# Fresh Architecture

Fresh is a high-performance, terminal-based text editor built in Rust. It's designed to be fast, responsive, and extensible, with a modern architecture that draws inspiration from the best ideas in the world of text editors.

## Core Design Principles

*   **Performance First:** Every architectural decision is made with performance in mind. This includes the choice of data structures, the design of the rendering pipeline, and the implementation of core features.
*   **Event-Driven:** All state changes are represented as events, which are processed by a central event loop. This makes the editor's state predictable and enables features like unlimited undo/redo.
*   **Asynchronous I/O:** All file and process I/O is handled asynchronously on a separate thread pool. This ensures that the editor's UI is never blocked by slow I/O operations.
*   **Extensible:** A powerful TypeScript-based plugin system allows for deep customization and extension of the editor's functionality.

## High-Level Architecture

```
┌─────────────────────────────────────────────────────────┐
│                   MAIN THREAD (Sync)                    │
│  ┌──────────────┐  ┌──────────────┐  ┌─────────────┐    │
│  │ Event Loop   │→ │  Editor      │→ │  Renderer   │    │
│  │ (crossterm)  │  │  (state)     │  │  (ratatui)  │    │
│  └──────────────┘  └──────────────┘  └─────────────┘    │
│         ↓                 ↑                             │
│    Input Queue      EventQueue (mpsc)                   │
└─────────────────────────────────────────────────────────┘
         ↑                      ↑
         │ send events          │ send messages
         │                      │
    ┌────┴──────────┐  ┌────────┴──────────┐
    │ LSP Tasks     │  │ File I/O Tasks    │
    │ (Tokio)       │  │ (Tokio)           │
    └───────────────┘  └───────────────────┘
```

## The Document Model

To provide a clean separation between the editor's UI and the underlying text buffer, Fresh uses a `DocumentModel` trait. This abstraction layer is responsible for all interactions with the text buffer and provides a consistent API for both small and large files.

### Dual Position System

To support multi-gigabyte files where line indexing may be unavailable, the `DocumentModel` uses a dual position system:

*   **`DocumentPosition::LineColumn`:** For small files, this provides precise line and column-based positioning.
*   **`DocumentPosition::ByteOffset`:** For large files, this provides byte-offset-based positioning, which is always available and precise.

### The `DocumentModel` Trait

The `DocumentModel` trait defines a set of methods for interacting with the document, including:

*   **`get_viewport_content`:** The core rendering primitive, which returns the content for the visible portion of the screen.
*   **`position_to_offset` and `offset_to_position`:** For converting between the two position types.
*   **`insert`, `delete`, and `replace`:** For modifying the document's content.

This abstraction allows the rest of the editor to be blissfully unaware of the details of the underlying text buffer, such as whether it's a small file with a full line index or a large file with lazy loading.

## The Buffer

The core of the editor is the text buffer, which is implemented as a **`PieceTree`**. A `PieceTree` is a balanced binary tree that represents the text as a sequence of "pieces," which are references to either the original, immutable file buffer or an in-memory buffer of user additions.

This data structure provides several key advantages:

*   **O(log n) Edits:** Inserts and deletes are O(log n), where n is the number of pieces. This makes text editing extremely fast, even in large files.
*   **Efficient Memory Usage:** The `PieceTree` only stores the changes to the file, not the entire file content. This makes it very memory-efficient, especially for large files.
*   **Lazy Loading:** For multi-gigabyte files, Fresh uses a lazy loading strategy. The file is not loaded into memory all at once. Instead, chunks of the file are loaded on demand as the user scrolls through the file.

## The Rendering Pipeline

The rendering pipeline transforms source bytes into styled terminal output through a series of well-defined stages. Each stage preserves source-byte mappings for cursor positioning, and plugins can intercept the pipeline at multiple points.

### Complete Data Flow

```mermaid
flowchart TD
    subgraph Storage["1. Storage Layer"]
        FILE[Source File bytes]
        FILE --> TB[TextBuffer::load_from_file]
        TB --> PT[PieceTree + Vec‹StringBuffer›]
    end

    subgraph Viewport["2. Viewport Calculation"]
        PT --> VP[Viewport.top_byte]
        VP --> VR[Visible byte range]
    end

    subgraph Tokenization["3. Tokenization"]
        VR --> BT[build_base_tokens]
        BT --> VTW[Vec‹ViewTokenWire›]
    end

    subgraph PluginTransform["4. Plugin Transform"]
        VTW --> HOOK{view_transform_request hook}
        HOOK -->|Plugin responds| SVT[SubmitViewTransform]
        SVT --> TVT[Transformed tokens]
        HOOK -->|No plugin| TVT
    end

    subgraph ViewLines["5. ViewLine Generation"]
        TVT --> WRAP{Line wrapping?}
        WRAP -->|Yes| WRP[apply_wrapping_transform]
        WRAP -->|No| VLI
        WRP --> VLI[ViewLineIterator]
        VLI --> VL[Vec‹ViewLine›]
    end

    subgraph Styling["6. Styling Layers"]
        VL --> TS[Token styles]
        TS --> ANSI[ANSI escape parsing]
        ANSI --> SYN[Syntax highlighting]
        SYN --> SEM[Semantic highlighting]
        SEM --> SEL[Selection ranges]
        SEL --> OVL[Overlays]
        OVL --> STYLED[Styled characters]
    end

    subgraph Output["7. Ratatui Output"]
        STYLED --> SPANS[Vec‹Line‹Span››]
        SPANS --> PARA[Paragraph widget]
        PARA --> FRAME[frame.render_widget]
        FRAME --> TERM[Terminal]
    end
```

### Stage 1: Source Storage

Source bytes are stored in a `PieceTree` (`src/piece_tree.rs`), a balanced tree of pieces referencing either the original file or an append-only buffer of edits.

| File Size | Storage Strategy | Line Indexing |
|-----------|-----------------|---------------|
| < 100MB | `BufferData::Loaded { data, line_starts }` | Full index available |
| ≥ 100MB | `BufferData::Unloaded { file_path, file_offset, bytes }` | Lazy chunk loading |

**Key files:** `src/text_buffer.rs:141-149`, `src/piece_tree.rs:12-36`

### Stage 2: Viewport Calculation

The `Viewport` (`src/viewport.rs`) determines which bytes are visible:

```rust
Viewport {
    top_byte: usize,      // Authoritative scroll position
    left_column: usize,   // Horizontal scroll
    width: u16,
    height: u16,
}
```

`top_byte` is the anchor—line boundaries are discovered by scanning backward. This works even for files without line indexing.

**Key file:** `src/viewport.rs:94-104`

### Stage 3: Tokenization

`build_base_tokens()` converts visible bytes into a stream of `ViewTokenWire`:

```rust
pub struct ViewTokenWire {
    pub source_offset: Option<usize>,  // Maps back to source byte
    pub kind: ViewTokenWireKind,       // Text | Newline | Space | Break
    pub style: Option<ViewTokenStyle>, // For injected content
}
```

The `source_offset` field is critical—it enables cursor positioning and determines whether syntax highlighting applies.

**Key file:** `src/ui/split_rendering.rs:581-654`

### Stage 4: Plugin View Transform

Plugins can intercept and transform the token stream before it becomes display lines.

#### Synchronization Model

The view transform system uses a **synchronous, single-threaded frame pipeline** to guarantee per-frame coherence:

```mermaid
sequenceDiagram
    participant R as Renderer
    participant H as Hook System
    participant P as Plugin
    participant Q as Command Queue
    participant S as SplitViewState

    Note over R: Frame N begins
    R->>R: build_base_tokens()
    R->>H: Fire view_transform_request (BLOCKING)
    H->>P: Send buffer_id, viewport, base tokens
    P->>P: Transform tokens
    P->>Q: submitViewTransform() enqueues command
    H-->>R: Hook returns (unblocks)
    R->>Q: Drain command queue (non-blocking)
    Q-->>R: SubmitViewTransform command
    R->>S: view_transform = Some(payload)
    R->>R: Render using SplitViewState.view_transform
    Note over R: Frame N complete (coherent)
```

| Phase | Operation | Sync/Async | Blocks Frame? |
|-------|-----------|-----------|---------------|
| Hook fire | `run_hook("view_transform_request")` | **Sync** | Yes |
| Plugin execution | Plugin transforms tokens | **Sync** | Yes (in-thread) |
| Command enqueue | `submitViewTransform()` | Sync | No (mpsc send) |
| Queue drain | `process_commands()` | Sync | No (try_recv) |
| State update | `view_state.view_transform = payload` | Sync | No |
| Render | Read `SplitViewState.view_transform` | Sync | No |

**Key files:**
- Hook firing: `src/editor/render.rs:135`
- Command queue: `src/ts_runtime.rs:2996-3002`
- Queue drain: `src/editor/render.rs:194-200`
- State update: `src/editor/mod.rs:4209-4226`

#### Frame Coherence Guarantees

The design ensures **frame coherence**—each frame renders with a consistent view of all state:

1. **Hooks block the frame** - `run_hook()` does not return until all plugin callbacks complete
2. **Commands drain immediately** - After hooks return, pending commands are processed before rendering
3. **Single source of truth** - `SplitViewState.view_transform` is updated in-place, then read during render
4. **No async gaps** - No `await` points between state mutation and rendering

```mermaid
flowchart LR
    subgraph Frame["Single Frame (No Async Boundaries)"]
        direction LR
        A[Build tokens] --> B[Fire hook<br/>BLOCKS]
        B --> C[Drain queue]
        C --> D[Apply transforms]
        D --> E[Render]
    end
```

#### Stale Transform Behavior

If a plugin does **not** respond (doesn't call `submitViewTransform`), the renderer uses whatever transform is already in `SplitViewState`:

| Scenario | Behavior |
|----------|----------|
| Plugin responds this frame | New transform used immediately |
| Plugin doesn't respond | Previous frame's transform reused |
| Plugin calls `ClearViewTransform` | Falls back to base tokens |
| No plugin registered | Base tokens used (no transform) |

#### Performance Implications

Because hooks are **blocking**, slow plugins stall the entire frame:

```
Frame timeline (60 FPS target = 16.6ms budget):

Fast plugin:    [tokens 1ms][hook 2ms][drain <1ms][render 5ms] = 8ms ✓
Slow plugin:    [tokens 1ms][hook 50ms][drain <1ms][render 5ms] = 56ms ✗ (dropped frames)
```

**Best practices for plugins:**
- Pre-compute annotation positions during async operations (file load, git commands)
- Store computed data in plugin state
- In `view_transform_request`, only do O(viewport) token manipulation
- Never perform I/O or spawn processes inside the hook

**Hook:** `view_transform_request` (`src/editor/render.rs:113-135`)

**Plugin command:** `PluginCommand::SubmitViewTransform` (`src/plugin_api.rs:259-264`)

**Token semantics:**
- `source_offset: Some(n)` → Source content, syntax highlighting applied
- `source_offset: None` → Injected by plugin, uses `style` field instead

**Use cases:** Git blame annotations, markdown soft breaks, interleaved views, code coverage headers.

### Stage 5: ViewLine Generation

`ViewLineIterator` (`src/ui/view_pipeline.rs`) converts tokens into display lines:

```rust
pub struct ViewLine {
    pub text: String,                       // Tabs expanded to spaces
    pub char_mappings: Vec<Option<usize>>,  // Display char → source byte
    pub char_styles: Vec<Option<ViewTokenStyle>>,
    pub tab_starts: HashSet<usize>,
    pub line_start: LineStart,              // Source | Injected | Wrapped
}
```

**Tab expansion:** `'\t'` → `(8 - (col % 8))` spaces, each mapped to the original tab byte.

**Line classification:** `LineStart::AfterInjectedNewline` distinguishes plugin-injected lines (no line number shown) from source lines.

**Key file:** `src/ui/view_pipeline.rs:72-74`

### Stage 6: Styling Layers

Multiple styling systems are applied per character, in order:

```mermaid
flowchart LR
    subgraph Layers["Styling Priority (low → high)"]
        direction LR
        A[Token style] --> B[ANSI escapes]
        B --> C[Syntax highlighting]
        C --> D[Semantic highlighting]
        D --> E[Selection]
        E --> F[Overlays]
    end
```

| Layer | Source | Applies To |
|-------|--------|-----------|
| Token style | `ViewTokenWire.style` | Injected content (`source_offset: None`) |
| ANSI escapes | `AnsiParser::parse_char()` | Content with escape sequences |
| Syntax | Tree-sitter (`src/highlighter.rs`) | Source content only |
| Semantic | Word occurrence matching | Matching identifiers |
| Selection | Cursor ranges | Selected text |
| Overlays | `OverlayManager` | Diagnostics, search hits |

**Key file:** `src/ui/split_rendering.rs:1005-1720`

### Stage 7: Ratatui Output

Styled characters are accumulated into ratatui `Span`s, grouped into `Line`s:

```rust
// Final structure
Vec<Line> {
    Line {
        spans: Vec<Span> {
            Span { content: String, style: Style }
        }
    }
}

// Handoff to ratatui
frame.render_widget(Clear, render_area);
frame.render_widget(Paragraph::new(lines), render_area);
frame.set_cursor_position((cursor_x, cursor_y));
```

**Key file:** `src/ui/split_rendering.rs:1926-1930`

### Plugin Integration Points

Fresh provides four independent mechanisms for plugins to affect rendering:

```mermaid
flowchart TB
    subgraph Mechanisms["Plugin Rendering Mechanisms"]
        VT[View Transform<br/>Token stream modification]
        VL[Virtual Lines<br/>Header/footer lines]
        VTX[Virtual Text<br/>Inline insertions]
        OV[Overlays<br/>Range decorations]
    end

    subgraph UseCases["Use Cases"]
        VT --> UC1[Interleaved views]
        VT --> UC2[Semantic reordering]

        VL --> UC3[Git blame headers]
        VL --> UC4[Code coverage]
        VL --> UC5[Section separators]

        VTX --> UC6[Type hints]
        VTX --> UC7[Parameter hints]
        VTX --> UC8[Color swatches]

        OV --> UC9[Diagnostics]
        OV --> UC10[Search highlights]
        OV --> UC11[Lint underlines]
    end
```

#### Emacs-like Persistent State Model

Fresh follows an **Emacs-inspired architecture** for plugin rendering where:

1. **Plugins update state in response to events** (buffer changes, file open) - async, fire-and-forget
2. **State is stored persistently** with marker-based position tracking
3. **Render loop reads state synchronously** from memory - no async waiting

This ensures **frame coherence**: each frame renders with a consistent snapshot of all plugin state.

```mermaid
sequenceDiagram
    participant B as Buffer
    participant P as Plugin
    participant S as VirtualTextManager
    participant R as Renderer

    Note over B,R: Plugin updates state (async)
    B->>P: buffer_changed event
    P->>P: Compute data (can take time)
    P->>S: AddVirtualLine commands
    S->>S: Store with markers

    Note over B,R: Render reads state (sync)
    R->>S: query_lines_in_range()
    S-->>R: Virtual lines (from memory)
    R->>R: Inject into ViewLines
    R->>R: Render frame
```

#### View Transform (`PluginCommand::SubmitViewTransform`)

Modifies the entire token stream. For advanced use cases requiring complete restructuring.

```rust
PluginCommand::SubmitViewTransform {
    buffer_id,
    split_id,
    payload: ViewTransformPayload {
        range: Range<usize>,
        tokens: Vec<ViewTokenWire>,
        layout_hints: Option<LayoutHints>,
    },
}
```

**Note:** View transforms use a per-frame hook which has sync/async timing considerations. For simpler annotation use cases, prefer Virtual Lines.

**Key file:** `src/plugin_api.rs:122-130`

#### Virtual Lines (`PluginCommand::AddVirtualLine`)

Injects full display lines above or below source lines. **Recommended for git blame, code coverage, section headers.**

```rust
PluginCommand::AddVirtualLine {
    buffer_id,
    position: usize,           // Anchor byte position
    text: String,              // Full line content
    color: (u8, u8, u8),       // RGB color
    above: bool,               // true = above, false = below
    namespace: String,         // For bulk removal
    priority: i32,             // Ordering at same position
}

// Clear all virtual lines in a namespace (before updating)
PluginCommand::ClearVirtualTextNamespace {
    buffer_id,
    namespace: String,
}
```

**Characteristics:**
- Lines do NOT show line numbers in the gutter
- Positions auto-adjust on buffer edits (marker-based)
- Namespaced for efficient bulk removal
- Read synchronously during render (no frame lag)

**Example: Git blame plugin**
```typescript
fresh.hooks.on('buffer_changed', async (bufferId) => {
  const blame = await computeGitBlame(bufferId);

  // Clear old headers
  fresh.clearVirtualTextNamespace(bufferId, 'git-blame');

  // Add new headers (persistent until next change)
  for (const chunk of blame.chunks) {
    fresh.addVirtualLine({
      bufferId,
      position: chunk.startByte,
      text: `── ${chunk.commit} (${chunk.author}) ──`,
      color: [128, 128, 128],
      above: true,
      namespace: 'git-blame',
      priority: 0,
    });
  }
});
```

**Key file:** `src/virtual_text.rs`

#### Virtual Text - Inline (`PluginCommand::AddVirtualText`)

Inserts styled text inline (before/after a character). For type hints, parameter hints.

```rust
PluginCommand::AddVirtualText {
    buffer_id,
    virtual_text_id: String,
    position: usize,
    text: String,
    color: (u8, u8, u8),
    before: bool,  // true = before char, false = after
}
```

**Key file:** `src/virtual_text.rs`

#### Overlays (`PluginCommand::AddOverlay`)

Applies decorations to byte ranges. Stored in `BTreeMap<usize, Vec<Overlay>>` for O(log n) lookup.

```rust
PluginCommand::AddOverlay {
    buffer_id,
    namespace: Option<OverlayNamespace>,
    range: Range<usize>,
    color: (u8, u8, u8),
    underline: bool,
    bold: bool,
    italic: bool,
} -> OverlayHandle
```

**Performance optimizations:**
1. **Line-indexed storage:** Overlays keyed by starting line for fast viewport filtering
2. **Render-time cache:** Visible overlays cached per frame
3. **Diagnostic hash check:** LSP diagnostics skip update if unchanged

**Key file:** `src/overlay.rs`

### Per-Split View State

Each split maintains independent view state, enabling different presentations of the same buffer:

```rust
pub struct SplitViewState {
    pub view_transform: Option<ViewTransformPayload>,
    pub compose_width: Option<u16>,
    pub compose_column_guides: Option<Vec<u16>>,
    pub layout: Option<Layout>,
    pub layout_dirty: bool,
}
```

**Example:** Split A shows normal view, Split B shows git blame—same buffer, different transforms.

**Key file:** `src/split.rs:60-105`

### Render Phase Hook Sequence

```mermaid
sequenceDiagram
    participant E as Editor
    participant B as Buffer
    participant H as Hooks
    participant P as Plugins
    participant R as SplitRenderer

    E->>B: prepare_for_render() (lazy load chunks)

    loop For each visible buffer
        E->>H: Fire render_start
        E->>B: build_base_tokens()
        E->>H: Fire view_transform_request
        H->>P: Plugin transforms tokens
        P->>E: SubmitViewTransform
        E->>H: Fire lines_changed (new visible lines)
        H->>P: Plugin adds overlays
    end

    E->>E: Process queued PluginCommands
    E->>R: render_buffer_in_split()
    R->>R: Apply view_transform if present
    R->>R: Build ViewLines, apply styling
    R->>R: Render to ratatui
```

**Key file:** `src/editor/render.rs:97-201`

### Performance Characteristics

| Operation | Complexity | Notes |
|-----------|-----------|-------|
| File load (small) | O(n) | Full line indexing |
| File load (large) | O(1) | Lazy, viewport-only |
| Viewport scroll | O(log n) | Binary search for line boundary |
| Syntax highlighting | O(viewport) | Tree-sitter parses visible lines only |
| Overlay lookup | O(log n + k) | n = total overlays, k = visible |
| Render frame | O(height) | Independent of file size |

## LSP Integration

Fresh has a deep and robust integration with the Language Server Protocol (LSP), providing features like code completion, diagnostics, and go-to-definition.

The LSP integration is built on a multi-threaded architecture that ensures the editor's UI is never blocked by the language server.

*   **`LspManager`:** A central coordinator that manages multiple language servers (one for each language).
*   **`LspHandle`:** A handle to a specific language server, providing a non-blocking API for sending commands and notifications.
*   **`AsyncBridge`:** An `mpsc` channel that bridges the asynchronous world of the LSP tasks with the synchronous world of the editor's main event loop.

This architecture allows Fresh to communicate with language servers in a highly efficient and non-blocking way, providing a smooth and responsive user experience.

## Annotated Views

Fresh supports **Annotated Views**, a powerful pattern for displaying file content with injected annotations (such as headers, metadata lines, or visual separators) while preserving core editor features like line numbers and syntax highlighting. This architecture enables features like git blame, code coverage overlays, and inline documentation.

### The Problem

Consider displaying git blame information: you want to show the file content with header lines above each block indicating the commit, author, and date. A naive approach creates several problems:

1. **Line Number Mismatch:** If headers are part of the buffer content, line numbers include them, making it impossible to show "line 42" next to the actual line 42 of the source file.

2. **Syntax Highlighting Loss:** If the buffer contains mixed content (headers + code), tree-sitter cannot parse it correctly, and syntax highlighting breaks.

3. **Historical Content:** For features like "blame at parent commit," you need to display historical file versions that differ from the current file.

### The Solution: View Transforms + Virtual Buffers

Fresh solves this with a hybrid architecture combining two mechanisms:

```
┌─────────────────────────────────────────────────────────────────┐
│                    ANNOTATED VIEW ARCHITECTURE                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │              Virtual Buffer (Content Layer)              │  │
│   │  • Contains actual file content (current or historical) │  │
│   │  • Language detected from buffer name extension         │  │
│   │  • Syntax highlighting via tree-sitter                  │  │
│   │  • Text properties store annotation metadata            │  │
│   └─────────────────────────────────────────────────────────┘  │
│                              │                                  │
│                              ▼                                  │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │             View Transform (Presentation Layer)          │  │
│   │  • Injects annotation headers between content blocks    │  │
│   │  • Headers: source_offset = None (no line number)       │  │
│   │  • Content: source_offset = Some(byte) (has line num)   │  │
│   │  • Styling via ViewTokenWire.style field                │  │
│   └─────────────────────────────────────────────────────────┘  │
│                              │                                  │
│                              ▼                                  │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │                   Rendered Output                        │  │
│   │     ── abc123 (Alice, 2 days ago) "Fix bug" ──          │  │
│   │  42 │ fn main() {                                       │  │
│   │  43 │     println!("Hello");                            │  │
│   │     ── def456 (Bob, 1 week ago) "Add feature" ──        │  │
│   │  44 │     do_something();                               │  │
│   │  45 │ }                                                 │  │
│   └─────────────────────────────────────────────────────────┘  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
```

### Key Components

#### 1. Virtual Buffer with Content

The first layer is a virtual buffer containing the actual file content. Key properties:
- Buffer name includes file extension (e.g., `*blame:main.rs*`) for automatic language detection
- Content is pure source code, enabling correct tree-sitter parsing
- Text properties can store metadata without affecting content

#### 2. ViewTokenWire with Source Mapping

The `ViewTokenWire` structure (in `src/plugin_api.rs`) enables precise control over line numbering via its `source_offset` field:

- **`Some(byte_position)`:** Token maps to source content. The renderer includes it in line number calculation, applies syntax highlighting, and enables cursor positioning.
- **`None`:** Token is injected annotation. The renderer skips line number increment (shows blank in gutter), applies the `style` field if present, and does not participate in source-based features.

#### 3. View Transform Hook

Plugins register for the `view_transform_request` hook, called each render frame. The plugin receives the current viewport's tokens and can inject headers with `source_offset: None`, passing through content tokens unchanged to preserve line numbers and highlighting.

### How Line Numbers Work

The renderer in `split_rendering.rs` checks if the previous character had no source mapping. Lines starting after a `source_offset: None` newline show blank in the line number gutter. Lines starting after a `source_offset: Some(_)` newline increment and display the line number.

### How Syntax Highlighting Works

Syntax highlighting is applied based on `source_offset`. Content tokens with `Some(byte)` are looked up in highlight spans and colored. Annotation tokens with `None` skip highlight lookup and use the `style` field instead.

### Use Cases

**Git Blame:** Virtual buffer contains historical file content from `git show commit:path`. Annotations are commit headers above each blame block. Line numbers match historical file lines.

**Code Coverage:** Buffer contains current file content. Annotations are coverage percentage headers above functions. Line numbers match current file.

**Inline Documentation:** Buffer contains source code. Annotations are doc comments rendered as styled blocks. Line numbers only appear for code lines.

### Performance Considerations

1. **Viewport-Only Processing:** View transforms only process the visible viewport, not the entire file. For a 100K line file, only ~50 lines are transformed per frame.

2. **Efficient Block Lookup:** Annotation metadata should be stored in a sorted structure enabling O(log n) lookup of blocks overlapping the viewport.

3. **Frame-Rate Transform:** The `view_transform_request` hook is called every frame. Plugins must respond quickly. Pre-compute annotation positions; don't run git commands during the hook.

4. **Caching:** View transforms are cached per-split and reused until explicitly cleared or the viewport changes significantly.

### Implementation Checklist

To implement an annotated view feature:

1. **Define annotation structure:** What metadata accompanies each block? (commit info, coverage data, etc.)

2. **Create content buffer:** Use `createVirtualBuffer` with appropriate name for language detection

3. **Store block positions:** Track byte ranges for each annotation block in the content

4. **Implement view transform hook:** Inject headers with `source_offset: None` and `style`, pass through content tokens unchanged

5. **Handle navigation:** Map cursor positions in the view back to logical positions in your annotation model

6. **Support refresh:** When annotations change (e.g., blame at different commit), update buffer content and block positions; view transform auto-updates on next render

## Buffer vs View: State Separation

Fresh cleanly separates **Buffer state** (the document) from **View state** (how it's displayed). This enables multiple independent views of the same buffer, each with its own scroll position, cursor, and view transform.

### Why Separate?

Consider two splits showing the same file:
- Split A: Normal view, scrolled to line 100, cursor at line 105
- Split B: Git blame view, scrolled to line 1, cursor at line 10

Both views share the same underlying text, but have completely independent:
- Scroll positions (viewport)
- Cursor positions
- View transforms (blame vs normal)
- Computed layouts

### The State Model

```
┌─────────────────────────────────────────────────────────────┐
│                    BUFFER STATE (shared)                    │
│  EditorState per buffer_id                                  │
│  ┌────────────────────────────────────────────────────────┐ │
│  │ • buffer: PieceTree (actual text content)              │ │
│  │ • undo/redo history                                    │ │
│  │ • syntax highlighter (tokens derived from content)     │ │
│  │ • overlays (content-anchored decorations)              │ │
│  │ • markers (bookmarks, breakpoints)                     │ │
│  └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
            ┌─────────────────┼─────────────────┐
            ▼                                   ▼
┌───────────────────────────┐     ┌───────────────────────────┐
│   VIEW STATE (Split A)    │     │   VIEW STATE (Split B)    │
│   SplitViewState          │     │   SplitViewState          │
│ ┌───────────────────────┐ │     │ ┌───────────────────────┐ │
│ │ • viewport (scroll)   │ │     │ │ • viewport (scroll)   │ │
│ │ • cursors             │ │     │ │ • cursors             │ │
│ │ • view_transform      │ │     │ │ • view_transform      │ │
│ │ • view_mode           │ │     │ │ • view_mode           │ │
│ │ • layout (computed)   │ │     │ │ • layout (computed)   │ │
│ └───────────────────────┘ │     │ └───────────────────────┘ │
└───────────────────────────┘     └───────────────────────────┘
```

### Event Routing

Events are routed based on whether they affect the buffer or the view:

**Buffer Events** → Applied to shared `EditorState`:
- `Insert`, `Delete` (text modifications)
- `AddOverlay`, `RemoveOverlay` (content decorations)
- Undo/Redo

**View Events** → Applied to `SplitViewState` for active split:
- `Scroll` (viewport movement)
- `MoveCursor` (cursor navigation)
- `SetViewMode` (normal/blame/etc)

When a buffer event modifies content, ALL views of that buffer must:
1. Adjust their cursor positions (if affected by the edit)
2. Mark their layout as dirty (needs rebuild)

### Syntax Highlighting: Buffer or View?

Base syntax highlighting is **buffer state** because:
- Tokens are derived from content (same content = same tokens)
- Expensive to compute, wasteful to duplicate per-view
- Most editors share highlighting across views

However, **style application** can be view-specific via view transforms. A git blame view transform can restyle tokens (dim old code, highlight recent changes) without affecting other views.

## The Layout Layer

The Layout is **View state**, not Buffer state. Each view (split) has its own Layout computed from its view_transform. This enables different views of the same buffer to have different layouts (e.g., blame view vs normal view).

### The Two Layers

```
┌─────────────────────────────────────────────────────────────┐
│              BUFFER LAYER (EditorState - shared)            │
│  • Buffer: source bytes (PieceTree)                         │
│  • Syntax tokens (shared across views)                      │
│  • Overlays, markers (content-anchored)                     │
└─────────────────────────────────────────────────────────────┘
          ┌───────────────────┼───────────────────┐
          ▼                                       ▼
┌─────────────────────────────┐     ┌─────────────────────────────┐
│  VIEW LAYER (Split A)       │     │  VIEW LAYER (Split B)       │
│  SplitViewState             │     │  SplitViewState             │
│ ┌─────────────────────────┐ │     │ ┌─────────────────────────┐ │
│ │ view_transform (plugin) │ │     │ │ view_transform (plugin) │ │
│ │         │               │ │     │ │         │               │ │
│ │         ▼               │ │     │ │         ▼               │ │
│ │ layout (ViewLines)      │ │     │ │ layout (ViewLines)      │ │
│ │         │               │ │     │ │         │               │ │
│ │         ▼               │ │     │ │         ▼               │ │
│ │ viewport (view line idx)│ │     │ │ viewport (view line idx)│ │
│ │ cursors (source bytes)  │ │     │ │ cursors (source bytes)  │ │
│ └─────────────────────────┘ │     │ └─────────────────────────┘ │
└─────────────────────────────┘     └─────────────────────────────┘
                │                                 │
                ▼                                 ▼
┌─────────────────────────────┐     ┌─────────────────────────────┐
│         RENDERING           │     │         RENDERING           │
│  ViewLines[top..top+height] │     │  ViewLines[top..top+height] │
└─────────────────────────────┘     └─────────────────────────────┘
```

### Why Two Layers?

When view transforms inject content (like git blame headers), the display has **more lines** than the source buffer:

```
Source Buffer (5 lines):     Display Layout (7 lines):
─────────────────────        ─────────────────────────
Line 1                       ── Header (injected) ──
Line 2                       Line 1
Line 3                       Line 2
Line 4                       ── Header (injected) ──
Line 5                       Line 3
                             Line 4
                             Line 5
```

If the viewport operates on source lines, scroll limits are wrong (can't scroll to show all 7 display lines). If cursor movement uses source lines, pressing ↓ skips over injected headers.

The solution: **viewport and visual navigation operate on the Layout Layer**.

### Key Data Structures

See source files for current implementations:

- **ViewLine** (`src/ui/view_pipeline.rs`): A single display line with `char_mappings` mapping each character to source byte (None = injected), plus styles and tab expansion info.

- **Layout** (`src/ui/view_pipeline.rs`): Collection of ViewLines for the viewport region, with `byte_to_line` index for fast source byte → view line lookup.

- **SplitViewState** (`src/split.rs`): Per-split state including cursors, viewport, view_transform, layout, and layout_dirty flag.

- **Viewport** (`src/viewport.rs`): Scroll position (currently `top_byte` in source bytes), dimensions, and scroll offset settings.

- **Cursor** (`src/cursor.rs`): Position in source bytes with optional selection anchor.

### Layout Building Strategy

Following VSCode's ViewModel pattern, the Layout is built **lazily but is never optional**. Every operation that needs Layout calls `ensure_layout()` first (see `src/split.rs`). There is no "fallback to buffer-based scrolling" - Layout always exists when needed.

**Layout becomes dirty when:**
- Buffer content changes (Insert/Delete events)
- View transform changes (plugin sends new tokens)
- Scroll would move past current layout's source_range

### Viewport Stability

When the layout changes (edit, view transform update), view line indices shift. To maintain stable scroll position:

1. **Store anchor_byte**: The source byte at the top of the viewport
2. **On layout rebuild**: Find anchor_byte in new layout → new top_view_line
3. **Clamp if needed**: If anchor_byte no longer exists, clamp to valid range

### Scrolling Beyond the Current Layout

The Layout is built from viewport-scoped tokens (for performance). When scrolling would move past the current layout:

1. **Request new tokens** from the view transform for the target range
2. **Rebuild layout** to cover the new viewport
3. **Complete the scroll** using the new layout

The estimate for the target byte doesn't need to be perfect - the view transform will give correct tokens for whatever range is requested.

### Tracking Total View Lines

To know scroll limits without building the entire layout, we track `total_view_lines` and `total_injected_lines` in the Layout struct. The plugin knows how many headers it injects and can report this in the view transform response via `total_injected_lines` in layout hints.

## Viewport Ownership and Scrolling

This section documents the authoritative architecture for viewport state and scrolling. The key principle: **SplitViewState.viewport is the single source of truth for scroll position**.

### The Problem with Duplicate State

Early implementations had viewport state in two places: `EditorState` (per buffer) and `SplitViewState` (per split). This caused several bugs:

1. **Sync loops**: Changes to one viewport would sync to the other, then sync back, causing flickering
2. **Stale state**: After cursor movement, scroll position would reset because sync copied old values
3. **Wrong dimensions**: Editor.render() used EditorState.viewport dimensions before split_rendering resized it, causing incorrect scroll calculations

### The Solution: Single Source of Truth

**SplitViewState.viewport is authoritative.** EditorState should NOT have a viewport (or at most, temporary dimensions for PageDown/PageUp before layout is available).

```
EditorState (per buffer):
  └── buffer: PieceTree
  └── cursors: Cursors (deprecated - use SplitViewState.cursors)
  └── (NO viewport)

SplitViewState (per split):
  └── viewport: Viewport      // THE source of truth
  └── cursors: Cursors        // per-view cursors
  └── view_transform: Option<...>
  └── layout: Option<Layout>
```

### Scrolling Flow

The correct flow for cursor-triggered scrolling:

```
1. Input Event (↓ key)
      2. Cursor moves in SplitViewState.cursors
      3. Render phase begins
      ├─► Build view_lines from tokens (with view_transform if present)
      ├─► Call ensure_cursor_visible(view_lines, cursor, viewport)
   │   • Find cursor's position in view_lines (accounts for virtual lines)
   │   • If cursor outside [top, top+height], adjust top_view_line
   │   • This is Layout-aware scrolling
      ├─► If scrolled, rebuild view_lines for new viewport position
      └─► Render view_lines[top..top+height]
```

**Key insight**: Scrolling must happen DURING render, when view_lines (with virtual lines) are available. Scrolling BEFORE render doesn't know about injected virtual lines.

### Why Layout-Aware Scrolling Matters

Consider git blame with 120 virtual header lines injected at the top:

```
Source buffer:          Display (with view transform):
Line 1                  ── Header (virtual) ──
Line 2                  ── Header (virtual) ──
Line 3                  ... (120 headers) ...
                        Line 1  ← cursor here
                        Line 2
                        Line 3
```

Source-based scrolling thinks "cursor is at byte 0, which is source line 1, keep top at line 1". But the display has 120 virtual lines before that! The cursor appears off-screen.

Layout-aware scrolling looks at view_lines: "cursor maps to view_line 121, viewport shows lines 0-30, need to scroll to line 121".

### Migration Tasks

To reach the target architecture:

1. **Move cursors to SplitViewState**: Each view has independent cursor positions
2. **Remove EditorState.viewport**: Or keep only for dimension hints
3. **Remove sync functions**: No more bidirectional state syncing
4. **Single scrolling function**: Only Layout-aware `ensure_cursor_visible`
5. **Scroll during render**: After building view_lines, before rendering

### Temporary Workarounds

Until full migration, these workarounds maintain correctness:

1. `sync_viewport_from_split_view_state` only syncs DIMENSIONS, not scroll position
2. `ensure_visible_in_layout` called in render phase with actual view_lines
3. Editor.render() does NOT call sync_with_cursor (let split_rendering handle it)

## Session Persistence

Fresh automatically saves and restores editor state per working directory. When you close the editor and reopen in the same directory, your workspace is restored.

### What Gets Saved

- **Split layout**: Window split configuration (horizontal/vertical splits, ratios)
- **Open files**: All open files in each split, with active tab
- **Cursor positions**: Per-file cursor position and selection state
- **Scroll positions**: Per-file scroll position (as byte offset)
- **File explorer state**: Visibility, width, expanded directories
- **Search/replace history**: Per-project search terms
- **Search options**: Case sensitivity, regex, whole word settings
- **Bookmarks**: Global bookmarks with file paths and positions
- **Config overrides**: Per-project toggle overrides (line wrap, line numbers, etc.)

### Storage

Sessions are stored in `~/.local/share/fresh/sessions/` with filenames derived from the working directory path:

```
~/.local/share/fresh/sessions/
├── home_user_my%20project.json    # Encoded path as filename
└── ...
```

Path encoding: `/` → `_`, spaces → `%20` (URL encoding for special chars).

### Implementation

**Key files:**
- `src/session.rs` - Session types and serialization
- `src/app/session.rs` - Save/restore integration

**Session lifecycle:**
1. **Load on startup**: `SessionManager::load()` checks for existing session
2. **Apply to editor**: `Session::apply_to_editor()` opens files, restores splits, cursors
3. **Save on exit**: `SessionManager::save()` captures current state
4. **Incremental saves**: Dirty flag + debounce saves periodically (crash resistance)

### CLI Flags

- `--no-session`: Don't restore previous session
- `--no-save-session`: Don't save session on exit

### Edge Cases

| Scenario | Behavior |
|----------|----------|
| File deleted | Skipped, warning logged |
| File moved | Skipped (path-based lookup) |
| Corrupt session | Logged, start fresh |
| Future version | Error with message |
| Virtual buffers | Not persisted (file-backed only) |

## File Open Dialog

Fresh includes a built-in file browser for the Open File command (`Ctrl+O`). This works without plugins and provides a rich file browsing experience.

### Features

- **Column sorting**: Click headers to sort by name, size, modified date, or type
- **Quick navigation**: Shortcuts for parent (`..`), root (`/`), and home (`~`)
- **Fuzzy filtering**: Type to filter; non-matching entries shown grayed at bottom
- **Metadata display**: File size (human-readable) and modification date
- **Mouse support**: Click to select, double-click to open, drag scrollbar

### Architecture

**Key files:**
- `src/app/file_open.rs` - `FileOpenState`, sorting, filtering logic
- `src/view/ui/file_browser.rs` - `FileBrowserRenderer`

**State management:**
```rust
pub struct FileOpenState {
    pub current_dir: PathBuf,
    pub entries: Vec<FileOpenEntry>,
    pub sort_mode: SortMode,      // Name, Size, Modified, Type
    pub sort_ascending: bool,
    pub selected_index: usize,
    pub filter: String,           // From prompt input
}
```

**Rendering**: The file browser popup renders above the prompt when `PromptType::OpenFile` is active. Directories load asynchronously via `FsManager`.

## Syntax Highlighting

Fresh supports two syntax highlighting backends with automatic fallback:

1. **Tree-sitter** (primary): Fast, accurate, incremental parsing. Used for built-in languages (Rust, JavaScript, TypeScript, Python, etc.)
2. **TextMate grammars** (fallback): Uses `syntect` crate for broad language coverage. Supports VSCode-compatible grammars.

### Highlighter Priority

```
1. Tree-sitter (if built-in support exists)
2. User TextMate grammar (from ~/.config/fresh/grammars/)
3. Built-in syntect grammars (100+ languages)
4. No highlighting
```

### User Grammar Installation

Add VSCode-compatible grammars to `~/.config/fresh/grammars/`:

```
~/.config/fresh/grammars/
  my-language/
    package.json           # VSCode extension manifest
    syntaxes/
      language.tmLanguage.json
```

### Implementation

**Key files:**
- `src/primitives/highlighter.rs` - Tree-sitter highlighter
- `src/primitives/textmate_highlighter.rs` - TextMate grammar highlighter
- `src/primitives/highlight_engine.rs` - Unified `HighlightEngine` abstraction
- `src/primitives/grammar_registry.rs` - Grammar discovery and loading

**Configuration** (in `config.json`):
```json
{
  "languages": {
    "haskell": {
      "extensions": ["hs"],
      "highlighter": "textmate"
    }
  }
}
```

Options: `auto` (default), `tree-sitter`, `textmate`

### Performance

Both backends use viewport-only parsing:
- Parse only visible lines + 1KB context
- Cache parsed results (category-based for theme independence)
- Invalidate cache on edits