@@ -65,6 +65,8 @@ pub enum Motion {
EndOfLine {
display_lines: bool,
},
+ SentenceBackward,
+ SentenceForward,
StartOfParagraph,
EndOfParagraph,
StartOfDocument,
@@ -228,6 +230,8 @@ actions!(
Right,
Space,
CurrentLine,
+ SentenceForward,
+ SentenceBackward,
StartOfParagraph,
EndOfParagraph,
StartOfDocument,
@@ -306,6 +310,13 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, |vim, _: &EndOfParagraph, cx| {
vim.motion(Motion::EndOfParagraph, cx)
});
+
+ Vim::action(editor, cx, |vim, _: &SentenceForward, cx| {
+ vim.motion(Motion::SentenceForward, cx)
+ });
+ Vim::action(editor, cx, |vim, _: &SentenceBackward, cx| {
+ vim.motion(Motion::SentenceBackward, cx)
+ });
Vim::action(editor, cx, |vim, _: &StartOfDocument, cx| {
vim.motion(Motion::StartOfDocument, cx)
});
@@ -483,12 +494,14 @@ impl Motion {
| NextLineStart
| PreviousLineStart
| StartOfLineDownward
+ | SentenceBackward
+ | SentenceForward
| StartOfParagraph
+ | EndOfParagraph
| WindowTop
| WindowMiddle
| WindowBottom
- | Jump { line: true, .. }
- | EndOfParagraph => true,
+ | Jump { line: true, .. } => true,
EndOfLine { .. }
| Matching
| FindForward { .. }
@@ -533,6 +546,8 @@ impl Motion {
| StartOfLine { .. }
| StartOfParagraph
| EndOfParagraph
+ | SentenceBackward
+ | SentenceForward
| StartOfLineDownward
| EndOfLineDownward
| GoToColumn
@@ -586,6 +601,8 @@ impl Motion {
| StartOfLineDownward
| StartOfParagraph
| EndOfParagraph
+ | SentenceBackward
+ | SentenceForward
| GoToColumn
| NextWordStart { .. }
| PreviousWordStart { .. }
@@ -673,6 +690,8 @@ impl Motion {
end_of_line(map, *display_lines, point, times),
SelectionGoal::None,
),
+ SentenceBackward => (sentence_backwards(map, point, times), SelectionGoal::None),
+ SentenceForward => (sentence_forwards(map, point, times), SelectionGoal::None),
StartOfParagraph => (
movement::start_of_paragraph(map, point, times),
SelectionGoal::None,
@@ -1534,6 +1553,129 @@ pub(crate) fn end_of_line(
}
}
+fn sentence_backwards(
+ map: &DisplaySnapshot,
+ point: DisplayPoint,
+ mut times: usize,
+) -> DisplayPoint {
+ let mut start = point.to_point(&map).to_offset(&map.buffer_snapshot);
+ let mut chars = map.reverse_buffer_chars_at(start).peekable();
+
+ let mut was_newline = map
+ .buffer_chars_at(start)
+ .next()
+ .is_some_and(|(c, _)| c == '\n');
+
+ while let Some((ch, offset)) = chars.next() {
+ let start_of_next_sentence = if was_newline && ch == '\n' {
+ Some(offset + ch.len_utf8())
+ } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
+ Some(next_non_blank(map, offset + ch.len_utf8()))
+ } else if ch == '.' || ch == '?' || ch == '!' {
+ start_of_next_sentence(map, offset + ch.len_utf8())
+ } else {
+ None
+ };
+
+ if let Some(start_of_next_sentence) = start_of_next_sentence {
+ if start_of_next_sentence < start {
+ times = times.saturating_sub(1);
+ }
+ if times == 0 || offset == 0 {
+ return map.clip_point(
+ start_of_next_sentence
+ .to_offset(&map.buffer_snapshot)
+ .to_display_point(&map),
+ Bias::Left,
+ );
+ }
+ }
+ if was_newline {
+ start = offset;
+ }
+ was_newline = ch == '\n';
+ }
+
+ return DisplayPoint::zero();
+}
+
+fn sentence_forwards(map: &DisplaySnapshot, point: DisplayPoint, mut times: usize) -> DisplayPoint {
+ let start = point.to_point(&map).to_offset(&map.buffer_snapshot);
+ let mut chars = map.buffer_chars_at(start).peekable();
+
+ let mut was_newline = map
+ .reverse_buffer_chars_at(start)
+ .next()
+ .is_some_and(|(c, _)| c == '\n')
+ && chars.peek().is_some_and(|(c, _)| *c == '\n');
+
+ while let Some((ch, offset)) = chars.next() {
+ if was_newline && ch == '\n' {
+ continue;
+ }
+ let start_of_next_sentence = if was_newline {
+ Some(next_non_blank(map, offset))
+ } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
+ Some(next_non_blank(map, offset + ch.len_utf8()))
+ } else if ch == '.' || ch == '?' || ch == '!' {
+ start_of_next_sentence(map, offset + ch.len_utf8())
+ } else {
+ None
+ };
+
+ if let Some(start_of_next_sentence) = start_of_next_sentence {
+ times = times.saturating_sub(1);
+ if times == 0 {
+ return map.clip_point(
+ start_of_next_sentence
+ .to_offset(&map.buffer_snapshot)
+ .to_display_point(&map),
+ Bias::Right,
+ );
+ }
+ }
+
+ was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
+ }
+
+ return map.max_point();
+}
+
+fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
+ for (c, o) in map.buffer_chars_at(start) {
+ if c == '\n' || !c.is_whitespace() {
+ return o;
+ }
+ }
+
+ return map.buffer_snapshot.len();
+}
+
+// given the offset after a ., !, or ? find the start of the next sentence.
+// if this is not a sentence boundary, returns None.
+fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
+ let mut chars = map.buffer_chars_at(end_of_sentence);
+ let mut seen_space = false;
+
+ while let Some((char, offset)) = chars.next() {
+ if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
+ continue;
+ }
+
+ if char == '\n' && seen_space {
+ return Some(offset);
+ } else if char.is_whitespace() {
+ seen_space = true;
+ } else if seen_space {
+ return Some(offset);
+ } else {
+ return None;
+ }
+ }
+
+ return Some(map.buffer_snapshot.len());
+}
+
fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
*new_point.column_mut() = point.column();
@@ -0,0 +1,32 @@
+{"Put":{"state":"one\n\ntwo\nthree\nˇ\nfour"}}
+{"Key":"("}
+{"Get":{"state":"one\n\nˇtwo\nthree\n\nfour","mode":"Normal"}}
+{"Put":{"state":"hello.\n\n\nworˇld."}}
+{"Key":"("}
+{"Get":{"state":"hello.\n\n\nˇworld.","mode":"Normal"}}
+{"Key":"("}
+{"Get":{"state":"hello.\n\nˇ\nworld.","mode":"Normal"}}
+{"Key":"("}
+{"Get":{"state":"ˇhello.\n\n\nworld.","mode":"Normal"}}
+{"Put":{"state":"hello. worlˇd."}}
+{"Key":"("}
+{"Get":{"state":"hello. ˇworld.","mode":"Normal"}}
+{"Key":"("}
+{"Get":{"state":"ˇhello. world.","mode":"Normal"}}
+{"Put":{"state":". helˇlo."}}
+{"Key":"("}
+{"Get":{"state":". ˇhello.","mode":"Normal"}}
+{"Key":"("}
+{"Get":{"state":". ˇhello.","mode":"Normal"}}
+{"Put":{"state":"{\n hello_world();\nˇ}"}}
+{"Key":"("}
+{"Get":{"state":"ˇ{\n hello_world();\n}","mode":"Normal"}}
+{"Put":{"state":"Hello! World..?\n\n\tHello! World... ˇ"}}
+{"Key":"("}
+{"Get":{"state":"Hello! World..?\n\n\tHello! ˇWorld... ","mode":"Normal"}}
+{"Key":"("}
+{"Get":{"state":"Hello! World..?\n\n\tˇHello! World... ","mode":"Normal"}}
+{"Key":"("}
+{"Get":{"state":"Hello! World..?\nˇ\n\tHello! World... ","mode":"Normal"}}
+{"Key":"("}
+{"Get":{"state":"Hello! ˇWorld..?\n\n\tHello! World... ","mode":"Normal"}}