Make word deletions less greedy (#37352)
Closes https://github.com/zed-industries/zed/issues/37144 Adjusts `editor::DeleteToPreviousWordStart`, `editor::DeleteToNextWordEnd`, `editor::DeleteToNextSubwordEnd` and `editor::DeleteToPreviousSubwordStart` actions to * take whitespace sequences with length >= 2 into account and stop after removing them (whilst movement would also include the word after such sequences) * take current language's brackets into account and stop after removing the text before them The latter is configurable and can be disabled with `"ignore_brackets": true` parameter in the action. Release Notes: - Improved word deletions to consider whitespace sequences and brackets by default
This commit is contained in:
@@ -2476,51 +2476,379 @@ async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
|
||||
async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let editor = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple("one two three four", cx);
|
||||
build_editor(buffer, window, cx)
|
||||
});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_display_ranges([
|
||||
// an empty selection - the preceding word fragment is deleted
|
||||
DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
|
||||
// characters selected - they are deleted
|
||||
DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
|
||||
])
|
||||
});
|
||||
// For an empty selection, the preceding word fragment is deleted.
|
||||
// For non-empty selections, only selected characters are deleted.
|
||||
cx.set_state("onˇe two t«hreˇ»e four");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_previous_word_start(
|
||||
&DeleteToPreviousWordStart {
|
||||
ignore_newlines: false,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
|
||||
});
|
||||
cx.assert_editor_state("ˇe two tˇe four");
|
||||
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_display_ranges([
|
||||
// an empty selection - the following word fragment is deleted
|
||||
DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
|
||||
// characters selected - they are deleted
|
||||
DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
|
||||
])
|
||||
});
|
||||
cx.set_state("e tˇwo te «fˇ»our");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_next_word_end(
|
||||
&DeleteToNextWordEnd {
|
||||
ignore_newlines: false,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
|
||||
});
|
||||
cx.assert_editor_state("e tˇ te ˇour");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_whitespaces(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
cx.set_state("here is some text ˇwith a space");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_previous_word_start(
|
||||
&DeleteToPreviousWordStart {
|
||||
ignore_newlines: false,
|
||||
ignore_brackets: true,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
// Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
|
||||
cx.assert_editor_state("here is some textˇwith a space");
|
||||
|
||||
cx.set_state("here is some text ˇwith a space");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_previous_word_start(
|
||||
&DeleteToPreviousWordStart {
|
||||
ignore_newlines: false,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.assert_editor_state("here is some textˇwith a space");
|
||||
|
||||
cx.set_state("here is some textˇ with a space");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_next_word_end(
|
||||
&DeleteToNextWordEnd {
|
||||
ignore_newlines: false,
|
||||
ignore_brackets: true,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
// Same happens in the other direction.
|
||||
cx.assert_editor_state("here is some textˇwith a space");
|
||||
|
||||
cx.set_state("here is some textˇ with a space");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_next_word_end(
|
||||
&DeleteToNextWordEnd {
|
||||
ignore_newlines: false,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.assert_editor_state("here is some textˇwith a space");
|
||||
|
||||
cx.set_state("here is some textˇ with a space");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_next_word_end(
|
||||
&DeleteToNextWordEnd {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.assert_editor_state("here is some textˇwith a space");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_previous_word_start(
|
||||
&DeleteToPreviousWordStart {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.assert_editor_state("here is some ˇwith a space");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_previous_word_start(
|
||||
&DeleteToPreviousWordStart {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
// Single whitespaces are removed with the word behind them.
|
||||
cx.assert_editor_state("here is ˇwith a space");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_previous_word_start(
|
||||
&DeleteToPreviousWordStart {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.assert_editor_state("here ˇwith a space");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_previous_word_start(
|
||||
&DeleteToPreviousWordStart {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.assert_editor_state("ˇwith a space");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_previous_word_start(
|
||||
&DeleteToPreviousWordStart {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.assert_editor_state("ˇwith a space");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_next_word_end(
|
||||
&DeleteToNextWordEnd {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
// Same happens in the other direction.
|
||||
cx.assert_editor_state("ˇ a space");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_next_word_end(
|
||||
&DeleteToNextWordEnd {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.assert_editor_state("ˇ space");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_next_word_end(
|
||||
&DeleteToNextWordEnd {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.assert_editor_state("ˇ");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_next_word_end(
|
||||
&DeleteToNextWordEnd {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.assert_editor_state("ˇ");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_previous_word_start(
|
||||
&DeleteToPreviousWordStart {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.assert_editor_state("ˇ");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_to_bracket(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let language = Arc::new(
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
brackets: BracketPairConfig {
|
||||
pairs: vec![
|
||||
BracketPair {
|
||||
start: "\"".to_string(),
|
||||
end: "\"".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
},
|
||||
BracketPair {
|
||||
start: "(".to_string(),
|
||||
end: ")".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: true,
|
||||
},
|
||||
],
|
||||
..BracketPairConfig::default()
|
||||
},
|
||||
..LanguageConfig::default()
|
||||
},
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
)
|
||||
.with_brackets_query(
|
||||
r#"
|
||||
("(" @open ")" @close)
|
||||
("\"" @open "\"" @close)
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||
|
||||
cx.set_state(r#"macro!("// ˇCOMMENT");"#);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_previous_word_start(
|
||||
&DeleteToPreviousWordStart {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
// Deletion stops before brackets if asked to not ignore them.
|
||||
cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_previous_word_start(
|
||||
&DeleteToPreviousWordStart {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
// Deletion has to remove a single bracket and then stop again.
|
||||
cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_previous_word_start(
|
||||
&DeleteToPreviousWordStart {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_previous_word_start(
|
||||
&DeleteToPreviousWordStart {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.assert_editor_state(r#"ˇCOMMENT");"#);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_previous_word_start(
|
||||
&DeleteToPreviousWordStart {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.assert_editor_state(r#"ˇCOMMENT");"#);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_next_word_end(
|
||||
&DeleteToNextWordEnd {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
// Brackets on the right are not paired anymore, hence deletion does not stop at them
|
||||
cx.assert_editor_state(r#"ˇ");"#);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_next_word_end(
|
||||
&DeleteToNextWordEnd {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.assert_editor_state(r#"ˇ"#);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_next_word_end(
|
||||
&DeleteToNextWordEnd {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.assert_editor_state(r#"ˇ"#);
|
||||
|
||||
cx.set_state(r#"macro!("// ˇCOMMENT");"#);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.delete_to_previous_word_start(
|
||||
&DeleteToPreviousWordStart {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: true,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.assert_editor_state(r#"macroˇCOMMENT");"#);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -2533,9 +2861,11 @@ fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
|
||||
});
|
||||
let del_to_prev_word_start = DeleteToPreviousWordStart {
|
||||
ignore_newlines: false,
|
||||
ignore_brackets: false,
|
||||
};
|
||||
let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: false,
|
||||
};
|
||||
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
@@ -2569,9 +2899,11 @@ fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
|
||||
});
|
||||
let del_to_next_word_end = DeleteToNextWordEnd {
|
||||
ignore_newlines: false,
|
||||
ignore_brackets: false,
|
||||
};
|
||||
let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
|
||||
ignore_newlines: true,
|
||||
ignore_brackets: false,
|
||||
};
|
||||
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
@@ -2600,6 +2932,8 @@ fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
|
||||
editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
|
||||
assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
|
||||
editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
|
||||
assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
|
||||
editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
|
||||
assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user