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:
Kirill Bulatov
2025-09-03 17:48:17 +03:00
committed by GitHub
parent 7327ef662b
commit 0cbacb8500
14 changed files with 509 additions and 48 deletions
+2 -2
View File
@@ -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",
+3 -3
View File
@@ -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",
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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",
+4 -4
View File
@@ -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 }],
+1 -1
View File
@@ -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",
+8
View File
@@ -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.
+20 -4
View File
@@ -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);
}
});
+359 -25
View File
@@ -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(), "");
});
}
+103 -1
View File
@@ -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.
+1
View File
@@ -1250,6 +1250,7 @@ struct InjectionPatternConfig {
combined: bool,
}
#[derive(Debug)]
struct BracketsConfig {
query: Query,
open_capture_ix: u32,