Skip to main content

mq_edit/document/
cursor.rs

1use mq_markdown::{Markdown, Node};
2
3/// Cursor position in the editor (0-indexed)
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct Cursor {
6    pub line: usize,
7    pub column: usize,
8    /// Desired column for vertical movement (sticky column)
9    pub desired_column: usize,
10}
11
12impl Cursor {
13    pub fn new() -> Self {
14        Self {
15            line: 0,
16            column: 0,
17            desired_column: 0,
18        }
19    }
20
21    pub fn with_position(line: usize, column: usize) -> Self {
22        Self {
23            line,
24            column,
25            desired_column: column,
26        }
27    }
28
29    /// Update desired column when moving horizontally
30    pub fn update_desired_column(&mut self) {
31        self.desired_column = self.column;
32    }
33}
34
35impl Default for Cursor {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41/// Cursor movement directions
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum CursorMovement {
44    Up,
45    Down,
46    Left,
47    Right,
48    StartOfLine,
49    EndOfLine,
50    PageUp,
51    PageDown,
52    StartOfDocument,
53    EndOfDocument,
54}
55
56/// Maps visual line numbers to AST nodes
57#[derive(Debug, Clone)]
58pub struct LineMap {
59    entries: Vec<LineEntry>,
60}
61
62#[derive(Debug, Clone)]
63pub struct LineEntry {
64    /// Index in the Markdown.nodes array
65    pub node_index: usize,
66    /// Line offset within a multi-line node
67    pub node_line_offset: usize,
68    /// Visual line number in the editor (0-indexed)
69    pub visual_line: usize,
70    /// Starting character offset from the beginning of the node
71    pub char_offset: usize,
72}
73
74impl LineMap {
75    pub fn new() -> Self {
76        Self {
77            entries: Vec::new(),
78        }
79    }
80
81    /// Build LineMap from Markdown AST
82    pub fn from_markdown(markdown: &Markdown) -> Self {
83        let mut map = Self::new();
84        let mut current_line = 0;
85
86        for (node_idx, node) in markdown.nodes.iter().enumerate() {
87            Self::process_node(&mut map, node, node_idx, &mut current_line);
88        }
89
90        map
91    }
92
93    fn process_node(map: &mut LineMap, node: &Node, node_idx: usize, current_line: &mut usize) {
94        if let Some(pos) = node.position() {
95            let start_line = pos.start.line.saturating_sub(1); // Convert to 0-indexed
96            let end_line = pos.end.line.saturating_sub(1);
97
98            for line_offset in 0..=(end_line - start_line) {
99                map.entries.push(LineEntry {
100                    node_index: node_idx,
101                    node_line_offset: line_offset,
102                    visual_line: *current_line,
103                    char_offset: 0,
104                });
105                *current_line += 1;
106            }
107        } else {
108            // Node without position, allocate one line
109            map.entries.push(LineEntry {
110                node_index: node_idx,
111                node_line_offset: 0,
112                visual_line: *current_line,
113                char_offset: 0,
114            });
115            *current_line += 1;
116        }
117    }
118
119    /// Get the node index at a given visual line
120    pub fn get_entry(&self, line: usize) -> Option<&LineEntry> {
121        self.entries.get(line)
122    }
123
124    /// Get node at a given visual line
125    pub fn get_node_at_line<'a>(&self, markdown: &'a Markdown, line: usize) -> Option<&'a Node> {
126        let entry = self.get_entry(line)?;
127        markdown.nodes.get(entry.node_index)
128    }
129
130    /// Total number of visual lines
131    pub fn line_count(&self) -> usize {
132        self.entries.len()
133    }
134
135    /// Invalidate entries from a given line onwards (for incremental updates)
136    pub fn invalidate_from(&mut self, from_line: usize) {
137        self.entries.truncate(from_line);
138    }
139
140    /// Rebuild from a specific line in the Markdown AST
141    pub fn rebuild_from(&mut self, markdown: &Markdown, from_node_idx: usize) {
142        let mut current_line = if let Some(entry) = self.entries.last() {
143            entry.visual_line + 1
144        } else {
145            0
146        };
147
148        for (idx, node) in markdown.nodes.iter().enumerate().skip(from_node_idx) {
149            Self::process_node(self, node, idx, &mut current_line);
150        }
151    }
152}
153
154impl Default for LineMap {
155    fn default() -> Self {
156        Self::new()
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_cursor_creation() {
166        let cursor = Cursor::new();
167        assert_eq!(cursor.line, 0);
168        assert_eq!(cursor.column, 0);
169        assert_eq!(cursor.desired_column, 0);
170    }
171
172    #[test]
173    fn test_cursor_with_position() {
174        let cursor = Cursor::with_position(5, 10);
175        assert_eq!(cursor.line, 5);
176        assert_eq!(cursor.column, 10);
177        assert_eq!(cursor.desired_column, 10);
178    }
179
180    #[test]
181    fn test_linemap_creation() {
182        let linemap = LineMap::new();
183        assert_eq!(linemap.line_count(), 0);
184    }
185}