From d34491e822e6344d8dc186a7a937aac7be4957ca Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 16 Aug 2023 14:21:26 +0300 Subject: [PATCH 01/24] Draft inlay hint part hover detection --- crates/editor/src/display_map.rs | 19 +++- crates/editor/src/element.rs | 122 +++++++++++++++++++++----- crates/editor/src/inlay_hint_cache.rs | 11 +++ 3 files changed, 125 insertions(+), 27 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index aee41e6c53..9159253142 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -27,7 +27,7 @@ pub use block_map::{ BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock, }; -pub use self::inlay_map::Inlay; +pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum FoldStatus { @@ -387,12 +387,25 @@ impl DisplaySnapshot { } fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point { + self.inlay_snapshot + .to_buffer_point(self.display_point_to_inlay_point(point, bias)) + } + + pub fn display_point_to_inlay_offset(&self, point: DisplayPoint, bias: Bias) -> InlayOffset { + self.inlay_snapshot + .to_offset(self.display_point_to_inlay_point(point, bias)) + } + + pub fn inlay_point_to_inlay_offset(&self, point: InlayPoint) -> InlayOffset { + self.inlay_snapshot.to_offset(point) + } + + fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint { let block_point = point.0; let wrap_point = self.block_snapshot.to_wrap_point(block_point); let tab_point = self.wrap_snapshot.to_tab_point(wrap_point); let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0; - let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot); - self.inlay_snapshot.to_buffer_point(inlay_point) + fold_point.to_inlay_point(&self.fold_snapshot) } pub fn max_point(&self) -> DisplayPoint { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 9f74eed790..e2aaa3a3bb 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4,7 +4,7 @@ use super::{ MAX_LINE_LEN, }; use crate::{ - display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock}, + display_map::{BlockStyle, DisplaySnapshot, FoldStatus, InlayPoint, TransformBlock}, editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::{ @@ -42,7 +42,7 @@ use language::{ }; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, - ProjectPath, + InlayHintLabelPart, ProjectPath, }; use smallvec::SmallVec; use std::{ @@ -455,11 +455,67 @@ impl EditorElement { ) -> bool { // 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 = position_to_display_point(position, text_bounds, position_map); - - update_go_to_definition_link(editor, point, cmd, shift, cx); - hover_at(editor, point, cx); + if text_bounds.contains_point(position) { + let (nearest_valid_position, unclipped_position) = + position_map.point_for_position(text_bounds, position); + if nearest_valid_position == unclipped_position { + update_go_to_definition_link(editor, Some(nearest_valid_position), cmd, shift, cx); + hover_at(editor, Some(nearest_valid_position), cx); + return true; + } else { + let buffer = editor.buffer().read(cx); + let snapshot = buffer.snapshot(cx); + let previous_valid_position = position_map + .snapshot + .clip_point(unclipped_position, Bias::Left) + .to_point(&position_map.snapshot.display_snapshot); + let previous_valid_anchor = snapshot.anchor_at(previous_valid_position, Bias::Left); + let next_valid_position = position_map + .snapshot + .clip_point(unclipped_position, Bias::Right) + .to_point(&position_map.snapshot.display_snapshot); + let next_valid_anchor = snapshot.anchor_at(next_valid_position, Bias::Right); + if let Some(hovered_hint) = editor + .visible_inlay_hints(cx) + .into_iter() + .skip_while(|hint| hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt()) + .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) + .max_by_key(|hint| hint.id) + { + if let Some(cached_hint) = editor + .inlay_hint_cache() + .hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) + { + match &cached_hint.label { + project::InlayHintLabel::String(regular_label) => { + // TODO kb remove + check for tooltip for hover and resolve, if needed + eprintln!("regular string: {regular_label}"); + } + project::InlayHintLabel::LabelParts(label_parts) => { + // TODO kb how to properly convert it? + let unclipped_inlay_position = InlayPoint::new( + unclipped_position.row(), + unclipped_position.column(), + ); + if let Some(hovered_hint_part) = find_hovered_hint_part( + &position_map.snapshot, + &label_parts, + previous_valid_position, + next_valid_position, + unclipped_inlay_position, + ) { + // TODO kb remove + check for tooltip and location and resolve, if needed + eprintln!("hint_part: {hovered_hint_part:?}"); + } + } + }; + } + } + } + }; + update_go_to_definition_link(editor, None, cmd, shift, cx); + hover_at(editor, None, cx); true } @@ -909,7 +965,7 @@ impl EditorElement { &text, cursor_row_layout.font_size(), &[( - text.len(), + text.chars().count(), RunStyle { font_id, color: style.background, @@ -1804,6 +1860,40 @@ impl EditorElement { } } +fn find_hovered_hint_part<'a>( + snapshot: &EditorSnapshot, + label_parts: &'a [InlayHintLabelPart], + hint_start: Point, + hint_end: Point, + hovered_position: InlayPoint, +) -> Option<&'a InlayHintLabelPart> { + let hint_start_offset = + snapshot.display_point_to_inlay_offset(hint_start.to_display_point(&snapshot), Bias::Left); + let hint_end_offset = + snapshot.display_point_to_inlay_offset(hint_end.to_display_point(&snapshot), Bias::Right); + dbg!(( + "~~~~~~~~~", + hint_start, + hint_start_offset, + hint_end, + hint_end_offset, + hovered_position + )); + let hovered_offset = snapshot.inlay_point_to_inlay_offset(hovered_position); + if hovered_offset >= hint_start_offset && hovered_offset <= hint_end_offset { + let mut hovered_character = (hovered_offset - hint_start_offset).0; + for part in label_parts { + let part_len = part.value.chars().count(); + if hovered_character >= part_len { + hovered_character -= part_len; + } else { + return Some(part); + } + } + } + None +} + struct HighlightedChunk<'a> { chunk: &'a str, style: Option, @@ -2663,6 +2753,7 @@ impl PositionMap { let mut target_point = DisplayPoint::new(row, column); let point = self.snapshot.clip_point(target_point, Bias::Left); + // TODO kb looks wrong, need to construct inlay point instead? operate offsets? *target_point.column_mut() += (x_overshoot / self.em_advance) as u32; (point, target_point) @@ -2919,23 +3010,6 @@ impl HighlightedRange { } } -fn position_to_display_point( - position: Vector2F, - text_bounds: RectF, - position_map: &PositionMap, -) -> Option { - if text_bounds.contains_point(position) { - let (point, target_point) = position_map.point_for_position(text_bounds, position); - if point == target_point { - Some(point) - } else { - None - } - } else { - None - } -} - fn range_to_bounds( range: &Range, content_origin: Vector2F, diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 70cccf21da..5b9bdd08ec 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -386,6 +386,17 @@ impl InlayHintCache { self.hints.clear(); } + pub fn hint_by_id(&self, excerpt_id: ExcerptId, hint_id: InlayId) -> Option { + self.hints + .get(&excerpt_id)? + .read() + .hints + .iter() + .find(|&(id, _)| id == &hint_id) + .map(|(_, hint)| hint) + .cloned() + } + pub fn hints(&self) -> Vec { let mut hints = Vec::new(); for excerpt_hints in self.hints.values() { From d1cb0b3c27fcbe1b02df3e4eae5426fb15802622 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Aug 2023 11:56:38 +0300 Subject: [PATCH 02/24] Properly detect hovered inlay hint label part --- crates/editor/src/element.rs | 219 +++++++++++++++++++---------------- 1 file changed, 118 insertions(+), 101 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e2aaa3a3bb..5738419551 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4,7 +4,7 @@ use super::{ MAX_LINE_LEN, }; use crate::{ - display_map::{BlockStyle, DisplaySnapshot, FoldStatus, InlayPoint, TransformBlock}, + display_map::{BlockStyle, DisplaySnapshot, FoldStatus, InlayOffset, TransformBlock}, editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::{ @@ -287,13 +287,13 @@ impl EditorElement { return false; } - let (position, target_position) = position_map.point_for_position(text_bounds, position); - + let point_for_position = position_map.point_for_position(text_bounds, position); + let position = point_for_position.previous_valid; if shift && alt { editor.select( SelectPhase::BeginColumnar { position, - goal_column: target_position.column(), + goal_column: point_for_position.exact_unclipped.column(), }, cx, ); @@ -329,9 +329,13 @@ impl EditorElement { if !text_bounds.contains_point(position) { return false; } - - let (point, _) = position_map.point_for_position(text_bounds, position); - mouse_context_menu::deploy_context_menu(editor, position, point, cx); + let point_for_position = position_map.point_for_position(text_bounds, position); + mouse_context_menu::deploy_context_menu( + editor, + position, + point_for_position.previous_valid, + cx, + ); true } @@ -353,9 +357,10 @@ impl EditorElement { } if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) { - let (point, target_point) = position_map.point_for_position(text_bounds, position); - - if point == target_point { + if let Some(point) = position_map + .point_for_position(text_bounds, position) + .as_valid() + { if shift { go_to_fetched_type_definition(editor, point, alt, cx); } else { @@ -383,12 +388,9 @@ impl EditorElement { // 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 text_bounds.contains_point(position) { - let (point, target_point) = position_map.point_for_position(text_bounds, position); - if point == target_point { - Some(point) - } else { - None - } + position_map + .point_for_position(text_bounds, position) + .as_valid() } else { None }; @@ -422,13 +424,12 @@ impl EditorElement { )) } - let (position, target_position) = - position_map.point_for_position(text_bounds, position); + let point_for_position = position_map.point_for_position(text_bounds, position); editor.select( SelectPhase::Update { - position, - goal_column: target_position.column(), + position: point_for_position.previous_valid, + goal_column: point_for_position.exact_unclipped.column(), scroll_position: (position_map.snapshot.scroll_position() + scroll_delta) .clamp(Vector2F::zero(), position_map.scroll_max), }, @@ -456,59 +457,74 @@ impl EditorElement { // 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 if text_bounds.contains_point(position) { - let (nearest_valid_position, unclipped_position) = - position_map.point_for_position(text_bounds, position); - if nearest_valid_position == unclipped_position { - update_go_to_definition_link(editor, Some(nearest_valid_position), cmd, shift, cx); - hover_at(editor, Some(nearest_valid_position), cx); + let point_for_position = position_map.point_for_position(text_bounds, position); + if let Some(point) = point_for_position.as_valid() { + update_go_to_definition_link(editor, Some(point), cmd, shift, cx); + hover_at(editor, Some(point), cx); return true; } else { - let buffer = editor.buffer().read(cx); - let snapshot = buffer.snapshot(cx); - let previous_valid_position = position_map + let hint_start_offset = position_map .snapshot - .clip_point(unclipped_position, Bias::Left) - .to_point(&position_map.snapshot.display_snapshot); - let previous_valid_anchor = snapshot.anchor_at(previous_valid_position, Bias::Left); - let next_valid_position = position_map + .display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); + let hint_end_offset = position_map .snapshot - .clip_point(unclipped_position, Bias::Right) - .to_point(&position_map.snapshot.display_snapshot); - let next_valid_anchor = snapshot.anchor_at(next_valid_position, Bias::Right); - if let Some(hovered_hint) = editor - .visible_inlay_hints(cx) - .into_iter() - .skip_while(|hint| hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt()) - .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) - .max_by_key(|hint| hint.id) - { - if let Some(cached_hint) = editor - .inlay_hint_cache() - .hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) + .display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); + let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; + let hovered_offset = if offset_overshoot == 0 { + Some(position_map.snapshot.display_point_to_inlay_offset( + point_for_position.exact_unclipped, + Bias::Left, + )) + } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { + Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) + } else { + None + }; + if let Some(hovered_offset) = hovered_offset { + let buffer = editor.buffer().read(cx); + let snapshot = buffer.snapshot(cx); + let previous_valid_anchor = snapshot.anchor_at( + point_for_position + .previous_valid + .to_point(&position_map.snapshot.display_snapshot), + Bias::Left, + ); + let next_valid_anchor = snapshot.anchor_at( + point_for_position + .next_valid + .to_point(&position_map.snapshot.display_snapshot), + Bias::Right, + ); + if let Some(hovered_hint) = editor + .visible_inlay_hints(cx) + .into_iter() + .skip_while(|hint| { + hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt() + }) + .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) + .max_by_key(|hint| hint.id) { - match &cached_hint.label { - project::InlayHintLabel::String(regular_label) => { - // TODO kb remove + check for tooltip for hover and resolve, if needed - eprintln!("regular string: {regular_label}"); - } - project::InlayHintLabel::LabelParts(label_parts) => { - // TODO kb how to properly convert it? - let unclipped_inlay_position = InlayPoint::new( - unclipped_position.row(), - unclipped_position.column(), - ); - if let Some(hovered_hint_part) = find_hovered_hint_part( - &position_map.snapshot, - &label_parts, - previous_valid_position, - next_valid_position, - unclipped_inlay_position, - ) { - // TODO kb remove + check for tooltip and location and resolve, if needed - eprintln!("hint_part: {hovered_hint_part:?}"); + if let Some(cached_hint) = editor + .inlay_hint_cache() + .hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) + { + match &cached_hint.label { + project::InlayHintLabel::String(regular_label) => { + // TODO kb remove + check for tooltip for hover and resolve, if needed + eprintln!("regular string: {regular_label}"); } - } - }; + project::InlayHintLabel::LabelParts(label_parts) => { + if let Some(hovered_hint_part) = find_hovered_hint_part( + &label_parts, + hint_start_offset..hint_end_offset, + hovered_offset, + ) { + // TODO kb remove + check for tooltip and location and resolve, if needed + eprintln!("hint_part: {hovered_hint_part:?}"); + } + } + }; + } } } } @@ -1861,27 +1877,12 @@ impl EditorElement { } fn find_hovered_hint_part<'a>( - snapshot: &EditorSnapshot, label_parts: &'a [InlayHintLabelPart], - hint_start: Point, - hint_end: Point, - hovered_position: InlayPoint, + hint_range: Range, + hovered_offset: InlayOffset, ) -> Option<&'a InlayHintLabelPart> { - let hint_start_offset = - snapshot.display_point_to_inlay_offset(hint_start.to_display_point(&snapshot), Bias::Left); - let hint_end_offset = - snapshot.display_point_to_inlay_offset(hint_end.to_display_point(&snapshot), Bias::Right); - dbg!(( - "~~~~~~~~~", - hint_start, - hint_start_offset, - hint_end, - hint_end_offset, - hovered_position - )); - let hovered_offset = snapshot.inlay_point_to_inlay_offset(hovered_position); - if hovered_offset >= hint_start_offset && hovered_offset <= hint_end_offset { - let mut hovered_character = (hovered_offset - hint_start_offset).0; + if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { + let mut hovered_character = (hovered_offset - hint_range.start).0; for part in label_parts { let part_len = part.value.chars().count(); if hovered_character >= part_len { @@ -2722,22 +2723,32 @@ struct PositionMap { snapshot: EditorSnapshot, } +#[derive(Debug)] +struct PointForPosition { + previous_valid: DisplayPoint, + next_valid: DisplayPoint, + exact_unclipped: DisplayPoint, + column_overshoot_after_line_end: u32, +} + +impl PointForPosition { + fn as_valid(&self) -> Option { + if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped { + Some(self.previous_valid) + } else { + None + } + } +} + impl PositionMap { - /// Returns two display points: - /// 1. The nearest *valid* position in the editor - /// 2. An unclipped, potentially *invalid* position that maps directly to - /// the given pixel position. - fn point_for_position( - &self, - text_bounds: RectF, - position: Vector2F, - ) -> (DisplayPoint, DisplayPoint) { + fn point_for_position(&self, text_bounds: RectF, position: Vector2F) -> PointForPosition { let scroll_position = self.snapshot.scroll_position(); let position = position - text_bounds.origin(); let y = position.y().max(0.0).min(self.size.y()); let x = position.x() + (scroll_position.x() * self.em_width); let row = (y / self.line_height + scroll_position.y()) as u32; - let (column, x_overshoot) = if let Some(line) = self + let (column, x_overshoot_after_line_end) = if let Some(line) = self .line_layouts .get(row as usize - scroll_position.y() as usize) .map(|line_with_spaces| &line_with_spaces.line) @@ -2751,12 +2762,18 @@ impl PositionMap { (0, x) }; - let mut target_point = DisplayPoint::new(row, column); - let point = self.snapshot.clip_point(target_point, Bias::Left); - // TODO kb looks wrong, need to construct inlay point instead? operate offsets? - *target_point.column_mut() += (x_overshoot / self.em_advance) as u32; + let mut exact_unclipped = DisplayPoint::new(row, column); + let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left); + let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right); - (point, target_point) + let column_overshoot_after_line_end = (x_overshoot_after_line_end / self.em_advance) as u32; + *exact_unclipped.column_mut() += column_overshoot_after_line_end; + PointForPosition { + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end, + } } } From e4b78e322edfea72039d3b2afce0646c4946d9fd Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Aug 2023 12:37:05 +0300 Subject: [PATCH 03/24] Revert "Strip off inlay hints data that should be resolved" Without holding all hints in host's cache, this is impossile. Currenly, we keep hint caches separate and isolated, so this will not work when we actually resolve. --- crates/lsp/src/lsp.rs | 4 +-- crates/project/src/lsp_command.rs | 56 +++++++++++++++++++++++++------ 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index e0ae64d806..78c858a90c 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -434,9 +434,7 @@ impl LanguageServer { ..Default::default() }), inlay_hint: Some(InlayHintClientCapabilities { - resolve_support: Some(InlayHintResolveClientCapabilities { - properties: vec!["textEdits".to_string(), "tooltip".to_string()], - }), + resolve_support: None, dynamic_registration: Some(false), }), ..Default::default() diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index a8692257d8..08261b64f1 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1954,7 +1954,7 @@ impl LspCommand for InlayHints { _: &mut Project, _: PeerId, buffer_version: &clock::Global, - _: &mut AppContext, + cx: &mut AppContext, ) -> proto::InlayHintsResponse { proto::InlayHintsResponse { hints: response @@ -1963,17 +1963,51 @@ impl LspCommand for InlayHints { position: Some(language::proto::serialize_anchor(&response_hint.position)), padding_left: response_hint.padding_left, padding_right: response_hint.padding_right, - kind: response_hint.kind.map(|kind| kind.name().to_string()), - // Do not pass extra data such as tooltips to clients: host can put tooltip data from the cache during resolution. - tooltip: None, - // Similarly, do not pass label parts to clients: host can return a detailed list during resolution. label: Some(proto::InlayHintLabel { - label: Some(proto::inlay_hint_label::Label::Value( - match response_hint.label { - InlayHintLabel::String(s) => s, - InlayHintLabel::LabelParts(_) => response_hint.text(), - }, - )), + label: Some(match response_hint.label { + InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), + InlayHintLabel::LabelParts(label_parts) => { + proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { + parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart { + value: label_part.value, + tooltip: label_part.tooltip.map(|tooltip| { + let proto_tooltip = match tooltip { + InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), + InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { + kind: markup_content.kind, + value: markup_content.value, + }), + }; + proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} + }), + location: label_part.location.map(|location| proto::Location { + start: Some(serialize_anchor(&location.range.start)), + end: Some(serialize_anchor(&location.range.end)), + buffer_id: location.buffer.read(cx).remote_id(), + }), + }).collect() + }) + } + }), + }), + kind: response_hint.kind.map(|kind| kind.name().to_string()), + tooltip: response_hint.tooltip.map(|response_tooltip| { + let proto_tooltip = match response_tooltip { + InlayHintTooltip::String(s) => { + proto::inlay_hint_tooltip::Content::Value(s) + } + InlayHintTooltip::MarkupContent(markup_content) => { + proto::inlay_hint_tooltip::Content::MarkupContent( + proto::MarkupContent { + kind: markup_content.kind, + value: markup_content.value, + }, + ) + } + }; + proto::InlayHintTooltip { + content: Some(proto_tooltip), + } }), }) .collect(), From 3434990b70cf79772d7f3f9d9f47a2b385fc2cb2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Aug 2023 15:54:30 +0300 Subject: [PATCH 04/24] Store inlay hint resolve data --- crates/editor/src/display_map/inlay_map.rs | 6 +- crates/project/src/lsp_command.rs | 149 ++++++++++++++------- crates/project/src/project.rs | 16 +++ crates/rpc/proto/zed.proto | 16 +++ 4 files changed, 134 insertions(+), 53 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 9794ac45c1..3cea513dea 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1109,7 +1109,7 @@ mod tests { use super::*; use crate::{InlayId, MultiBuffer}; use gpui::AppContext; - use project::{InlayHint, InlayHintLabel}; + use project::{InlayHint, InlayHintLabel, ResolveState}; use rand::prelude::*; use settings::SettingsStore; use std::{cmp::Reverse, env, sync::Arc}; @@ -1131,6 +1131,7 @@ mod tests { padding_right: false, tooltip: None, kind: None, + resolve_state: ResolveState::Resolved, }, ) .text @@ -1151,6 +1152,7 @@ mod tests { padding_right: true, tooltip: None, kind: None, + resolve_state: ResolveState::Resolved, }, ) .text @@ -1171,6 +1173,7 @@ mod tests { padding_right: false, tooltip: None, kind: None, + resolve_state: ResolveState::Resolved, }, ) .text @@ -1191,6 +1194,7 @@ mod tests { padding_right: true, tooltip: None, kind: None, + resolve_state: ResolveState::Resolved, }, ) .text diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 08261b64f1..d46ba4f5f7 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,7 +1,7 @@ use crate::{ DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, - MarkupContent, Project, ProjectTransaction, + MarkupContent, Project, ProjectTransaction, ResolveState, }; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; @@ -1817,7 +1817,8 @@ impl LspCommand for InlayHints { server_id: LanguageServerId, mut cx: AsyncAppContext, ) -> Result> { - let (lsp_adapter, _) = language_server_for_buffer(&project, &buffer, server_id, &mut cx)?; + let (lsp_adapter, lsp_server) = + language_server_for_buffer(&project, &buffer, server_id, &mut cx)?; // `typescript-language-server` adds padding to the left for type hints, turning // `const foo: boolean` into `const foo : boolean` which looks odd. // `rust-analyzer` does not have the padding for this case, and we have to accomodate both. @@ -1833,6 +1834,15 @@ impl LspCommand for InlayHints { .unwrap_or_default() .into_iter() .map(|lsp_hint| { + let resolve_state = match lsp_server.capabilities().inlay_hint_provider { + Some(lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options( + lsp::InlayHintOptions { + resolve_provider: Some(true), + .. + }, + ))) => ResolveState::CanResolve(lsp_hint.data), + _ => ResolveState::Resolved, + }; let kind = lsp_hint.kind.and_then(|kind| match kind { lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), @@ -1910,6 +1920,7 @@ impl LspCommand for InlayHints { }) } }), + resolve_state, } }) .collect()) @@ -1959,57 +1970,69 @@ impl LspCommand for InlayHints { proto::InlayHintsResponse { hints: response .into_iter() - .map(|response_hint| proto::InlayHint { - position: Some(language::proto::serialize_anchor(&response_hint.position)), - padding_left: response_hint.padding_left, - padding_right: response_hint.padding_right, - label: Some(proto::InlayHintLabel { - label: Some(match response_hint.label { - InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), - InlayHintLabel::LabelParts(label_parts) => { - proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { - parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart { - value: label_part.value, - tooltip: label_part.tooltip.map(|tooltip| { - let proto_tooltip = match tooltip { - InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), - InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }), - }; - proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} - }), - location: label_part.location.map(|location| proto::Location { - start: Some(serialize_anchor(&location.range.start)), - end: Some(serialize_anchor(&location.range.end)), - buffer_id: location.buffer.read(cx).remote_id(), - }), - }).collect() - }) + .map(|response_hint| { + let (state, lsp_resolve_state) = match response_hint.resolve_state { + ResolveState::CanResolve(resolve_data) => { + (0, resolve_data.map(|json_data| serde_json::to_string(&json_data).expect("failed to serialize resolve json data")).map(|value| proto::resolve_state::LspResolveState{ value })) + } + ResolveState::Resolved => (1, None), + ResolveState::Resolving => (2, None), + }; + let resolve_state = Some(proto::ResolveState { + state, lsp_resolve_state + }); + proto::InlayHint { + position: Some(language::proto::serialize_anchor(&response_hint.position)), + padding_left: response_hint.padding_left, + padding_right: response_hint.padding_right, + label: Some(proto::InlayHintLabel { + label: Some(match response_hint.label { + InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), + InlayHintLabel::LabelParts(label_parts) => { + proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { + parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart { + value: label_part.value, + tooltip: label_part.tooltip.map(|tooltip| { + let proto_tooltip = match tooltip { + InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), + InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { + kind: markup_content.kind, + value: markup_content.value, + }), + }; + proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} + }), + location: label_part.location.map(|location| proto::Location { + start: Some(serialize_anchor(&location.range.start)), + end: Some(serialize_anchor(&location.range.end)), + buffer_id: location.buffer.read(cx).remote_id(), + }), + }).collect() + }) + } + }), + }), + kind: response_hint.kind.map(|kind| kind.name().to_string()), + tooltip: response_hint.tooltip.map(|response_tooltip| { + let proto_tooltip = match response_tooltip { + InlayHintTooltip::String(s) => { + proto::inlay_hint_tooltip::Content::Value(s) + } + InlayHintTooltip::MarkupContent(markup_content) => { + proto::inlay_hint_tooltip::Content::MarkupContent( + proto::MarkupContent { + kind: markup_content.kind, + value: markup_content.value, + }, + ) + } + }; + proto::InlayHintTooltip { + content: Some(proto_tooltip), } }), - }), - kind: response_hint.kind.map(|kind| kind.name().to_string()), - tooltip: response_hint.tooltip.map(|response_tooltip| { - let proto_tooltip = match response_tooltip { - InlayHintTooltip::String(s) => { - proto::inlay_hint_tooltip::Content::Value(s) - } - InlayHintTooltip::MarkupContent(markup_content) => { - proto::inlay_hint_tooltip::Content::MarkupContent( - proto::MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }, - ) - } - }; - proto::InlayHintTooltip { - content: Some(proto_tooltip), - } - }), - }) + resolve_state, + }}) .collect(), version: serialize_version(buffer_version), } @@ -2021,7 +2044,7 @@ impl LspCommand for InlayHints { project: ModelHandle, buffer: ModelHandle, mut cx: AsyncAppContext, - ) -> Result> { + ) -> anyhow::Result> { buffer .update(&mut cx, |buffer, _| { buffer.wait_for_version(deserialize_version(&message.version)) @@ -2035,6 +2058,27 @@ impl LspCommand for InlayHints { .as_ref() .and_then(|location| location.buffer_id) .context("missing buffer id")?; + let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| { + panic!( + "incorrect proto inlay hint message: no resolve state in hint {message_hint:?}", + ) + }); + + let lsp_resolve_state = resolve_state + .lsp_resolve_state.as_ref() + .map(|lsp_resolve_state| { + serde_json::from_str::(&lsp_resolve_state.value) + .with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}")) + }) + .transpose()?; + let resolve_state = match resolve_state.state { + 0 => ResolveState::Resolved, + 1 => ResolveState::CanResolve(lsp_resolve_state), + 2 => ResolveState::Resolving, + invalid => { + anyhow::bail!("Unexpected resolve state {invalid} for hint {message_hint:?}") + } + }; let hint = InlayHint { buffer_id, position: message_hint @@ -2103,6 +2147,7 @@ impl LspCommand for InlayHints { } }) }), + resolve_state, }; hints.push(hint); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 49074268f2..1628234f98 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -342,6 +342,22 @@ pub struct InlayHint { pub padding_left: bool, pub padding_right: bool, pub tooltip: Option, + pub resolve_state: ResolveState, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResolveState { + Resolved, + CanResolve(Option), + Resolving, +} + +impl Hash for ResolveState { + fn hash(&self, state: &mut H) { + // Regular `lsp::LSPAny` is not hashable, so we can't hash it. + // LSP expects this data to not to change between requests, so we only hash the discriminant. + std::mem::discriminant(self).hash(state); + } } impl InlayHint { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index f032ccce51..d0964064ab 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -754,6 +754,7 @@ message InlayHint { bool padding_left = 4; bool padding_right = 5; InlayHintTooltip tooltip = 6; + ResolveState resolve_state = 7; } message InlayHintLabel { @@ -787,6 +788,21 @@ message InlayHintLabelPartTooltip { } } +message ResolveState { + State state = 1; + LspResolveState lsp_resolve_state = 2; + + enum State { + Resolved = 0; + CanResolve = 1; + Resolving = 2; + } + + message LspResolveState { + string value = 1; + } +} + message RefreshInlayHints { uint64 project_id = 1; } From 80e871424194fb8c018d5d85d0d5182492a75238 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Aug 2023 21:09:04 +0300 Subject: [PATCH 05/24] Send inlay hint resolve requests --- crates/editor/src/display_map/inlay_map.rs | 4 - crates/editor/src/element.rs | 177 +++--- crates/editor/src/inlay_hint_cache.rs | 81 ++- crates/project/src/lsp_command.rs | 624 +++++++++++++-------- crates/project/src/project.rs | 140 ++++- crates/rpc/proto/zed.proto | 16 +- crates/rpc/src/proto.rs | 4 + crates/rpc/src/rpc.rs | 2 +- 8 files changed, 705 insertions(+), 343 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 3cea513dea..026f1fc2c2 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1125,7 +1125,6 @@ mod tests { Anchor::min(), &InlayHint { label: InlayHintLabel::String("a".to_string()), - buffer_id: 0, position: text::Anchor::default(), padding_left: false, padding_right: false, @@ -1146,7 +1145,6 @@ mod tests { Anchor::min(), &InlayHint { label: InlayHintLabel::String("a".to_string()), - buffer_id: 0, position: text::Anchor::default(), padding_left: true, padding_right: true, @@ -1167,7 +1165,6 @@ mod tests { Anchor::min(), &InlayHint { label: InlayHintLabel::String(" a ".to_string()), - buffer_id: 0, position: text::Anchor::default(), padding_left: false, padding_right: false, @@ -1188,7 +1185,6 @@ mod tests { Anchor::min(), &InlayHint { label: InlayHintLabel::String(" a ".to_string()), - buffer_id: 0, position: text::Anchor::default(), padding_left: true, padding_right: true, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 5738419551..2d2191e77f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -42,7 +42,7 @@ use language::{ }; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, - InlayHintLabelPart, ProjectPath, + InlayHintLabelPart, ProjectPath, ResolveState, }; use smallvec::SmallVec; use std::{ @@ -456,82 +456,21 @@ impl EditorElement { ) -> bool { // 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 mut go_to_definition_point = None; + let mut hover_at_point = None; if text_bounds.contains_point(position) { let point_for_position = position_map.point_for_position(text_bounds, position); if let Some(point) = point_for_position.as_valid() { - update_go_to_definition_link(editor, Some(point), cmd, shift, cx); - hover_at(editor, Some(point), cx); - return true; + go_to_definition_point = Some(point); + hover_at_point = Some(point); } else { - let hint_start_offset = position_map - .snapshot - .display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); - let hint_end_offset = position_map - .snapshot - .display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); - let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; - let hovered_offset = if offset_overshoot == 0 { - Some(position_map.snapshot.display_point_to_inlay_offset( - point_for_position.exact_unclipped, - Bias::Left, - )) - } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { - Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) - } else { - None - }; - if let Some(hovered_offset) = hovered_offset { - let buffer = editor.buffer().read(cx); - let snapshot = buffer.snapshot(cx); - let previous_valid_anchor = snapshot.anchor_at( - point_for_position - .previous_valid - .to_point(&position_map.snapshot.display_snapshot), - Bias::Left, - ); - let next_valid_anchor = snapshot.anchor_at( - point_for_position - .next_valid - .to_point(&position_map.snapshot.display_snapshot), - Bias::Right, - ); - if let Some(hovered_hint) = editor - .visible_inlay_hints(cx) - .into_iter() - .skip_while(|hint| { - hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt() - }) - .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) - .max_by_key(|hint| hint.id) - { - if let Some(cached_hint) = editor - .inlay_hint_cache() - .hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) - { - match &cached_hint.label { - project::InlayHintLabel::String(regular_label) => { - // TODO kb remove + check for tooltip for hover and resolve, if needed - eprintln!("regular string: {regular_label}"); - } - project::InlayHintLabel::LabelParts(label_parts) => { - if let Some(hovered_hint_part) = find_hovered_hint_part( - &label_parts, - hint_start_offset..hint_end_offset, - hovered_offset, - ) { - // TODO kb remove + check for tooltip and location and resolve, if needed - eprintln!("hint_part: {hovered_hint_part:?}"); - } - } - }; - } - } - } + (go_to_definition_point, hover_at_point) = + inlay_link_and_hover_points(position_map, point_for_position, editor, cx); } }; - update_go_to_definition_link(editor, None, cmd, shift, cx); - hover_at(editor, None, cx); + update_go_to_definition_link(editor, go_to_definition_point, cmd, shift, cx); + hover_at(editor, hover_at_point, cx); true } @@ -1876,6 +1815,104 @@ impl EditorElement { } } +fn inlay_link_and_hover_points( + position_map: &PositionMap, + point_for_position: PointForPosition, + editor: &mut Editor, + cx: &mut ViewContext<'_, '_, Editor>, +) -> (Option, Option) { + let hint_start_offset = position_map + .snapshot + .display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); + let hint_end_offset = position_map + .snapshot + .display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); + let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; + let hovered_offset = if offset_overshoot == 0 { + Some( + position_map + .snapshot + .display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left), + ) + } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { + Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) + } else { + None + }; + let mut go_to_definition_point = None; + let mut hover_at_point = None; + if let Some(hovered_offset) = hovered_offset { + let buffer = editor.buffer().read(cx); + let snapshot = buffer.snapshot(cx); + let previous_valid_anchor = snapshot.anchor_at( + point_for_position + .previous_valid + .to_point(&position_map.snapshot.display_snapshot), + Bias::Left, + ); + let next_valid_anchor = snapshot.anchor_at( + point_for_position + .next_valid + .to_point(&position_map.snapshot.display_snapshot), + Bias::Right, + ); + if let Some(hovered_hint) = editor + .visible_inlay_hints(cx) + .into_iter() + .skip_while(|hint| hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt()) + .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) + .max_by_key(|hint| hint.id) + { + let inlay_hint_cache = editor.inlay_hint_cache(); + if let Some(cached_hint) = + inlay_hint_cache.hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) + { + match &cached_hint.resolve_state { + ResolveState::CanResolve(_, _) => { + if let Some(buffer_id) = previous_valid_anchor.buffer_id { + inlay_hint_cache.spawn_hint_resolve( + buffer_id, + previous_valid_anchor.excerpt_id, + hovered_hint.id, + cx, + ); + } + } + ResolveState::Resolved => { + match &cached_hint.label { + project::InlayHintLabel::String(_) => { + if cached_hint.tooltip.is_some() { + dbg!(&cached_hint.tooltip); // TODO kb + // hover_at_point = Some(hovered_offset); + } + } + project::InlayHintLabel::LabelParts(label_parts) => { + if let Some(hovered_hint_part) = find_hovered_hint_part( + &label_parts, + hint_start_offset..hint_end_offset, + hovered_offset, + ) { + if hovered_hint_part.tooltip.is_some() { + dbg!(&hovered_hint_part.tooltip); // TODO kb + // hover_at_point = Some(hovered_offset); + } + if let Some(location) = &hovered_hint_part.location { + dbg!(location); // TODO kb + // go_to_definition_point = Some(location); + } + } + } + }; + } + ResolveState::Resolving => {} + } + } + } + } + + (go_to_definition_point, hover_at_point) +} + fn find_hovered_hint_part<'a>( label_parts: &'a [InlayHintLabelPart], hint_range: Range, diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 5b9bdd08ec..52a4039a76 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -13,7 +13,7 @@ use gpui::{ModelContext, ModelHandle, Task, ViewContext}; use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot}; use log::error; use parking_lot::RwLock; -use project::InlayHint; +use project::{InlayHint, ResolveState}; use collections::{hash_map, HashMap, HashSet}; use language::language_settings::InlayHintSettings; @@ -60,7 +60,7 @@ struct ExcerptHintsUpdate { excerpt_id: ExcerptId, remove_from_visible: Vec, remove_from_cache: HashSet, - add_to_cache: HashSet, + add_to_cache: Vec, } #[derive(Debug, Clone, Copy)] @@ -409,6 +409,79 @@ impl InlayHintCache { pub fn version(&self) -> usize { self.version } + + pub fn spawn_hint_resolve( + &self, + buffer_id: u64, + excerpt_id: ExcerptId, + id: InlayId, + cx: &mut ViewContext<'_, '_, Editor>, + ) { + if let Some(excerpt_hints) = self.hints.get(&excerpt_id) { + let mut guard = excerpt_hints.write(); + if let Some(cached_hint) = guard + .hints + .iter_mut() + .find(|(hint_id, _)| hint_id == &id) + .map(|(_, hint)| hint) + { + if let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state { + let hint_to_resolve = cached_hint.clone(); + let server_id = *server_id; + cached_hint.resolve_state = ResolveState::Resolving; + drop(guard); + cx.spawn(|editor, mut cx| async move { + let resolved_hint_task = editor.update(&mut cx, |editor, cx| { + editor + .buffer() + .read(cx) + .buffer(buffer_id) + .and_then(|buffer| { + let project = editor.project.as_ref()?; + Some(project.update(cx, |project, cx| { + project.resolve_inlay_hint( + hint_to_resolve, + buffer, + server_id, + cx, + ) + })) + }) + })?; + if let Some(resolved_hint_task) = resolved_hint_task { + if let Some(mut resolved_hint) = + resolved_hint_task.await.context("hint resolve task")? + { + editor.update(&mut cx, |editor, _| { + if let Some(excerpt_hints) = + editor.inlay_hint_cache.hints.get(&excerpt_id) + { + let mut guard = excerpt_hints.write(); + if let Some(cached_hint) = guard + .hints + .iter_mut() + .find(|(hint_id, _)| hint_id == &id) + .map(|(_, hint)| hint) + { + if cached_hint.resolve_state == ResolveState::Resolving + { + resolved_hint.resolve_state = + ResolveState::Resolved; + *cached_hint = resolved_hint; + } + } + } + })?; + } + } + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + } + } + } } fn spawn_new_update_tasks( @@ -632,7 +705,7 @@ fn calculate_hint_updates( cached_excerpt_hints: Option>>, visible_hints: &[Inlay], ) -> Option { - let mut add_to_cache: HashSet = HashSet::default(); + let mut add_to_cache = Vec::::new(); let mut excerpt_hints_to_persist = HashMap::default(); for new_hint in new_excerpt_hints { if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) { @@ -659,7 +732,7 @@ fn calculate_hint_updates( None => true, }; if missing_from_cache { - add_to_cache.insert(new_hint); + add_to_cache.push(new_hint); } } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index d46ba4f5f7..7b4d689a81 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,6 +1,6 @@ use crate::{ DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, - InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, + InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Item, Location, LocationLink, MarkupContent, Project, ProjectTransaction, ResolveState, }; use anyhow::{anyhow, Context, Result}; @@ -12,8 +12,9 @@ use language::{ language_settings::{language_settings, InlayHintKind}, point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, - range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction, - Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped, + range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, + CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, + Unclipped, }; use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; @@ -1776,6 +1777,371 @@ impl LspCommand for OnTypeFormatting { } } +impl InlayHints { + pub fn lsp_to_project_hint( + lsp_hint: lsp::InlayHint, + buffer_handle: &ModelHandle, + resolve_state: ResolveState, + force_no_type_left_padding: bool, + cx: &AppContext, + ) -> InlayHint { + let kind = lsp_hint.kind.and_then(|kind| match kind { + lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), + lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), + _ => None, + }); + let buffer = buffer_handle.read(cx); + let position = buffer.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); + let padding_left = if force_no_type_left_padding && kind == Some(InlayHintKind::Type) { + false + } else { + lsp_hint.padding_left.unwrap_or(false) + }; + InlayHint { + position: if kind == Some(InlayHintKind::Parameter) { + buffer.anchor_before(position) + } else { + buffer.anchor_after(position) + }, + padding_left, + padding_right: lsp_hint.padding_right.unwrap_or(false), + label: match lsp_hint.label { + lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), + lsp::InlayHintLabel::LabelParts(lsp_parts) => InlayHintLabel::LabelParts( + lsp_parts + .into_iter() + .map(|label_part| InlayHintLabelPart { + value: label_part.value, + tooltip: label_part.tooltip.map(|tooltip| match tooltip { + lsp::InlayHintLabelPartTooltip::String(s) => { + InlayHintLabelPartTooltip::String(s) + } + lsp::InlayHintLabelPartTooltip::MarkupContent(markup_content) => { + InlayHintLabelPartTooltip::MarkupContent(MarkupContent { + kind: match markup_content.kind { + lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, + lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, + }, + value: markup_content.value, + }) + } + }), + location: label_part.location.map(|lsp_location| { + let target_start = buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.start), + Bias::Left, + ); + let target_end = buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.end), + Bias::Left, + ); + Location { + buffer: buffer_handle.clone(), + range: buffer.anchor_after(target_start) + ..buffer.anchor_before(target_end), + } + }), + }) + .collect(), + ), + }, + kind, + tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip { + lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s), + lsp::InlayHintTooltip::MarkupContent(markup_content) => { + InlayHintTooltip::MarkupContent(MarkupContent { + kind: match markup_content.kind { + lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, + lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, + }, + value: markup_content.value, + }) + } + }), + resolve_state, + } + } + + pub fn project_to_proto_hint(response_hint: InlayHint, cx: &AppContext) -> proto::InlayHint { + let (state, lsp_resolve_state) = match response_hint.resolve_state { + ResolveState::CanResolve(server_id, resolve_data) => ( + 0, + resolve_data + .map(|json_data| { + serde_json::to_string(&json_data) + .expect("failed to serialize resolve json data") + }) + .map(|value| proto::resolve_state::LspResolveState { + server_id: server_id.0 as u64, + value, + }), + ), + ResolveState::Resolved => (1, None), + ResolveState::Resolving => (2, None), + }; + let resolve_state = Some(proto::ResolveState { + state, + lsp_resolve_state, + }); + proto::InlayHint { + position: Some(language::proto::serialize_anchor(&response_hint.position)), + padding_left: response_hint.padding_left, + padding_right: response_hint.padding_right, + label: Some(proto::InlayHintLabel { + label: Some(match response_hint.label { + InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), + InlayHintLabel::LabelParts(label_parts) => { + proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { + parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart { + value: label_part.value, + tooltip: label_part.tooltip.map(|tooltip| { + let proto_tooltip = match tooltip { + InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), + InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { + is_markdown: markup_content.kind == HoverBlockKind::Markdown, + value: markup_content.value, + }), + }; + proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} + }), + location: label_part.location.map(|location| proto::Location { + start: Some(serialize_anchor(&location.range.start)), + end: Some(serialize_anchor(&location.range.end)), + buffer_id: location.buffer.read(cx).remote_id(), + }), + }).collect() + }) + } + }), + }), + kind: response_hint.kind.map(|kind| kind.name().to_string()), + tooltip: response_hint.tooltip.map(|response_tooltip| { + let proto_tooltip = match response_tooltip { + InlayHintTooltip::String(s) => { + proto::inlay_hint_tooltip::Content::Value(s) + } + InlayHintTooltip::MarkupContent(markup_content) => { + proto::inlay_hint_tooltip::Content::MarkupContent( + proto::MarkupContent { + is_markdown: markup_content.kind == HoverBlockKind::Markdown, + value: markup_content.value, + }, + ) + } + }; + proto::InlayHintTooltip { + content: Some(proto_tooltip), + } + }), + resolve_state, + } + } + + pub async fn proto_to_project_hint( + message_hint: proto::InlayHint, + project: &ModelHandle, + cx: &mut AsyncAppContext, + ) -> anyhow::Result { + let buffer_id = message_hint + .position + .as_ref() + .and_then(|location| location.buffer_id) + .context("missing buffer id")?; + let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| { + panic!("incorrect proto inlay hint message: no resolve state in hint {message_hint:?}",) + }); + let resolve_state_data = resolve_state + .lsp_resolve_state.as_ref() + .map(|lsp_resolve_state| { + serde_json::from_str::>(&lsp_resolve_state.value) + .with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}")) + .map(|state| (LanguageServerId(lsp_resolve_state.server_id as usize), state)) + }) + .transpose()?; + let resolve_state = match resolve_state.state { + 0 => ResolveState::Resolved, + 1 => { + let (server_id, lsp_resolve_state) = resolve_state_data.with_context(|| { + format!( + "No lsp resolve data for the hint that can be resolved: {message_hint:?}" + ) + })?; + ResolveState::CanResolve(server_id, lsp_resolve_state) + } + 2 => ResolveState::Resolving, + invalid => { + anyhow::bail!("Unexpected resolve state {invalid} for hint {message_hint:?}") + } + }; + Ok(InlayHint { + position: message_hint + .position + .and_then(language::proto::deserialize_anchor) + .context("invalid position")?, + label: match message_hint + .label + .and_then(|label| label.label) + .context("missing label")? + { + proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s), + proto::inlay_hint_label::Label::LabelParts(parts) => { + let mut label_parts = Vec::new(); + for part in parts.parts { + let buffer = project + .update(cx, |this, cx| this.wait_for_remote_buffer(buffer_id, cx)) + .await?; + label_parts.push(InlayHintLabelPart { + value: part.value, + tooltip: part.tooltip.map(|tooltip| match tooltip.content { + Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => { + InlayHintLabelPartTooltip::String(s) + } + Some( + proto::inlay_hint_label_part_tooltip::Content::MarkupContent( + markup_content, + ), + ) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent { + kind: if markup_content.is_markdown { + HoverBlockKind::Markdown + } else { + HoverBlockKind::PlainText + }, + value: markup_content.value, + }), + None => InlayHintLabelPartTooltip::String(String::new()), + }), + location: match part.location { + Some(location) => Some(Location { + range: location + .start + .and_then(language::proto::deserialize_anchor) + .context("invalid start")? + ..location + .end + .and_then(language::proto::deserialize_anchor) + .context("invalid end")?, + buffer, + }), + None => None, + }, + }); + } + + InlayHintLabel::LabelParts(label_parts) + } + }, + padding_left: message_hint.padding_left, + padding_right: message_hint.padding_right, + kind: message_hint + .kind + .as_deref() + .and_then(InlayHintKind::from_name), + tooltip: message_hint.tooltip.and_then(|tooltip| { + Some(match tooltip.content? { + proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s), + proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => { + InlayHintTooltip::MarkupContent(MarkupContent { + kind: if markup_content.is_markdown { + HoverBlockKind::Markdown + } else { + HoverBlockKind::PlainText + }, + value: markup_content.value, + }) + } + }) + }), + resolve_state, + }) + } + + // TODO kb instead, store all LSP data inside the project::InlayHint? + pub fn project_to_lsp_hint( + hint: InlayHint, + project: &ModelHandle, + snapshot: &BufferSnapshot, + cx: &AsyncAppContext, + ) -> lsp::InlayHint { + lsp::InlayHint { + position: point_to_lsp(hint.position.to_point_utf16(snapshot)), + kind: hint.kind.map(|kind| match kind { + InlayHintKind::Type => lsp::InlayHintKind::TYPE, + InlayHintKind::Parameter => lsp::InlayHintKind::PARAMETER, + }), + text_edits: None, + tooltip: hint.tooltip.and_then(|tooltip| { + Some(match tooltip { + InlayHintTooltip::String(s) => lsp::InlayHintTooltip::String(s), + InlayHintTooltip::MarkupContent(markup_content) => { + lsp::InlayHintTooltip::MarkupContent(lsp::MarkupContent { + kind: match markup_content.kind { + HoverBlockKind::PlainText => lsp::MarkupKind::PlainText, + HoverBlockKind::Markdown => lsp::MarkupKind::Markdown, + HoverBlockKind::Code { .. } => return None, + }, + value: markup_content.value, + }) + } + }) + }), + label: match hint.label { + InlayHintLabel::String(s) => lsp::InlayHintLabel::String(s), + InlayHintLabel::LabelParts(label_parts) => lsp::InlayHintLabel::LabelParts( + label_parts + .into_iter() + .map(|part| lsp::InlayHintLabelPart { + value: part.value, + tooltip: part.tooltip.and_then(|tooltip| { + Some(match tooltip { + InlayHintLabelPartTooltip::String(s) => { + lsp::InlayHintLabelPartTooltip::String(s) + } + InlayHintLabelPartTooltip::MarkupContent(markup_content) => { + lsp::InlayHintLabelPartTooltip::MarkupContent( + lsp::MarkupContent { + kind: match markup_content.kind { + HoverBlockKind::PlainText => { + lsp::MarkupKind::PlainText + } + HoverBlockKind::Markdown => { + lsp::MarkupKind::Markdown + } + HoverBlockKind::Code { .. } => return None, + }, + value: markup_content.value, + }, + ) + } + }) + }), + location: part.location.and_then(|location| { + let path = cx.read(|cx| { + let project_path = location.buffer.read(cx).project_path(cx)?; + project.read(cx).absolute_path(&project_path, cx) + })?; + Some(lsp::Location::new( + lsp::Url::from_file_path(path).unwrap(), + range_to_lsp( + location.range.start.to_point_utf16(snapshot) + ..location.range.end.to_point_utf16(snapshot), + ), + )) + }), + command: None, + }) + .collect(), + ), + }, + padding_left: Some(hint.padding_left), + padding_right: Some(hint.padding_right), + data: match hint.resolve_state { + ResolveState::CanResolve(_, data) => data, + ResolveState::Resolving | ResolveState::Resolved => None, + }, + } + } +} + #[async_trait(?Send)] impl LspCommand for InlayHints { type Response = Vec; @@ -1829,7 +2195,6 @@ impl LspCommand for InlayHints { let force_no_type_left_padding = lsp_adapter.name.0.as_ref() == "typescript-language-server"; cx.read(|cx| { - let origin_buffer = buffer.read(cx); Ok(message .unwrap_or_default() .into_iter() @@ -1840,88 +2205,18 @@ impl LspCommand for InlayHints { resolve_provider: Some(true), .. }, - ))) => ResolveState::CanResolve(lsp_hint.data), + ))) => { + ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()) + } _ => ResolveState::Resolved, }; - let kind = lsp_hint.kind.and_then(|kind| match kind { - lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), - lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), - _ => None, - }); - let position = origin_buffer - .clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); - let padding_left = - if force_no_type_left_padding && kind == Some(InlayHintKind::Type) { - false - } else { - lsp_hint.padding_left.unwrap_or(false) - }; - InlayHint { - buffer_id: origin_buffer.remote_id(), - position: if kind == Some(InlayHintKind::Parameter) { - origin_buffer.anchor_before(position) - } else { - origin_buffer.anchor_after(position) - }, - padding_left, - padding_right: lsp_hint.padding_right.unwrap_or(false), - label: match lsp_hint.label { - lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), - lsp::InlayHintLabel::LabelParts(lsp_parts) => { - InlayHintLabel::LabelParts( - lsp_parts - .into_iter() - .map(|label_part| InlayHintLabelPart { - value: label_part.value, - tooltip: label_part.tooltip.map( - |tooltip| { - match tooltip { - lsp::InlayHintLabelPartTooltip::String(s) => { - InlayHintLabelPartTooltip::String(s) - } - lsp::InlayHintLabelPartTooltip::MarkupContent( - markup_content, - ) => InlayHintLabelPartTooltip::MarkupContent( - MarkupContent { - kind: format!("{:?}", markup_content.kind), - value: markup_content.value, - }, - ), - } - }, - ), - location: label_part.location.map(|lsp_location| { - let target_start = origin_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.start), - Bias::Left, - ); - let target_end = origin_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.end), - Bias::Left, - ); - Location { - buffer: buffer.clone(), - range: origin_buffer.anchor_after(target_start) - ..origin_buffer.anchor_before(target_end), - } - }), - }) - .collect(), - ) - } - }, - kind, - tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip { - lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s), - lsp::InlayHintTooltip::MarkupContent(markup_content) => { - InlayHintTooltip::MarkupContent(MarkupContent { - kind: format!("{:?}", markup_content.kind), - value: markup_content.value, - }) - } - }), + InlayHints::lsp_to_project_hint( + lsp_hint, + &buffer, resolve_state, - } + force_no_type_left_padding, + cx, + ) }) .collect()) }) @@ -1970,69 +2265,7 @@ impl LspCommand for InlayHints { proto::InlayHintsResponse { hints: response .into_iter() - .map(|response_hint| { - let (state, lsp_resolve_state) = match response_hint.resolve_state { - ResolveState::CanResolve(resolve_data) => { - (0, resolve_data.map(|json_data| serde_json::to_string(&json_data).expect("failed to serialize resolve json data")).map(|value| proto::resolve_state::LspResolveState{ value })) - } - ResolveState::Resolved => (1, None), - ResolveState::Resolving => (2, None), - }; - let resolve_state = Some(proto::ResolveState { - state, lsp_resolve_state - }); - proto::InlayHint { - position: Some(language::proto::serialize_anchor(&response_hint.position)), - padding_left: response_hint.padding_left, - padding_right: response_hint.padding_right, - label: Some(proto::InlayHintLabel { - label: Some(match response_hint.label { - InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), - InlayHintLabel::LabelParts(label_parts) => { - proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { - parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart { - value: label_part.value, - tooltip: label_part.tooltip.map(|tooltip| { - let proto_tooltip = match tooltip { - InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), - InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }), - }; - proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} - }), - location: label_part.location.map(|location| proto::Location { - start: Some(serialize_anchor(&location.range.start)), - end: Some(serialize_anchor(&location.range.end)), - buffer_id: location.buffer.read(cx).remote_id(), - }), - }).collect() - }) - } - }), - }), - kind: response_hint.kind.map(|kind| kind.name().to_string()), - tooltip: response_hint.tooltip.map(|response_tooltip| { - let proto_tooltip = match response_tooltip { - InlayHintTooltip::String(s) => { - proto::inlay_hint_tooltip::Content::Value(s) - } - InlayHintTooltip::MarkupContent(markup_content) => { - proto::inlay_hint_tooltip::Content::MarkupContent( - proto::MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }, - ) - } - }; - proto::InlayHintTooltip { - content: Some(proto_tooltip), - } - }), - resolve_state, - }}) + .map(|response_hint| InlayHints::project_to_proto_hint(response_hint, cx)) .collect(), version: serialize_version(buffer_version), } @@ -2053,104 +2286,7 @@ impl LspCommand for InlayHints { let mut hints = Vec::new(); for message_hint in message.hints { - let buffer_id = message_hint - .position - .as_ref() - .and_then(|location| location.buffer_id) - .context("missing buffer id")?; - let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| { - panic!( - "incorrect proto inlay hint message: no resolve state in hint {message_hint:?}", - ) - }); - - let lsp_resolve_state = resolve_state - .lsp_resolve_state.as_ref() - .map(|lsp_resolve_state| { - serde_json::from_str::(&lsp_resolve_state.value) - .with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}")) - }) - .transpose()?; - let resolve_state = match resolve_state.state { - 0 => ResolveState::Resolved, - 1 => ResolveState::CanResolve(lsp_resolve_state), - 2 => ResolveState::Resolving, - invalid => { - anyhow::bail!("Unexpected resolve state {invalid} for hint {message_hint:?}") - } - }; - let hint = InlayHint { - buffer_id, - position: message_hint - .position - .and_then(language::proto::deserialize_anchor) - .context("invalid position")?, - label: match message_hint - .label - .and_then(|label| label.label) - .context("missing label")? - { - proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s), - proto::inlay_hint_label::Label::LabelParts(parts) => { - let mut label_parts = Vec::new(); - for part in parts.parts { - label_parts.push(InlayHintLabelPart { - value: part.value, - tooltip: part.tooltip.map(|tooltip| match tooltip.content { - Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => InlayHintLabelPartTooltip::String(s), - Some(proto::inlay_hint_label_part_tooltip::Content::MarkupContent(markup_content)) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }), - None => InlayHintLabelPartTooltip::String(String::new()), - }), - location: match part.location { - Some(location) => { - let target_buffer = project - .update(&mut cx, |this, cx| { - this.wait_for_remote_buffer(location.buffer_id, cx) - }) - .await?; - Some(Location { - range: location - .start - .and_then(language::proto::deserialize_anchor) - .context("invalid start")? - ..location - .end - .and_then(language::proto::deserialize_anchor) - .context("invalid end")?, - buffer: target_buffer, - })}, - None => None, - }, - }); - } - - InlayHintLabel::LabelParts(label_parts) - } - }, - padding_left: message_hint.padding_left, - padding_right: message_hint.padding_right, - kind: message_hint - .kind - .as_deref() - .and_then(InlayHintKind::from_name), - tooltip: message_hint.tooltip.and_then(|tooltip| { - Some(match tooltip.content? { - proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s), - proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => { - InlayHintTooltip::MarkupContent(MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }) - } - }) - }), - resolve_state, - }; - - hints.push(hint); + hints.push(InlayHints::proto_to_project_hint(message_hint, &project, &mut cx).await?); } Ok(hints) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1628234f98..65be7cceb1 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -333,9 +333,8 @@ pub struct Location { pub range: Range, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct InlayHint { - pub buffer_id: u64, pub position: language::Anchor, pub label: InlayHintLabel, pub kind: Option, @@ -348,18 +347,10 @@ pub struct InlayHint { #[derive(Debug, Clone, PartialEq, Eq)] pub enum ResolveState { Resolved, - CanResolve(Option), + CanResolve(LanguageServerId, Option), Resolving, } -impl Hash for ResolveState { - fn hash(&self, state: &mut H) { - // Regular `lsp::LSPAny` is not hashable, so we can't hash it. - // LSP expects this data to not to change between requests, so we only hash the discriminant. - std::mem::discriminant(self).hash(state); - } -} - impl InlayHint { pub fn text(&self) -> String { match &self.label { @@ -369,34 +360,34 @@ impl InlayHint { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum InlayHintLabel { String(String), LabelParts(Vec), } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct InlayHintLabelPart { pub value: String, pub tooltip: Option, pub location: Option, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum InlayHintTooltip { String(String), MarkupContent(MarkupContent), } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum InlayHintLabelPartTooltip { String(String), MarkupContent(MarkupContent), } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct MarkupContent { - pub kind: String, + pub kind: HoverBlockKind, pub value: String, } @@ -430,7 +421,7 @@ pub struct HoverBlock { pub kind: HoverBlockKind, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum HoverBlockKind { PlainText, Markdown, @@ -567,6 +558,7 @@ impl Project { client.add_model_request_handler(Self::handle_apply_code_action); client.add_model_request_handler(Self::handle_on_type_formatting); client.add_model_request_handler(Self::handle_inlay_hints); + client.add_model_request_handler(Self::handle_resolve_inlay_hint); client.add_model_request_handler(Self::handle_refresh_inlay_hints); client.add_model_request_handler(Self::handle_reload_buffers); client.add_model_request_handler(Self::handle_synchronize_buffers); @@ -4985,7 +4977,7 @@ impl Project { buffer_handle: ModelHandle, range: Range, cx: &mut ModelContext, - ) -> Task>> { + ) -> Task>> { let buffer = buffer_handle.read(cx); let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end); let range_start = range.start; @@ -5035,6 +5027,79 @@ impl Project { } } + pub fn resolve_inlay_hint( + &self, + hint: InlayHint, + buffer_handle: ModelHandle, + server_id: LanguageServerId, + cx: &mut ModelContext, + ) -> Task>> { + if self.is_local() { + let buffer = buffer_handle.read(cx); + let (_, lang_server) = if let Some((adapter, server)) = + self.language_server_for_buffer(buffer, server_id, cx) + { + (adapter.clone(), server.clone()) + } else { + return Task::ready(Ok(None)); + }; + let can_resolve = lang_server + .capabilities() + .completion_provider + .as_ref() + .and_then(|options| options.resolve_provider) + .unwrap_or(false); + if !can_resolve { + return Task::ready(Ok(None)); + } + + let buffer_snapshot = buffer.snapshot(); + cx.spawn(|project, cx| async move { + let resolve_task = lang_server.request::( + InlayHints::project_to_lsp_hint(hint, &project, &buffer_snapshot, &cx), + ); + let resolved_hint = resolve_task + .await + .context("inlay hint resolve LSP request")?; + let resolved_hint = cx.read(|cx| { + InlayHints::lsp_to_project_hint( + resolved_hint, + &buffer_handle, + ResolveState::Resolved, + false, + cx, + ) + }); + Ok(Some(resolved_hint)) + }) + } else if let Some(project_id) = self.remote_id() { + let client = self.client.clone(); + let request = proto::ResolveInlayHint { + project_id, + buffer_id: buffer_handle.read(cx).remote_id(), + language_server_id: server_id.0 as u64, + hint: Some(InlayHints::project_to_proto_hint(hint, cx)), + }; + cx.spawn(|project, mut cx| async move { + let response = client + .request(request) + .await + .context("inlay hints proto request")?; + match response.hint { + Some(resolved_hint) => { + InlayHints::proto_to_project_hint(resolved_hint, &project, &mut cx) + .await + .map(Some) + .context("inlay hints proto response conversion") + } + None => Ok(None), + } + }) + } else { + Task::ready(Err(anyhow!("project does not have a remote id"))) + } + } + #[allow(clippy::type_complexity)] pub fn search( &self, @@ -6832,6 +6897,43 @@ impl Project { })) } + async fn handle_resolve_inlay_hint( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let proto_hint = envelope + .payload + .hint + .expect("incorrect protobuf resolve inlay hint message: missing the inlay hint"); + let hint = InlayHints::proto_to_project_hint(proto_hint, &this, &mut cx) + .await + .context("resolved proto inlay hint conversion")?; + let buffer = this.update(&mut cx, |this, cx| { + this.opened_buffers + .get(&envelope.payload.buffer_id) + .and_then(|buffer| buffer.upgrade(cx)) + .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id)) + })?; + let resolved_hint = this + .update(&mut cx, |project, cx| { + project.resolve_inlay_hint( + hint, + buffer, + LanguageServerId(envelope.payload.language_server_id as usize), + cx, + ) + }) + .await + .context("inlay hints fetch")? + .map(|hint| cx.read(|cx| InlayHints::project_to_proto_hint(hint, cx))); + + Ok(proto::ResolveInlayHintResponse { + hint: resolved_hint, + }) + } + async fn handle_refresh_inlay_hints( this: ModelHandle, _: TypedEnvelope, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index d0964064ab..dbd700e264 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -128,6 +128,8 @@ message Envelope { InlayHints inlay_hints = 116; InlayHintsResponse inlay_hints_response = 117; + ResolveInlayHint resolve_inlay_hint = 131; + ResolveInlayHintResponse resolve_inlay_hint_response = 132; RefreshInlayHints refresh_inlay_hints = 118; CreateChannel create_channel = 119; @@ -800,15 +802,27 @@ message ResolveState { message LspResolveState { string value = 1; + uint64 server_id = 2; } } +message ResolveInlayHint { + uint64 project_id = 1; + uint64 buffer_id = 2; + uint64 language_server_id = 3; + InlayHint hint = 4; +} + +message ResolveInlayHintResponse { + InlayHint hint = 1; +} + message RefreshInlayHints { uint64 project_id = 1; } message MarkupContent { - string kind = 1; + bool is_markdown = 1; string value = 2; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index f0f49c6230..2e4dce01e1 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -197,6 +197,8 @@ messages!( (OnTypeFormattingResponse, Background), (InlayHints, Background), (InlayHintsResponse, Background), + (ResolveInlayHint, Background), + (ResolveInlayHintResponse, Background), (RefreshInlayHints, Foreground), (Ping, Foreground), (PrepareRename, Background), @@ -299,6 +301,7 @@ request_messages!( (PrepareRename, PrepareRenameResponse), (OnTypeFormatting, OnTypeFormattingResponse), (InlayHints, InlayHintsResponse), + (ResolveInlayHint, ResolveInlayHintResponse), (RefreshInlayHints, Ack), (ReloadBuffers, ReloadBuffersResponse), (RequestContact, Ack), @@ -355,6 +358,7 @@ entity_messages!( PerformRename, OnTypeFormatting, InlayHints, + ResolveInlayHint, RefreshInlayHints, PrepareRename, ReloadBuffers, diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 3cb8b6bffa..bc9dd6f80b 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 60; +pub const PROTOCOL_VERSION: u32 = 61; From ac86bbac75b7f0347b9f1619f247c388bf8aea19 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 21 Aug 2023 16:25:01 +0300 Subject: [PATCH 06/24] Prepare for hover functionality refactoring --- crates/editor/src/element.rs | 37 +++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2d2191e77f..405a6e1a08 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -456,21 +456,27 @@ impl EditorElement { ) -> bool { // 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 mut go_to_definition_point = None; - let mut hover_at_point = None; if text_bounds.contains_point(position) { let point_for_position = position_map.point_for_position(text_bounds, position); - if let Some(point) = point_for_position.as_valid() { - go_to_definition_point = Some(point); - hover_at_point = Some(point); - } else { - (go_to_definition_point, hover_at_point) = - inlay_link_and_hover_points(position_map, point_for_position, editor, cx); + match point_for_position.as_valid() { + Some(point) => { + update_go_to_definition_link(editor, Some(point), cmd, shift, cx); + hover_at(editor, Some(point), cx); + } + None => { + update_inlay_link_and_hover_points( + position_map, + point_for_position, + editor, + cx, + ); + } } - }; + } else { + update_go_to_definition_link(editor, None, cmd, shift, cx); + hover_at(editor, None, cx); + } - update_go_to_definition_link(editor, go_to_definition_point, cmd, shift, cx); - hover_at(editor, hover_at_point, cx); true } @@ -1815,12 +1821,13 @@ impl EditorElement { } } -fn inlay_link_and_hover_points( +// TODO kb implement +fn update_inlay_link_and_hover_points( position_map: &PositionMap, point_for_position: PointForPosition, editor: &mut Editor, cx: &mut ViewContext<'_, '_, Editor>, -) -> (Option, Option) { +) { let hint_start_offset = position_map .snapshot .display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); @@ -1839,8 +1846,6 @@ fn inlay_link_and_hover_points( } else { None }; - let mut go_to_definition_point = None; - let mut hover_at_point = None; if let Some(hovered_offset) = hovered_offset { let buffer = editor.buffer().read(cx); let snapshot = buffer.snapshot(cx); @@ -1909,8 +1914,6 @@ fn inlay_link_and_hover_points( } } } - - (go_to_definition_point, hover_at_point) } fn find_hovered_hint_part<'a>( From 7eab18ec897738b7e6e5a8b71f15bcc3a3af4fe5 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 21 Aug 2023 17:54:34 +0300 Subject: [PATCH 07/24] Pass inlay go to definition data --- crates/editor/src/editor.rs | 13 +- crates/editor/src/element.rs | 82 ++++++-- crates/editor/src/items.rs | 2 +- crates/editor/src/link_go_to_definition.rs | 216 +++++++++++++++------ 4 files changed, 229 insertions(+), 84 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 775f3c07ec..fb1e87580a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -64,9 +64,7 @@ use language::{ Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, }; -use link_go_to_definition::{ - hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState, -}; +use link_go_to_definition::{hide_link_definition, show_link_definition, LinkGoToDefinitionState}; use log::error; use multi_buffer::ToOffsetUtf16; pub use multi_buffer::{ @@ -8307,14 +8305,11 @@ impl View for Editor { ) -> bool { let pending_selection = self.has_pending_selection(); - if let Some(point) = self.link_go_to_definition_state.last_mouse_location.clone() { + if let Some(point) = &self.link_go_to_definition_state.last_trigger_point { if event.cmd && !pending_selection { + let point = point.clone(); let snapshot = self.snapshot(cx); - let kind = if event.shift { - LinkDefinitionKind::Type - } else { - LinkDefinitionKind::Symbol - }; + let kind = point.definition_kind(event.shift); show_link_definition(kind, self, point, snapshot, cx); return false; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 405a6e1a08..15b28914af 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -13,6 +13,7 @@ use crate::{ }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, + GoToDefinitionTrigger, }, mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, }; @@ -42,7 +43,7 @@ use language::{ }; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, - InlayHintLabelPart, ProjectPath, ResolveState, + InlayHintLabelPart, Location, LocationLink, ProjectPath, ResolveState, }; use smallvec::SmallVec; use std::{ @@ -395,7 +396,15 @@ impl EditorElement { None }; - update_go_to_definition_link(editor, point, cmd, shift, cx); + update_go_to_definition_link( + editor, + point + .map(GoToDefinitionTrigger::Text) + .unwrap_or(GoToDefinitionTrigger::None), + cmd, + shift, + cx, + ); if editor.has_pending_selection() { let mut scroll_delta = Vector2F::zero(); @@ -460,7 +469,13 @@ impl EditorElement { let point_for_position = position_map.point_for_position(text_bounds, position); match point_for_position.as_valid() { Some(point) => { - update_go_to_definition_link(editor, Some(point), cmd, shift, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(point), + cmd, + shift, + cx, + ); hover_at(editor, Some(point), cx); } None => { @@ -468,12 +483,13 @@ impl EditorElement { position_map, point_for_position, editor, + (cmd, shift), cx, ); } } } else { - update_go_to_definition_link(editor, None, cmd, shift, cx); + update_go_to_definition_link(editor, GoToDefinitionTrigger::None, cmd, shift, cx); hover_at(editor, None, cx); } @@ -1821,11 +1837,11 @@ impl EditorElement { } } -// TODO kb implement fn update_inlay_link_and_hover_points( position_map: &PositionMap, point_for_position: PointForPosition, editor: &mut Editor, + (cmd_held, shift_held): (bool, bool), cx: &mut ViewContext<'_, '_, Editor>, ) { let hint_start_offset = position_map @@ -1861,6 +1877,9 @@ fn update_inlay_link_and_hover_points( .to_point(&position_map.snapshot.display_snapshot), Bias::Right, ); + + let mut go_to_definition_updated = false; + let mut hover_updated = false; if let Some(hovered_hint) = editor .visible_inlay_hints(cx) .into_iter() @@ -1872,7 +1891,7 @@ fn update_inlay_link_and_hover_points( if let Some(cached_hint) = inlay_hint_cache.hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) { - match &cached_hint.resolve_state { + match cached_hint.resolve_state { ResolveState::CanResolve(_, _) => { if let Some(buffer_id) = previous_valid_anchor.buffer_id { inlay_hint_cache.spawn_hint_resolve( @@ -1884,7 +1903,7 @@ fn update_inlay_link_and_hover_points( } } ResolveState::Resolved => { - match &cached_hint.label { + match cached_hint.label { project::InlayHintLabel::String(_) => { if cached_hint.tooltip.is_some() { dbg!(&cached_hint.tooltip); // TODO kb @@ -1893,7 +1912,7 @@ fn update_inlay_link_and_hover_points( } project::InlayHintLabel::LabelParts(label_parts) => { if let Some(hovered_hint_part) = find_hovered_hint_part( - &label_parts, + label_parts, hint_start_offset..hint_end_offset, hovered_offset, ) { @@ -1901,9 +1920,31 @@ fn update_inlay_link_and_hover_points( dbg!(&hovered_hint_part.tooltip); // TODO kb // hover_at_point = Some(hovered_offset); } - if let Some(location) = &hovered_hint_part.location { - dbg!(location); // TODO kb - // go_to_definition_point = Some(location); + if let Some(location) = hovered_hint_part.location { + if let Some(buffer) = cached_hint + .position + .buffer_id + .and_then(|buffer_id| buffer.buffer(buffer_id)) + { + go_to_definition_updated = true; + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::InlayHint( + hovered_hint.position, + LocationLink { + origin: Some(Location { + buffer, + range: cached_hint.position + ..cached_hint.position, + }), + target: location, + }, + ), + cmd_held, + shift_held, + cx, + ); + } } } } @@ -1913,14 +1954,27 @@ fn update_inlay_link_and_hover_points( } } } + + if !go_to_definition_updated { + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::None, + cmd_held, + shift_held, + cx, + ); + } + if !hover_updated { + hover_at(editor, None, cx); + } } } -fn find_hovered_hint_part<'a>( - label_parts: &'a [InlayHintLabelPart], +fn find_hovered_hint_part( + label_parts: Vec, hint_range: Range, hovered_offset: InlayOffset, -) -> Option<&'a InlayHintLabelPart> { +) -> Option { if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { let mut hovered_character = (hovered_offset - hint_range.start).0; for part in label_parts { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 477eab41ac..30ed56af47 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -615,7 +615,7 @@ impl Item for Editor { fn workspace_deactivated(&mut self, cx: &mut ViewContext) { hide_link_definition(self, cx); - self.link_go_to_definition_state.last_mouse_location = None; + self.link_go_to_definition_state.last_trigger_point = None; } fn is_dirty(&self, cx: &AppContext) -> bool { diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 31df11a019..a001433727 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -7,16 +7,50 @@ use util::TryFutureExt; #[derive(Debug, Default)] pub struct LinkGoToDefinitionState { - pub last_mouse_location: Option, + pub last_trigger_point: Option, pub symbol_range: Option>, pub kind: Option, pub definitions: Vec, pub task: Option>>, } +pub enum GoToDefinitionTrigger { + Text(DisplayPoint), + InlayHint(Anchor, LocationLink), + None, +} + +#[derive(Debug, Clone)] +pub enum TriggerPoint { + Text(Anchor), + InlayHint(Anchor, LocationLink), +} + +impl TriggerPoint { + fn anchor(&self) -> &Anchor { + match self { + TriggerPoint::Text(anchor) => anchor, + TriggerPoint::InlayHint(anchor, _) => anchor, + } + } + + pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind { + match self { + TriggerPoint::Text(_) => { + if shift { + LinkDefinitionKind::Type + } else { + LinkDefinitionKind::Symbol + } + } + TriggerPoint::InlayHint(_, link) => LinkDefinitionKind::Type, + } + } +} + pub fn update_go_to_definition_link( editor: &mut Editor, - point: Option, + origin: GoToDefinitionTrigger, cmd_held: bool, shift_held: bool, cx: &mut ViewContext, @@ -25,23 +59,30 @@ pub fn update_go_to_definition_link( // Store new mouse point as an anchor let snapshot = editor.snapshot(cx); - let point = point.map(|point| { - snapshot - .buffer_snapshot - .anchor_before(point.to_offset(&snapshot.display_snapshot, Bias::Left)) - }); + let trigger_point = match origin { + GoToDefinitionTrigger::Text(p) => { + Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before( + p.to_offset(&snapshot.display_snapshot, Bias::Left), + ))) + } + GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerPoint::InlayHint(p, target)), + GoToDefinitionTrigger::None => None, + }; // If the new point is the same as the previously stored one, return early if let (Some(a), Some(b)) = ( - &point, - &editor.link_go_to_definition_state.last_mouse_location, + &trigger_point, + &editor.link_go_to_definition_state.last_trigger_point, ) { - if a.cmp(b, &snapshot.buffer_snapshot).is_eq() { + if a.anchor() + .cmp(b.anchor(), &snapshot.buffer_snapshot) + .is_eq() + { return; } } - editor.link_go_to_definition_state.last_mouse_location = point.clone(); + editor.link_go_to_definition_state.last_trigger_point = trigger_point.clone(); if pending_nonempty_selection { hide_link_definition(editor, cx); @@ -49,14 +90,9 @@ pub fn update_go_to_definition_link( } if cmd_held { - if let Some(point) = point { - let kind = if shift_held { - LinkDefinitionKind::Type - } else { - LinkDefinitionKind::Symbol - }; - - show_link_definition(kind, editor, point, snapshot, cx); + 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; } } @@ -73,7 +109,7 @@ pub enum LinkDefinitionKind { pub fn show_link_definition( definition_kind: LinkDefinitionKind, editor: &mut Editor, - trigger_point: Anchor, + trigger_point: TriggerPoint, snapshot: EditorSnapshot, cx: &mut ViewContext, ) { @@ -86,10 +122,11 @@ pub fn show_link_definition( return; } + let trigger_anchor = trigger_point.anchor().clone(); let (buffer, buffer_position) = if let Some(output) = editor .buffer .read(cx) - .text_anchor_for_position(trigger_point.clone(), cx) + .text_anchor_for_position(trigger_anchor.clone(), cx) { output } else { @@ -99,7 +136,7 @@ pub fn show_link_definition( let excerpt_id = if let Some((excerpt_id, _, _)) = editor .buffer() .read(cx) - .excerpt_containing(trigger_point.clone(), cx) + .excerpt_containing(trigger_anchor.clone(), cx) { excerpt_id } else { @@ -116,12 +153,12 @@ pub fn show_link_definition( if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range { let point_after_start = symbol_range .start - .cmp(&trigger_point, &snapshot.buffer_snapshot) + .cmp(&trigger_anchor, &snapshot.buffer_snapshot) .is_le(); let point_before_end = symbol_range .end - .cmp(&trigger_point, &snapshot.buffer_snapshot) + .cmp(&trigger_anchor, &snapshot.buffer_snapshot) .is_ge(); let point_within_range = point_after_start && point_before_end; @@ -132,34 +169,45 @@ pub fn show_link_definition( let task = cx.spawn(|this, mut cx| { async move { - // query the LSP for definition info - let definition_request = cx.update(|cx| { - project.update(cx, |project, cx| match definition_kind { - LinkDefinitionKind::Symbol => project.definition(&buffer, buffer_position, cx), + let result = match trigger_point { + TriggerPoint::Text(_) => { + // query the LSP for definition info + cx.update(|cx| { + project.update(cx, |project, cx| match definition_kind { + LinkDefinitionKind::Symbol => { + project.definition(&buffer, buffer_position, cx) + } - LinkDefinitionKind::Type => { - project.type_definition(&buffer, buffer_position, cx) - } - }) - }); - - let result = definition_request.await.ok().map(|definition_result| { - ( - 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); - - start..end + LinkDefinitionKind::Type => { + project.type_definition(&buffer, buffer_position, cx) + } }) - }), - definition_result, - ) - }); + }) + .await + .ok() + .map(|definition_result| { + ( + 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); + + start..end + }) + }), + definition_result, + ) + }) + } + TriggerPoint::InlayHint(trigger_source, trigger_target) => { + // TODO kb range is wrong, should be in inlay coordinates + Some((Some(trigger_source..trigger_source), vec![trigger_target])) + } + }; this.update(&mut cx, |this, cx| { // Clear any existing highlights @@ -202,7 +250,7 @@ pub fn show_link_definition( // If no symbol range returned from language server, use the surrounding word. let highlight_range = symbol_range.unwrap_or_else(|| { let snapshot = &snapshot.buffer_snapshot; - let (offset_range, _) = snapshot.surrounding_word(trigger_point); + let (offset_range, _) = snapshot.surrounding_word(trigger_anchor); snapshot.anchor_before(offset_range.start) ..snapshot.anchor_after(offset_range.end) @@ -355,7 +403,13 @@ mod tests { // Press cmd+shift to trigger highlight cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, true, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + true, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -461,7 +515,13 @@ mod tests { }); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -482,7 +542,7 @@ mod tests { "}); // Response without source range still highlights word - cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None); + cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_trigger_point = None); let mut requests = cx.handle_request::(move |url, _, _| async move { Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ lsp::LocationLink { @@ -495,7 +555,13 @@ mod tests { ]))) }); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -517,7 +583,13 @@ mod tests { Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) }); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -534,7 +606,13 @@ mod tests { fn do_work() { teˇst(); } "}); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), false, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + false, + false, + cx, + ); }); cx.foreground().run_until_parked(); @@ -593,7 +671,13 @@ mod tests { // Moving the mouse restores the highlights. cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); cx.foreground().run_until_parked(); cx.assert_editor_text_highlights::(indoc! {" @@ -607,7 +691,13 @@ mod tests { fn do_work() { tesˇt(); } "}); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); cx.foreground().run_until_parked(); cx.assert_editor_text_highlights::(indoc! {" @@ -703,7 +793,13 @@ mod tests { }); }); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); cx.foreground().run_until_parked(); assert!(requests.try_next().is_err()); From 477fc865f5b6f2b1bf43cb4350f49eced80e4e29 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 21 Aug 2023 20:21:38 +0300 Subject: [PATCH 08/24] Properly resolve inlay label parts' locations and buffers --- crates/editor/src/display_map/inlay_map.rs | 1 + crates/editor/src/element.rs | 33 +-- crates/editor/src/link_go_to_definition.rs | 43 ++-- crates/project/src/lsp_command.rs | 256 ++++++++++++++------- crates/project/src/project.rs | 21 +- 5 files changed, 222 insertions(+), 132 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 026f1fc2c2..34181a5769 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1010,6 +1010,7 @@ impl InlaySnapshot { }) { Ok(i) | Err(i) => i, }; + // TODO kb add a way to highlight inlay hints through here. for range in &ranges[start_ix..] { if range.start.cmp(&transform_end, &self.buffer).is_ge() { break; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 15b28914af..c28f14d98a 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -358,18 +358,15 @@ impl EditorElement { } if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) { - if let Some(point) = position_map - .point_for_position(text_bounds, position) - .as_valid() - { - if shift { - go_to_fetched_type_definition(editor, point, alt, cx); - } else { - go_to_fetched_definition(editor, point, alt, cx); - } - - return true; + let point = position_map.point_for_position(text_bounds, position); + let could_be_inlay = point.as_valid().is_none(); + if shift || could_be_inlay { + go_to_fetched_type_definition(editor, point, alt, cx); + } else { + go_to_fetched_definition(editor, point, alt, cx); } + + return true; } end_selection @@ -2818,14 +2815,24 @@ struct PositionMap { } #[derive(Debug)] -struct PointForPosition { +pub struct PointForPosition { previous_valid: DisplayPoint, - next_valid: DisplayPoint, + pub next_valid: DisplayPoint, exact_unclipped: DisplayPoint, column_overshoot_after_line_end: u32, } 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, + } + } + fn as_valid(&self) -> Option { if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped { Some(self.previous_valid) diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index a001433727..b043af6132 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1,4 +1,4 @@ -use crate::{Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase}; +use crate::{element::PointForPosition, Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase}; use gpui::{Task, ViewContext}; use language::{Bias, ToOffset}; use project::LocationLink; @@ -7,7 +7,7 @@ use util::TryFutureExt; #[derive(Debug, Default)] pub struct LinkGoToDefinitionState { - pub last_trigger_point: Option, + pub last_trigger_point: Option, pub symbol_range: Option>, pub kind: Option, pub definitions: Vec, @@ -21,29 +21,29 @@ pub enum GoToDefinitionTrigger { } #[derive(Debug, Clone)] -pub enum TriggerPoint { +pub enum TriggerAnchor { Text(Anchor), InlayHint(Anchor, LocationLink), } -impl TriggerPoint { +impl TriggerAnchor { fn anchor(&self) -> &Anchor { match self { - TriggerPoint::Text(anchor) => anchor, - TriggerPoint::InlayHint(anchor, _) => anchor, + TriggerAnchor::Text(anchor) => anchor, + TriggerAnchor::InlayHint(anchor, _) => anchor, } } pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind { match self { - TriggerPoint::Text(_) => { + TriggerAnchor::Text(_) => { if shift { LinkDefinitionKind::Type } else { LinkDefinitionKind::Symbol } } - TriggerPoint::InlayHint(_, link) => LinkDefinitionKind::Type, + TriggerAnchor::InlayHint(_, _) => LinkDefinitionKind::Type, } } } @@ -61,11 +61,11 @@ pub fn update_go_to_definition_link( let snapshot = editor.snapshot(cx); let trigger_point = match origin { GoToDefinitionTrigger::Text(p) => { - Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before( + Some(TriggerAnchor::Text(snapshot.buffer_snapshot.anchor_before( p.to_offset(&snapshot.display_snapshot, Bias::Left), ))) } - GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerPoint::InlayHint(p, target)), + GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerAnchor::InlayHint(p, target)), GoToDefinitionTrigger::None => None, }; @@ -109,7 +109,7 @@ pub enum LinkDefinitionKind { pub fn show_link_definition( definition_kind: LinkDefinitionKind, editor: &mut Editor, - trigger_point: TriggerPoint, + trigger_point: TriggerAnchor, snapshot: EditorSnapshot, cx: &mut ViewContext, ) { @@ -170,7 +170,7 @@ pub fn show_link_definition( let task = cx.spawn(|this, mut cx| { async move { let result = match trigger_point { - TriggerPoint::Text(_) => { + TriggerAnchor::Text(_) => { // query the LSP for definition info cx.update(|cx| { project.update(cx, |project, cx| match definition_kind { @@ -203,8 +203,9 @@ pub fn show_link_definition( ) }) } - TriggerPoint::InlayHint(trigger_source, trigger_target) => { - // TODO kb range is wrong, should be in inlay coordinates + TriggerAnchor::InlayHint(trigger_source, trigger_target) => { + // TODO kb range is wrong, should be in inlay coordinates have a proper inlay range. + // Or highlight inlays differently, in their layer? Some((Some(trigger_source..trigger_source), vec![trigger_target])) } }; @@ -293,7 +294,7 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext) { pub fn go_to_fetched_definition( editor: &mut Editor, - point: DisplayPoint, + point: PointForPosition, split: bool, cx: &mut ViewContext, ) { @@ -302,7 +303,7 @@ pub fn go_to_fetched_definition( pub fn go_to_fetched_type_definition( editor: &mut Editor, - point: DisplayPoint, + point: PointForPosition, split: bool, cx: &mut ViewContext, ) { @@ -312,7 +313,7 @@ pub fn go_to_fetched_type_definition( fn go_to_fetched_definition_of_kind( kind: LinkDefinitionKind, editor: &mut Editor, - point: DisplayPoint, + point: PointForPosition, split: bool, cx: &mut ViewContext, ) { @@ -330,7 +331,7 @@ fn go_to_fetched_definition_of_kind( } else { editor.select( SelectPhase::Begin { - position: point, + position: point.next_valid, add: false, click_count: 1, }, @@ -460,7 +461,7 @@ mod tests { }); cx.update_editor(|editor, cx| { - go_to_fetched_type_definition(editor, hover_point, false, cx); + go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx); }); requests.next().await; cx.foreground().run_until_parked(); @@ -707,7 +708,7 @@ mod tests { // Cmd click with existing definition doesn't re-request and dismisses highlight cx.update_editor(|editor, cx| { - go_to_fetched_definition(editor, hover_point, false, cx); + go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx); }); // Assert selection moved to to definition cx.lsp @@ -748,7 +749,7 @@ mod tests { ]))) }); cx.update_editor(|editor, cx| { - go_to_fetched_definition(editor, hover_point, false, cx); + go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx); }); requests.next().await; cx.foreground().run_until_parked(); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 7b4d689a81..20bb302b5b 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -7,14 +7,15 @@ use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use client::proto::{self, PeerId}; use fs::LineEnding; +use futures::future; use gpui::{AppContext, AsyncAppContext, ModelHandle}; use language::{ language_settings::{language_settings, InlayHintKind}, point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, - CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, - Unclipped, + CodeAction, Completion, LanguageServerName, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, + Transaction, Unclipped, }; use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; @@ -1432,7 +1433,7 @@ impl LspCommand for GetCompletions { }) }); - Ok(futures::future::join_all(completions).await) + Ok(future::join_all(completions).await) } fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCompletions { @@ -1500,7 +1501,7 @@ impl LspCommand for GetCompletions { let completions = message.completions.into_iter().map(|completion| { language::proto::deserialize_completion(completion, language.clone()) }); - futures::future::try_join_all(completions).await + future::try_join_all(completions).await } fn buffer_id_from_proto(message: &proto::GetCompletions) -> u64 { @@ -1778,73 +1779,50 @@ impl LspCommand for OnTypeFormatting { } impl InlayHints { - pub fn lsp_to_project_hint( + pub async fn lsp_to_project_hint( lsp_hint: lsp::InlayHint, + project: &ModelHandle, buffer_handle: &ModelHandle, + server_id: LanguageServerId, resolve_state: ResolveState, force_no_type_left_padding: bool, - cx: &AppContext, - ) -> InlayHint { + cx: &mut AsyncAppContext, + ) -> anyhow::Result { let kind = lsp_hint.kind.and_then(|kind| match kind { lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), _ => None, }); - let buffer = buffer_handle.read(cx); - let position = buffer.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); + + let position = cx.update(|cx| { + let buffer = buffer_handle.read(cx); + let position = buffer.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); + if kind == Some(InlayHintKind::Parameter) { + buffer.anchor_before(position) + } else { + buffer.anchor_after(position) + } + }); + let label = Self::lsp_inlay_label_to_project( + &buffer_handle, + project, + server_id, + lsp_hint.label, + cx, + ) + .await + .context("lsp to project inlay hint conversion")?; let padding_left = if force_no_type_left_padding && kind == Some(InlayHintKind::Type) { false } else { lsp_hint.padding_left.unwrap_or(false) }; - InlayHint { - position: if kind == Some(InlayHintKind::Parameter) { - buffer.anchor_before(position) - } else { - buffer.anchor_after(position) - }, + + Ok(InlayHint { + position, padding_left, padding_right: lsp_hint.padding_right.unwrap_or(false), - label: match lsp_hint.label { - lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), - lsp::InlayHintLabel::LabelParts(lsp_parts) => InlayHintLabel::LabelParts( - lsp_parts - .into_iter() - .map(|label_part| InlayHintLabelPart { - value: label_part.value, - tooltip: label_part.tooltip.map(|tooltip| match tooltip { - lsp::InlayHintLabelPartTooltip::String(s) => { - InlayHintLabelPartTooltip::String(s) - } - lsp::InlayHintLabelPartTooltip::MarkupContent(markup_content) => { - InlayHintLabelPartTooltip::MarkupContent(MarkupContent { - kind: match markup_content.kind { - lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, - lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, - }, - value: markup_content.value, - }) - } - }), - location: label_part.location.map(|lsp_location| { - let target_start = buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.start), - Bias::Left, - ); - let target_end = buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.end), - Bias::Left, - ); - Location { - buffer: buffer_handle.clone(), - range: buffer.anchor_after(target_start) - ..buffer.anchor_before(target_end), - } - }), - }) - .collect(), - ), - }, + label, kind, tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip { lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s), @@ -1859,7 +1837,100 @@ impl InlayHints { } }), resolve_state, - } + }) + } + + async fn lsp_inlay_label_to_project( + buffer: &ModelHandle, + project: &ModelHandle, + server_id: LanguageServerId, + lsp_label: lsp::InlayHintLabel, + cx: &mut AsyncAppContext, + ) -> anyhow::Result { + let label = match lsp_label { + lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), + lsp::InlayHintLabel::LabelParts(lsp_parts) => { + let mut parts_data = Vec::with_capacity(lsp_parts.len()); + buffer.update(cx, |buffer, cx| { + for lsp_part in lsp_parts { + let location_buffer_task = match &lsp_part.location { + Some(lsp_location) => { + let location_buffer_task = project.update(cx, |project, cx| { + let language_server_name = project + .language_server_for_buffer(buffer, server_id, cx) + .map(|(_, lsp_adapter)| { + LanguageServerName(Arc::from(lsp_adapter.name())) + }); + language_server_name.map(|language_server_name| { + project.open_local_buffer_via_lsp( + lsp_location.uri.clone(), + server_id, + language_server_name, + cx, + ) + }) + }); + Some(lsp_location.clone()).zip(location_buffer_task) + } + None => None, + }; + + parts_data.push((lsp_part, location_buffer_task)); + } + }); + + let mut parts = Vec::with_capacity(parts_data.len()); + for (lsp_part, location_buffer_task) in parts_data { + let location = match location_buffer_task { + Some((lsp_location, target_buffer_handle_task)) => { + let target_buffer_handle = target_buffer_handle_task + .await + .context("resolving location for label part buffer")?; + let range = cx.read(|cx| { + let target_buffer = target_buffer_handle.read(cx); + let target_start = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.start), + Bias::Left, + ); + let target_end = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.end), + Bias::Left, + ); + target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end) + }); + Some(Location { + buffer: target_buffer_handle, + range, + }) + } + None => None, + }; + + parts.push(InlayHintLabelPart { + value: lsp_part.value, + tooltip: lsp_part.tooltip.map(|tooltip| match tooltip { + lsp::InlayHintLabelPartTooltip::String(s) => { + InlayHintLabelPartTooltip::String(s) + } + lsp::InlayHintLabelPartTooltip::MarkupContent(markup_content) => { + InlayHintLabelPartTooltip::MarkupContent(MarkupContent { + kind: match markup_content.kind { + lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, + lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, + }, + value: markup_content.value, + }) + } + }), + location, + }); + } + InlayHintLabel::LabelParts(parts) + } + }; + + Ok(label) } pub fn project_to_proto_hint(response_hint: InlayHint, cx: &AppContext) -> proto::InlayHint { @@ -2115,15 +2186,18 @@ impl InlayHints { }) }), location: part.location.and_then(|location| { - let path = cx.read(|cx| { - let project_path = location.buffer.read(cx).project_path(cx)?; - project.read(cx).absolute_path(&project_path, cx) + let (path, location_snapshot) = cx.read(|cx| { + let buffer = location.buffer.read(cx); + let project_path = buffer.project_path(cx)?; + let location_snapshot = buffer.snapshot(); + let path = project.read(cx).absolute_path(&project_path, cx); + path.zip(Some(location_snapshot)) })?; Some(lsp::Location::new( lsp::Url::from_file_path(path).unwrap(), range_to_lsp( - location.range.start.to_point_utf16(snapshot) - ..location.range.end.to_point_utf16(snapshot), + location.range.start.to_point_utf16(&location_snapshot) + ..location.range.end.to_point_utf16(&location_snapshot), ), )) }), @@ -2182,7 +2256,7 @@ impl LspCommand for InlayHints { buffer: ModelHandle, server_id: LanguageServerId, mut cx: AsyncAppContext, - ) -> Result> { + ) -> anyhow::Result> { let (lsp_adapter, lsp_server) = language_server_for_buffer(&project, &buffer, server_id, &mut cx)?; // `typescript-language-server` adds padding to the left for type hints, turning @@ -2194,32 +2268,38 @@ impl LspCommand for InlayHints { // Hence let's use a heuristic first to handle the most awkward case and look for more. let force_no_type_left_padding = lsp_adapter.name.0.as_ref() == "typescript-language-server"; - cx.read(|cx| { - Ok(message - .unwrap_or_default() - .into_iter() - .map(|lsp_hint| { - let resolve_state = match lsp_server.capabilities().inlay_hint_provider { - Some(lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options( - lsp::InlayHintOptions { - resolve_provider: Some(true), - .. - }, - ))) => { - ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()) - } - _ => ResolveState::Resolved, - }; - InlayHints::lsp_to_project_hint( - lsp_hint, - &buffer, - resolve_state, - force_no_type_left_padding, - cx, - ) - }) - .collect()) - }) + + let hints = message.unwrap_or_default().into_iter().map(|lsp_hint| { + let resolve_state = match lsp_server.capabilities().inlay_hint_provider { + Some(lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options( + lsp::InlayHintOptions { + resolve_provider: Some(true), + .. + }, + ))) => ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()), + _ => ResolveState::Resolved, + }; + + let project = project.clone(); + let buffer = buffer.clone(); + cx.spawn(|mut cx| async move { + InlayHints::lsp_to_project_hint( + lsp_hint, + &project, + &buffer, + server_id, + resolve_state, + force_no_type_left_padding, + &mut cx, + ) + .await + }) + }); + future::join_all(hints) + .await + .into_iter() + .collect::>() + .context("lsp to project inlay hints conversion") } fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::InlayHints { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 65be7cceb1..fbdbd04664 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5054,22 +5054,23 @@ impl Project { } let buffer_snapshot = buffer.snapshot(); - cx.spawn(|project, cx| async move { + cx.spawn(|project, mut cx| async move { let resolve_task = lang_server.request::( InlayHints::project_to_lsp_hint(hint, &project, &buffer_snapshot, &cx), ); let resolved_hint = resolve_task .await .context("inlay hint resolve LSP request")?; - let resolved_hint = cx.read(|cx| { - InlayHints::lsp_to_project_hint( - resolved_hint, - &buffer_handle, - ResolveState::Resolved, - false, - cx, - ) - }); + let resolved_hint = InlayHints::lsp_to_project_hint( + resolved_hint, + &project, + &buffer_handle, + server_id, + ResolveState::Resolved, + false, + &mut cx, + ) + .await?; Ok(Some(resolved_hint)) }) } else if let Some(project_id) = self.remote_id() { From 6c5761d05bccadfde383281560ad15e14f452c24 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 21 Aug 2023 23:21:37 +0300 Subject: [PATCH 09/24] Pass inlay highlight information --- crates/editor/src/display_map.rs | 39 +++++- crates/editor/src/display_map/block_map.rs | 14 +- crates/editor/src/display_map/fold_map.rs | 27 ++-- crates/editor/src/display_map/inlay_map.rs | 109 ++++++++-------- crates/editor/src/display_map/tab_map.rs | 36 ++++-- crates/editor/src/display_map/wrap_map.rs | 16 ++- crates/editor/src/editor.rs | 31 ++++- crates/editor/src/element.rs | 9 +- crates/editor/src/link_go_to_definition.rs | 143 +++++++++++++-------- 9 files changed, 279 insertions(+), 145 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 9159253142..9df2919351 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -4,7 +4,10 @@ mod inlay_map; mod tab_map; mod wrap_map; -use crate::{Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint}; +use crate::{ + link_go_to_definition::InlayCoordinates, Anchor, AnchorRangeExt, InlayId, MultiBuffer, + MultiBufferSnapshot, ToOffset, ToPoint, +}; pub use block_map::{BlockMap, BlockPoint}; use collections::{HashMap, HashSet}; use fold_map::FoldMap; @@ -40,6 +43,7 @@ pub trait ToDisplayPoint { } type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec>)>>; +type InlayHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; pub struct DisplayMap { buffer: ModelHandle, @@ -50,6 +54,7 @@ pub struct DisplayMap { wrap_map: ModelHandle, block_map: BlockMap, text_highlights: TextHighlights, + inlay_highlights: InlayHighlights, pub clip_at_line_ends: bool, } @@ -85,6 +90,7 @@ impl DisplayMap { wrap_map, block_map, text_highlights: Default::default(), + inlay_highlights: Default::default(), clip_at_line_ends: false, } } @@ -109,6 +115,7 @@ impl DisplayMap { wrap_snapshot, block_snapshot, text_highlights: self.text_highlights.clone(), + inlay_highlights: self.inlay_highlights.clone(), clip_at_line_ends: self.clip_at_line_ends, } } @@ -215,6 +222,16 @@ impl DisplayMap { .insert(Some(type_id), Arc::new((style, ranges))); } + pub fn highlight_inlays( + &mut self, + type_id: TypeId, + ranges: Vec, + style: HighlightStyle, + ) { + self.inlay_highlights + .insert(Some(type_id), Arc::new((style, ranges))); + } + pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range])> { let highlights = self.text_highlights.get(&Some(type_id))?; Some((highlights.0, &highlights.1)) @@ -227,6 +244,13 @@ impl DisplayMap { self.text_highlights.remove(&Some(type_id)) } + pub fn clear_inlay_highlights( + &mut self, + type_id: TypeId, + ) -> Option)>> { + self.inlay_highlights.remove(&Some(type_id)) + } + pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext) -> bool { self.wrap_map .update(cx, |map, cx| map.set_font(font_id, font_size, cx)) @@ -296,6 +320,7 @@ pub struct DisplaySnapshot { wrap_snapshot: wrap_map::WrapSnapshot, block_snapshot: block_map::BlockSnapshot, text_highlights: TextHighlights, + inlay_highlights: InlayHighlights, clip_at_line_ends: bool, } @@ -421,6 +446,7 @@ impl DisplaySnapshot { None, None, None, + None, ) .map(|h| h.text) } @@ -429,7 +455,7 @@ impl DisplaySnapshot { pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator { (0..=display_row).into_iter().rev().flat_map(|row| { self.block_snapshot - .chunks(row..row + 1, false, None, None, None) + .chunks(row..row + 1, false, None, None, None, None) .map(|h| h.text) .collect::>() .into_iter() @@ -441,15 +467,16 @@ impl DisplaySnapshot { &self, display_rows: Range, language_aware: bool, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> DisplayChunks<'_> { self.block_snapshot.chunks( display_rows, language_aware, Some(&self.text_highlights), - hint_highlights, - suggestion_highlights, + Some(&self.inlay_highlights), + inlay_highlight_style, + suggestion_highlight_style, ) } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 4b76ded3d5..083d8be5f3 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -1,6 +1,6 @@ use super::{ wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}, - TextHighlights, + InlayHighlights, TextHighlights, }; use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _}; use collections::{Bound, HashMap, HashSet}; @@ -579,6 +579,7 @@ impl BlockSnapshot { None, None, None, + None, ) .map(|chunk| chunk.text) .collect() @@ -589,8 +590,9 @@ impl BlockSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlights: Option<&'a InlayHighlights>, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> BlockChunks<'a> { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); @@ -623,8 +625,9 @@ impl BlockSnapshot { input_start..input_end, language_aware, text_highlights, - hint_highlights, - suggestion_highlights, + inlay_highlights, + inlay_highlight_style, + suggestion_highlight_style, ), input_chunk: Default::default(), transforms: cursor, @@ -1504,6 +1507,7 @@ mod tests { None, None, None, + None, ) .map(|chunk| chunk.text) .collect::(); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 0b1523fe75..800b51fcd8 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1,6 +1,6 @@ use super::{ inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot}, - TextHighlights, + InlayHighlights, TextHighlights, }; use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset}; use gpui::{color::Color, fonts::HighlightStyle}; @@ -475,7 +475,7 @@ pub struct FoldSnapshot { impl FoldSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(FoldOffset(0)..self.len(), false, None, None, None) + self.chunks(FoldOffset(0)..self.len(), false, None, None, None, None) .map(|c| c.text) .collect() } @@ -652,8 +652,9 @@ impl FoldSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlights: Option<&'a InlayHighlights>, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> FoldChunks<'a> { let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>(); @@ -675,8 +676,9 @@ impl FoldSnapshot { inlay_start..inlay_end, language_aware, text_highlights, - hint_highlights, - suggestion_highlights, + inlay_highlights, + inlay_highlight_style, + suggestion_highlight_style, ), inlay_chunk: None, inlay_offset: inlay_start, @@ -687,8 +689,15 @@ impl FoldSnapshot { } pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator { - self.chunks(start.to_offset(self)..self.len(), false, None, None, None) - .flat_map(|chunk| chunk.text.chars()) + self.chunks( + start.to_offset(self)..self.len(), + false, + None, + None, + None, + None, + ) + .flat_map(|chunk| chunk.text.chars()) } #[cfg(test)] @@ -1496,7 +1505,7 @@ mod tests { let text = &expected_text[start.0..end.0]; assert_eq!( snapshot - .chunks(start..end, false, None, None, None) + .chunks(start..end, false, None, None, None, None) .map(|c| c.text) .collect::(), text, diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 34181a5769..5617b6a6b6 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -15,7 +15,7 @@ use std::{ use sum_tree::{Bias, Cursor, SumTree}; use text::{Patch, Rope}; -use super::TextHighlights; +use super::{InlayHighlights, TextHighlights}; pub struct InlayMap { snapshot: InlaySnapshot, @@ -973,8 +973,9 @@ impl InlaySnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlights: Option<&'a InlayHighlights>, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> InlayChunks<'a> { let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(); cursor.seek(&range.start, Bias::Right, &()); @@ -983,53 +984,50 @@ impl InlaySnapshot { if let Some(text_highlights) = text_highlights { if !text_highlights.is_empty() { while cursor.start().0 < range.end { - if true { - let transform_start = self.buffer.anchor_after( - self.to_buffer_offset(cmp::max(range.start, cursor.start().0)), - ); + let transform_start = self.buffer.anchor_after( + self.to_buffer_offset(cmp::max(range.start, cursor.start().0)), + ); - let transform_end = { - let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); - self.buffer.anchor_before(self.to_buffer_offset(cmp::min( - cursor.end(&()).0, - cursor.start().0 + overshoot, - ))) - }; + let transform_end = { + let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); + self.buffer.anchor_before(self.to_buffer_offset(cmp::min( + cursor.end(&()).0, + cursor.start().0 + overshoot, + ))) + }; - for (tag, highlights) in text_highlights.iter() { - let style = highlights.0; - let ranges = &highlights.1; + for (tag, highlights) in text_highlights.iter() { + let style = highlights.0; + let ranges = &highlights.1; - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&transform_start, &self.buffer); - if cmp.is_gt() { - cmp::Ordering::Greater - } else { - cmp::Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - // TODO kb add a way to highlight inlay hints through here. - for range in &ranges[start_ix..] { - if range.start.cmp(&transform_end, &self.buffer).is_ge() { - break; - } - - highlight_endpoints.push(HighlightEndpoint { - offset: self - .to_inlay_offset(range.start.to_offset(&self.buffer)), - is_start: true, - tag: *tag, - style, - }); - highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), - is_start: false, - tag: *tag, - style, - }); + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&transform_start, &self.buffer); + if cmp.is_gt() { + cmp::Ordering::Greater + } else { + cmp::Ordering::Less } + }) { + Ok(i) | Err(i) => i, + }; + // TODO kb add a way to highlight inlay hints through here. + for range in &ranges[start_ix..] { + if range.start.cmp(&transform_end, &self.buffer).is_ge() { + break; + } + + highlight_endpoints.push(HighlightEndpoint { + offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)), + is_start: true, + tag: *tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), + is_start: false, + tag: *tag, + style, + }); } } @@ -1050,8 +1048,8 @@ impl InlaySnapshot { buffer_chunk: None, output_offset: range.start, max_output_offset: range.end, - hint_highlight_style: hint_highlights, - suggestion_highlight_style: suggestion_highlights, + hint_highlight_style: inlay_highlight_style, + suggestion_highlight_style, highlight_endpoints: highlight_endpoints.into_iter().peekable(), active_highlights: Default::default(), snapshot: self, @@ -1060,9 +1058,16 @@ impl InlaySnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(Default::default()..self.len(), false, None, None, None) - .map(|chunk| chunk.text) - .collect() + self.chunks( + Default::default()..self.len(), + false, + None, + None, + None, + None, + ) + .map(|chunk| chunk.text) + .collect() } fn check_invariants(&self) { @@ -1636,6 +1641,8 @@ mod tests { InlayOffset(start)..InlayOffset(end), false, Some(&highlights), + // TODO kb add tests + None, None, None, ) diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index ca73f6a1a7..187a8de1d3 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -1,6 +1,6 @@ use super::{ fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot}, - TextHighlights, + InlayHighlights, TextHighlights, }; use crate::MultiBufferSnapshot; use gpui::fonts::HighlightStyle; @@ -71,6 +71,7 @@ impl TabMap { None, None, None, + None, ) { for (ix, _) in chunk.text.match_indices('\t') { let offset_from_edit = offset_from_edit + (ix as u32); @@ -183,7 +184,7 @@ impl TabSnapshot { self.max_point() }; for c in self - .chunks(range.start..line_end, false, None, None, None) + .chunks(range.start..line_end, false, None, None, None, None) .flat_map(|chunk| chunk.text.chars()) { if c == '\n' { @@ -203,6 +204,7 @@ impl TabSnapshot { None, None, None, + None, ) .flat_map(|chunk| chunk.text.chars()) { @@ -223,9 +225,11 @@ impl TabSnapshot { &'a self, range: Range, language_aware: bool, + // TODO kb extract into one struct text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlights: Option<&'a InlayHighlights>, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> TabChunks<'a> { let (input_start, expanded_char_column, to_next_stop) = self.to_fold_point(range.start, Bias::Left); @@ -246,8 +250,9 @@ impl TabSnapshot { input_start..input_end, language_aware, text_highlights, - hint_highlights, - suggestion_highlights, + inlay_highlights, + inlay_highlight_style, + suggestion_highlight_style, ), input_column, column: expanded_char_column, @@ -270,9 +275,16 @@ impl TabSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(TabPoint::zero()..self.max_point(), false, None, None, None) - .map(|chunk| chunk.text) - .collect() + self.chunks( + TabPoint::zero()..self.max_point(), + false, + None, + None, + None, + None, + ) + .map(|chunk| chunk.text) + .collect() } pub fn max_point(&self) -> TabPoint { @@ -600,6 +612,7 @@ mod tests { None, None, None, + None, ) .map(|c| c.text) .collect::(), @@ -674,7 +687,8 @@ mod tests { let mut chunks = Vec::new(); let mut was_tab = false; let mut text = String::new(); - for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None) { + for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None, None) + { if chunk.is_tab != was_tab { if !text.is_empty() { chunks.push((mem::take(&mut text), was_tab)); @@ -743,7 +757,7 @@ mod tests { let expected_summary = TextSummary::from(expected_text.as_str()); assert_eq!( tabs_snapshot - .chunks(start..end, false, None, None, None) + .chunks(start..end, false, None, None, None, None) .map(|c| c.text) .collect::(), expected_text, diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index f21c7151ad..f8287af799 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1,7 +1,7 @@ use super::{ fold_map::FoldBufferRows, tab_map::{self, TabEdit, TabPoint, TabSnapshot}, - TextHighlights, + InlayHighlights, TextHighlights, }; use crate::MultiBufferSnapshot; use gpui::{ @@ -447,6 +447,7 @@ impl WrapSnapshot { None, None, None, + None, ); let mut edit_transforms = Vec::::new(); for _ in edit.new_rows.start..edit.new_rows.end { @@ -576,8 +577,9 @@ impl WrapSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlights: Option<&'a InlayHighlights>, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> WrapChunks<'a> { let output_start = WrapPoint::new(rows.start, 0); let output_end = WrapPoint::new(rows.end, 0); @@ -595,8 +597,9 @@ impl WrapSnapshot { input_start..input_end, language_aware, text_highlights, - hint_highlights, - suggestion_highlights, + inlay_highlights, + inlay_highlight_style, + suggestion_highlight_style, ), input_chunk: Default::default(), output_position: output_start, @@ -1326,6 +1329,7 @@ mod tests { None, None, None, + None, ) .map(|h| h.text) } @@ -1350,7 +1354,7 @@ mod tests { } let actual_text = self - .chunks(start_row..end_row, true, None, None, None) + .chunks(start_row..end_row, true, None, None, None, None) .map(|c| c.text) .collect::(); assert_eq!( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index fb1e87580a..f10734a76c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -64,7 +64,9 @@ use language::{ Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, }; -use link_go_to_definition::{hide_link_definition, show_link_definition, LinkGoToDefinitionState}; +use link_go_to_definition::{ + hide_link_definition, show_link_definition, InlayCoordinates, LinkGoToDefinitionState, +}; use log::error; use multi_buffer::ToOffsetUtf16; pub use multi_buffer::{ @@ -7716,6 +7718,18 @@ impl Editor { cx.notify(); } + pub fn highlight_inlays( + &mut self, + ranges: Vec, + style: HighlightStyle, + cx: &mut ViewContext, + ) { + self.display_map.update(cx, |map, _| { + map.highlight_inlays(TypeId::of::(), ranges, style) + }); + cx.notify(); + } + pub fn text_highlights<'a, T: 'static>( &'a self, cx: &'a AppContext, @@ -7736,6 +7750,19 @@ impl Editor { highlights } + pub fn clear_highlights( + &mut self, + cx: &mut ViewContext, + ) -> Option>)>> { + let highlights = self + .display_map + .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); + if highlights.is_some() { + cx.notify(); + } + highlights + } + pub fn show_local_cursors(&self, cx: &AppContext) -> bool { self.blink_manager.read(cx).visible() && self.focused } @@ -8327,7 +8354,7 @@ impl View for Editor { self.link_go_to_definition_state.task = None; - self.clear_text_highlights::(cx); + self.clear_highlights::(cx); } false diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c28f14d98a..c448ba4f22 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -13,7 +13,7 @@ use crate::{ }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, - GoToDefinitionTrigger, + GoToDefinitionTrigger, InlayCoordinates, }, mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, }; @@ -1927,7 +1927,12 @@ fn update_inlay_link_and_hover_points( update_go_to_definition_link( editor, GoToDefinitionTrigger::InlayHint( - hovered_hint.position, + InlayCoordinates { + inlay_id: hovered_hint.id, + inlay_position: hovered_hint.position, + inlay_start: hint_start_offset, + highlight_end: hovered_offset, + }, LocationLink { origin: Some(Location { buffer, diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index b043af6132..35e367497e 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1,4 +1,7 @@ -use crate::{element::PointForPosition, Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase}; +use crate::{ + display_map::InlayOffset, element::PointForPosition, Anchor, DisplayPoint, Editor, + EditorSnapshot, InlayId, SelectPhase, +}; use gpui::{Task, ViewContext}; use language::{Bias, ToOffset}; use project::LocationLink; @@ -7,8 +10,8 @@ use util::TryFutureExt; #[derive(Debug, Default)] pub struct LinkGoToDefinitionState { - pub last_trigger_point: Option, - pub symbol_range: Option>, + pub last_trigger_point: Option, + pub symbol_range: Option, pub kind: Option, pub definitions: Vec, pub task: Option>>, @@ -16,34 +19,65 @@ pub struct LinkGoToDefinitionState { pub enum GoToDefinitionTrigger { Text(DisplayPoint), - InlayHint(Anchor, LocationLink), + InlayHint(InlayCoordinates, LocationLink), None, } -#[derive(Debug, Clone)] -pub enum TriggerAnchor { - Text(Anchor), - InlayHint(Anchor, LocationLink), +#[derive(Debug, Clone, Copy)] +pub struct InlayCoordinates { + pub inlay_id: InlayId, + pub inlay_position: Anchor, + pub inlay_start: InlayOffset, + pub highlight_end: InlayOffset, } -impl TriggerAnchor { +#[derive(Debug, Clone)] +pub enum TriggerPoint { + Text(Anchor), + InlayHint(InlayCoordinates, LocationLink), +} + +#[derive(Debug, Clone)] +pub enum SymbolRange { + Text(Range), + Inlay(InlayCoordinates), +} + +impl SymbolRange { + fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool { + match (self, trigger_point) { + (SymbolRange::Text(range), TriggerPoint::Text(point)) => { + let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le(); + point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge() + } + (SymbolRange::Inlay(range), TriggerPoint::InlayHint(point, _)) => { + range.inlay_start.cmp(&point.highlight_end).is_le() + && range.highlight_end.cmp(&point.highlight_end).is_ge() + } + (SymbolRange::Inlay(_), TriggerPoint::Text(_)) + | (SymbolRange::Text(_), TriggerPoint::InlayHint(_, _)) => false, + } + } +} + +impl TriggerPoint { fn anchor(&self) -> &Anchor { match self { - TriggerAnchor::Text(anchor) => anchor, - TriggerAnchor::InlayHint(anchor, _) => anchor, + TriggerPoint::Text(anchor) => anchor, + TriggerPoint::InlayHint(coordinates, _) => &coordinates.inlay_position, } } pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind { match self { - TriggerAnchor::Text(_) => { + TriggerPoint::Text(_) => { if shift { LinkDefinitionKind::Type } else { LinkDefinitionKind::Symbol } } - TriggerAnchor::InlayHint(_, _) => LinkDefinitionKind::Type, + TriggerPoint::InlayHint(_, _) => LinkDefinitionKind::Type, } } } @@ -61,11 +95,11 @@ pub fn update_go_to_definition_link( let snapshot = editor.snapshot(cx); let trigger_point = match origin { GoToDefinitionTrigger::Text(p) => { - Some(TriggerAnchor::Text(snapshot.buffer_snapshot.anchor_before( + Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before( p.to_offset(&snapshot.display_snapshot, Bias::Left), ))) } - GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerAnchor::InlayHint(p, target)), + GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerPoint::InlayHint(p, target)), GoToDefinitionTrigger::None => None, }; @@ -109,7 +143,7 @@ pub enum LinkDefinitionKind { pub fn show_link_definition( definition_kind: LinkDefinitionKind, editor: &mut Editor, - trigger_point: TriggerAnchor, + trigger_point: TriggerPoint, snapshot: EditorSnapshot, cx: &mut ViewContext, ) { @@ -122,7 +156,7 @@ pub fn show_link_definition( return; } - let trigger_anchor = trigger_point.anchor().clone(); + let trigger_anchor = trigger_point.anchor(); let (buffer, buffer_position) = if let Some(output) = editor .buffer .read(cx) @@ -151,26 +185,15 @@ pub fn show_link_definition( // 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 { - let point_after_start = symbol_range - .start - .cmp(&trigger_anchor, &snapshot.buffer_snapshot) - .is_le(); - - let point_before_end = symbol_range - .end - .cmp(&trigger_anchor, &snapshot.buffer_snapshot) - .is_ge(); - - let point_within_range = point_after_start && point_before_end; - if point_within_range && same_kind { + if same_kind && symbol_range.point_within_range(&trigger_point, &snapshot) { return; } } let task = cx.spawn(|this, mut cx| { async move { - let result = match trigger_point { - TriggerAnchor::Text(_) => { + let result = match &trigger_point { + TriggerPoint::Text(_) => { // query the LSP for definition info cx.update(|cx| { project.update(cx, |project, cx| match definition_kind { @@ -196,23 +219,22 @@ pub fn show_link_definition( .buffer_snapshot .anchor_in_excerpt(excerpt_id.clone(), origin.range.end); - start..end + SymbolRange::Text(start..end) }) }), definition_result, ) }) } - TriggerAnchor::InlayHint(trigger_source, trigger_target) => { - // TODO kb range is wrong, should be in inlay coordinates have a proper inlay range. - // Or highlight inlays differently, in their layer? - Some((Some(trigger_source..trigger_source), vec![trigger_target])) - } + TriggerPoint::InlayHint(trigger_source, trigger_target) => Some(( + Some(SymbolRange::Inlay(trigger_source.clone())), + vec![trigger_target.clone()], + )), }; this.update(&mut cx, |this, cx| { // Clear any existing highlights - this.clear_text_highlights::(cx); + this.clear_highlights::(cx); this.link_go_to_definition_state.kind = Some(definition_kind); this.link_go_to_definition_state.symbol_range = result .as_ref() @@ -248,22 +270,37 @@ pub fn show_link_definition( }); if any_definition_does_not_contain_current_location { - // If no symbol range returned from language server, use the surrounding word. - let highlight_range = symbol_range.unwrap_or_else(|| { - let snapshot = &snapshot.buffer_snapshot; - let (offset_range, _) = snapshot.surrounding_word(trigger_anchor); - - snapshot.anchor_before(offset_range.start) - ..snapshot.anchor_after(offset_range.end) - }); - // Highlight symbol using theme link definition highlight style let style = theme::current(cx).editor.link_definition; - this.highlight_text::( - vec![highlight_range], - style, - cx, - ); + 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); + SymbolRange::Text( + snapshot.anchor_before(offset_range.start) + ..snapshot.anchor_after(offset_range.end), + ) + } + TriggerPoint::InlayHint(inlay_trigger, _) => { + SymbolRange::Inlay(inlay_trigger) + } + }); + + match highlight_range { + SymbolRange::Text(text_range) => this + .highlight_text::( + vec![text_range], + style, + cx, + ), + SymbolRange::Inlay(inlay_coordinates) => this + .highlight_inlays::( + vec![inlay_coordinates], + style, + cx, + ), + } } else { hide_link_definition(this, cx); } @@ -289,7 +326,7 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext) { editor.link_go_to_definition_state.task = None; - editor.clear_text_highlights::(cx); + editor.clear_highlights::(cx); } pub fn go_to_fetched_definition( From f8874a726cc4a3fcc09613d8faa7a215ad8dc915 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 22 Aug 2023 01:02:25 +0300 Subject: [PATCH 10/24] Attempt to highlight inlays --- crates/editor/src/display_map/inlay_map.rs | 168 +++++++++++++++------ crates/editor/src/editor.rs | 13 +- crates/editor/src/element.rs | 23 +-- crates/editor/src/link_go_to_definition.rs | 11 +- 4 files changed, 143 insertions(+), 72 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 5617b6a6b6..23b39ca5ab 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -2,7 +2,7 @@ use crate::{ multi_buffer::{MultiBufferChunks, MultiBufferRows}, Anchor, InlayId, MultiBufferSnapshot, ToOffset, }; -use collections::{BTreeMap, BTreeSet}; +use collections::{BTreeMap, BTreeSet, HashSet}; use gpui::fonts::HighlightStyle; use language::{Chunk, Edit, Point, TextSummary}; use std::{ @@ -183,7 +183,7 @@ pub struct InlayBufferRows<'a> { max_buffer_row: u32, } -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] struct HighlightEndpoint { offset: InlayOffset, is_start: bool, @@ -243,6 +243,7 @@ impl<'a> Iterator for InlayChunks<'a> { return None; } + // TODO kb highlights are not displayed still let mut next_highlight_endpoint = InlayOffset(usize::MAX); while let Some(endpoint) = self.highlight_endpoints.peek().copied() { if endpoint.offset <= self.output_offset { @@ -980,62 +981,89 @@ impl InlaySnapshot { let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(); cursor.seek(&range.start, Bias::Right, &()); + let empty_text_highlights = TextHighlights::default(); + let text_highlights = text_highlights.unwrap_or_else(|| &empty_text_highlights); + let empty_inlay_highlights = InlayHighlights::default(); + let inlay_highlights = inlay_highlights.unwrap_or_else(|| &empty_inlay_highlights); + let mut highlight_endpoints = Vec::new(); - if let Some(text_highlights) = text_highlights { - if !text_highlights.is_empty() { - while cursor.start().0 < range.end { - let transform_start = self.buffer.anchor_after( - self.to_buffer_offset(cmp::max(range.start, cursor.start().0)), - ); + if !text_highlights.is_empty() || !inlay_highlights.is_empty() { + while cursor.start().0 < range.end { + let transform_start = self + .buffer + .anchor_after(self.to_buffer_offset(cmp::max(range.start, cursor.start().0))); - let transform_end = { - let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); - self.buffer.anchor_before(self.to_buffer_offset(cmp::min( - cursor.end(&()).0, - cursor.start().0 + overshoot, - ))) - }; + let transform_end = { + let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); + self.buffer.anchor_before(self.to_buffer_offset(cmp::min( + cursor.end(&()).0, + cursor.start().0 + overshoot, + ))) + }; - for (tag, highlights) in text_highlights.iter() { - let style = highlights.0; - let ranges = &highlights.1; + let mut covered_tags = HashSet::default(); + for (tag, text_highlights) in text_highlights.iter() { + covered_tags.insert(*tag); + let style = text_highlights.0; + let ranges = &text_highlights.1; - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&transform_start, &self.buffer); - if cmp.is_gt() { - cmp::Ordering::Greater - } else { - cmp::Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - // TODO kb add a way to highlight inlay hints through here. - for range in &ranges[start_ix..] { - if range.start.cmp(&transform_end, &self.buffer).is_ge() { - break; - } - - highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)), - is_start: true, - tag: *tag, - style, - }); - highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), - is_start: false, - tag: *tag, - style, - }); + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&transform_start, &self.buffer); + if cmp.is_gt() { + cmp::Ordering::Greater + } else { + cmp::Ordering::Less } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range.start.cmp(&transform_end, &self.buffer).is_ge() { + break; + } + + highlight_endpoints.push(HighlightEndpoint { + offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)), + is_start: true, + tag: *tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), + is_start: false, + tag: *tag, + style, + }); } - cursor.next(&()); + if let Some(inlay_highlights) = inlay_highlights.get(tag) { + self.push_inlay_highlight_range( + inlay_highlights, + transform_start, + transform_end, + &mut highlight_endpoints, + tag, + ); + } } - highlight_endpoints.sort(); - cursor.seek(&range.start, Bias::Right, &()); + + for (tag, inlay_highlights) in inlay_highlights + .iter() + .filter(|(tag, _)| !covered_tags.contains(tag)) + { + self.push_inlay_highlight_range( + inlay_highlights, + transform_start, + transform_end, + &mut highlight_endpoints, + tag, + ); + } + + cursor.next(&()); } + highlight_endpoints.sort(); + cursor.seek(&range.start, Bias::Right, &()); } let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end); @@ -1056,6 +1084,48 @@ impl InlaySnapshot { } } + fn push_inlay_highlight_range( + &self, + inlay_highlights: &std::sync::Arc<( + HighlightStyle, + Vec, + )>, + transform_start: Anchor, + transform_end: Anchor, + highlight_endpoints: &mut Vec, + tag: &Option, + ) { + let style = inlay_highlights.0; + let ranges = &inlay_highlights.1; + let start_ix = match ranges + .binary_search_by(|probe| probe.inlay_position.cmp(&transform_start, &self.buffer)) + { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range + .inlay_position + .cmp(&transform_end, &self.buffer) + .is_ge() + { + break; + } + + highlight_endpoints.push(HighlightEndpoint { + offset: range.highlight_start, + is_start: true, + tag: *tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: range.highlight_end, + is_start: false, + tag: *tag, + style, + }); + } + } + #[cfg(test)] pub fn text(&self) -> String { self.chunks( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f10734a76c..6c5d1a45ac 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7750,17 +7750,16 @@ impl Editor { highlights } - pub fn clear_highlights( - &mut self, - cx: &mut ViewContext, - ) -> Option>)>> { - let highlights = self + pub fn clear_highlights(&mut self, cx: &mut ViewContext) { + let text_highlights = self .display_map .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); - if highlights.is_some() { + let inlay_highlights = self + .display_map + .update(cx, |map, _| map.clear_inlay_highlights(TypeId::of::())); + if text_highlights.is_some() || inlay_highlights.is_some() { cx.notify(); } - highlights } pub fn show_local_cursors(&self, cx: &AppContext) -> bool { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c448ba4f22..10809d8962 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1908,11 +1908,13 @@ fn update_inlay_link_and_hover_points( } } project::InlayHintLabel::LabelParts(label_parts) => { - if let Some(hovered_hint_part) = find_hovered_hint_part( - label_parts, - hint_start_offset..hint_end_offset, - hovered_offset, - ) { + if let Some((hovered_hint_part, part_range)) = + find_hovered_hint_part( + label_parts, + hint_start_offset..hint_end_offset, + hovered_offset, + ) + { if hovered_hint_part.tooltip.is_some() { dbg!(&hovered_hint_part.tooltip); // TODO kb // hover_at_point = Some(hovered_offset); @@ -1928,10 +1930,9 @@ fn update_inlay_link_and_hover_points( editor, GoToDefinitionTrigger::InlayHint( InlayCoordinates { - inlay_id: hovered_hint.id, inlay_position: hovered_hint.position, - inlay_start: hint_start_offset, - highlight_end: hovered_offset, + highlight_start: part_range.start, + highlight_end: part_range.end, }, LocationLink { origin: Some(Location { @@ -1976,15 +1977,17 @@ fn find_hovered_hint_part( label_parts: Vec, hint_range: Range, hovered_offset: InlayOffset, -) -> Option { +) -> Option<(InlayHintLabelPart, Range)> { if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { let mut hovered_character = (hovered_offset - hint_range.start).0; + let mut part_start = hint_range.start; for part in label_parts { let part_len = part.value.chars().count(); if hovered_character >= part_len { hovered_character -= part_len; + part_start.0 += part_len; } else { - return Some(part); + return Some((part, part_start..InlayOffset(part_start.0 + part_len))); } } } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 35e367497e..ee2b536042 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1,6 +1,6 @@ use crate::{ display_map::InlayOffset, element::PointForPosition, Anchor, DisplayPoint, Editor, - EditorSnapshot, InlayId, SelectPhase, + EditorSnapshot, SelectPhase, }; use gpui::{Task, ViewContext}; use language::{Bias, ToOffset}; @@ -25,9 +25,8 @@ pub enum GoToDefinitionTrigger { #[derive(Debug, Clone, Copy)] pub struct InlayCoordinates { - pub inlay_id: InlayId, pub inlay_position: Anchor, - pub inlay_start: InlayOffset, + pub highlight_start: InlayOffset, pub highlight_end: InlayOffset, } @@ -51,7 +50,7 @@ impl SymbolRange { point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge() } (SymbolRange::Inlay(range), TriggerPoint::InlayHint(point, _)) => { - range.inlay_start.cmp(&point.highlight_end).is_le() + range.highlight_start.cmp(&point.highlight_end).is_le() && range.highlight_end.cmp(&point.highlight_end).is_ge() } (SymbolRange::Inlay(_), TriggerPoint::Text(_)) @@ -282,8 +281,8 @@ pub fn show_link_definition( ..snapshot.anchor_after(offset_range.end), ) } - TriggerPoint::InlayHint(inlay_trigger, _) => { - SymbolRange::Inlay(inlay_trigger) + TriggerPoint::InlayHint(inlay_coordinates, _) => { + SymbolRange::Inlay(inlay_coordinates) } }); From 4cc9f2f525e9e2991f1c71cf5a2582bfa492ec83 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 22 Aug 2023 11:34:40 +0300 Subject: [PATCH 11/24] Highlight inlay hint parts on cmd-hover Co-Authored-By: Antonio --- crates/editor/src/display_map/inlay_map.rs | 24 ++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 23b39ca5ab..19b3ec7924 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -210,6 +210,7 @@ pub struct InlayChunks<'a> { buffer_chunks: MultiBufferChunks<'a>, buffer_chunk: Option>, inlay_chunks: Option>, + inlay_chunk: Option<&'a str>, output_offset: InlayOffset, max_output_offset: InlayOffset, hint_highlight_style: Option, @@ -298,13 +299,31 @@ impl<'a> Iterator for InlayChunks<'a> { - self.transforms.start().0; inlay.text.chunks_in_range(start.0..end.0) }); + let inlay_chunk = self + .inlay_chunk + .get_or_insert_with(|| inlay_chunks.next().unwrap()); + let (chunk, remainder) = inlay_chunk.split_at( + inlay_chunk + .len() + .min(next_highlight_endpoint.0 - self.output_offset.0), + ); + *inlay_chunk = remainder; + if inlay_chunk.is_empty() { + self.inlay_chunk = None; + } - let chunk = inlay_chunks.next().unwrap(); self.output_offset.0 += chunk.len(); - let highlight_style = match inlay.id { + let mut highlight_style = match inlay.id { InlayId::Suggestion(_) => self.suggestion_highlight_style, InlayId::Hint(_) => self.hint_highlight_style, }; + if !self.active_highlights.is_empty() { + for active_highlight in self.active_highlights.values() { + highlight_style + .get_or_insert(Default::default()) + .highlight(*active_highlight); + } + } Chunk { text: chunk, highlight_style, @@ -1073,6 +1092,7 @@ impl InlaySnapshot { transforms: cursor, buffer_chunks, inlay_chunks: None, + inlay_chunk: None, buffer_chunk: None, output_offset: range.start, max_output_offset: range.end, From 420f8b7b1597e7ea132cba5ffd4f5459834aef9d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 22 Aug 2023 22:06:48 +0300 Subject: [PATCH 12/24] Prepare for inlay and text highlight unification --- crates/editor/src/display_map.rs | 8 ++--- crates/editor/src/display_map/inlay_map.rs | 2 +- crates/editor/src/editor.rs | 21 +++--------- crates/editor/src/element.rs | 4 +-- crates/editor/src/link_go_to_definition.rs | 38 +++++++++++----------- 5 files changed, 30 insertions(+), 43 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 9df2919351..4f08be73b9 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -5,7 +5,7 @@ mod tab_map; mod wrap_map; use crate::{ - link_go_to_definition::InlayCoordinates, Anchor, AnchorRangeExt, InlayId, MultiBuffer, + link_go_to_definition::InlayRange, Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; pub use block_map::{BlockMap, BlockPoint}; @@ -43,7 +43,7 @@ pub trait ToDisplayPoint { } type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec>)>>; -type InlayHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; +type InlayHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; pub struct DisplayMap { buffer: ModelHandle, @@ -225,7 +225,7 @@ impl DisplayMap { pub fn highlight_inlays( &mut self, type_id: TypeId, - ranges: Vec, + ranges: Vec, style: HighlightStyle, ) { self.inlay_highlights @@ -247,7 +247,7 @@ impl DisplayMap { pub fn clear_inlay_highlights( &mut self, type_id: TypeId, - ) -> Option)>> { + ) -> Option)>> { self.inlay_highlights.remove(&Some(type_id)) } diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 19b3ec7924..cc730f9333 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1108,7 +1108,7 @@ impl InlaySnapshot { &self, inlay_highlights: &std::sync::Arc<( HighlightStyle, - Vec, + Vec, )>, transform_start: Anchor, transform_end: Anchor, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6c5d1a45ac..af352d2649 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -65,7 +65,7 @@ use language::{ OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, }; use link_go_to_definition::{ - hide_link_definition, show_link_definition, InlayCoordinates, LinkGoToDefinitionState, + hide_link_definition, show_link_definition, InlayRange, LinkGoToDefinitionState, }; use log::error; use multi_buffer::ToOffsetUtf16; @@ -7720,7 +7720,7 @@ impl Editor { pub fn highlight_inlays( &mut self, - ranges: Vec, + ranges: Vec, style: HighlightStyle, cx: &mut ViewContext, ) { @@ -7737,20 +7737,7 @@ impl Editor { self.display_map.read(cx).text_highlights(TypeId::of::()) } - pub fn clear_text_highlights( - &mut self, - cx: &mut ViewContext, - ) -> Option>)>> { - let highlights = self - .display_map - .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); - if highlights.is_some() { - cx.notify(); - } - highlights - } - - pub fn clear_highlights(&mut self, cx: &mut ViewContext) { + pub fn clear_text_highlights(&mut self, cx: &mut ViewContext) { let text_highlights = self .display_map .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); @@ -8353,7 +8340,7 @@ impl View for Editor { self.link_go_to_definition_state.task = None; - self.clear_highlights::(cx); + self.clear_text_highlights::(cx); } false diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 10809d8962..2be5105b33 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -13,7 +13,7 @@ use crate::{ }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, - GoToDefinitionTrigger, InlayCoordinates, + GoToDefinitionTrigger, InlayRange, }, mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, }; @@ -1929,7 +1929,7 @@ fn update_inlay_link_and_hover_points( update_go_to_definition_link( editor, GoToDefinitionTrigger::InlayHint( - InlayCoordinates { + InlayRange { inlay_position: hovered_hint.position, highlight_start: part_range.start, highlight_end: part_range.end, diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index ee2b536042..5bc45720a2 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -11,7 +11,7 @@ use util::TryFutureExt; #[derive(Debug, Default)] pub struct LinkGoToDefinitionState { pub last_trigger_point: Option, - pub symbol_range: Option, + pub symbol_range: Option, pub kind: Option, pub definitions: Vec, pub task: Option>>, @@ -19,12 +19,12 @@ pub struct LinkGoToDefinitionState { pub enum GoToDefinitionTrigger { Text(DisplayPoint), - InlayHint(InlayCoordinates, LocationLink), + InlayHint(InlayRange, LocationLink), None, } #[derive(Debug, Clone, Copy)] -pub struct InlayCoordinates { +pub struct InlayRange { pub inlay_position: Anchor, pub highlight_start: InlayOffset, pub highlight_end: InlayOffset, @@ -33,28 +33,28 @@ pub struct InlayCoordinates { #[derive(Debug, Clone)] pub enum TriggerPoint { Text(Anchor), - InlayHint(InlayCoordinates, LocationLink), + InlayHint(InlayRange, LocationLink), } #[derive(Debug, Clone)] -pub enum SymbolRange { +pub enum DocumentRange { Text(Range), - Inlay(InlayCoordinates), + Inlay(InlayRange), } -impl SymbolRange { +impl DocumentRange { fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool { match (self, trigger_point) { - (SymbolRange::Text(range), TriggerPoint::Text(point)) => { + (DocumentRange::Text(range), TriggerPoint::Text(point)) => { let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le(); point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge() } - (SymbolRange::Inlay(range), TriggerPoint::InlayHint(point, _)) => { + (DocumentRange::Inlay(range), TriggerPoint::InlayHint(point, _)) => { range.highlight_start.cmp(&point.highlight_end).is_le() && range.highlight_end.cmp(&point.highlight_end).is_ge() } - (SymbolRange::Inlay(_), TriggerPoint::Text(_)) - | (SymbolRange::Text(_), TriggerPoint::InlayHint(_, _)) => false, + (DocumentRange::Inlay(_), TriggerPoint::Text(_)) + | (DocumentRange::Text(_), TriggerPoint::InlayHint(_, _)) => false, } } } @@ -218,7 +218,7 @@ pub fn show_link_definition( .buffer_snapshot .anchor_in_excerpt(excerpt_id.clone(), origin.range.end); - SymbolRange::Text(start..end) + DocumentRange::Text(start..end) }) }), definition_result, @@ -226,14 +226,14 @@ pub fn show_link_definition( }) } TriggerPoint::InlayHint(trigger_source, trigger_target) => Some(( - Some(SymbolRange::Inlay(trigger_source.clone())), + Some(DocumentRange::Inlay(trigger_source.clone())), vec![trigger_target.clone()], )), }; this.update(&mut cx, |this, cx| { // Clear any existing highlights - this.clear_highlights::(cx); + this.clear_text_highlights::(cx); this.link_go_to_definition_state.kind = Some(definition_kind); this.link_go_to_definition_state.symbol_range = result .as_ref() @@ -276,24 +276,24 @@ pub fn show_link_definition( 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); - SymbolRange::Text( + DocumentRange::Text( snapshot.anchor_before(offset_range.start) ..snapshot.anchor_after(offset_range.end), ) } TriggerPoint::InlayHint(inlay_coordinates, _) => { - SymbolRange::Inlay(inlay_coordinates) + DocumentRange::Inlay(inlay_coordinates) } }); match highlight_range { - SymbolRange::Text(text_range) => this + DocumentRange::Text(text_range) => this .highlight_text::( vec![text_range], style, cx, ), - SymbolRange::Inlay(inlay_coordinates) => this + DocumentRange::Inlay(inlay_coordinates) => this .highlight_inlays::( vec![inlay_coordinates], style, @@ -325,7 +325,7 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext) { editor.link_go_to_definition_state.task = None; - editor.clear_highlights::(cx); + editor.clear_text_highlights::(cx); } pub fn go_to_fetched_definition( From 12ffbe54fb2c07ceafd2722aea9ee767a31e8c72 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 22 Aug 2023 22:38:49 +0300 Subject: [PATCH 13/24] Unify text and inlay highlights --- crates/editor/src/display_map.rs | 43 +++---- crates/editor/src/display_map/block_map.rs | 6 +- crates/editor/src/display_map/fold_map.rs | 19 +-- crates/editor/src/display_map/inlay_map.rs | 113 +++++------------- crates/editor/src/display_map/tab_map.rs | 29 ++--- crates/editor/src/display_map/wrap_map.rs | 8 +- crates/editor/src/editor.rs | 13 +- crates/editor/src/link_go_to_definition.rs | 7 ++ crates/editor/src/test/editor_test_context.rs | 1 + 9 files changed, 76 insertions(+), 163 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 4f08be73b9..037854435b 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -5,8 +5,8 @@ mod tab_map; mod wrap_map; use crate::{ - link_go_to_definition::InlayRange, Anchor, AnchorRangeExt, InlayId, MultiBuffer, - MultiBufferSnapshot, ToOffset, ToPoint, + link_go_to_definition::{DocumentRange, InlayRange}, + Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; pub use block_map::{BlockMap, BlockPoint}; use collections::{HashMap, HashSet}; @@ -42,8 +42,7 @@ pub trait ToDisplayPoint { fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint; } -type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec>)>>; -type InlayHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; +type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; pub struct DisplayMap { buffer: ModelHandle, @@ -54,7 +53,6 @@ pub struct DisplayMap { wrap_map: ModelHandle, block_map: BlockMap, text_highlights: TextHighlights, - inlay_highlights: InlayHighlights, pub clip_at_line_ends: bool, } @@ -90,7 +88,6 @@ impl DisplayMap { wrap_map, block_map, text_highlights: Default::default(), - inlay_highlights: Default::default(), clip_at_line_ends: false, } } @@ -115,7 +112,6 @@ impl DisplayMap { wrap_snapshot, block_snapshot, text_highlights: self.text_highlights.clone(), - inlay_highlights: self.inlay_highlights.clone(), clip_at_line_ends: self.clip_at_line_ends, } } @@ -218,8 +214,10 @@ impl DisplayMap { ranges: Vec>, style: HighlightStyle, ) { - self.text_highlights - .insert(Some(type_id), Arc::new((style, ranges))); + self.text_highlights.insert( + Some(type_id), + Arc::new((style, ranges.into_iter().map(DocumentRange::Text).collect())), + ); } pub fn highlight_inlays( @@ -228,11 +226,16 @@ impl DisplayMap { ranges: Vec, style: HighlightStyle, ) { - self.inlay_highlights - .insert(Some(type_id), Arc::new((style, ranges))); + self.text_highlights.insert( + Some(type_id), + Arc::new(( + style, + ranges.into_iter().map(DocumentRange::Inlay).collect(), + )), + ); } - pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range])> { + pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[DocumentRange])> { let highlights = self.text_highlights.get(&Some(type_id))?; Some((highlights.0, &highlights.1)) } @@ -240,17 +243,10 @@ impl DisplayMap { pub fn clear_text_highlights( &mut self, type_id: TypeId, - ) -> Option>)>> { + ) -> Option)>> { self.text_highlights.remove(&Some(type_id)) } - pub fn clear_inlay_highlights( - &mut self, - type_id: TypeId, - ) -> Option)>> { - self.inlay_highlights.remove(&Some(type_id)) - } - pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext) -> bool { self.wrap_map .update(cx, |map, cx| map.set_font(font_id, font_size, cx)) @@ -320,7 +316,6 @@ pub struct DisplaySnapshot { wrap_snapshot: wrap_map::WrapSnapshot, block_snapshot: block_map::BlockSnapshot, text_highlights: TextHighlights, - inlay_highlights: InlayHighlights, clip_at_line_ends: bool, } @@ -446,7 +441,6 @@ impl DisplaySnapshot { None, None, None, - None, ) .map(|h| h.text) } @@ -455,7 +449,7 @@ impl DisplaySnapshot { pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator { (0..=display_row).into_iter().rev().flat_map(|row| { self.block_snapshot - .chunks(row..row + 1, false, None, None, None, None) + .chunks(row..row + 1, false, None, None, None) .map(|h| h.text) .collect::>() .into_iter() @@ -474,7 +468,6 @@ impl DisplaySnapshot { display_rows, language_aware, Some(&self.text_highlights), - Some(&self.inlay_highlights), inlay_highlight_style, suggestion_highlight_style, ) @@ -797,7 +790,7 @@ impl DisplaySnapshot { #[cfg(any(test, feature = "test-support"))] pub fn highlight_ranges( &self, - ) -> Option>)>> { + ) -> Option)>> { let type_id = TypeId::of::(); self.text_highlights.get(&Some(type_id)).cloned() } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 083d8be5f3..8577e92819 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -1,6 +1,6 @@ use super::{ wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}, - InlayHighlights, TextHighlights, + TextHighlights, }; use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _}; use collections::{Bound, HashMap, HashSet}; @@ -579,7 +579,6 @@ impl BlockSnapshot { None, None, None, - None, ) .map(|chunk| chunk.text) .collect() @@ -590,7 +589,6 @@ impl BlockSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlights: Option<&'a InlayHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, ) -> BlockChunks<'a> { @@ -625,7 +623,6 @@ impl BlockSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlights, inlay_highlight_style, suggestion_highlight_style, ), @@ -1507,7 +1504,6 @@ mod tests { None, None, None, - None, ) .map(|chunk| chunk.text) .collect::(); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 800b51fcd8..dcbc156c47 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1,6 +1,6 @@ use super::{ inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot}, - InlayHighlights, TextHighlights, + TextHighlights, }; use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset}; use gpui::{color::Color, fonts::HighlightStyle}; @@ -475,7 +475,7 @@ pub struct FoldSnapshot { impl FoldSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(FoldOffset(0)..self.len(), false, None, None, None, None) + self.chunks(FoldOffset(0)..self.len(), false, None, None, None) .map(|c| c.text) .collect() } @@ -652,7 +652,6 @@ impl FoldSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlights: Option<&'a InlayHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, ) -> FoldChunks<'a> { @@ -676,7 +675,6 @@ impl FoldSnapshot { inlay_start..inlay_end, language_aware, text_highlights, - inlay_highlights, inlay_highlight_style, suggestion_highlight_style, ), @@ -689,15 +687,8 @@ impl FoldSnapshot { } pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator { - self.chunks( - start.to_offset(self)..self.len(), - false, - None, - None, - None, - None, - ) - .flat_map(|chunk| chunk.text.chars()) + self.chunks(start.to_offset(self)..self.len(), false, None, None, None) + .flat_map(|chunk| chunk.text.chars()) } #[cfg(test)] @@ -1505,7 +1496,7 @@ mod tests { let text = &expected_text[start.0..end.0]; assert_eq!( snapshot - .chunks(start..end, false, None, None, None, None) + .chunks(start..end, false, None, None, None) .map(|c| c.text) .collect::(), text, diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index cc730f9333..06d166b86c 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1,8 +1,9 @@ use crate::{ + link_go_to_definition::DocumentRange, multi_buffer::{MultiBufferChunks, MultiBufferRows}, Anchor, InlayId, MultiBufferSnapshot, ToOffset, }; -use collections::{BTreeMap, BTreeSet, HashSet}; +use collections::{BTreeMap, BTreeSet}; use gpui::fonts::HighlightStyle; use language::{Chunk, Edit, Point, TextSummary}; use std::{ @@ -15,7 +16,7 @@ use std::{ use sum_tree::{Bias, Cursor, SumTree}; use text::{Patch, Rope}; -use super::{InlayHighlights, TextHighlights}; +use super::TextHighlights; pub struct InlayMap { snapshot: InlaySnapshot, @@ -244,7 +245,6 @@ impl<'a> Iterator for InlayChunks<'a> { return None; } - // TODO kb highlights are not displayed still let mut next_highlight_endpoint = InlayOffset(usize::MAX); while let Some(endpoint) = self.highlight_endpoints.peek().copied() { if endpoint.offset <= self.output_offset { @@ -993,7 +993,6 @@ impl InlaySnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlights: Option<&'a InlayHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, ) -> InlayChunks<'a> { @@ -1002,15 +1001,14 @@ impl InlaySnapshot { let empty_text_highlights = TextHighlights::default(); let text_highlights = text_highlights.unwrap_or_else(|| &empty_text_highlights); - let empty_inlay_highlights = InlayHighlights::default(); - let inlay_highlights = inlay_highlights.unwrap_or_else(|| &empty_inlay_highlights); let mut highlight_endpoints = Vec::new(); - if !text_highlights.is_empty() || !inlay_highlights.is_empty() { + if !text_highlights.is_empty() { while cursor.start().0 < range.end { let transform_start = self .buffer .anchor_after(self.to_buffer_offset(cmp::max(range.start, cursor.start().0))); + let transform_start = self.to_inlay_offset(transform_start.to_offset(&self.buffer)); let transform_end = { let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); @@ -1019,15 +1017,17 @@ impl InlaySnapshot { cursor.start().0 + overshoot, ))) }; + let transform_end = self.to_inlay_offset(transform_end.to_offset(&self.buffer)); - let mut covered_tags = HashSet::default(); for (tag, text_highlights) in text_highlights.iter() { - covered_tags.insert(*tag); let style = text_highlights.0; let ranges = &text_highlights.1; let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&transform_start, &self.buffer); + let cmp = self + .document_to_inlay_range(probe) + .end + .cmp(&transform_start); if cmp.is_gt() { cmp::Ordering::Greater } else { @@ -1037,46 +1037,24 @@ impl InlaySnapshot { Ok(i) | Err(i) => i, }; for range in &ranges[start_ix..] { - if range.start.cmp(&transform_end, &self.buffer).is_ge() { + let range = self.document_to_inlay_range(range); + if range.start.cmp(&transform_end).is_ge() { break; } highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)), + offset: range.start, is_start: true, tag: *tag, style, }); highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), + offset: range.end, is_start: false, tag: *tag, style, }); } - - if let Some(inlay_highlights) = inlay_highlights.get(tag) { - self.push_inlay_highlight_range( - inlay_highlights, - transform_start, - transform_end, - &mut highlight_endpoints, - tag, - ); - } - } - - for (tag, inlay_highlights) in inlay_highlights - .iter() - .filter(|(tag, _)| !covered_tags.contains(tag)) - { - self.push_inlay_highlight_range( - inlay_highlights, - transform_start, - transform_end, - &mut highlight_endpoints, - tag, - ); } cursor.next(&()); @@ -1104,60 +1082,23 @@ impl InlaySnapshot { } } - fn push_inlay_highlight_range( - &self, - inlay_highlights: &std::sync::Arc<( - HighlightStyle, - Vec, - )>, - transform_start: Anchor, - transform_end: Anchor, - highlight_endpoints: &mut Vec, - tag: &Option, - ) { - let style = inlay_highlights.0; - let ranges = &inlay_highlights.1; - let start_ix = match ranges - .binary_search_by(|probe| probe.inlay_position.cmp(&transform_start, &self.buffer)) - { - Ok(i) | Err(i) => i, - }; - for range in &ranges[start_ix..] { - if range - .inlay_position - .cmp(&transform_end, &self.buffer) - .is_ge() - { - break; + fn document_to_inlay_range(&self, range: &DocumentRange) -> Range { + match range { + DocumentRange::Text(text_range) => { + self.to_inlay_offset(text_range.start.to_offset(&self.buffer)) + ..self.to_inlay_offset(text_range.end.to_offset(&self.buffer)) + } + DocumentRange::Inlay(inlay_range) => { + inlay_range.highlight_start..inlay_range.highlight_end } - - highlight_endpoints.push(HighlightEndpoint { - offset: range.highlight_start, - is_start: true, - tag: *tag, - style, - }); - highlight_endpoints.push(HighlightEndpoint { - offset: range.highlight_end, - is_start: false, - tag: *tag, - style, - }); } } #[cfg(test)] pub fn text(&self) -> String { - self.chunks( - Default::default()..self.len(), - false, - None, - None, - None, - None, - ) - .map(|chunk| chunk.text) - .collect() + self.chunks(Default::default()..self.len(), false, None, None, None) + .map(|chunk| chunk.text) + .collect() } fn check_invariants(&self) { @@ -1651,6 +1592,8 @@ mod tests { .map(|range| { buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end) }) + // TODO add inlay highlight tests + .map(DocumentRange::Text) .collect::>(); highlights.insert( @@ -1731,8 +1674,6 @@ mod tests { InlayOffset(start)..InlayOffset(end), false, Some(&highlights), - // TODO kb add tests - None, None, None, ) diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index 187a8de1d3..cae9ccc91f 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -1,6 +1,6 @@ use super::{ fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot}, - InlayHighlights, TextHighlights, + TextHighlights, }; use crate::MultiBufferSnapshot; use gpui::fonts::HighlightStyle; @@ -71,7 +71,6 @@ impl TabMap { None, None, None, - None, ) { for (ix, _) in chunk.text.match_indices('\t') { let offset_from_edit = offset_from_edit + (ix as u32); @@ -184,7 +183,7 @@ impl TabSnapshot { self.max_point() }; for c in self - .chunks(range.start..line_end, false, None, None, None, None) + .chunks(range.start..line_end, false, None, None, None) .flat_map(|chunk| chunk.text.chars()) { if c == '\n' { @@ -204,7 +203,6 @@ impl TabSnapshot { None, None, None, - None, ) .flat_map(|chunk| chunk.text.chars()) { @@ -225,9 +223,8 @@ impl TabSnapshot { &'a self, range: Range, language_aware: bool, - // TODO kb extract into one struct + // TODO kb extract into one struct? text_highlights: Option<&'a TextHighlights>, - inlay_highlights: Option<&'a InlayHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, ) -> TabChunks<'a> { @@ -250,7 +247,6 @@ impl TabSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlights, inlay_highlight_style, suggestion_highlight_style, ), @@ -275,16 +271,9 @@ impl TabSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks( - TabPoint::zero()..self.max_point(), - false, - None, - None, - None, - None, - ) - .map(|chunk| chunk.text) - .collect() + self.chunks(TabPoint::zero()..self.max_point(), false, None, None, None) + .map(|chunk| chunk.text) + .collect() } pub fn max_point(&self) -> TabPoint { @@ -612,7 +601,6 @@ mod tests { None, None, None, - None, ) .map(|c| c.text) .collect::(), @@ -687,8 +675,7 @@ mod tests { let mut chunks = Vec::new(); let mut was_tab = false; let mut text = String::new(); - for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None, None) - { + for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None) { if chunk.is_tab != was_tab { if !text.is_empty() { chunks.push((mem::take(&mut text), was_tab)); @@ -757,7 +744,7 @@ mod tests { let expected_summary = TextSummary::from(expected_text.as_str()); assert_eq!( tabs_snapshot - .chunks(start..end, false, None, None, None, None) + .chunks(start..end, false, None, None, None) .map(|c| c.text) .collect::(), expected_text, diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index f8287af799..d4b52d5893 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1,7 +1,7 @@ use super::{ fold_map::FoldBufferRows, tab_map::{self, TabEdit, TabPoint, TabSnapshot}, - InlayHighlights, TextHighlights, + TextHighlights, }; use crate::MultiBufferSnapshot; use gpui::{ @@ -447,7 +447,6 @@ impl WrapSnapshot { None, None, None, - None, ); let mut edit_transforms = Vec::::new(); for _ in edit.new_rows.start..edit.new_rows.end { @@ -577,7 +576,6 @@ impl WrapSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlights: Option<&'a InlayHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, ) -> WrapChunks<'a> { @@ -597,7 +595,6 @@ impl WrapSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlights, inlay_highlight_style, suggestion_highlight_style, ), @@ -1329,7 +1326,6 @@ mod tests { None, None, None, - None, ) .map(|h| h.text) } @@ -1354,7 +1350,7 @@ mod tests { } let actual_text = self - .chunks(start_row..end_row, true, None, None, None, None) + .chunks(start_row..end_row, true, None, None, None) .map(|c| c.text) .collect::(); assert_eq!( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index af352d2649..99db78f1c2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -65,7 +65,7 @@ use language::{ OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, }; use link_go_to_definition::{ - hide_link_definition, show_link_definition, InlayRange, LinkGoToDefinitionState, + hide_link_definition, show_link_definition, DocumentRange, InlayRange, LinkGoToDefinitionState, }; use log::error; use multi_buffer::ToOffsetUtf16; @@ -7733,7 +7733,7 @@ impl Editor { pub fn text_highlights<'a, T: 'static>( &'a self, cx: &'a AppContext, - ) -> Option<(HighlightStyle, &'a [Range])> { + ) -> Option<(HighlightStyle, &'a [DocumentRange])> { self.display_map.read(cx).text_highlights(TypeId::of::()) } @@ -7741,10 +7741,7 @@ impl Editor { let text_highlights = self .display_map .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); - let inlay_highlights = self - .display_map - .update(cx, |map, _| map.clear_inlay_highlights(TypeId::of::())); - if text_highlights.is_some() || inlay_highlights.is_some() { + if text_highlights.is_some() { cx.notify(); } } @@ -7953,6 +7950,8 @@ impl Editor { Some( ranges .iter() + // TODO kb mark inlays too + .filter_map(|range| range.as_text_range()) .map(move |range| { range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot) }) @@ -8406,6 +8405,8 @@ impl View for Editor { fn marked_text_range(&self, cx: &AppContext) -> Option> { let snapshot = self.buffer.read(cx).read(cx); let range = self.text_highlights::(cx)?.1.get(0)?; + // TODO kb mark inlays too + let range = range.as_text_range()?; Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0) } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 5bc45720a2..30f273065a 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -43,6 +43,13 @@ pub enum DocumentRange { } impl DocumentRange { + pub fn as_text_range(&self) -> Option> { + match self { + Self::Text(range) => Some(range.clone()), + Self::Inlay(_) => None, + } + } + fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool { match (self, trigger_point) { (DocumentRange::Text(range), TriggerPoint::Text(point)) => { diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 118cddaa92..8d7b6aa5c9 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -240,6 +240,7 @@ impl<'a> EditorTestContext<'a> { .map(|ranges| ranges.as_ref().clone().1) .unwrap_or_default() .into_iter() + .filter_map(|range| range.as_text_range()) .map(|range| range.to_offset(&snapshot.buffer_snapshot)) .collect(); assert_set_eq!(actual_ranges, expected_ranges); From 4b78678923018f47dc80747dd9e826564edd9a21 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 23 Aug 2023 13:24:17 +0300 Subject: [PATCH 14/24] Prepare background highlights for inlay highlights --- crates/editor/src/display_map.rs | 14 +- crates/editor/src/display_map/inlay_map.rs | 4 + crates/editor/src/editor.rs | 163 +++++++++--------- crates/editor/src/test/editor_test_context.rs | 1 + 4 files changed, 96 insertions(+), 86 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 037854435b..34e877a448 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -416,8 +416,18 @@ impl DisplaySnapshot { .to_offset(self.display_point_to_inlay_point(point, bias)) } - pub fn inlay_point_to_inlay_offset(&self, point: InlayPoint) -> InlayOffset { - self.inlay_snapshot.to_offset(point) + pub fn anchor_to_inlay_offset(&self, anchor: Anchor) -> InlayOffset { + self.inlay_snapshot + .to_inlay_offset(anchor.to_offset(&self.buffer_snapshot)) + } + + pub fn inlay_offset_to_display_point(&self, offset: InlayOffset, bias: Bias) -> DisplayPoint { + let inlay_point = self.inlay_snapshot.to_point(offset); + let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias); + let tab_point = self.tab_snapshot.to_tab_point(fold_point); + let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point); + let block_point = self.block_snapshot.to_block_point(wrap_point); + DisplayPoint(block_point) } fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint { diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 06d166b86c..5094d2fab9 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -392,6 +392,10 @@ impl InlayPoint { pub fn row(self) -> u32 { self.0.row } + + pub fn column(self) -> u32 { + self.0.column + } } impl InlayMap { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 99db78f1c2..2089fe0f5f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -535,6 +535,8 @@ type CompletionId = usize; type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option; +type BackgroundHighlight = (fn(&Theme) -> Color, Vec); + pub struct Editor { handle: WeakViewHandle, buffer: ModelHandle, @@ -564,8 +566,7 @@ pub struct Editor { show_wrap_guides: Option, placeholder_text: Option>, highlighted_rows: Option>, - #[allow(clippy::type_complexity)] - background_highlights: BTreeMap Color, Vec>)>, + background_highlights: BTreeMap, nav_history: Option, context_menu: Option, mouse_context_menu: ViewHandle, @@ -6758,10 +6759,18 @@ impl Editor { let rename_range = if let Some(range) = prepare_rename.await? { Some(range) } else { - this.read_with(&cx, |this, cx| { + this.update(&mut cx, |this, cx| { let buffer = this.buffer.read(cx).snapshot(cx); + let display_snapshot = this + .display_map + .update(cx, |display_map, cx| display_map.snapshot(cx)); let mut buffer_highlights = this - .document_highlights_for_position(selection.head(), &buffer) + .document_highlights_for_position( + selection.head(), + &buffer, + &display_snapshot, + ) + .filter_map(|highlight| highlight.as_text_range()) .filter(|highlight| { highlight.start.excerpt_id() == selection.head().excerpt_id() && highlight.end.excerpt_id() == selection.head().excerpt_id() @@ -6816,11 +6825,15 @@ impl Editor { let ranges = this .clear_background_highlights::(cx) .into_iter() - .flat_map(|(_, ranges)| ranges) + .flat_map(|(_, ranges)| { + ranges.into_iter().filter_map(|range| range.as_text_range()) + }) .chain( this.clear_background_highlights::(cx) .into_iter() - .flat_map(|(_, ranges)| ranges), + .flat_map(|(_, ranges)| { + ranges.into_iter().filter_map(|range| range.as_text_range()) + }), ) .collect(); @@ -7488,16 +7501,20 @@ impl Editor { color_fetcher: fn(&Theme) -> Color, cx: &mut ViewContext, ) { - self.background_highlights - .insert(TypeId::of::(), (color_fetcher, ranges)); + self.background_highlights.insert( + TypeId::of::(), + ( + color_fetcher, + ranges.into_iter().map(DocumentRange::Text).collect(), + ), + ); cx.notify(); } - #[allow(clippy::type_complexity)] pub fn clear_background_highlights( &mut self, cx: &mut ViewContext, - ) -> Option<(fn(&Theme) -> Color, Vec>)> { + ) -> Option { let highlights = self.background_highlights.remove(&TypeId::of::()); if highlights.is_some() { cx.notify(); @@ -7522,7 +7539,8 @@ impl Editor { &'a self, position: Anchor, buffer: &'a MultiBufferSnapshot, - ) -> impl 'a + Iterator> { + display_snapshot: &'a DisplaySnapshot, + ) -> impl 'a + Iterator { let read_highlights = self .background_highlights .get(&TypeId::of::()) @@ -7531,14 +7549,16 @@ impl Editor { .background_highlights .get(&TypeId::of::()) .map(|h| &h.1); - let left_position = position.bias_left(buffer); - let right_position = position.bias_right(buffer); + let left_position = display_snapshot.anchor_to_inlay_offset(position.bias_left(buffer)); + let right_position = display_snapshot.anchor_to_inlay_offset(position.bias_right(buffer)); read_highlights .into_iter() .chain(write_highlights) .flat_map(move |ranges| { let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&left_position, buffer); + let cmp = document_to_inlay_range(probe, display_snapshot) + .end + .cmp(&left_position); if cmp.is_ge() { Ordering::Greater } else { @@ -7549,9 +7569,12 @@ impl Editor { }; let right_position = right_position.clone(); - ranges[start_ix..] - .iter() - .take_while(move |range| range.start.cmp(&right_position, buffer).is_le()) + ranges[start_ix..].iter().take_while(move |range| { + document_to_inlay_range(range, &display_snapshot) + .start + .cmp(&right_position) + .is_le() + }) }) } @@ -7561,12 +7584,15 @@ impl Editor { display_snapshot: &DisplaySnapshot, theme: &Theme, ) -> Vec<(Range, Color)> { + let search_range = display_snapshot.anchor_to_inlay_offset(search_range.start) + ..display_snapshot.anchor_to_inlay_offset(search_range.end); let mut results = Vec::new(); - let buffer = &display_snapshot.buffer_snapshot; for (color_fetcher, ranges) in self.background_highlights.values() { let color = color_fetcher(theme); let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&search_range.start, buffer); + let cmp = document_to_inlay_range(probe, display_snapshot) + .end + .cmp(&search_range.start); if cmp.is_gt() { Ordering::Greater } else { @@ -7576,63 +7602,18 @@ impl Editor { Ok(i) | Err(i) => i, }; for range in &ranges[start_ix..] { - if range.start.cmp(&search_range.end, buffer).is_ge() { + let range = document_to_inlay_range(range, display_snapshot); + if range.start.cmp(&search_range.end).is_ge() { break; } - let start = range - .start - .to_point(buffer) - .to_display_point(display_snapshot); - let end = range - .end - .to_point(buffer) - .to_display_point(display_snapshot); + + let start = display_snapshot.inlay_offset_to_display_point(range.start, Bias::Left); + let end = display_snapshot.inlay_offset_to_display_point(range.end, Bias::Right); results.push((start..end, color)) } } results } - pub fn background_highlights_in_range_for( - &self, - search_range: Range, - display_snapshot: &DisplaySnapshot, - theme: &Theme, - ) -> Vec<(Range, Color)> { - let mut results = Vec::new(); - let buffer = &display_snapshot.buffer_snapshot; - let Some((color_fetcher, ranges)) = self.background_highlights - .get(&TypeId::of::()) else { - return vec![]; - }; - - let color = color_fetcher(theme); - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&search_range.start, buffer); - if cmp.is_gt() { - Ordering::Greater - } else { - Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - for range in &ranges[start_ix..] { - if range.start.cmp(&search_range.end, buffer).is_ge() { - break; - } - let start = range - .start - .to_point(buffer) - .to_display_point(display_snapshot); - let end = range - .end - .to_point(buffer) - .to_display_point(display_snapshot); - results.push((start..end, color)) - } - - results - } pub fn background_highlight_row_ranges( &self, @@ -7640,15 +7621,18 @@ impl Editor { display_snapshot: &DisplaySnapshot, count: usize, ) -> Vec> { + let search_range = display_snapshot.anchor_to_inlay_offset(search_range.start) + ..display_snapshot.anchor_to_inlay_offset(search_range.end); let mut results = Vec::new(); - let buffer = &display_snapshot.buffer_snapshot; let Some((_, ranges)) = self.background_highlights .get(&TypeId::of::()) else { return vec![]; }; let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&search_range.start, buffer); + let cmp = document_to_inlay_range(probe, display_snapshot) + .end + .cmp(&search_range.start); if cmp.is_gt() { Ordering::Greater } else { @@ -7657,30 +7641,28 @@ impl Editor { }) { Ok(i) | Err(i) => i, }; - let mut push_region = |start: Option, end: Option| { + let mut push_region = |start: Option, end: Option| { if let (Some(start_display), Some(end_display)) = (start, end) { - results.push( - start_display.to_display_point(display_snapshot) - ..=end_display.to_display_point(display_snapshot), - ); + results.push(start_display..=end_display); } }; - let mut start_row: Option = None; - let mut end_row: Option = None; + let mut start_row: Option = None; + let mut end_row: Option = None; if ranges.len() > count { - return vec![]; + return Vec::new(); } for range in &ranges[start_ix..] { - if range.start.cmp(&search_range.end, buffer).is_ge() { + let range = document_to_inlay_range(range, display_snapshot); + if range.start.cmp(&search_range.end).is_ge() { break; } - let end = range.end.to_point(buffer); + let end = display_snapshot.inlay_offset_to_display_point(range.end, Bias::Right); if let Some(current_row) = &end_row { - if end.row == current_row.row { + if end.row() == current_row.row() { continue; } } - let start = range.start.to_point(buffer); + let start = display_snapshot.inlay_offset_to_display_point(range.start, Bias::Left); if start_row.is_none() { assert_eq!(end_row, None); @@ -7689,7 +7671,7 @@ impl Editor { continue; } if let Some(current_end) = end_row.as_mut() { - if start.row > current_end.row + 1 { + if start.row() > current_end.row() + 1 { push_region(start_row, end_row); start_row = Some(start); end_row = Some(end); @@ -8133,6 +8115,19 @@ impl Editor { } } +fn document_to_inlay_range( + range: &DocumentRange, + snapshot: &DisplaySnapshot, +) -> Range { + match range { + DocumentRange::Text(text_range) => { + snapshot.anchor_to_inlay_offset(text_range.start) + ..snapshot.anchor_to_inlay_offset(text_range.end) + } + DocumentRange::Inlay(inlay_range) => inlay_range.highlight_start..inlay_range.highlight_end, + } +} + fn inlay_hint_settings( location: Anchor, snapshot: &MultiBufferSnapshot, diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 8d7b6aa5c9..033525395e 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -225,6 +225,7 @@ impl<'a> EditorTestContext<'a> { .map(|h| h.1.clone()) .unwrap_or_default() .into_iter() + .filter_map(|range| range.as_text_range()) .map(|range| range.to_offset(&snapshot.buffer_snapshot)) .collect() }); From bcaff0a18a924412348dbf4dfee7fbb2c6176856 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 23 Aug 2023 15:14:25 +0300 Subject: [PATCH 15/24] Propagate inlay background highlights to data storage --- crates/editor/src/display_map/inlay_map.rs | 2 +- crates/editor/src/display_map/tab_map.rs | 1 - crates/editor/src/editor.rs | 18 +++- crates/editor/src/element.rs | 87 ++++++++++++---- crates/editor/src/hover_popover.rs | 109 +++++++++++++++++++-- crates/project/src/lsp_command.rs | 1 - 6 files changed, 185 insertions(+), 33 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 5094d2fab9..56df722f52 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1596,7 +1596,7 @@ mod tests { .map(|range| { buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end) }) - // TODO add inlay highlight tests + // TODO kb add inlay highlight tests .map(DocumentRange::Text) .collect::>(); diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index cae9ccc91f..fcdef17a8b 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -223,7 +223,6 @@ impl TabSnapshot { &'a self, range: Range, language_aware: bool, - // TODO kb extract into one struct? text_highlights: Option<&'a TextHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2089fe0f5f..b21da05958 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7511,6 +7511,22 @@ impl Editor { cx.notify(); } + pub fn highlight_inlay_background( + &mut self, + ranges: Vec, + color_fetcher: fn(&Theme) -> Color, + cx: &mut ViewContext, + ) { + self.background_highlights.insert( + TypeId::of::(), + ( + color_fetcher, + ranges.into_iter().map(DocumentRange::Inlay).collect(), + ), + ); + cx.notify(); + } + pub fn clear_background_highlights( &mut self, cx: &mut ViewContext, @@ -7932,7 +7948,6 @@ impl Editor { Some( ranges .iter() - // TODO kb mark inlays too .filter_map(|range| range.as_text_range()) .map(move |range| { range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot) @@ -8400,7 +8415,6 @@ impl View for Editor { fn marked_text_range(&self, cx: &AppContext) -> Option> { let snapshot = self.buffer.read(cx).read(cx); let range = self.text_highlights::(cx)?.1.get(0)?; - // TODO kb mark inlays too let range = range.as_text_range()?; Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0) } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2be5105b33..e9a154ddb0 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -8,8 +8,8 @@ use crate::{ editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::{ - hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, - MIN_POPOVER_LINE_HEIGHT, + hide_hover, hover_at, hover_at_inlay, InlayHover, HOVER_POPOVER_GAP, + MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, @@ -43,7 +43,8 @@ use language::{ }; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, - InlayHintLabelPart, Location, LocationLink, ProjectPath, ResolveState, + HoverBlock, HoverBlockKind, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, + Location, LocationLink, ProjectPath, ResolveState, }; use smallvec::SmallVec; use std::{ @@ -1860,8 +1861,7 @@ fn update_inlay_link_and_hover_points( None }; if let Some(hovered_offset) = hovered_offset { - let buffer = editor.buffer().read(cx); - let snapshot = buffer.snapshot(cx); + let snapshot = editor.buffer().read(cx).snapshot(cx); let previous_valid_anchor = snapshot.anchor_at( point_for_position .previous_valid @@ -1885,15 +1885,14 @@ fn update_inlay_link_and_hover_points( .max_by_key(|hint| hint.id) { let inlay_hint_cache = editor.inlay_hint_cache(); - if let Some(cached_hint) = - inlay_hint_cache.hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) - { + let excerpt_id = previous_valid_anchor.excerpt_id; + if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) { match cached_hint.resolve_state { ResolveState::CanResolve(_, _) => { if let Some(buffer_id) = previous_valid_anchor.buffer_id { inlay_hint_cache.spawn_hint_resolve( buffer_id, - previous_valid_anchor.excerpt_id, + excerpt_id, hovered_hint.id, cx, ); @@ -1902,9 +1901,33 @@ fn update_inlay_link_and_hover_points( ResolveState::Resolved => { match cached_hint.label { project::InlayHintLabel::String(_) => { - if cached_hint.tooltip.is_some() { - dbg!(&cached_hint.tooltip); // TODO kb - // hover_at_point = Some(hovered_offset); + if let Some(tooltip) = cached_hint.tooltip { + hover_at_inlay( + editor, + InlayHover { + excerpt: excerpt_id, + tooltip: match tooltip { + InlayHintTooltip::String(text) => HoverBlock { + text, + kind: HoverBlockKind::PlainText, + }, + InlayHintTooltip::MarkupContent(content) => { + HoverBlock { + text: content.value, + kind: content.kind, + } + } + }, + triggered_from: hovered_offset, + range: InlayRange { + inlay_position: hovered_hint.position, + highlight_start: hint_start_offset, + highlight_end: hint_end_offset, + }, + }, + cx, + ); + hover_updated = true; } } project::InlayHintLabel::LabelParts(label_parts) => { @@ -1915,15 +1938,41 @@ fn update_inlay_link_and_hover_points( hovered_offset, ) { - if hovered_hint_part.tooltip.is_some() { - dbg!(&hovered_hint_part.tooltip); // TODO kb - // hover_at_point = Some(hovered_offset); + if let Some(tooltip) = hovered_hint_part.tooltip { + hover_at_inlay( + editor, + InlayHover { + excerpt: excerpt_id, + tooltip: match tooltip { + InlayHintLabelPartTooltip::String(text) => { + HoverBlock { + text, + kind: HoverBlockKind::PlainText, + } + } + InlayHintLabelPartTooltip::MarkupContent( + content, + ) => HoverBlock { + text: content.value, + kind: content.kind, + }, + }, + triggered_from: hovered_offset, + range: InlayRange { + inlay_position: hovered_hint.position, + highlight_start: part_range.start, + highlight_end: part_range.end, + }, + }, + cx, + ); + hover_updated = true; } if let Some(location) = hovered_hint_part.location { - if let Some(buffer) = cached_hint - .position - .buffer_id - .and_then(|buffer_id| buffer.buffer(buffer_id)) + if let Some(buffer) = + cached_hint.position.buffer_id.and_then(|buffer_id| { + editor.buffer().read(cx).buffer(buffer_id) + }) { go_to_definition_updated = true; update_go_to_definition_link( diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index e4509a765c..8e8babb44a 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,6 +1,8 @@ use crate::{ - display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, - EditorSnapshot, EditorStyle, RangeToAnchorExt, + display_map::{InlayOffset, ToDisplayPoint}, + link_go_to_definition::{DocumentRange, InlayRange}, + Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, + ExcerptId, RangeToAnchorExt, }; use futures::FutureExt; use gpui::{ @@ -46,6 +48,84 @@ pub fn hover_at(editor: &mut Editor, point: Option, cx: &mut ViewC } } +pub struct InlayHover { + pub excerpt: ExcerptId, + pub triggered_from: InlayOffset, + pub range: InlayRange, + pub tooltip: HoverBlock, +} + +pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext) { + if settings::get::(cx).hover_popover_enabled { + if editor.pending_rename.is_some() { + return; + } + + let Some(project) = editor.project.clone() else { + return; + }; + + if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { + if let DocumentRange::Inlay(range) = symbol_range { + if (range.highlight_start..=range.highlight_end) + .contains(&inlay_hover.triggered_from) + { + // Hover triggered from same location as last time. Don't show again. + return; + } + } + hide_hover(editor, cx); + } + + let snapshot = editor.snapshot(cx); + // Don't request again if the location is the same as the previous request + if let Some(triggered_from) = editor.hover_state.triggered_from { + if inlay_hover.triggered_from + == snapshot + .display_snapshot + .anchor_to_inlay_offset(triggered_from) + { + return; + } + } + + let task = cx.spawn(|this, mut cx| { + async move { + cx.background() + .timer(Duration::from_millis(HOVER_DELAY_MILLIS)) + .await; + this.update(&mut cx, |this, _| { + this.hover_state.diagnostic_popover = None; + })?; + + let hover_popover = InfoPopover { + project: project.clone(), + symbol_range: DocumentRange::Inlay(inlay_hover.range), + blocks: vec![inlay_hover.tooltip], + language: None, + rendered_content: None, + }; + + this.update(&mut cx, |this, cx| { + // Highlight the selected symbol using a background highlight + this.highlight_inlay_background::( + vec![inlay_hover.range], + |theme| theme.editor.hover_popover.highlight, + cx, + ); + this.hover_state.info_popover = Some(hover_popover); + cx.notify(); + })?; + + anyhow::Ok(()) + } + .log_err() + }); + + editor.hover_state.info_task = Some(task); + } +} + /// Hides the type information popup. /// Triggered by the `Hover` action when the cursor is not over a symbol or when the /// selections changed. @@ -110,8 +190,13 @@ fn show_hover( if !ignore_timeout { if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { if symbol_range - .to_offset(&snapshot.buffer_snapshot) - .contains(&multibuffer_offset) + .as_text_range() + .map(|range| { + range + .to_offset(&snapshot.buffer_snapshot) + .contains(&multibuffer_offset) + }) + .unwrap_or(false) { // Hover triggered from same location as last time. Don't show again. return; @@ -219,7 +304,7 @@ fn show_hover( Some(InfoPopover { project: project.clone(), - symbol_range: range, + symbol_range: DocumentRange::Text(range), blocks: hover_result.contents, language: hover_result.language, rendered_content: None, @@ -227,10 +312,13 @@ fn show_hover( }); this.update(&mut cx, |this, cx| { - if let Some(hover_popover) = hover_popover.as_ref() { + if let Some(symbol_range) = hover_popover + .as_ref() + .and_then(|hover_popover| hover_popover.symbol_range.as_text_range()) + { // Highlight the selected symbol using a background highlight this.highlight_background::( - vec![hover_popover.symbol_range.clone()], + vec![symbol_range], |theme| theme.editor.hover_popover.highlight, cx, ); @@ -497,7 +585,10 @@ impl HoverState { .or_else(|| { self.info_popover .as_ref() - .map(|info_popover| &info_popover.symbol_range.start) + .map(|info_popover| match &info_popover.symbol_range { + DocumentRange::Text(range) => &range.start, + DocumentRange::Inlay(range) => &range.inlay_position, + }) })?; let point = anchor.to_display_point(&snapshot.display_snapshot); @@ -522,7 +613,7 @@ impl HoverState { #[derive(Debug, Clone)] pub struct InfoPopover { pub project: ModelHandle, - pub symbol_range: Range, + symbol_range: DocumentRange, pub blocks: Vec, language: Option>, rendered_content: Option, diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 20bb302b5b..c057718bf3 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2126,7 +2126,6 @@ impl InlayHints { }) } - // TODO kb instead, store all LSP data inside the project::InlayHint? pub fn project_to_lsp_hint( hint: InlayHint, project: &ModelHandle, From dcf570bb03d3ce7e6290c161c390ddbdd19b6a20 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 23 Aug 2023 19:43:05 +0300 Subject: [PATCH 16/24] Fix resolve status conversion --- crates/editor/src/display_map.rs | 4 +- crates/editor/src/display_map/block_map.rs | 4 +- crates/editor/src/display_map/fold_map.rs | 4 +- crates/editor/src/display_map/inlay_map.rs | 114 ++++++++++----------- crates/editor/src/display_map/tab_map.rs | 4 +- crates/editor/src/display_map/wrap_map.rs | 4 +- crates/editor/src/editor.rs | 3 +- crates/project/src/lsp_command.rs | 4 +- crates/project/src/project.rs | 2 +- 9 files changed, 71 insertions(+), 72 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 34e877a448..611866bcad 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -471,14 +471,14 @@ impl DisplaySnapshot { &self, display_rows: Range, language_aware: bool, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> DisplayChunks<'_> { self.block_snapshot.chunks( display_rows, language_aware, Some(&self.text_highlights), - inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, ) } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 8577e92819..741507004c 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -589,7 +589,7 @@ impl BlockSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> BlockChunks<'a> { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); @@ -623,7 +623,7 @@ impl BlockSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, ), input_chunk: Default::default(), diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index dcbc156c47..d5473027a6 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -652,7 +652,7 @@ impl FoldSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> FoldChunks<'a> { let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>(); @@ -675,7 +675,7 @@ impl FoldSnapshot { inlay_start..inlay_end, language_aware, text_highlights, - inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, ), inlay_chunk: None, diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 56df722f52..748e1f0cd8 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -997,74 +997,74 @@ impl InlaySnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> InlayChunks<'a> { let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(); cursor.seek(&range.start, Bias::Right, &()); - let empty_text_highlights = TextHighlights::default(); - let text_highlights = text_highlights.unwrap_or_else(|| &empty_text_highlights); - let mut highlight_endpoints = Vec::new(); - if !text_highlights.is_empty() { - while cursor.start().0 < range.end { - let transform_start = self - .buffer - .anchor_after(self.to_buffer_offset(cmp::max(range.start, cursor.start().0))); - let transform_start = self.to_inlay_offset(transform_start.to_offset(&self.buffer)); + if let Some(text_highlights) = text_highlights { + if !text_highlights.is_empty() { + while cursor.start().0 < range.end { + let transform_start = self.buffer.anchor_after( + self.to_buffer_offset(cmp::max(range.start, cursor.start().0)), + ); + let transform_start = + self.to_inlay_offset(transform_start.to_offset(&self.buffer)); - let transform_end = { - let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); - self.buffer.anchor_before(self.to_buffer_offset(cmp::min( - cursor.end(&()).0, - cursor.start().0 + overshoot, - ))) - }; - let transform_end = self.to_inlay_offset(transform_end.to_offset(&self.buffer)); - - for (tag, text_highlights) in text_highlights.iter() { - let style = text_highlights.0; - let ranges = &text_highlights.1; - - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = self - .document_to_inlay_range(probe) - .end - .cmp(&transform_start); - if cmp.is_gt() { - cmp::Ordering::Greater - } else { - cmp::Ordering::Less - } - }) { - Ok(i) | Err(i) => i, + let transform_end = { + let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); + self.buffer.anchor_before(self.to_buffer_offset(cmp::min( + cursor.end(&()).0, + cursor.start().0 + overshoot, + ))) }; - for range in &ranges[start_ix..] { - let range = self.document_to_inlay_range(range); - if range.start.cmp(&transform_end).is_ge() { - break; + let transform_end = self.to_inlay_offset(transform_end.to_offset(&self.buffer)); + + for (tag, text_highlights) in text_highlights.iter() { + let style = text_highlights.0; + let ranges = &text_highlights.1; + + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = self + .document_to_inlay_range(probe) + .end + .cmp(&transform_start); + if cmp.is_gt() { + cmp::Ordering::Greater + } else { + cmp::Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + let range = self.document_to_inlay_range(range); + if range.start.cmp(&transform_end).is_ge() { + break; + } + + highlight_endpoints.push(HighlightEndpoint { + offset: range.start, + is_start: true, + tag: *tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: range.end, + is_start: false, + tag: *tag, + style, + }); } - - highlight_endpoints.push(HighlightEndpoint { - offset: range.start, - is_start: true, - tag: *tag, - style, - }); - highlight_endpoints.push(HighlightEndpoint { - offset: range.end, - is_start: false, - tag: *tag, - style, - }); } - } - cursor.next(&()); + cursor.next(&()); + } + highlight_endpoints.sort(); + cursor.seek(&range.start, Bias::Right, &()); } - highlight_endpoints.sort(); - cursor.seek(&range.start, Bias::Right, &()); } let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end); @@ -1078,7 +1078,7 @@ impl InlaySnapshot { buffer_chunk: None, output_offset: range.start, max_output_offset: range.end, - hint_highlight_style: inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, highlight_endpoints: highlight_endpoints.into_iter().peekable(), active_highlights: Default::default(), diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index fcdef17a8b..2cf0471b37 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -224,7 +224,7 @@ impl TabSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> TabChunks<'a> { let (input_start, expanded_char_column, to_next_stop) = @@ -246,7 +246,7 @@ impl TabSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, ), input_column, diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index d4b52d5893..f3600936f9 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -576,7 +576,7 @@ impl WrapSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> WrapChunks<'a> { let output_start = WrapPoint::new(rows.start, 0); @@ -595,7 +595,7 @@ impl WrapSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, ), input_chunk: Default::default(), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b21da05958..785d43f0b6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4882,7 +4882,6 @@ impl Editor { if let Some(clipboard_selection) = clipboard_selections.get(ix) { let end_offset = start_offset + clipboard_selection.len; to_insert = &clipboard_text[start_offset..end_offset]; - dbg!(start_offset, end_offset, &clipboard_text, &to_insert); entire_line = clipboard_selection.is_entire_line; start_offset = end_offset + 1; original_indent_column = @@ -7586,7 +7585,7 @@ impl Editor { let right_position = right_position.clone(); ranges[start_ix..].iter().take_while(move |range| { - document_to_inlay_range(range, &display_snapshot) + document_to_inlay_range(range, display_snapshot) .start .cmp(&right_position) .is_le() diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index c057718bf3..9f7799c555 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1935,8 +1935,9 @@ impl InlayHints { pub fn project_to_proto_hint(response_hint: InlayHint, cx: &AppContext) -> proto::InlayHint { let (state, lsp_resolve_state) = match response_hint.resolve_state { + ResolveState::Resolved => (0, None), ResolveState::CanResolve(server_id, resolve_data) => ( - 0, + 1, resolve_data .map(|json_data| { serde_json::to_string(&json_data) @@ -1947,7 +1948,6 @@ impl InlayHints { value, }), ), - ResolveState::Resolved => (1, None), ResolveState::Resolving => (2, None), }; let resolve_state = Some(proto::ResolveState { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fbdbd04664..1fe307eec2 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5091,7 +5091,7 @@ impl Project { InlayHints::proto_to_project_hint(resolved_hint, &project, &mut cx) .await .map(Some) - .context("inlay hints proto response conversion") + .context("inlay hints proto resolve response conversion") } None => Ok(None), } From 7cd60d6afb95d777644a27e9e3096129c19771c0 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 23 Aug 2023 20:22:38 +0300 Subject: [PATCH 17/24] Simplify and restore client resolve capabilities --- crates/editor/src/display_map/inlay_map.rs | 4 ---- crates/editor/src/editor.rs | 23 ++++++++++++++-------- crates/lsp/src/lsp.rs | 4 +++- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 748e1f0cd8..1b65719448 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -392,10 +392,6 @@ impl InlayPoint { pub fn row(self) -> u32 { self.0.row } - - pub fn column(self) -> u32 { - self.0.column - } } impl InlayMap { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 785d43f0b6..681e1d48b2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7656,13 +7656,16 @@ impl Editor { }) { Ok(i) | Err(i) => i, }; - let mut push_region = |start: Option, end: Option| { + let mut push_region = |start: Option, end: Option| { if let (Some(start_display), Some(end_display)) = (start, end) { - results.push(start_display..=end_display); + results.push( + start_display.to_display_point(display_snapshot) + ..=end_display.to_display_point(display_snapshot), + ); } }; - let mut start_row: Option = None; - let mut end_row: Option = None; + let mut start_row: Option = None; + let mut end_row: Option = None; if ranges.len() > count { return Vec::new(); } @@ -7671,13 +7674,17 @@ impl Editor { if range.start.cmp(&search_range.end).is_ge() { break; } - let end = display_snapshot.inlay_offset_to_display_point(range.end, Bias::Right); + let end = display_snapshot + .inlay_offset_to_display_point(range.end, Bias::Right) + .to_point(display_snapshot); if let Some(current_row) = &end_row { - if end.row() == current_row.row() { + if end.row == current_row.row { continue; } } - let start = display_snapshot.inlay_offset_to_display_point(range.start, Bias::Left); + let start = display_snapshot + .inlay_offset_to_display_point(range.start, Bias::Left) + .to_point(display_snapshot); if start_row.is_none() { assert_eq!(end_row, None); @@ -7686,7 +7693,7 @@ impl Editor { continue; } if let Some(current_end) = end_row.as_mut() { - if start.row() > current_end.row() + 1 { + if start.row > current_end.row + 1 { push_region(start_row, end_row); start_row = Some(start); end_row = Some(end); diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 78c858a90c..e0ae64d806 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -434,7 +434,9 @@ impl LanguageServer { ..Default::default() }), inlay_hint: Some(InlayHintClientCapabilities { - resolve_support: None, + resolve_support: Some(InlayHintResolveClientCapabilities { + properties: vec!["textEdits".to_string(), "tooltip".to_string()], + }), dynamic_registration: Some(false), }), ..Default::default() From 852427e87b0f6c7b56b7dca38536a255c25aef86 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 24 Aug 2023 16:08:08 +0300 Subject: [PATCH 18/24] Use inlay highlights in randomized tests --- crates/editor/src/display_map/inlay_map.rs | 57 +++++++++++++--------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 1b65719448..25b8d3aef6 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1144,13 +1144,12 @@ fn push_isomorphic(sum_tree: &mut SumTree, summary: TextSummary) { #[cfg(test)] mod tests { use super::*; - use crate::{InlayId, MultiBuffer}; + use crate::{link_go_to_definition::InlayRange, InlayId, MultiBuffer}; use gpui::AppContext; use project::{InlayHint, InlayHintLabel, ResolveState}; use rand::prelude::*; use settings::SettingsStore; use std::{cmp::Reverse, env, sync::Arc}; - use sum_tree::TreeMap; use text::Patch; use util::post_inc; @@ -1579,28 +1578,6 @@ mod tests { let mut buffer_snapshot = buffer.read(cx).snapshot(cx); let mut next_inlay_id = 0; log::info!("buffer text: {:?}", buffer_snapshot.text()); - - let mut highlights = TreeMap::default(); - let highlight_count = rng.gen_range(0_usize..10); - let mut highlight_ranges = (0..highlight_count) - .map(|_| buffer_snapshot.random_byte_range(0, &mut rng)) - .collect::>(); - highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end))); - log::info!("highlighting ranges {:?}", highlight_ranges); - let highlight_ranges = highlight_ranges - .into_iter() - .map(|range| { - buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end) - }) - // TODO kb add inlay highlight tests - .map(DocumentRange::Text) - .collect::>(); - - highlights.insert( - Some(TypeId::of::<()>()), - Arc::new((HighlightStyle::default(), highlight_ranges)), - ); - let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); for _ in 0..operations { let mut inlay_edits = Patch::default(); @@ -1663,6 +1640,38 @@ mod tests { ); } + let mut highlights = TextHighlights::default(); + let highlight_count = rng.gen_range(0_usize..10); + let mut highlight_ranges = (0..highlight_count) + .map(|_| buffer_snapshot.random_byte_range(0, &mut rng)) + .collect::>(); + highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end))); + log::info!("highlighting ranges {:?}", highlight_ranges); + let highlight_ranges = if rng.gen_bool(0.5) { + highlight_ranges + .into_iter() + .map(|range| InlayRange { + inlay_position: buffer_snapshot.anchor_before(range.start), + highlight_start: inlay_snapshot.to_inlay_offset(range.start), + highlight_end: inlay_snapshot.to_inlay_offset(range.end), + }) + .map(DocumentRange::Inlay) + .collect::>() + } else { + highlight_ranges + .into_iter() + .map(|range| { + buffer_snapshot.anchor_before(range.start) + ..buffer_snapshot.anchor_after(range.end) + }) + .map(DocumentRange::Text) + .collect::>() + }; + highlights.insert( + Some(TypeId::of::<()>()), + Arc::new((HighlightStyle::default(), highlight_ranges)), + ); + for _ in 0..5 { let mut end = rng.gen_range(0..=inlay_snapshot.len().0); end = expected_text.clip_offset(end, Bias::Right); From 3c55c933d4064d06f3d5d3e2cf8336c3deefaf40 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 24 Aug 2023 16:37:53 +0300 Subject: [PATCH 19/24] Be more lenient with hint resolution, always return some hint --- crates/editor/src/inlay_hint_cache.rs | 38 ++++++++++++--------------- crates/project/src/project.rs | 21 +++++++-------- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 52a4039a76..d4325e13d9 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -449,30 +449,26 @@ impl InlayHintCache { }) })?; if let Some(resolved_hint_task) = resolved_hint_task { - if let Some(mut resolved_hint) = - resolved_hint_task.await.context("hint resolve task")? - { - editor.update(&mut cx, |editor, _| { - if let Some(excerpt_hints) = - editor.inlay_hint_cache.hints.get(&excerpt_id) + let mut resolved_hint = + resolved_hint_task.await.context("hint resolve task")?; + editor.update(&mut cx, |editor, _| { + if let Some(excerpt_hints) = + editor.inlay_hint_cache.hints.get(&excerpt_id) + { + let mut guard = excerpt_hints.write(); + if let Some(cached_hint) = guard + .hints + .iter_mut() + .find(|(hint_id, _)| hint_id == &id) + .map(|(_, hint)| hint) { - let mut guard = excerpt_hints.write(); - if let Some(cached_hint) = guard - .hints - .iter_mut() - .find(|(hint_id, _)| hint_id == &id) - .map(|(_, hint)| hint) - { - if cached_hint.resolve_state == ResolveState::Resolving - { - resolved_hint.resolve_state = - ResolveState::Resolved; - *cached_hint = resolved_hint; - } + if cached_hint.resolve_state == ResolveState::Resolving { + resolved_hint.resolve_state = ResolveState::Resolved; + *cached_hint = resolved_hint; } } - })?; - } + } + })?; } anyhow::Ok(()) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1fe307eec2..0bbb61dfcb 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5033,7 +5033,7 @@ impl Project { buffer_handle: ModelHandle, server_id: LanguageServerId, cx: &mut ModelContext, - ) -> Task>> { + ) -> Task> { if self.is_local() { let buffer = buffer_handle.read(cx); let (_, lang_server) = if let Some((adapter, server)) = @@ -5041,7 +5041,7 @@ impl Project { { (adapter.clone(), server.clone()) } else { - return Task::ready(Ok(None)); + return Task::ready(Ok(hint)); }; let can_resolve = lang_server .capabilities() @@ -5050,7 +5050,7 @@ impl Project { .and_then(|options| options.resolve_provider) .unwrap_or(false); if !can_resolve { - return Task::ready(Ok(None)); + return Task::ready(Ok(hint)); } let buffer_snapshot = buffer.snapshot(); @@ -5071,7 +5071,7 @@ impl Project { &mut cx, ) .await?; - Ok(Some(resolved_hint)) + Ok(resolved_hint) }) } else if let Some(project_id) = self.remote_id() { let client = self.client.clone(); @@ -5079,7 +5079,7 @@ impl Project { project_id, buffer_id: buffer_handle.read(cx).remote_id(), language_server_id: server_id.0 as u64, - hint: Some(InlayHints::project_to_proto_hint(hint, cx)), + hint: Some(InlayHints::project_to_proto_hint(hint.clone(), cx)), }; cx.spawn(|project, mut cx| async move { let response = client @@ -5090,10 +5090,9 @@ impl Project { Some(resolved_hint) => { InlayHints::proto_to_project_hint(resolved_hint, &project, &mut cx) .await - .map(Some) .context("inlay hints proto resolve response conversion") } - None => Ok(None), + None => Ok(hint), } }) } else { @@ -6917,7 +6916,7 @@ impl Project { .and_then(|buffer| buffer.upgrade(cx)) .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id)) })?; - let resolved_hint = this + let response_hint = this .update(&mut cx, |project, cx| { project.resolve_inlay_hint( hint, @@ -6927,11 +6926,11 @@ impl Project { ) }) .await - .context("inlay hints fetch")? - .map(|hint| cx.read(|cx| InlayHints::project_to_proto_hint(hint, cx))); + .context("inlay hints fetch")?; + let resolved_hint = cx.read(|cx| InlayHints::project_to_proto_hint(response_hint, cx)); Ok(proto::ResolveInlayHintResponse { - hint: resolved_hint, + hint: Some(resolved_hint), }) } From abd2d012b1914fcea6830116a5dea88fbd69963d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 24 Aug 2023 16:50:53 +0300 Subject: [PATCH 20/24] Properly binary search cached inlay hints --- crates/editor/src/inlay_hint_cache.rs | 38 ++++++++++++++++++++------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index d4325e13d9..71b65de676 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -714,13 +714,21 @@ fn calculate_hint_updates( probe.1.position.cmp(&new_hint.position, buffer_snapshot) }) { Ok(ix) => { - let (cached_inlay_id, cached_hint) = &cached_excerpt_hints.hints[ix]; - if cached_hint == &new_hint { - excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind); - false - } else { - true + let mut missing_from_cache = true; + for (cached_inlay_id, cached_hint) in &cached_excerpt_hints.hints[ix..] { + if new_hint + .position + .cmp(&cached_hint.position, buffer_snapshot) + .is_gt() + { + break; + } + if cached_hint == &new_hint { + excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind); + missing_from_cache = false; + } } + missing_from_cache } Err(_) => true, } @@ -820,11 +828,21 @@ fn apply_hint_update( .binary_search_by(|probe| probe.1.position.cmp(&new_hint.position, &buffer_snapshot)) { Ok(i) => { - if cached_hints[i].1.text() == new_hint.text() { - None - } else { - Some(i) + let mut insert_position = Some(i); + for (_, cached_hint) in &cached_hints[i..] { + if new_hint + .position + .cmp(&cached_hint.position, &buffer_snapshot) + .is_gt() + { + break; + } + if cached_hint.text() == new_hint.text() { + insert_position = None; + break; + } } + insert_position } Err(i) => Some(i), }; From f19c659ed6aa63e56effddfcf7d3952d325854da Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 11:45:07 +0300 Subject: [PATCH 21/24] Add link_go_to_definition test for inlays --- crates/editor/src/element.rs | 232 +----------- crates/editor/src/hover_popover.rs | 23 +- crates/editor/src/inlay_hint_cache.rs | 14 +- crates/editor/src/link_go_to_definition.rs | 413 ++++++++++++++++++++- 4 files changed, 447 insertions(+), 235 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e9a154ddb0..3ba807308c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4,16 +4,16 @@ use super::{ MAX_LINE_LEN, }; use crate::{ - display_map::{BlockStyle, DisplaySnapshot, FoldStatus, InlayOffset, TransformBlock}, + display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock}, editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::{ - hide_hover, hover_at, hover_at_inlay, InlayHover, HOVER_POPOVER_GAP, - MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, + hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, + MIN_POPOVER_LINE_HEIGHT, }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, - GoToDefinitionTrigger, InlayRange, + update_inlay_link_and_hover_points, GoToDefinitionTrigger, }, mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, }; @@ -43,8 +43,7 @@ use language::{ }; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, - HoverBlock, HoverBlockKind, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, - Location, LocationLink, ProjectPath, ResolveState, + ProjectPath, }; use smallvec::SmallVec; use std::{ @@ -478,10 +477,11 @@ impl EditorElement { } None => { update_inlay_link_and_hover_points( - position_map, + &position_map.snapshot, point_for_position, editor, - (cmd, shift), + cmd, + shift, cx, ); } @@ -1835,214 +1835,6 @@ impl EditorElement { } } -fn update_inlay_link_and_hover_points( - position_map: &PositionMap, - point_for_position: PointForPosition, - editor: &mut Editor, - (cmd_held, shift_held): (bool, bool), - cx: &mut ViewContext<'_, '_, Editor>, -) { - let hint_start_offset = position_map - .snapshot - .display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); - let hint_end_offset = position_map - .snapshot - .display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); - let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; - let hovered_offset = if offset_overshoot == 0 { - Some( - position_map - .snapshot - .display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left), - ) - } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { - Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) - } else { - None - }; - if let Some(hovered_offset) = hovered_offset { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let previous_valid_anchor = snapshot.anchor_at( - point_for_position - .previous_valid - .to_point(&position_map.snapshot.display_snapshot), - Bias::Left, - ); - let next_valid_anchor = snapshot.anchor_at( - point_for_position - .next_valid - .to_point(&position_map.snapshot.display_snapshot), - Bias::Right, - ); - - let mut go_to_definition_updated = false; - let mut hover_updated = false; - if let Some(hovered_hint) = editor - .visible_inlay_hints(cx) - .into_iter() - .skip_while(|hint| hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt()) - .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) - .max_by_key(|hint| hint.id) - { - let inlay_hint_cache = editor.inlay_hint_cache(); - let excerpt_id = previous_valid_anchor.excerpt_id; - if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) { - match cached_hint.resolve_state { - ResolveState::CanResolve(_, _) => { - if let Some(buffer_id) = previous_valid_anchor.buffer_id { - inlay_hint_cache.spawn_hint_resolve( - buffer_id, - excerpt_id, - hovered_hint.id, - cx, - ); - } - } - ResolveState::Resolved => { - match cached_hint.label { - project::InlayHintLabel::String(_) => { - if let Some(tooltip) = cached_hint.tooltip { - hover_at_inlay( - editor, - InlayHover { - excerpt: excerpt_id, - tooltip: match tooltip { - InlayHintTooltip::String(text) => HoverBlock { - text, - kind: HoverBlockKind::PlainText, - }, - InlayHintTooltip::MarkupContent(content) => { - HoverBlock { - text: content.value, - kind: content.kind, - } - } - }, - triggered_from: hovered_offset, - range: InlayRange { - inlay_position: hovered_hint.position, - highlight_start: hint_start_offset, - highlight_end: hint_end_offset, - }, - }, - cx, - ); - hover_updated = true; - } - } - project::InlayHintLabel::LabelParts(label_parts) => { - if let Some((hovered_hint_part, part_range)) = - find_hovered_hint_part( - label_parts, - hint_start_offset..hint_end_offset, - hovered_offset, - ) - { - if let Some(tooltip) = hovered_hint_part.tooltip { - hover_at_inlay( - editor, - InlayHover { - excerpt: excerpt_id, - tooltip: match tooltip { - InlayHintLabelPartTooltip::String(text) => { - HoverBlock { - text, - kind: HoverBlockKind::PlainText, - } - } - InlayHintLabelPartTooltip::MarkupContent( - content, - ) => HoverBlock { - text: content.value, - kind: content.kind, - }, - }, - triggered_from: hovered_offset, - range: InlayRange { - inlay_position: hovered_hint.position, - highlight_start: part_range.start, - highlight_end: part_range.end, - }, - }, - cx, - ); - hover_updated = true; - } - if let Some(location) = hovered_hint_part.location { - if let Some(buffer) = - cached_hint.position.buffer_id.and_then(|buffer_id| { - editor.buffer().read(cx).buffer(buffer_id) - }) - { - go_to_definition_updated = true; - update_go_to_definition_link( - editor, - GoToDefinitionTrigger::InlayHint( - InlayRange { - inlay_position: hovered_hint.position, - highlight_start: part_range.start, - highlight_end: part_range.end, - }, - LocationLink { - origin: Some(Location { - buffer, - range: cached_hint.position - ..cached_hint.position, - }), - target: location, - }, - ), - cmd_held, - shift_held, - cx, - ); - } - } - } - } - }; - } - ResolveState::Resolving => {} - } - } - } - - if !go_to_definition_updated { - update_go_to_definition_link( - editor, - GoToDefinitionTrigger::None, - cmd_held, - shift_held, - cx, - ); - } - if !hover_updated { - hover_at(editor, None, cx); - } - } -} - -fn find_hovered_hint_part( - label_parts: Vec, - hint_range: Range, - hovered_offset: InlayOffset, -) -> Option<(InlayHintLabelPart, Range)> { - if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { - let mut hovered_character = (hovered_offset - hint_range.start).0; - let mut part_start = hint_range.start; - for part in label_parts { - let part_len = part.value.chars().count(); - if hovered_character >= part_len { - hovered_character -= part_len; - part_start.0 += part_len; - } else { - return Some((part, part_start..InlayOffset(part_start.0 + part_len))); - } - } - } - None -} - struct HighlightedChunk<'a> { chunk: &'a str, style: Option, @@ -2871,12 +2663,12 @@ struct PositionMap { snapshot: EditorSnapshot, } -#[derive(Debug)] +#[derive(Debug, Copy, Clone)] pub struct PointForPosition { - previous_valid: DisplayPoint, + pub previous_valid: DisplayPoint, pub next_valid: DisplayPoint, - exact_unclipped: DisplayPoint, - column_overshoot_after_line_end: u32, + pub exact_unclipped: DisplayPoint, + pub column_overshoot_after_line_end: u32, } impl PointForPosition { diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 8e8babb44a..6eae470bad 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -13,7 +13,7 @@ use gpui::{ AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext, }; use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry}; -use project::{HoverBlock, HoverBlockKind, Project}; +use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; use std::{ops::Range, sync::Arc, time::Duration}; use util::TryFutureExt; @@ -55,6 +55,27 @@ pub struct InlayHover { pub tooltip: HoverBlock, } +pub fn find_hovered_hint_part( + label_parts: Vec, + hint_range: Range, + hovered_offset: InlayOffset, +) -> Option<(InlayHintLabelPart, Range)> { + if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { + let mut hovered_character = (hovered_offset - hint_range.start).0; + let mut part_start = hint_range.start; + for part in label_parts { + let part_len = part.value.chars().count(); + if hovered_character >= part_len { + hovered_character -= part_len; + part_start.0 += part_len; + } else { + return Some((part, part_start..InlayOffset(part_start.0 + part_len))); + } + } + } + None +} + pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext) { if settings::get::(cx).hover_popover_enabled { if editor.pending_rename.is_some() { diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 71b65de676..b0c7d9e0f1 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -904,7 +904,7 @@ fn apply_hint_update( } #[cfg(test)] -mod tests { +pub mod tests { use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use crate::{ @@ -2989,15 +2989,11 @@ all hints should be invalidated and requeried for all of its visible excerpts" ("/a/main.rs", editor, fake_server) } - fn cached_hint_labels(editor: &Editor) -> Vec { + pub fn cached_hint_labels(editor: &Editor) -> Vec { let mut labels = Vec::new(); for (_, excerpt_hints) in &editor.inlay_hint_cache().hints { - let excerpt_hints = excerpt_hints.read(); - for (_, inlay) in excerpt_hints.hints.iter() { - match &inlay.label { - project::InlayHintLabel::String(s) => labels.push(s.to_string()), - _ => unreachable!(), - } + for (_, inlay) in &excerpt_hints.read().hints { + labels.push(inlay.text()); } } @@ -3005,7 +3001,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" labels } - fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec { + pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec { let mut hints = editor .visible_inlay_hints(cx) .into_iter() diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 30f273065a..ea22ea5eae 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1,10 +1,15 @@ use crate::{ - display_map::InlayOffset, element::PointForPosition, Anchor, DisplayPoint, Editor, - EditorSnapshot, SelectPhase, + display_map::{DisplaySnapshot, InlayOffset}, + element::PointForPosition, + hover_popover::{self, InlayHover}, + Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase, }; use gpui::{Task, ViewContext}; use language::{Bias, ToOffset}; -use project::LocationLink; +use project::{ + HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, Location, + LocationLink, ResolveState, +}; use std::ops::Range; use util::TryFutureExt; @@ -23,7 +28,7 @@ pub enum GoToDefinitionTrigger { None, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct InlayRange { pub inlay_position: Anchor, pub highlight_start: InlayOffset, @@ -140,6 +145,192 @@ pub fn update_go_to_definition_link( hide_link_definition(editor, cx); } +pub fn update_inlay_link_and_hover_points( + snapshot: &DisplaySnapshot, + point_for_position: PointForPosition, + editor: &mut Editor, + cmd_held: bool, + shift_held: bool, + cx: &mut ViewContext<'_, '_, Editor>, +) { + let hint_start_offset = + snapshot.display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); + let hint_end_offset = + snapshot.display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); + let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; + let hovered_offset = if offset_overshoot == 0 { + Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left)) + } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { + Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) + } else { + None + }; + if let Some(hovered_offset) = hovered_offset { + let buffer_snapshot = editor.buffer().read(cx).snapshot(cx); + let previous_valid_anchor = buffer_snapshot.anchor_at( + point_for_position.previous_valid.to_point(snapshot), + Bias::Left, + ); + let next_valid_anchor = buffer_snapshot.anchor_at( + point_for_position.next_valid.to_point(snapshot), + Bias::Right, + ); + + let mut go_to_definition_updated = false; + let mut hover_updated = false; + if let Some(hovered_hint) = editor + .visible_inlay_hints(cx) + .into_iter() + .skip_while(|hint| { + hint.position + .cmp(&previous_valid_anchor, &buffer_snapshot) + .is_lt() + }) + .take_while(|hint| { + hint.position + .cmp(&next_valid_anchor, &buffer_snapshot) + .is_le() + }) + .max_by_key(|hint| hint.id) + { + let inlay_hint_cache = editor.inlay_hint_cache(); + let excerpt_id = previous_valid_anchor.excerpt_id; + if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) { + match cached_hint.resolve_state { + ResolveState::CanResolve(_, _) => { + if let Some(buffer_id) = previous_valid_anchor.buffer_id { + inlay_hint_cache.spawn_hint_resolve( + buffer_id, + excerpt_id, + hovered_hint.id, + cx, + ); + } + } + ResolveState::Resolved => { + match cached_hint.label { + project::InlayHintLabel::String(_) => { + if let Some(tooltip) = cached_hint.tooltip { + hover_popover::hover_at_inlay( + editor, + InlayHover { + excerpt: excerpt_id, + tooltip: match tooltip { + InlayHintTooltip::String(text) => HoverBlock { + text, + kind: HoverBlockKind::PlainText, + }, + InlayHintTooltip::MarkupContent(content) => { + HoverBlock { + text: content.value, + kind: content.kind, + } + } + }, + triggered_from: hovered_offset, + range: InlayRange { + inlay_position: hovered_hint.position, + highlight_start: hint_start_offset, + highlight_end: hint_end_offset, + }, + }, + cx, + ); + hover_updated = true; + } + } + project::InlayHintLabel::LabelParts(label_parts) => { + if let Some((hovered_hint_part, part_range)) = + hover_popover::find_hovered_hint_part( + label_parts, + hint_start_offset..hint_end_offset, + hovered_offset, + ) + { + if let Some(tooltip) = hovered_hint_part.tooltip { + hover_popover::hover_at_inlay( + editor, + InlayHover { + excerpt: excerpt_id, + tooltip: match tooltip { + InlayHintLabelPartTooltip::String(text) => { + HoverBlock { + text, + kind: HoverBlockKind::PlainText, + } + } + InlayHintLabelPartTooltip::MarkupContent( + content, + ) => HoverBlock { + text: content.value, + kind: content.kind, + }, + }, + triggered_from: hovered_offset, + range: InlayRange { + inlay_position: hovered_hint.position, + highlight_start: part_range.start, + highlight_end: part_range.end, + }, + }, + cx, + ); + hover_updated = true; + } + if let Some(location) = hovered_hint_part.location { + if let Some(buffer) = + cached_hint.position.buffer_id.and_then(|buffer_id| { + editor.buffer().read(cx).buffer(buffer_id) + }) + { + go_to_definition_updated = true; + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::InlayHint( + InlayRange { + inlay_position: hovered_hint.position, + highlight_start: part_range.start, + highlight_end: part_range.end, + }, + LocationLink { + origin: Some(Location { + buffer, + range: cached_hint.position + ..cached_hint.position, + }), + target: location, + }, + ), + cmd_held, + shift_held, + cx, + ); + } + } + } + } + }; + } + ResolveState::Resolving => {} + } + } + } + + if !go_to_definition_updated { + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::None, + cmd_held, + shift_held, + cx, + ); + } + if !hover_updated { + hover_popover::hover_at(editor, None, cx); + } + } +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum LinkDefinitionKind { Symbol, @@ -391,14 +582,21 @@ fn go_to_fetched_definition_of_kind( #[cfg(test)] mod tests { use super::*; - use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; + use crate::{ + display_map::ToDisplayPoint, + editor_tests::init_test, + inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, + test::editor_lsp_test_context::EditorLspTestContext, + }; use futures::StreamExt; use gpui::{ platform::{self, Modifiers, ModifiersChangedEvent}, View, }; use indoc::indoc; + use language::language_settings::InlayHintSettings; use lsp::request::{GotoDefinition, GotoTypeDefinition}; + use util::assert_set_eq; #[gpui::test] async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) { @@ -853,4 +1051,209 @@ mod tests { "}); cx.foreground().run_until_parked(); } + + #[gpui::test] + async fn test_link_go_to_inlay(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + cx, + ) + .await; + cx.set_state(indoc! {" + struct TestStruct; + + fn main() { + let variableˇ = TestStruct; + } + "}); + let hint_start_offset = cx.ranges(indoc! {" + struct TestStruct; + + fn main() { + let variableˇ = TestStruct; + } + "})[0] + .start; + let hint_position = cx.to_lsp(hint_start_offset); + let target_range = cx.lsp_range(indoc! {" + struct «TestStruct»; + + fn main() { + let variable = TestStruct; + } + "}); + + let expected_uri = cx.buffer_lsp_url.clone(); + let inlay_label = ": TestStruct"; + cx.lsp + .handle_request::(move |params, _| { + let expected_uri = expected_uri.clone(); + async move { + assert_eq!(params.text_document.uri, expected_uri); + Ok(Some(vec![lsp::InlayHint { + position: hint_position, + label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { + value: inlay_label.to_string(), + location: Some(lsp::Location { + uri: params.text_document.uri, + range: target_range, + }), + ..Default::default() + }]), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: Some(false), + padding_right: Some(false), + data: None, + }])) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let expected_layers = vec![inlay_label.to_string()]; + assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + }); + + let inlay_range = cx + .ranges(indoc! {" + struct TestStruct; + + fn main() { + let variable« »= TestStruct; + } + "}) + .get(0) + .cloned() + .unwrap(); + let hint_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + PointForPosition { + previous_valid: inlay_range.start.to_display_point(&snapshot), + next_valid: inlay_range.end.to_display_point(&snapshot), + exact_unclipped: inlay_range.end.to_display_point(&snapshot), + column_overshoot_after_line_end: (inlay_label.len() / 2) as u32, + } + }); + // Press cmd to trigger highlight + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + hint_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let actual_ranges = snapshot + .highlight_ranges::() + .map(|ranges| ranges.as_ref().clone().1) + .unwrap_or_default() + .into_iter() + .map(|range| match range { + DocumentRange::Text(range) => { + panic!("Unexpected regular text selection range {range:?}") + } + DocumentRange::Inlay(inlay_range) => inlay_range, + }) + .collect::>(); + + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + let expected_highlight_start = snapshot.display_point_to_inlay_offset( + inlay_range.start.to_display_point(&snapshot), + Bias::Left, + ); + let expected_ranges = vec![InlayRange { + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + highlight_start: expected_highlight_start, + highlight_end: InlayOffset(expected_highlight_start.0 + inlay_label.len()), + }]; + assert_set_eq!(actual_ranges, expected_ranges); + }); + + // Unpress cmd causes highlight to go away + cx.update_editor(|editor, cx| { + editor.modifiers_changed( + &platform::ModifiersChangedEvent { + modifiers: Modifiers { + cmd: false, + ..Default::default() + }, + ..Default::default() + }, + cx, + ); + }); + // Assert no link highlights + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let actual_ranges = snapshot + .highlight_ranges::() + .map(|ranges| ranges.as_ref().clone().1) + .unwrap_or_default() + .into_iter() + .map(|range| match range { + DocumentRange::Text(range) => { + panic!("Unexpected regular text selection range {range:?}") + } + DocumentRange::Inlay(inlay_range) => inlay_range, + }) + .collect::>(); + + assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}"); + }); + + // Cmd+click without existing definition requests and jumps + cx.update_editor(|editor, cx| { + editor.modifiers_changed( + &platform::ModifiersChangedEvent { + modifiers: Modifiers { + cmd: true, + ..Default::default() + }, + ..Default::default() + }, + cx, + ); + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + hint_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + go_to_fetched_type_definition(editor, hint_hover_position, false, cx); + }); + cx.foreground().run_until_parked(); + cx.assert_editor_state(indoc! {" + struct «TestStructˇ»; + + fn main() { + let variable = TestStruct; + } + "}); + } } From e44516cc6c8197f401934394d939d89fd414a634 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 14:26:04 +0300 Subject: [PATCH 22/24] Add hover tests --- crates/editor/src/hover_popover.rs | 318 ++++++++++++++++++++- crates/editor/src/link_go_to_definition.rs | 12 +- crates/project/src/lsp_command.rs | 30 +- crates/project/src/project.rs | 8 +- 4 files changed, 344 insertions(+), 24 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 6eae470bad..19020b643a 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -804,10 +804,17 @@ impl DiagnosticPopover { #[cfg(test)] mod tests { use super::*; - use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; + use crate::{ + editor_tests::init_test, + element::PointForPosition, + 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, + }; + use collections::BTreeSet; use gpui::fonts::Weight; use indoc::indoc; - use language::{Diagnostic, DiagnosticSet}; + use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; use lsp::LanguageServerId; use project::{HoverBlock, HoverBlockKind}; use smol::stream::StreamExt; @@ -1243,4 +1250,311 @@ mod tests { editor }); } + + #[gpui::test] + async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Right( + lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions { + resolve_provider: Some(true), + ..Default::default() + }), + )), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variableˇ = TestNewType(TestStruct); + } + "}); + + let hint_start_offset = cx.ranges(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variableˇ = TestNewType(TestStruct); + } + "})[0] + .start; + let hint_position = cx.to_lsp(hint_start_offset); + let new_type_target_range = cx.lsp_range(indoc! {" + struct TestStruct; + + // ================== + + struct «TestNewType»(T); + + fn main() { + let variable = TestNewType(TestStruct); + } + "}); + let struct_target_range = cx.lsp_range(indoc! {" + struct «TestStruct»; + + // ================== + + struct TestNewType(T); + + fn main() { + let variable = TestNewType(TestStruct); + } + "}); + + let uri = cx.buffer_lsp_url.clone(); + let new_type_label = "TestNewType"; + let struct_label = "TestStruct"; + let entire_hint_label = ": TestNewType"; + let closure_uri = uri.clone(); + cx.lsp + .handle_request::(move |params, _| { + let task_uri = closure_uri.clone(); + async move { + assert_eq!(params.text_document.uri, task_uri); + Ok(Some(vec![lsp::InlayHint { + position: hint_position, + label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { + value: entire_hint_label.to_string(), + ..Default::default() + }]), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: Some(false), + padding_right: Some(false), + data: None, + }])) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let expected_layers = vec![entire_hint_label.to_string()]; + assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + }); + + let inlay_range = cx + .ranges(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variable« »= TestNewType(TestStruct); + } + "}) + .get(0) + .cloned() + .unwrap(); + let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + PointForPosition { + previous_valid: inlay_range.start.to_display_point(&snapshot), + next_valid: inlay_range.end.to_display_point(&snapshot), + exact_unclipped: inlay_range.end.to_display_point(&snapshot), + column_overshoot_after_line_end: (entire_hint_label.find(new_type_label).unwrap() + + new_type_label.len() / 2) + as u32, + } + }); + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + new_type_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + + let resolve_closure_uri = uri.clone(); + cx.lsp + .handle_request::( + move |mut hint_to_resolve, _| { + let mut resolved_hint_positions = BTreeSet::new(); + let task_uri = resolve_closure_uri.clone(); + async move { + let inserted = resolved_hint_positions.insert(hint_to_resolve.position); + assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice"); + + // `: TestNewType` + hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![ + lsp::InlayHintLabelPart { + value: ": ".to_string(), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: new_type_label.to_string(), + location: Some(lsp::Location { + uri: task_uri.clone(), + range: new_type_target_range, + }), + tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!( + "A tooltip for `{new_type_label}`" + ))), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: "<".to_string(), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: struct_label.to_string(), + location: Some(lsp::Location { + uri: task_uri, + range: struct_target_range, + }), + tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent( + lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: format!("A tooltip for `{struct_label}`"), + }, + )), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: ">".to_string(), + ..Default::default() + }, + ]); + + Ok(hint_to_resolve) + } + }, + ) + .next() + .await; + cx.foreground().run_until_parked(); + + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + new_type_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground() + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let hover_state = &editor.hover_state; + assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); + let popover = hover_state.info_popover.as_ref().unwrap(); + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + let entire_inlay_start = snapshot.display_point_to_inlay_offset( + inlay_range.start.to_display_point(&snapshot), + Bias::Left, + ); + + let expected_new_type_label_start = InlayOffset(entire_inlay_start.0 + ": ".len()); + assert_eq!( + popover.symbol_range, + DocumentRange::Inlay(InlayRange { + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + highlight_start: expected_new_type_label_start, + highlight_end: InlayOffset( + expected_new_type_label_start.0 + new_type_label.len() + ), + }), + "Popover range should match the new type label part" + ); + assert_eq!( + popover + .rendered_content + .as_ref() + .expect("should have label text for new type hint") + .text, + format!("A tooltip for `{new_type_label}`"), + "Rendered text should not anyhow alter backticks" + ); + }); + + let struct_hint_part_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + PointForPosition { + previous_valid: inlay_range.start.to_display_point(&snapshot), + next_valid: inlay_range.end.to_display_point(&snapshot), + exact_unclipped: inlay_range.end.to_display_point(&snapshot), + column_overshoot_after_line_end: (entire_hint_label.find(struct_label).unwrap() + + struct_label.len() / 2) + as u32, + } + }); + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + struct_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground() + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let hover_state = &editor.hover_state; + assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); + let popover = hover_state.info_popover.as_ref().unwrap(); + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + let entire_inlay_start = snapshot.display_point_to_inlay_offset( + inlay_range.start.to_display_point(&snapshot), + Bias::Left, + ); + let expected_struct_label_start = + InlayOffset(entire_inlay_start.0 + ": ".len() + new_type_label.len() + "<".len()); + assert_eq!( + popover.symbol_range, + DocumentRange::Inlay(InlayRange { + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + highlight_start: expected_struct_label_start, + highlight_end: InlayOffset(expected_struct_label_start.0 + struct_label.len()), + }), + "Popover range should match the struct label part" + ); + assert_eq!( + popover + .rendered_content + .as_ref() + .expect("should have label text for struct hint") + .text, + format!("A tooltip for {struct_label}"), + "Rendered markdown element should remove backticks from text" + ); + }); + } } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index ea22ea5eae..926c0d6dde 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -41,7 +41,7 @@ pub enum TriggerPoint { InlayHint(InlayRange, LocationLink), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum DocumentRange { Text(Range), Inlay(InlayRange), @@ -1096,7 +1096,7 @@ mod tests { "}); let expected_uri = cx.buffer_lsp_url.clone(); - let inlay_label = ": TestStruct"; + let hint_label = ": TestStruct"; cx.lsp .handle_request::(move |params, _| { let expected_uri = expected_uri.clone(); @@ -1105,7 +1105,7 @@ mod tests { Ok(Some(vec![lsp::InlayHint { position: hint_position, label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { - value: inlay_label.to_string(), + value: hint_label.to_string(), location: Some(lsp::Location { uri: params.text_document.uri, range: target_range, @@ -1125,7 +1125,7 @@ mod tests { .await; cx.foreground().run_until_parked(); cx.update_editor(|editor, cx| { - let expected_layers = vec![inlay_label.to_string()]; + let expected_layers = vec![hint_label.to_string()]; assert_eq!(expected_layers, cached_hint_labels(editor)); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); }); @@ -1147,7 +1147,7 @@ mod tests { previous_valid: inlay_range.start.to_display_point(&snapshot), next_valid: inlay_range.end.to_display_point(&snapshot), exact_unclipped: inlay_range.end.to_display_point(&snapshot), - column_overshoot_after_line_end: (inlay_label.len() / 2) as u32, + column_overshoot_after_line_end: (hint_label.len() / 2) as u32, } }); // Press cmd to trigger highlight @@ -1185,7 +1185,7 @@ mod tests { let expected_ranges = vec![InlayRange { inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), highlight_start: expected_highlight_start, - highlight_end: InlayOffset(expected_highlight_start.0 + inlay_label.len()), + highlight_end: InlayOffset(expected_highlight_start.0 + hint_label.len()), }]; assert_set_eq!(actual_ranges, expected_ranges); }); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 9f7799c555..292f9a5226 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -17,7 +17,7 @@ use language::{ CodeAction, Completion, LanguageServerName, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped, }; -use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities}; +use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, OneOf, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions { @@ -2213,6 +2213,22 @@ impl InlayHints { }, } } + + pub fn can_resolve_inlays(capabilities: &ServerCapabilities) -> bool { + capabilities + .inlay_hint_provider + .as_ref() + .and_then(|options| match options { + OneOf::Left(_is_supported) => None, + OneOf::Right(capabilities) => match capabilities { + lsp::InlayHintServerCapabilities::Options(o) => o.resolve_provider, + lsp::InlayHintServerCapabilities::RegistrationOptions(o) => { + o.inlay_hint_options.resolve_provider + } + }, + }) + .unwrap_or(false) + } } #[async_trait(?Send)] @@ -2269,14 +2285,10 @@ impl LspCommand for InlayHints { lsp_adapter.name.0.as_ref() == "typescript-language-server"; let hints = message.unwrap_or_default().into_iter().map(|lsp_hint| { - let resolve_state = match lsp_server.capabilities().inlay_hint_provider { - Some(lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options( - lsp::InlayHintOptions { - resolve_provider: Some(true), - .. - }, - ))) => ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()), - _ => ResolveState::Resolved, + let resolve_state = if InlayHints::can_resolve_inlays(lsp_server.capabilities()) { + ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()) + } else { + ResolveState::Resolved }; let project = project.clone(); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 0bbb61dfcb..c7765bf55a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5043,13 +5043,7 @@ impl Project { } else { return Task::ready(Ok(hint)); }; - let can_resolve = lang_server - .capabilities() - .completion_provider - .as_ref() - .and_then(|options| options.resolve_provider) - .unwrap_or(false); - if !can_resolve { + if !InlayHints::can_resolve_inlays(lang_server.capabilities()) { return Task::ready(Ok(hint)); } From 8ed280a029c397b572b961db7aa892e1ed726562 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 14:30:07 +0300 Subject: [PATCH 23/24] Rebase fixes --- Cargo.lock | 2 +- crates/rpc/proto/zed.proto | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d669be5d9d..8197f883c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1220,7 +1220,7 @@ dependencies = [ "tempfile", "text", "thiserror", - "time 0.3.24", + "time 0.3.27", "tiny_http", "url", "util", diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index dbd700e264..ce47830af2 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -128,8 +128,8 @@ message Envelope { InlayHints inlay_hints = 116; InlayHintsResponse inlay_hints_response = 117; - ResolveInlayHint resolve_inlay_hint = 131; - ResolveInlayHintResponse resolve_inlay_hint_response = 132; + ResolveInlayHint resolve_inlay_hint = 137; + ResolveInlayHintResponse resolve_inlay_hint_response = 138; RefreshInlayHints refresh_inlay_hints = 118; CreateChannel create_channel = 119; From 0a18aa694f13f8cb0f5b3c015f317cb6c03d5dfe Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 14:46:39 +0300 Subject: [PATCH 24/24] Use stricter inlay range checks to avoid stuck highlights Often, hint ranges are separated by a single '<` char as in `Option>`. When moving the caret from left to right, avoid inclusive ranges to faster update the matching hint underline. --- crates/editor/src/hover_popover.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 19020b643a..3ce936ae82 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -88,7 +88,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { if let DocumentRange::Inlay(range) = symbol_range { - if (range.highlight_start..=range.highlight_end) + if (range.highlight_start..range.highlight_end) .contains(&inlay_hover.triggered_from) { // Hover triggered from same location as last time. Don't show again.