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