mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-26 18:41:10 +00:00
vim: Sentence motion (#17425)
Closes #12161 Release Notes: - vim: Added `(` and `)` for sentence motion
This commit is contained in:
parent
01e40928d8
commit
1e09884a22
5 changed files with 276 additions and 2 deletions
|
@ -29,6 +29,8 @@
|
|||
"shift-g": "vim::EndOfDocument",
|
||||
"{": "vim::StartOfParagraph",
|
||||
"}": "vim::EndOfParagraph",
|
||||
"(": "vim::SentenceBackward",
|
||||
")": "vim::SentenceForward",
|
||||
"|": "vim::GoToColumn",
|
||||
// Word motions
|
||||
"w": "vim::NextWordStart",
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -1426,3 +1426,93 @@ async fn test_record_replay_recursion(cx: &mut gpui::TestAppContext) {
|
|||
cx.simulate_shared_keystrokes(".").await;
|
||||
cx.shared_state().await.assert_eq("ˇhello world"); // takes a _long_ time
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_sentence_backwards(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("one\n\ntwo\nthree\nˇ\nfour").await;
|
||||
cx.simulate_shared_keystrokes("(").await;
|
||||
cx.shared_state()
|
||||
.await
|
||||
.assert_eq("one\n\nˇtwo\nthree\n\nfour");
|
||||
|
||||
cx.set_shared_state("hello.\n\n\nworˇld.").await;
|
||||
cx.simulate_shared_keystrokes("(").await;
|
||||
cx.shared_state().await.assert_eq("hello.\n\n\nˇworld.");
|
||||
cx.simulate_shared_keystrokes("(").await;
|
||||
cx.shared_state().await.assert_eq("hello.\n\nˇ\nworld.");
|
||||
cx.simulate_shared_keystrokes("(").await;
|
||||
cx.shared_state().await.assert_eq("ˇhello.\n\n\nworld.");
|
||||
|
||||
cx.set_shared_state("hello. worlˇd.").await;
|
||||
cx.simulate_shared_keystrokes("(").await;
|
||||
cx.shared_state().await.assert_eq("hello. ˇworld.");
|
||||
cx.simulate_shared_keystrokes("(").await;
|
||||
cx.shared_state().await.assert_eq("ˇhello. world.");
|
||||
|
||||
cx.set_shared_state(". helˇlo.").await;
|
||||
cx.simulate_shared_keystrokes("(").await;
|
||||
cx.shared_state().await.assert_eq(". ˇhello.");
|
||||
cx.simulate_shared_keystrokes("(").await;
|
||||
cx.shared_state().await.assert_eq(". ˇhello.");
|
||||
|
||||
cx.set_shared_state(indoc! {
|
||||
"{
|
||||
hello_world();
|
||||
ˇ}"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("(").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {
|
||||
"ˇ{
|
||||
hello_world();
|
||||
}"
|
||||
});
|
||||
|
||||
cx.set_shared_state(indoc! {
|
||||
"Hello! World..?
|
||||
|
||||
\tHello! World... ˇ"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("(").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {
|
||||
"Hello! World..?
|
||||
|
||||
\tHello! ˇWorld... "
|
||||
});
|
||||
cx.simulate_shared_keystrokes("(").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {
|
||||
"Hello! World..?
|
||||
|
||||
\tˇHello! World... "
|
||||
});
|
||||
cx.simulate_shared_keystrokes("(").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {
|
||||
"Hello! World..?
|
||||
ˇ
|
||||
\tHello! World... "
|
||||
});
|
||||
cx.simulate_shared_keystrokes("(").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {
|
||||
"Hello! ˇWorld..?
|
||||
|
||||
\tHello! World... "
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_sentence_forwards(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state("helˇlo.\n\n\nworld.").await;
|
||||
cx.simulate_shared_keystrokes(")").await;
|
||||
cx.shared_state().await.assert_eq("hello.\nˇ\n\nworld.");
|
||||
cx.simulate_shared_keystrokes(")").await;
|
||||
cx.shared_state().await.assert_eq("hello.\n\n\nˇworld.");
|
||||
cx.simulate_shared_keystrokes(")").await;
|
||||
cx.shared_state().await.assert_eq("hello.\n\n\nworldˇ.");
|
||||
|
||||
cx.set_shared_state("helˇlo.\n\n\nworld.").await;
|
||||
}
|
||||
|
|
32
crates/vim/test_data/test_sentence_backwards.json
Normal file
32
crates/vim/test_data/test_sentence_backwards.json
Normal file
|
@ -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"}}
|
8
crates/vim/test_data/test_sentence_forwards.json
Normal file
8
crates/vim/test_data/test_sentence_forwards.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{"Put":{"state":"helˇlo.\n\n\nworld."}}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"hello.\nˇ\n\nworld.","mode":"Normal"}}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"hello.\n\n\nˇworld.","mode":"Normal"}}
|
||||
{"Key":")"}
|
||||
{"Get":{"state":"hello.\n\n\nworldˇ.","mode":"Normal"}}
|
||||
{"Put":{"state":"helˇlo.\n\n\nworld."}}
|
Loading…
Reference in a new issue