Skip to main content

fresh/view/
stream.rs

1//! View stream representation for rendering
2//!
3//! This module defines a lightweight, source-anchored view stream that can be
4//! transformed (e.g., by plugins) before layout. It keeps mappings back to
5//! source offsets for hit-testing and cursor positioning.
6
7use crate::state::EditorState;
8use crate::view::overlay::OverlayFace;
9use crate::view::virtual_text::VirtualTextPosition;
10use ratatui::style::Style;
11
12/// Kind of token in the view stream
13#[derive(Debug, Clone, PartialEq)]
14pub enum ViewTokenKind {
15    /// Plain text slice
16    Text(String),
17    /// Newline in the source
18    Newline,
19    /// Whitespace (commonly used when transforming newlines to spaces)
20    Space,
21    /// Virtual text (injected, not in source)
22    VirtualText {
23        text: String,
24        style: Style,
25        position: VirtualTextPosition,
26        priority: i32,
27    },
28    /// Style span start/end (source-anchored)
29    StyleStart(Style),
30    StyleEnd,
31    /// Overlay span (for decorations)
32    Overlay(OverlayFace),
33}
34
35/// A view token with source mapping
36#[derive(Debug, Clone, PartialEq)]
37pub struct ViewToken {
38    /// Byte offset in source for this token, if any
39    pub source_offset: Option<usize>,
40    /// The token kind
41    pub kind: ViewTokenKind,
42}
43
44/// A view stream for a viewport
45#[derive(Debug, Clone, Default)]
46pub struct ViewStream {
47    pub tokens: Vec<ViewToken>,
48    /// Mapping from view token index to source offset (if present)
49    pub source_map: Vec<Option<usize>>,
50}
51
52impl ViewStream {
53    pub fn new() -> Self {
54        Self {
55            tokens: Vec::new(),
56            source_map: Vec::new(),
57        }
58    }
59
60    pub fn push(&mut self, token: ViewToken) {
61        self.source_map.push(token.source_offset);
62        self.tokens.push(token);
63    }
64}
65
66/// Build a base view stream for a viewport range (byte offsets)
67/// This stream contains plain text and newline tokens only; overlays and virtual
68/// text are not included here (they remain applied during rendering).
69pub fn build_base_stream(state: &mut EditorState, start: usize, end: usize) -> ViewStream {
70    let mut stream = ViewStream::new();
71
72    if start >= end {
73        return stream;
74    }
75
76    let text = state.get_text_range(start, end);
77
78    let mut current_offset = start;
79    let mut buffer = String::new();
80
81    for ch in text.chars() {
82        if ch == '\n' {
83            if !buffer.is_empty() {
84                stream.push(ViewToken {
85                    source_offset: Some(current_offset - buffer.len()),
86                    kind: ViewTokenKind::Text(buffer.clone()),
87                });
88                buffer.clear();
89            }
90            stream.push(ViewToken {
91                source_offset: Some(current_offset),
92                kind: ViewTokenKind::Newline,
93            });
94            current_offset += 1;
95        } else {
96            buffer.push(ch);
97            current_offset += ch.len_utf8();
98        }
99    }
100
101    if !buffer.is_empty() {
102        stream.push(ViewToken {
103            source_offset: Some(current_offset - buffer.len()),
104            kind: ViewTokenKind::Text(buffer),
105        });
106    }
107
108    stream
109}