@@ -5,6 +5,7 @@ use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
use crate::{char_kind, scroll::ScrollAnchor, CharKind, EditorStyle, ToOffset, ToPoint};
use gpui::{px, Pixels, WindowTextSystem};
use language::Point;
+use multi_buffer::MultiBufferSnapshot;
use std::{ops::Range, sync::Arc};
@@ -254,7 +255,7 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa
let raw_point = point.to_point(map);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
- find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
+ find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
(char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace())
|| left == '\n'
})
@@ -267,7 +268,7 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis
let raw_point = point.to_point(map);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
- find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
+ find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
let is_word_start =
char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace();
let is_subword_start =
@@ -366,16 +367,16 @@ pub fn end_of_paragraph(
/// 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,
+pub fn find_preceding_boundary_point(
+ buffer_snapshot: &MultiBufferSnapshot,
+ from: Point,
find_range: FindRange,
mut is_boundary: impl FnMut(char, char) -> bool,
-) -> DisplayPoint {
+) -> Point {
let mut prev_ch = None;
- let mut offset = from.to_point(map).to_offset(&map.buffer_snapshot);
+ let mut offset = from.to_offset(&buffer_snapshot);
- for ch in map.buffer_snapshot.reversed_chars_at(offset) {
+ for ch in buffer_snapshot.reversed_chars_at(offset) {
if find_range == FindRange::SingleLine && ch == '\n' {
break;
}
@@ -389,7 +390,26 @@ pub fn find_preceding_boundary(
prev_ch = Some(ch);
}
- map.clip_point(offset.to_display_point(map), Bias::Left)
+ offset.to_point(&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.
+/// 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_display_point(
+ map: &DisplaySnapshot,
+ from: DisplayPoint,
+ find_range: FindRange,
+ is_boundary: impl FnMut(char, char) -> bool,
+) -> DisplayPoint {
+ let result = find_preceding_boundary_point(
+ &map.buffer_snapshot,
+ from.to_point(map),
+ find_range,
+ is_boundary,
+ );
+ map.clip_point(result.to_display_point(map), Bias::Left)
}
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
@@ -626,7 +646,7 @@ mod tests {
) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
- find_preceding_boundary(
+ find_preceding_boundary_display_point(
&snapshot,
display_points[1],
FindRange::MultiLine,
@@ -700,7 +720,7 @@ mod tests {
});
assert_eq!(
- find_preceding_boundary(
+ find_preceding_boundary_display_point(
&snapshot,
buffer_snapshot.len().to_display_point(&snapshot),
FindRange::MultiLine,
@@ -1,6 +1,8 @@
use editor::{
display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
- movement::{self, find_boundary, find_preceding_boundary, FindRange, TextLayoutDetails},
+ movement::{
+ self, find_boundary, find_preceding_boundary_display_point, FindRange, TextLayoutDetails,
+ },
Bias, DisplayPoint, ToOffset,
};
use gpui::{actions, impl_actions, px, ViewContext, WindowContext};
@@ -27,6 +29,7 @@ pub enum Motion {
NextWordStart { ignore_punctuation: bool },
NextWordEnd { ignore_punctuation: bool },
PreviousWordStart { ignore_punctuation: bool },
+ PreviousWordEnd { ignore_punctuation: bool },
FirstNonWhitespace { display_lines: bool },
CurrentLine,
StartOfLine { display_lines: bool },
@@ -70,6 +73,13 @@ struct PreviousWordStart {
ignore_punctuation: bool,
}
+#[derive(Clone, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+struct PreviousWordEnd {
+ #[serde(default)]
+ ignore_punctuation: bool,
+}
+
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Up {
@@ -114,6 +124,7 @@ impl_actions!(
Down,
Up,
PreviousWordStart,
+ PreviousWordEnd,
NextWordEnd,
NextWordStart
]
@@ -263,6 +274,11 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(|_: &mut Workspace, &WindowBottom, cx: _| {
motion(Motion::WindowBottom, cx)
});
+ workspace.register_action(
+ |_: &mut Workspace, &PreviousWordEnd { ignore_punctuation }, cx: _| {
+ motion(Motion::PreviousWordEnd { ignore_punctuation }, cx)
+ },
+ );
}
pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
@@ -315,6 +331,7 @@ impl Motion {
| GoToColumn
| NextWordStart { .. }
| PreviousWordStart { .. }
+ | PreviousWordEnd { .. }
| FirstNonWhitespace { .. }
| FindBackward { .. }
| RepeatFind { .. }
@@ -351,6 +368,7 @@ impl Motion {
| WindowTop
| WindowMiddle
| WindowBottom
+ | PreviousWordEnd { .. }
| NextLineStart => false,
}
}
@@ -371,6 +389,7 @@ impl Motion {
| WindowTop
| WindowMiddle
| WindowBottom
+ | PreviousWordEnd { .. }
| NextLineStart => true,
Left
| Backspace
@@ -431,6 +450,10 @@ impl Motion {
previous_word_start(map, point, *ignore_punctuation, times),
SelectionGoal::None,
),
+ PreviousWordEnd { ignore_punctuation } => (
+ previous_word_end(map, point, *ignore_punctuation, times),
+ SelectionGoal::None,
+ ),
FirstNonWhitespace { display_lines } => (
first_non_whitespace(map, *display_lines, point),
SelectionGoal::None,
@@ -840,13 +863,17 @@ 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.
- let new_point =
- movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
+ let new_point = movement::find_preceding_boundary_display_point(
+ map,
+ point,
+ FindRange::MultiLine,
+ |left, right| {
let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
(left_kind != right_kind && !right.is_whitespace()) || left == '\n'
- });
+ },
+ );
if point == new_point {
break;
}
@@ -1023,7 +1050,9 @@ fn find_backward(
for _ in 0..times {
let new_to =
- find_preceding_boundary(map, to, FindRange::SingleLine, |_, right| right == target);
+ find_preceding_boundary_display_point(map, to, FindRange::SingleLine, |_, right| {
+ right == target
+ });
if to == new_to {
break;
}
@@ -1147,6 +1176,44 @@ fn window_bottom(
}
}
+fn previous_word_end(
+ map: &DisplaySnapshot,
+ point: DisplayPoint,
+ ignore_punctuation: bool,
+ times: usize,
+) -> DisplayPoint {
+ let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
+ let mut point = point.to_point(map);
+
+ if point.column < map.buffer_snapshot.line_len(point.row) {
+ point.column += 1;
+ }
+ for _ in 0..times {
+ let new_point = movement::find_preceding_boundary_point(
+ &map.buffer_snapshot,
+ point,
+ FindRange::MultiLine,
+ |left, right| {
+ let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
+ let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
+ match (left_kind, right_kind) {
+ (CharKind::Punctuation, CharKind::Whitespace)
+ | (CharKind::Punctuation, CharKind::Word)
+ | (CharKind::Word, CharKind::Whitespace)
+ | (CharKind::Word, CharKind::Punctuation) => true,
+ (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
+ _ => false,
+ }
+ },
+ );
+ if new_point == point {
+ break;
+ }
+ point = new_point;
+ }
+ movement::saturating_left(map, point.to_display_point(map))
+}
+
#[cfg(test)]
mod test {
@@ -1564,4 +1631,98 @@ mod test {
"})
.await;
}
+
+ #[gpui::test]
+ async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+ cx.set_shared_state(indoc! {r"
+ 456 5Ė67 678
+ "})
+ .await;
+ cx.simulate_shared_keystrokes(["g", "e"]).await;
+ cx.assert_shared_state(indoc! {r"
+ 45Ė6 567 678
+ "})
+ .await;
+
+ // Test times
+ cx.set_shared_state(indoc! {r"
+ 123 234 345
+ 456 5Ė67 678
+ "})
+ .await;
+ cx.simulate_shared_keystrokes(["4", "g", "e"]).await;
+ cx.assert_shared_state(indoc! {r"
+ 12Ė3 234 345
+ 456 567 678
+ "})
+ .await;
+
+ // With punctuation
+ cx.set_shared_state(indoc! {r"
+ 123 234 345
+ 4;5.6 5Ė67 678
+ 789 890 901
+ "})
+ .await;
+ cx.simulate_shared_keystrokes(["g", "e"]).await;
+ cx.assert_shared_state(indoc! {r"
+ 123 234 345
+ 4;5.Ė6 567 678
+ 789 890 901
+ "})
+ .await;
+
+ // With punctuation and count
+ cx.set_shared_state(indoc! {r"
+ 123 234 345
+ 4;5.6 5Ė67 678
+ 789 890 901
+ "})
+ .await;
+ cx.simulate_shared_keystrokes(["5", "g", "e"]).await;
+ cx.assert_shared_state(indoc! {r"
+ 123 234 345
+ Ė4;5.6 567 678
+ 789 890 901
+ "})
+ .await;
+
+ // newlines
+ cx.set_shared_state(indoc! {r"
+ 123 234 345
+
+ 78Ė9 890 901
+ "})
+ .await;
+ cx.simulate_shared_keystrokes(["g", "e"]).await;
+ cx.assert_shared_state(indoc! {r"
+ 123 234 345
+ Ė
+ 789 890 901
+ "})
+ .await;
+ cx.simulate_shared_keystrokes(["g", "e"]).await;
+ cx.assert_shared_state(indoc! {r"
+ 123 234 34Ė5
+
+ 789 890 901
+ "})
+ .await;
+
+ // With punctuation
+ cx.set_shared_state(indoc! {r"
+ 123 234 345
+ 4;5.Ė6 567 678
+ 789 890 901
+ "})
+ .await;
+ cx.simulate_shared_keystrokes(["g", "shift-e"]).await;
+ cx.assert_shared_state(indoc! {r"
+ 123 234 34Ė5
+ 4;5.6 567 678
+ 789 890 901
+ "})
+ .await;
+ }
}