use crate::model::composite_buffer::CompositeBuffer;
use crate::model::event::BufferId;
use crate::view::composite_view::CompositeViewState;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
#[derive(Debug, Clone)]
pub enum RoutedEvent {
CompositeScroll(ScrollAction),
SwitchPane(Direction),
NavigateHunk(Direction),
ToSourceBuffer {
buffer_id: BufferId,
action: BufferAction,
},
PaneCursor(CursorAction),
Selection(SelectionAction),
Yank,
Blocked(&'static str),
Close,
Unhandled,
}
#[derive(Debug, Clone, Copy)]
pub enum SelectionAction {
StartVisual,
StartVisualLine,
ClearSelection,
ExtendUp,
ExtendDown,
ExtendLeft,
ExtendRight,
}
#[derive(Debug, Clone, Copy)]
pub enum ScrollAction {
Up(usize),
Down(usize),
PageUp,
PageDown,
ToTop,
ToBottom,
ToRow(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Next,
Prev,
}
#[derive(Debug, Clone)]
pub enum BufferAction {
Insert(char),
InsertString(String),
Delete,
Backspace,
NewLine,
}
#[derive(Debug, Clone, Copy)]
pub enum CursorAction {
Up,
Down,
Left,
Right,
LineStart,
LineEnd,
WordLeft,
WordRight,
}
pub struct CompositeInputRouter;
impl CompositeInputRouter {
pub fn route_key_event(
composite: &CompositeBuffer,
view_state: &CompositeViewState,
event: &KeyEvent,
) -> RoutedEvent {
let focused_pane = composite.sources.get(view_state.focused_pane);
match (event.modifiers, event.code) {
(KeyModifiers::NONE, KeyCode::Up) | (KeyModifiers::NONE, KeyCode::Char('k')) => {
RoutedEvent::CompositeScroll(ScrollAction::Up(1))
}
(KeyModifiers::NONE, KeyCode::Down) | (KeyModifiers::NONE, KeyCode::Char('j')) => {
RoutedEvent::CompositeScroll(ScrollAction::Down(1))
}
(KeyModifiers::CONTROL, KeyCode::Char('u')) => {
RoutedEvent::CompositeScroll(ScrollAction::PageUp)
}
(KeyModifiers::CONTROL, KeyCode::Char('d')) => {
RoutedEvent::CompositeScroll(ScrollAction::PageDown)
}
(KeyModifiers::NONE, KeyCode::PageUp) => {
RoutedEvent::CompositeScroll(ScrollAction::PageUp)
}
(KeyModifiers::NONE, KeyCode::PageDown) => {
RoutedEvent::CompositeScroll(ScrollAction::PageDown)
}
(KeyModifiers::NONE, KeyCode::Home) | (KeyModifiers::NONE, KeyCode::Char('g')) => {
RoutedEvent::CompositeScroll(ScrollAction::ToTop)
}
(KeyModifiers::SHIFT, KeyCode::Char('G')) | (KeyModifiers::NONE, KeyCode::End) => {
RoutedEvent::CompositeScroll(ScrollAction::ToBottom)
}
(KeyModifiers::NONE, KeyCode::Tab) => RoutedEvent::SwitchPane(Direction::Next),
(KeyModifiers::SHIFT, KeyCode::BackTab) => RoutedEvent::SwitchPane(Direction::Prev),
(KeyModifiers::NONE, KeyCode::Char('h')) => RoutedEvent::SwitchPane(Direction::Prev),
(KeyModifiers::NONE, KeyCode::Char('l')) => RoutedEvent::SwitchPane(Direction::Next),
(KeyModifiers::NONE, KeyCode::Char('n')) => RoutedEvent::NavigateHunk(Direction::Next),
(KeyModifiers::NONE, KeyCode::Char('p')) => RoutedEvent::NavigateHunk(Direction::Prev),
(KeyModifiers::NONE, KeyCode::Char(']')) => RoutedEvent::NavigateHunk(Direction::Next),
(KeyModifiers::NONE, KeyCode::Char('[')) => RoutedEvent::NavigateHunk(Direction::Prev),
(KeyModifiers::NONE, KeyCode::Char('q')) | (KeyModifiers::NONE, KeyCode::Esc) => {
RoutedEvent::Close
}
(KeyModifiers::NONE, KeyCode::Char('v')) => {
RoutedEvent::Selection(SelectionAction::StartVisual)
}
(KeyModifiers::SHIFT, KeyCode::Char('V')) => {
RoutedEvent::Selection(SelectionAction::StartVisualLine)
}
(KeyModifiers::NONE, KeyCode::Char('y')) => RoutedEvent::Yank,
(KeyModifiers::NONE, KeyCode::Char(c)) => {
if let Some(pane) = focused_pane {
if pane.editable {
RoutedEvent::ToSourceBuffer {
buffer_id: pane.buffer_id,
action: BufferAction::Insert(c),
}
} else {
RoutedEvent::Blocked("Pane is read-only")
}
} else {
RoutedEvent::Unhandled
}
}
(KeyModifiers::NONE, KeyCode::Backspace) => {
if let Some(pane) = focused_pane {
if pane.editable {
RoutedEvent::ToSourceBuffer {
buffer_id: pane.buffer_id,
action: BufferAction::Backspace,
}
} else {
RoutedEvent::Blocked("Pane is read-only")
}
} else {
RoutedEvent::Unhandled
}
}
(KeyModifiers::NONE, KeyCode::Delete) => {
if let Some(pane) = focused_pane {
if pane.editable {
RoutedEvent::ToSourceBuffer {
buffer_id: pane.buffer_id,
action: BufferAction::Delete,
}
} else {
RoutedEvent::Blocked("Pane is read-only")
}
} else {
RoutedEvent::Unhandled
}
}
(KeyModifiers::NONE, KeyCode::Enter) => {
if let Some(pane) = focused_pane {
if pane.editable {
RoutedEvent::ToSourceBuffer {
buffer_id: pane.buffer_id,
action: BufferAction::NewLine,
}
} else {
RoutedEvent::Blocked("Pane is read-only")
}
} else {
RoutedEvent::Unhandled
}
}
(KeyModifiers::NONE, KeyCode::Left) => RoutedEvent::PaneCursor(CursorAction::Left),
(KeyModifiers::NONE, KeyCode::Right) => RoutedEvent::PaneCursor(CursorAction::Right),
(KeyModifiers::CONTROL, KeyCode::Left) => {
RoutedEvent::PaneCursor(CursorAction::WordLeft)
}
(KeyModifiers::CONTROL, KeyCode::Right) => {
RoutedEvent::PaneCursor(CursorAction::WordRight)
}
_ => RoutedEvent::Unhandled,
}
}
pub fn display_to_source(
composite: &CompositeBuffer,
_view_state: &CompositeViewState,
display_row: usize,
display_col: usize,
pane_index: usize,
) -> Option<SourceCoordinate> {
let aligned_row = composite.alignment.get_row(display_row)?;
let source_ref = aligned_row.get_pane_line(pane_index)?;
Some(SourceCoordinate {
buffer_id: composite.sources.get(pane_index)?.buffer_id,
byte_offset: source_ref.byte_range.start + display_col,
line: source_ref.line,
column: display_col,
})
}
pub fn click_to_pane(
view_state: &CompositeViewState,
click_x: u16,
area_x: u16,
) -> Option<usize> {
let mut x = area_x;
for (i, &width) in view_state.pane_widths.iter().enumerate() {
if click_x >= x && click_x < x + width {
return Some(i);
}
x += width + 1; }
None
}
pub fn navigate_to_hunk(
composite: &CompositeBuffer,
view_state: &mut CompositeViewState,
direction: Direction,
) -> bool {
let current_row = view_state.scroll_row;
let new_row = match direction {
Direction::Next => composite.alignment.next_hunk_row(current_row),
Direction::Prev => composite.alignment.prev_hunk_row(current_row),
};
if let Some(row) = new_row {
view_state.scroll_row = row;
true
} else {
false
}
}
}
#[derive(Debug, Clone)]
pub struct SourceCoordinate {
pub buffer_id: BufferId,
pub byte_offset: usize,
pub line: usize,
pub column: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::composite_buffer::{CompositeLayout, SourcePane};
fn create_test_composite() -> (CompositeBuffer, CompositeViewState) {
let sources = vec![
SourcePane::new(BufferId(1), "OLD", false),
SourcePane::new(BufferId(2), "NEW", true),
];
let composite = CompositeBuffer::new(
BufferId(0),
"Test Diff".to_string(),
"diff-view".to_string(),
CompositeLayout::default(),
sources,
);
let view_state = CompositeViewState::new(BufferId(0), 2);
(composite, view_state)
}
#[test]
fn test_scroll_routing() {
let (composite, view_state) = create_test_composite();
let event = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
let result = CompositeInputRouter::route_key_event(&composite, &view_state, &event);
matches!(result, RoutedEvent::CompositeScroll(ScrollAction::Down(1)));
}
#[test]
fn test_pane_switch_routing() {
let (composite, view_state) = create_test_composite();
let event = KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE);
let result = CompositeInputRouter::route_key_event(&composite, &view_state, &event);
matches!(result, RoutedEvent::SwitchPane(Direction::Next));
}
#[test]
fn test_readonly_blocking() {
let (composite, view_state) = create_test_composite();
let event = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
let result = CompositeInputRouter::route_key_event(&composite, &view_state, &event);
matches!(result, RoutedEvent::Blocked(_));
}
#[test]
fn test_editable_routing() {
let (composite, mut view_state) = create_test_composite();
view_state.focused_pane = 1;
let event = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
let result = CompositeInputRouter::route_key_event(&composite, &view_state, &event);
matches!(
result,
RoutedEvent::ToSourceBuffer {
buffer_id: BufferId(2),
action: BufferAction::Insert('x'),
}
);
}
}