use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::layout::Rect;
use crate::app::App;
use crate::runtime::EventResult;
use crate::ui::component::file_explorer::FileExplorer;
use crate::ui::page;
use crate::ui::state::app_mode::{AppMode, DiffScrollCache, HelpContext};
pub(crate) fn handle(app: &mut App, content_area: Rect, key: KeyEvent) -> EventResult {
if handle_help_key(app, key) {
return EventResult::Continue;
}
if handle_exit_key(app, key) {
return EventResult::Continue;
}
if handle_toggle_panel_key(app, key) {
return EventResult::Continue;
}
handle_navigation_key(app, content_area, key);
EventResult::Continue
}
fn handle_toggle_panel_key(app: &mut App, key: KeyEvent) -> bool {
if !is_plain_char_key(key, 'c') {
return false;
}
if let AppMode::Diff { right_panel, .. } = &mut app.mode {
*right_panel = right_panel.toggled();
return true;
}
false
}
fn handle_help_key(app: &mut App, key: KeyEvent) -> bool {
if key.code != KeyCode::Char('?') {
return false;
}
let mode = std::mem::replace(&mut app.mode, AppMode::List);
if let AppMode::Diff {
diff,
file_explorer_selected_index,
restore_question,
right_panel,
session_id,
scroll_offset,
..
} = mode
{
app.mode = AppMode::Help {
context: HelpContext::Diff {
diff,
file_explorer_selected_index,
restore_question,
right_panel,
session_id,
scroll_offset,
},
scroll_offset: 0,
};
} else {
app.mode = mode;
}
true
}
fn handle_exit_key(app: &mut App, key: KeyEvent) -> bool {
if !matches!(key.code, KeyCode::Char('q') | KeyCode::Esc) {
return false;
}
let mode = std::mem::replace(&mut app.mode, AppMode::List);
if let AppMode::Diff {
restore_question,
session_id,
..
} = mode
{
app.mode = if let Some(snapshot) = restore_question {
snapshot.into_question_mode()
} else {
let (review_status_message, review_text) = app.review_view_state(&session_id);
AppMode::View {
review_status_message,
review_text,
session_id,
scroll_offset: None,
}
};
} else {
app.mode = mode;
}
true
}
fn handle_navigation_key(app: &mut App, content_area: Rect, key: KeyEvent) {
let mode = std::mem::replace(&mut app.mode, AppMode::List);
let AppMode::Diff {
diff,
mut file_explorer_selected_index,
restore_question,
right_panel,
mut scroll_cache,
mut scroll_offset,
session_id,
} = mode
else {
app.mode = mode;
return;
};
match key.code {
KeyCode::Char('j') if is_plain_char_key(key, 'j') => {
let content = app.render_cache_store().diff_layout_cache().content(&diff);
let new_index = FileExplorer::next_selected_index(
file_explorer_selected_index,
content.item_count(),
);
if file_explorer_selected_index != new_index {
file_explorer_selected_index = new_index;
scroll_cache = None;
scroll_offset = 0;
}
}
KeyCode::Char('k') if is_plain_char_key(key, 'k') => {
let content = app.render_cache_store().diff_layout_cache().content(&diff);
let new_index = FileExplorer::previous_selected_index(
file_explorer_selected_index,
content.item_count(),
);
if file_explorer_selected_index != new_index {
file_explorer_selected_index = new_index;
scroll_cache = None;
scroll_offset = 0;
}
}
KeyCode::Down | KeyCode::Char('J' | 'j')
if key.code == KeyCode::Down || is_shift_char_key(key, 'j') =>
{
let review_comment_snapshot = app.services.review_comment_cache().snapshot(&session_id);
let max_scroll_offset = diff_max_scroll_offset(
&diff,
content_area,
file_explorer_selected_index,
&mut scroll_cache,
review_comment_snapshot.as_ref(),
app.render_cache_store().markdown_render_cache(),
app.render_cache_store().diff_layout_cache(),
);
let clamped_scroll_offset = scroll_offset.min(max_scroll_offset);
scroll_offset = clamped_scroll_offset
.saturating_add(1)
.min(max_scroll_offset);
}
KeyCode::Up | KeyCode::Char('K' | 'k')
if key.code == KeyCode::Up || is_shift_char_key(key, 'k') =>
{
let review_comment_snapshot = app.services.review_comment_cache().snapshot(&session_id);
let max_scroll_offset = diff_max_scroll_offset(
&diff,
content_area,
file_explorer_selected_index,
&mut scroll_cache,
review_comment_snapshot.as_ref(),
app.render_cache_store().markdown_render_cache(),
app.render_cache_store().diff_layout_cache(),
);
let clamped_scroll_offset = scroll_offset.min(max_scroll_offset);
scroll_offset = clamped_scroll_offset.saturating_sub(1);
}
_ => {}
}
app.mode = AppMode::Diff {
diff,
file_explorer_selected_index,
restore_question,
right_panel,
scroll_cache,
scroll_offset,
session_id,
};
}
fn is_plain_char_key(key: KeyEvent, character: char) -> bool {
key.code == KeyCode::Char(character) && key.modifiers == KeyModifiers::NONE
}
fn is_shift_char_key(key: KeyEvent, character: char) -> bool {
let lowercase_character = character.to_ascii_lowercase();
let uppercase_character = character.to_ascii_uppercase();
key.modifiers == KeyModifiers::SHIFT
&& matches!(
key.code,
KeyCode::Char(pressed)
if pressed == lowercase_character || pressed == uppercase_character
)
}
fn diff_max_scroll_offset(
diff: &str,
content_area: Rect,
selected_index: usize,
scroll_cache: &mut Option<DiffScrollCache>,
review_comment_snapshot: Option<
&crate::infra::review_comment_cache::CachedReviewCommentSnapshot,
>,
markdown_render_cache: &crate::ui::markdown::MarkdownRenderCache,
diff_layout_cache: &page::diff::DiffLayoutCache,
) -> u16 {
if let Some(cached_scroll_limit) = scroll_cache
&& cached_scroll_limit.content_area == content_area
&& cached_scroll_limit.file_explorer_selected_index == selected_index
{
return cached_scroll_limit.max_scroll_offset;
}
let max_scroll_offset = page::diff::diff_view_max_scroll_offset_with_comments(
diff,
selected_index,
content_area,
review_comment_snapshot,
markdown_render_cache,
diff_layout_cache,
);
*scroll_cache = Some(DiffScrollCache {
content_area,
file_explorer_selected_index: selected_index,
max_scroll_offset,
});
max_scroll_offset
}
#[cfg(test)]
mod tests {
use ag_forge::{
ReviewComment, ReviewCommentAnchorSide, ReviewCommentSnapshot, ReviewCommentThread,
};
use crossterm::event::KeyModifiers;
use ratatui::layout::Rect;
use tempfile::tempdir;
use super::*;
use crate::db::Database;
use crate::ui::markdown;
const TEST_TERMINAL_SIZE: Rect = Rect::new(0, 0, 80, 12);
fn test_app_clients() -> crate::app::AppClients {
crate::app::AppClients::new().with_agent_availability_probe(std::sync::Arc::new(
crate::infra::agent::StaticAgentAvailabilityProbe {
available_agent_kinds: crate::domain::agent::AgentKind::ALL.to_vec(),
},
))
}
async fn new_test_app() -> (App, tempfile::TempDir) {
let base_dir = tempdir().expect("failed to create temp dir");
let base_path = base_dir.path().to_path_buf();
let database = Database::open_in_memory()
.await
.expect("failed to open in-memory db");
let app = App::new_with_clients(
base_path.clone(),
base_path,
None,
database,
test_app_clients(),
)
.await
.expect("failed to build app");
(app, base_dir)
}
fn scrollable_diff_fixture() -> String {
(0..40)
.map(|index| format!("+line {index}"))
.collect::<Vec<_>>()
.join("\n")
}
fn review_comment_snapshot() -> ReviewCommentSnapshot {
ReviewCommentSnapshot {
pr_level_comments: Vec::new(),
threads: vec![ReviewCommentThread {
anchor_side: ReviewCommentAnchorSide::New,
comments: vec![ReviewComment {
author: "alice".to_string(),
body: "This inline comment adds extra rendered rows.".to_string(),
}],
is_outdated: Some(false),
is_resolved: false,
line: Some(1),
path: "src/main.rs".to_string(),
start_line: None,
}],
}
}
fn cached_review_comment_snapshot(
snapshot: ReviewCommentSnapshot,
) -> crate::infra::review_comment_cache::CachedReviewCommentSnapshot {
let cache = crate::infra::review_comment_cache::ReviewCommentCache::default();
let session_id: crate::domain::session::SessionId = "scroll-comments".into();
cache.record_snapshot(session_id.clone(), snapshot);
cache
.snapshot(&session_id)
.expect("test snapshot should be cached")
}
#[tokio::test]
async fn test_handle_quit_key_returns_to_view_mode() {
let (mut app, _base_dir) = new_test_app().await;
app.mode = AppMode::Diff {
session_id: "session-id".into(),
diff: "diff output".to_string(),
scroll_offset: 7,
file_explorer_selected_index: 0,
restore_question: None,
scroll_cache: None,
right_panel: crate::ui::state::app_mode::DiffRightPanel::Diff,
};
let event_result = handle(
&mut app,
TEST_TERMINAL_SIZE,
KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE),
);
assert!(matches!(event_result, EventResult::Continue));
assert!(matches!(
app.mode,
AppMode::View {
ref session_id,
scroll_offset: None,
..
} if session_id == "session-id"
));
}
#[tokio::test]
async fn test_handle_quit_key_restores_cached_review_output() {
let (mut app, _base_dir) = new_test_app().await;
app.review_cache.insert(
"session-id".into(),
crate::app::ReviewCacheEntry::Ready {
text: "Focused review".to_string(),
diff_hash: 7,
},
);
app.mode = AppMode::Diff {
session_id: "session-id".into(),
diff: "diff output".to_string(),
scroll_offset: 7,
file_explorer_selected_index: 0,
restore_question: None,
scroll_cache: None,
right_panel: crate::ui::state::app_mode::DiffRightPanel::Diff,
};
let event_result = handle(
&mut app,
TEST_TERMINAL_SIZE,
KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE),
);
assert!(matches!(event_result, EventResult::Continue));
assert!(matches!(
app.mode,
AppMode::View {
ref session_id,
review_status_message: None,
review_text: Some(ref review_text),
scroll_offset: None,
..
} if session_id == "session-id" && review_text == "Focused review"
));
}
#[tokio::test]
async fn test_handle_down_key_increments_scroll_offset() {
let (mut app, _base_dir) = new_test_app().await;
app.mode = AppMode::Diff {
session_id: "session-id".into(),
diff: scrollable_diff_fixture(),
scroll_offset: 0,
file_explorer_selected_index: 0,
restore_question: None,
scroll_cache: None,
right_panel: crate::ui::state::app_mode::DiffRightPanel::Diff,
};
let event_result = handle(
&mut app,
TEST_TERMINAL_SIZE,
KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
);
assert!(matches!(event_result, EventResult::Continue));
assert!(matches!(
app.mode,
AppMode::Diff {
scroll_offset: 1,
..
}
));
}
#[tokio::test]
async fn test_handle_shift_j_increments_scroll_offset() {
let (mut app, _base_dir) = new_test_app().await;
app.mode = AppMode::Diff {
session_id: "session-id".into(),
diff: scrollable_diff_fixture(),
scroll_offset: 3,
file_explorer_selected_index: 2,
restore_question: None,
scroll_cache: None,
right_panel: crate::ui::state::app_mode::DiffRightPanel::Diff,
};
let event_result = handle(
&mut app,
TEST_TERMINAL_SIZE,
KeyEvent::new(KeyCode::Char('J'), KeyModifiers::SHIFT),
);
assert!(matches!(event_result, EventResult::Continue));
assert!(matches!(
app.mode,
AppMode::Diff {
scroll_offset: 4,
file_explorer_selected_index: 2,
..
}
));
}
#[tokio::test]
async fn test_handle_up_key_saturates_scroll_offset_at_zero() {
let (mut app, _base_dir) = new_test_app().await;
app.mode = AppMode::Diff {
session_id: "session-id".into(),
diff: "diff output".to_string(),
scroll_offset: 0,
file_explorer_selected_index: 0,
restore_question: None,
scroll_cache: None,
right_panel: crate::ui::state::app_mode::DiffRightPanel::Diff,
};
let event_result = handle(
&mut app,
TEST_TERMINAL_SIZE,
KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
);
assert!(matches!(event_result, EventResult::Continue));
assert!(matches!(
app.mode,
AppMode::Diff {
scroll_offset: 0,
..
}
));
}
#[tokio::test]
async fn test_handle_shift_k_saturates_scroll_offset_at_zero() {
let (mut app, _base_dir) = new_test_app().await;
app.mode = AppMode::Diff {
session_id: "session-id".into(),
diff: "diff output".to_string(),
scroll_offset: 0,
file_explorer_selected_index: 2,
restore_question: None,
scroll_cache: None,
right_panel: crate::ui::state::app_mode::DiffRightPanel::Diff,
};
let event_result = handle(
&mut app,
TEST_TERMINAL_SIZE,
KeyEvent::new(KeyCode::Char('K'), KeyModifiers::SHIFT),
);
assert!(matches!(event_result, EventResult::Continue));
assert!(matches!(
app.mode,
AppMode::Diff {
scroll_offset: 0,
file_explorer_selected_index: 2,
..
}
));
}
#[tokio::test]
async fn test_handle_non_diff_mode_leaves_mode_unchanged() {
let (mut app, _base_dir) = new_test_app().await;
app.mode = AppMode::List;
let event_result = handle(
&mut app,
TEST_TERMINAL_SIZE,
KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE),
);
assert!(matches!(event_result, EventResult::Continue));
assert!(matches!(app.mode, AppMode::List));
}
#[tokio::test]
async fn test_handle_j_resets_scroll_offset() {
let (mut app, _base_dir) = new_test_app().await;
app.mode = AppMode::Diff {
session_id: "session-id".into(),
diff: "diff --git a/src/main.rs b/src/main.rs\n+added".to_string(),
scroll_offset: 10,
file_explorer_selected_index: 0,
restore_question: None,
scroll_cache: None,
right_panel: crate::ui::state::app_mode::DiffRightPanel::Diff,
};
handle(
&mut app,
TEST_TERMINAL_SIZE,
KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE),
);
assert!(matches!(
app.mode,
AppMode::Diff {
scroll_offset: 0,
file_explorer_selected_index: 1,
..
}
));
}
#[tokio::test]
async fn test_handle_j_wraps_file_selection_from_last_to_first() {
let (mut app, _base_dir) = new_test_app().await;
app.mode = AppMode::Diff {
session_id: "session-id".into(),
diff: "diff --git a/src/main.rs b/src/main.rs\n+added".to_string(),
scroll_offset: 10,
file_explorer_selected_index: 1,
restore_question: None,
scroll_cache: None,
right_panel: crate::ui::state::app_mode::DiffRightPanel::Diff,
};
handle(
&mut app,
TEST_TERMINAL_SIZE,
KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE),
);
assert!(matches!(
app.mode,
AppMode::Diff {
scroll_offset: 0,
file_explorer_selected_index: 0,
..
}
));
}
#[tokio::test]
async fn test_handle_k_resets_scroll_offset() {
let (mut app, _base_dir) = new_test_app().await;
app.mode = AppMode::Diff {
session_id: "session-id".into(),
diff: "diff --git a/src/main.rs b/src/main.rs\n+added".to_string(),
scroll_offset: 10,
file_explorer_selected_index: 1,
restore_question: None,
scroll_cache: None,
right_panel: crate::ui::state::app_mode::DiffRightPanel::Diff,
};
handle(
&mut app,
TEST_TERMINAL_SIZE,
KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE),
);
assert!(matches!(
app.mode,
AppMode::Diff {
scroll_offset: 0,
file_explorer_selected_index: 0,
..
}
));
}
#[tokio::test]
async fn test_handle_k_wraps_file_selection_from_first_to_last() {
let (mut app, _base_dir) = new_test_app().await;
app.mode = AppMode::Diff {
session_id: "session-id".into(),
diff: "diff --git a/src/main.rs b/src/main.rs\n+added".to_string(),
scroll_offset: 10,
file_explorer_selected_index: 0,
restore_question: None,
scroll_cache: None,
right_panel: crate::ui::state::app_mode::DiffRightPanel::Diff,
};
handle(
&mut app,
TEST_TERMINAL_SIZE,
KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE),
);
assert!(matches!(
app.mode,
AppMode::Diff {
scroll_offset: 0,
file_explorer_selected_index: 1,
..
}
));
}
#[tokio::test]
async fn test_handle_question_mark_opens_help_overlay() {
let (mut app, _base_dir) = new_test_app().await;
app.mode = AppMode::Diff {
session_id: "session-id".into(),
diff: "diff output".to_string(),
scroll_offset: 5,
file_explorer_selected_index: 3,
restore_question: None,
scroll_cache: None,
right_panel: crate::ui::state::app_mode::DiffRightPanel::Diff,
};
let event_result = handle(
&mut app,
TEST_TERMINAL_SIZE,
KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE),
);
assert!(matches!(event_result, EventResult::Continue));
assert!(matches!(
app.mode,
AppMode::Help {
context: HelpContext::Diff {
ref session_id,
ref diff,
scroll_offset: 5,
file_explorer_selected_index: 3,
..
},
scroll_offset: 0,
} if session_id == "session-id" && diff == "diff output"
));
}
#[tokio::test]
async fn test_handle_down_key_clamps_scroll_offset_at_bottom() {
let (mut app, _base_dir) = new_test_app().await;
let diff = scrollable_diff_fixture();
let markdown_render_cache = markdown::MarkdownRenderCache::default();
let diff_layout_cache = page::diff::DiffLayoutCache::default();
let max_scroll_offset = diff_max_scroll_offset(
&diff,
TEST_TERMINAL_SIZE,
0,
&mut None,
None,
&markdown_render_cache,
&diff_layout_cache,
);
app.mode = AppMode::Diff {
session_id: "session-id".into(),
diff,
scroll_offset: max_scroll_offset,
file_explorer_selected_index: 0,
restore_question: None,
scroll_cache: None,
right_panel: crate::ui::state::app_mode::DiffRightPanel::Diff,
};
let event_result = handle(
&mut app,
TEST_TERMINAL_SIZE,
KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
);
assert!(matches!(event_result, EventResult::Continue));
assert!(matches!(
app.mode,
AppMode::Diff {
scroll_offset,
..
} if scroll_offset == max_scroll_offset
));
}
#[tokio::test]
async fn test_handle_up_key_recovers_immediately_from_overscrolled_state() {
let (mut app, _base_dir) = new_test_app().await;
let diff = scrollable_diff_fixture();
let markdown_render_cache = markdown::MarkdownRenderCache::default();
let diff_layout_cache = page::diff::DiffLayoutCache::default();
let max_scroll_offset = diff_max_scroll_offset(
&diff,
TEST_TERMINAL_SIZE,
0,
&mut None,
None,
&markdown_render_cache,
&diff_layout_cache,
);
app.mode = AppMode::Diff {
session_id: "session-id".into(),
diff,
scroll_offset: u16::MAX,
file_explorer_selected_index: 0,
restore_question: None,
scroll_cache: None,
right_panel: crate::ui::state::app_mode::DiffRightPanel::Diff,
};
let event_result = handle(
&mut app,
TEST_TERMINAL_SIZE,
KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
);
assert!(matches!(event_result, EventResult::Continue));
assert!(matches!(
app.mode,
AppMode::Diff {
scroll_offset,
..
} if scroll_offset == max_scroll_offset.saturating_sub(1)
));
}
#[test]
fn test_diff_max_scroll_offset_includes_inline_review_comments() {
let diff = concat!(
"diff --git a/src/main.rs b/src/main.rs\n",
"@@ -1 +1 @@\n",
"+new content\n"
);
let snapshot = cached_review_comment_snapshot(review_comment_snapshot());
let markdown_render_cache = markdown::MarkdownRenderCache::default();
let diff_layout_cache = page::diff::DiffLayoutCache::default();
let terminal_size = Rect::new(0, 0, 80, 6);
let without_comments = diff_max_scroll_offset(
diff,
terminal_size,
0,
&mut None,
None,
&markdown_render_cache,
&diff_layout_cache,
);
let with_comments = diff_max_scroll_offset(
diff,
terminal_size,
0,
&mut None,
Some(&snapshot),
&markdown_render_cache,
&diff_layout_cache,
);
assert!(with_comments > without_comments);
}
#[tokio::test]
async fn test_handle_quit_with_question_snapshot_restores_question_mode() {
use crate::domain::input::InputState;
use crate::domain::question::QuestionItem;
use crate::ui::state::app_mode::QuestionModeSnapshot;
let (mut app, _base_dir) = new_test_app().await;
app.mode = AppMode::Diff {
session_id: "session-q".into(),
diff: "diff output".to_string(),
scroll_offset: 0,
file_explorer_selected_index: 0,
restore_question: Some(QuestionModeSnapshot {
at_mention_state: None,
current_index: 0,
input: InputState::default(),
questions: vec![QuestionItem {
options: Vec::new(),
text: "Q?".to_string(),
}],
review_status_message: None,
review_text: None,
responses: Vec::new(),
scroll_offset: None,
selected_option_index: None,
session_id: "session-q".into(),
}),
scroll_cache: None,
right_panel: crate::ui::state::app_mode::DiffRightPanel::Diff,
};
let event_result = handle(
&mut app,
TEST_TERMINAL_SIZE,
KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
);
assert!(matches!(event_result, EventResult::Continue));
assert!(matches!(
app.mode,
AppMode::Question {
ref session_id,
focus: crate::ui::state::app_mode::QuestionFocus::Answer,
..
} if session_id == "session-q"
));
}
#[tokio::test]
async fn test_handle_question_then_help_then_exit_preserves_restore_question() {
use crate::domain::input::InputState;
use crate::domain::question::QuestionItem;
use crate::ui::state::app_mode::QuestionModeSnapshot;
let (mut app, _base_dir) = new_test_app().await;
let snapshot = QuestionModeSnapshot {
at_mention_state: None,
current_index: 1,
input: InputState::default(),
questions: vec![
QuestionItem {
options: Vec::new(),
text: "Q1?".to_string(),
},
QuestionItem {
options: Vec::new(),
text: "Q2?".to_string(),
},
],
review_status_message: None,
review_text: None,
responses: vec!["answer-1".to_string()],
scroll_offset: None,
selected_option_index: None,
session_id: "session-q".into(),
};
app.mode = AppMode::Diff {
session_id: "session-q".into(),
diff: "diff output".to_string(),
scroll_offset: 3,
file_explorer_selected_index: 1,
restore_question: Some(snapshot),
scroll_cache: None,
right_panel: crate::ui::state::app_mode::DiffRightPanel::Diff,
};
handle(
&mut app,
TEST_TERMINAL_SIZE,
KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE),
);
assert!(matches!(
app.mode,
AppMode::Help {
context: HelpContext::Diff {
restore_question: Some(_),
..
},
..
}
));
crate::runtime::mode::help::handle(
&mut app,
KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
);
assert!(matches!(
app.mode,
AppMode::Diff {
restore_question: Some(_),
..
}
));
handle(
&mut app,
TEST_TERMINAL_SIZE,
KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE),
);
assert!(matches!(
app.mode,
AppMode::Question {
ref session_id,
current_index: 1,
focus: crate::ui::state::app_mode::QuestionFocus::Answer,
..
} if session_id == "session-q"
));
}
#[tokio::test]
async fn test_handle_c_toggles_diff_right_panel() {
use crate::ui::state::app_mode::DiffRightPanel;
let (mut app, _base_dir) = new_test_app().await;
app.mode = AppMode::Diff {
session_id: "session-id".into(),
diff: "diff output".to_string(),
scroll_offset: 0,
file_explorer_selected_index: 0,
restore_question: None,
scroll_cache: None,
right_panel: DiffRightPanel::Diff,
};
handle(
&mut app,
TEST_TERMINAL_SIZE,
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE),
);
let first_panel_is_comments = matches!(
app.mode,
AppMode::Diff {
right_panel: DiffRightPanel::Comments,
..
}
);
assert!(first_panel_is_comments);
handle(
&mut app,
TEST_TERMINAL_SIZE,
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE),
);
let second_panel_is_diff = matches!(
app.mode,
AppMode::Diff {
right_panel: DiffRightPanel::Diff,
..
}
);
assert!(second_panel_is_diff);
}
}