ane/frontend/
cli_frontend.rs1use 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 #[test]
165 fn cli_frontend_is_not_interactive() {
166 let frontend = CliFrontend::new();
167 assert!(!frontend.is_interactive());
168 }
169
170 #[test]
173 fn show_list_format_is_one_indexed_line_and_col() {
174 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}