Working underline based on symbol origin

Keith Simmons created

Change summary

crates/collab/src/integration_tests.rs     |  21 ++-
crates/editor/src/editor.rs                |   9 +
crates/editor/src/element.rs               |  34 +++--
crates/editor/src/link_go_to_definition.rs | 130 +++++++++++++++++++++++
crates/gpui/src/platform/event.rs          |  10 +
crates/gpui/src/platform/mac/event.rs      |  32 ++++-
crates/gpui/src/platform/mac/window.rs     |  11 +
crates/gpui/src/presenter.rs               |  13 ++
crates/project/src/lsp_command.rs          | 127 +++++++++++++++++-----
crates/project/src/project.rs              |  12 +
crates/rpc/proto/zed.proto                 |   7 +
11 files changed, 332 insertions(+), 74 deletions(-)

Detailed changes

crates/collab/src/integration_tests.rs 🔗

@@ -1980,13 +1980,13 @@ async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
     cx_b.read(|cx| {
         assert_eq!(definitions_1.len(), 1);
         assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
-        let target_buffer = definitions_1[0].buffer.read(cx);
+        let target_buffer = definitions_1[0].target.buffer.read(cx);
         assert_eq!(
             target_buffer.text(),
             "const TWO: usize = 2;\nconst THREE: usize = 3;"
         );
         assert_eq!(
-            definitions_1[0].range.to_point(target_buffer),
+            definitions_1[0].target.range.to_point(target_buffer),
             Point::new(0, 6)..Point::new(0, 9)
         );
     });
@@ -2009,17 +2009,20 @@ async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
     cx_b.read(|cx| {
         assert_eq!(definitions_2.len(), 1);
         assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
-        let target_buffer = definitions_2[0].buffer.read(cx);
+        let target_buffer = definitions_2[0].target.buffer.read(cx);
         assert_eq!(
             target_buffer.text(),
             "const TWO: usize = 2;\nconst THREE: usize = 3;"
         );
         assert_eq!(
-            definitions_2[0].range.to_point(target_buffer),
+            definitions_2[0].target.range.to_point(target_buffer),
             Point::new(1, 6)..Point::new(1, 11)
         );
     });
-    assert_eq!(definitions_1[0].buffer, definitions_2[0].buffer);
+    assert_eq!(
+        definitions_1[0].target.buffer,
+        definitions_2[0].target.buffer
+    );
 }
 
 #[gpui::test(iterations = 10)]
@@ -2554,7 +2557,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
     let buffer_b2 = buffer_b2.await.unwrap();
     let definitions = definitions.await.unwrap();
     assert_eq!(definitions.len(), 1);
-    assert_eq!(definitions[0].buffer, buffer_b2);
+    assert_eq!(definitions[0].target.buffer, buffer_b2);
 }
 
 #[gpui::test(iterations = 10)]
@@ -5593,9 +5596,9 @@ impl TestClient {
                             log::info!("{}: detaching definitions request", guest_username);
                             cx.update(|cx| definitions.detach_and_log_err(cx));
                         } else {
-                            client
-                                .buffers
-                                .extend(definitions.await?.into_iter().map(|loc| loc.buffer));
+                            client.buffers.extend(
+                                definitions.await?.into_iter().map(|loc| loc.target.buffer),
+                            );
                         }
                     }
                     50..=54 => {

crates/editor/src/editor.rs 🔗

@@ -316,6 +316,7 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_async_action(Editor::find_all_references);
 
     hover_popover::init(cx);
+    link_go_to_definition::init(cx);
 
     workspace::register_project_item::<Editor>(cx);
     workspace::register_followable_item::<Editor>(cx);
@@ -4603,9 +4604,13 @@ impl Editor {
             workspace.update(&mut cx, |workspace, cx| {
                 let nav_history = workspace.active_pane().read(cx).nav_history().clone();
                 for definition in definitions {
-                    let range = definition.range.to_offset(definition.buffer.read(cx));
+                    let range = definition
+                        .target
+                        .range
+                        .to_offset(definition.target.buffer.read(cx));
 
-                    let target_editor_handle = workspace.open_project_item(definition.buffer, cx);
+                    let target_editor_handle =
+                        workspace.open_project_item(definition.target.buffer, cx);
                     target_editor_handle.update(cx, |target_editor, cx| {
                         // When selecting a definition in a different buffer, disable the nav history
                         // to avoid creating a history entry at the previous cursor location.

crates/editor/src/element.rs 🔗

@@ -8,6 +8,7 @@ use crate::{
     hover_popover::HoverAt,
     EditorStyle,
 };
+use crate::{hover_popover::HoverAt, link_go_to_definition::FetchDefinition};
 use clock::ReplicaId;
 use collections::{BTreeMap, HashMap};
 use gpui::{
@@ -1417,7 +1418,7 @@ impl Element for EditorElement {
                 cx,
             ),
             Event::LeftMouseUp { position, .. } => self.mouse_up(*position, cx),
-            Event::LeftMouseDragged { position } => {
+            Event::LeftMouseDragged { position, .. } => {
                 self.mouse_dragged(*position, layout, paint, cx)
             }
             Event::ScrollWheel {
@@ -1426,9 +1427,26 @@ impl Element for EditorElement {
                 precise,
             } => self.scroll(*position, *delta, *precise, layout, paint, cx),
             Event::KeyDown { input, .. } => self.key_down(input.as_deref(), cx),
-            Event::MouseMoved { position, .. } => {
+            Event::MouseMoved { position, cmd, .. } => {
                 // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
                 // Don't trigger hover popover if mouse is hovering over context menu
+
+                let point = if paint.text_bounds.contains_point(*position) {
+                    let (point, overshoot) =
+                        paint.point_for_position(&self.snapshot(cx), layout, *position);
+                    if overshoot.is_zero() {
+                        Some(point)
+                    } else {
+                        None
+                    }
+                } else {
+                    None
+                };
+
+                if *cmd {
+                    cx.dispatch_action(FetchDefinition { point });
+                }
+
                 if paint
                     .context_menu_bounds
                     .map_or(false, |context_menu_bounds| {
@@ -1445,18 +1463,6 @@ impl Element for EditorElement {
                     return false;
                 }
 
-                let point = if paint.text_bounds.contains_point(*position) {
-                    let (point, overshoot) =
-                        paint.point_for_position(&self.snapshot(cx), layout, *position);
-                    if overshoot.is_zero() {
-                        Some(point)
-                    } else {
-                        None
-                    }
-                } else {
-                    None
-                };
-
                 cx.dispatch_action(HoverAt { point });
                 true
             }
@@ -5,7 +5,9 @@ use std::{
 
 use gpui::{
     actions,
+    color::Color,
     elements::{Flex, MouseEventHandler, Padding, Text},
+    fonts::{HighlightStyle, Underline},
     impl_internal_actions,
     platform::CursorStyle,
     Axis, Element, ElementBox, ModelHandle, MutableAppContext, RenderContext, Task, ViewContext,
@@ -45,9 +47,135 @@ pub struct LinkGoToDefinitionState {
 
 pub fn fetch_definition(
     editor: &mut Editor,
-    FetchDefinition { point }: &FetchDefinition,
+    &FetchDefinition { point }: &FetchDefinition,
     cx: &mut ViewContext<Editor>,
 ) {
+    if let Some(point) = point {
+        show_link_definition(editor, point, cx);
+    } else {
+        //TODO: Also needs to be dispatched when cmd modifier is released
+        hide_link_definition(editor, cx);
+    }
+}
+
+pub fn show_link_definition(
+    editor: &mut Editor,
+    point: DisplayPoint,
+    cx: &mut ViewContext<Editor>,
+) {
+    if editor.pending_rename.is_some() {
+        return;
+    }
+
+    let snapshot = editor.snapshot(cx);
+    let multibuffer_offset = point.to_offset(&snapshot.display_snapshot, Bias::Left);
+
+    let (buffer, buffer_position) = if let Some(output) = editor
+        .buffer
+        .read(cx)
+        .text_anchor_for_position(multibuffer_offset, cx)
+    {
+        output
+    } else {
+        return;
+    };
+
+    let excerpt_id = if let Some((excerpt_id, _, _)) = editor
+        .buffer()
+        .read(cx)
+        .excerpt_containing(multibuffer_offset, cx)
+    {
+        excerpt_id
+    } else {
+        return;
+    };
+
+    let project = if let Some(project) = editor.project.clone() {
+        project
+    } else {
+        return;
+    };
+
+    // Get input anchor
+    let anchor = snapshot
+        .buffer_snapshot
+        .anchor_at(multibuffer_offset, Bias::Left);
+
+    // Don't request again if the location is the same as the previous request
+    if let Some(triggered_from) = &editor.link_go_to_definition_state.triggered_from {
+        if triggered_from
+            .cmp(&anchor, &snapshot.buffer_snapshot)
+            .is_eq()
+        {
+            return;
+        }
+    }
+
+    let task = cx.spawn_weak(|this, mut cx| {
+        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)
+                })
+            });
+
+            let origin_range = definition_request.await.ok().and_then(|definition_result| {
+                definition_result
+                    .into_iter()
+                    .filter_map(|link| {
+                        link.origin.map(|origin| {
+                            let start = snapshot
+                                .buffer_snapshot
+                                .anchor_in_excerpt(excerpt_id.clone(), origin.range.start);
+                            let end = snapshot
+                                .buffer_snapshot
+                                .anchor_in_excerpt(excerpt_id.clone(), origin.range.end);
+
+                            start..end
+                        })
+                    })
+                    .next()
+            });
+
+            if let Some(this) = this.upgrade(&cx) {
+                this.update(&mut cx, |this, cx| {
+                    if let Some(origin_range) = origin_range {
+                        this.highlight_text::<LinkGoToDefinitionState>(
+                            vec![origin_range],
+                            HighlightStyle {
+                                underline: Some(Underline {
+                                    color: Some(Color::red()),
+                                    thickness: 1.0.into(),
+                                    squiggly: false,
+                                }),
+                                ..Default::default()
+                            },
+                            cx,
+                        )
+                    }
+                })
+            }
+
+            Ok::<_, anyhow::Error>(())
+        }
+        .log_err()
+    });
+
+    editor.link_go_to_definition_state.task = Some(task);
+}
+
+pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
+    // only notify the context once
+    if editor.link_go_to_definition_state.symbol_range.is_some() {
+        editor.link_go_to_definition_state.symbol_range.take();
+        cx.notify();
+    }
+
+    editor.link_go_to_definition_state.task = None;
+    editor.link_go_to_definition_state.triggered_from = None;
+
+    editor.clear_text_highlights::<LinkGoToDefinitionState>(cx);
 }
 
 pub fn go_to_fetched_definition(

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

@@ -32,6 +32,10 @@ pub enum Event {
     },
     LeftMouseDragged {
         position: Vector2F,
+        ctrl: bool,
+        alt: bool,
+        shift: bool,
+        cmd: bool,
     },
     RightMouseDown {
         position: Vector2F,
@@ -61,6 +65,10 @@ pub enum Event {
     MouseMoved {
         position: Vector2F,
         left_mouse_down: bool,
+        ctrl: bool,
+        cmd: bool,
+        alt: bool,
+        shift: bool,
     },
 }
 
@@ -71,7 +79,7 @@ impl Event {
             Event::ScrollWheel { position, .. }
             | Event::LeftMouseDown { position, .. }
             | Event::LeftMouseUp { position, .. }
-            | Event::LeftMouseDragged { position }
+            | Event::LeftMouseDragged { position, .. }
             | Event::RightMouseDown { position, .. }
             | Event::RightMouseUp { position, .. }
             | Event::NavigateMouseDown { position, .. }

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

@@ -218,14 +218,19 @@ impl Event {
                     direction,
                 })
             }
-            NSEventType::NSLeftMouseDragged => {
-                window_height.map(|window_height| Self::LeftMouseDragged {
+            NSEventType::NSLeftMouseDragged => window_height.map(|window_height| {
+                let modifiers = native_event.modifierFlags();
+                Self::LeftMouseDragged {
                     position: vec2f(
                         native_event.locationInWindow().x as f32,
                         window_height - native_event.locationInWindow().y as f32,
                     ),
-                })
-            }
+                    ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
+                    alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
+                    shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
+                    cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
+                }
+            }),
             NSEventType::NSScrollWheel => window_height.map(|window_height| Self::ScrollWheel {
                 position: vec2f(
                     native_event.locationInWindow().x as f32,
@@ -237,12 +242,19 @@ impl Event {
                 ),
                 precise: native_event.hasPreciseScrollingDeltas() == YES,
             }),
-            NSEventType::NSMouseMoved => window_height.map(|window_height| Self::MouseMoved {
-                position: vec2f(
-                    native_event.locationInWindow().x as f32,
-                    window_height - native_event.locationInWindow().y as f32,
-                ),
-                left_mouse_down: NSEvent::pressedMouseButtons(nil) & 1 != 0,
+            NSEventType::NSMouseMoved => window_height.map(|window_height| {
+                let modifiers = native_event.modifierFlags();
+                Self::MouseMoved {
+                    position: vec2f(
+                        native_event.locationInWindow().x as f32,
+                        window_height - native_event.locationInWindow().y as f32,
+                    ),
+                    left_mouse_down: NSEvent::pressedMouseButtons(nil) & 1 != 0,
+                    ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
+                    alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
+                    shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
+                    cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
+                }
             }),
             _ => None,
         }

crates/gpui/src/platform/mac/window.rs 🔗

@@ -597,7 +597,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
 
     if let Some(event) = event {
         match &event {
-            Event::LeftMouseDragged { position } => {
+            Event::LeftMouseDragged { position, .. } => {
                 window_state_borrow.synthetic_drag_counter += 1;
                 window_state_borrow
                     .executor
@@ -805,7 +805,14 @@ async fn synthetic_drag(
             if window_state_borrow.synthetic_drag_counter == drag_id {
                 if let Some(mut callback) = window_state_borrow.event_callback.take() {
                     drop(window_state_borrow);
-                    callback(Event::LeftMouseDragged { position });
+                    callback(Event::LeftMouseDragged {
+                        // TODO: Make sure empty modifiers is correct for this
+                        position,
+                        shift: false,
+                        ctrl: false,
+                        alt: false,
+                        cmd: false,
+                    });
                     window_state.borrow_mut().event_callback = Some(callback);
                 }
             } else {

crates/gpui/src/presenter.rs 🔗

@@ -294,7 +294,13 @@ impl Presenter {
                 Event::MouseMoved { .. } => {
                     self.last_mouse_moved_event = Some(event.clone());
                 }
-                Event::LeftMouseDragged { position } => {
+                Event::LeftMouseDragged {
+                    position,
+                    shift,
+                    ctrl,
+                    alt,
+                    cmd,
+                } => {
                     if let Some((clicked_region, prev_drag_position)) = self
                         .clicked_region
                         .as_ref()
@@ -308,6 +314,10 @@ impl Presenter {
                     self.last_mouse_moved_event = Some(Event::MouseMoved {
                         position,
                         left_mouse_down: true,
+                        shift,
+                        ctrl,
+                        alt,
+                        cmd,
                     });
                 }
                 _ => {}
@@ -403,6 +413,7 @@ impl Presenter {
         if let Event::MouseMoved {
             position,
             left_mouse_down,
+            ..
         } = event
         {
             if !left_mouse_down {

crates/project/src/lsp_command.rs 🔗

@@ -1,4 +1,6 @@
-use crate::{DocumentHighlight, Hover, HoverBlock, Location, Project, ProjectTransaction};
+use crate::{
+    DocumentHighlight, Hover, HoverBlock, Location, LocationLink, Project, ProjectTransaction,
+};
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use client::{proto, PeerId};
@@ -328,7 +330,7 @@ impl LspCommand for PerformRename {
 
 #[async_trait(?Send)]
 impl LspCommand for GetDefinition {
-    type Response = Vec<Location>;
+    type Response = Vec<LocationLink>;
     type LspRequest = lsp::request::GotoDefinition;
     type ProtoRequest = proto::GetDefinition;
 
@@ -351,7 +353,7 @@ impl LspCommand for GetDefinition {
         project: ModelHandle<Project>,
         buffer: ModelHandle<Buffer>,
         mut cx: AsyncAppContext,
-    ) -> Result<Vec<Location>> {
+    ) -> Result<Vec<LocationLink>> {
         let mut definitions = Vec::new();
         let (lsp_adapter, language_server) = project
             .read_with(&cx, |project, cx| {
@@ -362,24 +364,26 @@ impl LspCommand for GetDefinition {
             .ok_or_else(|| anyhow!("no language server found for buffer"))?;
 
         if let Some(message) = message {
-            let mut unresolved_locations = Vec::new();
+            let mut unresolved_links = Vec::new();
             match message {
                 lsp::GotoDefinitionResponse::Scalar(loc) => {
-                    unresolved_locations.push((loc.uri, loc.range));
+                    unresolved_links.push((None, loc.uri, loc.range));
                 }
                 lsp::GotoDefinitionResponse::Array(locs) => {
-                    unresolved_locations.extend(locs.into_iter().map(|l| (l.uri, l.range)));
+                    unresolved_links.extend(locs.into_iter().map(|l| (None, l.uri, l.range)));
                 }
                 lsp::GotoDefinitionResponse::Link(links) => {
-                    unresolved_locations.extend(
-                        links
-                            .into_iter()
-                            .map(|l| (l.target_uri, l.target_selection_range)),
-                    );
+                    unresolved_links.extend(links.into_iter().map(|l| {
+                        (
+                            l.origin_selection_range,
+                            l.target_uri,
+                            l.target_selection_range,
+                        )
+                    }));
                 }
             }
 
-            for (target_uri, target_range) in unresolved_locations {
+            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(
@@ -392,16 +396,34 @@ impl LspCommand for GetDefinition {
                     .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);
-                    definitions.push(Location {
+                    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,
+                    })
                 });
             }
         }
@@ -441,24 +463,39 @@ impl LspCommand for GetDefinition {
     }
 
     fn response_to_proto(
-        response: Vec<Location>,
+        response: Vec<LocationLink>,
         project: &mut Project,
         peer_id: PeerId,
         _: &clock::Global,
         cx: &AppContext,
     ) -> proto::GetDefinitionResponse {
-        let locations = response
+        let links = response
             .into_iter()
             .map(|definition| {
-                let buffer = project.serialize_buffer_for_peer(&definition.buffer, peer_id, cx);
-                proto::Location {
-                    start: Some(serialize_anchor(&definition.range.start)),
-                    end: Some(serialize_anchor(&definition.range.end)),
+                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 { locations }
+        proto::GetDefinitionResponse { links }
     }
 
     async fn response_from_proto(
@@ -467,30 +504,60 @@ impl LspCommand for GetDefinition {
         project: ModelHandle<Project>,
         _: ModelHandle<Buffer>,
         mut cx: AsyncAppContext,
-    ) -> Result<Vec<Location>> {
-        let mut locations = Vec::new();
-        for location in message.locations {
-            let buffer = location.buffer.ok_or_else(|| anyhow!("missing buffer"))?;
+    ) -> 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,
+            };
+
+            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 = location
+            let start = target
                 .start
                 .and_then(deserialize_anchor)
                 .ok_or_else(|| anyhow!("missing target start"))?;
-            let end = location
+            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;
-            locations.push(Location {
+            let target = Location {
                 buffer,
                 range: start..end,
-            })
+            };
+
+            links.push(LocationLink { origin, target })
         }
-        Ok(locations)
+        Ok(links)
     }
 
     fn buffer_id_from_proto(message: &proto::GetDefinition) -> u64 {

crates/project/src/project.rs 🔗

@@ -208,6 +208,12 @@ pub struct Location {
     pub range: Range<language::Anchor>,
 }
 
+#[derive(Debug)]
+pub struct LocationLink {
+    pub origin: Option<Location>,
+    pub target: Location,
+}
+
 #[derive(Debug)]
 pub struct DocumentHighlight {
     pub range: Range<language::Anchor>,
@@ -2915,7 +2921,7 @@ impl Project {
         buffer: &ModelHandle<Buffer>,
         position: T,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<Vec<Location>>> {
+    ) -> Task<Result<Vec<LocationLink>>> {
         let position = position.to_point_utf16(buffer.read(cx));
         self.request_lsp(buffer.clone(), GetDefinition { position }, cx)
     }
@@ -7564,7 +7570,7 @@ mod tests {
         assert_eq!(definitions.len(), 1);
         let definition = definitions.pop().unwrap();
         cx.update(|cx| {
-            let target_buffer = definition.buffer.read(cx);
+            let target_buffer = definition.target.buffer.read(cx);
             assert_eq!(
                 target_buffer
                     .file()
@@ -7574,7 +7580,7 @@ mod tests {
                     .abs_path(cx),
                 Path::new("/dir/a.rs"),
             );
-            assert_eq!(definition.range.to_offset(target_buffer), 9..10);
+            assert_eq!(definition.target.range.to_offset(target_buffer), 9..10);
             assert_eq!(
                 list_worktrees(&project, cx),
                 [("/dir/b.rs".as_ref(), true), ("/dir/a.rs".as_ref(), false)]

crates/rpc/proto/zed.proto 🔗

@@ -248,7 +248,7 @@ message GetDefinition {
  }
 
 message GetDefinitionResponse {
-    repeated Location locations = 1;
+    repeated LocationLink links = 1;
 }
 
 message GetReferences {
@@ -279,6 +279,11 @@ message Location {
     Anchor end = 3;
 }
 
+message LocationLink {
+    optional Location origin = 1;
+    Location target = 2;
+}
+
 message DocumentHighlight {
     Kind kind = 1;
     Anchor start = 2;