Skip to main content

ane/frontend/
cli_frontend.rs

1use std::sync::Arc;
2
3use anyhow::Result;
4
5use crate::commands::chord::FrontendCapabilities;
6use crate::commands::chord_engine::types::{ChordAction, ListFrontend, ListItem};
7use crate::commands::lsp_engine::InstallProgress;
8use crate::data::state::EditorState;
9
10use super::traits::ApplyChordAction;
11
12pub struct CliInstallProgress;
13
14impl InstallProgress for CliInstallProgress {
15    fn on_stdout(&self, line: &str) {
16        println!("{line}");
17    }
18    fn on_stderr(&self, line: &str) {
19        eprintln!("{line}");
20    }
21    fn on_failed(&self, message: &str) {
22        eprintln!("{message}");
23    }
24    fn on_complete(&self) {}
25}
26
27pub fn cli_install_progress() -> Arc<dyn InstallProgress> {
28    Arc::new(CliInstallProgress)
29}
30
31pub struct CliFrontend;
32
33impl Default for CliFrontend {
34    fn default() -> Self {
35        Self
36    }
37}
38
39impl CliFrontend {
40    pub fn new() -> Self {
41        Self
42    }
43}
44
45impl FrontendCapabilities for CliFrontend {
46    fn is_interactive(&self) -> bool {
47        false
48    }
49}
50
51impl ListFrontend for CliFrontend {
52    fn show_list(&mut self, _state: &mut EditorState, items: &[ListItem]) -> Result<()> {
53        for item in items {
54            println!("{}:{}  {}", item.line + 1, item.col + 1, item.val);
55        }
56        Ok(())
57    }
58}
59
60impl ApplyChordAction for CliFrontend {
61    fn apply(&mut self, state: &mut EditorState, action: &ChordAction) -> Result<String> {
62        if !action.listed_items.is_empty() {
63            self.show_list(state, &action.listed_items)?;
64            return Ok(String::new());
65        }
66        if let Some(ref diff) = action.diff {
67            if let Some(buf) = state.current_buffer_mut() {
68                let new_lines: Vec<String> = diff.modified.lines().map(String::from).collect();
69                buf.lines = if new_lines.is_empty() {
70                    vec![String::new()]
71                } else {
72                    new_lines
73                };
74                buf.dirty = true;
75            }
76            Ok(diff.modified.clone())
77        } else if let Some(ref yanked) = action.yanked_content {
78            Ok(yanked.clone())
79        } else {
80            Ok(String::new())
81        }
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use crate::commands::chord_engine::types::UnifiedDiff;
89    use crate::data::state::EditorState;
90    use std::io::Write;
91
92    fn make_state(content: &str) -> (tempfile::NamedTempFile, EditorState) {
93        let mut f = tempfile::NamedTempFile::new().unwrap();
94        f.write_all(content.as_bytes()).unwrap();
95        f.flush().unwrap();
96        let state = EditorState::for_file(f.path()).unwrap();
97        (f, state)
98    }
99
100    fn diff_action(modified: &str) -> ChordAction {
101        ChordAction {
102            buffer_name: "test".to_string(),
103            diff: Some(UnifiedDiff {
104                original: String::new(),
105                modified: modified.to_string(),
106                hunks: vec![],
107            }),
108            yanked_content: None,
109            cursor_destination: None,
110            mode_after: None,
111            highlight_ranges: vec![],
112            warnings: vec![],
113            listed_items: vec![],
114        }
115    }
116
117    fn yank_action(content: &str) -> ChordAction {
118        ChordAction {
119            buffer_name: "test".to_string(),
120            diff: None,
121            yanked_content: Some(content.to_string()),
122            cursor_destination: None,
123            mode_after: None,
124            highlight_ranges: vec![],
125            warnings: vec![],
126            listed_items: vec![],
127        }
128    }
129
130    #[test]
131    fn apply_diff_updates_buffer_lines_and_returns_modified_content() {
132        let (_f, mut state) = make_state("old line 1\nold line 2");
133        let action = diff_action("new line 1\nnew line 2");
134
135        let mut frontend = CliFrontend::new();
136        let result = frontend.apply(&mut state, &action).unwrap();
137
138        assert_eq!(result, "new line 1\nnew line 2");
139        let buf = state.current_buffer().unwrap();
140        assert_eq!(buf.lines, vec!["new line 1", "new line 2"]);
141        assert!(buf.dirty);
142    }
143
144    #[test]
145    fn apply_yank_returns_content_without_modifying_buffer() {
146        let (_f, mut state) = make_state("original line 1\noriginal line 2");
147        let original_lines = state.current_buffer().unwrap().lines.clone();
148        let action = yank_action("yanked content");
149
150        let mut frontend = CliFrontend::new();
151        let result = frontend.apply(&mut state, &action).unwrap();
152
153        assert_eq!(result, "yanked content");
154        let buf = state.current_buffer().unwrap();
155        assert_eq!(
156            buf.lines, original_lines,
157            "yank must not modify buffer lines"
158        );
159        assert!(!buf.dirty, "yank must not mark buffer dirty");
160    }
161
162    // --- work item 0005: Jump / To / Delimiter ---
163
164    #[test]
165    fn cli_frontend_is_not_interactive() {
166        let frontend = CliFrontend::new();
167        assert!(!frontend.is_interactive());
168    }
169
170    // --- work item 0011: List action ---
171
172    #[test]
173    fn show_list_format_is_one_indexed_line_and_col() {
174        // The format string used by show_list: "{}:{}  {}", line+1, col+1, val
175        use crate::commands::chord_engine::types::ListItem;
176        let item = ListItem {
177            val: "foo".to_string(),
178            line: 0,
179            col: 0,
180        };
181        let formatted = format!("{}:{}  {}", item.line + 1, item.col + 1, item.val);
182        assert_eq!(formatted, "1:1  foo");
183
184        let item2 = ListItem {
185            val: "bar".to_string(),
186            line: 4,
187            col: 0,
188        };
189        let formatted2 = format!("{}:{}  {}", item2.line + 1, item2.col + 1, item2.val);
190        assert_eq!(formatted2, "5:1  bar");
191    }
192
193    #[test]
194    fn apply_with_listed_items_returns_empty_string() {
195        use crate::commands::chord_engine::types::ListItem;
196        let (_f, mut state) = make_state("hello");
197        let action = ChordAction {
198            buffer_name: "test".to_string(),
199            diff: None,
200            yanked_content: None,
201            cursor_destination: None,
202            mode_after: None,
203            highlight_ranges: vec![],
204            warnings: vec![],
205            listed_items: vec![
206                ListItem {
207                    val: "foo".to_string(),
208                    line: 0,
209                    col: 0,
210                },
211                ListItem {
212                    val: "bar".to_string(),
213                    line: 4,
214                    col: 0,
215                },
216            ],
217        };
218        let mut frontend = CliFrontend::new();
219        let result = frontend.apply(&mut state, &action).unwrap();
220        assert_eq!(result, "");
221    }
222
223    #[test]
224    fn apply_with_empty_listed_items_falls_through_to_diff() {
225        let (_f, mut state) = make_state("old");
226        let action = diff_action("new");
227        let mut frontend = CliFrontend::new();
228        let result = frontend.apply(&mut state, &action).unwrap();
229        assert_eq!(result, "new");
230    }
231}