1use crate::edit_control::{EditModification, ModifiableEdit, EditModificationInterface, ViewMode, InterfaceState};
3use crossterm::{
4 cursor,
5 event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
6 execute,
7 style::{Color, ResetColor, SetForegroundColor},
8 terminal::{self, ClearType},
9};
10use std::io::{self, stdout, Write};
11
12pub struct InlineEditor {
14 interface: EditModificationInterface,
15 content: Vec<String>,
16 cursor_row: usize,
17 cursor_col: usize,
18 scroll_offset: usize,
19 modified: bool,
20 show_help: bool,
21 show_history: bool,
22}
23
24impl InlineEditor {
25 pub fn new(modifiable_edit: ModifiableEdit) -> Self {
27 let interface = EditModificationInterface::new(modifiable_edit.clone());
28 let content: Vec<String> = modifiable_edit
29 .base_edit
30 .new_code
31 .lines()
32 .map(|s| s.to_string())
33 .collect();
34
35 Self {
36 interface,
37 content,
38 cursor_row: 0,
39 cursor_col: 0,
40 scroll_offset: 0,
41 modified: false,
42 show_help: false,
43 show_history: false,
44 }
45 }
46
47 pub fn edit(&mut self) -> io::Result<Option<EditModificationInterface>> {
49 terminal::enable_raw_mode()?;
50
51 let result = loop {
52 self.render()?;
53
54 if let Event::Key(key_event) = event::read()? {
55 match self.handle_key(key_event)? {
56 EditorAction::Exit => {
57 break Ok(None);
58 }
59 EditorAction::Save => {
60 break Ok(Some(self.interface.clone()));
61 }
62 EditorAction::Continue => {}
63 }
64 }
65 };
66
67 terminal::disable_raw_mode()?;
68 result
69 }
70
71 fn render(&self) -> io::Result<()> {
73 execute!(
74 stdout(),
75 terminal::Clear(ClearType::All),
76 cursor::MoveTo(0, 0)
77 )?;
78
79 self.render_header()?;
81
82 match self.interface.view_mode {
83 ViewMode::Single => self.render_single_view()?,
84 ViewMode::SideBySide => self.render_side_by_side_view()?,
85 ViewMode::Unified => self.render_unified_diff_view()?,
86 ViewMode::FullScreen => self.render_fullscreen_view()?,
87 }
88
89 self.render_footer()?;
91
92 self.position_cursor()?;
94
95 stdout().flush()
96 }
97
98 fn render_header(&self) -> io::Result<()> {
99 execute!(stdout(), SetForegroundColor(Color::Cyan))?;
100 println!("🚀 SOMA EditModificationInterface v2.0 - Issue #17");
101 execute!(stdout(), ResetColor)?;
102
103 println!(
104 "📁 File: {} | Lines: {}-{} | State: {:?} | View: {:?}",
105 self.interface.modifiable_edit.base_edit.file,
106 self.interface.modifiable_edit.base_edit.line_range.0,
107 self.interface.modifiable_edit.base_edit.line_range.1,
108 self.interface.interface_state,
109 self.interface.view_mode
110 );
111
112 if self.show_help {
113 self.render_help_panel()?;
114 }
115
116 if self.show_history {
117 self.render_history_panel()?;
118 }
119
120 println!(); Ok(())
122 }
123
124 fn render_help_panel(&self) -> io::Result<()> {
125 execute!(stdout(), SetForegroundColor(Color::Yellow))?;
126 println!("┌─ Help Panel ─────────────────────────────────────────────────────────┐");
127 println!("│ Ctrl+S: Save Ctrl+Q: Quit Ctrl+Z: Undo Ctrl+Y: Redo │");
128 println!("│ Ctrl+H: Help Ctrl+R: History Ctrl+V: View Mode Ctrl+T: Syntax │");
129 println!("│ Ctrl+C: Compare Ctrl+P: Preview Arrow Keys: Navigate │");
130 println!("└──────────────────────────────────────────────────────────────────────┘");
131 execute!(stdout(), ResetColor)?;
132 Ok(())
133 }
134
135 fn render_history_panel(&self) -> io::Result<()> {
136 execute!(stdout(), SetForegroundColor(Color::Green))?;
137 println!("┌─ Modification History ───────────────────────────────────────────────┐");
138
139 let history = self.interface.get_history_summary();
140 for (_i, entry) in history.iter().take(5).enumerate() {
141 println!("│ {:<68} │", entry);
142 }
143
144 if history.len() > 5 {
145 println!("│ ... and {} more entries │", history.len() - 5);
146 }
147
148 println!("└──────────────────────────────────────────────────────────────────────┘");
149 execute!(stdout(), ResetColor)?;
150 Ok(())
151 }
152
153 fn render_single_view(&self) -> io::Result<()> {
154 let visible_lines = if self.show_help || self.show_history { 15 } else { 20 };
155 let start = self.scroll_offset;
156 let end = (start + visible_lines).min(self.content.len());
157
158 for (idx, line) in self.content[start..end].iter().enumerate() {
159 let line_num = start + idx + 1;
160 let marker = if start + idx == self.cursor_row { ">" } else { " " };
161
162 if self.interface.syntax_highlighting {
163 self.render_syntax_highlighted_line(line_num, marker, line)?;
164 } else {
165 println!("{:3} {} {}", line_num, marker, line);
166 }
167 }
168
169 Ok(())
170 }
171
172 fn render_side_by_side_view(&self) -> io::Result<()> {
173 let original_lines: Vec<&str> = self.interface.modifiable_edit.base_edit.new_code.lines().collect();
174 let current_lines = &self.content;
175
176 execute!(stdout(), SetForegroundColor(Color::Blue))?;
177 println!("┌─ Original ──────────────────────┬─ Modified ──────────────────────┐");
178 execute!(stdout(), ResetColor)?;
179
180 let max_lines = original_lines.len().max(current_lines.len());
181 let visible_lines = 15;
182 let start = self.scroll_offset;
183 let end = (start + visible_lines).min(max_lines);
184
185 for i in start..end {
186 let orig = original_lines.get(i).unwrap_or(&"");
187 let curr = current_lines.get(i).map(|s| s.as_str()).unwrap_or("");
188
189 let marker = if i == self.cursor_row { ">" } else { " " };
190
191 if *orig != curr {
192 execute!(stdout(), SetForegroundColor(Color::Red))?;
193 }
194
195 println!("│{:3}{} {:<26}│{:3}{} {:<26}│",
196 i + 1, marker, orig.chars().take(26).collect::<String>(),
197 i + 1, marker, curr.chars().take(26).collect::<String>());
198
199 execute!(stdout(), ResetColor)?;
200 }
201
202 println!("└─────────────────────────────────┴─────────────────────────────────┘");
203 Ok(())
204 }
205
206 fn render_unified_diff_view(&self) -> io::Result<()> {
207 execute!(stdout(), SetForegroundColor(Color::Magenta))?;
208 println!("┌─ Unified Diff View ──────────────────────────────────────────────────┐");
209 execute!(stdout(), ResetColor)?;
210
211 let diff_summary = self.interface.get_diff_summary();
212 execute!(stdout(), SetForegroundColor(Color::Green))?;
213 println!("│ +{} lines added, -{} removed, ~{} modified │",
214 diff_summary.lines_added, diff_summary.lines_removed, diff_summary.lines_modified);
215 execute!(stdout(), ResetColor)?;
216
217 let original_lines: Vec<&str> = self.interface.modifiable_edit.base_edit.new_code.lines().collect();
219
220 for (i, line) in self.content.iter().enumerate() {
221 let orig = original_lines.get(i).unwrap_or(&"");
222
223 if line != orig {
224 execute!(stdout(), SetForegroundColor(Color::Red))?;
225 println!("│ -{:<66} │", orig.chars().take(66).collect::<String>());
226 execute!(stdout(), SetForegroundColor(Color::Green))?;
227 println!("│ +{:<66} │", line.chars().take(66).collect::<String>());
228 execute!(stdout(), ResetColor)?;
229 } else {
230 println!("│ {:<66} │", line.chars().take(66).collect::<String>());
231 }
232 }
233
234 println!("└──────────────────────────────────────────────────────────────────────┘");
235 Ok(())
236 }
237
238 fn render_fullscreen_view(&self) -> io::Result<()> {
239 let visible_lines = 25;
241 let start = self.scroll_offset;
242 let end = (start + visible_lines).min(self.content.len());
243
244 for (idx, line) in self.content[start..end].iter().enumerate() {
245 let line_num = start + idx + 1;
246 let marker = if start + idx == self.cursor_row { ">" } else { " " };
247 println!("{:4} {} {}", line_num, marker, line);
248 }
249
250 Ok(())
251 }
252
253 fn render_syntax_highlighted_line(&self, line_num: usize, marker: &str, line: &str) -> io::Result<()> {
254 print!("{:3} {} ", line_num, marker);
255
256 let mut in_string = false;
258 let mut in_comment = false;
259
260 let mut chars = line.chars().peekable();
261 while let Some(ch) = chars.next() {
262 if in_comment {
263 execute!(stdout(), SetForegroundColor(Color::DarkGreen))?;
264 print!("{}", ch);
265 } else if in_string {
266 execute!(stdout(), SetForegroundColor(Color::Yellow))?;
267 print!("{}", ch);
268 if ch == '"' && chars.peek() != Some(&'\\') {
269 in_string = false;
270 execute!(stdout(), ResetColor)?;
271 }
272 } else {
273 match ch {
274 '/' if chars.peek() == Some(&'/') => {
275 in_comment = true;
276 execute!(stdout(), SetForegroundColor(Color::DarkGreen))?;
277 print!("{}", ch);
278 }
279 '"' => {
280 in_string = true;
281 execute!(stdout(), SetForegroundColor(Color::Yellow))?;
282 print!("{}", ch);
283 }
284 'f' if line.starts_with("fn ") => {
285 execute!(stdout(), SetForegroundColor(Color::Blue))?;
286 print!("{}", ch);
287 }
288 _ => {
289 execute!(stdout(), ResetColor)?;
290 print!("{}", ch);
291 }
292 }
293 }
294 }
295
296 execute!(stdout(), ResetColor)?;
297 println!();
298 Ok(())
299 }
300
301 fn render_footer(&self) -> io::Result<()> {
302 let diff_summary = self.interface.get_diff_summary();
303
304 execute!(stdout(), SetForegroundColor(Color::Cyan))?;
305 println!("─────────────────────────────────────────────────────────────────────────");
306 execute!(stdout(), ResetColor)?;
307
308 println!(
309 "Cursor: {}:{} | Modified: {} | Changes: {} | Undo: {} | Redo: {} | Syntax: {}",
310 self.cursor_row + 1,
311 self.cursor_col + 1,
312 if self.modified { "YES" } else { "NO" },
313 diff_summary.total_changes,
314 if self.interface.can_undo() { "YES" } else { "NO" },
315 if self.interface.can_redo() { "YES" } else { "NO" },
316 if self.interface.syntax_highlighting { "ON" } else { "OFF" }
317 );
318
319 if let Some(desc) = self.interface.current_snapshot_description() {
320 println!("Current: {}", desc);
321 }
322
323 Ok(())
324 }
325
326 fn position_cursor(&self) -> io::Result<()> {
327 let header_offset = if self.show_help { 9 } else if self.show_history { 8 } else { 4 };
328 let display_row = header_offset + self.cursor_row - self.scroll_offset;
329 let display_col = match self.interface.view_mode {
330 ViewMode::SideBySide => self.cursor_col + 38, _ => self.cursor_col + 6, };
333
334 execute!(
335 stdout(),
336 cursor::MoveTo(display_col as u16, display_row as u16)
337 )?;
338
339 Ok(())
340 }
341
342 fn handle_key(&mut self, key: KeyEvent) -> io::Result<EditorAction> {
344 match (key.code, key.modifiers) {
345 (KeyCode::Char('q'), KeyModifiers::CONTROL) => Ok(EditorAction::Exit),
347 (KeyCode::Char('s'), KeyModifiers::CONTROL) => {
348 self.save_current_changes();
349 Ok(EditorAction::Save)
350 },
351
352 (KeyCode::Char('z'), KeyModifiers::CONTROL) => {
354 if self.interface.undo() {
355 self.update_content_from_interface();
356 }
357 Ok(EditorAction::Continue)
358 },
359 (KeyCode::Char('y'), KeyModifiers::CONTROL) => {
360 if self.interface.redo() {
361 self.update_content_from_interface();
362 }
363 Ok(EditorAction::Continue)
364 },
365
366 (KeyCode::Char('h'), KeyModifiers::CONTROL) => {
368 self.show_help = !self.show_help;
369 Ok(EditorAction::Continue)
370 },
371 (KeyCode::Char('r'), KeyModifiers::CONTROL) => {
372 self.show_history = !self.show_history;
373 Ok(EditorAction::Continue)
374 },
375 (KeyCode::Char('t'), KeyModifiers::CONTROL) => {
376 self.interface.toggle_syntax_highlighting();
377 Ok(EditorAction::Continue)
378 },
379 (KeyCode::Char('v'), KeyModifiers::CONTROL) => {
380 self.cycle_view_mode();
381 Ok(EditorAction::Continue)
382 },
383 (KeyCode::Char('c'), KeyModifiers::CONTROL) => {
384 self.interface.set_state(InterfaceState::Comparing);
385 self.interface.set_view_mode(ViewMode::SideBySide);
386 Ok(EditorAction::Continue)
387 },
388 (KeyCode::Char('p'), KeyModifiers::CONTROL) => {
389 self.interface.set_state(InterfaceState::Previewing);
390 self.interface.set_view_mode(ViewMode::Unified);
391 Ok(EditorAction::Continue)
392 },
393
394 (KeyCode::Up, _) => {
396 if self.cursor_row > 0 {
397 self.cursor_row -= 1;
398 self.adjust_cursor_bounds();
399 self.adjust_scroll();
400 }
401 Ok(EditorAction::Continue)
402 }
403 (KeyCode::Down, _) => {
404 if self.cursor_row < self.content.len().saturating_sub(1) {
405 self.cursor_row += 1;
406 self.adjust_cursor_bounds();
407 self.adjust_scroll();
408 }
409 Ok(EditorAction::Continue)
410 }
411 (KeyCode::Left, _) => {
412 if self.cursor_col > 0 {
413 self.cursor_col -= 1;
414 } else if self.cursor_row > 0 {
415 self.cursor_row -= 1;
416 self.cursor_col = self
417 .content
418 .get(self.cursor_row)
419 .map(|s| s.len())
420 .unwrap_or(0);
421 self.adjust_scroll();
422 }
423 Ok(EditorAction::Continue)
424 }
425 (KeyCode::Right, _) => {
426 let line_len = self
427 .content
428 .get(self.cursor_row)
429 .map(|s| s.len())
430 .unwrap_or(0);
431 if self.cursor_col < line_len {
432 self.cursor_col += 1;
433 } else if self.cursor_row < self.content.len().saturating_sub(1) {
434 self.cursor_row += 1;
435 self.cursor_col = 0;
436 self.adjust_scroll();
437 }
438 Ok(EditorAction::Continue)
439 }
440 (KeyCode::Home, _) => {
441 self.cursor_col = 0;
442 Ok(EditorAction::Continue)
443 }
444 (KeyCode::End, _) => {
445 self.cursor_col = self
446 .content
447 .get(self.cursor_row)
448 .map(|s| s.len())
449 .unwrap_or(0);
450 Ok(EditorAction::Continue)
451 }
452
453 (KeyCode::Enter, _) => {
455 self.insert_newline();
456 Ok(EditorAction::Continue)
457 }
458 (KeyCode::Char(c), _) => {
459 self.insert_char(c);
460 Ok(EditorAction::Continue)
461 }
462 (KeyCode::Backspace, _) => {
463 self.delete_char();
464 Ok(EditorAction::Continue)
465 }
466 (KeyCode::Delete, _) => {
467 self.delete_char_forward();
468 Ok(EditorAction::Continue)
469 }
470 _ => Ok(EditorAction::Continue),
471 }
472 }
473
474 fn cycle_view_mode(&mut self) {
475 let new_mode = match self.interface.view_mode {
476 ViewMode::Single => ViewMode::SideBySide,
477 ViewMode::SideBySide => ViewMode::Unified,
478 ViewMode::Unified => ViewMode::FullScreen,
479 ViewMode::FullScreen => ViewMode::Single,
480 };
481 self.interface.set_view_mode(new_mode);
482 }
483
484 fn save_current_changes(&mut self) {
485 if self.modified {
486 let new_content = self.content.join("\n");
487 let old_content = self.interface.modifiable_edit.compute_final_code();
488
489 if new_content != old_content {
490 self.interface.add_modification(
491 EditModification::CodeChange {
492 line: 0,
493 old: old_content,
494 new: new_content,
495 },
496 "Manual edit changes".to_string(),
497 );
498 self.modified = false;
499 }
500 }
501 }
502
503 fn update_content_from_interface(&mut self) {
504 let new_content = self.interface.modifiable_edit.compute_final_code();
505 self.content = new_content.lines().map(|s| s.to_string()).collect();
506 self.modified = false;
507 }
508
509 fn adjust_cursor_bounds(&mut self) {
510 if let Some(line) = self.content.get(self.cursor_row) {
511 if self.cursor_col > line.len() {
512 self.cursor_col = line.len();
513 }
514 }
515 }
516
517 fn adjust_scroll(&mut self) {
518 let visible_lines = 20;
519 if self.cursor_row < self.scroll_offset {
520 self.scroll_offset = self.cursor_row;
521 } else if self.cursor_row >= self.scroll_offset + visible_lines {
522 self.scroll_offset = self.cursor_row - visible_lines + 1;
523 }
524 }
525
526 fn insert_char(&mut self, c: char) {
527 if self.cursor_row < self.content.len() {
528 self.content[self.cursor_row].insert(self.cursor_col, c);
529 self.cursor_col += 1;
530 self.modified = true;
531 }
532 }
533
534 fn insert_newline(&mut self) {
535 if self.cursor_row < self.content.len() {
536 let current_line = self.content[self.cursor_row].clone();
537 let (left, right) = current_line.split_at(self.cursor_col);
538
539 self.content[self.cursor_row] = left.to_string();
540 self.content.insert(self.cursor_row + 1, right.to_string());
541
542 self.cursor_row += 1;
543 self.cursor_col = 0;
544 self.modified = true;
545 self.adjust_scroll();
546 }
547 }
548
549 fn delete_char(&mut self) {
550 if self.cursor_col > 0 && self.cursor_row < self.content.len() {
551 self.content[self.cursor_row].remove(self.cursor_col - 1);
552 self.cursor_col -= 1;
553 self.modified = true;
554 } else if self.cursor_row > 0 {
555 let current_line = self.content.remove(self.cursor_row);
557 self.cursor_row -= 1;
558 self.cursor_col = self.content[self.cursor_row].len();
559 self.content[self.cursor_row].push_str(¤t_line);
560 self.modified = true;
561 self.adjust_scroll();
562 }
563 }
564
565 fn delete_char_forward(&mut self) {
566 if self.cursor_row < self.content.len() {
567 let line_len = self.content[self.cursor_row].len();
568 if self.cursor_col < line_len {
569 self.content[self.cursor_row].remove(self.cursor_col);
570 self.modified = true;
571 } else if self.cursor_row < self.content.len() - 1 {
572 let next_line = self.content.remove(self.cursor_row + 1);
574 self.content[self.cursor_row].push_str(&next_line);
575 self.modified = true;
576 }
577 }
578 }
579}
580
581#[derive(Debug)]
582enum EditorAction {
583 Continue,
584 Save,
585 Exit,
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591 use crate::agents::gpt4_agent::ProposedEdit;
592
593 fn create_test_edit() -> ModifiableEdit {
594 let proposed = ProposedEdit {
595 file: "test.rs".to_string(),
596 line_range: (10, 15),
597 new_code: "fn test() {\n println!(\"Hello\");\n}".to_string(),
598 reason: "Test function".to_string(),
599 confidence: 0.9,
600 };
601 ModifiableEdit::from_proposed_edit(proposed)
602 }
603
604 #[test]
605 fn test_editor_creation() {
606 let edit = create_test_edit();
607 let editor = InlineEditor::new(edit);
608
609 assert_eq!(editor.content.len(), 3);
610 assert_eq!(editor.cursor_row, 0);
611 assert_eq!(editor.cursor_col, 0);
612 assert!(!editor.modified);
613 }
614
615 #[test]
616 fn test_text_insertion() {
617 let edit = create_test_edit();
618 let mut editor = InlineEditor::new(edit);
619
620 editor.insert_char('X');
621 assert!(editor.modified);
622 assert!(editor.content[0].starts_with('X'));
623 assert_eq!(editor.cursor_col, 1);
624 }
625
626 #[test]
627 fn test_newline_insertion() {
628 let edit = create_test_edit();
629 let mut editor = InlineEditor::new(edit);
630
631 let initial_lines = editor.content.len();
632 editor.insert_newline();
633
634 assert_eq!(editor.content.len(), initial_lines + 1);
635 assert_eq!(editor.cursor_row, 1);
636 assert_eq!(editor.cursor_col, 0);
637 assert!(editor.modified);
638 }
639
640 #[test]
641 fn test_cursor_movement() {
642 let edit = create_test_edit();
643 let mut editor = InlineEditor::new(edit);
644
645 editor.cursor_col = 5;
647 editor.adjust_cursor_bounds();
648
649 editor.cursor_row = 1;
651 editor.adjust_cursor_bounds();
652
653 assert!(editor.cursor_col <= editor.content[editor.cursor_row].len());
654 }
655}