Merge pull request #1420 from zed-industries/goto-type-definition

Julia created

Draft: Add "go to type definition" action

Change summary

assets/keymaps/default.json                |   1 
crates/editor/src/editor.rs                |  29 +
crates/editor/src/element.rs               |  41 +
crates/editor/src/link_go_to_definition.rs | 439 +++++++++++++++++-----
crates/editor/src/mouse_context_menu.rs    |   5 
crates/gpui/src/platform/event.rs          |   4 
crates/lsp/src/lsp.rs                      |   1 
crates/project/src/lsp_command.rs          | 457 +++++++++++++++--------
crates/project/src/project.rs              |  10 
crates/rpc/proto/zed.proto                 | 171 ++++----
crates/rpc/src/proto.rs                    |   4 
crates/rpc/src/rpc.rs                      |   2 
crates/zed/src/menus.rs                    |   4 
13 files changed, 798 insertions(+), 370 deletions(-)

Detailed changes

assets/keymaps/default.json 🔗

@@ -192,6 +192,7 @@
             "shift-f8": "editor::GoToPrevDiagnostic",
             "f2": "editor::Rename",
             "f12": "editor::GoToDefinition",
+            "cmd-f12": "editor::GoToTypeDefinition",
             "alt-shift-f12": "editor::FindAllReferences",
             "ctrl-m": "editor::MoveToEnclosingBracket",
             "alt-cmd-[": "editor::Fold",

crates/editor/src/editor.rs 🔗

@@ -187,6 +187,7 @@ actions!(
         SelectLargerSyntaxNode,
         SelectSmallerSyntaxNode,
         GoToDefinition,
+        GoToTypeDefinition,
         MoveToEnclosingBracket,
         UndoSelection,
         RedoSelection,
@@ -297,6 +298,7 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Editor::go_to_diagnostic);
     cx.add_action(Editor::go_to_prev_diagnostic);
     cx.add_action(Editor::go_to_definition);
+    cx.add_action(Editor::go_to_type_definition);
     cx.add_action(Editor::page_up);
     cx.add_action(Editor::page_down);
     cx.add_action(Editor::fold);
@@ -895,6 +897,11 @@ pub struct NavigationData {
 
 pub struct EditorCreated(pub ViewHandle<Editor>);
 
+enum GotoDefinitionKind {
+    Symbol,
+    Type,
+}
+
 impl Editor {
     pub fn single_line(
         field_editor_style: Option<GetFieldEditorTheme>,
@@ -4693,6 +4700,22 @@ impl Editor {
         workspace: &mut Workspace,
         _: &GoToDefinition,
         cx: &mut ViewContext<Workspace>,
+    ) {
+        Self::go_to_definition_of_kind(GotoDefinitionKind::Symbol, workspace, cx);
+    }
+
+    pub fn go_to_type_definition(
+        workspace: &mut Workspace,
+        _: &GoToTypeDefinition,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        Self::go_to_definition_of_kind(GotoDefinitionKind::Type, workspace, cx);
+    }
+
+    fn go_to_definition_of_kind(
+        kind: GotoDefinitionKind,
+        workspace: &mut Workspace,
+        cx: &mut ViewContext<Workspace>,
     ) {
         let active_item = workspace.active_item(cx);
         let editor_handle = if let Some(editor) = active_item
@@ -4714,7 +4737,11 @@ impl Editor {
         };
 
         let project = workspace.project().clone();
-        let definitions = project.update(cx, |project, cx| project.definition(&buffer, head, cx));
+        let definitions = project.update(cx, |project, cx| match kind {
+            GotoDefinitionKind::Symbol => project.definition(&buffer, head, cx),
+            GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx),
+        });
+
         cx.spawn(|workspace, mut cx| async move {
             let definitions = definitions.await?;
             workspace.update(&mut cx, |workspace, cx| {

crates/editor/src/element.rs 🔗

@@ -6,7 +6,9 @@ use super::{
 use crate::{
     display_map::{BlockStyle, DisplaySnapshot, TransformBlock},
     hover_popover::HoverAt,
-    link_go_to_definition::{CmdChanged, GoToFetchedDefinition, UpdateGoToDefinitionLink},
+    link_go_to_definition::{
+        CmdShiftChanged, GoToFetchedDefinition, GoToFetchedTypeDefinition, UpdateGoToDefinitionLink,
+    },
     mouse_context_menu::DeployMouseContextMenu,
     EditorStyle,
 };
@@ -122,7 +124,12 @@ impl EditorElement {
         if cmd && paint.text_bounds.contains_point(position) {
             let (point, overshoot) = paint.point_for_position(&self.snapshot(cx), layout, position);
             if overshoot.is_zero() {
-                cx.dispatch_action(GoToFetchedDefinition { point });
+                if shift {
+                    cx.dispatch_action(GoToFetchedTypeDefinition { point });
+                } else {
+                    cx.dispatch_action(GoToFetchedDefinition { point });
+                }
+
                 return true;
             }
         }
@@ -238,8 +245,12 @@ impl EditorElement {
 
     fn mouse_moved(
         &self,
-        position: Vector2F,
-        cmd: bool,
+        MouseMovedEvent {
+            cmd,
+            shift,
+            position,
+            ..
+        }: MouseMovedEvent,
         layout: &LayoutState,
         paint: &PaintState,
         cx: &mut EventContext,
@@ -260,6 +271,7 @@ impl EditorElement {
         cx.dispatch_action(UpdateGoToDefinitionLink {
             point,
             cmd_held: cmd,
+            shift_held: shift,
         });
 
         if paint
@@ -283,8 +295,11 @@ impl EditorElement {
         true
     }
 
-    fn modifiers_changed(&self, cmd: bool, cx: &mut EventContext) -> bool {
-        cx.dispatch_action(CmdChanged { cmd_down: cmd });
+    fn modifiers_changed(&self, event: ModifiersChangedEvent, cx: &mut EventContext) -> bool {
+        cx.dispatch_action(CmdShiftChanged {
+            cmd_down: event.cmd,
+            shift_down: event.shift,
+        });
         false
     }
 
@@ -1534,32 +1549,34 @@ impl Element for EditorElement {
                 paint,
                 cx,
             ),
+
             Event::MouseDown(MouseButtonEvent {
                 button: MouseButton::Right,
                 position,
                 ..
             }) => self.mouse_right_down(*position, layout, paint, cx),
+
             Event::MouseUp(MouseButtonEvent {
                 button: MouseButton::Left,
                 position,
                 ..
             }) => self.mouse_up(*position, cx),
+
             Event::MouseMoved(MouseMovedEvent {
                 pressed_button: Some(MouseButton::Left),
                 position,
                 ..
             }) => self.mouse_dragged(*position, layout, paint, cx),
+
             Event::ScrollWheel(ScrollWheelEvent {
                 position,
                 delta,
                 precise,
             }) => self.scroll(*position, *delta, *precise, layout, paint, cx),
-            Event::ModifiersChanged(ModifiersChangedEvent { cmd, .. }) => {
-                self.modifiers_changed(*cmd, cx)
-            }
-            Event::MouseMoved(MouseMovedEvent { position, cmd, .. }) => {
-                self.mouse_moved(*position, *cmd, layout, paint, cx)
-            }
+
+            &Event::ModifiersChanged(event) => self.modifiers_changed(event, cx),
+
+            &Event::MouseMoved(event) => self.mouse_moved(event, layout, paint, cx),
 
             _ => false,
         }
@@ -8,18 +8,21 @@ use util::TryFutureExt;
 use workspace::Workspace;
 
 use crate::{
-    Anchor, DisplayPoint, Editor, EditorSnapshot, Event, GoToDefinition, Select, SelectPhase,
+    Anchor, DisplayPoint, Editor, EditorSnapshot, Event, GoToDefinition, GoToTypeDefinition,
+    Select, SelectPhase,
 };
 
 #[derive(Clone, PartialEq)]
 pub struct UpdateGoToDefinitionLink {
     pub point: Option<DisplayPoint>,
     pub cmd_held: bool,
+    pub shift_held: bool,
 }
 
 #[derive(Clone, PartialEq)]
-pub struct CmdChanged {
+pub struct CmdShiftChanged {
     pub cmd_down: bool,
+    pub shift_down: bool,
 }
 
 #[derive(Clone, PartialEq)]
@@ -27,28 +30,44 @@ pub struct GoToFetchedDefinition {
     pub point: DisplayPoint,
 }
 
+#[derive(Clone, PartialEq)]
+pub struct GoToFetchedTypeDefinition {
+    pub point: DisplayPoint,
+}
+
 impl_internal_actions!(
     editor,
-    [UpdateGoToDefinitionLink, CmdChanged, GoToFetchedDefinition]
+    [
+        UpdateGoToDefinitionLink,
+        CmdShiftChanged,
+        GoToFetchedDefinition,
+        GoToFetchedTypeDefinition
+    ]
 );
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(update_go_to_definition_link);
-    cx.add_action(cmd_changed);
+    cx.add_action(cmd_shift_changed);
     cx.add_action(go_to_fetched_definition);
+    cx.add_action(go_to_fetched_type_definition);
 }
 
 #[derive(Default)]
 pub struct LinkGoToDefinitionState {
     pub last_mouse_location: Option<Anchor>,
     pub symbol_range: Option<Range<Anchor>>,
+    pub kind: Option<LinkDefinitionKind>,
     pub definitions: Vec<LocationLink>,
     pub task: Option<Task<Option<()>>>,
 }
 
 pub fn update_go_to_definition_link(
     editor: &mut Editor,
-    &UpdateGoToDefinitionLink { point, cmd_held }: &UpdateGoToDefinitionLink,
+    &UpdateGoToDefinitionLink {
+        point,
+        cmd_held,
+        shift_held,
+    }: &UpdateGoToDefinitionLink,
     cx: &mut ViewContext<Editor>,
 ) {
     // Store new mouse point as an anchor
@@ -72,7 +91,13 @@ pub fn update_go_to_definition_link(
     editor.link_go_to_definition_state.last_mouse_location = point.clone();
     if cmd_held {
         if let Some(point) = point {
-            show_link_definition(editor, point, snapshot, cx);
+            let kind = if shift_held {
+                LinkDefinitionKind::Type
+            } else {
+                LinkDefinitionKind::Symbol
+            };
+
+            show_link_definition(kind, editor, point, snapshot, cx);
             return;
         }
     }
@@ -80,9 +105,12 @@ pub fn update_go_to_definition_link(
     hide_link_definition(editor, cx);
 }
 
-pub fn cmd_changed(
+pub fn cmd_shift_changed(
     editor: &mut Editor,
-    &CmdChanged { cmd_down }: &CmdChanged,
+    &CmdShiftChanged {
+        cmd_down,
+        shift_down,
+    }: &CmdShiftChanged,
     cx: &mut ViewContext<Editor>,
 ) {
     if let Some(point) = editor
@@ -92,19 +120,37 @@ pub fn cmd_changed(
     {
         if cmd_down {
             let snapshot = editor.snapshot(cx);
-            show_link_definition(editor, point.clone(), snapshot, cx);
+            let kind = if shift_down {
+                LinkDefinitionKind::Type
+            } else {
+                LinkDefinitionKind::Symbol
+            };
+
+            show_link_definition(kind, editor, point.clone(), snapshot, cx);
         } else {
             hide_link_definition(editor, cx)
         }
     }
 }
 
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum LinkDefinitionKind {
+    Symbol,
+    Type,
+}
+
 pub fn show_link_definition(
+    definition_kind: LinkDefinitionKind,
     editor: &mut Editor,
     trigger_point: Anchor,
     snapshot: EditorSnapshot,
     cx: &mut ViewContext<Editor>,
 ) {
+    let same_kind = editor.link_go_to_definition_state.kind == Some(definition_kind);
+    if !same_kind {
+        hide_link_definition(editor, cx);
+    }
+
     if editor.pending_rename.is_some() {
         return;
     }
@@ -135,17 +181,20 @@ pub fn show_link_definition(
         return;
     };
 
-    // Don't request again if the location is within the symbol region of a previous request
+    // Don't request again if the location is within the symbol region of a previous request with the same kind
     if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range {
-        if symbol_range
+        let point_after_start = symbol_range
             .start
             .cmp(&trigger_point, &snapshot.buffer_snapshot)
-            .is_le()
-            && symbol_range
-                .end
-                .cmp(&trigger_point, &snapshot.buffer_snapshot)
-                .is_ge()
-        {
+            .is_le();
+
+        let point_before_end = symbol_range
+            .end
+            .cmp(&trigger_point, &snapshot.buffer_snapshot)
+            .is_ge();
+
+        let point_within_range = point_after_start && point_before_end;
+        if point_within_range && same_kind {
             return;
         }
     }
@@ -154,8 +203,14 @@ pub fn show_link_definition(
         async move {
             // query the LSP for definition info
             let definition_request = cx.update(|cx| {
-                project.update(cx, |project, cx| {
-                    project.definition(&buffer, buffer_position.clone(), cx)
+                project.update(cx, |project, cx| match definition_kind {
+                    LinkDefinitionKind::Symbol => {
+                        project.definition(&buffer, buffer_position.clone(), cx)
+                    }
+
+                    LinkDefinitionKind::Type => {
+                        project.type_definition(&buffer, buffer_position.clone(), cx)
+                    }
                 })
             });
 
@@ -181,6 +236,7 @@ pub fn show_link_definition(
                 this.update(&mut cx, |this, cx| {
                     // Clear any existing highlights
                     this.clear_text_highlights::<LinkGoToDefinitionState>(cx);
+                    this.link_go_to_definition_state.kind = Some(definition_kind);
                     this.link_go_to_definition_state.symbol_range = result
                         .as_ref()
                         .and_then(|(symbol_range, _)| symbol_range.clone());
@@ -258,7 +314,24 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
 
 pub fn go_to_fetched_definition(
     workspace: &mut Workspace,
-    GoToFetchedDefinition { point }: &GoToFetchedDefinition,
+    &GoToFetchedDefinition { point }: &GoToFetchedDefinition,
+    cx: &mut ViewContext<Workspace>,
+) {
+    go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, workspace, point, cx);
+}
+
+pub fn go_to_fetched_type_definition(
+    workspace: &mut Workspace,
+    &GoToFetchedTypeDefinition { point }: &GoToFetchedTypeDefinition,
+    cx: &mut ViewContext<Workspace>,
+) {
+    go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, workspace, point, cx);
+}
+
+fn go_to_fetched_definition_of_kind(
+    kind: LinkDefinitionKind,
+    workspace: &mut Workspace,
+    point: DisplayPoint,
     cx: &mut ViewContext<Workspace>,
 ) {
     let active_item = workspace.active_item(cx);
@@ -271,13 +344,14 @@ pub fn go_to_fetched_definition(
         return;
     };
 
-    let definitions = editor_handle.update(cx, |editor, cx| {
+    let (cached_definitions, cached_definitions_kind) = editor_handle.update(cx, |editor, cx| {
         let definitions = editor.link_go_to_definition_state.definitions.clone();
         hide_link_definition(editor, cx);
-        definitions
+        (definitions, editor.link_go_to_definition_state.kind)
     });
 
-    if !definitions.is_empty() {
+    let is_correct_kind = cached_definitions_kind == Some(kind);
+    if !cached_definitions.is_empty() && is_correct_kind {
         editor_handle.update(cx, |editor, cx| {
             if !editor.focused {
                 cx.focus_self();
@@ -285,7 +359,7 @@ pub fn go_to_fetched_definition(
             }
         });
 
-        Editor::navigate_to_definitions(workspace, editor_handle, definitions, cx);
+        Editor::navigate_to_definitions(workspace, editor_handle, cached_definitions, cx);
     } else {
         editor_handle.update(cx, |editor, cx| {
             editor.select(
@@ -298,7 +372,13 @@ pub fn go_to_fetched_definition(
             );
         });
 
-        Editor::go_to_definition(workspace, &GoToDefinition, cx);
+        match kind {
+            LinkDefinitionKind::Symbol => Editor::go_to_definition(workspace, &GoToDefinition, cx),
+
+            LinkDefinitionKind::Type => {
+                Editor::go_to_type_definition(workspace, &GoToTypeDefinition, cx)
+            }
+        }
     }
 }
 
@@ -306,11 +386,128 @@ pub fn go_to_fetched_definition(
 mod tests {
     use futures::StreamExt;
     use indoc::indoc;
+    use lsp::request::{GotoDefinition, GotoTypeDefinition};
 
     use crate::test::EditorLspTestContext;
 
     use super::*;
 
+    #[gpui::test]
+    async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        cx.set_state(indoc! {"
+            struct A;
+            let v|ariable = A;
+        "});
+
+        // Basic hold cmd+shift, expect highlight in region if response contains type definition
+        let hover_point = cx.display_point(indoc! {"
+            struct A;
+            let v|ariable = A;
+        "});
+        let symbol_range = cx.lsp_range(indoc! {"
+            struct A;
+            let [variable] = A;
+        "});
+        let target_range = cx.lsp_range(indoc! {"
+            struct [A];
+            let variable = A;
+        "});
+
+        let mut requests =
+            cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
+                Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
+                    lsp::LocationLink {
+                        origin_selection_range: Some(symbol_range),
+                        target_uri: url.clone(),
+                        target_range,
+                        target_selection_range: target_range,
+                    },
+                ])))
+            });
+
+        // Press cmd+shift to trigger highlight
+        cx.update_editor(|editor, cx| {
+            update_go_to_definition_link(
+                editor,
+                &UpdateGoToDefinitionLink {
+                    point: Some(hover_point),
+                    cmd_held: true,
+                    shift_held: true,
+                },
+                cx,
+            );
+        });
+        requests.next().await;
+        cx.foreground().run_until_parked();
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+            struct A;
+            let [variable] = A;
+        "});
+
+        // Unpress shift causes highlight to go away (normal goto-definition is not valid here)
+        cx.update_editor(|editor, cx| {
+            cmd_shift_changed(
+                editor,
+                &CmdShiftChanged {
+                    cmd_down: true,
+                    shift_down: false,
+                },
+                cx,
+            );
+        });
+        // Assert no link highlights
+        cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+            struct A;
+            let variable = A;
+        "});
+
+        // Cmd+shift click without existing definition requests and jumps
+        let hover_point = cx.display_point(indoc! {"
+            struct A;
+            let v|ariable = A;
+        "});
+        let target_range = cx.lsp_range(indoc! {"
+            struct [A];
+            let variable = A;
+        "});
+
+        let mut requests =
+            cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
+                Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
+                    lsp::LocationLink {
+                        origin_selection_range: None,
+                        target_uri: url,
+                        target_range,
+                        target_selection_range: target_range,
+                    },
+                ])))
+            });
+
+        cx.update_workspace(|workspace, cx| {
+            go_to_fetched_type_definition(
+                workspace,
+                &GoToFetchedTypeDefinition { point: hover_point },
+                cx,
+            );
+        });
+        requests.next().await;
+        cx.foreground().run_until_parked();
+
+        cx.assert_editor_state(indoc! {"
+            struct [A};
+            let variable = A;
+        "});
+    }
+
     #[gpui::test]
     async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
         let mut cx = EditorLspTestContext::new_rust(
@@ -327,7 +524,8 @@ mod tests {
                 do_work();
             
             fn do_work()
-                test();"});
+                test();
+        "});
 
         // Basic hold cmd, expect highlight in region if response contains definition
         let hover_point = cx.display_point(indoc! {"
@@ -335,38 +533,41 @@ mod tests {
                 do_w|ork();
             
             fn do_work()
-                test();"});
-
+                test();
+        "});
         let symbol_range = cx.lsp_range(indoc! {"
             fn test()
                 [do_work]();
             
             fn do_work()
-                test();"});
+                test();
+        "});
         let target_range = cx.lsp_range(indoc! {"
             fn test()
                 do_work();
             
             fn [do_work]()
-                test();"});
+                test();
+        "});
+
+        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+                lsp::LocationLink {
+                    origin_selection_range: Some(symbol_range),
+                    target_uri: url.clone(),
+                    target_range,
+                    target_selection_range: target_range,
+                },
+            ])))
+        });
 
-        let mut requests =
-            cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
-                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
-                    lsp::LocationLink {
-                        origin_selection_range: Some(symbol_range),
-                        target_uri: url.clone(),
-                        target_range,
-                        target_selection_range: target_range,
-                    },
-                ])))
-            });
         cx.update_editor(|editor, cx| {
             update_go_to_definition_link(
                 editor,
                 &UpdateGoToDefinitionLink {
                     point: Some(hover_point),
                     cmd_held: true,
+                    shift_held: false,
                 },
                 cx,
             );
@@ -378,11 +579,19 @@ mod tests {
                 [do_work]();
             
             fn do_work()
-                test();"});
+                test();
+        "});
 
         // Unpress cmd causes highlight to go away
         cx.update_editor(|editor, cx| {
-            cmd_changed(editor, &CmdChanged { cmd_down: false }, cx);
+            cmd_shift_changed(
+                editor,
+                &CmdShiftChanged {
+                    cmd_down: false,
+                    shift_down: false,
+                },
+                cx,
+            );
         });
         // Assert no link highlights
         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
@@ -390,28 +599,29 @@ mod tests {
                 do_work();
             
             fn do_work()
-                test();"});
+                test();
+        "});
 
         // Response without source range still highlights word
         cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None);
-        let mut requests =
-            cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
-                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
-                    lsp::LocationLink {
-                        // No origin range
-                        origin_selection_range: None,
-                        target_uri: url.clone(),
-                        target_range,
-                        target_selection_range: target_range,
-                    },
-                ])))
-            });
+        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+                lsp::LocationLink {
+                    // No origin range
+                    origin_selection_range: None,
+                    target_uri: url.clone(),
+                    target_range,
+                    target_selection_range: target_range,
+                },
+            ])))
+        });
         cx.update_editor(|editor, cx| {
             update_go_to_definition_link(
                 editor,
                 &UpdateGoToDefinitionLink {
                     point: Some(hover_point),
                     cmd_held: true,
+                    shift_held: false,
                 },
                 cx,
             );
@@ -424,7 +634,8 @@ mod tests {
                 [do_work]();
             
             fn do_work()
-                test();"});
+                test();
+        "});
 
         // Moving mouse to location with no response dismisses highlight
         let hover_point = cx.display_point(indoc! {"
@@ -432,19 +643,21 @@ mod tests {
                 do_work();
             
             fn do_work()
-                test();"});
-        let mut requests =
-            cx.lsp
-                .handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
-                    // No definitions returned
-                    Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
-                });
+                test();
+        "});
+        let mut requests = cx
+            .lsp
+            .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
+                // No definitions returned
+                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
+            });
         cx.update_editor(|editor, cx| {
             update_go_to_definition_link(
                 editor,
                 &UpdateGoToDefinitionLink {
                     point: Some(hover_point),
                     cmd_held: true,
+                    shift_held: false,
                 },
                 cx,
             );
@@ -458,7 +671,8 @@ mod tests {
                 do_work();
             
             fn do_work()
-                test();"});
+                test();
+        "});
 
         // Move mouse without cmd and then pressing cmd triggers highlight
         let hover_point = cx.display_point(indoc! {"
@@ -466,13 +680,15 @@ mod tests {
                 do_work();
             
             fn do_work()
-                te|st();"});
+                te|st();
+        "});
         cx.update_editor(|editor, cx| {
             update_go_to_definition_link(
                 editor,
                 &UpdateGoToDefinitionLink {
                     point: Some(hover_point),
                     cmd_held: false,
+                    shift_held: false,
                 },
                 cx,
             );
@@ -485,34 +701,43 @@ mod tests {
                 do_work();
             
             fn do_work()
-                test();"});
+                test();
+        "});
 
         let symbol_range = cx.lsp_range(indoc! {"
             fn test()
                 do_work();
             
             fn do_work()
-                [test]();"});
+                [test]();
+        "});
         let target_range = cx.lsp_range(indoc! {"
             fn [test]()
                 do_work();
             
             fn do_work()
-                test();"});
-
-        let mut requests =
-            cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
-                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
-                    lsp::LocationLink {
-                        origin_selection_range: Some(symbol_range),
-                        target_uri: url,
-                        target_range,
-                        target_selection_range: target_range,
-                    },
-                ])))
-            });
+                test();
+        "});
+
+        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+                lsp::LocationLink {
+                    origin_selection_range: Some(symbol_range),
+                    target_uri: url,
+                    target_range,
+                    target_selection_range: target_range,
+                },
+            ])))
+        });
         cx.update_editor(|editor, cx| {
-            cmd_changed(editor, &CmdChanged { cmd_down: true }, cx);
+            cmd_shift_changed(
+                editor,
+                &CmdShiftChanged {
+                    cmd_down: true,
+                    shift_down: false,
+                },
+                cx,
+            );
         });
         requests.next().await;
         cx.foreground().run_until_parked();
@@ -522,7 +747,8 @@ mod tests {
                 do_work();
             
             fn do_work()
-                [test]();"});
+                [test]();
+        "});
 
         // Moving within symbol range doesn't re-request
         let hover_point = cx.display_point(indoc! {"
@@ -530,13 +756,15 @@ mod tests {
                 do_work();
             
             fn do_work()
-                tes|t();"});
+                tes|t();
+        "});
         cx.update_editor(|editor, cx| {
             update_go_to_definition_link(
                 editor,
                 &UpdateGoToDefinitionLink {
                     point: Some(hover_point),
                     cmd_held: true,
+                    shift_held: false,
                 },
                 cx,
             );
@@ -547,7 +775,8 @@ mod tests {
                 do_work();
             
             fn do_work()
-                [test]();"});
+                [test]();
+        "});
 
         // Cmd click with existing definition doesn't re-request and dismisses highlight
         cx.update_workspace(|workspace, cx| {
@@ -555,7 +784,7 @@ mod tests {
         });
         // Assert selection moved to to definition
         cx.lsp
-            .handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
+            .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
                 // Empty definition response to make sure we aren't hitting the lsp and using
                 // the cached location instead
                 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
@@ -565,14 +794,16 @@ mod tests {
                 do_work();
             
             fn do_work()
-                test();"});
+                test();
+        "});
         // Assert no link highlights after jump
         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
             fn test()
                 do_work();
             
             fn do_work()
-                test();"});
+                test();
+        "});
 
         // Cmd click without existing definition requests and jumps
         let hover_point = cx.display_point(indoc! {"
@@ -580,25 +811,26 @@ mod tests {
                 do_w|ork();
             
             fn do_work()
-                test();"});
+                test();
+        "});
         let target_range = cx.lsp_range(indoc! {"
             fn test()
                 do_work();
             
             fn [do_work]()
-                test();"});
-
-        let mut requests =
-            cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
-                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
-                    lsp::LocationLink {
-                        origin_selection_range: None,
-                        target_uri: url,
-                        target_range,
-                        target_selection_range: target_range,
-                    },
-                ])))
-            });
+                test();
+        "});
+
+        let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+            Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+                lsp::LocationLink {
+                    origin_selection_range: None,
+                    target_uri: url,
+                    target_range,
+                    target_selection_range: target_range,
+                },
+            ])))
+        });
         cx.update_workspace(|workspace, cx| {
             go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
         });
@@ -610,6 +842,7 @@ mod tests {
                 do_work();
             
             fn [do_work}()
-                test();"});
+                test();
+        "});
     }
 }

crates/editor/src/mouse_context_menu.rs 🔗

@@ -2,8 +2,8 @@ use context_menu::ContextMenuItem;
 use gpui::{geometry::vector::Vector2F, impl_internal_actions, MutableAppContext, ViewContext};
 
 use crate::{
-    DisplayPoint, Editor, EditorMode, Event, FindAllReferences, GoToDefinition, Rename, SelectMode,
-    ToggleCodeActions,
+    DisplayPoint, Editor, EditorMode, Event, FindAllReferences, GoToDefinition, GoToTypeDefinition,
+    Rename, SelectMode, ToggleCodeActions,
 };
 
 #[derive(Clone, PartialEq)]
@@ -50,6 +50,7 @@ pub fn deploy_context_menu(
             vec![
                 ContextMenuItem::item("Rename Symbol", Rename),
                 ContextMenuItem::item("Go To Definition", GoToDefinition),
+                ContextMenuItem::item("Go To Type Definition", GoToTypeDefinition),
                 ContextMenuItem::item("Find All References", FindAllReferences),
                 ContextMenuItem::item(
                     "Code Actions",

crates/gpui/src/platform/event.rs 🔗

@@ -11,7 +11,7 @@ pub struct KeyUpEvent {
     pub keystroke: Keystroke,
 }
 
-#[derive(Clone, Debug)]
+#[derive(Clone, Copy, Debug)]
 pub struct ModifiersChangedEvent {
     pub ctrl: bool,
     pub alt: bool,
@@ -19,7 +19,7 @@ pub struct ModifiersChangedEvent {
     pub cmd: bool,
 }
 
-#[derive(Clone, Debug, Default)]
+#[derive(Clone, Copy, Debug, Default)]
 pub struct ScrollWheelEvent {
     pub position: Vector2F,
     pub delta: Vector2F,

crates/lsp/src/lsp.rs 🔗

@@ -1,3 +1,4 @@
+pub use lsp_types::request::*;
 pub use lsp_types::*;
 
 use anyhow::{anyhow, Context, Result};

crates/project/src/lsp_command.rs 🔗

@@ -8,11 +8,11 @@ use gpui::{AppContext, AsyncAppContext, ModelHandle};
 use language::{
     point_from_lsp, point_to_lsp,
     proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
-    range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToPointUtf16,
+    range_from_lsp, Anchor, Bias, Buffer, CachedLspAdapter, PointUtf16, ToPointUtf16,
 };
-use lsp::{DocumentHighlightKind, ServerCapabilities};
+use lsp::{DocumentHighlightKind, LanguageServer, ServerCapabilities};
 use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
-use std::{cmp::Reverse, ops::Range, path::Path};
+use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
 
 #[async_trait(?Send)]
 pub(crate) trait LspCommand: 'static + Sized {
@@ -75,6 +75,10 @@ pub(crate) struct GetDefinition {
     pub position: PointUtf16,
 }
 
+pub(crate) struct GetTypeDefinition {
+    pub position: PointUtf16,
+}
+
 pub(crate) struct GetReferences {
     pub position: PointUtf16,
 }
@@ -238,13 +242,7 @@ impl LspCommand for PerformRename {
         mut cx: AsyncAppContext,
     ) -> Result<ProjectTransaction> {
         if let Some(edit) = message {
-            let (lsp_adapter, lsp_server) = project
-                .read_with(&cx, |project, cx| {
-                    project
-                        .language_server_for_buffer(buffer.read(cx), cx)
-                        .map(|(adapter, server)| (adapter.clone(), server.clone()))
-                })
-                .ok_or_else(|| anyhow!("no language server found for buffer"))?;
+            let (lsp_adapter, lsp_server) = language_server_for_buffer(&project, &buffer, &mut cx)?;
             Project::deserialize_workspace_edit(
                 project,
                 edit,
@@ -352,87 +350,99 @@ impl LspCommand for GetDefinition {
         message: Option<lsp::GotoDefinitionResponse>,
         project: ModelHandle<Project>,
         buffer: ModelHandle<Buffer>,
-        mut cx: AsyncAppContext,
+        cx: AsyncAppContext,
     ) -> Result<Vec<LocationLink>> {
-        let mut definitions = Vec::new();
-        let (lsp_adapter, language_server) = project
-            .read_with(&cx, |project, cx| {
-                project
-                    .language_server_for_buffer(buffer.read(cx), cx)
-                    .map(|(adapter, server)| (adapter.clone(), server.clone()))
+        location_links_from_lsp(message, project, buffer, cx).await
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDefinition {
+        proto::GetDefinition {
+            project_id,
+            buffer_id: buffer.remote_id(),
+            position: Some(language::proto::serialize_anchor(
+                &buffer.anchor_before(self.position),
+            )),
+            version: serialize_version(&buffer.version()),
+        }
+    }
+
+    async fn from_proto(
+        message: proto::GetDefinition,
+        _: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self> {
+        let position = message
+            .position
+            .and_then(deserialize_anchor)
+            .ok_or_else(|| anyhow!("invalid position"))?;
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(message.version))
             })
-            .ok_or_else(|| anyhow!("no language server found for buffer"))?;
+            .await;
+        Ok(Self {
+            position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
+        })
+    }
 
-        if let Some(message) = message {
-            let mut unresolved_links = Vec::new();
-            match message {
-                lsp::GotoDefinitionResponse::Scalar(loc) => {
-                    unresolved_links.push((None, loc.uri, loc.range));
-                }
-                lsp::GotoDefinitionResponse::Array(locs) => {
-                    unresolved_links.extend(locs.into_iter().map(|l| (None, l.uri, l.range)));
-                }
-                lsp::GotoDefinitionResponse::Link(links) => {
-                    unresolved_links.extend(links.into_iter().map(|l| {
-                        (
-                            l.origin_selection_range,
-                            l.target_uri,
-                            l.target_selection_range,
-                        )
-                    }));
-                }
-            }
+    fn response_to_proto(
+        response: Vec<LocationLink>,
+        project: &mut Project,
+        peer_id: PeerId,
+        _: &clock::Global,
+        cx: &AppContext,
+    ) -> proto::GetDefinitionResponse {
+        let links = location_links_to_proto(response, project, peer_id, cx);
+        proto::GetDefinitionResponse { links }
+    }
 
-            for (origin_range, target_uri, target_range) in unresolved_links {
-                let target_buffer_handle = project
-                    .update(&mut cx, |this, cx| {
-                        this.open_local_buffer_via_lsp(
-                            target_uri,
-                            language_server.server_id(),
-                            lsp_adapter.name.clone(),
-                            cx,
-                        )
-                    })
-                    .await?;
+    async fn response_from_proto(
+        self,
+        message: proto::GetDefinitionResponse,
+        project: ModelHandle<Project>,
+        _: ModelHandle<Buffer>,
+        cx: AsyncAppContext,
+    ) -> Result<Vec<LocationLink>> {
+        location_links_from_proto(message.links, project, cx).await
+    }
 
-                cx.read(|cx| {
-                    let origin_location = origin_range.map(|origin_range| {
-                        let origin_buffer = buffer.read(cx);
-                        let origin_start = origin_buffer
-                            .clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left);
-                        let origin_end = origin_buffer
-                            .clip_point_utf16(point_from_lsp(origin_range.end), Bias::Left);
-                        Location {
-                            buffer: buffer.clone(),
-                            range: origin_buffer.anchor_after(origin_start)
-                                ..origin_buffer.anchor_before(origin_end),
-                        }
-                    });
+    fn buffer_id_from_proto(message: &proto::GetDefinition) -> u64 {
+        message.buffer_id
+    }
+}
 
-                    let target_buffer = target_buffer_handle.read(cx);
-                    let target_start = target_buffer
-                        .clip_point_utf16(point_from_lsp(target_range.start), Bias::Left);
-                    let target_end = target_buffer
-                        .clip_point_utf16(point_from_lsp(target_range.end), Bias::Left);
-                    let target_location = Location {
-                        buffer: target_buffer_handle,
-                        range: target_buffer.anchor_after(target_start)
-                            ..target_buffer.anchor_before(target_end),
-                    };
+#[async_trait(?Send)]
+impl LspCommand for GetTypeDefinition {
+    type Response = Vec<LocationLink>;
+    type LspRequest = lsp::request::GotoTypeDefinition;
+    type ProtoRequest = proto::GetTypeDefinition;
 
-                    definitions.push(LocationLink {
-                        origin: origin_location,
-                        target: target_location,
-                    })
-                });
-            }
+    fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::GotoTypeDefinitionParams {
+        lsp::GotoTypeDefinitionParams {
+            text_document_position_params: lsp::TextDocumentPositionParams {
+                text_document: lsp::TextDocumentIdentifier {
+                    uri: lsp::Url::from_file_path(path).unwrap(),
+                },
+                position: point_to_lsp(self.position),
+            },
+            work_done_progress_params: Default::default(),
+            partial_result_params: Default::default(),
         }
+    }
 
-        Ok(definitions)
+    async fn response_from_lsp(
+        self,
+        message: Option<lsp::GotoTypeDefinitionResponse>,
+        project: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        cx: AsyncAppContext,
+    ) -> Result<Vec<LocationLink>> {
+        location_links_from_lsp(message, project, buffer, cx).await
     }
 
-    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDefinition {
-        proto::GetDefinition {
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetTypeDefinition {
+        proto::GetTypeDefinition {
             project_id,
             buffer_id: buffer.remote_id(),
             position: Some(language::proto::serialize_anchor(
@@ -443,7 +453,7 @@ impl LspCommand for GetDefinition {
     }
 
     async fn from_proto(
-        message: proto::GetDefinition,
+        message: proto::GetTypeDefinition,
         _: ModelHandle<Project>,
         buffer: ModelHandle<Buffer>,
         mut cx: AsyncAppContext,
@@ -468,101 +478,213 @@ impl LspCommand for GetDefinition {
         peer_id: PeerId,
         _: &clock::Global,
         cx: &AppContext,
-    ) -> proto::GetDefinitionResponse {
-        let links = response
-            .into_iter()
-            .map(|definition| {
-                let origin = definition.origin.map(|origin| {
-                    let buffer = project.serialize_buffer_for_peer(&origin.buffer, peer_id, cx);
-                    proto::Location {
-                        start: Some(serialize_anchor(&origin.range.start)),
-                        end: Some(serialize_anchor(&origin.range.end)),
-                        buffer: Some(buffer),
-                    }
-                });
-
-                let buffer =
-                    project.serialize_buffer_for_peer(&definition.target.buffer, peer_id, cx);
-                let target = proto::Location {
-                    start: Some(serialize_anchor(&definition.target.range.start)),
-                    end: Some(serialize_anchor(&definition.target.range.end)),
-                    buffer: Some(buffer),
-                };
-
-                proto::LocationLink {
-                    origin,
-                    target: Some(target),
-                }
-            })
-            .collect();
-        proto::GetDefinitionResponse { links }
+    ) -> proto::GetTypeDefinitionResponse {
+        let links = location_links_to_proto(response, project, peer_id, cx);
+        proto::GetTypeDefinitionResponse { links }
     }
 
     async fn response_from_proto(
         self,
-        message: proto::GetDefinitionResponse,
+        message: proto::GetTypeDefinitionResponse,
         project: ModelHandle<Project>,
         _: ModelHandle<Buffer>,
-        mut cx: AsyncAppContext,
+        cx: AsyncAppContext,
     ) -> Result<Vec<LocationLink>> {
-        let mut links = Vec::new();
-        for link in message.links {
-            let origin = match link.origin {
-                Some(origin) => {
-                    let buffer = origin
-                        .buffer
-                        .ok_or_else(|| anyhow!("missing origin buffer"))?;
-                    let buffer = project
-                        .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
-                        .await?;
-                    let start = origin
-                        .start
-                        .and_then(deserialize_anchor)
-                        .ok_or_else(|| anyhow!("missing origin start"))?;
-                    let end = origin
-                        .end
-                        .and_then(deserialize_anchor)
-                        .ok_or_else(|| anyhow!("missing origin end"))?;
-                    buffer
-                        .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
-                        .await;
-                    Some(Location {
-                        buffer,
-                        range: start..end,
-                    })
-                }
-                None => None,
-            };
+        location_links_from_proto(message.links, project, cx).await
+    }
 
-            let target = link.target.ok_or_else(|| anyhow!("missing target"))?;
-            let buffer = target.buffer.ok_or_else(|| anyhow!("missing buffer"))?;
-            let buffer = project
-                .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
-                .await?;
-            let start = target
-                .start
-                .and_then(deserialize_anchor)
-                .ok_or_else(|| anyhow!("missing target start"))?;
-            let end = target
-                .end
-                .and_then(deserialize_anchor)
-                .ok_or_else(|| anyhow!("missing target end"))?;
-            buffer
-                .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
-                .await;
-            let target = Location {
-                buffer,
-                range: start..end,
-            };
+    fn buffer_id_from_proto(message: &proto::GetTypeDefinition) -> u64 {
+        message.buffer_id
+    }
+}
 
-            links.push(LocationLink { origin, target })
+fn language_server_for_buffer(
+    project: &ModelHandle<Project>,
+    buffer: &ModelHandle<Buffer>,
+    cx: &mut AsyncAppContext,
+) -> Result<(Arc<CachedLspAdapter>, Arc<LanguageServer>)> {
+    project
+        .read_with(cx, |project, cx| {
+            project
+                .language_server_for_buffer(buffer.read(cx), cx)
+                .map(|(adapter, server)| (adapter.clone(), server.clone()))
+        })
+        .ok_or_else(|| anyhow!("no language server found for buffer"))
+}
+
+async fn location_links_from_proto(
+    proto_links: Vec<proto::LocationLink>,
+    project: ModelHandle<Project>,
+    mut cx: AsyncAppContext,
+) -> Result<Vec<LocationLink>> {
+    let mut links = Vec::new();
+
+    for link in proto_links {
+        let origin = match link.origin {
+            Some(origin) => {
+                let buffer = origin
+                    .buffer
+                    .ok_or_else(|| anyhow!("missing origin buffer"))?;
+                let buffer = project
+                    .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
+                    .await?;
+                let start = origin
+                    .start
+                    .and_then(deserialize_anchor)
+                    .ok_or_else(|| anyhow!("missing origin start"))?;
+                let end = origin
+                    .end
+                    .and_then(deserialize_anchor)
+                    .ok_or_else(|| anyhow!("missing origin end"))?;
+                buffer
+                    .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
+                    .await;
+                Some(Location {
+                    buffer,
+                    range: start..end,
+                })
+            }
+            None => None,
+        };
+
+        let target = link.target.ok_or_else(|| anyhow!("missing target"))?;
+        let buffer = target.buffer.ok_or_else(|| anyhow!("missing buffer"))?;
+        let buffer = project
+            .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
+            .await?;
+        let start = target
+            .start
+            .and_then(deserialize_anchor)
+            .ok_or_else(|| anyhow!("missing target start"))?;
+        let end = target
+            .end
+            .and_then(deserialize_anchor)
+            .ok_or_else(|| anyhow!("missing target end"))?;
+        buffer
+            .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
+            .await;
+        let target = Location {
+            buffer,
+            range: start..end,
+        };
+
+        links.push(LocationLink { origin, target })
+    }
+
+    Ok(links)
+}
+
+async fn location_links_from_lsp(
+    message: Option<lsp::GotoDefinitionResponse>,
+    project: ModelHandle<Project>,
+    buffer: ModelHandle<Buffer>,
+    mut cx: AsyncAppContext,
+) -> Result<Vec<LocationLink>> {
+    let message = match message {
+        Some(message) => message,
+        None => return Ok(Vec::new()),
+    };
+
+    let mut unresolved_links = Vec::new();
+    match message {
+        lsp::GotoDefinitionResponse::Scalar(loc) => {
+            unresolved_links.push((None, loc.uri, loc.range));
+        }
+
+        lsp::GotoDefinitionResponse::Array(locs) => {
+            unresolved_links.extend(locs.into_iter().map(|l| (None, l.uri, l.range)));
+        }
+
+        lsp::GotoDefinitionResponse::Link(links) => {
+            unresolved_links.extend(links.into_iter().map(|l| {
+                (
+                    l.origin_selection_range,
+                    l.target_uri,
+                    l.target_selection_range,
+                )
+            }));
         }
-        Ok(links)
     }
 
-    fn buffer_id_from_proto(message: &proto::GetDefinition) -> u64 {
-        message.buffer_id
+    let (lsp_adapter, language_server) = language_server_for_buffer(&project, &buffer, &mut cx)?;
+    let mut definitions = Vec::new();
+    for (origin_range, target_uri, target_range) in unresolved_links {
+        let target_buffer_handle = project
+            .update(&mut cx, |this, cx| {
+                this.open_local_buffer_via_lsp(
+                    target_uri,
+                    language_server.server_id(),
+                    lsp_adapter.name.clone(),
+                    cx,
+                )
+            })
+            .await?;
+
+        cx.read(|cx| {
+            let origin_location = origin_range.map(|origin_range| {
+                let origin_buffer = buffer.read(cx);
+                let origin_start =
+                    origin_buffer.clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left);
+                let origin_end =
+                    origin_buffer.clip_point_utf16(point_from_lsp(origin_range.end), Bias::Left);
+                Location {
+                    buffer: buffer.clone(),
+                    range: origin_buffer.anchor_after(origin_start)
+                        ..origin_buffer.anchor_before(origin_end),
+                }
+            });
+
+            let target_buffer = target_buffer_handle.read(cx);
+            let target_start =
+                target_buffer.clip_point_utf16(point_from_lsp(target_range.start), Bias::Left);
+            let target_end =
+                target_buffer.clip_point_utf16(point_from_lsp(target_range.end), Bias::Left);
+            let target_location = Location {
+                buffer: target_buffer_handle,
+                range: target_buffer.anchor_after(target_start)
+                    ..target_buffer.anchor_before(target_end),
+            };
+
+            definitions.push(LocationLink {
+                origin: origin_location,
+                target: target_location,
+            })
+        });
     }
+    Ok(definitions)
+}
+
+fn location_links_to_proto(
+    links: Vec<LocationLink>,
+    project: &mut Project,
+    peer_id: PeerId,
+    cx: &AppContext,
+) -> Vec<proto::LocationLink> {
+    links
+        .into_iter()
+        .map(|definition| {
+            let origin = definition.origin.map(|origin| {
+                let buffer = project.serialize_buffer_for_peer(&origin.buffer, peer_id, cx);
+                proto::Location {
+                    start: Some(serialize_anchor(&origin.range.start)),
+                    end: Some(serialize_anchor(&origin.range.end)),
+                    buffer: Some(buffer),
+                }
+            });
+
+            let buffer = project.serialize_buffer_for_peer(&definition.target.buffer, peer_id, cx);
+            let target = proto::Location {
+                start: Some(serialize_anchor(&definition.target.range.start)),
+                end: Some(serialize_anchor(&definition.target.range.end)),
+                buffer: Some(buffer),
+            };
+
+            proto::LocationLink {
+                origin,
+                target: Some(target),
+            }
+        })
+        .collect()
 }
 
 #[async_trait(?Send)]
@@ -595,13 +717,8 @@ impl LspCommand for GetReferences {
         mut cx: AsyncAppContext,
     ) -> Result<Vec<Location>> {
         let mut references = Vec::new();
-        let (lsp_adapter, language_server) = project
-            .read_with(&cx, |project, cx| {
-                project
-                    .language_server_for_buffer(buffer.read(cx), cx)
-                    .map(|(adapter, server)| (adapter.clone(), server.clone()))
-            })
-            .ok_or_else(|| anyhow!("no language server found for buffer"))?;
+        let (lsp_adapter, language_server) =
+            language_server_for_buffer(&project, &buffer, &mut cx)?;
 
         if let Some(locations) = locations {
             for lsp_location in locations {

crates/project/src/project.rs 🔗

@@ -3250,6 +3250,16 @@ impl Project {
         self.request_lsp(buffer.clone(), GetDefinition { position }, cx)
     }
 
+    pub fn type_definition<T: ToPointUtf16>(
+        &self,
+        buffer: &ModelHandle<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<LocationLink>>> {
+        let position = position.to_point_utf16(buffer.read(cx));
+        self.request_lsp(buffer.clone(), GetTypeDefinition { position }, cx)
+    }
+
     pub fn references<T: ToPointUtf16>(
         &self,
         buffer: &ModelHandle<Buffer>,

crates/rpc/proto/zed.proto 🔗

@@ -26,85 +26,87 @@ message Envelope {
 
         GetDefinition get_definition = 20;
         GetDefinitionResponse get_definition_response = 21;
-        GetReferences get_references = 22;
-        GetReferencesResponse get_references_response = 23;
-        GetDocumentHighlights get_document_highlights = 24;
-        GetDocumentHighlightsResponse get_document_highlights_response = 25;
-        GetProjectSymbols get_project_symbols = 26;
-        GetProjectSymbolsResponse get_project_symbols_response = 27;
-        OpenBufferForSymbol open_buffer_for_symbol = 28;
-        OpenBufferForSymbolResponse open_buffer_for_symbol_response = 29;
-
-        UpdateProject update_project = 30;
-        RegisterProjectActivity register_project_activity = 31;
-        UpdateWorktree update_worktree = 32;
-        UpdateWorktreeExtensions update_worktree_extensions = 33;
-
-        CreateProjectEntry create_project_entry = 34;
-        RenameProjectEntry rename_project_entry = 35;
-        CopyProjectEntry copy_project_entry = 36;
-        DeleteProjectEntry delete_project_entry = 37;
-        ProjectEntryResponse project_entry_response = 38;
-
-        UpdateDiagnosticSummary update_diagnostic_summary = 39;
-        StartLanguageServer start_language_server = 40;
-        UpdateLanguageServer update_language_server = 41;
-
-        OpenBufferById open_buffer_by_id = 42;
-        OpenBufferByPath open_buffer_by_path = 43;
-        OpenBufferResponse open_buffer_response = 44;
-        UpdateBuffer update_buffer = 45;
-        UpdateBufferFile update_buffer_file = 46;
-        SaveBuffer save_buffer = 47;
-        BufferSaved buffer_saved = 48;
-        BufferReloaded buffer_reloaded = 49;
-        ReloadBuffers reload_buffers = 50;
-        ReloadBuffersResponse reload_buffers_response = 51;
-        FormatBuffers format_buffers = 52;
-        FormatBuffersResponse format_buffers_response = 53;
-        GetCompletions get_completions = 54;
-        GetCompletionsResponse get_completions_response = 55;
-        ApplyCompletionAdditionalEdits apply_completion_additional_edits = 56;
-        ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 57;
-        GetCodeActions get_code_actions = 58;
-        GetCodeActionsResponse get_code_actions_response = 59;
-        GetHover get_hover = 60;
-        GetHoverResponse get_hover_response = 61;
-        ApplyCodeAction apply_code_action = 62;
-        ApplyCodeActionResponse apply_code_action_response = 63;
-        PrepareRename prepare_rename = 64;
-        PrepareRenameResponse prepare_rename_response = 65;
-        PerformRename perform_rename = 66;
-        PerformRenameResponse perform_rename_response = 67;
-        SearchProject search_project = 68;
-        SearchProjectResponse search_project_response = 69;
-
-        GetChannels get_channels = 70;
-        GetChannelsResponse get_channels_response = 71;
-        JoinChannel join_channel = 72;
-        JoinChannelResponse join_channel_response = 73;
-        LeaveChannel leave_channel = 74;
-        SendChannelMessage send_channel_message = 75;
-        SendChannelMessageResponse send_channel_message_response = 76;
-        ChannelMessageSent channel_message_sent = 77;
-        GetChannelMessages get_channel_messages = 78;
-        GetChannelMessagesResponse get_channel_messages_response = 79;
-
-        UpdateContacts update_contacts = 80;
-        UpdateInviteInfo update_invite_info = 81;
-        ShowContacts show_contacts = 82;
-
-        GetUsers get_users = 83;
-        FuzzySearchUsers fuzzy_search_users = 84;
-        UsersResponse users_response = 85;
-        RequestContact request_contact = 86;
-        RespondToContactRequest respond_to_contact_request = 87;
-        RemoveContact remove_contact = 88;
-
-        Follow follow = 89;
-        FollowResponse follow_response = 90;
-        UpdateFollowers update_followers = 91;
-        Unfollow unfollow = 92;
+        GetTypeDefinition get_type_definition = 22;
+        GetTypeDefinitionResponse get_type_definition_response = 23;
+        GetReferences get_references = 24;
+        GetReferencesResponse get_references_response = 25;
+        GetDocumentHighlights get_document_highlights = 26;
+        GetDocumentHighlightsResponse get_document_highlights_response = 27;
+        GetProjectSymbols get_project_symbols = 28;
+        GetProjectSymbolsResponse get_project_symbols_response = 29;
+        OpenBufferForSymbol open_buffer_for_symbol = 30;
+        OpenBufferForSymbolResponse open_buffer_for_symbol_response = 31;
+
+        UpdateProject update_project = 32;
+        RegisterProjectActivity register_project_activity = 33;
+        UpdateWorktree update_worktree = 34;
+        UpdateWorktreeExtensions update_worktree_extensions = 35;
+
+        CreateProjectEntry create_project_entry = 36;
+        RenameProjectEntry rename_project_entry = 37;
+        CopyProjectEntry copy_project_entry = 38;
+        DeleteProjectEntry delete_project_entry = 39;
+        ProjectEntryResponse project_entry_response = 40;
+
+        UpdateDiagnosticSummary update_diagnostic_summary = 41;
+        StartLanguageServer start_language_server = 42;
+        UpdateLanguageServer update_language_server = 43;
+
+        OpenBufferById open_buffer_by_id = 44;
+        OpenBufferByPath open_buffer_by_path = 45;
+        OpenBufferResponse open_buffer_response = 46;
+        UpdateBuffer update_buffer = 47;
+        UpdateBufferFile update_buffer_file = 48;
+        SaveBuffer save_buffer = 49;
+        BufferSaved buffer_saved = 50;
+        BufferReloaded buffer_reloaded = 51;
+        ReloadBuffers reload_buffers = 52;
+        ReloadBuffersResponse reload_buffers_response = 53;
+        FormatBuffers format_buffers = 54;
+        FormatBuffersResponse format_buffers_response = 55;
+        GetCompletions get_completions = 56;
+        GetCompletionsResponse get_completions_response = 57;
+        ApplyCompletionAdditionalEdits apply_completion_additional_edits = 58;
+        ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 59;
+        GetCodeActions get_code_actions = 60;
+        GetCodeActionsResponse get_code_actions_response = 61;
+        GetHover get_hover = 62;
+        GetHoverResponse get_hover_response = 63;
+        ApplyCodeAction apply_code_action = 64;
+        ApplyCodeActionResponse apply_code_action_response = 65;
+        PrepareRename prepare_rename = 66;
+        PrepareRenameResponse prepare_rename_response = 67;
+        PerformRename perform_rename = 68;
+        PerformRenameResponse perform_rename_response = 69;
+        SearchProject search_project = 70;
+        SearchProjectResponse search_project_response = 71;
+
+        GetChannels get_channels = 72;
+        GetChannelsResponse get_channels_response = 73;
+        JoinChannel join_channel = 74;
+        JoinChannelResponse join_channel_response = 75;
+        LeaveChannel leave_channel = 76;
+        SendChannelMessage send_channel_message = 77;
+        SendChannelMessageResponse send_channel_message_response = 78;
+        ChannelMessageSent channel_message_sent = 79;
+        GetChannelMessages get_channel_messages = 80;
+        GetChannelMessagesResponse get_channel_messages_response = 81;
+
+        UpdateContacts update_contacts = 82;
+        UpdateInviteInfo update_invite_info = 83;
+        ShowContacts show_contacts = 84;
+
+        GetUsers get_users = 85;
+        FuzzySearchUsers fuzzy_search_users = 86;
+        UsersResponse users_response = 87;
+        RequestContact request_contact = 88;
+        RespondToContactRequest respond_to_contact_request = 89;
+        RemoveContact remove_contact = 90;
+
+        Follow follow = 91;
+        FollowResponse follow_response = 92;
+        UpdateFollowers update_followers = 93;
+        Unfollow unfollow = 94;
     }
 }
 
@@ -263,6 +265,17 @@ message GetDefinitionResponse {
     repeated LocationLink links = 1;
 }
 
+message GetTypeDefinition {
+     uint64 project_id = 1;
+     uint64 buffer_id = 2;
+     Anchor position = 3;
+     repeated VectorClockEntry version = 4;
+ }
+
+message GetTypeDefinitionResponse {
+    repeated LocationLink links = 1;
+}
+
 message GetReferences {
      uint64 project_id = 1;
      uint64 buffer_id = 2;

crates/rpc/src/proto.rs 🔗

@@ -106,6 +106,8 @@ messages!(
     (GetCompletionsResponse, Background),
     (GetDefinition, Background),
     (GetDefinitionResponse, Background),
+    (GetTypeDefinition, Background),
+    (GetTypeDefinitionResponse, Background),
     (GetDocumentHighlights, Background),
     (GetDocumentHighlightsResponse, Background),
     (GetReferences, Background),
@@ -183,6 +185,7 @@ request_messages!(
     (GetHover, GetHoverResponse),
     (GetCompletions, GetCompletionsResponse),
     (GetDefinition, GetDefinitionResponse),
+    (GetTypeDefinition, GetTypeDefinitionResponse),
     (GetDocumentHighlights, GetDocumentHighlightsResponse),
     (GetReferences, GetReferencesResponse),
     (GetProjectSymbols, GetProjectSymbolsResponse),
@@ -226,6 +229,7 @@ entity_messages!(
     GetCodeActions,
     GetCompletions,
     GetDefinition,
+    GetTypeDefinition,
     GetDocumentHighlights,
     GetHover,
     GetReferences,

crates/rpc/src/rpc.rs 🔗

@@ -6,4 +6,4 @@ pub use conn::Connection;
 pub use peer::*;
 mod macros;
 
-pub const PROTOCOL_VERSION: u32 = 28;
+pub const PROTOCOL_VERSION: u32 = 29;

crates/zed/src/menus.rs 🔗

@@ -274,6 +274,10 @@ pub fn menus() -> Vec<Menu<'static>> {
                     name: "Go to Definition",
                     action: Box::new(editor::GoToDefinition),
                 },
+                MenuItem::Action {
+                    name: "Go to Type Definition",
+                    action: Box::new(editor::GoToTypeDefinition),
+                },
                 MenuItem::Action {
                     name: "Go to References",
                     action: Box::new(editor::FindAllReferences),