Detailed changes
@@ -2416,6 +2416,7 @@ dependencies = [
"itertools 0.10.5",
"language",
"lazy_static",
+ "linkify",
"log",
"lsp",
"multi_buffer",
@@ -4134,6 +4135,15 @@ dependencies = [
"safemem",
]
+[[package]]
+name = "linkify"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1dfa36d52c581e9ec783a7ce2a5e0143da6237be5811a0b3153fedfdbe9f780"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "linkme"
version = "0.3.17"
@@ -20,7 +20,7 @@ test-support = [
"util/test-support",
"workspace/test-support",
"tree-sitter-rust",
- "tree-sitter-typescript"
+ "tree-sitter-typescript",
]
[dependencies]
@@ -33,13 +33,14 @@ convert_case = "0.6.0"
copilot = { path = "../copilot" }
db = { path = "../db" }
futures.workspace = true
-fuzzy = { path = "../fuzzy" }
+fuzzy = { path = "../fuzzy" }
git = { path = "../git" }
gpui = { path = "../gpui" }
indoc = "1.0.4"
itertools = "0.10"
language = { path = "../language" }
lazy_static.workspace = true
+linkify = "0.10.0"
log.workspace = true
lsp = { path = "../lsp" }
multi_buffer = { path = "../multi_buffer" }
@@ -25,8 +25,8 @@ mod wrap_map;
use crate::EditorStyle;
use crate::{
- link_go_to_definition::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt,
- InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
+ hover_links::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt, InlayId,
+ MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
};
pub use block_map::{BlockMap, BlockPoint};
use collections::{BTreeMap, HashMap, HashSet};
@@ -1168,7 +1168,7 @@ mod tests {
use super::*;
use crate::{
display_map::{InlayHighlights, TextHighlights},
- link_go_to_definition::InlayHighlight,
+ hover_links::InlayHighlight,
InlayId, MultiBuffer,
};
use gpui::AppContext;
@@ -22,9 +22,9 @@ mod inlay_hint_cache;
mod debounced_delay;
mod git;
mod highlight_matching_bracket;
+mod hover_links;
mod hover_popover;
pub mod items;
-mod link_go_to_definition;
mod mouse_context_menu;
pub mod movement;
mod persistence;
@@ -77,7 +77,7 @@ use language::{
Language, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
};
-use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState};
+use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
use lsp::{DiagnosticSeverity, LanguageServerId};
use mouse_context_menu::MouseContextMenu;
use movement::TextLayoutDetails;
@@ -402,7 +402,7 @@ pub struct Editor {
remote_id: Option<ViewId>,
hover_state: HoverState,
gutter_hovered: bool,
- link_go_to_definition_state: LinkGoToDefinitionState,
+ hovered_link_state: Option<HoveredLinkState>,
copilot_state: CopilotState,
inlay_hint_cache: InlayHintCache,
next_inlay_id: usize,
@@ -1477,7 +1477,7 @@ impl Editor {
leader_peer_id: None,
remote_id: None,
hover_state: Default::default(),
- link_go_to_definition_state: Default::default(),
+ hovered_link_state: Default::default(),
copilot_state: Default::default(),
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
gutter_hovered: false,
@@ -7243,11 +7243,8 @@ impl Editor {
cx.spawn(|editor, mut cx| async move {
let definitions = definitions.await?;
editor.update(&mut cx, |editor, cx| {
- editor.navigate_to_definitions(
- definitions
- .into_iter()
- .map(GoToDefinitionLink::Text)
- .collect(),
+ editor.navigate_to_hover_links(
+ definitions.into_iter().map(HoverLink::Text).collect(),
split,
cx,
);
@@ -7257,9 +7254,9 @@ impl Editor {
.detach_and_log_err(cx);
}
- pub fn navigate_to_definitions(
+ pub fn navigate_to_hover_links(
&mut self,
- mut definitions: Vec<GoToDefinitionLink>,
+ mut definitions: Vec<HoverLink>,
split: bool,
cx: &mut ViewContext<Editor>,
) {
@@ -7271,10 +7268,14 @@ impl Editor {
if definitions.len() == 1 {
let definition = definitions.pop().unwrap();
let target_task = match definition {
- GoToDefinitionLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))),
- GoToDefinitionLink::InlayHint(lsp_location, server_id) => {
+ HoverLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))),
+ HoverLink::InlayHint(lsp_location, server_id) => {
self.compute_target_location(lsp_location, server_id, cx)
}
+ HoverLink::Url(url) => {
+ cx.open_url(&url);
+ Task::ready(Ok(None))
+ }
};
cx.spawn(|editor, mut cx| async move {
let target = target_task.await.context("target resolution task")?;
@@ -7325,29 +7326,27 @@ impl Editor {
let title = definitions
.iter()
.find_map(|definition| match definition {
- GoToDefinitionLink::Text(link) => {
- link.origin.as_ref().map(|origin| {
- let buffer = origin.buffer.read(cx);
- format!(
- "Definitions for {}",
- buffer
- .text_for_range(origin.range.clone())
- .collect::<String>()
- )
- })
- }
- GoToDefinitionLink::InlayHint(_, _) => None,
+ HoverLink::Text(link) => link.origin.as_ref().map(|origin| {
+ let buffer = origin.buffer.read(cx);
+ format!(
+ "Definitions for {}",
+ buffer
+ .text_for_range(origin.range.clone())
+ .collect::<String>()
+ )
+ }),
+ HoverLink::InlayHint(_, _) => None,
+ HoverLink::Url(_) => None,
})
.unwrap_or("Definitions".to_string());
let location_tasks = definitions
.into_iter()
.map(|definition| match definition {
- GoToDefinitionLink::Text(link) => {
- Task::Ready(Some(Ok(Some(link.target))))
- }
- GoToDefinitionLink::InlayHint(lsp_location, server_id) => {
+ HoverLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))),
+ HoverLink::InlayHint(lsp_location, server_id) => {
editor.compute_target_location(lsp_location, server_id, cx)
}
+ HoverLink::Url(_) => Task::ready(Ok(None)),
})
.collect::<Vec<_>>();
(title, location_tasks)
@@ -9,11 +9,6 @@ use crate::{
self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
},
items::BufferSearchHighlights,
- link_go_to_definition::{
- go_to_fetched_definition, go_to_fetched_type_definition, show_link_definition,
- update_go_to_definition_link, update_inlay_link_and_hover_points, GoToDefinitionTrigger,
- LinkGoToDefinitionState,
- },
mouse_context_menu,
scroll::scroll_amount::ScrollAmount,
CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode,
@@ -337,7 +332,14 @@ impl EditorElement {
register_action(view, cx, Editor::display_cursor_names);
}
- fn register_key_listeners(&self, cx: &mut ElementContext) {
+ fn register_key_listeners(
+ &self,
+ cx: &mut ElementContext,
+ text_bounds: Bounds<Pixels>,
+ layout: &LayoutState,
+ ) {
+ let position_map = layout.position_map.clone();
+ let stacking_order = cx.stacking_order().clone();
cx.on_key_event({
let editor = self.editor.clone();
move |event: &ModifiersChangedEvent, phase, cx| {
@@ -345,46 +347,41 @@ impl EditorElement {
return;
}
- if editor.update(cx, |editor, cx| Self::modifiers_changed(editor, event, cx)) {
- cx.stop_propagation();
- }
+ editor.update(cx, |editor, cx| {
+ Self::modifiers_changed(
+ editor,
+ event,
+ &position_map,
+ text_bounds,
+ &stacking_order,
+ cx,
+ )
+ })
}
});
}
- pub(crate) fn modifiers_changed(
+ fn modifiers_changed(
editor: &mut Editor,
event: &ModifiersChangedEvent,
+ position_map: &PositionMap,
+ text_bounds: Bounds<Pixels>,
+ stacking_order: &StackingOrder,
cx: &mut ViewContext<Editor>,
- ) -> bool {
- let pending_selection = editor.has_pending_selection();
-
- if let Some(point) = &editor.link_go_to_definition_state.last_trigger_point {
- if event.command && !pending_selection {
- let point = point.clone();
- let snapshot = editor.snapshot(cx);
- let kind = point.definition_kind(event.shift);
-
- show_link_definition(kind, editor, point, snapshot, cx);
- return false;
- }
- }
-
+ ) {
+ let mouse_position = cx.mouse_position();
+ if !text_bounds.contains(&mouse_position)
+ || !cx.was_top_layer(&mouse_position, stacking_order)
{
- if editor.link_go_to_definition_state.symbol_range.is_some()
- || !editor.link_go_to_definition_state.definitions.is_empty()
- {
- editor.link_go_to_definition_state.symbol_range.take();
- editor.link_go_to_definition_state.definitions.clear();
- cx.notify();
- }
-
- editor.link_go_to_definition_state.task = None;
-
- editor.clear_highlights::<LinkGoToDefinitionState>(cx);
+ return;
}
- false
+ editor.update_hovered_link(
+ position_map.point_for_position(text_bounds, mouse_position),
+ &position_map.snapshot,
+ event.modifiers,
+ cx,
+ )
}
fn mouse_left_down(
@@ -485,13 +482,7 @@ impl EditorElement {
&& cx.was_top_layer(&event.position, stacking_order)
{
let point = position_map.point_for_position(text_bounds, event.position);
- let could_be_inlay = point.as_valid().is_none();
- let split = event.modifiers.alt;
- if event.modifiers.shift || could_be_inlay {
- go_to_fetched_type_definition(editor, point, split, cx);
- } else {
- go_to_fetched_definition(editor, point, split, cx);
- }
+ editor.handle_click_hovered_link(point, event.modifiers, cx);
cx.stop_propagation();
} else if end_selection {
@@ -564,31 +555,14 @@ impl EditorElement {
if text_hovered && was_top {
let point_for_position = position_map.point_for_position(text_bounds, event.position);
- match point_for_position.as_valid() {
- Some(point) => {
- update_go_to_definition_link(
- editor,
- Some(GoToDefinitionTrigger::Text(point)),
- modifiers.command,
- modifiers.shift,
- cx,
- );
- hover_at(editor, Some(point), cx);
- Self::update_visible_cursor(editor, point, position_map, cx);
- }
- None => {
- update_inlay_link_and_hover_points(
- &position_map.snapshot,
- point_for_position,
- editor,
- modifiers.command,
- modifiers.shift,
- cx,
- );
- }
+ editor.update_hovered_link(point_for_position, &position_map.snapshot, modifiers, cx);
+
+ if let Some(point) = point_for_position.as_valid() {
+ hover_at(editor, Some(point), cx);
+ Self::update_visible_cursor(editor, point, position_map, cx);
}
} else {
- update_go_to_definition_link(editor, None, modifiers.command, modifiers.shift, cx);
+ editor.hide_hovered_link(cx);
hover_at(editor, None, cx);
if gutter_hovered && was_top {
cx.stop_propagation();
@@ -930,13 +904,13 @@ impl EditorElement {
if self
.editor
.read(cx)
- .link_go_to_definition_state
- .definitions
- .is_empty()
+ .hovered_link_state
+ .as_ref()
+ .is_some_and(|hovered_link_state| !hovered_link_state.links.is_empty())
{
- cx.set_cursor_style(CursorStyle::IBeam);
- } else {
cx.set_cursor_style(CursorStyle::PointingHand);
+ } else {
+ cx.set_cursor_style(CursorStyle::IBeam);
}
}
@@ -3105,9 +3079,9 @@ impl Element for EditorElement {
let key_context = self.editor.read(cx).key_context(cx);
cx.with_key_dispatch(Some(key_context), Some(focus_handle.clone()), |_, cx| {
self.register_actions(cx);
- self.register_key_listeners(cx);
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
+ self.register_key_listeners(cx, text_bounds, &layout);
cx.handle_input(
&focus_handle,
ElementInputHandler::new(bounds, self.editor.clone()),
@@ -3224,16 +3198,6 @@ pub struct PointForPosition {
}
impl PointForPosition {
- #[cfg(test)]
- pub fn valid(valid: DisplayPoint) -> Self {
- Self {
- previous_valid: valid,
- next_valid: valid,
- exact_unclipped: valid,
- column_overshoot_after_line_end: 0,
- }
- }
-
pub fn as_valid(&self) -> Option<DisplayPoint> {
if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped {
Some(self.previous_valid)
@@ -1,12 +1,11 @@
use crate::{
- display_map::DisplaySnapshot,
element::PointForPosition,
hover_popover::{self, InlayHover},
- Anchor, DisplayPoint, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, InlayId,
- SelectPhase,
+ Anchor, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, InlayId, SelectPhase,
};
-use gpui::{px, Task, ViewContext};
+use gpui::{px, AsyncWindowContext, Model, Modifiers, Task, ViewContext};
use language::{Bias, ToOffset};
+use linkify::{LinkFinder, LinkKind};
use lsp::LanguageServerId;
use project::{
HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink,
@@ -16,12 +15,12 @@ use std::ops::Range;
use theme::ActiveTheme as _;
use util::TryFutureExt;
-#[derive(Debug, Default)]
-pub struct LinkGoToDefinitionState {
- pub last_trigger_point: Option<TriggerPoint>,
+#[derive(Debug)]
+pub struct HoveredLinkState {
+ pub last_trigger_point: TriggerPoint,
+ pub preferred_kind: LinkDefinitionKind,
pub symbol_range: Option<RangeInEditor>,
- pub kind: Option<LinkDefinitionKind>,
- pub definitions: Vec<GoToDefinitionLink>,
+ pub links: Vec<HoverLink>,
pub task: Option<Task<Option<()>>>,
}
@@ -56,14 +55,9 @@ impl RangeInEditor {
}
}
-#[derive(Debug)]
-pub enum GoToDefinitionTrigger {
- Text(DisplayPoint),
- InlayHint(InlayHighlight, lsp::Location, LanguageServerId),
-}
-
#[derive(Debug, Clone)]
-pub enum GoToDefinitionLink {
+pub enum HoverLink {
+ Url(String),
Text(LocationLink),
InlayHint(lsp::Location, LanguageServerId),
}
@@ -75,26 +69,13 @@ pub(crate) struct InlayHighlight {
pub range: Range<usize>,
}
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
pub enum TriggerPoint {
Text(Anchor),
InlayHint(InlayHighlight, lsp::Location, LanguageServerId),
}
impl TriggerPoint {
- pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind {
- match self {
- TriggerPoint::Text(_) => {
- if shift {
- LinkDefinitionKind::Type
- } else {
- LinkDefinitionKind::Symbol
- }
- }
- TriggerPoint::InlayHint(_, _, _) => LinkDefinitionKind::Type,
- }
- }
-
fn anchor(&self) -> &Anchor {
match self {
TriggerPoint::Text(anchor) => anchor,
@@ -103,69 +84,88 @@ impl TriggerPoint {
}
}
-pub fn update_go_to_definition_link(
- editor: &mut Editor,
- origin: Option<GoToDefinitionTrigger>,
- cmd_held: bool,
- shift_held: bool,
- cx: &mut ViewContext<Editor>,
-) {
- let pending_nonempty_selection = editor.has_pending_nonempty_selection();
-
- // Store new mouse point as an anchor
- let snapshot = editor.snapshot(cx);
- let trigger_point = match origin {
- Some(GoToDefinitionTrigger::Text(p)) => {
- Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before(
- p.to_offset(&snapshot.display_snapshot, Bias::Left),
- )))
- }
- Some(GoToDefinitionTrigger::InlayHint(p, lsp_location, language_server_id)) => {
- Some(TriggerPoint::InlayHint(p, lsp_location, language_server_id))
+impl Editor {
+ pub(crate) fn update_hovered_link(
+ &mut self,
+ point_for_position: PointForPosition,
+ snapshot: &EditorSnapshot,
+ modifiers: Modifiers,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if !modifiers.command || self.has_pending_selection() {
+ self.hide_hovered_link(cx);
+ return;
}
- None => None,
- };
- // If the new point is the same as the previously stored one, return early
- if let (Some(a), Some(b)) = (
- &trigger_point,
- &editor.link_go_to_definition_state.last_trigger_point,
- ) {
- match (a, b) {
- (TriggerPoint::Text(anchor_a), TriggerPoint::Text(anchor_b)) => {
- if anchor_a.cmp(anchor_b, &snapshot.buffer_snapshot).is_eq() {
- return;
- }
+ match point_for_position.as_valid() {
+ Some(point) => {
+ let trigger_point = TriggerPoint::Text(
+ snapshot
+ .buffer_snapshot
+ .anchor_before(point.to_offset(&snapshot.display_snapshot, Bias::Left)),
+ );
+
+ show_link_definition(modifiers.shift, self, trigger_point, snapshot, cx);
}
- (TriggerPoint::InlayHint(range_a, _, _), TriggerPoint::InlayHint(range_b, _, _)) => {
- if range_a == range_b {
- return;
- }
+ None => {
+ update_inlay_link_and_hover_points(
+ &snapshot,
+ point_for_position,
+ self,
+ modifiers.command,
+ modifiers.shift,
+ cx,
+ );
}
- _ => {}
}
}
- editor.link_go_to_definition_state.last_trigger_point = trigger_point.clone();
-
- if pending_nonempty_selection {
- hide_link_definition(editor, cx);
- return;
+ pub(crate) fn hide_hovered_link(&mut self, cx: &mut ViewContext<Self>) {
+ self.hovered_link_state.take();
+ self.clear_highlights::<HoveredLinkState>(cx);
}
- if cmd_held {
- if let Some(trigger_point) = trigger_point {
- let kind = trigger_point.definition_kind(shift_held);
- show_link_definition(kind, editor, trigger_point, snapshot, cx);
- return;
+ pub(crate) fn handle_click_hovered_link(
+ &mut self,
+ point: PointForPosition,
+ modifiers: Modifiers,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ if let Some(hovered_link_state) = self.hovered_link_state.take() {
+ self.hide_hovered_link(cx);
+ if !hovered_link_state.links.is_empty() {
+ if !self.focus_handle.is_focused(cx) {
+ cx.focus(&self.focus_handle);
+ }
+
+ self.navigate_to_hover_links(hovered_link_state.links, modifiers.alt, cx);
+ return;
+ }
}
- }
- hide_link_definition(editor, cx);
+ // We don't have the correct kind of link cached, set the selection on
+ // click and immediately trigger GoToDefinition.
+ self.select(
+ SelectPhase::Begin {
+ position: point.next_valid,
+ add: false,
+ click_count: 1,
+ },
+ cx,
+ );
+
+ if point.as_valid().is_some() {
+ if modifiers.shift {
+ self.go_to_type_definition(&GoToTypeDefinition, cx)
+ } else {
+ self.go_to_definition(&GoToDefinition, cx)
+ }
+ }
+ }
}
pub fn update_inlay_link_and_hover_points(
- snapshot: &DisplaySnapshot,
+ snapshot: &EditorSnapshot,
point_for_position: PointForPosition,
editor: &mut Editor,
cmd_held: bool,
@@ -306,18 +306,20 @@ pub fn update_inlay_link_and_hover_points(
if let Some((language_server_id, location)) =
hovered_hint_part.location
{
- go_to_definition_updated = true;
- update_go_to_definition_link(
- editor,
- Some(GoToDefinitionTrigger::InlayHint(
- highlight,
- location,
- language_server_id,
- )),
- cmd_held,
- shift_held,
- cx,
- );
+ if cmd_held && !editor.has_pending_nonempty_selection() {
+ go_to_definition_updated = true;
+ show_link_definition(
+ shift_held,
+ editor,
+ TriggerPoint::InlayHint(
+ highlight,
+ location,
+ language_server_id,
+ ),
+ snapshot,
+ cx,
+ );
+ }
}
}
}
@@ -330,7 +332,7 @@ pub fn update_inlay_link_and_hover_points(
}
if !go_to_definition_updated {
- update_go_to_definition_link(editor, None, cmd_held, shift_held, cx);
+ editor.hide_hovered_link(cx)
}
if !hover_updated {
hover_popover::hover_at(editor, None, cx);
@@ -344,113 +346,149 @@ pub enum LinkDefinitionKind {
}
pub fn show_link_definition(
- definition_kind: LinkDefinitionKind,
+ shift_held: bool,
editor: &mut Editor,
trigger_point: TriggerPoint,
- snapshot: EditorSnapshot,
+ 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);
- }
+ let preferred_kind = match trigger_point {
+ TriggerPoint::Text(_) if !shift_held => LinkDefinitionKind::Symbol,
+ _ => LinkDefinitionKind::Type,
+ };
+
+ let (mut hovered_link_state, is_cached) =
+ if let Some(existing) = editor.hovered_link_state.take() {
+ (existing, true)
+ } else {
+ (
+ HoveredLinkState {
+ last_trigger_point: trigger_point.clone(),
+ symbol_range: None,
+ preferred_kind,
+ links: vec![],
+ task: None,
+ },
+ false,
+ )
+ };
if editor.pending_rename.is_some() {
return;
}
let trigger_anchor = trigger_point.anchor();
- let (buffer, buffer_position) = if let Some(output) = editor
+ let Some((buffer, buffer_position)) = editor
.buffer
.read(cx)
.text_anchor_for_position(trigger_anchor.clone(), cx)
- {
- output
- } else {
+ else {
return;
};
- let excerpt_id = if let Some((excerpt_id, _, _)) = editor
+ let Some((excerpt_id, _, _)) = editor
.buffer()
.read(cx)
.excerpt_containing(trigger_anchor.clone(), cx)
- {
- excerpt_id
- } else {
+ else {
return;
};
- let project = if let Some(project) = editor.project.clone() {
- project
- } else {
+ let Some(project) = editor.project.clone() else {
return;
};
- // 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 same_kind && symbol_range.point_within_range(&trigger_point, &snapshot) {
+ let same_kind = hovered_link_state.preferred_kind == preferred_kind
+ || hovered_link_state
+ .links
+ .first()
+ .is_some_and(|d| matches!(d, HoverLink::Url(_)));
+
+ if same_kind {
+ if is_cached && (&hovered_link_state.last_trigger_point == &trigger_point)
+ || hovered_link_state
+ .symbol_range
+ .as_ref()
+ .is_some_and(|symbol_range| {
+ symbol_range.point_within_range(&trigger_point, &snapshot)
+ })
+ {
+ editor.hovered_link_state = Some(hovered_link_state);
return;
}
+ } else {
+ editor.hide_hovered_link(cx)
}
- let task = cx.spawn(|this, mut cx| {
+ let snapshot = snapshot.buffer_snapshot.clone();
+ hovered_link_state.task = Some(cx.spawn(|this, mut cx| {
async move {
let result = match &trigger_point {
TriggerPoint::Text(_) => {
- // query the LSP for definition info
- project
- .update(&mut cx, |project, cx| match definition_kind {
- LinkDefinitionKind::Symbol => {
- project.definition(&buffer, buffer_position, cx)
- }
-
- LinkDefinitionKind::Type => {
- project.type_definition(&buffer, buffer_position, cx)
- }
- })?
- .await
- .ok()
- .map(|definition_result| {
+ if let Some((url_range, url)) = find_url(&buffer, buffer_position, cx.clone()) {
+ this.update(&mut cx, |_, _| {
+ let start =
+ snapshot.anchor_in_excerpt(excerpt_id.clone(), url_range.start);
+ let end = snapshot.anchor_in_excerpt(excerpt_id.clone(), url_range.end);
(
- definition_result.iter().find_map(|link| {
- link.origin.as_ref().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,
- );
- RangeInEditor::Text(start..end)
- })
- }),
- definition_result
- .into_iter()
- .map(GoToDefinitionLink::Text)
- .collect(),
+ Some(RangeInEditor::Text(start..end)),
+ vec![HoverLink::Url(url)],
)
})
+ .ok()
+ } else {
+ // query the LSP for definition info
+ project
+ .update(&mut cx, |project, cx| match preferred_kind {
+ LinkDefinitionKind::Symbol => {
+ project.definition(&buffer, buffer_position, cx)
+ }
+
+ LinkDefinitionKind::Type => {
+ project.type_definition(&buffer, buffer_position, cx)
+ }
+ })?
+ .await
+ .ok()
+ .map(|definition_result| {
+ (
+ definition_result.iter().find_map(|link| {
+ link.origin.as_ref().map(|origin| {
+ let start = snapshot.anchor_in_excerpt(
+ excerpt_id.clone(),
+ origin.range.start,
+ );
+ let end = snapshot.anchor_in_excerpt(
+ excerpt_id.clone(),
+ origin.range.end,
+ );
+ RangeInEditor::Text(start..end)
+ })
+ }),
+ definition_result.into_iter().map(HoverLink::Text).collect(),
+ )
+ })
+ }
}
TriggerPoint::InlayHint(highlight, lsp_location, server_id) => Some((
Some(RangeInEditor::Inlay(highlight.clone())),
- vec![GoToDefinitionLink::InlayHint(
- lsp_location.clone(),
- *server_id,
- )],
+ vec![HoverLink::InlayHint(lsp_location.clone(), *server_id)],
)),
};
this.update(&mut cx, |this, cx| {
// Clear any existing highlights
- this.clear_highlights::<LinkGoToDefinitionState>(cx);
- this.link_go_to_definition_state.kind = Some(definition_kind);
- this.link_go_to_definition_state.symbol_range = result
+ this.clear_highlights::<HoveredLinkState>(cx);
+ let Some(hovered_link_state) = this.hovered_link_state.as_mut() else {
+ return;
+ };
+ hovered_link_state.preferred_kind = preferred_kind;
+ hovered_link_state.symbol_range = result
.as_ref()
.and_then(|(symbol_range, _)| symbol_range.clone());
if let Some((symbol_range, definitions)) = result {
- this.link_go_to_definition_state.definitions = definitions.clone();
+ hovered_link_state.links = definitions.clone();
let buffer_snapshot = buffer.read(cx).snapshot();
@@ -459,7 +497,7 @@ pub fn show_link_definition(
let any_definition_does_not_contain_current_location =
definitions.iter().any(|definition| {
match &definition {
- GoToDefinitionLink::Text(link) => {
+ HoverLink::Text(link) => {
if link.target.buffer == buffer {
let range = &link.target.range;
// Expand range by one character as lsp definition ranges include positions adjacent
@@ -481,7 +519,8 @@ pub fn show_link_definition(
true
}
}
- GoToDefinitionLink::InlayHint(_, _) => true,
+ HoverLink::InlayHint(_, _) => true,
+ HoverLink::Url(_) => true,
}
});
@@ -497,7 +536,6 @@ pub fn show_link_definition(
let highlight_range =
symbol_range.unwrap_or_else(|| match &trigger_point {
TriggerPoint::Text(trigger_anchor) => {
- let snapshot = &snapshot.buffer_snapshot;
// If no symbol range returned from language server, use the surrounding word.
let (offset_range, _) =
snapshot.surrounding_word(*trigger_anchor);
@@ -512,21 +550,14 @@ pub fn show_link_definition(
});
match highlight_range {
- RangeInEditor::Text(text_range) => this
- .highlight_text::<LinkGoToDefinitionState>(
- vec![text_range],
- style,
- cx,
- ),
+ RangeInEditor::Text(text_range) => {
+ this.highlight_text::<HoveredLinkState>(vec![text_range], style, cx)
+ }
RangeInEditor::Inlay(highlight) => this
- .highlight_inlays::<LinkGoToDefinitionState>(
- vec![highlight],
- style,
- cx,
- ),
+ .highlight_inlays::<HoveredLinkState>(vec![highlight], style, cx),
}
} else {
- hide_link_definition(this, cx);
+ this.hide_hovered_link(cx);
}
}
})?;
@@ -534,78 +565,68 @@ pub fn show_link_definition(
Ok::<_, anyhow::Error>(())
}
.log_err()
- });
+ }));
- editor.link_go_to_definition_state.task = Some(task);
+ editor.hovered_link_state = Some(hovered_link_state);
}
-pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
- if editor.link_go_to_definition_state.symbol_range.is_some()
- || !editor.link_go_to_definition_state.definitions.is_empty()
- {
- editor.link_go_to_definition_state.symbol_range.take();
- editor.link_go_to_definition_state.definitions.clear();
- cx.notify();
- }
+fn find_url(
+ buffer: &Model<language::Buffer>,
+ position: text::Anchor,
+ mut cx: AsyncWindowContext,
+) -> Option<(Range<text::Anchor>, String)> {
+ const LIMIT: usize = 2048;
- editor.link_go_to_definition_state.task = None;
-
- editor.clear_highlights::<LinkGoToDefinitionState>(cx);
-}
-
-pub fn go_to_fetched_definition(
- editor: &mut Editor,
- point: PointForPosition,
- split: bool,
- cx: &mut ViewContext<Editor>,
-) {
- go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, editor, point, split, cx);
-}
+ let Ok(snapshot) = buffer.update(&mut cx, |buffer, _| buffer.snapshot()) else {
+ return None;
+ };
-pub fn go_to_fetched_type_definition(
- editor: &mut Editor,
- point: PointForPosition,
- split: bool,
- cx: &mut ViewContext<Editor>,
-) {
- go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, editor, point, split, cx);
-}
+ let offset = position.to_offset(&snapshot);
+ let mut token_start = offset;
+ let mut token_end = offset;
+ let mut found_start = false;
+ let mut found_end = false;
-fn go_to_fetched_definition_of_kind(
- kind: LinkDefinitionKind,
- editor: &mut Editor,
- point: PointForPosition,
- split: bool,
- cx: &mut ViewContext<Editor>,
-) {
- let cached_definitions = editor.link_go_to_definition_state.definitions.clone();
- hide_link_definition(editor, cx);
- let cached_definitions_kind = editor.link_go_to_definition_state.kind;
-
- let is_correct_kind = cached_definitions_kind == Some(kind);
- if !cached_definitions.is_empty() && is_correct_kind {
- if !editor.focus_handle.is_focused(cx) {
- cx.focus(&editor.focus_handle);
+ for ch in snapshot.reversed_chars_at(offset).take(LIMIT) {
+ if ch.is_whitespace() {
+ found_start = true;
+ break;
}
+ token_start -= ch.len_utf8();
+ }
+ if !found_start {
+ return None;
+ }
- editor.navigate_to_definitions(cached_definitions, split, cx);
- } else {
- editor.select(
- SelectPhase::Begin {
- position: point.next_valid,
- add: false,
- click_count: 1,
- },
- cx,
- );
+ for ch in snapshot
+ .chars_at(offset)
+ .take(LIMIT - (offset - token_start))
+ {
+ if ch.is_whitespace() {
+ found_end = true;
+ break;
+ }
+ token_end += ch.len_utf8();
+ }
+ if !found_end {
+ return None;
+ }
- if point.as_valid().is_some() {
- match kind {
- LinkDefinitionKind::Symbol => editor.go_to_definition(&GoToDefinition, cx),
- LinkDefinitionKind::Type => editor.go_to_type_definition(&GoToTypeDefinition, cx),
- }
+ let mut finder = LinkFinder::new();
+ finder.kinds(&[LinkKind::Url]);
+ let input = snapshot
+ .text_for_range(token_start..token_end)
+ .collect::<String>();
+
+ let relative_offset = offset - token_start;
+ for link in finder.links(&input) {
+ if link.start() <= relative_offset && link.end() >= relative_offset {
+ let range = snapshot.anchor_before(token_start + link.start())
+ ..snapshot.anchor_after(token_start + link.end());
+ return Some((range, link.as_str().to_string()));
}
}
+ None
}
#[cfg(test)]
@@ -616,16 +637,18 @@ mod tests {
editor_tests::init_test,
inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
test::editor_lsp_test_context::EditorLspTestContext,
+ DisplayPoint,
};
use futures::StreamExt;
- use gpui::{Modifiers, ModifiersChangedEvent};
+ use gpui::Modifiers;
use indoc::indoc;
use language::language_settings::InlayHintSettings;
use lsp::request::{GotoDefinition, GotoTypeDefinition};
use util::assert_set_eq;
+ use workspace::item::Item;
#[gpui::test]
- async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
+ async fn test_hover_type_links(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
@@ -642,12 +665,9 @@ mod tests {
struct A;
let vˇariable = A;
"});
+ let screen_coord = cx.editor(|editor, cx| editor.pixel_position_of_cursor(cx));
// 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;
@@ -657,6 +677,8 @@ mod tests {
let variable = A;
"});
+ cx.run_until_parked();
+
let mut requests =
cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
@@ -669,70 +691,28 @@ mod tests {
])))
});
- // Press cmd+shift to trigger highlight
- cx.update_editor(|editor, cx| {
- update_go_to_definition_link(
- editor,
- Some(GoToDefinitionTrigger::Text(hover_point)),
- true,
- true,
- cx,
- );
- });
+ cx.cx
+ .cx
+ .simulate_mouse_move(screen_coord.unwrap(), Modifiers::command_shift());
+
requests.next().await;
- cx.background_executor.run_until_parked();
- cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ cx.run_until_parked();
+ cx.assert_editor_text_highlights::<HoveredLinkState>(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| {
- crate::element::EditorElement::modifiers_changed(
- editor,
- &ModifiersChangedEvent {
- modifiers: Modifiers {
- command: true,
- ..Default::default()
- },
- ..Default::default()
- },
- cx,
- );
- });
+ cx.simulate_modifiers_change(Modifiers::command());
+ cx.run_until_parked();
// 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! {"
+ cx.assert_editor_text_highlights::<HoveredLinkState>(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_editor(|editor, cx| {
- go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx);
- });
- requests.next().await;
- cx.background_executor.run_until_parked();
+ cx.cx
+ .cx
+ .simulate_click(screen_coord.unwrap(), Modifiers::command_shift());
cx.assert_editor_state(indoc! {"
struct «Aˇ»;
@@ -741,7 +721,7 @@ mod tests {
}
#[gpui::test]
- async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
+ async fn test_hover_links(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
@@ -759,7 +739,7 @@ mod tests {
"});
// Basic hold cmd, expect highlight in region if response contains definition
- let hover_point = cx.display_point(indoc! {"
+ let hover_point = cx.pixel_position(indoc! {"
fn test() { do_wˇork(); }
fn do_work() { test(); }
"});
@@ -783,65 +763,42 @@ mod tests {
])))
});
- cx.update_editor(|editor, cx| {
- update_go_to_definition_link(
- editor,
- Some(GoToDefinitionTrigger::Text(hover_point)),
- true,
- false,
- cx,
- );
- });
+ cx.simulate_mouse_move(hover_point, Modifiers::command());
requests.next().await;
cx.background_executor.run_until_parked();
- cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
fn test() { «do_work»(); }
fn do_work() { test(); }
"});
// Unpress cmd causes highlight to go away
- cx.update_editor(|editor, cx| {
- crate::element::EditorElement::modifiers_changed(editor, &Default::default(), cx);
- });
-
- // Assert no link highlights
- cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ cx.simulate_modifiers_change(Modifiers::none());
+ cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
fn test() { do_work(); }
fn do_work() { test(); }
"});
- // Response without source range still highlights word
- cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_trigger_point = None);
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,
+ 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,
- Some(GoToDefinitionTrigger::Text(hover_point)),
- true,
- false,
- cx,
- );
- });
+
+ cx.simulate_mouse_move(hover_point, Modifiers::command());
requests.next().await;
cx.background_executor.run_until_parked();
-
- cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
fn test() { «do_work»(); }
fn do_work() { test(); }
"});
// Moving mouse to location with no response dismisses highlight
- let hover_point = cx.display_point(indoc! {"
+ let hover_point = cx.pixel_position(indoc! {"
fˇn test() { do_work(); }
fn do_work() { test(); }
"});
@@ -851,42 +808,26 @@ mod tests {
// No definitions returned
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
});
- cx.update_editor(|editor, cx| {
- update_go_to_definition_link(
- editor,
- Some(GoToDefinitionTrigger::Text(hover_point)),
- true,
- false,
- cx,
- );
- });
+ cx.simulate_mouse_move(hover_point, Modifiers::command());
+
requests.next().await;
cx.background_executor.run_until_parked();
// Assert no link highlights
- cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
fn test() { do_work(); }
fn do_work() { test(); }
"});
- // Move mouse without cmd and then pressing cmd triggers highlight
- let hover_point = cx.display_point(indoc! {"
+ // // Move mouse without cmd and then pressing cmd triggers highlight
+ let hover_point = cx.pixel_position(indoc! {"
fn test() { do_work(); }
fn do_work() { teˇst(); }
"});
- cx.update_editor(|editor, cx| {
- update_go_to_definition_link(
- editor,
- Some(GoToDefinitionTrigger::Text(hover_point)),
- false,
- false,
- cx,
- );
- });
- cx.background_executor.run_until_parked();
+ cx.simulate_mouse_move(hover_point, Modifiers::none());
// Assert no link highlights
- cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
fn test() { do_work(); }
fn do_work() { test(); }
"});
@@ -910,73 +851,44 @@ mod tests {
},
])))
});
- cx.update_editor(|editor, cx| {
- crate::element::EditorElement::modifiers_changed(
- editor,
- &ModifiersChangedEvent {
- modifiers: Modifiers {
- command: true,
- ..Default::default()
- },
- },
- cx,
- );
- });
+
+ cx.simulate_modifiers_change(Modifiers::command());
+
requests.next().await;
cx.background_executor.run_until_parked();
- cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
fn test() { do_work(); }
fn do_work() { «test»(); }
"});
- cx.cx.cx.deactivate_window();
- cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ cx.deactivate_window();
+ cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
fn test() { do_work(); }
fn do_work() { test(); }
"});
- // Moving the mouse restores the highlights.
- cx.update_editor(|editor, cx| {
- update_go_to_definition_link(
- editor,
- Some(GoToDefinitionTrigger::Text(hover_point)),
- true,
- false,
- cx,
- );
- });
+ cx.simulate_mouse_move(hover_point, Modifiers::command());
cx.background_executor.run_until_parked();
- cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
fn test() { do_work(); }
fn do_work() { «test»(); }
"});
// Moving again within the same symbol range doesn't re-request
- let hover_point = cx.display_point(indoc! {"
+ let hover_point = cx.pixel_position(indoc! {"
fn test() { do_work(); }
fn do_work() { tesˇt(); }
"});
- cx.update_editor(|editor, cx| {
- update_go_to_definition_link(
- editor,
- Some(GoToDefinitionTrigger::Text(hover_point)),
- true,
- false,
- cx,
- );
- });
+ cx.simulate_mouse_move(hover_point, Modifiers::command());
cx.background_executor.run_until_parked();
- cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
fn test() { do_work(); }
fn do_work() { «test»(); }
"});
// Cmd click with existing definition doesn't re-request and dismisses highlight
- cx.update_editor(|editor, cx| {
- go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx);
- });
- // Assert selection moved to to definition
+ cx.simulate_click(hover_point, Modifiers::command());
cx.lsp
.handle_request::<GotoDefinition, _, _>(move |_, _| async move {
// Empty definition response to make sure we aren't hitting the lsp and using
@@ -1,6 +1,6 @@
use crate::{
display_map::{InlayOffset, ToDisplayPoint},
- link_go_to_definition::{InlayHighlight, RangeInEditor},
+ hover_links::{InlayHighlight, RangeInEditor},
Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
ExcerptId, Hover, RangeToAnchorExt,
};
@@ -605,8 +605,8 @@ mod tests {
use crate::{
editor_tests::init_test,
element::PointForPosition,
+ hover_links::update_inlay_link_and_hover_points,
inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
- link_go_to_definition::update_inlay_link_and_hover_points,
test::editor_lsp_test_context::EditorLspTestContext,
InlayId,
};
@@ -1,7 +1,7 @@
use crate::{
- editor_settings::SeedQuerySetting, link_go_to_definition::hide_link_definition,
- persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, EditorEvent, EditorSettings,
- ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
+ editor_settings::SeedQuerySetting, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll,
+ Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot,
+ NavigationData, ToPoint as _,
};
use anyhow::{anyhow, Context as _, Result};
use collections::HashSet;
@@ -682,8 +682,7 @@ impl Item for Editor {
}
fn workspace_deactivated(&mut self, cx: &mut ViewContext<Self>) {
- hide_link_definition(self, cx);
- self.link_go_to_definition_state.last_trigger_point = None;
+ self.hide_hovered_link(cx);
}
fn is_dirty(&self, cx: &AppContext) -> bool {
@@ -4,7 +4,8 @@ use crate::{
use collections::BTreeMap;
use futures::Future;
use gpui::{
- AnyWindowHandle, AppContext, Keystroke, ModelContext, View, ViewContext, VisualTestContext,
+ AnyWindowHandle, AppContext, Keystroke, ModelContext, Pixels, Point, View, ViewContext,
+ VisualTestContext,
};
use indoc::indoc;
use itertools::Itertools;
@@ -187,6 +188,31 @@ impl EditorTestContext {
ranges[0].start.to_display_point(&snapshot)
}
+ pub fn pixel_position(&mut self, marked_text: &str) -> Point<Pixels> {
+ let display_point = self.display_point(marked_text);
+ self.pixel_position_for(display_point)
+ }
+
+ pub fn pixel_position_for(&mut self, display_point: DisplayPoint) -> Point<Pixels> {
+ self.update_editor(|editor, cx| {
+ let newest_point = editor.selections.newest_display(cx).head();
+ let pixel_position = editor.pixel_position_of_newest_cursor.unwrap();
+ let line_height = editor
+ .style()
+ .unwrap()
+ .text
+ .line_height_in_pixels(cx.rem_size());
+ let snapshot = editor.snapshot(cx);
+ let details = editor.text_layout_details(cx);
+
+ let y = pixel_position.y
+ + line_height * (display_point.row() as f32 - newest_point.row() as f32);
+ let x = pixel_position.x + snapshot.x_for_display_point(display_point, &details)
+ - snapshot.x_for_display_point(newest_point, &details);
+ Point::new(x, y)
+ })
+ }
+
// Returns anchors for the current buffer using `«` and `»`
pub fn text_anchor_range(&mut self, marked_text: &str) -> Range<language::Anchor> {
let ranges = self.ranges(marked_text);
@@ -343,7 +369,7 @@ impl EditorTestContext {
}
impl Deref for EditorTestContext {
- type Target = gpui::TestAppContext;
+ type Target = gpui::VisualTestContext;
fn deref(&self) -> &Self::Target {
&self.cx
@@ -1,9 +1,10 @@
use crate::{
Action, AnyElement, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem, Context, Entity, EventEmitter,
- ForegroundExecutor, Global, InputEvent, Keystroke, Model, ModelContext, Pixels, Platform,
- Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow, TextSystem, View,
- ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
+ ForegroundExecutor, Global, InputEvent, Keystroke, Model, ModelContext, Modifiers,
+ ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
+ Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow,
+ TextSystem, View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
};
use anyhow::{anyhow, bail};
use futures::{Stream, StreamExt};
@@ -236,6 +237,11 @@ impl TestAppContext {
self.test_platform.has_pending_prompt()
}
+ /// All the urls that have been opened with cx.open_url() during this test.
+ pub fn opened_url(&self) -> Option<String> {
+ self.test_platform.opened_url.borrow().clone()
+ }
+
/// Simulates the user resizing the window to the new size.
pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size<Pixels>) {
self.test_window(window_handle).simulate_resize(size);
@@ -625,6 +631,36 @@ impl<'a> VisualTestContext {
self.cx.simulate_input(self.window, input)
}
+ /// Simulate a mouse move event to the given point
+ pub fn simulate_mouse_move(&mut self, position: Point<Pixels>, modifiers: Modifiers) {
+ self.simulate_event(MouseMoveEvent {
+ position,
+ modifiers,
+ pressed_button: None,
+ })
+ }
+
+ /// Simulate a primary mouse click at the given point
+ pub fn simulate_click(&mut self, position: Point<Pixels>, modifiers: Modifiers) {
+ self.simulate_event(MouseDownEvent {
+ position,
+ modifiers,
+ button: MouseButton::Left,
+ click_count: 1,
+ });
+ self.simulate_event(MouseUpEvent {
+ position,
+ modifiers,
+ button: MouseButton::Left,
+ click_count: 1,
+ });
+ }
+
+ /// Simulate a modifiers changed event
+ pub fn simulate_modifiers_change(&mut self, modifiers: Modifiers) {
+ self.simulate_event(ModifiersChangedEvent { modifiers })
+ }
+
/// Simulates the user resizing the window to the new size.
pub fn simulate_resize(&self, size: Size<Pixels>) {
self.simulate_window_resize(self.window, size)
@@ -170,4 +170,34 @@ impl Modifiers {
pub fn modified(&self) -> bool {
self.control || self.alt || self.shift || self.command || self.function
}
+
+ /// helper method for Modifiers with no modifiers
+ pub fn none() -> Modifiers {
+ Default::default()
+ }
+
+ /// helper method for Modifiers with just command
+ pub fn command() -> Modifiers {
+ Modifiers {
+ command: true,
+ ..Default::default()
+ }
+ }
+
+ /// helper method for Modifiers with just shift
+ pub fn shift() -> Modifiers {
+ Modifiers {
+ shift: true,
+ ..Default::default()
+ }
+ }
+
+ /// helper method for Modifiers with command + shift
+ pub fn command_shift() -> Modifiers {
+ Modifiers {
+ shift: true,
+ command: true,
+ ..Default::default()
+ }
+ }
}
@@ -25,6 +25,7 @@ pub(crate) struct TestPlatform {
active_cursor: Mutex<CursorStyle>,
current_clipboard_item: Mutex<Option<ClipboardItem>>,
pub(crate) prompts: RefCell<TestPrompts>,
+ pub opened_url: RefCell<Option<String>>,
weak: Weak<Self>,
}
@@ -45,6 +46,7 @@ impl TestPlatform {
active_window: Default::default(),
current_clipboard_item: Mutex::new(None),
weak: weak.clone(),
+ opened_url: Default::default(),
})
}
@@ -188,8 +190,8 @@ impl Platform for TestPlatform {
fn stop_display_link(&self, _display_id: DisplayId) {}
- fn open_url(&self, _url: &str) {
- unimplemented!()
+ fn open_url(&self, url: &str) {
+ *self.opened_url.borrow_mut() = Some(url.to_string())
}
fn on_open_urls(&self, _callback: Box<dyn FnMut(Vec<String>)>) {