diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index e3f862a60..4d50e48bf 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -98,7 +98,7 @@ pub use {regex, tree_sitter}; pub use graphemes::RopeGraphemes; pub use position::{ char_idx_at_visual_offset, coords_at_pos, pos_at_coords, visual_offset_from_anchor, - visual_offset_from_block, Position, + visual_offset_from_block, Position, VisualOffsetError, }; #[allow(deprecated)] pub use position::{pos_at_visual_coords, visual_coords_at_pos}; diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs index 7b8dc326e..c3233a340 100644 --- a/helix-core/src/position.rs +++ b/helix-core/src/position.rs @@ -137,6 +137,12 @@ pub fn visual_offset_from_block( (last_pos, block_start) } +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum VisualOffsetError { + PosBeforeAnchorRow, + PosAfterMaxRow, +} + /// Returns the visual offset from the start of the visual line /// that contains anchor. pub fn visual_offset_from_anchor( @@ -146,28 +152,46 @@ pub fn visual_offset_from_anchor( text_fmt: &TextFormat, annotations: &TextAnnotations, max_rows: usize, -) -> Option<(Position, usize)> { +) -> Result<(Position, usize), VisualOffsetError> { let (formatter, block_start) = DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor); let mut char_pos = block_start; let mut anchor_line = None; + let mut found_pos = None; let mut last_pos = Position::default(); + if pos < block_start { + return Err(VisualOffsetError::PosBeforeAnchorRow); + } + for (grapheme, vpos) in formatter { last_pos = vpos; char_pos += grapheme.doc_chars(); - if char_pos > anchor && anchor_line.is_none() { - anchor_line = Some(last_pos.row); - } if char_pos > pos { - last_pos.row -= anchor_line.unwrap(); - return Some((last_pos, block_start)); + if let Some(anchor_line) = anchor_line { + last_pos.row -= anchor_line; + return Ok((last_pos, block_start)); + } else { + found_pos = Some(last_pos); + } + } + if char_pos > anchor && anchor_line.is_none() { + if let Some(mut found_pos) = found_pos { + return if found_pos.row == last_pos.row { + found_pos.row = 0; + Ok((found_pos, block_start)) + } else { + Err(VisualOffsetError::PosBeforeAnchorRow) + }; + } else { + anchor_line = Some(last_pos.row); + } } if let Some(anchor_line) = anchor_line { if vpos.row >= anchor_line + max_rows { - return None; + return Err(VisualOffsetError::PosAfterMaxRow); } } } @@ -175,7 +199,7 @@ pub fn visual_offset_from_anchor( let anchor_line = anchor_line.unwrap_or(last_pos.row); last_pos.row -= anchor_line; - Some((last_pos, block_start)) + Ok((last_pos, block_start)) } /// Convert (line, column) coordinates to a character index. diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 0ac7ca3b1..ee6fc1275 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -7,9 +7,13 @@ use crate::{ }; use helix_core::{ - char_idx_at_visual_offset, doc_formatter::TextFormat, syntax::Highlight, - text_annotations::TextAnnotations, visual_offset_from_anchor, visual_offset_from_block, - Position, RopeSlice, Selection, Transaction, + char_idx_at_visual_offset, + doc_formatter::TextFormat, + syntax::Highlight, + text_annotations::TextAnnotations, + visual_offset_from_anchor, visual_offset_from_block, Position, RopeSlice, Selection, + Transaction, + VisualOffsetError::{PosAfterMaxRow, PosBeforeAnchorRow}, }; use std::{ @@ -213,46 +217,38 @@ impl View { // - 1 so we have at least one gap in the middle. // a height of 6 with padding of 3 on each side will keep shifting the view back and forth // as we type - let scrolloff = scrolloff.min(viewport.height.saturating_sub(1) as usize / 2); + let scrolloff = if CENTERING { + 0 + } else { + scrolloff.min(viewport.height.saturating_sub(1) as usize / 2) + }; let cursor = doc.selection(self.id).primary().cursor(doc_text); let mut offset = self.offset; + let off = visual_offset_from_anchor( + doc_text, + offset.anchor, + cursor, + &text_fmt, + &annotations, + vertical_viewport_end, + ); - let (visual_off, mut at_top) = if cursor >= offset.anchor { - let off = visual_offset_from_anchor( - doc_text, - offset.anchor, - cursor, - &text_fmt, - &annotations, - vertical_viewport_end, - ); - (off, false) - } else if CENTERING { - // cursor out of view - return None; - } else { - (None, true) - }; - - let new_anchor = match visual_off { - Some((visual_pos, _)) if visual_pos.row < scrolloff + offset.vertical_offset => { - if CENTERING && visual_pos.row < offset.vertical_offset { + let (new_anchor, at_top) = match off { + Ok((visual_pos, _)) if visual_pos.row < scrolloff + offset.vertical_offset => { + if CENTERING { // cursor out of view return None; } - at_top = true; - true + (true, true) } - Some((visual_pos, _)) if visual_pos.row + scrolloff + 1 >= vertical_viewport_end => { - if CENTERING && visual_pos.row >= vertical_viewport_end { - // cursor out of view - return None; - } - true + Ok((visual_pos, _)) if visual_pos.row + scrolloff >= vertical_viewport_end => { + (true, false) } - Some(_) => false, - None => true, + Ok((_, _)) => (false, false), + Err(_) if CENTERING => return None, + Err(PosBeforeAnchorRow) => (true, true), + Err(PosAfterMaxRow) => (true, false), }; if new_anchor { @@ -269,8 +265,8 @@ impl View { offset.horizontal_offset = 0; } else { // determine the current visual column of the text - let col = visual_off - .unwrap_or_else(|| { + let col = off + .unwrap_or_else(|_| { visual_offset_from_block( doc_text, offset.anchor, @@ -360,8 +356,9 @@ impl View { ); match pos { - Some((Position { row, .. }, _)) => row.saturating_sub(self.offset.vertical_offset), - None => visual_height.saturating_sub(1), + Ok((Position { row, .. }, _)) => row.saturating_sub(self.offset.vertical_offset), + Err(PosAfterMaxRow) => visual_height.saturating_sub(1), + Err(PosBeforeAnchorRow) => 0, } } @@ -390,7 +387,8 @@ impl View { &text_fmt, &annotations, viewport.height as usize, - )? + ) + .ok()? .0; if pos.row < self.offset.vertical_offset { return None;