mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-18 18:08:07 +03:00
vim: Sentence motion (#17425)
Closes #12161 Release Notes: - vim: Added `(` and `)` for sentence motion
This commit is contained in:
parent
01e40928d8
commit
1e09884a22
@ -29,6 +29,8 @@
|
|||||||
"shift-g": "vim::EndOfDocument",
|
"shift-g": "vim::EndOfDocument",
|
||||||
"{": "vim::StartOfParagraph",
|
"{": "vim::StartOfParagraph",
|
||||||
"}": "vim::EndOfParagraph",
|
"}": "vim::EndOfParagraph",
|
||||||
|
"(": "vim::SentenceBackward",
|
||||||
|
")": "vim::SentenceForward",
|
||||||
"|": "vim::GoToColumn",
|
"|": "vim::GoToColumn",
|
||||||
// Word motions
|
// Word motions
|
||||||
"w": "vim::NextWordStart",
|
"w": "vim::NextWordStart",
|
||||||
|
@ -65,6 +65,8 @@ pub enum Motion {
|
|||||||
EndOfLine {
|
EndOfLine {
|
||||||
display_lines: bool,
|
display_lines: bool,
|
||||||
},
|
},
|
||||||
|
SentenceBackward,
|
||||||
|
SentenceForward,
|
||||||
StartOfParagraph,
|
StartOfParagraph,
|
||||||
EndOfParagraph,
|
EndOfParagraph,
|
||||||
StartOfDocument,
|
StartOfDocument,
|
||||||
@ -228,6 +230,8 @@ actions!(
|
|||||||
Right,
|
Right,
|
||||||
Space,
|
Space,
|
||||||
CurrentLine,
|
CurrentLine,
|
||||||
|
SentenceForward,
|
||||||
|
SentenceBackward,
|
||||||
StartOfParagraph,
|
StartOfParagraph,
|
||||||
EndOfParagraph,
|
EndOfParagraph,
|
||||||
StartOfDocument,
|
StartOfDocument,
|
||||||
@ -306,6 +310,13 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
|
|||||||
Vim::action(editor, cx, |vim, _: &EndOfParagraph, cx| {
|
Vim::action(editor, cx, |vim, _: &EndOfParagraph, cx| {
|
||||||
vim.motion(Motion::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::action(editor, cx, |vim, _: &StartOfDocument, cx| {
|
||||||
vim.motion(Motion::StartOfDocument, cx)
|
vim.motion(Motion::StartOfDocument, cx)
|
||||||
});
|
});
|
||||||
@ -483,12 +494,14 @@ impl Motion {
|
|||||||
| NextLineStart
|
| NextLineStart
|
||||||
| PreviousLineStart
|
| PreviousLineStart
|
||||||
| StartOfLineDownward
|
| StartOfLineDownward
|
||||||
|
| SentenceBackward
|
||||||
|
| SentenceForward
|
||||||
| StartOfParagraph
|
| StartOfParagraph
|
||||||
|
| EndOfParagraph
|
||||||
| WindowTop
|
| WindowTop
|
||||||
| WindowMiddle
|
| WindowMiddle
|
||||||
| WindowBottom
|
| WindowBottom
|
||||||
| Jump { line: true, .. }
|
| Jump { line: true, .. } => true,
|
||||||
| EndOfParagraph => true,
|
|
||||||
EndOfLine { .. }
|
EndOfLine { .. }
|
||||||
| Matching
|
| Matching
|
||||||
| FindForward { .. }
|
| FindForward { .. }
|
||||||
@ -533,6 +546,8 @@ impl Motion {
|
|||||||
| StartOfLine { .. }
|
| StartOfLine { .. }
|
||||||
| StartOfParagraph
|
| StartOfParagraph
|
||||||
| EndOfParagraph
|
| EndOfParagraph
|
||||||
|
| SentenceBackward
|
||||||
|
| SentenceForward
|
||||||
| StartOfLineDownward
|
| StartOfLineDownward
|
||||||
| EndOfLineDownward
|
| EndOfLineDownward
|
||||||
| GoToColumn
|
| GoToColumn
|
||||||
@ -586,6 +601,8 @@ impl Motion {
|
|||||||
| StartOfLineDownward
|
| StartOfLineDownward
|
||||||
| StartOfParagraph
|
| StartOfParagraph
|
||||||
| EndOfParagraph
|
| EndOfParagraph
|
||||||
|
| SentenceBackward
|
||||||
|
| SentenceForward
|
||||||
| GoToColumn
|
| GoToColumn
|
||||||
| NextWordStart { .. }
|
| NextWordStart { .. }
|
||||||
| PreviousWordStart { .. }
|
| PreviousWordStart { .. }
|
||||||
@ -673,6 +690,8 @@ impl Motion {
|
|||||||
end_of_line(map, *display_lines, point, times),
|
end_of_line(map, *display_lines, point, times),
|
||||||
SelectionGoal::None,
|
SelectionGoal::None,
|
||||||
),
|
),
|
||||||
|
SentenceBackward => (sentence_backwards(map, point, times), SelectionGoal::None),
|
||||||
|
SentenceForward => (sentence_forwards(map, point, times), SelectionGoal::None),
|
||||||
StartOfParagraph => (
|
StartOfParagraph => (
|
||||||
movement::start_of_paragraph(map, point, times),
|
movement::start_of_paragraph(map, point, times),
|
||||||
SelectionGoal::None,
|
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 {
|
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);
|
let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
|
||||||
*new_point.column_mut() = point.column();
|
*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.simulate_shared_keystrokes(".").await;
|
||||||
cx.shared_state().await.assert_eq("ˇhello world"); // takes a _long_ time
|
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 New Issue
Block a user