Skip to main content

ane/data/
state.rs

1use std::path::{Path, PathBuf};
2use std::sync::{Arc, Mutex};
3
4use anyhow::Result;
5
6use super::buffer::Buffer;
7use super::file_tree::{FileEntry, FileTree};
8use super::lsp::types::LspSharedState;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Mode {
12    Edit,
13    Chord,
14}
15
16#[derive(Debug)]
17pub struct EditorState {
18    pub buffers: Vec<Buffer>,
19    pub active_buffer: usize,
20    pub file_tree: Option<FileTree>,
21    pub cursor_line: usize,
22    pub cursor_col: usize,
23    pub scroll_offset: usize,
24    pub mode: Mode,
25    pub should_quit: bool,
26    pub status_msg: String,
27    pub tree_selected: usize,
28    pub focus_tree: bool,
29    pub chord_input: String,
30    pub show_exit_modal: bool,
31    pub opened_path: PathBuf,
32    pub chord_cursor_col: usize,
33    pub chord_error: bool,
34    pub chord_running: bool,
35    pub chord_history: Vec<String>,
36    pub chord_history_index: Option<usize>,
37    pub pre_tree_mode: Mode,
38    pub pending_open_path: Option<PathBuf>,
39    pub tree_view: Vec<FileEntry>,
40    pub lsp_state: Arc<Mutex<LspSharedState>>,
41}
42
43impl EditorState {
44    pub fn for_file(path: &Path) -> Result<Self> {
45        let buf = if path.exists() {
46            Buffer::from_file(path)?
47        } else {
48            Buffer::empty(path)
49        };
50        Ok(Self {
51            buffers: vec![buf],
52            active_buffer: 0,
53            file_tree: None,
54            cursor_line: 0,
55            cursor_col: 0,
56            scroll_offset: 0,
57            mode: Mode::Chord,
58            should_quit: false,
59            status_msg: String::new(),
60            tree_selected: 0,
61            focus_tree: false,
62            chord_input: String::new(),
63            show_exit_modal: false,
64            opened_path: path.to_path_buf(),
65            chord_cursor_col: 0,
66            chord_error: false,
67            chord_running: false,
68            chord_history: Vec::new(),
69            chord_history_index: None,
70            pre_tree_mode: Mode::Chord,
71            pending_open_path: None,
72            tree_view: Vec::new(),
73            lsp_state: Arc::new(Mutex::new(LspSharedState::default())),
74        })
75    }
76
77    pub fn for_directory(path: &Path) -> Result<Self> {
78        let tree = FileTree::from_dir(path)?;
79        let tree_view: Vec<FileEntry> = tree
80            .entries
81            .iter()
82            .filter(|e| e.depth == 0)
83            .cloned()
84            .collect();
85        Ok(Self {
86            buffers: Vec::new(),
87            active_buffer: 0,
88            file_tree: Some(tree),
89            cursor_line: 0,
90            cursor_col: 0,
91            scroll_offset: 0,
92            mode: Mode::Chord,
93            should_quit: false,
94            status_msg: String::new(),
95            tree_selected: 0,
96            focus_tree: true,
97            chord_input: String::new(),
98            show_exit_modal: false,
99            opened_path: path.to_path_buf(),
100            chord_cursor_col: 0,
101            chord_error: false,
102            chord_running: false,
103            chord_history: Vec::new(),
104            chord_history_index: None,
105            pre_tree_mode: Mode::Chord,
106            pending_open_path: None,
107            tree_view,
108            lsp_state: Arc::new(Mutex::new(LspSharedState::default())),
109        })
110    }
111
112    pub fn current_buffer(&self) -> Option<&Buffer> {
113        self.buffers.get(self.active_buffer)
114    }
115
116    pub fn current_buffer_mut(&mut self) -> Option<&mut Buffer> {
117        self.buffers.get_mut(self.active_buffer)
118    }
119
120    pub fn open_file(&mut self, path: &Path) -> Result<()> {
121        if let Some(idx) = self.buffers.iter().position(|b| b.path == path) {
122            self.active_buffer = idx;
123        } else {
124            let buf = Buffer::from_file(path)?;
125            self.buffers.push(buf);
126            self.active_buffer = self.buffers.len() - 1;
127        }
128        self.cursor_line = 0;
129        self.cursor_col = 0;
130        self.scroll_offset = 0;
131        self.focus_tree = false;
132        Ok(())
133    }
134
135    pub fn snapshot_contents(&self) -> Vec<(PathBuf, String)> {
136        self.buffers
137            .iter()
138            .map(|b| (b.path.clone(), b.content()))
139            .collect()
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use std::fs;
147    use tempfile::TempDir;
148
149    #[test]
150    fn for_file_initializes_no_tree_and_empty_tree_view() {
151        let tmp = TempDir::new().unwrap();
152        let path = tmp.path().join("test.rs");
153        fs::write(&path, "fn main() {}").unwrap();
154
155        let state = EditorState::for_file(&path).unwrap();
156
157        assert!(state.file_tree.is_none());
158        assert!(state.tree_view.is_empty());
159    }
160
161    #[test]
162    fn for_file_chord_fields_initialized_to_defaults() {
163        let tmp = TempDir::new().unwrap();
164        let path = tmp.path().join("test.rs");
165        fs::write(&path, "fn main() {}").unwrap();
166
167        let state = EditorState::for_file(&path).unwrap();
168
169        assert_eq!(state.chord_cursor_col, 0);
170        assert!(!state.chord_error);
171        assert!(!state.chord_running);
172    }
173
174    #[test]
175    fn for_directory_tree_view_contains_only_depth_zero_entries() {
176        let tmp = TempDir::new().unwrap();
177        fs::create_dir(tmp.path().join("subdir")).unwrap();
178        fs::write(tmp.path().join("file.rs"), "").unwrap();
179        fs::write(tmp.path().join("subdir/nested.rs"), "").unwrap();
180
181        let state = EditorState::for_directory(tmp.path()).unwrap();
182
183        assert!(!state.tree_view.is_empty());
184        for entry in &state.tree_view {
185            assert_eq!(entry.depth, 0, "unexpected depth for {:?}", entry.path);
186        }
187        let tree = state.file_tree.as_ref().unwrap();
188        assert!(
189            tree.entries.iter().any(|e| e.depth > 0),
190            "full tree should have nested entries"
191        );
192    }
193
194    #[test]
195    fn for_directory_chord_fields_initialized_to_defaults() {
196        let tmp = TempDir::new().unwrap();
197        fs::write(tmp.path().join("x.rs"), "").unwrap();
198
199        let state = EditorState::for_directory(tmp.path()).unwrap();
200
201        assert_eq!(state.chord_cursor_col, 0);
202        assert!(!state.chord_error);
203        assert!(!state.chord_running);
204    }
205}