1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::path::PathBuf;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::Arc;
10
11#[derive(Debug, Clone)]
14pub struct CancelSignal {
15 inner: Arc<AtomicBool>,
16}
17
18impl CancelSignal {
19 pub fn new() -> Self {
21 Self {
22 inner: Arc::new(AtomicBool::new(false)),
23 }
24 }
25
26 pub fn request_cancel(&self) {
28 self.inner.store(true, Ordering::Relaxed);
29 }
30
31 pub fn reset(&self) {
33 self.inner.store(false, Ordering::Relaxed);
34 }
35
36 pub fn is_cancelled(&self) -> bool {
38 self.inner.load(Ordering::Relaxed)
39 }
40}
41
42impl Default for CancelSignal {
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48pub struct ToolState {
50 pub open_files: HashMap<PathBuf, FileState>,
52 pub current_file: Option<PathBuf>,
54 pub history: Vec<StateSnapshot>,
56 pub working_directory: PathBuf,
58 cancel_signal: Arc<AtomicBool>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct FileState {
66 pub content: Vec<String>,
68 pub window_start: usize,
70 pub window_size: usize,
72 pub modified: bool,
74 pub last_modified: Option<std::time::SystemTime>,
76}
77
78impl FileState {
79 pub fn new(content: Vec<String>, window_size: usize) -> Self {
81 Self {
82 content,
83 window_start: 0,
84 window_size,
85 modified: false,
86 last_modified: None,
87 }
88 }
89
90 pub fn get_window(&self) -> Vec<&String> {
92 let end = std::cmp::min(self.window_start + self.window_size, self.content.len());
93 self.content[self.window_start..end].iter().collect()
94 }
95
96 pub fn get_window_with_line_numbers(&self) -> Vec<String> {
98 let end = std::cmp::min(self.window_start + self.window_size, self.content.len());
99 self.content[self.window_start..end]
100 .iter()
101 .enumerate()
102 .map(|(i, line)| format!("{:4} | {}", self.window_start + i + 1, line))
103 .collect()
104 }
105
106 pub fn goto_line(&mut self, line_number: usize) {
108 if line_number == 0 {
109 return;
110 }
111 let target_line = line_number.saturating_sub(1);
112 if target_line < self.content.len() {
113 let half_window = self.window_size / 2;
114 self.window_start = target_line.saturating_sub(half_window);
115
116 let max_start = self.content.len().saturating_sub(self.window_size);
117 if self.window_start > max_start {
118 self.window_start = max_start;
119 }
120 }
121 }
122
123 pub fn scroll_up(&mut self) {
125 self.window_start = self.window_start.saturating_sub(self.window_size);
126 }
127
128 pub fn scroll_down(&mut self) {
130 let max_start = self.content.len().saturating_sub(self.window_size);
131 self.window_start = std::cmp::min(self.window_start + self.window_size, max_start);
132 }
133
134 pub fn total_lines(&self) -> usize {
136 self.content.len()
137 }
138
139 pub fn is_at_start(&self) -> bool {
141 self.window_start == 0
142 }
143
144 pub fn is_at_end(&self) -> bool {
146 self.window_start + self.window_size >= self.content.len()
147 }
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct StateSnapshot {
153 pub timestamp: std::time::SystemTime,
154 pub current_file: Option<PathBuf>,
155 pub operation: String,
156 pub file_states: HashMap<PathBuf, FileState>,
157}
158
159impl ToolState {
160 pub fn new() -> Self {
162 Self {
163 open_files: HashMap::new(),
164 current_file: None,
165 history: Vec::new(),
166 working_directory: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
167 cancel_signal: Arc::new(AtomicBool::new(false)),
168 }
169 }
170
171 pub fn open_file(
173 &mut self,
174 path: PathBuf,
175 content: Vec<String>,
176 window_size: usize,
177 ) -> anyhow::Result<()> {
178 let file_state = FileState::new(content, window_size);
179 self.open_files.insert(path.clone(), file_state);
180 self.current_file = Some(path);
181 Ok(())
182 }
183
184 pub fn get_current_file_state(&self) -> Option<&FileState> {
186 self.current_file
187 .as_ref()
188 .and_then(|path| self.open_files.get(path))
189 }
190
191 pub fn get_current_file_state_mut(&mut self) -> Option<&mut FileState> {
193 let current_file = self.current_file.clone()?;
194 self.open_files.get_mut(¤t_file)
195 }
196
197 pub fn switch_to_file(&mut self, path: &PathBuf) -> Result<(), crate::core::ToolError> {
199 if self.open_files.contains_key(path) {
200 self.current_file = Some(path.clone());
201 Ok(())
202 } else {
203 Err(crate::core::ToolError::FileNotFound {
204 path: path.to_string_lossy().to_string(),
205 })
206 }
207 }
208
209 pub fn close_file(&mut self, path: &PathBuf) {
211 self.open_files.remove(path);
212 if self.current_file.as_ref() == Some(path) {
213 self.current_file = self.open_files.keys().next().cloned();
214 }
215 }
216
217 pub fn create_snapshot(&self, operation: String) -> StateSnapshot {
219 StateSnapshot {
220 timestamp: std::time::SystemTime::now(),
221 current_file: self.current_file.clone(),
222 operation,
223 file_states: self.open_files.clone(),
224 }
225 }
226
227 pub fn push_history(&mut self, operation: String) {
229 let snapshot = self.create_snapshot(operation);
230 self.history.push(snapshot);
231
232 const MAX_HISTORY: usize = 100;
233 if self.history.len() > MAX_HISTORY {
234 self.history.remove(0);
235 }
236 }
237
238 pub fn get_summary(&self) -> String {
240 let mut summary = String::new();
241
242 summary.push_str(&format!(
243 "Working Directory: {}\n",
244 self.working_directory.display()
245 ));
246
247 if let Some(current) = &self.current_file {
248 summary.push_str(&format!("Current File: {}\n", current.display()));
249
250 if let Some(file_state) = self.open_files.get(current) {
251 summary.push_str(&format!(
252 "Lines: {} | Window: {}-{} (size: {})\n",
253 file_state.total_lines(),
254 file_state.window_start + 1,
255 std::cmp::min(
256 file_state.window_start + file_state.window_size,
257 file_state.total_lines()
258 ),
259 file_state.window_size
260 ));
261 }
262 } else {
263 summary.push_str("No file currently open\n");
264 }
265
266 if !self.open_files.is_empty() {
267 summary.push_str("\nOpen Files:\n");
268 for (path, file_state) in &self.open_files {
269 let modified = if file_state.modified {
270 " (modified)"
271 } else {
272 ""
273 };
274 summary.push_str(&format!(
275 " {} - {} lines{}\n",
276 path.display(),
277 file_state.total_lines(),
278 modified
279 ));
280 }
281 }
282
283 summary
284 }
285
286 pub fn request_cancel(&self) {
290 self.cancel_signal.store(true, Ordering::Relaxed);
291 }
292
293 pub fn reset_cancel(&self) {
295 self.cancel_signal.store(false, Ordering::Relaxed);
296 }
297
298 pub fn is_cancelled(&self) -> bool {
300 self.cancel_signal.load(Ordering::Relaxed)
301 }
302
303 pub fn cancel_signal(&self) -> Arc<AtomicBool> {
306 Arc::clone(&self.cancel_signal)
307 }
308}
309
310impl Default for ToolState {
311 fn default() -> Self {
312 Self::new()
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
321 fn test_file_state_creation() {
322 let content = vec![
323 "line1".to_string(),
324 "line2".to_string(),
325 "line3".to_string(),
326 ];
327 let file_state = FileState::new(content, 2);
328
329 assert_eq!(file_state.window_size, 2);
330 assert_eq!(file_state.window_start, 0);
331 assert_eq!(file_state.total_lines(), 3);
332 assert!(!file_state.modified);
333 }
334
335 #[test]
336 fn test_file_state_windowing() {
337 let content: Vec<String> = (1..=10).map(|i| format!("line {}", i)).collect();
338 let mut file_state = FileState::new(content, 3);
339
340 let window = file_state.get_window();
341 assert_eq!(window.len(), 3);
342 assert_eq!(window[0], "line 1");
343 assert_eq!(window[2], "line 3");
344
345 file_state.scroll_down();
346 let window = file_state.get_window();
347 assert_eq!(window[0], "line 4");
348
349 file_state.goto_line(8);
350 assert!(file_state.window_start >= 5);
351 }
352
353 #[test]
354 fn test_tool_state_file_management() {
355 let mut state = ToolState::new();
356 let path = PathBuf::from("test.txt");
357 let content = vec!["line1".to_string(), "line2".to_string()];
358
359 state.open_file(path.clone(), content, 10).unwrap();
360 assert_eq!(state.current_file, Some(path.clone()));
361 assert!(state.open_files.contains_key(&path));
362
363 let file_state = state.get_current_file_state().unwrap();
364 assert_eq!(file_state.total_lines(), 2);
365
366 state.close_file(&path);
367 assert!(!state.open_files.contains_key(&path));
368 assert_eq!(state.current_file, None);
369 }
370}