use vim::vim::{ BracketChar, CharacterMotion, Direction, FindCharMotion, FirstNonWhitespaceMotion, InsertPosition, LineMotion, ModeTransition, MotionType, TextObjectType, VimHandler, VimMode, VimMotion, VimOperand, VimOperator, VimTextObject, WordMotion,};use warp_editor::content::buffer::{ AutoScrollBehavior, BufferEditAction, EditOrigin, SelectionOffsets, VimInsertPoint,};use warp_editor::model::{CoreEditorModel, PlainTextEditorModel};use warp_editor::render::model::AutoScrollMode;use warp_editor::selection::{TextDirection, TextUnit};use warpui::units::IntoPixels;use warpui::{SingletonEntity, ViewContext}; use super::{CodeEditorEvent, CodeEditorView};use crate::code::editor::find::view::Event as FindViewEvent;use crate::code::editor::model::{CaseTransform, CodeEditorModel, LineBound};use crate::view_components::find::FindDirection;use crate::vim_registers::{RegisterContent, VimRegisters}; impl VimHandler for CodeEditorView { fn insert_char(&mut self, c: char, ctx: &mut ViewContext<Self>) { self.user_insert(&c.to_string(), ctx); } fn keyword_prg(&mut self, _ctx: &mut ViewContext<Self>) { // no-op } fn navigate_char( &mut self, count: u32, character_motion: &CharacterMotion, ctx: &mut ViewContext<Self>, ) { self.model.update(ctx, |model, ctx| match character_motion { CharacterMotion::Right => { model.vim_move_horizontal_by_offset(count, &Direction::Forward, false, true, ctx); } CharacterMotion::Up => { model.vim_move_vertical_by_offset(count, TextDirection::Backwards, false, ctx); } CharacterMotion::Down => { model.vim_move_vertical_by_offset(count, TextDirection::Forwards, false, ctx); } CharacterMotion::Left => { model.vim_move_horizontal_by_offset(count, &Direction::Backward, false, true, ctx); } CharacterMotion::WrappingLeft => { model.vim_move_horizontal_by_offset(count, &Direction::Backward, false, false, ctx); } CharacterMotion::WrappingRight => { model.vim_move_horizontal_by_offset(count, &Direction::Forward, false, false, ctx); } }); } fn navigate_word(&mut self, count: u32, word_motion: &WordMotion, ctx: &mut ViewContext<Self>) { let WordMotion { direction, bound, word_type, } = word_motion; self.model.update(ctx, |model, ctx| { model.vim_navigate_word(*direction, *bound, *word_type, count, ctx); }); } fn navigate_line(&mut self, line_count: u32, motion: &LineMotion, ctx: &mut ViewContext<Self>) { self.model.update(ctx, |model, ctx| { match motion { LineMotion::Start => model.vim_move_to_line_bound(LineBound::Start, false, ctx), LineMotion::FirstNonWhitespace => model.vim_move_to_first_nonwhitespace(false, ctx), LineMotion::End => { // Only moving to the end of the line ($) uses number-repeat (the line-count var) model.vim_move_vertical_by_offset( line_count.saturating_sub(1), TextDirection::Forwards, false, ctx, ); model.vim_move_to_line_bound(LineBound::End, false, ctx); } } }) } fn first_nonwhitespace_motion( &mut self, count: u32, motion: &FirstNonWhitespaceMotion, ctx: &mut ViewContext<Self>, ) { self.model.update(ctx, |model, ctx| { match motion { FirstNonWhitespaceMotion::Up => { model.vim_move_vertical_by_offset(count, TextDirection::Backwards, false, ctx); } FirstNonWhitespaceMotion::Down => { model.vim_move_vertical_by_offset(count, TextDirection::Forwards, false, ctx) } FirstNonWhitespaceMotion::DownMinusOne => model.vim_move_vertical_by_offset( count - 1, TextDirection::Forwards, false, ctx, ), } model.vim_move_to_first_nonwhitespace(false, ctx); }) } fn find_char( &mut self, occurrence_count: u32, find_char_motion: &FindCharMotion, ctx: &mut ViewContext<Self>, ) { self.model.update(ctx, |model, ctx| { model.vim_find_char( false, /* keep_selection */ occurrence_count, find_char_motion, ctx, ); }); } fn navigate_paragraph( &mut self, count: u32, direction: &Direction, ctx: &mut ViewContext<Self>, ) { self.model.update(ctx, |model, ctx| { model.vim_move_by_paragraph(count, direction, false, ctx); }); } fn operation( &mut self, operator: &VimOperator, operand_count: u32, operand: &VimOperand, register_name: char, replacement_text: &str, ctx: &mut ViewContext<Self>, ) { // Selection logic is almost the same for all operators, so capture that in a closure first. let selection_change = |model: &mut CodeEditorModel, ctx: &mut warpui::ModelContext<CodeEditorModel>| { match operand { VimOperand::Motion { motion, motion_type, } => { match motion { VimMotion::Character(char_motion) => { model.vim_select_for_char_motion( char_motion, motion_type, operator, operand_count, ctx, ); } VimMotion::Word(word_motion) => { model.vim_select_for_word_motion( word_motion, operand_count, motion_type, operator, ctx, ); } VimMotion::Line(line_motion) => { model.vim_select_for_line_motion( line_motion, operand_count, motion_type, operator, ctx, ); } VimMotion::FirstNonWhitespace(nonws_motion) => { model.vim_select_for_first_nonwhitespace_motion( nonws_motion, motion_type, operator, operand_count, ctx, ); } VimMotion::Paragraph(direction) => { model.vim_move_by_paragraph(operand_count, direction, true, ctx); if *motion_type == MotionType::Linewise { model.vim_extend_selection_linewise( operator.includes_trailing_newline(), *operator == VimOperator::Delete, ctx, ); } } VimMotion::JumpToLastLine => { model.vim_select_to_buffer_end(ctx); if *motion_type == MotionType::Linewise { model.vim_extend_selection_linewise( operator.includes_trailing_newline(), *operator == VimOperator::Delete, ctx, ); } } VimMotion::JumpToFirstLine => { model.vim_select_to_buffer_start(ctx); if *motion_type == MotionType::Linewise { model.vim_extend_selection_linewise( operator.includes_trailing_newline(), *operator == VimOperator::Delete, ctx, ); } } VimMotion::FindChar(m) => { // Extend selection to the found character according to the motion model.vim_find_char( true, /* keep_selection */ operand_count, m, ctx, ); } VimMotion::JumpToLine(line_number) => { model.vim_select_to_line(*line_number, motion_type, operator, ctx); } VimMotion::JumpToMatchingBracket => { model.vim_select_for_matching_bracket(ctx); } VimMotion::JumpToUnmatchedBracket(bracket) => { model.vim_jump_to_unmatched_bracket(bracket, true, ctx); } } } VimOperand::Line => { // Extend selection down by count-1 lines if operand_count > 1 { model.vim_move_vertical_by_offset( operand_count - 1, TextDirection::Forwards, true, ctx, ); } let include_newline = operator.includes_trailing_newline() && *operator != VimOperator::ToggleComment; model.vim_extend_selection_linewise( include_newline, *operator == VimOperator::Delete, ctx, ); } VimOperand::TextObject(text_object) => { model.vim_select_text_object(text_object, Some(operator), ctx); } } }; let motion_type = match operand { VimOperand::Motion { motion_type, .. } => *motion_type, VimOperand::TextObject(text_object) => match text_object { VimTextObject { object_type: TextObjectType::Paragraph, .. } => MotionType::Linewise, _ => MotionType::Charwise, }, VimOperand::Line => MotionType::Linewise, }; match operator { VimOperator::Delete | VimOperator::Change => { self.model.update(ctx, |model, ctx| { selection_change(model, ctx); // Copy selection to vim register before modifying let buffer = model.content().as_ref(ctx); let selection_model = model.buffer_selection_model().clone(); let selected_text = buffer .selected_text_as_plain_text(selection_model, ctx) .into_string(); let register_text = if selected_text.is_empty() && motion_type == MotionType::Linewise { Some("\n".to_owned()) } else { (!selected_text.is_empty()).then(|| selected_text.clone()) }; if let Some(register_text) = register_text { VimRegisters::handle(ctx).update(ctx, |registers, ctx| { registers.write_to_register( register_name, register_text, motion_type, ctx, ); }); } if !selected_text.is_empty() { if *operator == VimOperator::Change && motion_type == MotionType::Linewise { // Use smart indent to position the cursor when changing the entire // line. model.vim_change_line_with_smart_indent(ctx); } else { model.delete(TextDirection::Forwards, TextUnit::Character, false, ctx); // Insert replacement text if provided if *operator == VimOperator::Change && !replacement_text.is_empty() { model.insert(replacement_text, EditOrigin::UserInitiated, ctx); } if motion_type == MotionType::Linewise { model.vim_move_to_line_bound(LineBound::Start, false, ctx); } } } }); } VimOperator::Yank => { self.model.update(ctx, |model, ctx| { // Store existing selections to restore after yank let existing_selections = model.selections(ctx).clone(); selection_change(model, ctx); // Copy selection to vim register let buffer = model.content().as_ref(ctx); let selection_model = model.buffer_selection_model().clone(); let selected_text = buffer .selected_text_as_plain_text(selection_model, ctx) .into_string(); let register_text = if selected_text.is_empty() && motion_type == MotionType::Linewise { Some("\n".to_owned()) } else { (!selected_text.is_empty()).then_some(selected_text) }; if let Some(register_text) = register_text { VimRegisters::handle(ctx).update(ctx, |registers, ctx| { registers.write_to_register( register_name, register_text, motion_type, ctx, ); }); } match operand { VimOperand::TextObject(_) => { // For text objects, move to the start (min) of the selected range let starts = model .buffer_selection_model() .as_ref(ctx) .selection_offsets() .mapped(|selection| { let start = selection.head.min(selection.tail); SelectionOffsets { head: start, tail: start, } }); model.vim_set_selections(starts, AutoScrollBehavior::None, ctx); } _ => { model.vim_set_selections( existing_selections, AutoScrollBehavior::None, ctx, ); } } }); } VimOperator::ToggleCase => { self.model.update(ctx, |model, ctx| { model.apply_case_transformation_with_selection_change( selection_change, CaseTransform::Toggle, ctx, ); }); } VimOperator::Uppercase => { self.model.update(ctx, |model, ctx| { model.apply_case_transformation_with_selection_change( selection_change, CaseTransform::Uppercase, ctx, ); }); } VimOperator::Lowercase => { self.model.update(ctx, |model, ctx| { model.apply_case_transformation_with_selection_change( selection_change, CaseTransform::Lowercase, ctx, ); }); } VimOperator::ToggleComment => { self.model.update(ctx, |model, ctx| { let existing_selections = model.selections(ctx).clone(); selection_change(model, ctx); model.toggle_comments(ctx); if motion_type == MotionType::Linewise { model.vim_move_to_first_nonwhitespace(false, ctx); } else { model.vim_set_selections( existing_selections, AutoScrollBehavior::None, ctx, ); } }); } VimOperator::Indent | VimOperator::Dedent => { self.model.update(ctx, |model, ctx| { selection_change(model, ctx); let shift = *operator == VimOperator::Dedent; model.indent(shift, ctx); model.vim_move_to_first_nonwhitespace(false, ctx); }); } } } fn replace_char( &mut self, c: char, char_count: u32, advance: bool, ctx: &mut ViewContext<Self>, ) { self.model.update(ctx, |model, ctx| { if advance { model.vim_replace_text(&c.to_string(), ctx); } else { model.replace_char(c, char_count, ctx); } }); // Explicit call to ctx.notify() in the case that we don't make any updates to the model ctx.notify(); } fn replace_text( &mut self, text: &str, count: u32, already_applied: bool, ctx: &mut ViewContext<Self>, ) { let repeat_count = count.saturating_sub(u32::from(already_applied)); self.model.update(ctx, |model, ctx| { if repeat_count > 0 { model.vim_replace_text(&text.repeat(repeat_count as usize), ctx); } if !text.is_empty() { model.vim_move_horizontal_by_offset(1, &Direction::Backward, false, true, ctx); } }); ctx.notify(); } fn search(&mut self, direction: &Direction, ctx: &mut ViewContext<Self>) { self.last_search_direction = *direction; self.show_find_bar(ctx); } fn cycle_search(&mut self, direction: &Direction, ctx: &mut ViewContext<Self>) { let Some(find_bar) = &self.find_bar else { return; }; if !self.searcher.as_ref(ctx).has_query() { return; } if !find_bar.as_ref(ctx).is_open() { find_bar.update(ctx, |find_bar, _| find_bar.set_open(true)); } // Vim-like behavior: // 'n' (Forward) repeats in the same direction // 'N' (Backward) reverses the last direction let effective_dir = match (direction, self.last_search_direction) { (Direction::Forward, dir) => dir, (Direction::Backward, Direction::Backward) => Direction::Forward,Lines 1–500 of 1037. Load more
use vim::vim::{ BracketChar, CharacterMotion, Direction, FindCharMotion, FirstNonWhitespaceMotion, InsertPosition, LineMotion, ModeTransition, MotionType, TextObjectType, VimHandler, VimMode, VimMotion, VimOperand, VimOperator, VimTextObject, WordMotion, }; use warp_editor::content::buffer::{ AutoScrollBehavior, BufferEditAction, EditOrigin, SelectionOffsets, VimInsertPoint, }; use warp_editor::model::{CoreEditorModel, PlainTextEditorModel}; use warp_editor::render::model::AutoScrollMode; use warp_editor::selection::{TextDirection, TextUnit}; use warpui::units::IntoPixels; use warpui::{SingletonEntity, ViewContext}; use super::{CodeEditorEvent, CodeEditorView}; use crate::code::editor::find::view::Event as FindViewEvent; use crate::code::editor::model::{CaseTransform, CodeEditorModel, LineBound}; use crate::view_components::find::FindDirection; use crate::vim_registers::{RegisterContent, VimRegisters}; impl VimHandler for CodeEditorView { fn insert_char(&mut self, c: char, ctx: &mut ViewContext<Self>) { self.user_insert(&c.to_string(), ctx); } fn keyword_prg(&mut self, _ctx: &mut ViewContext<Self>) { // no-op } fn navigate_char( &mut self, count: u32, character_motion: &CharacterMotion, ctx: &mut ViewContext<Self>, ) { self.model.update(ctx, |model, ctx| match character_motion { CharacterMotion::Right => { model.vim_move_horizontal_by_offset(count, &Direction::Forward, false, true, ctx); } CharacterMotion::Up => { model.vim_move_vertical_by_offset(count, TextDirection::Backwards, false, ctx); } CharacterMotion::Down => { model.vim_move_vertical_by_offset(count, TextDirection::Forwards, false, ctx); } CharacterMotion::Left => { model.vim_move_horizontal_by_offset(count, &Direction::Backward, false, true, ctx); } CharacterMotion::WrappingLeft => { model.vim_move_horizontal_by_offset(count, &Direction::Backward, false, false, ctx); } CharacterMotion::WrappingRight => { model.vim_move_horizontal_by_offset(count, &Direction::Forward, false, false, ctx); } }); } fn navigate_word(&mut self, count: u32, word_motion: &WordMotion, ctx: &mut ViewContext<Self>) { let WordMotion { direction, bound, word_type, } = word_motion; self.model.update(ctx, |model, ctx| { model.vim_navigate_word(*direction, *bound, *word_type, count, ctx); }); } fn navigate_line(&mut self, line_count: u32, motion: &LineMotion, ctx: &mut ViewContext<Self>) { self.model.update(ctx, |model, ctx| { match motion { LineMotion::Start => model.vim_move_to_line_bound(LineBound::Start, false, ctx), LineMotion::FirstNonWhitespace => model.vim_move_to_first_nonwhitespace(false, ctx), LineMotion::End => { // Only moving to the end of the line ($) uses number-repeat (the line-count var) model.vim_move_vertical_by_offset( line_count.saturating_sub(1), TextDirection::Forwards, false, ctx, ); model.vim_move_to_line_bound(LineBound::End, false, ctx); } } }) } fn first_nonwhitespace_motion( &mut self, count: u32, motion: &FirstNonWhitespaceMotion, ctx: &mut ViewContext<Self>, ) { self.model.update(ctx, |model, ctx| { match motion { FirstNonWhitespaceMotion::Up => { model.vim_move_vertical_by_offset(count, TextDirection::Backwards, false, ctx); } FirstNonWhitespaceMotion::Down => { model.vim_move_vertical_by_offset(count, TextDirection::Forwards, false, ctx) } FirstNonWhitespaceMotion::DownMinusOne => model.vim_move_vertical_by_offset( count - 1, TextDirection::Forwards, false, ctx, ), } model.vim_move_to_first_nonwhitespace(false, ctx); }) } fn find_char( &mut self, occurrence_count: u32, find_char_motion: &FindCharMotion, ctx: &mut ViewContext<Self>, ) { self.model.update(ctx, |model, ctx| { model.vim_find_char( false, /* keep_selection */ occurrence_count, find_char_motion, ctx, ); }); } fn navigate_paragraph( &mut self, count: u32, direction: &Direction, ctx: &mut ViewContext<Self>, ) { self.model.update(ctx, |model, ctx| { model.vim_move_by_paragraph(count, direction, false, ctx); }); } fn operation( &mut self, operator: &VimOperator, operand_count: u32, operand: &VimOperand, register_name: char, replacement_text: &str, ctx: &mut ViewContext<Self>, ) { // Selection logic is almost the same for all operators, so capture that in a closure first. let selection_change = |model: &mut CodeEditorModel, ctx: &mut warpui::ModelContext<CodeEditorModel>| { match operand { VimOperand::Motion { motion, motion_type, } => { match motion { VimMotion::Character(char_motion) => { model.vim_select_for_char_motion( char_motion, motion_type, operator, operand_count, ctx, ); } VimMotion::Word(word_motion) => { model.vim_select_for_word_motion( word_motion, operand_count, motion_type, operator, ctx, ); } VimMotion::Line(line_motion) => { model.vim_select_for_line_motion( line_motion, operand_count, motion_type, operator, ctx, ); } VimMotion::FirstNonWhitespace(nonws_motion) => { model.vim_select_for_first_nonwhitespace_motion( nonws_motion, motion_type, operator, operand_count, ctx, ); } VimMotion::Paragraph(direction) => { model.vim_move_by_paragraph(operand_count, direction, true, ctx); if *motion_type == MotionType::Linewise { model.vim_extend_selection_linewise( operator.includes_trailing_newline(), *operator == VimOperator::Delete, ctx, ); } } VimMotion::JumpToLastLine => { model.vim_select_to_buffer_end(ctx); if *motion_type == MotionType::Linewise { model.vim_extend_selection_linewise( operator.includes_trailing_newline(), *operator == VimOperator::Delete, ctx, ); } } VimMotion::JumpToFirstLine => { model.vim_select_to_buffer_start(ctx); if *motion_type == MotionType::Linewise { model.vim_extend_selection_linewise( operator.includes_trailing_newline(), *operator == VimOperator::Delete, ctx, ); } } VimMotion::FindChar(m) => { // Extend selection to the found character according to the motion model.vim_find_char( true, /* keep_selection */ operand_count, m, ctx, ); } VimMotion::JumpToLine(line_number) => { model.vim_select_to_line(*line_number, motion_type, operator, ctx); } VimMotion::JumpToMatchingBracket => { model.vim_select_for_matching_bracket(ctx); } VimMotion::JumpToUnmatchedBracket(bracket) => { model.vim_jump_to_unmatched_bracket(bracket, true, ctx); } } } VimOperand::Line => { // Extend selection down by count-1 lines if operand_count > 1 { model.vim_move_vertical_by_offset( operand_count - 1, TextDirection::Forwards, true, ctx, ); } let include_newline = operator.includes_trailing_newline() && *operator != VimOperator::ToggleComment; model.vim_extend_selection_linewise( include_newline, *operator == VimOperator::Delete, ctx, ); } VimOperand::TextObject(text_object) => { model.vim_select_text_object(text_object, Some(operator), ctx); } } }; let motion_type = match operand { VimOperand::Motion { motion_type, .. } => *motion_type, VimOperand::TextObject(text_object) => match text_object { VimTextObject { object_type: TextObjectType::Paragraph, .. } => MotionType::Linewise, _ => MotionType::Charwise, }, VimOperand::Line => MotionType::Linewise, }; match operator { VimOperator::Delete | VimOperator::Change => { self.model.update(ctx, |model, ctx| { selection_change(model, ctx); // Copy selection to vim register before modifying let buffer = model.content().as_ref(ctx); let selection_model = model.buffer_selection_model().clone(); let selected_text = buffer .selected_text_as_plain_text(selection_model, ctx) .into_string(); let register_text = if selected_text.is_empty() && motion_type == MotionType::Linewise { Some("\n".to_owned()) } else { (!selected_text.is_empty()).then(|| selected_text.clone()) }; if let Some(register_text) = register_text { VimRegisters::handle(ctx).update(ctx, |registers, ctx| { registers.write_to_register( register_name, register_text, motion_type, ctx, ); }); } if !selected_text.is_empty() { if *operator == VimOperator::Change && motion_type == MotionType::Linewise { // Use smart indent to position the cursor when changing the entire // line. model.vim_change_line_with_smart_indent(ctx); } else { model.delete(TextDirection::Forwards, TextUnit::Character, false, ctx); // Insert replacement text if provided if *operator == VimOperator::Change && !replacement_text.is_empty() { model.insert(replacement_text, EditOrigin::UserInitiated, ctx); } if motion_type == MotionType::Linewise { model.vim_move_to_line_bound(LineBound::Start, false, ctx); } } } }); } VimOperator::Yank => { self.model.update(ctx, |model, ctx| { // Store existing selections to restore after yank let existing_selections = model.selections(ctx).clone(); selection_change(model, ctx); // Copy selection to vim register let buffer = model.content().as_ref(ctx); let selection_model = model.buffer_selection_model().clone(); let selected_text = buffer .selected_text_as_plain_text(selection_model, ctx) .into_string(); let register_text = if selected_text.is_empty() && motion_type == MotionType::Linewise { Some("\n".to_owned()) } else { (!selected_text.is_empty()).then_some(selected_text) }; if let Some(register_text) = register_text { VimRegisters::handle(ctx).update(ctx, |registers, ctx| { registers.write_to_register( register_name, register_text, motion_type, ctx, ); }); } match operand { VimOperand::TextObject(_) => { // For text objects, move to the start (min) of the selected range let starts = model .buffer_selection_model() .as_ref(ctx) .selection_offsets() .mapped(|selection| { let start = selection.head.min(selection.tail); SelectionOffsets { head: start, tail: start, } }); model.vim_set_selections(starts, AutoScrollBehavior::None, ctx); } _ => { model.vim_set_selections( existing_selections, AutoScrollBehavior::None, ctx, ); } } }); } VimOperator::ToggleCase => { self.model.update(ctx, |model, ctx| { model.apply_case_transformation_with_selection_change( selection_change, CaseTransform::Toggle, ctx, ); }); } VimOperator::Uppercase => { self.model.update(ctx, |model, ctx| { model.apply_case_transformation_with_selection_change( selection_change, CaseTransform::Uppercase, ctx, ); }); } VimOperator::Lowercase => { self.model.update(ctx, |model, ctx| { model.apply_case_transformation_with_selection_change( selection_change, CaseTransform::Lowercase, ctx, ); }); } VimOperator::ToggleComment => { self.model.update(ctx, |model, ctx| { let existing_selections = model.selections(ctx).clone(); selection_change(model, ctx); model.toggle_comments(ctx); if motion_type == MotionType::Linewise { model.vim_move_to_first_nonwhitespace(false, ctx); } else { model.vim_set_selections( existing_selections, AutoScrollBehavior::None, ctx, ); } }); } VimOperator::Indent | VimOperator::Dedent => { self.model.update(ctx, |model, ctx| { selection_change(model, ctx); let shift = *operator == VimOperator::Dedent; model.indent(shift, ctx); model.vim_move_to_first_nonwhitespace(false, ctx); }); } } } fn replace_char( &mut self, c: char, char_count: u32, advance: bool, ctx: &mut ViewContext<Self>, ) { self.model.update(ctx, |model, ctx| { if advance { model.vim_replace_text(&c.to_string(), ctx); } else { model.replace_char(c, char_count, ctx); } }); // Explicit call to ctx.notify() in the case that we don't make any updates to the model ctx.notify(); } fn replace_text( &mut self, text: &str, count: u32, already_applied: bool, ctx: &mut ViewContext<Self>, ) { let repeat_count = count.saturating_sub(u32::from(already_applied)); self.model.update(ctx, |model, ctx| { if repeat_count > 0 { model.vim_replace_text(&text.repeat(repeat_count as usize), ctx); } if !text.is_empty() { model.vim_move_horizontal_by_offset(1, &Direction::Backward, false, true, ctx); } }); ctx.notify(); } fn search(&mut self, direction: &Direction, ctx: &mut ViewContext<Self>) { self.last_search_direction = *direction; self.show_find_bar(ctx); } fn cycle_search(&mut self, direction: &Direction, ctx: &mut ViewContext<Self>) { let Some(find_bar) = &self.find_bar else { return; }; if !self.searcher.as_ref(ctx).has_query() { return; } if !find_bar.as_ref(ctx).is_open() { find_bar.update(ctx, |find_bar, _| find_bar.set_open(true)); } // Vim-like behavior: // 'n' (Forward) repeats in the same direction // 'N' (Backward) reverses the last direction let effective_dir = match (direction, self.last_search_direction) { (Direction::Forward, dir) => dir, (Direction::Backward, Direction::Backward) => Direction::Forward,