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