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:
@@ -63,8 +63,8 @@
|
||||
"ctrl-k": "editor::CutToEndOfLine",
|
||||
"ctrl-k ctrl-q": "editor::Rewrap",
|
||||
"ctrl-k q": "editor::Rewrap",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"cut": "editor::Cut",
|
||||
"shift-delete": "editor::Cut",
|
||||
"ctrl-x": "editor::Cut",
|
||||
|
||||
@@ -70,9 +70,9 @@
|
||||
"cmd-k q": "editor::Rewrap",
|
||||
"cmd-backspace": "editor::DeleteToBeginningOfLine",
|
||||
"cmd-delete": "editor::DeleteToEndOfLine",
|
||||
"alt-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-w": "editor::DeleteToPreviousWordStart",
|
||||
"alt-delete": "editor::DeleteToNextWordEnd",
|
||||
"alt-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-w": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"alt-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"cmd-x": "editor::Cut",
|
||||
"cmd-c": "editor::Copy",
|
||||
"cmd-v": "editor::Paste",
|
||||
|
||||
@@ -66,8 +66,8 @@
|
||||
"ctrl-k": "editor::CutToEndOfLine",
|
||||
"ctrl-k ctrl-q": "editor::Rewrap",
|
||||
"ctrl-k q": "editor::Rewrap",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"cut": "editor::Cut",
|
||||
"shift-delete": "editor::Cut",
|
||||
"ctrl-x": "editor::Cut",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||
"ctrl-d": "editor::Delete", // delete-char
|
||||
"alt-d": "editor::DeleteToNextWordEnd", // kill-word
|
||||
"alt-d": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], // kill-word
|
||||
"ctrl-k": "editor::KillRingCut", // kill-line
|
||||
"ctrl-w": "editor::Cut", // kill-region
|
||||
"alt-w": "editor::Copy", // kill-ring-save
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"ctrl-k ctrl-u": "editor::ConvertToUpperCase",
|
||||
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"alt-right": "editor::MoveToNextSubwordEnd",
|
||||
"alt-left": "editor::MoveToPreviousSubwordStart",
|
||||
"alt-shift-right": "editor::SelectToNextSubwordEnd",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||
"ctrl-d": "editor::Delete", // delete-char
|
||||
"alt-d": "editor::DeleteToNextWordEnd", // kill-word
|
||||
"alt-d": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], // kill-word
|
||||
"ctrl-k": "editor::KillRingCut", // kill-line
|
||||
"ctrl-w": "editor::Cut", // kill-region
|
||||
"alt-w": "editor::Copy", // kill-ring-save
|
||||
|
||||
@@ -52,8 +52,8 @@
|
||||
"cmd-k cmd-l": "editor::ConvertToLowerCase",
|
||||
"cmd-shift-j": "editor::JoinLines",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-right": "editor::MoveToNextSubwordEnd",
|
||||
"ctrl-left": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-shift-right": "editor::SelectToNextSubwordEnd",
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"alt-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"alt-shift-backspace": "editor::DeleteToNextWordEnd",
|
||||
"alt-delete": "editor::DeleteToNextWordEnd",
|
||||
"alt-shift-delete": "editor::DeleteToNextWordEnd",
|
||||
"alt-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"alt-shift-backspace": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"alt-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"alt-shift-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextSubwordEnd",
|
||||
"alt-left": ["editor::MoveToPreviousWordStart", { "stop_at_soft_wraps": true }],
|
||||
|
||||
@@ -337,7 +337,7 @@
|
||||
"ctrl-x ctrl-z": "editor::Cancel",
|
||||
"ctrl-x ctrl-e": "vim::LineDown",
|
||||
"ctrl-x ctrl-y": "vim::LineUp",
|
||||
"ctrl-w": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-w": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-u": "editor::DeleteToBeginningOfLine",
|
||||
"ctrl-t": "vim::Indent",
|
||||
"ctrl-d": "vim::Outdent",
|
||||
|
||||
@@ -228,21 +228,29 @@ pub struct ShowCompletions {
|
||||
pub struct HandleInput(pub String);
|
||||
|
||||
/// Deletes from the cursor to the end of the next word.
|
||||
/// Stops before the end of the next word, if whitespace sequences of length >= 2 are encountered.
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
||||
#[action(namespace = editor)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DeleteToNextWordEnd {
|
||||
#[serde(default)]
|
||||
pub ignore_newlines: bool,
|
||||
// Whether to stop before the end of the next word, if language-defined bracket is encountered.
|
||||
#[serde(default)]
|
||||
pub ignore_brackets: bool,
|
||||
}
|
||||
|
||||
/// Deletes from the cursor to the start of the previous word.
|
||||
/// Stops before the start of the previous word, if whitespace sequences of length >= 2 are encountered.
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
||||
#[action(namespace = editor)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DeleteToPreviousWordStart {
|
||||
#[serde(default)]
|
||||
pub ignore_newlines: bool,
|
||||
// Whether to stop before the start of the previous word, if language-defined bracket is encountered.
|
||||
#[serde(default)]
|
||||
pub ignore_brackets: bool,
|
||||
}
|
||||
|
||||
/// Folds all code blocks at the specified indentation level.
|
||||
|
||||
@@ -13153,11 +13153,17 @@ impl Editor {
|
||||
this.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
if selection.is_empty() {
|
||||
let cursor = if action.ignore_newlines {
|
||||
let mut cursor = if action.ignore_newlines {
|
||||
movement::previous_word_start(map, selection.head())
|
||||
} else {
|
||||
movement::previous_word_start_or_newline(map, selection.head())
|
||||
};
|
||||
cursor = movement::adjust_greedy_deletion(
|
||||
map,
|
||||
selection.head(),
|
||||
cursor,
|
||||
action.ignore_brackets,
|
||||
);
|
||||
selection.set_head(cursor, SelectionGoal::None);
|
||||
}
|
||||
});
|
||||
@@ -13178,7 +13184,9 @@ impl Editor {
|
||||
this.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
if selection.is_empty() {
|
||||
let cursor = movement::previous_subword_start(map, selection.head());
|
||||
let mut cursor = movement::previous_subword_start(map, selection.head());
|
||||
cursor =
|
||||
movement::adjust_greedy_deletion(map, selection.head(), cursor, false);
|
||||
selection.set_head(cursor, SelectionGoal::None);
|
||||
}
|
||||
});
|
||||
@@ -13254,11 +13262,17 @@ impl Editor {
|
||||
this.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
if selection.is_empty() {
|
||||
let cursor = if action.ignore_newlines {
|
||||
let mut cursor = if action.ignore_newlines {
|
||||
movement::next_word_end(map, selection.head())
|
||||
} else {
|
||||
movement::next_word_end_or_newline(map, selection.head())
|
||||
};
|
||||
cursor = movement::adjust_greedy_deletion(
|
||||
map,
|
||||
selection.head(),
|
||||
cursor,
|
||||
action.ignore_brackets,
|
||||
);
|
||||
selection.set_head(cursor, SelectionGoal::None);
|
||||
}
|
||||
});
|
||||
@@ -13278,7 +13292,9 @@ impl Editor {
|
||||
this.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
if selection.is_empty() {
|
||||
let cursor = movement::next_subword_end(map, selection.head());
|
||||
let mut cursor = movement::next_subword_end(map, selection.head());
|
||||
cursor =
|
||||
movement::adjust_greedy_deletion(map, selection.head(), cursor, false);
|
||||
selection.set_head(cursor, SelectionGoal::None);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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(), "");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -289,12 +289,114 @@ pub fn previous_word_start_or_newline(map: &DisplaySnapshot, point: DisplayPoint
|
||||
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
|
||||
|
||||
find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
|
||||
(classifier.kind(left) != classifier.kind(right) && !right.is_whitespace())
|
||||
(classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(right))
|
||||
|| left == '\n'
|
||||
|| right == '\n'
|
||||
})
|
||||
}
|
||||
|
||||
/// Text movements are too greedy, making deletions too greedy too.
|
||||
/// Makes deletions more ergonomic by potentially reducing the deletion range based on its text contents:
|
||||
/// * whitespace sequences with length >= 2 stop the deletion after removal (despite movement jumping over the word behind the whitespaces)
|
||||
/// * brackets stop the deletion after removal (despite movement currently not accounting for these and jumping over)
|
||||
pub fn adjust_greedy_deletion(
|
||||
map: &DisplaySnapshot,
|
||||
delete_from: DisplayPoint,
|
||||
delete_until: DisplayPoint,
|
||||
ignore_brackets: bool,
|
||||
) -> DisplayPoint {
|
||||
if delete_from == delete_until {
|
||||
return delete_until;
|
||||
}
|
||||
let is_backward = delete_from > delete_until;
|
||||
let delete_range = if is_backward {
|
||||
map.display_point_to_point(delete_until, Bias::Left)
|
||||
.to_offset(&map.buffer_snapshot)
|
||||
..map
|
||||
.display_point_to_point(delete_from, Bias::Right)
|
||||
.to_offset(&map.buffer_snapshot)
|
||||
} else {
|
||||
map.display_point_to_point(delete_from, Bias::Left)
|
||||
.to_offset(&map.buffer_snapshot)
|
||||
..map
|
||||
.display_point_to_point(delete_until, Bias::Right)
|
||||
.to_offset(&map.buffer_snapshot)
|
||||
};
|
||||
|
||||
let trimmed_delete_range = if ignore_brackets {
|
||||
delete_range
|
||||
} else {
|
||||
let brackets_in_delete_range = map
|
||||
.buffer_snapshot
|
||||
.bracket_ranges(delete_range.clone())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flat_map(|(left_bracket, right_bracket)| {
|
||||
[
|
||||
left_bracket.start,
|
||||
left_bracket.end,
|
||||
right_bracket.start,
|
||||
right_bracket.end,
|
||||
]
|
||||
})
|
||||
.filter(|&bracket| delete_range.start < bracket && bracket < delete_range.end);
|
||||
let closest_bracket = if is_backward {
|
||||
brackets_in_delete_range.max()
|
||||
} else {
|
||||
brackets_in_delete_range.min()
|
||||
};
|
||||
|
||||
if is_backward {
|
||||
closest_bracket.unwrap_or(delete_range.start)..delete_range.end
|
||||
} else {
|
||||
delete_range.start..closest_bracket.unwrap_or(delete_range.end)
|
||||
}
|
||||
};
|
||||
|
||||
let mut whitespace_sequences = Vec::new();
|
||||
let mut current_offset = trimmed_delete_range.start;
|
||||
let mut whitespace_sequence_length = 0;
|
||||
let mut whitespace_sequence_start = 0;
|
||||
for ch in map
|
||||
.buffer_snapshot
|
||||
.text_for_range(trimmed_delete_range.clone())
|
||||
.flat_map(str::chars)
|
||||
{
|
||||
if ch.is_whitespace() {
|
||||
if whitespace_sequence_length == 0 {
|
||||
whitespace_sequence_start = current_offset;
|
||||
}
|
||||
whitespace_sequence_length += 1;
|
||||
} else {
|
||||
if whitespace_sequence_length >= 2 {
|
||||
whitespace_sequences.push((whitespace_sequence_start, current_offset));
|
||||
}
|
||||
whitespace_sequence_start = 0;
|
||||
whitespace_sequence_length = 0;
|
||||
}
|
||||
current_offset += ch.len_utf8();
|
||||
}
|
||||
if whitespace_sequence_length >= 2 {
|
||||
whitespace_sequences.push((whitespace_sequence_start, current_offset));
|
||||
}
|
||||
|
||||
let closest_whitespace_end = if is_backward {
|
||||
whitespace_sequences.last().map(|&(start, _)| start)
|
||||
} else {
|
||||
whitespace_sequences.first().map(|&(_, end)| end)
|
||||
};
|
||||
|
||||
closest_whitespace_end
|
||||
.unwrap_or_else(|| {
|
||||
if is_backward {
|
||||
trimmed_delete_range.start
|
||||
} else {
|
||||
trimmed_delete_range.end
|
||||
}
|
||||
})
|
||||
.to_display_point(map)
|
||||
}
|
||||
|
||||
/// Returns a position of the previous subword boundary, where a subword is defined as a run of
|
||||
/// word characters of the same "subkind" - where subcharacter kinds are '_' character,
|
||||
/// lowerspace characters and uppercase characters.
|
||||
|
||||
@@ -1250,6 +1250,7 @@ struct InjectionPatternConfig {
|
||||
combined: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BracketsConfig {
|
||||
query: Query,
|
||||
open_capture_ix: u32,
|
||||
|
||||
Reference in New Issue
Block a user