//! Movement module contains helper functions for calculating intended position//! in editor given a given motion (e.g. it handles converting a "move left" command into coordinates in editor). It is exposed mostly for use by vim crate. use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};use crate::{ DisplayRow, EditorStyle, ToOffset, ToPoint, scroll::{ScrollOffset, SharedScrollAnchor},};use gpui::{Pixels, WindowTextSystem};use language::{CharClassifier, Point};use multi_buffer::{MultiBufferOffset, MultiBufferRow, MultiBufferSnapshot};use serde::Deserialize;use workspace::searchable::Direction; use std::{ops::Range, sync::Arc}; /// Defines search strategy for items in `movement` module./// `FindRange::SingeLine` only looks for a match on a single line at a time, whereas/// `FindRange::MultiLine` keeps going until the end of a string.#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]pub enum FindRange { SingleLine, MultiLine,} /// TextLayoutDetails encompasses everything we need to move vertically/// taking into account variable width characters.pub struct TextLayoutDetails { pub(crate) text_system: Arc<WindowTextSystem>, pub(crate) editor_style: EditorStyle, pub(crate) rem_size: Pixels, pub scroll_anchor: SharedScrollAnchor, pub visible_rows: Option<f64>, pub vertical_scroll_margin: ScrollOffset,} /// Returns a column to the left of the current point, wrapping/// to the previous line if that point is at the start of line.pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { if point.column() > 0 { *point.column_mut() -= 1; } else if point.row().0 > 0 { *point.row_mut() -= 1; *point.column_mut() = map.line_len(point.row()); } map.clip_point(point, Bias::Left)} /// Returns a column to the left of the current point, doing nothing if/// that point is already at the start of line.pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { if point.column() > 0 { *point.column_mut() -= 1; } else if point.column() == 0 { // If the current sofr_wrap mode is used, the column corresponding to the display is 0, // which does not necessarily mean that the actual beginning of a paragraph if map.display_point_to_fold_point(point, Bias::Left).column() > 0 { return left(map, point); } } map.clip_point(point, Bias::Left)} /// Returns a column to the right of the current point, wrapping/// to the next line if that point is at the end of line.pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { if point.column() < map.line_len(point.row()) { *point.column_mut() += 1; } else if point.row() < map.max_point().row() { *point.row_mut() += 1; *point.column_mut() = 0; } map.clip_point(point, Bias::Right)} /// Returns a column to the right of the current point, not performing any wrapping/// if that point is already at the end of line.pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { *point.column_mut() += 1; map.clip_point(point, Bias::Right)} /// Returns a display point for the preceding displayed line (which might be a soft-wrapped line).pub fn up( map: &DisplaySnapshot, start: DisplayPoint, goal: SelectionGoal, preserve_column_at_start: bool, text_layout_details: &TextLayoutDetails,) -> (DisplayPoint, SelectionGoal) { up_by_rows( map, start, 1, goal, preserve_column_at_start, text_layout_details, )} /// Returns a display point for the next displayed line (which might be a soft-wrapped line).pub fn down( map: &DisplaySnapshot, start: DisplayPoint, goal: SelectionGoal, preserve_column_at_end: bool, text_layout_details: &TextLayoutDetails,) -> (DisplayPoint, SelectionGoal) { down_by_rows( map, start, 1, goal, preserve_column_at_end, text_layout_details, )} pub(crate) fn up_by_rows( map: &DisplaySnapshot, start: DisplayPoint, row_count: u32, goal: SelectionGoal, preserve_column_at_start: bool, text_layout_details: &TextLayoutDetails,) -> (DisplayPoint, SelectionGoal) { let goal_x = match goal { SelectionGoal::HorizontalPosition(x) => x.into(), SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(), SelectionGoal::HorizontalRange { end, .. } => end.into(), _ => map.x_for_display_point(start, text_layout_details), }; let prev_row = DisplayRow(start.row().0.saturating_sub(row_count)); let mut point = map.clip_point( DisplayPoint::new(prev_row, map.line_len(prev_row)), Bias::Left, ); if point.row() < start.row() { *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details) } else if preserve_column_at_start { return (start, goal); } else { point = DisplayPoint::new(DisplayRow(0), 0); } let mut clipped_point = map.clip_point(point, Bias::Left); if clipped_point.row() < point.row() { clipped_point = map.clip_point(point, Bias::Right); } ( clipped_point, SelectionGoal::HorizontalPosition(goal_x.into()), )} pub(crate) fn down_by_rows( map: &DisplaySnapshot, start: DisplayPoint, row_count: u32, goal: SelectionGoal, preserve_column_at_end: bool, text_layout_details: &TextLayoutDetails,) -> (DisplayPoint, SelectionGoal) { let goal_x = match goal { SelectionGoal::HorizontalPosition(x) => x.into(), SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(), SelectionGoal::HorizontalRange { end, .. } => end.into(), _ => map.x_for_display_point(start, text_layout_details), }; let new_row = DisplayRow(start.row().0 + row_count); let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right); if point.row() > start.row() { *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details) } else if preserve_column_at_end { return (start, goal); } else { point = map.max_point(); } let mut clipped_point = map.clip_point(point, Bias::Right); if clipped_point.row() > point.row() { clipped_point = map.clip_point(point, Bias::Left); } ( clipped_point, SelectionGoal::HorizontalPosition(goal_x.into()), )} /// Returns a position of the start of line./// If `stop_at_soft_boundaries` is true, the returned position is that of the/// displayed line (e.g. it could actually be in the middle of a text line if that line is soft-wrapped)./// Otherwise it's always going to be the start of a logical line.pub fn line_beginning( map: &DisplaySnapshot, display_point: DisplayPoint, stop_at_soft_boundaries: bool,) -> DisplayPoint { let point = display_point.to_point(map); let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right); let line_start = map.prev_line_boundary(point).1; if stop_at_soft_boundaries && display_point != soft_line_start { soft_line_start } else { line_start }} /// Returns the last indented position on a given line./// If `stop_at_soft_boundaries` is true, the returned [`DisplayPoint`] is that of a/// displayed line (e.g. if there's soft wrap it's gonna be returned),/// otherwise it's always going to be a start of a logical line.pub fn indented_line_beginning( map: &DisplaySnapshot, display_point: DisplayPoint, stop_at_soft_boundaries: bool, stop_at_indent: bool,) -> DisplayPoint { let point = display_point.to_point(map); let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right); let indent_start = Point::new( point.row, map.buffer_snapshot() .indent_size_for_line(MultiBufferRow(point.row)) .len, ) .to_display_point(map); let line_start = map.prev_line_boundary(point).1; if stop_at_soft_boundaries && soft_line_start > indent_start && display_point != soft_line_start { soft_line_start } else if stop_at_indent && (display_point > indent_start || display_point == line_start) { indent_start } else { line_start }} /// Returns a position of the end of line.////// If `stop_at_soft_boundaries` is true, the returned position is that of the/// displayed line (e.g. it could actually be in the middle of a text line if that line is soft-wrapped)./// Otherwise it's always going to be the end of a logical line.pub fn line_end( map: &DisplaySnapshot, display_point: DisplayPoint, stop_at_soft_boundaries: bool,) -> DisplayPoint { let soft_line_end = map.clip_point( DisplayPoint::new(display_point.row(), map.line_len(display_point.row())), Bias::Left, ); if stop_at_soft_boundaries && display_point != soft_line_end { soft_line_end } else { map.next_line_boundary(display_point.to_point(map)).1 }} /// Returns a position of the previous word boundary, where a word character is defined as either/// uppercase letter, lowercase letter, '_' character or language-specific word character (like '-' in CSS).pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let classifier = map.buffer_snapshot().char_classifier_at(raw_point); let mut is_first_iteration = true; find_preceding_boundary_display_point(map, point, FindRange::MultiLine, &mut |left, right| { // Make alt-left skip punctuation to respect VSCode behaviour. For example: hello.| goes to |hello. if is_first_iteration && classifier.is_punctuation(right) && !classifier.is_punctuation(left) && left != '\n' { is_first_iteration = false; return false; } is_first_iteration = false; (classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(right)) || left == '\n' })} /// Returns a position of the previous word boundary, where a word character is defined as either/// uppercase letter, lowercase letter, '_' character, language-specific word character (like '-' in CSS) or newline.pub fn previous_word_start_or_newline(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let classifier = map.buffer_snapshot().char_classifier_at(raw_point); find_preceding_boundary_display_point(map, point, FindRange::MultiLine, &mut |left, right| { (classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(right)) || left == '\n' || right == '\n' })} /// Text movements are too greedy, making deletions too greedy too./// Makes deletions more ergonomic by potentially reducing the deletion range based on its text contents:/// * whitespace sequences with length >= 2 stop the deletion after removal (despite movement jumping over the word behind the whitespaces)/// * brackets stop the deletion after removal (despite movement currently not accounting for these and jumping over)pub fn adjust_greedy_deletion( map: &DisplaySnapshot, delete_from: DisplayPoint, delete_until: DisplayPoint, ignore_brackets: bool,) -> DisplayPoint { if delete_from == delete_until { return delete_until; } let is_backward = delete_from > delete_until; let delete_range = if is_backward { map.display_point_to_point(delete_until, Bias::Left) .to_offset(map.buffer_snapshot()) ..map .display_point_to_point(delete_from, Bias::Right) .to_offset(map.buffer_snapshot()) } else { map.display_point_to_point(delete_from, Bias::Left) .to_offset(map.buffer_snapshot()) ..map .display_point_to_point(delete_until, Bias::Right) .to_offset(map.buffer_snapshot()) }; let trimmed_delete_range = if ignore_brackets { delete_range } else { let brackets_in_delete_range = map .buffer_snapshot() .bracket_ranges(delete_range.clone()) .into_iter() .flatten() .flat_map(|(left_bracket, right_bracket)| { [ left_bracket.start, left_bracket.end, right_bracket.start, right_bracket.end, ] }) .filter(|&bracket| delete_range.start < bracket && bracket < delete_range.end); let closest_bracket = if is_backward { brackets_in_delete_range.max() } else { brackets_in_delete_range.min() }; if is_backward { closest_bracket.unwrap_or(delete_range.start)..delete_range.end } else { delete_range.start..closest_bracket.unwrap_or(delete_range.end) } }; let mut whitespace_sequences = Vec::new(); let mut current_offset = trimmed_delete_range.start; let mut whitespace_sequence_length = MultiBufferOffset(0); let mut whitespace_sequence_start = MultiBufferOffset(0); for ch in map .buffer_snapshot() .text_for_range(trimmed_delete_range.clone()) .flat_map(str::chars) { if ch.is_whitespace() { if whitespace_sequence_length == MultiBufferOffset(0) { whitespace_sequence_start = current_offset; } whitespace_sequence_length += 1; } else { if whitespace_sequence_length >= MultiBufferOffset(2) { whitespace_sequences.push((whitespace_sequence_start, current_offset)); } whitespace_sequence_start = MultiBufferOffset(0); whitespace_sequence_length = MultiBufferOffset(0); } current_offset += ch.len_utf8(); } if whitespace_sequence_length >= MultiBufferOffset(2) { whitespace_sequences.push((whitespace_sequence_start, current_offset)); } let closest_whitespace_end = if is_backward { whitespace_sequences.last().map(|&(start, _)| start) } else { whitespace_sequences.first().map(|&(_, end)| end) }; closest_whitespace_end .unwrap_or_else(|| { if is_backward { trimmed_delete_range.start } else { trimmed_delete_range.end } }) .to_display_point(map)} /// Returns a position of the previous subword boundary, where a subword is defined as a run of/// word characters of the same "subkind" - where subcharacter kinds are '_' character,/// lowerspace characters and uppercase characters.pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let classifier = map.buffer_snapshot().char_classifier_at(raw_point); find_preceding_boundary_display_point(map, point, FindRange::MultiLine, &mut |left, right| { is_subword_start(left, right, &classifier) || left == '\n' || right == '\n' })} /// Returns a position of the previous subword boundary, where a subword is defined as a run of/// word characters of the same "subkind" - where subcharacter kinds are '_' character,/// lowerspace characters and uppercase characters or newline.pub fn previous_subword_start_or_newline( map: &DisplaySnapshot, point: DisplayPoint,) -> DisplayPoint { let raw_point = point.to_point(map); let classifier = map.buffer_snapshot().char_classifier_at(raw_point); find_preceding_boundary_display_point(map, point, FindRange::MultiLine, &mut |left, right| { (is_subword_start(left, right, &classifier)) || left == '\n' || right == '\n' })} pub fn is_subword_start(left: char, right: char, classifier: &CharClassifier) -> bool { let is_word_start = classifier.kind(left) != classifier.kind(right) && !right.is_whitespace(); let is_subword_start = classifier.is_word('-') && left == '-' && right != '-' || left == '_' && right != '_' || left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase(); is_word_start || is_subword_start} /// Returns a position of the next word boundary, where a word character is defined as either/// uppercase letter, lowercase letter, '_' character or language-specific word character (like '-' in CSS).pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let classifier = map.buffer_snapshot().char_classifier_at(raw_point); let mut is_first_iteration = true; find_boundary(map, point, FindRange::MultiLine, &mut |left, right| { // Make alt-right skip punctuation to respect VSCode behaviour. For example: |.hello goes to .hello| if is_first_iteration && classifier.is_punctuation(left) && !classifier.is_punctuation(right) && right != '\n' { is_first_iteration = false; return false; } is_first_iteration = false; (classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(left)) || right == '\n' })} /// Returns a position of the next word boundary, where a word character is defined as either/// uppercase letter, lowercase letter, '_' character, language-specific word character (like '-' in CSS) or newline.pub fn next_word_end_or_newline(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let classifier = map.buffer_snapshot().char_classifier_at(raw_point); let mut on_starting_row = true; find_boundary(map, point, FindRange::MultiLine, &mut |left, right| { if left == '\n' { on_starting_row = false; } (classifier.kind(left) != classifier.kind(right) && ((on_starting_row && !left.is_whitespace()) || (!on_starting_row && !right.is_whitespace()))) || right == '\n' })} /// Returns a position of the next subword boundary, where a subword is defined as a run of/// word characters of the same "subkind" - where subcharacter kinds are '_' character,/// lowerspace characters and uppercase characters.pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let classifier = map.buffer_snapshot().char_classifier_at(raw_point); find_boundary(map, point, FindRange::MultiLine, &mut |left, right| { is_subword_end(left, right, &classifier) || left == '\n' || right == '\n' })} /// Returns a position of the next subword boundary, where a subword is defined as a run of/// word characters of the same "subkind" - where subcharacter kinds are '_' character,/// lowerspace characters and uppercase characters or newline.pub fn next_subword_end_or_newline(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let classifier = map.buffer_snapshot().char_classifier_at(raw_point); let mut on_starting_row = true; find_boundary(map, point, FindRange::MultiLine, &mut |left, right| {Lines 1–500 of 1399. Load more
//! Movement module contains helper functions for calculating intended position //! in editor given a given motion (e.g. it handles converting a "move left" command into coordinates in editor). It is exposed mostly for use by vim crate. use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; use crate::{ DisplayRow, EditorStyle, ToOffset, ToPoint, scroll::{ScrollOffset, SharedScrollAnchor}, }; use gpui::{Pixels, WindowTextSystem}; use language::{CharClassifier, Point}; use multi_buffer::{MultiBufferOffset, MultiBufferRow, MultiBufferSnapshot}; use serde::Deserialize; use workspace::searchable::Direction; use std::{ops::Range, sync::Arc}; /// Defines search strategy for items in `movement` module. /// `FindRange::SingeLine` only looks for a match on a single line at a time, whereas /// `FindRange::MultiLine` keeps going until the end of a string. #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)] pub enum FindRange { SingleLine, MultiLine, } /// TextLayoutDetails encompasses everything we need to move vertically /// taking into account variable width characters. pub struct TextLayoutDetails { pub(crate) text_system: Arc<WindowTextSystem>, pub(crate) editor_style: EditorStyle, pub(crate) rem_size: Pixels, pub scroll_anchor: SharedScrollAnchor, pub visible_rows: Option<f64>, pub vertical_scroll_margin: ScrollOffset, } /// Returns a column to the left of the current point, wrapping /// to the previous line if that point is at the start of line. pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { if point.column() > 0 { *point.column_mut() -= 1; } else if point.row().0 > 0 { *point.row_mut() -= 1; *point.column_mut() = map.line_len(point.row()); } map.clip_point(point, Bias::Left) } /// Returns a column to the left of the current point, doing nothing if /// that point is already at the start of line. pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { if point.column() > 0 { *point.column_mut() -= 1; } else if point.column() == 0 { // If the current sofr_wrap mode is used, the column corresponding to the display is 0, // which does not necessarily mean that the actual beginning of a paragraph if map.display_point_to_fold_point(point, Bias::Left).column() > 0 { return left(map, point); } } map.clip_point(point, Bias::Left) } /// Returns a column to the right of the current point, wrapping /// to the next line if that point is at the end of line. pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { if point.column() < map.line_len(point.row()) { *point.column_mut() += 1; } else if point.row() < map.max_point().row() { *point.row_mut() += 1; *point.column_mut() = 0; } map.clip_point(point, Bias::Right) } /// Returns a column to the right of the current point, not performing any wrapping /// if that point is already at the end of line. pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { *point.column_mut() += 1; map.clip_point(point, Bias::Right) } /// Returns a display point for the preceding displayed line (which might be a soft-wrapped line). pub fn up( map: &DisplaySnapshot, start: DisplayPoint, goal: SelectionGoal, preserve_column_at_start: bool, text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { up_by_rows( map, start, 1, goal, preserve_column_at_start, text_layout_details, ) } /// Returns a display point for the next displayed line (which might be a soft-wrapped line). pub fn down( map: &DisplaySnapshot, start: DisplayPoint, goal: SelectionGoal, preserve_column_at_end: bool, text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { down_by_rows( map, start, 1, goal, preserve_column_at_end, text_layout_details, ) } pub(crate) fn up_by_rows( map: &DisplaySnapshot, start: DisplayPoint, row_count: u32, goal: SelectionGoal, preserve_column_at_start: bool, text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { let goal_x = match goal { SelectionGoal::HorizontalPosition(x) => x.into(), SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(), SelectionGoal::HorizontalRange { end, .. } => end.into(), _ => map.x_for_display_point(start, text_layout_details), }; let prev_row = DisplayRow(start.row().0.saturating_sub(row_count)); let mut point = map.clip_point( DisplayPoint::new(prev_row, map.line_len(prev_row)), Bias::Left, ); if point.row() < start.row() { *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details) } else if preserve_column_at_start { return (start, goal); } else { point = DisplayPoint::new(DisplayRow(0), 0); } let mut clipped_point = map.clip_point(point, Bias::Left); if clipped_point.row() < point.row() { clipped_point = map.clip_point(point, Bias::Right); } ( clipped_point, SelectionGoal::HorizontalPosition(goal_x.into()), ) } pub(crate) fn down_by_rows( map: &DisplaySnapshot, start: DisplayPoint, row_count: u32, goal: SelectionGoal, preserve_column_at_end: bool, text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { let goal_x = match goal { SelectionGoal::HorizontalPosition(x) => x.into(), SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(), SelectionGoal::HorizontalRange { end, .. } => end.into(), _ => map.x_for_display_point(start, text_layout_details), }; let new_row = DisplayRow(start.row().0 + row_count); let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right); if point.row() > start.row() { *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details) } else if preserve_column_at_end { return (start, goal); } else { point = map.max_point(); } let mut clipped_point = map.clip_point(point, Bias::Right); if clipped_point.row() > point.row() { clipped_point = map.clip_point(point, Bias::Left); } ( clipped_point, SelectionGoal::HorizontalPosition(goal_x.into()), ) } /// Returns a position of the start of line. /// If `stop_at_soft_boundaries` is true, the returned position is that of the /// displayed line (e.g. it could actually be in the middle of a text line if that line is soft-wrapped). /// Otherwise it's always going to be the start of a logical line. pub fn line_beginning( map: &DisplaySnapshot, display_point: DisplayPoint, stop_at_soft_boundaries: bool, ) -> DisplayPoint { let point = display_point.to_point(map); let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right); let line_start = map.prev_line_boundary(point).1; if stop_at_soft_boundaries && display_point != soft_line_start { soft_line_start } else { line_start } } /// Returns the last indented position on a given line. /// If `stop_at_soft_boundaries` is true, the returned [`DisplayPoint`] is that of a /// displayed line (e.g. if there's soft wrap it's gonna be returned), /// otherwise it's always going to be a start of a logical line. pub fn indented_line_beginning( map: &DisplaySnapshot, display_point: DisplayPoint, stop_at_soft_boundaries: bool, stop_at_indent: bool, ) -> DisplayPoint { let point = display_point.to_point(map); let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right); let indent_start = Point::new( point.row, map.buffer_snapshot() .indent_size_for_line(MultiBufferRow(point.row)) .len, ) .to_display_point(map); let line_start = map.prev_line_boundary(point).1; if stop_at_soft_boundaries && soft_line_start > indent_start && display_point != soft_line_start { soft_line_start } else if stop_at_indent && (display_point > indent_start || display_point == line_start) { indent_start } else { line_start } } /// Returns a position of the end of line. /// /// If `stop_at_soft_boundaries` is true, the returned position is that of the /// displayed line (e.g. it could actually be in the middle of a text line if that line is soft-wrapped). /// Otherwise it's always going to be the end of a logical line. pub fn line_end( map: &DisplaySnapshot, display_point: DisplayPoint, stop_at_soft_boundaries: bool, ) -> DisplayPoint { let soft_line_end = map.clip_point( DisplayPoint::new(display_point.row(), map.line_len(display_point.row())), Bias::Left, ); if stop_at_soft_boundaries && display_point != soft_line_end { soft_line_end } else { map.next_line_boundary(display_point.to_point(map)).1 } } /// Returns a position of the previous word boundary, where a word character is defined as either /// uppercase letter, lowercase letter, '_' character or language-specific word character (like '-' in CSS). pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let classifier = map.buffer_snapshot().char_classifier_at(raw_point); let mut is_first_iteration = true; find_preceding_boundary_display_point(map, point, FindRange::MultiLine, &mut |left, right| { // Make alt-left skip punctuation to respect VSCode behaviour. For example: hello.| goes to |hello. if is_first_iteration && classifier.is_punctuation(right) && !classifier.is_punctuation(left) && left != '\n' { is_first_iteration = false; return false; } is_first_iteration = false; (classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(right)) || left == '\n' }) } /// Returns a position of the previous word boundary, where a word character is defined as either /// uppercase letter, lowercase letter, '_' character, language-specific word character (like '-' in CSS) or newline. pub fn previous_word_start_or_newline(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let classifier = map.buffer_snapshot().char_classifier_at(raw_point); find_preceding_boundary_display_point(map, point, FindRange::MultiLine, &mut |left, right| { (classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(right)) || left == '\n' || right == '\n' }) } /// Text movements are too greedy, making deletions too greedy too. /// Makes deletions more ergonomic by potentially reducing the deletion range based on its text contents: /// * whitespace sequences with length >= 2 stop the deletion after removal (despite movement jumping over the word behind the whitespaces) /// * brackets stop the deletion after removal (despite movement currently not accounting for these and jumping over) pub fn adjust_greedy_deletion( map: &DisplaySnapshot, delete_from: DisplayPoint, delete_until: DisplayPoint, ignore_brackets: bool, ) -> DisplayPoint { if delete_from == delete_until { return delete_until; } let is_backward = delete_from > delete_until; let delete_range = if is_backward { map.display_point_to_point(delete_until, Bias::Left) .to_offset(map.buffer_snapshot()) ..map .display_point_to_point(delete_from, Bias::Right) .to_offset(map.buffer_snapshot()) } else { map.display_point_to_point(delete_from, Bias::Left) .to_offset(map.buffer_snapshot()) ..map .display_point_to_point(delete_until, Bias::Right) .to_offset(map.buffer_snapshot()) }; let trimmed_delete_range = if ignore_brackets { delete_range } else { let brackets_in_delete_range = map .buffer_snapshot() .bracket_ranges(delete_range.clone()) .into_iter() .flatten() .flat_map(|(left_bracket, right_bracket)| { [ left_bracket.start, left_bracket.end, right_bracket.start, right_bracket.end, ] }) .filter(|&bracket| delete_range.start < bracket && bracket < delete_range.end); let closest_bracket = if is_backward { brackets_in_delete_range.max() } else { brackets_in_delete_range.min() }; if is_backward { closest_bracket.unwrap_or(delete_range.start)..delete_range.end } else { delete_range.start..closest_bracket.unwrap_or(delete_range.end) } }; let mut whitespace_sequences = Vec::new(); let mut current_offset = trimmed_delete_range.start; let mut whitespace_sequence_length = MultiBufferOffset(0); let mut whitespace_sequence_start = MultiBufferOffset(0); for ch in map .buffer_snapshot() .text_for_range(trimmed_delete_range.clone()) .flat_map(str::chars) { if ch.is_whitespace() { if whitespace_sequence_length == MultiBufferOffset(0) { whitespace_sequence_start = current_offset; } whitespace_sequence_length += 1; } else { if whitespace_sequence_length >= MultiBufferOffset(2) { whitespace_sequences.push((whitespace_sequence_start, current_offset)); } whitespace_sequence_start = MultiBufferOffset(0); whitespace_sequence_length = MultiBufferOffset(0); } current_offset += ch.len_utf8(); } if whitespace_sequence_length >= MultiBufferOffset(2) { whitespace_sequences.push((whitespace_sequence_start, current_offset)); } let closest_whitespace_end = if is_backward { whitespace_sequences.last().map(|&(start, _)| start) } else { whitespace_sequences.first().map(|&(_, end)| end) }; closest_whitespace_end .unwrap_or_else(|| { if is_backward { trimmed_delete_range.start } else { trimmed_delete_range.end } }) .to_display_point(map) } /// Returns a position of the previous subword boundary, where a subword is defined as a run of /// word characters of the same "subkind" - where subcharacter kinds are '_' character, /// lowerspace characters and uppercase characters. pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let classifier = map.buffer_snapshot().char_classifier_at(raw_point); find_preceding_boundary_display_point(map, point, FindRange::MultiLine, &mut |left, right| { is_subword_start(left, right, &classifier) || left == '\n' || right == '\n' }) } /// Returns a position of the previous subword boundary, where a subword is defined as a run of /// word characters of the same "subkind" - where subcharacter kinds are '_' character, /// lowerspace characters and uppercase characters or newline. pub fn previous_subword_start_or_newline( map: &DisplaySnapshot, point: DisplayPoint, ) -> DisplayPoint { let raw_point = point.to_point(map); let classifier = map.buffer_snapshot().char_classifier_at(raw_point); find_preceding_boundary_display_point(map, point, FindRange::MultiLine, &mut |left, right| { (is_subword_start(left, right, &classifier)) || left == '\n' || right == '\n' }) } pub fn is_subword_start(left: char, right: char, classifier: &CharClassifier) -> bool { let is_word_start = classifier.kind(left) != classifier.kind(right) && !right.is_whitespace(); let is_subword_start = classifier.is_word('-') && left == '-' && right != '-' || left == '_' && right != '_' || left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase(); is_word_start || is_subword_start } /// Returns a position of the next word boundary, where a word character is defined as either /// uppercase letter, lowercase letter, '_' character or language-specific word character (like '-' in CSS). pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let classifier = map.buffer_snapshot().char_classifier_at(raw_point); let mut is_first_iteration = true; find_boundary(map, point, FindRange::MultiLine, &mut |left, right| { // Make alt-right skip punctuation to respect VSCode behaviour. For example: |.hello goes to .hello| if is_first_iteration && classifier.is_punctuation(left) && !classifier.is_punctuation(right) && right != '\n' { is_first_iteration = false; return false; } is_first_iteration = false; (classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(left)) || right == '\n' }) } /// Returns a position of the next word boundary, where a word character is defined as either /// uppercase letter, lowercase letter, '_' character, language-specific word character (like '-' in CSS) or newline. pub fn next_word_end_or_newline(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let classifier = map.buffer_snapshot().char_classifier_at(raw_point); let mut on_starting_row = true; find_boundary(map, point, FindRange::MultiLine, &mut |left, right| { if left == '\n' { on_starting_row = false; } (classifier.kind(left) != classifier.kind(right) && ((on_starting_row && !left.is_whitespace()) || (!on_starting_row && !right.is_whitespace()))) || right == '\n' }) } /// Returns a position of the next subword boundary, where a subword is defined as a run of /// word characters of the same "subkind" - where subcharacter kinds are '_' character, /// lowerspace characters and uppercase characters. pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let classifier = map.buffer_snapshot().char_classifier_at(raw_point); find_boundary(map, point, FindRange::MultiLine, &mut |left, right| { is_subword_end(left, right, &classifier) || left == '\n' || right == '\n' }) } /// Returns a position of the next subword boundary, where a subword is defined as a run of /// word characters of the same "subkind" - where subcharacter kinds are '_' character, /// lowerspace characters and uppercase characters or newline. pub fn next_subword_end_or_newline(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let classifier = map.buffer_snapshot().char_classifier_at(raw_point); let mut on_starting_row = true; find_boundary(map, point, FindRange::MultiLine, &mut |left, right| {