1use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
6
7use crate::sketch::{self, Parsed};
8use crate::words;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Preview {
13 Json,
15 Answers,
17 Rust,
19 Cost,
21}
22
23impl Preview {
24 pub const ALL: [Preview; 4] = [
25 Preview::Json,
26 Preview::Answers,
27 Preview::Rust,
28 Preview::Cost,
29 ];
30
31 pub fn label(self) -> &'static str {
32 match self {
33 Preview::Json => "json",
34 Preview::Answers => "answers",
35 Preview::Rust => "rust",
36 Preview::Cost => "cost",
37 }
38 }
39
40 fn next(self) -> Self {
41 match self {
42 Preview::Json => Preview::Answers,
43 Preview::Answers => Preview::Rust,
44 Preview::Rust => Preview::Cost,
45 Preview::Cost => Preview::Json,
46 }
47 }
48}
49
50pub enum Outcome {
52 Open,
54 Cancel,
56 Apply,
58 ApplyAndAsk,
60}
61
62pub struct Editor {
63 pub lines: Vec<String>,
64 pub row: usize,
65 pub col: usize,
67 pub top: usize,
69 pub preview: Preview,
70 pub dirty: bool,
71 esc_armed: bool,
73 cut: Option<String>,
75 pub message: Option<String>,
76}
77
78impl Editor {
79 pub fn new(text: &str) -> Self {
80 let lines: Vec<String> = text.split('\n').map(str::to_owned).collect();
81 Self {
82 lines,
83 row: 0,
84 col: 0,
85 top: 0,
86 preview: Preview::Json,
87 dirty: false,
88 esc_armed: false,
89 cut: None,
90 message: None,
91 }
92 }
93
94 pub fn text(&self) -> String {
95 self.lines.join("\n")
96 }
97
98 pub fn parsed(&self) -> Parsed {
99 sketch::parse(&self.text())
100 }
101
102 pub fn key(&mut self, key: KeyEvent) -> Outcome {
103 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
104 let alt = words::is_alt(&key);
105 self.message = None;
106 let armed = std::mem::take(&mut self.esc_armed);
107 if words::is_word_left(&key) {
109 self.word_left();
110 return Outcome::Open;
111 }
112 if words::is_word_right(&key) {
113 self.word_right();
114 return Outcome::Open;
115 }
116 if words::is_delete_word_left(&key) {
117 self.delete_word_left();
118 return Outcome::Open;
119 }
120 match key.code {
121 KeyCode::Esc => {
122 if self.dirty && !armed {
123 self.esc_armed = true;
124 self.message =
125 Some("unapplied edits — Esc again discards them, ^S applies".into());
126 } else {
127 return Outcome::Cancel;
128 }
129 }
130 KeyCode::Char('s') if ctrl => return Outcome::Apply,
131 KeyCode::Char('g') if ctrl => return Outcome::ApplyAndAsk,
132 KeyCode::Char('p') if ctrl => self.preview = self.preview.next(),
133 KeyCode::Char('x') if ctrl => self.cut_line(),
134 KeyCode::Char('u') if ctrl => self.paste_line(),
135 KeyCode::Char('a') if ctrl => self.col = 0,
136 KeyCode::Char('e') if ctrl => self.col = self.len(),
137 KeyCode::Up if alt => self.swap(-1),
138 KeyCode::Down if alt => self.swap(1),
139 KeyCode::Up => self.vertical(-1),
140 KeyCode::Down => self.vertical(1),
141 KeyCode::PageUp => self.vertical(-10),
142 KeyCode::PageDown => self.vertical(10),
143 KeyCode::Left => {
144 if self.col > 0 {
145 self.col -= 1;
146 } else if self.row > 0 {
147 self.row -= 1;
148 self.col = self.len();
149 }
150 }
151 KeyCode::Right => {
152 if self.col < self.len() {
153 self.col += 1;
154 } else if self.row + 1 < self.lines.len() {
155 self.row += 1;
156 self.col = 0;
157 }
158 }
159 KeyCode::Home => self.col = 0,
160 KeyCode::End => self.col = self.len(),
161 KeyCode::Enter => self.newline(),
162 KeyCode::Tab => {
163 self.insert_str(" ");
164 }
165 KeyCode::Backspace => self.backspace(),
166 KeyCode::Delete => self.delete(),
167 KeyCode::Char(c) if !ctrl && !alt => self.insert(c),
168 _ => {}
169 }
170 Outcome::Open
171 }
172
173 fn len(&self) -> usize {
174 self.lines[self.row].chars().count()
175 }
176
177 fn vertical(&mut self, delta: isize) {
178 let last = self.lines.len() as isize - 1;
179 self.row = (self.row as isize + delta).clamp(0, last) as usize;
180 self.col = self.col.min(self.len());
181 }
182
183 fn insert(&mut self, c: char) {
184 let at = byte_at(&self.lines[self.row], self.col);
185 self.lines[self.row].insert(at, c);
186 self.col += 1;
187 self.dirty = true;
188 }
189
190 pub fn insert_str(&mut self, s: &str) {
191 for c in s.chars() {
192 self.insert(c);
193 }
194 }
195
196 fn newline(&mut self) {
198 let at = byte_at(&self.lines[self.row], self.col);
199 let tail = self.lines[self.row].split_off(at);
200 let indent: String = self.lines[self.row]
201 .chars()
202 .take_while(|c| c.is_whitespace())
203 .collect();
204 let indent = if tail.trim().is_empty() && self.lines[self.row].trim().is_empty() {
205 String::new()
206 } else {
207 indent
208 };
209 self.row += 1;
210 self.col = indent.chars().count();
211 self.lines.insert(self.row, format!("{indent}{tail}"));
212 self.dirty = true;
213 }
214
215 fn backspace(&mut self) {
216 if self.col > 0 {
217 self.col -= 1;
218 let at = byte_at(&self.lines[self.row], self.col);
219 self.lines[self.row].remove(at);
220 self.dirty = true;
221 } else if self.row > 0 {
222 let line = self.lines.remove(self.row);
223 self.row -= 1;
224 self.col = self.len();
225 self.lines[self.row].push_str(&line);
226 self.dirty = true;
227 }
228 }
229
230 fn delete(&mut self) {
231 if self.col < self.len() {
232 let at = byte_at(&self.lines[self.row], self.col);
233 self.lines[self.row].remove(at);
234 self.dirty = true;
235 } else if self.row + 1 < self.lines.len() {
236 let next = self.lines.remove(self.row + 1);
237 self.lines[self.row].push_str(&next);
238 self.dirty = true;
239 }
240 }
241
242 fn cut_line(&mut self) {
244 let line = if self.lines.len() == 1 {
245 std::mem::take(&mut self.lines[0])
246 } else {
247 self.lines.remove(self.row)
248 };
249 self.cut = Some(line);
250 self.row = self.row.min(self.lines.len() - 1);
251 self.col = self.col.min(self.len());
252 self.dirty = true;
253 self.message = Some("line cut — ^U pastes it above the cursor".into());
254 }
255
256 fn paste_line(&mut self) {
257 match self.cut.clone() {
258 Some(line) => {
259 self.lines.insert(self.row, line);
260 self.row += 1;
261 self.dirty = true;
262 }
263 None => self.message = Some("nothing cut yet — ^X cuts the current line".into()),
264 }
265 }
266
267 fn chars(&self) -> Vec<char> {
268 self.lines[self.row].chars().collect()
269 }
270
271 fn word_left(&mut self) {
273 if self.col == 0 {
274 if self.row > 0 {
275 self.row -= 1;
276 self.col = self.len();
277 }
278 return;
279 }
280 self.col = words::word_left(&self.chars(), self.col);
281 }
282
283 fn word_right(&mut self) {
285 if self.col >= self.len() {
286 if self.row + 1 < self.lines.len() {
287 self.row += 1;
288 self.col = 0;
289 }
290 return;
291 }
292 self.col = words::word_right(&self.chars(), self.col);
293 }
294
295 fn delete_word_left(&mut self) {
297 if self.col == 0 {
298 self.backspace();
299 return;
300 }
301 let chars = self.chars();
302 let at = words::word_left(&chars, self.col);
303 let kept: String = chars[..at].iter().chain(chars[self.col..].iter()).collect();
304 self.lines[self.row] = kept;
305 self.col = at;
306 self.dirty = true;
307 }
308
309 fn swap(&mut self, delta: isize) {
311 let target = self.row as isize + delta;
312 if target < 0 || target >= self.lines.len() as isize {
313 return;
314 }
315 self.lines.swap(self.row, target as usize);
316 self.row = target as usize;
317 self.dirty = true;
318 }
319}
320
321fn byte_at(s: &str, char_index: usize) -> usize {
322 s.char_indices()
323 .nth(char_index)
324 .map(|(i, _)| i)
325 .unwrap_or(s.len())
326}