@@ -1985,15 +1985,37 @@ impl Editor {
19851985 split_id : SplitId ,
19861986 _estimated_line_length : usize ,
19871987 ) -> Option < Vec < Event > > {
1988- // Determine direction and whether this is a selection action
1988+ // Classify the action
1989+ enum VisualAction {
1990+ UpDown { direction : i8 , is_select : bool } ,
1991+ LineEnd { is_select : bool } ,
1992+ LineStart { is_select : bool } ,
1993+ }
1994+
19891995 // Note: We don't intercept BlockSelectUp/Down because block selection has
19901996 // special semantics (setting block_anchor) that require the default handler
1991- let ( direction, is_select) = match action {
1992- Action :: MoveUp => ( -1i8 , false ) ,
1993- Action :: MoveDown => ( 1 , false ) ,
1994- Action :: SelectUp => ( -1 , true ) ,
1995- Action :: SelectDown => ( 1 , true ) ,
1996- _ => return None , // Not a visual line movement action
1997+ let visual_action = match action {
1998+ Action :: MoveUp => VisualAction :: UpDown {
1999+ direction : -1 ,
2000+ is_select : false ,
2001+ } ,
2002+ Action :: MoveDown => VisualAction :: UpDown {
2003+ direction : 1 ,
2004+ is_select : false ,
2005+ } ,
2006+ Action :: SelectUp => VisualAction :: UpDown {
2007+ direction : -1 ,
2008+ is_select : true ,
2009+ } ,
2010+ Action :: SelectDown => VisualAction :: UpDown {
2011+ direction : 1 ,
2012+ is_select : true ,
2013+ } ,
2014+ Action :: MoveLineEnd => VisualAction :: LineEnd { is_select : false } ,
2015+ Action :: SelectLineEnd => VisualAction :: LineEnd { is_select : true } ,
2016+ Action :: MoveLineStart => VisualAction :: LineStart { is_select : false } ,
2017+ Action :: SelectLineStart => VisualAction :: LineStart { is_select : true } ,
2018+ _ => return None , // Not a visual line action
19972019 } ;
19982020
19992021 // First, collect cursor data we need (to avoid borrow conflicts)
@@ -2003,61 +2025,122 @@ impl Editor {
20032025 . cursors
20042026 . iter ( )
20052027 . map ( |( cursor_id, cursor) | {
2028+ // Check if cursor is at a physical line boundary:
2029+ // - at_line_ending: byte at cursor position is a newline or at buffer end
2030+ // - at_line_start: cursor is at position 0 or preceded by a newline
2031+ let at_line_ending = if cursor. position < state. buffer . len ( ) {
2032+ let bytes = state
2033+ . buffer
2034+ . slice_bytes ( cursor. position ..cursor. position + 1 ) ;
2035+ bytes. first ( ) == Some ( & b'\n' ) || bytes. first ( ) == Some ( & b'\r' )
2036+ } else {
2037+ true // end of buffer is a boundary
2038+ } ;
2039+ let at_line_start = if cursor. position == 0 {
2040+ true
2041+ } else {
2042+ let prev = state
2043+ . buffer
2044+ . slice_bytes ( cursor. position - 1 ..cursor. position ) ;
2045+ prev. first ( ) == Some ( & b'\n' )
2046+ } ;
20062047 (
20072048 cursor_id,
20082049 cursor. position ,
20092050 cursor. anchor ,
20102051 cursor. sticky_column ,
20112052 cursor. deselect_on_move ,
2053+ at_line_ending,
2054+ at_line_start,
20122055 )
20132056 } )
20142057 . collect ( )
20152058 } ;
20162059
20172060 let mut events = Vec :: new ( ) ;
20182061
2019- for ( cursor_id, position, anchor, sticky_column, deselect_on_move) in cursor_data {
2020- // Calculate current visual column from cached layout
2021- // If we can't find the position in the layout, bail out and let the default handler deal with it
2022- let current_visual_col =
2023- match self . cached_layout . byte_to_visual_column ( split_id, position) {
2024- Some ( col) => col,
2025- None => return None , // Position not in cached layout, use default handler
2026- } ;
2062+ for (
2063+ cursor_id,
2064+ position,
2065+ anchor,
2066+ sticky_column,
2067+ deselect_on_move,
2068+ at_line_ending,
2069+ at_line_start,
2070+ ) in cursor_data
2071+ {
2072+ let ( new_pos, new_sticky) = match & visual_action {
2073+ VisualAction :: UpDown { direction, .. } => {
2074+ // Calculate current visual column from cached layout
2075+ let current_visual_col =
2076+ match self . cached_layout . byte_to_visual_column ( split_id, position) {
2077+ Some ( col) => col,
2078+ None => return None ,
2079+ } ;
20272080
2028- // Use sticky column if set, otherwise use current visual column
2029- let goal_visual_col = if sticky_column > 0 {
2030- sticky_column
2031- } else {
2032- current_visual_col
2081+ let goal_visual_col = if sticky_column > 0 {
2082+ sticky_column
2083+ } else {
2084+ current_visual_col
2085+ } ;
2086+
2087+ match self . cached_layout . move_visual_line (
2088+ split_id,
2089+ position,
2090+ goal_visual_col,
2091+ * direction,
2092+ ) {
2093+ Some ( result) => result,
2094+ None => continue , // At boundary, skip this cursor
2095+ }
2096+ }
2097+ VisualAction :: LineEnd { .. } => {
2098+ // Allow advancing to next visual segment only if not at a physical line ending
2099+ let allow_advance = !at_line_ending;
2100+ match self
2101+ . cached_layout
2102+ . visual_line_end ( split_id, position, allow_advance)
2103+ {
2104+ Some ( end_pos) => ( end_pos, 0 ) ,
2105+ None => return None ,
2106+ }
2107+ }
2108+ VisualAction :: LineStart { .. } => {
2109+ // Allow advancing to previous visual segment only if not at a physical line start
2110+ let allow_advance = !at_line_start;
2111+ match self
2112+ . cached_layout
2113+ . visual_line_start ( split_id, position, allow_advance)
2114+ {
2115+ Some ( start_pos) => ( start_pos, 0 ) ,
2116+ None => return None ,
2117+ }
2118+ }
20332119 } ;
20342120
2035- // Try to move using cached layout
2036- let move_result =
2037- self . cached_layout
2038- . move_visual_line ( split_id, position, goal_visual_col, direction) ;
2121+ let is_select = match & visual_action {
2122+ VisualAction :: UpDown { is_select, .. } => * is_select,
2123+ VisualAction :: LineEnd { is_select } => * is_select,
2124+ VisualAction :: LineStart { is_select } => * is_select,
2125+ } ;
20392126
2040- if let Some ( ( new_pos, new_sticky) ) = move_result {
2041- // Determine anchor based on action type
2042- let new_anchor = if is_select {
2043- Some ( anchor. unwrap_or ( position) )
2044- } else if deselect_on_move {
2045- None
2046- } else {
2047- anchor
2048- } ;
2127+ let new_anchor = if is_select {
2128+ Some ( anchor. unwrap_or ( position) )
2129+ } else if deselect_on_move {
2130+ None
2131+ } else {
2132+ anchor
2133+ } ;
20492134
2050- events. push ( Event :: MoveCursor {
2051- cursor_id,
2052- old_position : position,
2053- new_position : new_pos,
2054- old_anchor : anchor,
2055- new_anchor,
2056- old_sticky_column : sticky_column,
2057- new_sticky_column : new_sticky,
2058- } ) ;
2059- }
2060- // If move_result is None, we're at a boundary - don't generate an event
2135+ events. push ( Event :: MoveCursor {
2136+ cursor_id,
2137+ old_position : position,
2138+ new_position : new_pos,
2139+ old_anchor : anchor,
2140+ new_anchor,
2141+ old_sticky_column : sticky_column,
2142+ new_sticky_column : new_sticky,
2143+ } ) ;
20612144 }
20622145
20632146 if events. is_empty ( ) {
0 commit comments