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}