Add "Select Enclosing Symbol" command (#13435)

Tristan Hume created

I use this for a much faster workflow with inline assist when using fast
models.

Release Notes:

- Added "Select Enclosing Symbol" command based on tree-sitter outline.
Useful in combination with inline assist to rewrite a function.

Change summary

assets/keymaps/default-linux.json |  3 +
assets/keymaps/default-macos.json |  3 +
crates/editor/src/actions.rs      |  1 
crates/editor/src/editor.rs       | 52 +++++++++++++++++++++++++++++++++
crates/editor/src/element.rs      |  1 
5 files changed, 58 insertions(+), 2 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -152,7 +152,8 @@
       //     "focus": false
       //   }
       // ],
-      "ctrl->": "assistant::QuoteSelection"
+      "ctrl->": "assistant::QuoteSelection",
+      "ctrl-alt-e": "editor::SelectEnclosingSymbol"
     }
   },
   {

assets/keymaps/default-macos.json 🔗

@@ -188,7 +188,8 @@
           "focus": false
         }
       ],
-      "cmd->": "assistant::QuoteSelection"
+      "cmd->": "assistant::QuoteSelection",
+      "cmd-alt-e": "editor::SelectEnclosingSymbol"
     }
   },
   {

crates/editor/src/actions.rs 🔗

@@ -268,6 +268,7 @@ gpui::actions!(
         SelectAllMatches,
         SelectDown,
         SelectLargerSyntaxNode,
+        SelectEnclosingSymbol,
         SelectLeft,
         SelectLine,
         SelectRight,

crates/editor/src/editor.rs 🔗

@@ -8226,6 +8226,58 @@ impl Editor {
         });
     }
 
+    pub fn select_enclosing_symbol(
+        &mut self,
+        _: &SelectEnclosingSymbol,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let buffer = self.buffer.read(cx).snapshot(cx);
+        let old_selections = self.selections.all::<usize>(cx).into_boxed_slice();
+
+        fn update_selection(
+            selection: &Selection<usize>,
+            buffer_snap: &MultiBufferSnapshot,
+        ) -> Option<Selection<usize>> {
+            let cursor = selection.head();
+            let (_buffer_id, symbols) = buffer_snap.symbols_containing(cursor, None)?;
+            for symbol in symbols.iter().rev() {
+                let start = symbol.range.start.to_offset(&buffer_snap);
+                let end = symbol.range.end.to_offset(&buffer_snap);
+                let new_range = start..end;
+                if start < selection.start || end > selection.end {
+                    return Some(Selection {
+                        id: selection.id,
+                        start: new_range.start,
+                        end: new_range.end,
+                        goal: SelectionGoal::None,
+                        reversed: selection.reversed,
+                    });
+                }
+            }
+            None
+        }
+
+        let mut selected_larger_symbol = false;
+        let new_selections = old_selections
+            .iter()
+            .map(|selection| match update_selection(selection, &buffer) {
+                Some(new_selection) => {
+                    if new_selection.range() != selection.range() {
+                        selected_larger_symbol = true;
+                    }
+                    new_selection
+                }
+                None => selection.clone(),
+            })
+            .collect::<Vec<_>>();
+
+        if selected_larger_symbol {
+            self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.select(new_selections);
+            });
+        }
+    }
+
     pub fn select_larger_syntax_node(
         &mut self,
         _: &SelectLargerSyntaxNode,

crates/editor/src/element.rs 🔗

@@ -276,6 +276,7 @@ impl EditorElement {
         register_action(view, cx, Editor::toggle_comments);
         register_action(view, cx, Editor::select_larger_syntax_node);
         register_action(view, cx, Editor::select_smaller_syntax_node);
+        register_action(view, cx, Editor::select_enclosing_symbol);
         register_action(view, cx, Editor::move_to_enclosing_bracket);
         register_action(view, cx, Editor::undo_selection);
         register_action(view, cx, Editor::redo_selection);