Detailed changes
@@ -1,8 +1,14 @@
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
-use crate::{char_kind, CharKind, ToPoint};
+use crate::{char_kind, CharKind, ToOffset, ToPoint};
use language::Point;
use std::ops::Range;
+#[derive(Debug, PartialEq)]
+pub enum FindRange {
+ SingleLine,
+ MultiLine,
+}
+
pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
if point.column() > 0 {
*point.column_mut() -= 1;
@@ -179,7 +185,7 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa
let raw_point = point.to_point(map);
let language = map.buffer_snapshot.language_at(raw_point);
- find_preceding_boundary(map, point, |left, right| {
+ find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
(char_kind(language, left) != char_kind(language, right) && !right.is_whitespace())
|| left == '\n'
})
@@ -188,7 +194,7 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa
pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
let language = map.buffer_snapshot.language_at(raw_point);
- find_preceding_boundary(map, point, |left, right| {
+ find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
let is_word_start =
char_kind(language, left) != char_kind(language, right) && !right.is_whitespace();
let is_subword_start =
@@ -200,7 +206,7 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis
pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
let language = map.buffer_snapshot.language_at(raw_point);
- find_boundary(map, point, |left, right| {
+ find_boundary(map, point, FindRange::MultiLine, |left, right| {
(char_kind(language, left) != char_kind(language, right) && !left.is_whitespace())
|| right == '\n'
})
@@ -209,7 +215,7 @@ pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint
pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
let language = map.buffer_snapshot.language_at(raw_point);
- find_boundary(map, point, |left, right| {
+ find_boundary(map, point, FindRange::MultiLine, |left, right| {
let is_word_end =
(char_kind(language, left) != char_kind(language, right)) && !left.is_whitespace();
let is_subword_end =
@@ -272,79 +278,34 @@ pub fn end_of_paragraph(
map.max_point()
}
-/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
-/// given predicate returning true. The predicate is called with the character to the left and right
-/// of the candidate boundary location, and will be called with `\n` characters indicating the start
-/// or end of a line.
+/// Scans for a boundary preceding the given start point `from` until a boundary is found,
+/// indicated by the given predicate returning true.
+/// The predicate is called with the character to the left and right of the candidate boundary location.
+/// If FindRange::SingleLine is specified and no boundary is found before the start of the current line, the start of the current line will be returned.
pub fn find_preceding_boundary(
map: &DisplaySnapshot,
from: DisplayPoint,
+ find_range: FindRange,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> DisplayPoint {
- let mut start_column = 0;
- let mut soft_wrap_row = from.row() + 1;
-
- let mut prev = None;
- for (ch, point) in map.reverse_chars_at(from) {
- // Recompute soft_wrap_indent if the row has changed
- if point.row() != soft_wrap_row {
- soft_wrap_row = point.row();
-
- if point.row() == 0 {
- start_column = 0;
- } else if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
- start_column = indent;
- }
- }
-
- // If the current point is in the soft_wrap, skip comparing it
- if point.column() < start_column {
- continue;
- }
-
- if let Some((prev_ch, prev_point)) = prev {
- if is_boundary(ch, prev_ch) {
- return map.clip_point(prev_point, Bias::Left);
- }
- }
-
- prev = Some((ch, point));
- }
- map.clip_point(DisplayPoint::zero(), Bias::Left)
-}
+ let mut prev_ch = None;
+ let mut offset = from.to_point(map).to_offset(&map.buffer_snapshot);
-/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
-/// given predicate returning true. The predicate is called with the character to the left and right
-/// of the candidate boundary location, and will be called with `\n` characters indicating the start
-/// or end of a line. If no boundary is found, the start of the line is returned.
-pub fn find_preceding_boundary_in_line(
- map: &DisplaySnapshot,
- from: DisplayPoint,
- mut is_boundary: impl FnMut(char, char) -> bool,
-) -> DisplayPoint {
- let mut start_column = 0;
- if from.row() > 0 {
- if let Some(indent) = map.soft_wrap_indent(from.row() - 1) {
- start_column = indent;
+ for ch in map.buffer_snapshot.reversed_chars_at(offset) {
+ if find_range == FindRange::SingleLine && ch == '\n' {
+ break;
}
- }
-
- let mut prev = None;
- for (ch, point) in map.reverse_chars_at(from) {
- if let Some((prev_ch, prev_point)) = prev {
+ if let Some(prev_ch) = prev_ch {
if is_boundary(ch, prev_ch) {
- return map.clip_point(prev_point, Bias::Left);
+ break;
}
}
- if ch == '\n' || point.column() < start_column {
- break;
- }
-
- prev = Some((ch, point));
+ offset -= ch.len_utf8();
+ prev_ch = Some(ch);
}
- map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Left)
+ map.clip_point(offset.to_display_point(map), Bias::Left)
}
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
@@ -354,47 +315,26 @@ pub fn find_preceding_boundary_in_line(
pub fn find_boundary(
map: &DisplaySnapshot,
from: DisplayPoint,
+ find_range: FindRange,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> DisplayPoint {
+ let mut offset = from.to_offset(&map, Bias::Right);
let mut prev_ch = None;
- for (ch, point) in map.chars_at(from) {
+
+ for ch in map.buffer_snapshot.chars_at(offset) {
+ if find_range == FindRange::SingleLine && ch == '\n' {
+ break;
+ }
if let Some(prev_ch) = prev_ch {
if is_boundary(prev_ch, ch) {
- return map.clip_point(point, Bias::Right);
+ break;
}
}
+ offset += ch.len_utf8();
prev_ch = Some(ch);
}
- map.clip_point(map.max_point(), Bias::Right)
-}
-
-/// Scans for a boundary following the given start point until a boundary is found, indicated by the
-/// given predicate returning true. The predicate is called with the character to the left and right
-/// of the candidate boundary location, and will be called with `\n` characters indicating the start
-/// or end of a line. If no boundary is found, the end of the line is returned
-pub fn find_boundary_in_line(
- map: &DisplaySnapshot,
- from: DisplayPoint,
- mut is_boundary: impl FnMut(char, char) -> bool,
-) -> DisplayPoint {
- let mut prev = None;
- for (ch, point) in map.chars_at(from) {
- if let Some((prev_ch, _)) = prev {
- if is_boundary(prev_ch, ch) {
- return map.clip_point(point, Bias::Right);
- }
- }
-
- prev = Some((ch, point));
-
- if ch == '\n' {
- break;
- }
- }
-
- // Return the last position checked so that we give a point right before the newline or eof.
- map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Right)
+ map.clip_point(offset.to_display_point(map), Bias::Right)
}
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
@@ -533,7 +473,12 @@ mod tests {
) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
- find_preceding_boundary(&snapshot, display_points[1], is_boundary),
+ find_preceding_boundary(
+ &snapshot,
+ display_points[1],
+ FindRange::MultiLine,
+ is_boundary
+ ),
display_points[0]
);
}
@@ -612,21 +557,15 @@ mod tests {
find_preceding_boundary(
&snapshot,
buffer_snapshot.len().to_display_point(&snapshot),
- |left, _| left == 'a',
+ FindRange::MultiLine,
+ |left, _| left == 'e',
),
- 0.to_display_point(&snapshot),
+ snapshot
+ .buffer_snapshot
+ .offset_to_point(5)
+ .to_display_point(&snapshot),
"Should not stop at inlays when looking for boundaries"
);
-
- assert_eq!(
- find_preceding_boundary_in_line(
- &snapshot,
- buffer_snapshot.len().to_display_point(&snapshot),
- |left, _| left == 'a',
- ),
- 0.to_display_point(&snapshot),
- "Should not stop at inlays when looking for boundaries in line"
- );
}
#[gpui::test]
@@ -699,7 +638,12 @@ mod tests {
) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
- find_boundary(&snapshot, display_points[0], is_boundary),
+ find_boundary(
+ &snapshot,
+ display_points[0],
+ FindRange::MultiLine,
+ is_boundary
+ ),
display_points[1]
);
}
@@ -3,7 +3,8 @@ use std::{cmp, sync::Arc};
use editor::{
char_kind,
display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
- movement, Bias, CharKind, DisplayPoint, ToOffset,
+ movement::{self, FindRange},
+ Bias, CharKind, DisplayPoint, ToOffset,
};
use gpui::{actions, impl_actions, AppContext, WindowContext};
use language::{Point, Selection, SelectionGoal};
@@ -592,7 +593,7 @@ pub(crate) fn next_word_start(
let language = map.buffer_snapshot.language_at(point.to_point(map));
for _ in 0..times {
let mut crossed_newline = false;
- point = movement::find_boundary(map, point, |left, right| {
+ point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
let at_newline = right == '\n';
@@ -616,8 +617,14 @@ fn next_word_end(
) -> DisplayPoint {
let language = map.buffer_snapshot.language_at(point.to_point(map));
for _ in 0..times {
- *point.column_mut() += 1;
- point = movement::find_boundary(map, point, |left, right| {
+ if point.column() < map.line_len(point.row()) {
+ *point.column_mut() += 1;
+ } else if point.row() < map.max_buffer_row() {
+ *point.row_mut() += 1;
+ *point.column_mut() = 0;
+ }
+ // *point.column_mut() += 1;
+ point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
@@ -649,12 +656,13 @@ fn previous_word_start(
for _ in 0..times {
// This works even though find_preceding_boundary is called for every character in the line containing
// cursor because the newline is checked only once.
- point = movement::find_preceding_boundary(map, point, |left, right| {
- let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
- let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
+ point =
+ movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
+ let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
+ let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
- (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
- });
+ (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
+ });
}
point
}
@@ -445,7 +445,7 @@ mod test {
}
#[gpui::test]
- async fn test_e(cx: &mut gpui::TestAppContext) {
+ async fn test_end_of_word(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["e"]);
cx.assert_all(indoc! {"
Thˇe quicˇkˇ-browˇn
@@ -1,7 +1,10 @@
use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim};
use editor::{
- char_kind, display_map::DisplaySnapshot, movement, scroll::autoscroll::Autoscroll, CharKind,
- DisplayPoint,
+ char_kind,
+ display_map::DisplaySnapshot,
+ movement::{self, FindRange},
+ scroll::autoscroll::Autoscroll,
+ CharKind, DisplayPoint,
};
use gpui::WindowContext;
use language::Selection;
@@ -96,12 +99,15 @@ fn expand_changed_word_selection(
.unwrap_or_default();
if in_word {
- selection.end = movement::find_boundary(map, selection.end, |left, right| {
- let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
- let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
-
- left_kind != right_kind && left_kind != CharKind::Whitespace
- });
+ selection.end =
+ movement::find_boundary(map, selection.end, FindRange::MultiLine, |left, right| {
+ let left_kind =
+ char_kind(language, left).coerce_punctuation(ignore_punctuation);
+ let right_kind =
+ char_kind(language, right).coerce_punctuation(ignore_punctuation);
+
+ left_kind != right_kind && left_kind != CharKind::Whitespace
+ });
true
} else {
Motion::NextWordStart { ignore_punctuation }
@@ -1,6 +1,11 @@
use std::ops::Range;
-use editor::{char_kind, display_map::DisplaySnapshot, movement, Bias, CharKind, DisplayPoint};
+use editor::{
+ char_kind,
+ display_map::DisplaySnapshot,
+ movement::{self, FindRange},
+ Bias, CharKind, DisplayPoint,
+};
use gpui::{actions, impl_actions, AppContext, WindowContext};
use language::Selection;
use serde::Deserialize;
@@ -178,15 +183,16 @@ fn in_word(
) -> Option<Range<DisplayPoint>> {
// Use motion::right so that we consider the character under the cursor when looking for the start
let language = map.buffer_snapshot.language_at(relative_to.to_point(map));
- let start = movement::find_preceding_boundary_in_line(
+ let start = movement::find_preceding_boundary(
map,
right(map, relative_to, 1),
+ movement::FindRange::SingleLine,
|left, right| {
char_kind(language, left).coerce_punctuation(ignore_punctuation)
!= char_kind(language, right).coerce_punctuation(ignore_punctuation)
},
);
- let end = movement::find_boundary_in_line(map, relative_to, |left, right| {
+ let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
char_kind(language, left).coerce_punctuation(ignore_punctuation)
!= char_kind(language, right).coerce_punctuation(ignore_punctuation)
});
@@ -241,9 +247,10 @@ fn around_next_word(
) -> Option<Range<DisplayPoint>> {
let language = map.buffer_snapshot.language_at(relative_to.to_point(map));
// Get the start of the word
- let start = movement::find_preceding_boundary_in_line(
+ let start = movement::find_preceding_boundary(
map,
right(map, relative_to, 1),
+ FindRange::SingleLine,
|left, right| {
char_kind(language, left).coerce_punctuation(ignore_punctuation)
!= char_kind(language, right).coerce_punctuation(ignore_punctuation)
@@ -251,7 +258,7 @@ fn around_next_word(
);
let mut word_found = false;
- let end = movement::find_boundary(map, relative_to, |left, right| {
+ let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| {
let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
@@ -566,11 +573,18 @@ mod test {
async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
- cx.set_shared_state("The quick ˇbrown\nfox").await;
+ /*
+ cx.set_shared_state("The quick ˇbrown\nfox").await;
+ cx.simulate_shared_keystrokes(["v"]).await;
+ cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
+ cx.simulate_shared_keystrokes(["i", "w"]).await;
+ cx.assert_shared_state("The quick «brownˇ»\nfox").await;
+ */
+ cx.set_shared_state("The quick brown\nˇ\nfox").await;
cx.simulate_shared_keystrokes(["v"]).await;
- cx.assert_shared_state("The quick «bˇ»rown\nfox").await;
+ cx.assert_shared_state("The quick brown\n«\nˇ»fox").await;
cx.simulate_shared_keystrokes(["i", "w"]).await;
- cx.assert_shared_state("The quick «brownˇ»\nfox").await;
+ cx.assert_shared_state("The quick brown\n«\nˇ»fox").await;
cx.assert_binding_matches_all(["v", "i", "w"], WORD_LOCATIONS)
.await;
@@ -431,6 +431,24 @@ async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) {
twelve char
"})
.await;
+
+ // line wraps as:
+ // fourteen ch
+ // ar
+ // fourteen ch
+ // ar
+ cx.set_shared_state(indoc! { "
+ fourteen chaˇr
+ fourteen char
+ "})
+ .await;
+
+ cx.simulate_shared_keystrokes(["d", "i", "w"]).await;
+ cx.assert_shared_state(indoc! {"
+ fourteenˇ•
+ fourteen char
+ "})
+ .await;
}
#[gpui::test]
@@ -153,6 +153,7 @@ impl<'a> NeovimBackedTestContext<'a> {
}
pub async fn assert_shared_state(&mut self, marked_text: &str) {
+ let marked_text = marked_text.replace("•", " ");
let neovim = self.neovim_state().await;
let editor = self.editor_state();
if neovim == marked_text && neovim == editor {
@@ -184,9 +185,9 @@ impl<'a> NeovimBackedTestContext<'a> {
message,
initial_state,
self.recent_keystrokes.join(" "),
- marked_text,
- neovim,
- editor
+ marked_text.replace(" \n", "•\n"),
+ neovim.replace(" \n", "•\n"),
+ editor.replace(" \n", "•\n")
)
}
@@ -0,0 +1,32 @@
+{"Put":{"state":"Thˇe quick-brown\n\n\nfox_jumps over\nthe"}}
+{"Key":"e"}
+{"Get":{"state":"The quicˇk-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
+{"Key":"e"}
+{"Get":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}}
+{"Key":"e"}
+{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}}
+{"Key":"e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumpˇs over\nthe","mode":"Normal"}}
+{"Key":"e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumps oveˇr\nthe","mode":"Normal"}}
+{"Key":"e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}}
+{"Key":"e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}}
+{"Put":{"state":"Thˇe quick-brown\n\n\nfox_jumps over\nthe"}}
+{"Key":"shift-e"}
+{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}}
+{"Put":{"state":"The quicˇk-brown\n\n\nfox_jumps over\nthe"}}
+{"Key":"shift-e"}
+{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}}
+{"Put":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe"}}
+{"Key":"shift-e"}
+{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}}
+{"Key":"shift-e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumpˇs over\nthe","mode":"Normal"}}
+{"Key":"shift-e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumps oveˇr\nthe","mode":"Normal"}}
+{"Key":"shift-e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}}
+{"Key":"shift-e"}
+{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}}
@@ -1,9 +1,9 @@
-{"Put":{"state":"The quick ˇbrown\nfox"}}
+{"Put":{"state":"The quick brown\nˇ\nfox"}}
{"Key":"v"}
-{"Get":{"state":"The quick «bˇ»rown\nfox","mode":"Visual"}}
+{"Get":{"state":"The quick brown\n«\nˇ»fox","mode":"Visual"}}
{"Key":"i"}
{"Key":"w"}
-{"Get":{"state":"The quick «brownˇ»\nfox","mode":"Visual"}}
+{"Get":{"state":"The quick brown\n«\nˇ»fox","mode":"Visual"}}
{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}}
{"Key":"v"}
{"Key":"i"}
@@ -48,3 +48,8 @@
{"Key":"o"}
{"Key":"escape"}
{"Get":{"state":"twelve char\nˇo\ntwelve char twelve char\ntwelve char\n","mode":"Normal"}}
+{"Put":{"state":"fourteen chaˇr\nfourteen char\n"}}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"w"}
+{"Get":{"state":"fourteenˇ \nfourteen char\n","mode":"Normal"}}