diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 02c3a2ea8a..2eb64d10dc 100644 --- a/crates/collab/src/integration_tests.rs +++ b/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 => { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6d384e1d57..8a20f8780b 100644 --- a/crates/editor/src/editor.rs +++ b/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::(cx); workspace::register_followable_item::(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. diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b4762b5774..cffa3454a6 100644 --- a/crates/editor/src/element.rs +++ b/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 } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 42dece7185..e1129dc092 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -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, ) { + 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, +) { + 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::( + 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) { + // 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::(cx); } pub fn go_to_fetched_definition( diff --git a/crates/gpui/src/platform/event.rs b/crates/gpui/src/platform/event.rs index 61cfa99bfe..4c4cc90b32 100644 --- a/crates/gpui/src/platform/event.rs +++ b/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, .. } diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index 4d3aa6cf9a..027c0ed5c8 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/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, } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 367a9d70c8..0624789a4e 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/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 { diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 4b0050e943..88e4d0a498 100644 --- a/crates/gpui/src/presenter.rs +++ b/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 { diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 2e124335c0..3af86db0fd 100644 --- a/crates/project/src/lsp_command.rs +++ b/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; + type Response = Vec; type LspRequest = lsp::request::GotoDefinition; type ProtoRequest = proto::GetDefinition; @@ -351,7 +353,7 @@ impl LspCommand for GetDefinition { project: ModelHandle, buffer: ModelHandle, mut cx: AsyncAppContext, - ) -> Result> { + ) -> Result> { 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, + response: Vec, 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, _: ModelHandle, mut cx: AsyncAppContext, - ) -> Result> { - let mut locations = Vec::new(); - for location in message.locations { - let buffer = location.buffer.ok_or_else(|| anyhow!("missing buffer"))?; + ) -> Result> { + 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 { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 68d6cbe983..7482e59650 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -208,6 +208,12 @@ pub struct Location { pub range: Range, } +#[derive(Debug)] +pub struct LocationLink { + pub origin: Option, + pub target: Location, +} + #[derive(Debug)] pub struct DocumentHighlight { pub range: Range, @@ -2915,7 +2921,7 @@ impl Project { buffer: &ModelHandle, position: T, cx: &mut ModelContext, - ) -> Task>> { + ) -> Task>> { 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)] diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 1c271344df..69ccae1704 100644 --- a/crates/rpc/proto/zed.proto +++ b/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;