use anyhow::Result;
use crossterm::event::{self, KeyCode, KeyModifiers};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io::Stdout;
use crate::filter::ListFilter;
use crate::keybinding::{event_to_keybinding, SequenceMatch};
use super::types::*;
use super::{App, AppState};
impl App {
pub(crate) async fn handle_split_view_file_list_input(
&mut self,
key: event::KeyEvent,
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
) -> Result<()> {
if self.handle_filter_input(&key, "file") {
self.sync_diff_to_selected_file();
return Ok(());
}
let kb = self.config.keybindings.clone();
let has_filter = self.file_list_filter.is_some();
if self.matches_single_key(&key, &kb.move_down) || key.code == KeyCode::Down {
if has_filter {
self.handle_filter_navigation("file", true);
} else if !self.files().is_empty() {
self.selected_file =
(self.selected_file + 1).min(self.files().len().saturating_sub(1));
}
self.sync_diff_to_selected_file();
return Ok(());
}
if self.matches_single_key(&key, &kb.move_up) || key.code == KeyCode::Up {
if has_filter {
self.handle_filter_navigation("file", false);
} else if self.selected_file > 0 {
self.selected_file = self.selected_file.saturating_sub(1);
}
self.sync_diff_to_selected_file();
return Ok(());
}
if self.matches_single_key(&key, &kb.page_down) || Self::is_shift_char_shortcut(&key, 'j') {
if !self.files().is_empty() && !has_filter {
let page_step = terminal.size()?.height.saturating_sub(8) as usize;
let step = page_step.max(1);
self.selected_file =
(self.selected_file + step).min(self.files().len().saturating_sub(1));
self.sync_diff_to_selected_file();
}
return Ok(());
}
if self.matches_single_key(&key, &kb.page_up) || Self::is_shift_char_shortcut(&key, 'k') {
if !has_filter {
let page_step = terminal.size()?.height.saturating_sub(8) as usize;
let step = page_step.max(1);
self.selected_file = self.selected_file.saturating_sub(step);
self.sync_diff_to_selected_file();
}
return Ok(());
}
if key.code == KeyCode::Esc {
if self.handle_filter_esc("file") {
return Ok(());
}
self.state = AppState::FileList;
return Ok(());
}
if let Some(kb_event) = event_to_keybinding(&key) {
self.check_sequence_timeout();
if !self.pending_keys.is_empty() {
self.push_pending_key(kb_event);
if self.try_match_sequence(&kb.filter) == SequenceMatch::Full {
self.clear_pending_keys();
if let Some(ref mut filter) = self.file_list_filter {
filter.input_active = true;
} else {
let mut filter = ListFilter::new();
let files = self.files();
filter.apply(files, |_file, _q| true);
if let Some(idx) = filter.sync_selection() {
self.selected_file = idx;
}
self.file_list_filter = Some(filter);
}
return Ok(());
}
self.clear_pending_keys();
} else {
if self.key_could_match_sequence(&key, &kb.filter) {
self.push_pending_key(kb_event);
return Ok(());
}
}
}
if self.matches_single_key(&key, &kb.open_panel)
|| self.matches_single_key(&key, &kb.move_right)
|| key.code == KeyCode::Right
{
if self.is_filter_selection_empty("file") {
return Ok(());
}
if !self.files().is_empty() {
self.state = AppState::SplitViewDiff;
}
return Ok(());
}
if self.matches_single_key(&key, &kb.quit)
|| self.matches_single_key(&key, &kb.move_left)
|| key.code == KeyCode::Left
{
self.state = AppState::FileList;
return Ok(());
}
if self.matches_single_key(&key, &kb.comment_list) {
self.previous_state = AppState::SplitViewFileList;
self.open_comment_list();
return Ok(());
}
if self.matches_single_key(&key, &kb.help) {
self.previous_state = AppState::SplitViewFileList;
self.state = AppState::Help;
self.help_scroll_offset = 0;
self.config_scroll_offset = 0;
return Ok(());
}
self.handle_common_file_list_keys(key, terminal).await?;
Ok(())
}
pub(super) async fn handle_diff_input_common(
&mut self,
key: event::KeyEvent,
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
variant: DiffViewVariant,
) -> Result<()> {
if self.symbol_popup.is_some() {
self.handle_symbol_popup_input(key, terminal).await?;
return Ok(());
}
let term_size = terminal.size()?;
let term_h = term_size.height as usize;
let term_w = term_size.width as usize;
let visible_lines = match variant {
DiffViewVariant::SplitPane => {
(term_h * 65 / 100).saturating_sub(8)
}
DiffViewVariant::Fullscreen => term_h.saturating_sub(8),
};
let panel_inner_width = self.comment_panel_inner_width(term_w);
let kb = self.config.keybindings.clone();
if self.multiline_selection.is_some() {
if self.matches_single_key(&key, &kb.move_down) || key.code == KeyCode::Down {
if self.diff_line_count > 0 {
let new_cursor =
(self.selected_line + 1).min(self.diff_line_count.saturating_sub(1));
self.selected_line = new_cursor;
if let Some(ref mut sel) = self.multiline_selection {
sel.cursor_line = new_cursor;
}
self.adjust_scroll(visible_lines);
}
return Ok(());
}
if self.matches_single_key(&key, &kb.move_up) || key.code == KeyCode::Up {
let new_cursor = self.selected_line.saturating_sub(1);
self.selected_line = new_cursor;
if let Some(ref mut sel) = self.multiline_selection {
sel.cursor_line = new_cursor;
}
self.adjust_scroll(visible_lines);
return Ok(());
}
if self.matches_single_key(&key, &kb.comment) {
self.enter_multiline_comment_input();
return Ok(());
}
if self.matches_single_key(&key, &kb.suggestion) {
self.enter_multiline_suggestion_input();
return Ok(());
}
if self.matches_single_key(&key, &kb.quit) || key.code == KeyCode::Esc {
self.multiline_selection = None;
return Ok(());
}
return Ok(());
}
if self.comment_panel_open {
if self.matches_single_key(&key, &kb.move_down) || key.code == KeyCode::Down {
let max_scroll = self.max_comment_panel_scroll(term_h, term_w);
self.comment_panel_scroll =
self.comment_panel_scroll.saturating_add(1).min(max_scroll);
return Ok(());
}
if self.matches_single_key(&key, &kb.move_up) || key.code == KeyCode::Up {
self.comment_panel_scroll = self.comment_panel_scroll.saturating_sub(1);
return Ok(());
}
if self.matches_single_key(&key, &kb.next_comment) {
let prev_line = self.selected_line;
self.jump_to_next_comment();
if self.selected_line != prev_line {
self.comment_panel_scroll = 0;
self.selected_inline_comment = 0;
self.adjust_scroll(visible_lines);
}
return Ok(());
}
if self.matches_single_key(&key, &kb.prev_comment) {
let prev_line = self.selected_line;
self.jump_to_prev_comment();
if self.selected_line != prev_line {
self.comment_panel_scroll = 0;
self.selected_inline_comment = 0;
self.adjust_scroll(visible_lines);
}
return Ok(());
}
if self.matches_single_key(&key, &kb.comment) {
self.enter_comment_input();
return Ok(());
}
if self.matches_single_key(&key, &kb.suggestion) {
self.enter_suggestion_input();
return Ok(());
}
if self.matches_single_key(&key, &kb.reply) {
if self.has_comment_at_current_line() {
self.enter_reply_input();
}
return Ok(());
}
if key.code == KeyCode::Tab {
if self.has_comment_at_current_line() {
let count = self.get_comment_indices_at_current_line().len();
if count > 1 && self.selected_inline_comment + 1 < count {
self.selected_inline_comment += 1;
self.comment_panel_scroll = self.comment_panel_offset_for(
self.selected_inline_comment,
panel_inner_width,
);
}
}
return Ok(());
}
if key.code == KeyCode::BackTab {
if self.has_comment_at_current_line() {
let count = self.get_comment_indices_at_current_line().len();
if count > 1 && self.selected_inline_comment > 0 {
self.selected_inline_comment -= 1;
self.comment_panel_scroll = self.comment_panel_offset_for(
self.selected_inline_comment,
panel_inner_width,
);
}
}
return Ok(());
}
match variant {
DiffViewVariant::SplitPane => {
if self.matches_single_key(&key, &kb.move_right) || key.code == KeyCode::Right {
self.diff_view_return_state = AppState::SplitViewDiff;
self.preview_return_state = AppState::DiffView;
self.state = AppState::DiffView;
return Ok(());
}
if self.matches_single_key(&key, &kb.quit)
|| self.matches_single_key(&key, &kb.move_left)
|| key.code == KeyCode::Left
|| key.code == KeyCode::Esc
{
self.comment_panel_open = false;
self.comment_panel_scroll = 0;
return Ok(());
}
}
DiffViewVariant::Fullscreen => {
if self.matches_single_key(&key, &kb.move_left) || key.code == KeyCode::Left {
self.state = self.diff_view_return_state;
return Ok(());
}
if self.matches_single_key(&key, &kb.quit) || key.code == KeyCode::Esc {
self.comment_panel_open = false;
self.comment_panel_scroll = 0;
return Ok(());
}
}
}
return Ok(());
}
self.check_sequence_timeout();
let current_kb = event_to_keybinding(&key);
if let Some(kb_event) = current_kb {
if !self.pending_keys.is_empty() {
self.push_pending_key(kb_event);
if self.try_match_sequence(&kb.go_to_definition) == SequenceMatch::Full {
self.clear_pending_keys();
self.open_symbol_popup(terminal).await?;
return Ok(());
}
if self.try_match_sequence(&kb.go_to_file) == SequenceMatch::Full {
self.clear_pending_keys();
self.open_current_file_in_editor(terminal).await?;
return Ok(());
}
if self.try_match_sequence(&kb.jump_to_first) == SequenceMatch::Full {
self.clear_pending_keys();
self.selected_line = 0;
self.scroll_offset = 0;
return Ok(());
}
self.clear_pending_keys();
} else {
let could_start_gd = self.key_could_match_sequence(&key, &kb.go_to_definition);
let could_start_gf = self.key_could_match_sequence(&key, &kb.go_to_file);
let could_start_gg = self.key_could_match_sequence(&key, &kb.jump_to_first);
if could_start_gd || could_start_gf || could_start_gg {
self.push_pending_key(kb_event);
return Ok(());
}
}
}
match variant {
DiffViewVariant::SplitPane => {
if self.matches_single_key(&key, &kb.move_right) || key.code == KeyCode::Right {
self.diff_view_return_state = AppState::SplitViewDiff;
self.preview_return_state = AppState::DiffView;
self.state = AppState::DiffView;
return Ok(());
}
if self.matches_single_key(&key, &kb.move_left) || key.code == KeyCode::Left {
self.state = AppState::SplitViewFileList;
return Ok(());
}
if self.matches_single_key(&key, &kb.quit) || key.code == KeyCode::Esc {
self.state = AppState::FileList;
return Ok(());
}
if self.matches_single_key(&key, &kb.comment) {
self.enter_comment_input();
return Ok(());
}
if self.matches_single_key(&key, &kb.suggestion) {
self.enter_suggestion_input();
return Ok(());
}
}
DiffViewVariant::Fullscreen => {
if self.matches_single_key(&key, &kb.quit) || key.code == KeyCode::Esc {
if self.started_from_pr_list
&& self.diff_view_return_state == AppState::FileList
{
self.back_to_pr_list();
} else {
self.state = self.diff_view_return_state;
}
return Ok(());
}
if self.matches_single_key(&key, &kb.move_left) || key.code == KeyCode::Left {
self.state = self.diff_view_return_state;
return Ok(());
}
}
}
if !self.local_mode && self.matches_single_key(&key, &kb.pr_description) {
self.open_pr_description();
return Ok(());
}
if (key.code == KeyCode::Enter && key.modifiers.contains(KeyModifiers::SHIFT))
|| self.matches_single_key(&key, &kb.multiline_select)
{
self.enter_multiline_selection();
return Ok(());
}
if self.matches_single_key(&key, &kb.move_down) || key.code == KeyCode::Down {
if self.diff_line_count > 0 {
self.selected_line =
(self.selected_line + 1).min(self.diff_line_count.saturating_sub(1));
self.adjust_scroll(visible_lines);
}
return Ok(());
}
if self.matches_single_key(&key, &kb.move_up) || key.code == KeyCode::Up {
self.selected_line = self.selected_line.saturating_sub(1);
self.adjust_scroll(visible_lines);
return Ok(());
}
if self.matches_single_key(&key, &kb.jump_to_last) {
if self.diff_line_count > 0 {
self.selected_line = self.diff_line_count.saturating_sub(1);
self.adjust_scroll(visible_lines);
}
return Ok(());
}
if self.matches_single_key(&key, &kb.jump_back) {
self.jump_back();
return Ok(());
}
if self.matches_single_key(&key, &kb.page_down) || Self::is_shift_char_shortcut(&key, 'j') {
if self.diff_line_count > 0 {
self.selected_line =
(self.selected_line + 20).min(self.diff_line_count.saturating_sub(1));
self.adjust_scroll(visible_lines);
}
return Ok(());
}
if self.matches_single_key(&key, &kb.page_up) || Self::is_shift_char_shortcut(&key, 'k') {
self.selected_line = self.selected_line.saturating_sub(20);
self.adjust_scroll(visible_lines);
return Ok(());
}
if self.matches_single_key(&key, &kb.next_comment) {
self.jump_to_next_comment();
return Ok(());
}
if self.matches_single_key(&key, &kb.prev_comment) {
self.jump_to_prev_comment();
return Ok(());
}
if self.matches_single_key(&key, &kb.toggle_markdown_rich) {
self.toggle_markdown_rich();
self.ensure_diff_cache();
return Ok(());
}
if !self.local_mode && self.matches_single_key(&key, &kb.open_panel) {
self.comment_panel_open = true;
self.comment_panel_scroll = 0;
self.selected_inline_comment = 0;
return Ok(());
}
if variant == DiffViewVariant::Fullscreen {
if self.matches_single_key(&key, &kb.comment) {
self.enter_comment_input();
return Ok(());
}
if self.matches_single_key(&key, &kb.suggestion) {
self.enter_suggestion_input();
return Ok(());
}
}
Ok(())
}
pub(crate) async fn handle_split_view_diff_input(
&mut self,
key: event::KeyEvent,
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
) -> Result<()> {
self.handle_diff_input_common(key, terminal, DiffViewVariant::SplitPane)
.await
}
pub(crate) async fn handle_diff_view_input(
&mut self,
key: event::KeyEvent,
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
) -> Result<()> {
self.handle_diff_input_common(key, terminal, DiffViewVariant::Fullscreen)
.await
}
pub(crate) fn adjust_scroll(&mut self, visible_lines: usize) {
if visible_lines == 0 {
return;
}
if self.selected_line < self.scroll_offset {
self.scroll_offset = self.selected_line;
}
if self.selected_line >= self.scroll_offset + visible_lines {
self.scroll_offset = self.selected_line.saturating_sub(visible_lines) + 1;
}
let padding = visible_lines / 2;
let max_scroll_with_padding = self.diff_line_count.saturating_sub(1);
if self.selected_line >= self.diff_line_count.saturating_sub(padding) {
let target_scroll = self.selected_line.saturating_sub(visible_lines / 2);
self.scroll_offset = target_scroll.min(max_scroll_with_padding);
}
}
}