editor: Expand selection to word under cursor before expanding to next enclosing syntax node (#28864)

Closes #27995

For strings in any language and Markdown, `select_larger_syntax_node`
will first select the word and then expand from there if:
- The cursor is on the word.
- The selection is inside the word.

It will not select the word and will directly proceed to expand if:
- The word is already selected.
- Multiple partial words are selected.

Todo:
- [x] Tests

Release Notes:

- Fixed `select_larger_syntax_node` to first expand to the word within a
string, and then to the larger syntax node.
This commit is contained in:
Smit Barmase 2025-04-16 20:52:26 +05:30 committed by GitHub
parent cb79420773
commit 0d8f77b5de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 220 additions and 1 deletions

View file

@ -12519,6 +12519,45 @@ impl Editor {
.iter()
.map(|selection| {
let old_range = selection.start..selection.end;
if let Some((node, _)) = buffer.syntax_ancestor(old_range.clone()) {
// manually select word at selection
if ["string_content", "inline"].contains(&node.kind()) {
let word_range = {
let display_point = buffer
.offset_to_point(old_range.start)
.to_display_point(&display_map);
let Range { start, end } =
movement::surrounding_word(&display_map, display_point);
start.to_point(&display_map).to_offset(&buffer)
..end.to_point(&display_map).to_offset(&buffer)
};
// ignore if word is already selected
if !word_range.is_empty() && old_range != word_range {
let last_word_range = {
let display_point = buffer
.offset_to_point(old_range.end)
.to_display_point(&display_map);
let Range { start, end } =
movement::surrounding_word(&display_map, display_point);
start.to_point(&display_map).to_offset(&buffer)
..end.to_point(&display_map).to_offset(&buffer)
};
// only select word if start and end point belongs to same word
if word_range == last_word_range {
selected_larger_node = true;
return Selection {
id: selection.id,
start: word_range.start,
end: word_range.end,
goal: SelectionGoal::None,
reversed: selection.reversed,
};
}
}
}
}
let mut new_range = old_range.clone();
let mut new_node = None;
while let Some((node, containing_range)) = buffer.syntax_ancestor(new_range.clone())

View file

@ -6309,7 +6309,187 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
use mod1::mod2::«{mod3, mod4}ˇ»;
fn fn_1«ˇ(param1: bool, param2: &str)» {
«ˇlet var1 = "text";»
let var1 = "«ˇtext»";
}
"#},
cx,
);
});
}
#[gpui::test]
async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let language = Arc::new(Language::new(
LanguageConfig::default(),
Some(tree_sitter_rust::LANGUAGE.into()),
));
let text = r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "hello world";
}
"#
.unindent();
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
editor
.condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
.await;
// Test 1: Cursor on a letter of a string word
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
]);
});
});
editor.update_in(cx, |editor, window, cx| {
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "hˇello world";
}
"#},
cx,
);
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "«ˇhello» world";
}
"#},
cx,
);
});
// Test 2: Partial selection within a word
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
]);
});
});
editor.update_in(cx, |editor, window, cx| {
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "h«elˇ»lo world";
}
"#},
cx,
);
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "«ˇhello» world";
}
"#},
cx,
);
});
// Test 3: Complete word already selected
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
]);
});
});
editor.update_in(cx, |editor, window, cx| {
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "«helloˇ» world";
}
"#},
cx,
);
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "«hello worldˇ»";
}
"#},
cx,
);
});
// Test 4: Selection spanning across words
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
]);
});
});
editor.update_in(cx, |editor, window, cx| {
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "hel«lo woˇ»rld";
}
"#},
cx,
);
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "«ˇhello world»";
}
"#},
cx,
);
});
// Test 5: Expansion beyond string
editor.update_in(cx, |editor, window, cx| {
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
«ˇlet var1 = "hello world";»
}
"#},
cx,