Merge pull request #669 from zed-industries/extend-selection-fixes

Max Brunsfeld created

Fix extending selections starting at ends of syntax nodes

Change summary

crates/language/src/buffer.rs | 12 +++++++++-
crates/language/src/tests.rs  | 38 +++++++++++++++++++++++++++++++++++++
2 files changed, 48 insertions(+), 2 deletions(-)

Detailed changes

crates/language/src/buffer.rs 🔗

@@ -1621,8 +1621,13 @@ impl BufferSnapshot {
         let range = range.start.to_offset(self)..range.end.to_offset(self);
         let mut cursor = tree.root_node().walk();
 
-        // Descend to smallest leaf that touches or exceeds the start of the range.
-        while cursor.goto_first_child_for_byte(range.start).is_some() {}
+        // Descend to the first leaf that touches the start of the range,
+        // and if the range is non-empty, extends beyond the start.
+        while cursor.goto_first_child_for_byte(range.start).is_some() {
+            if !range.is_empty() && cursor.node().end_byte() == range.start {
+                cursor.goto_next_sibling();
+            }
+        }
 
         // Ascend to the smallest ancestor that strictly contains the range.
         loop {
@@ -1656,6 +1661,9 @@ impl BufferSnapshot {
                 }
             }
 
+            // If there is a candidate node on both sides of the (empty) range, then
+            // decide between the two by favoring a named node over an anonymous token.
+            // If both nodes are the same in that regard, favor the right one.
             if let Some(right_node) = right_node {
                 if right_node.is_named() || !left_node.is_named() {
                     return Some(right_node.byte_range());

crates/language/src/tests.rs 🔗

@@ -458,6 +458,44 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
     );
 }
 
+#[gpui::test]
+fn test_range_for_syntax_ancestor(cx: &mut MutableAppContext) {
+    cx.add_model(|cx| {
+        let text = "fn a() { b(|c| {}) }";
+        let buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
+        let snapshot = buffer.snapshot();
+
+        assert_eq!(
+            snapshot.range_for_syntax_ancestor(empty_range_at(text, "|")),
+            Some(range_of(text, "|"))
+        );
+        assert_eq!(
+            snapshot.range_for_syntax_ancestor(range_of(text, "|")),
+            Some(range_of(text, "|c|"))
+        );
+        assert_eq!(
+            snapshot.range_for_syntax_ancestor(range_of(text, "|c|")),
+            Some(range_of(text, "|c| {}"))
+        );
+        assert_eq!(
+            snapshot.range_for_syntax_ancestor(range_of(text, "|c| {}")),
+            Some(range_of(text, "(|c| {})"))
+        );
+
+        buffer
+    });
+
+    fn empty_range_at(text: &str, part: &str) -> Range<usize> {
+        let start = text.find(part).unwrap();
+        start..start
+    }
+
+    fn range_of(text: &str, part: &str) -> Range<usize> {
+        let start = text.find(part).unwrap();
+        start..start + part.len()
+    }
+}
+
 #[gpui::test]
 fn test_edit_with_autoindent(cx: &mut MutableAppContext) {
     cx.add_model(|cx| {