use crate::edit_control::{EditModification, ModifiableEdit, EditModificationInterface, ViewMode, InterfaceState};
use crossterm::{
cursor,
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
execute,
style::{Color, ResetColor, SetForegroundColor},
terminal::{self, ClearType},
};
use std::io::{self, stdout, Write};
pub struct InlineEditor {
interface: EditModificationInterface,
content: Vec<String>,
cursor_row: usize,
cursor_col: usize,
scroll_offset: usize,
modified: bool,
show_help: bool,
show_history: bool,
}
impl InlineEditor {
pub fn new(modifiable_edit: ModifiableEdit) -> Self {
let interface = EditModificationInterface::new(modifiable_edit.clone());
let content: Vec<String> = modifiable_edit
.base_edit
.new_code
.lines()
.map(|s| s.to_string())
.collect();
Self {
interface,
content,
cursor_row: 0,
cursor_col: 0,
scroll_offset: 0,
modified: false,
show_help: false,
show_history: false,
}
}
pub fn edit(&mut self) -> io::Result<Option<EditModificationInterface>> {
terminal::enable_raw_mode()?;
let result = loop {
self.render()?;
if let Event::Key(key_event) = event::read()? {
match self.handle_key(key_event)? {
EditorAction::Exit => {
break Ok(None);
}
EditorAction::Save => {
break Ok(Some(self.interface.clone()));
}
EditorAction::Continue => {}
}
}
};
terminal::disable_raw_mode()?;
result
}
fn render(&self) -> io::Result<()> {
execute!(
stdout(),
terminal::Clear(ClearType::All),
cursor::MoveTo(0, 0)
)?;
self.render_header()?;
match self.interface.view_mode {
ViewMode::Single => self.render_single_view()?,
ViewMode::SideBySide => self.render_side_by_side_view()?,
ViewMode::Unified => self.render_unified_diff_view()?,
ViewMode::FullScreen => self.render_fullscreen_view()?,
}
self.render_footer()?;
self.position_cursor()?;
stdout().flush()
}
fn render_header(&self) -> io::Result<()> {
execute!(stdout(), SetForegroundColor(Color::Cyan))?;
println!("🚀 SOMA EditModificationInterface v2.0 - Issue #17");
execute!(stdout(), ResetColor)?;
println!(
"📁 File: {} | Lines: {}-{} | State: {:?} | View: {:?}",
self.interface.modifiable_edit.base_edit.file,
self.interface.modifiable_edit.base_edit.line_range.0,
self.interface.modifiable_edit.base_edit.line_range.1,
self.interface.interface_state,
self.interface.view_mode
);
if self.show_help {
self.render_help_panel()?;
}
if self.show_history {
self.render_history_panel()?;
}
println!(); Ok(())
}
fn render_help_panel(&self) -> io::Result<()> {
execute!(stdout(), SetForegroundColor(Color::Yellow))?;
println!("┌─ Help Panel ─────────────────────────────────────────────────────────┐");
println!("│ Ctrl+S: Save Ctrl+Q: Quit Ctrl+Z: Undo Ctrl+Y: Redo │");
println!("│ Ctrl+H: Help Ctrl+R: History Ctrl+V: View Mode Ctrl+T: Syntax │");
println!("│ Ctrl+C: Compare Ctrl+P: Preview Arrow Keys: Navigate │");
println!("└──────────────────────────────────────────────────────────────────────┘");
execute!(stdout(), ResetColor)?;
Ok(())
}
fn render_history_panel(&self) -> io::Result<()> {
execute!(stdout(), SetForegroundColor(Color::Green))?;
println!("┌─ Modification History ───────────────────────────────────────────────┐");
let history = self.interface.get_history_summary();
for (_i, entry) in history.iter().take(5).enumerate() {
println!("│ {:<68} │", entry);
}
if history.len() > 5 {
println!("│ ... and {} more entries │", history.len() - 5);
}
println!("└──────────────────────────────────────────────────────────────────────┘");
execute!(stdout(), ResetColor)?;
Ok(())
}
fn render_single_view(&self) -> io::Result<()> {
let visible_lines = if self.show_help || self.show_history { 15 } else { 20 };
let start = self.scroll_offset;
let end = (start + visible_lines).min(self.content.len());
for (idx, line) in self.content[start..end].iter().enumerate() {
let line_num = start + idx + 1;
let marker = if start + idx == self.cursor_row { ">" } else { " " };
if self.interface.syntax_highlighting {
self.render_syntax_highlighted_line(line_num, marker, line)?;
} else {
println!("{:3} {} {}", line_num, marker, line);
}
}
Ok(())
}
fn render_side_by_side_view(&self) -> io::Result<()> {
let original_lines: Vec<&str> = self.interface.modifiable_edit.base_edit.new_code.lines().collect();
let current_lines = &self.content;
execute!(stdout(), SetForegroundColor(Color::Blue))?;
println!("┌─ Original ──────────────────────┬─ Modified ──────────────────────┐");
execute!(stdout(), ResetColor)?;
let max_lines = original_lines.len().max(current_lines.len());
let visible_lines = 15;
let start = self.scroll_offset;
let end = (start + visible_lines).min(max_lines);
for i in start..end {
let orig = original_lines.get(i).unwrap_or(&"");
let curr = current_lines.get(i).map(|s| s.as_str()).unwrap_or("");
let marker = if i == self.cursor_row { ">" } else { " " };
if *orig != curr {
execute!(stdout(), SetForegroundColor(Color::Red))?;
}
println!("│{:3}{} {:<26}│{:3}{} {:<26}│",
i + 1, marker, orig.chars().take(26).collect::<String>(),
i + 1, marker, curr.chars().take(26).collect::<String>());
execute!(stdout(), ResetColor)?;
}
println!("└─────────────────────────────────┴─────────────────────────────────┘");
Ok(())
}
fn render_unified_diff_view(&self) -> io::Result<()> {
execute!(stdout(), SetForegroundColor(Color::Magenta))?;
println!("┌─ Unified Diff View ──────────────────────────────────────────────────┐");
execute!(stdout(), ResetColor)?;
let diff_summary = self.interface.get_diff_summary();
execute!(stdout(), SetForegroundColor(Color::Green))?;
println!("│ +{} lines added, -{} removed, ~{} modified │",
diff_summary.lines_added, diff_summary.lines_removed, diff_summary.lines_modified);
execute!(stdout(), ResetColor)?;
let original_lines: Vec<&str> = self.interface.modifiable_edit.base_edit.new_code.lines().collect();
for (i, line) in self.content.iter().enumerate() {
let orig = original_lines.get(i).unwrap_or(&"");
if line != orig {
execute!(stdout(), SetForegroundColor(Color::Red))?;
println!("│ -{:<66} │", orig.chars().take(66).collect::<String>());
execute!(stdout(), SetForegroundColor(Color::Green))?;
println!("│ +{:<66} │", line.chars().take(66).collect::<String>());
execute!(stdout(), ResetColor)?;
} else {
println!("│ {:<66} │", line.chars().take(66).collect::<String>());
}
}
println!("└──────────────────────────────────────────────────────────────────────┘");
Ok(())
}
fn render_fullscreen_view(&self) -> io::Result<()> {
let visible_lines = 25;
let start = self.scroll_offset;
let end = (start + visible_lines).min(self.content.len());
for (idx, line) in self.content[start..end].iter().enumerate() {
let line_num = start + idx + 1;
let marker = if start + idx == self.cursor_row { ">" } else { " " };
println!("{:4} {} {}", line_num, marker, line);
}
Ok(())
}
fn render_syntax_highlighted_line(&self, line_num: usize, marker: &str, line: &str) -> io::Result<()> {
print!("{:3} {} ", line_num, marker);
let mut in_string = false;
let mut in_comment = false;
let mut chars = line.chars().peekable();
while let Some(ch) = chars.next() {
if in_comment {
execute!(stdout(), SetForegroundColor(Color::DarkGreen))?;
print!("{}", ch);
} else if in_string {
execute!(stdout(), SetForegroundColor(Color::Yellow))?;
print!("{}", ch);
if ch == '"' && chars.peek() != Some(&'\\') {
in_string = false;
execute!(stdout(), ResetColor)?;
}
} else {
match ch {
'/' if chars.peek() == Some(&'/') => {
in_comment = true;
execute!(stdout(), SetForegroundColor(Color::DarkGreen))?;
print!("{}", ch);
}
'"' => {
in_string = true;
execute!(stdout(), SetForegroundColor(Color::Yellow))?;
print!("{}", ch);
}
'f' if line.starts_with("fn ") => {
execute!(stdout(), SetForegroundColor(Color::Blue))?;
print!("{}", ch);
}
_ => {
execute!(stdout(), ResetColor)?;
print!("{}", ch);
}
}
}
}
execute!(stdout(), ResetColor)?;
println!();
Ok(())
}
fn render_footer(&self) -> io::Result<()> {
let diff_summary = self.interface.get_diff_summary();
execute!(stdout(), SetForegroundColor(Color::Cyan))?;
println!("─────────────────────────────────────────────────────────────────────────");
execute!(stdout(), ResetColor)?;
println!(
"Cursor: {}:{} | Modified: {} | Changes: {} | Undo: {} | Redo: {} | Syntax: {}",
self.cursor_row + 1,
self.cursor_col + 1,
if self.modified { "YES" } else { "NO" },
diff_summary.total_changes,
if self.interface.can_undo() { "YES" } else { "NO" },
if self.interface.can_redo() { "YES" } else { "NO" },
if self.interface.syntax_highlighting { "ON" } else { "OFF" }
);
if let Some(desc) = self.interface.current_snapshot_description() {
println!("Current: {}", desc);
}
Ok(())
}
fn position_cursor(&self) -> io::Result<()> {
let header_offset = if self.show_help { 9 } else if self.show_history { 8 } else { 4 };
let display_row = header_offset + self.cursor_row - self.scroll_offset;
let display_col = match self.interface.view_mode {
ViewMode::SideBySide => self.cursor_col + 38, _ => self.cursor_col + 6, };
execute!(
stdout(),
cursor::MoveTo(display_col as u16, display_row as u16)
)?;
Ok(())
}
fn handle_key(&mut self, key: KeyEvent) -> io::Result<EditorAction> {
match (key.code, key.modifiers) {
(KeyCode::Char('q'), KeyModifiers::CONTROL) => Ok(EditorAction::Exit),
(KeyCode::Char('s'), KeyModifiers::CONTROL) => {
self.save_current_changes();
Ok(EditorAction::Save)
},
(KeyCode::Char('z'), KeyModifiers::CONTROL) => {
if self.interface.undo() {
self.update_content_from_interface();
}
Ok(EditorAction::Continue)
},
(KeyCode::Char('y'), KeyModifiers::CONTROL) => {
if self.interface.redo() {
self.update_content_from_interface();
}
Ok(EditorAction::Continue)
},
(KeyCode::Char('h'), KeyModifiers::CONTROL) => {
self.show_help = !self.show_help;
Ok(EditorAction::Continue)
},
(KeyCode::Char('r'), KeyModifiers::CONTROL) => {
self.show_history = !self.show_history;
Ok(EditorAction::Continue)
},
(KeyCode::Char('t'), KeyModifiers::CONTROL) => {
self.interface.toggle_syntax_highlighting();
Ok(EditorAction::Continue)
},
(KeyCode::Char('v'), KeyModifiers::CONTROL) => {
self.cycle_view_mode();
Ok(EditorAction::Continue)
},
(KeyCode::Char('c'), KeyModifiers::CONTROL) => {
self.interface.set_state(InterfaceState::Comparing);
self.interface.set_view_mode(ViewMode::SideBySide);
Ok(EditorAction::Continue)
},
(KeyCode::Char('p'), KeyModifiers::CONTROL) => {
self.interface.set_state(InterfaceState::Previewing);
self.interface.set_view_mode(ViewMode::Unified);
Ok(EditorAction::Continue)
},
(KeyCode::Up, _) => {
if self.cursor_row > 0 {
self.cursor_row -= 1;
self.adjust_cursor_bounds();
self.adjust_scroll();
}
Ok(EditorAction::Continue)
}
(KeyCode::Down, _) => {
if self.cursor_row < self.content.len().saturating_sub(1) {
self.cursor_row += 1;
self.adjust_cursor_bounds();
self.adjust_scroll();
}
Ok(EditorAction::Continue)
}
(KeyCode::Left, _) => {
if self.cursor_col > 0 {
self.cursor_col -= 1;
} else if self.cursor_row > 0 {
self.cursor_row -= 1;
self.cursor_col = self
.content
.get(self.cursor_row)
.map(|s| s.len())
.unwrap_or(0);
self.adjust_scroll();
}
Ok(EditorAction::Continue)
}
(KeyCode::Right, _) => {
let line_len = self
.content
.get(self.cursor_row)
.map(|s| s.len())
.unwrap_or(0);
if self.cursor_col < line_len {
self.cursor_col += 1;
} else if self.cursor_row < self.content.len().saturating_sub(1) {
self.cursor_row += 1;
self.cursor_col = 0;
self.adjust_scroll();
}
Ok(EditorAction::Continue)
}
(KeyCode::Home, _) => {
self.cursor_col = 0;
Ok(EditorAction::Continue)
}
(KeyCode::End, _) => {
self.cursor_col = self
.content
.get(self.cursor_row)
.map(|s| s.len())
.unwrap_or(0);
Ok(EditorAction::Continue)
}
(KeyCode::Enter, _) => {
self.insert_newline();
Ok(EditorAction::Continue)
}
(KeyCode::Char(c), _) => {
self.insert_char(c);
Ok(EditorAction::Continue)
}
(KeyCode::Backspace, _) => {
self.delete_char();
Ok(EditorAction::Continue)
}
(KeyCode::Delete, _) => {
self.delete_char_forward();
Ok(EditorAction::Continue)
}
_ => Ok(EditorAction::Continue),
}
}
fn cycle_view_mode(&mut self) {
let new_mode = match self.interface.view_mode {
ViewMode::Single => ViewMode::SideBySide,
ViewMode::SideBySide => ViewMode::Unified,
ViewMode::Unified => ViewMode::FullScreen,
ViewMode::FullScreen => ViewMode::Single,
};
self.interface.set_view_mode(new_mode);
}
fn save_current_changes(&mut self) {
if self.modified {
let new_content = self.content.join("\n");
let old_content = self.interface.modifiable_edit.compute_final_code();
if new_content != old_content {
self.interface.add_modification(
EditModification::CodeChange {
line: 0,
old: old_content,
new: new_content,
},
"Manual edit changes".to_string(),
);
self.modified = false;
}
}
}
fn update_content_from_interface(&mut self) {
let new_content = self.interface.modifiable_edit.compute_final_code();
self.content = new_content.lines().map(|s| s.to_string()).collect();
self.modified = false;
}
fn adjust_cursor_bounds(&mut self) {
if let Some(line) = self.content.get(self.cursor_row) {
if self.cursor_col > line.len() {
self.cursor_col = line.len();
}
}
}
fn adjust_scroll(&mut self) {
let visible_lines = 20;
if self.cursor_row < self.scroll_offset {
self.scroll_offset = self.cursor_row;
} else if self.cursor_row >= self.scroll_offset + visible_lines {
self.scroll_offset = self.cursor_row - visible_lines + 1;
}
}
fn insert_char(&mut self, c: char) {
if self.cursor_row < self.content.len() {
self.content[self.cursor_row].insert(self.cursor_col, c);
self.cursor_col += 1;
self.modified = true;
}
}
fn insert_newline(&mut self) {
if self.cursor_row < self.content.len() {
let current_line = self.content[self.cursor_row].clone();
let (left, right) = current_line.split_at(self.cursor_col);
self.content[self.cursor_row] = left.to_string();
self.content.insert(self.cursor_row + 1, right.to_string());
self.cursor_row += 1;
self.cursor_col = 0;
self.modified = true;
self.adjust_scroll();
}
}
fn delete_char(&mut self) {
if self.cursor_col > 0 && self.cursor_row < self.content.len() {
self.content[self.cursor_row].remove(self.cursor_col - 1);
self.cursor_col -= 1;
self.modified = true;
} else if self.cursor_row > 0 {
let current_line = self.content.remove(self.cursor_row);
self.cursor_row -= 1;
self.cursor_col = self.content[self.cursor_row].len();
self.content[self.cursor_row].push_str(¤t_line);
self.modified = true;
self.adjust_scroll();
}
}
fn delete_char_forward(&mut self) {
if self.cursor_row < self.content.len() {
let line_len = self.content[self.cursor_row].len();
if self.cursor_col < line_len {
self.content[self.cursor_row].remove(self.cursor_col);
self.modified = true;
} else if self.cursor_row < self.content.len() - 1 {
let next_line = self.content.remove(self.cursor_row + 1);
self.content[self.cursor_row].push_str(&next_line);
self.modified = true;
}
}
}
}
#[derive(Debug)]
enum EditorAction {
Continue,
Save,
Exit,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agents::gpt4_agent::ProposedEdit;
fn create_test_edit() -> ModifiableEdit {
let proposed = ProposedEdit {
file: "test.rs".to_string(),
line_range: (10, 15),
new_code: "fn test() {\n println!(\"Hello\");\n}".to_string(),
reason: "Test function".to_string(),
confidence: 0.9,
};
ModifiableEdit::from_proposed_edit(proposed)
}
#[test]
fn test_editor_creation() {
let edit = create_test_edit();
let editor = InlineEditor::new(edit);
assert_eq!(editor.content.len(), 3);
assert_eq!(editor.cursor_row, 0);
assert_eq!(editor.cursor_col, 0);
assert!(!editor.modified);
}
#[test]
fn test_text_insertion() {
let edit = create_test_edit();
let mut editor = InlineEditor::new(edit);
editor.insert_char('X');
assert!(editor.modified);
assert!(editor.content[0].starts_with('X'));
assert_eq!(editor.cursor_col, 1);
}
#[test]
fn test_newline_insertion() {
let edit = create_test_edit();
let mut editor = InlineEditor::new(edit);
let initial_lines = editor.content.len();
editor.insert_newline();
assert_eq!(editor.content.len(), initial_lines + 1);
assert_eq!(editor.cursor_row, 1);
assert_eq!(editor.cursor_col, 0);
assert!(editor.modified);
}
#[test]
fn test_cursor_movement() {
let edit = create_test_edit();
let mut editor = InlineEditor::new(edit);
editor.cursor_col = 5;
editor.adjust_cursor_bounds();
editor.cursor_row = 1;
editor.adjust_cursor_bounds();
assert!(editor.cursor_col <= editor.content[editor.cursor_row].len());
}
}