Cargo.lock 🔗
@@ -1220,7 +1220,7 @@ dependencies = [
"tempfile",
"text",
"thiserror",
- "time 0.3.24",
+ "time 0.3.27",
"tiny_http",
"url",
"util",
Kirill Bulatov created
Resolves inlay hints on hover, shows hint label parts' tooltips, allows
cmd+click to navigate to the hints' parts with locations,
correspondingly highlight the hints.
Release Notes:
- Support dynamic inlay hints
Cargo.lock | 2
crates/editor/src/display_map.rs | 71 +
crates/editor/src/display_map/block_map.rs | 8
crates/editor/src/display_map/fold_map.rs | 8
crates/editor/src/display_map/inlay_map.rs | 204 +++--
crates/editor/src/display_map/tab_map.rs | 8
crates/editor/src/display_map/wrap_map.rs | 8
crates/editor/src/editor.rs | 206 +++--
crates/editor/src/element.rs | 169 +++-
crates/editor/src/hover_popover.rs | 450 ++++++++++++
crates/editor/src/inlay_hint_cache.rs | 140 +++
crates/editor/src/items.rs | 2
crates/editor/src/link_go_to_definition.rs | 731 ++++++++++++++++++--
crates/editor/src/test/editor_test_context.rs | 2
crates/project/src/lsp_command.rs | 688 ++++++++++++++-----
crates/project/src/project.rs | 132 +++
crates/rpc/proto/zed.proto | 32
crates/rpc/src/proto.rs | 4
crates/rpc/src/rpc.rs | 2
19 files changed, 2,270 insertions(+), 597 deletions(-)
@@ -1220,7 +1220,7 @@ dependencies = [
"tempfile",
"text",
"thiserror",
- "time 0.3.24",
+ "time 0.3.27",
"tiny_http",
"url",
"util",
@@ -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::{DocumentRange, InlayRange},
+ Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
+};
pub use block_map::{BlockMap, BlockPoint};
use collections::{HashMap, HashSet};
use fold_map::FoldMap;
@@ -27,7 +30,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 {
@@ -39,7 +42,7 @@ pub trait ToDisplayPoint {
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
}
-type TextHighlights = TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>;
+type TextHighlights = TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<DocumentRange>)>>;
pub struct DisplayMap {
buffer: ModelHandle<MultiBuffer>,
@@ -211,11 +214,28 @@ impl DisplayMap {
ranges: Vec<Range<Anchor>>,
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(
+ &mut self,
+ type_id: TypeId,
+ ranges: Vec<InlayRange>,
+ style: HighlightStyle,
+ ) {
+ 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<Anchor>])> {
+ 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))
}
@@ -223,7 +243,7 @@ impl DisplayMap {
pub fn clear_text_highlights(
&mut self,
type_id: TypeId,
- ) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
+ ) -> Option<Arc<(HighlightStyle, Vec<DocumentRange>)>> {
self.text_highlights.remove(&Some(type_id))
}
@@ -387,12 +407,35 @@ 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 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 {
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 {
@@ -428,15 +471,15 @@ impl DisplaySnapshot {
&self,
display_rows: Range<u32>,
language_aware: bool,
- hint_highlights: Option<HighlightStyle>,
- suggestion_highlights: Option<HighlightStyle>,
+ hint_highlight_style: Option<HighlightStyle>,
+ suggestion_highlight_style: Option<HighlightStyle>,
) -> DisplayChunks<'_> {
self.block_snapshot.chunks(
display_rows,
language_aware,
Some(&self.text_highlights),
- hint_highlights,
- suggestion_highlights,
+ hint_highlight_style,
+ suggestion_highlight_style,
)
}
@@ -757,7 +800,7 @@ impl DisplaySnapshot {
#[cfg(any(test, feature = "test-support"))]
pub fn highlight_ranges<Tag: ?Sized + 'static>(
&self,
- ) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
+ ) -> Option<Arc<(HighlightStyle, Vec<DocumentRange>)>> {
let type_id = TypeId::of::<Tag>();
self.text_highlights.get(&Some(type_id)).cloned()
}
@@ -589,8 +589,8 @@ impl BlockSnapshot {
rows: Range<u32>,
language_aware: bool,
text_highlights: Option<&'a TextHighlights>,
- hint_highlights: Option<HighlightStyle>,
- suggestion_highlights: Option<HighlightStyle>,
+ hint_highlight_style: Option<HighlightStyle>,
+ suggestion_highlight_style: Option<HighlightStyle>,
) -> 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 +623,8 @@ impl BlockSnapshot {
input_start..input_end,
language_aware,
text_highlights,
- hint_highlights,
- suggestion_highlights,
+ hint_highlight_style,
+ suggestion_highlight_style,
),
input_chunk: Default::default(),
transforms: cursor,
@@ -652,8 +652,8 @@ impl FoldSnapshot {
range: Range<FoldOffset>,
language_aware: bool,
text_highlights: Option<&'a TextHighlights>,
- hint_highlights: Option<HighlightStyle>,
- suggestion_highlights: Option<HighlightStyle>,
+ hint_highlight_style: Option<HighlightStyle>,
+ suggestion_highlight_style: Option<HighlightStyle>,
) -> FoldChunks<'a> {
let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>();
@@ -675,8 +675,8 @@ impl FoldSnapshot {
inlay_start..inlay_end,
language_aware,
text_highlights,
- hint_highlights,
- suggestion_highlights,
+ hint_highlight_style,
+ suggestion_highlight_style,
),
inlay_chunk: None,
inlay_offset: inlay_start,
@@ -1,4 +1,5 @@
use crate::{
+ link_go_to_definition::DocumentRange,
multi_buffer::{MultiBufferChunks, MultiBufferRows},
Anchor, InlayId, MultiBufferSnapshot, ToOffset,
};
@@ -183,7 +184,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,
@@ -210,6 +211,7 @@ pub struct InlayChunks<'a> {
buffer_chunks: MultiBufferChunks<'a>,
buffer_chunk: Option<Chunk<'a>>,
inlay_chunks: Option<text::Chunks<'a>>,
+ inlay_chunk: Option<&'a str>,
output_offset: InlayOffset,
max_output_offset: InlayOffset,
hint_highlight_style: Option<HighlightStyle>,
@@ -297,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,
@@ -973,8 +993,8 @@ impl InlaySnapshot {
range: Range<InlayOffset>,
language_aware: bool,
text_highlights: Option<&'a TextHighlights>,
- hint_highlights: Option<HighlightStyle>,
- suggestion_highlights: Option<HighlightStyle>,
+ hint_highlight_style: Option<HighlightStyle>,
+ suggestion_highlight_style: Option<HighlightStyle>,
) -> InlayChunks<'a> {
let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
cursor.seek(&range.start, Bias::Right, &());
@@ -983,52 +1003,56 @@ 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_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_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,
};
-
- 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,
- };
- 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,
- });
+ 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,
+ });
}
}
@@ -1046,17 +1070,30 @@ impl InlaySnapshot {
transforms: cursor,
buffer_chunks,
inlay_chunks: None,
+ inlay_chunk: None,
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,
+ suggestion_highlight_style,
highlight_endpoints: highlight_endpoints.into_iter().peekable(),
active_highlights: Default::default(),
snapshot: self,
}
}
+ fn document_to_inlay_range(&self, range: &DocumentRange) -> Range<InlayOffset> {
+ 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
+ }
+ }
+ }
+
#[cfg(test)]
pub fn text(&self) -> String {
self.chunks(Default::default()..self.len(), false, None, None, None)
@@ -1107,13 +1144,12 @@ fn push_isomorphic(sum_tree: &mut SumTree<Transform>, 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};
+ 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;
@@ -1125,12 +1161,12 @@ mod tests {
Anchor::min(),
&InlayHint {
label: InlayHintLabel::String("a".to_string()),
- buffer_id: 0,
position: text::Anchor::default(),
padding_left: false,
padding_right: false,
tooltip: None,
kind: None,
+ resolve_state: ResolveState::Resolved,
},
)
.text
@@ -1145,12 +1181,12 @@ mod tests {
Anchor::min(),
&InlayHint {
label: InlayHintLabel::String("a".to_string()),
- buffer_id: 0,
position: text::Anchor::default(),
padding_left: true,
padding_right: true,
tooltip: None,
kind: None,
+ resolve_state: ResolveState::Resolved,
},
)
.text
@@ -1165,12 +1201,12 @@ mod tests {
Anchor::min(),
&InlayHint {
label: InlayHintLabel::String(" a ".to_string()),
- buffer_id: 0,
position: text::Anchor::default(),
padding_left: false,
padding_right: false,
tooltip: None,
kind: None,
+ resolve_state: ResolveState::Resolved,
},
)
.text
@@ -1185,12 +1221,12 @@ mod tests {
Anchor::min(),
&InlayHint {
label: InlayHintLabel::String(" a ".to_string()),
- buffer_id: 0,
position: text::Anchor::default(),
padding_left: true,
padding_right: true,
tooltip: None,
kind: None,
+ resolve_state: ResolveState::Resolved,
},
)
.text
@@ -1542,26 +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::<Vec<_>>();
- 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)
- })
- .collect::<Vec<_>>();
-
- 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();
@@ -1624,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::<Vec<_>>();
+ 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::<Vec<_>>()
+ } else {
+ highlight_ranges
+ .into_iter()
+ .map(|range| {
+ buffer_snapshot.anchor_before(range.start)
+ ..buffer_snapshot.anchor_after(range.end)
+ })
+ .map(DocumentRange::Text)
+ .collect::<Vec<_>>()
+ };
+ 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);
@@ -224,8 +224,8 @@ impl TabSnapshot {
range: Range<TabPoint>,
language_aware: bool,
text_highlights: Option<&'a TextHighlights>,
- hint_highlights: Option<HighlightStyle>,
- suggestion_highlights: Option<HighlightStyle>,
+ hint_highlight_style: Option<HighlightStyle>,
+ suggestion_highlight_style: Option<HighlightStyle>,
) -> TabChunks<'a> {
let (input_start, expanded_char_column, to_next_stop) =
self.to_fold_point(range.start, Bias::Left);
@@ -246,8 +246,8 @@ impl TabSnapshot {
input_start..input_end,
language_aware,
text_highlights,
- hint_highlights,
- suggestion_highlights,
+ hint_highlight_style,
+ suggestion_highlight_style,
),
input_column,
column: expanded_char_column,
@@ -576,8 +576,8 @@ impl WrapSnapshot {
rows: Range<u32>,
language_aware: bool,
text_highlights: Option<&'a TextHighlights>,
- hint_highlights: Option<HighlightStyle>,
- suggestion_highlights: Option<HighlightStyle>,
+ hint_highlight_style: Option<HighlightStyle>,
+ suggestion_highlight_style: Option<HighlightStyle>,
) -> WrapChunks<'a> {
let output_start = WrapPoint::new(rows.start, 0);
let output_end = WrapPoint::new(rows.end, 0);
@@ -595,8 +595,8 @@ impl WrapSnapshot {
input_start..input_end,
language_aware,
text_highlights,
- hint_highlights,
- suggestion_highlights,
+ hint_highlight_style,
+ suggestion_highlight_style,
),
input_chunk: Default::default(),
output_position: output_start,
@@ -65,7 +65,7 @@ use language::{
OffsetUtf16, Point, Selection, SelectionGoal, TransactionId,
};
use link_go_to_definition::{
- hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
+ hide_link_definition, show_link_definition, DocumentRange, InlayRange, LinkGoToDefinitionState,
};
use log::error;
use multi_buffer::ToOffsetUtf16;
@@ -535,6 +535,8 @@ type CompletionId = usize;
type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
+type BackgroundHighlight = (fn(&Theme) -> Color, Vec<DocumentRange>);
+
pub struct Editor {
handle: WeakViewHandle<Self>,
buffer: ModelHandle<MultiBuffer>,
@@ -564,8 +566,7 @@ pub struct Editor {
show_wrap_guides: Option<bool>,
placeholder_text: Option<Arc<str>>,
highlighted_rows: Option<Range<u32>>,
- #[allow(clippy::type_complexity)]
- background_highlights: BTreeMap<TypeId, (fn(&Theme) -> Color, Vec<Range<Anchor>>)>,
+ background_highlights: BTreeMap<TypeId, BackgroundHighlight>,
nav_history: Option<ItemNavHistory>,
context_menu: Option<ContextMenu>,
mouse_context_menu: ViewHandle<context_menu::ContextMenu>,
@@ -4881,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 =
@@ -6758,10 +6758,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 +6824,15 @@ impl Editor {
let ranges = this
.clear_background_highlights::<DocumentHighlightWrite>(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::<DocumentHighlightRead>(cx)
.into_iter()
- .flat_map(|(_, ranges)| ranges),
+ .flat_map(|(_, ranges)| {
+ ranges.into_iter().filter_map(|range| range.as_text_range())
+ }),
)
.collect();
@@ -7488,16 +7500,36 @@ impl Editor {
color_fetcher: fn(&Theme) -> Color,
cx: &mut ViewContext<Self>,
) {
- self.background_highlights
- .insert(TypeId::of::<T>(), (color_fetcher, ranges));
+ self.background_highlights.insert(
+ TypeId::of::<T>(),
+ (
+ color_fetcher,
+ ranges.into_iter().map(DocumentRange::Text).collect(),
+ ),
+ );
+ cx.notify();
+ }
+
+ pub fn highlight_inlay_background<T: 'static>(
+ &mut self,
+ ranges: Vec<InlayRange>,
+ color_fetcher: fn(&Theme) -> Color,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.background_highlights.insert(
+ TypeId::of::<T>(),
+ (
+ color_fetcher,
+ ranges.into_iter().map(DocumentRange::Inlay).collect(),
+ ),
+ );
cx.notify();
}
- #[allow(clippy::type_complexity)]
pub fn clear_background_highlights<T: 'static>(
&mut self,
cx: &mut ViewContext<Self>,
- ) -> Option<(fn(&Theme) -> Color, Vec<Range<Anchor>>)> {
+ ) -> Option<BackgroundHighlight> {
let highlights = self.background_highlights.remove(&TypeId::of::<T>());
if highlights.is_some() {
cx.notify();
@@ -7522,7 +7554,8 @@ impl Editor {
&'a self,
position: Anchor,
buffer: &'a MultiBufferSnapshot,
- ) -> impl 'a + Iterator<Item = &Range<Anchor>> {
+ display_snapshot: &'a DisplaySnapshot,
+ ) -> impl 'a + Iterator<Item = &DocumentRange> {
let read_highlights = self
.background_highlights
.get(&TypeId::of::<DocumentHighlightRead>())
@@ -7531,14 +7564,16 @@ impl Editor {
.background_highlights
.get(&TypeId::of::<DocumentHighlightWrite>())
.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 +7584,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 +7599,15 @@ impl Editor {
display_snapshot: &DisplaySnapshot,
theme: &Theme,
) -> Vec<(Range<DisplayPoint>, 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,61 +7617,16 @@ 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);
- results.push((start..end, color))
- }
- }
- results
- }
- pub fn background_highlights_in_range_for<T: 'static>(
- &self,
- search_range: Range<Anchor>,
- display_snapshot: &DisplaySnapshot,
- theme: &Theme,
- ) -> Vec<(Range<DisplayPoint>, Color)> {
- let mut results = Vec::new();
- let buffer = &display_snapshot.buffer_snapshot;
- let Some((color_fetcher, ranges)) = self.background_highlights
- .get(&TypeId::of::<T>()) 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 = 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))
}
- 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
}
@@ -7640,15 +7636,18 @@ impl Editor {
display_snapshot: &DisplaySnapshot,
count: usize,
) -> Vec<RangeInclusive<DisplayPoint>> {
+ 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::<T>()) 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 {
@@ -7668,19 +7667,24 @@ impl Editor {
let mut start_row: Option<Point> = None;
let mut end_row: Option<Point> = 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)
+ .to_point(display_snapshot);
if let Some(current_row) = &end_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)
+ .to_point(display_snapshot);
if start_row.is_none() {
assert_eq!(end_row, None);
@@ -7718,24 +7722,32 @@ impl Editor {
cx.notify();
}
+ pub fn highlight_inlays<T: 'static>(
+ &mut self,
+ ranges: Vec<InlayRange>,
+ style: HighlightStyle,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.display_map.update(cx, |map, _| {
+ map.highlight_inlays(TypeId::of::<T>(), ranges, style)
+ });
+ cx.notify();
+ }
+
pub fn text_highlights<'a, T: 'static>(
&'a self,
cx: &'a AppContext,
- ) -> Option<(HighlightStyle, &'a [Range<Anchor>])> {
+ ) -> Option<(HighlightStyle, &'a [DocumentRange])> {
self.display_map.read(cx).text_highlights(TypeId::of::<T>())
}
- pub fn clear_text_highlights<T: 'static>(
- &mut self,
- cx: &mut ViewContext<Self>,
- ) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
- let highlights = self
+ pub fn clear_text_highlights<T: 'static>(&mut self, cx: &mut ViewContext<Self>) {
+ let text_highlights = self
.display_map
.update(cx, |map, _| map.clear_text_highlights(TypeId::of::<T>()));
- if highlights.is_some() {
+ if text_highlights.is_some() {
cx.notify();
}
- highlights
}
pub fn show_local_cursors(&self, cx: &AppContext) -> bool {
@@ -7942,6 +7954,7 @@ impl Editor {
Some(
ranges
.iter()
+ .filter_map(|range| range.as_text_range())
.map(move |range| {
range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot)
})
@@ -8123,6 +8136,19 @@ impl Editor {
}
}
+fn document_to_inlay_range(
+ range: &DocumentRange,
+ snapshot: &DisplaySnapshot,
+) -> Range<InlayOffset> {
+ 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,
@@ -8307,14 +8333,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;
@@ -8398,6 +8421,7 @@ impl View for Editor {
fn marked_text_range(&self, cx: &AppContext) -> Option<Range<usize>> {
let snapshot = self.buffer.read(cx).read(cx);
let range = self.text_highlights::<InputComposition>(cx)?.1.get(0)?;
+ let range = range.as_text_range()?;
Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0)
}
@@ -13,6 +13,7 @@ use crate::{
},
link_go_to_definition::{
go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link,
+ update_inlay_link_and_hover_points, GoToDefinitionTrigger,
},
mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt,
};
@@ -287,13 +288,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 +330,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,17 +358,15 @@ 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 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
@@ -383,17 +386,22 @@ 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
};
- 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();
@@ -422,13 +430,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),
},
@@ -455,10 +462,34 @@ 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 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,
+ GoToDefinitionTrigger::Text(point),
+ cmd,
+ shift,
+ cx,
+ );
+ hover_at(editor, Some(point), cx);
+ }
+ None => {
+ update_inlay_link_and_hover_points(
+ &position_map.snapshot,
+ point_for_position,
+ editor,
+ cmd,
+ shift,
+ cx,
+ );
+ }
+ }
+ } else {
+ update_go_to_definition_link(editor, GoToDefinitionTrigger::None, cmd, shift, cx);
+ hover_at(editor, None, cx);
+ }
true
}
@@ -909,7 +940,7 @@ impl EditorElement {
&text,
cursor_row_layout.font_size(),
&[(
- text.len(),
+ text.chars().count(),
RunStyle {
font_id,
color: style.background,
@@ -2632,22 +2663,42 @@ struct PositionMap {
snapshot: EditorSnapshot,
}
+#[derive(Debug, Copy, Clone)]
+pub struct PointForPosition {
+ pub previous_valid: DisplayPoint,
+ pub next_valid: DisplayPoint,
+ pub exact_unclipped: DisplayPoint,
+ pub 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<DisplayPoint> {
+ 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)
@@ -2661,11 +2712,18 @@ impl PositionMap {
(0, x)
};
- let mut target_point = DisplayPoint::new(row, column);
- let point = self.snapshot.clip_point(target_point, Bias::Left);
- *target_point.column_mut() += (x_overshoot / self.em_advance) as u32;
-
- (point, target_point)
+ 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);
+
+ 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,
+ }
}
}
@@ -2919,23 +2977,6 @@ impl HighlightedRange {
}
}
-fn position_to_display_point(
- position: Vector2F,
- text_bounds: RectF,
- position_map: &PositionMap,
-) -> Option<DisplayPoint> {
- 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<DisplayPoint>,
content_origin: Vector2F,
@@ -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::{
@@ -11,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;
@@ -46,6 +48,105 @@ pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewC
}
}
+pub struct InlayHover {
+ pub excerpt: ExcerptId,
+ pub triggered_from: InlayOffset,
+ pub range: InlayRange,
+ pub tooltip: HoverBlock,
+}
+
+pub fn find_hovered_hint_part(
+ label_parts: Vec<InlayHintLabelPart>,
+ hint_range: Range<InlayOffset>,
+ hovered_offset: InlayOffset,
+) -> Option<(InlayHintLabelPart, Range<InlayOffset>)> {
+ 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<Editor>) {
+ if settings::get::<EditorSettings>(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::<HoverState>(
+ 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 +211,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 +325,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 +333,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::<HoverState>(
- vec![hover_popover.symbol_range.clone()],
+ vec![symbol_range],
|theme| theme.editor.hover_popover.highlight,
cx,
);
@@ -497,7 +606,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 +634,7 @@ impl HoverState {
#[derive(Debug, Clone)]
pub struct InfoPopover {
pub project: ModelHandle<Project>,
- pub symbol_range: Range<Anchor>,
+ symbol_range: DocumentRange,
pub blocks: Vec<HoverBlock>,
language: Option<Arc<Language>>,
rendered_content: Option<RenderedInfo>,
@@ -692,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;
@@ -1131,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>(T);
+
+ fn main() {
+ let variableˇ = TestNewType(TestStruct);
+ }
+ "});
+
+ let hint_start_offset = cx.ranges(indoc! {"
+ struct TestStruct;
+
+ // ==================
+
+ struct TestNewType<T>(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>(T);
+
+ fn main() {
+ let variable = TestNewType(TestStruct);
+ }
+ "});
+ let struct_target_range = cx.lsp_range(indoc! {"
+ struct «TestStruct»;
+
+ // ==================
+
+ struct TestNewType<T>(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<TestStruct>";
+ let closure_uri = uri.clone();
+ cx.lsp
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(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>(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::<lsp::request::InlayHintResolveRequest, _, _>(
+ 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<TestStruct>`
+ 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"
+ );
+ });
+ }
}
@@ -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<InlayId>,
remove_from_cache: HashSet<InlayId>,
- add_to_cache: HashSet<InlayHint>,
+ add_to_cache: Vec<InlayHint>,
}
#[derive(Debug, Clone, Copy)]
@@ -386,6 +386,17 @@ impl InlayHintCache {
self.hints.clear();
}
+ pub fn hint_by_id(&self, excerpt_id: ExcerptId, hint_id: InlayId) -> Option<InlayHint> {
+ self.hints
+ .get(&excerpt_id)?
+ .read()
+ .hints
+ .iter()
+ .find(|&(id, _)| id == &hint_id)
+ .map(|(_, hint)| hint)
+ .cloned()
+ }
+
pub fn hints(&self) -> Vec<InlayHint> {
let mut hints = Vec::new();
for excerpt_hints in self.hints.values() {
@@ -398,6 +409,75 @@ 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 {
+ 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)
+ {
+ 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(
@@ -621,7 +701,7 @@ fn calculate_hint_updates(
cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
visible_hints: &[Inlay],
) -> Option<ExcerptHintsUpdate> {
- let mut add_to_cache: HashSet<InlayHint> = HashSet::default();
+ let mut add_to_cache = Vec::<InlayHint>::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) {
@@ -634,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,
}
@@ -648,7 +736,7 @@ fn calculate_hint_updates(
None => true,
};
if missing_from_cache {
- add_to_cache.insert(new_hint);
+ add_to_cache.push(new_hint);
}
}
@@ -740,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),
};
@@ -806,7 +904,7 @@ fn apply_hint_update(
}
#[cfg(test)]
-mod tests {
+pub mod tests {
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use crate::{
@@ -2891,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<String> {
+ pub fn cached_hint_labels(editor: &Editor) -> Vec<String> {
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());
}
}
@@ -2907,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<String> {
+ pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
let mut hints = editor
.visible_inlay_hints(cx)
.into_iter()
@@ -615,7 +615,7 @@ impl Item for Editor {
fn workspace_deactivated(&mut self, cx: &mut ViewContext<Self>) {
hide_link_definition(self, cx);
- self.link_go_to_definition_state.last_mouse_location = None;
+ self.link_go_to_definition_state.last_trigger_point = None;
}
fn is_dirty(&self, cx: &AppContext) -> bool {
@@ -1,22 +1,101 @@
-use crate::{Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase};
+use crate::{
+ 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;
#[derive(Debug, Default)]
pub struct LinkGoToDefinitionState {
- pub last_mouse_location: Option<Anchor>,
- pub symbol_range: Option<Range<Anchor>>,
+ pub last_trigger_point: Option<TriggerPoint>,
+ pub symbol_range: Option<DocumentRange>,
pub kind: Option<LinkDefinitionKind>,
pub definitions: Vec<LocationLink>,
pub task: Option<Task<Option<()>>>,
}
+pub enum GoToDefinitionTrigger {
+ Text(DisplayPoint),
+ InlayHint(InlayRange, LocationLink),
+ None,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct InlayRange {
+ pub inlay_position: Anchor,
+ pub highlight_start: InlayOffset,
+ pub highlight_end: InlayOffset,
+}
+
+#[derive(Debug, Clone)]
+pub enum TriggerPoint {
+ Text(Anchor),
+ InlayHint(InlayRange, LocationLink),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum DocumentRange {
+ Text(Range<Anchor>),
+ Inlay(InlayRange),
+}
+
+impl DocumentRange {
+ pub fn as_text_range(&self) -> Option<Range<Anchor>> {
+ 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)) => {
+ 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()
+ }
+ (DocumentRange::Inlay(range), TriggerPoint::InlayHint(point, _)) => {
+ range.highlight_start.cmp(&point.highlight_end).is_le()
+ && range.highlight_end.cmp(&point.highlight_end).is_ge()
+ }
+ (DocumentRange::Inlay(_), TriggerPoint::Text(_))
+ | (DocumentRange::Text(_), TriggerPoint::InlayHint(_, _)) => false,
+ }
+ }
+}
+
+impl TriggerPoint {
+ fn anchor(&self) -> &Anchor {
+ match self {
+ TriggerPoint::Text(anchor) => anchor,
+ TriggerPoint::InlayHint(coordinates, _) => &coordinates.inlay_position,
+ }
+ }
+
+ pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind {
+ match self {
+ TriggerPoint::Text(_) => {
+ if shift {
+ LinkDefinitionKind::Type
+ } else {
+ LinkDefinitionKind::Symbol
+ }
+ }
+ TriggerPoint::InlayHint(_, _) => LinkDefinitionKind::Type,
+ }
+ }
+}
+
pub fn update_go_to_definition_link(
editor: &mut Editor,
- point: Option<DisplayPoint>,
+ origin: GoToDefinitionTrigger,
cmd_held: bool,
shift_held: bool,
cx: &mut ViewContext<Editor>,
@@ -25,23 +104,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 +135,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;
}
}
@@ -64,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,
@@ -73,7 +340,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<Editor>,
) {
@@ -86,10 +353,11 @@ pub fn show_link_definition(
return;
}
+ let trigger_anchor = trigger_point.anchor();
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 +367,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 {
@@ -114,52 +382,52 @@ 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_point, &snapshot.buffer_snapshot)
- .is_le();
-
- let point_before_end = symbol_range
- .end
- .cmp(&trigger_point, &snapshot.buffer_snapshot)
- .is_ge();
-
- let point_within_range = point_after_start && point_before_end;
- if point_within_range && same_kind {
+ if same_kind && symbol_range.point_within_range(&trigger_point, &snapshot) {
return;
}
}
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),
-
- LinkDefinitionKind::Type => {
- project.type_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)
+ }
- 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);
+
+ DocumentRange::Text(start..end)
+ })
+ }),
+ definition_result,
+ )
+ })
+ }
+ TriggerPoint::InlayHint(trigger_source, trigger_target) => Some((
+ Some(DocumentRange::Inlay(trigger_source.clone())),
+ vec![trigger_target.clone()],
+ )),
+ };
this.update(&mut cx, |this, cx| {
// Clear any existing highlights
@@ -199,22 +467,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_point);
-
- 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::<LinkGoToDefinitionState>(
- 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);
+ DocumentRange::Text(
+ snapshot.anchor_before(offset_range.start)
+ ..snapshot.anchor_after(offset_range.end),
+ )
+ }
+ TriggerPoint::InlayHint(inlay_coordinates, _) => {
+ DocumentRange::Inlay(inlay_coordinates)
+ }
+ });
+
+ match highlight_range {
+ DocumentRange::Text(text_range) => this
+ .highlight_text::<LinkGoToDefinitionState>(
+ vec![text_range],
+ style,
+ cx,
+ ),
+ DocumentRange::Inlay(inlay_coordinates) => this
+ .highlight_inlays::<LinkGoToDefinitionState>(
+ vec![inlay_coordinates],
+ style,
+ cx,
+ ),
+ }
} else {
hide_link_definition(this, cx);
}
@@ -245,7 +528,7 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
pub fn go_to_fetched_definition(
editor: &mut Editor,
- point: DisplayPoint,
+ point: PointForPosition,
split: bool,
cx: &mut ViewContext<Editor>,
) {
@@ -254,7 +537,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<Editor>,
) {
@@ -264,7 +547,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<Editor>,
) {
@@ -282,7 +565,7 @@ fn go_to_fetched_definition_of_kind(
} else {
editor.select(
SelectPhase::Begin {
- position: point,
+ position: point.next_valid,
add: false,
click_count: 1,
},
@@ -299,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) {
@@ -355,7 +645,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();
@@ -406,7 +702,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();
@@ -461,7 +757,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 +784,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::<GotoDefinition, _, _>(move |url, _, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink {
@@ -495,7 +797,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 +825,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 +848,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 +913,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::<LinkGoToDefinitionState>(indoc! {"
@@ -607,7 +933,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::<LinkGoToDefinitionState>(indoc! {"
@@ -617,7 +949,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
@@ -658,7 +990,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();
@@ -703,7 +1035,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());
@@ -713,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 hint_label = ": TestStruct";
+ cx.lsp
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(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: hint_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![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;
+
+ 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: (hint_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::<LinkGoToDefinitionState>()
+ .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::<Vec<_>>();
+
+ 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 + hint_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::<LinkGoToDefinitionState>()
+ .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::<Vec<_>>();
+
+ 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;
+ }
+ "});
+ }
}
@@ -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()
});
@@ -240,6 +241,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);
@@ -1,21 +1,23 @@
use crate::{
DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel,
- InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink,
- MarkupContent, Project, ProjectTransaction,
+ InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Item, Location, LocationLink,
+ MarkupContent, Project, ProjectTransaction, ResolveState,
};
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, CachedLspAdapter, CharKind, CodeAction,
- Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped,
+ range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind,
+ 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 {
@@ -1431,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 {
@@ -1499,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 {
@@ -1776,6 +1778,459 @@ impl LspCommand for OnTypeFormatting {
}
}
+impl InlayHints {
+ pub async fn lsp_to_project_hint(
+ lsp_hint: lsp::InlayHint,
+ project: &ModelHandle<Project>,
+ buffer_handle: &ModelHandle<Buffer>,
+ server_id: LanguageServerId,
+ resolve_state: ResolveState,
+ force_no_type_left_padding: bool,
+ cx: &mut AsyncAppContext,
+ ) -> anyhow::Result<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 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)
+ };
+
+ Ok(InlayHint {
+ position,
+ padding_left,
+ padding_right: lsp_hint.padding_right.unwrap_or(false),
+ label,
+ 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,
+ })
+ }
+
+ async fn lsp_inlay_label_to_project(
+ buffer: &ModelHandle<Buffer>,
+ project: &ModelHandle<Project>,
+ server_id: LanguageServerId,
+ lsp_label: lsp::InlayHintLabel,
+ cx: &mut AsyncAppContext,
+ ) -> anyhow::Result<InlayHintLabel> {
+ 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 {
+ let (state, lsp_resolve_state) = match response_hint.resolve_state {
+ ResolveState::Resolved => (0, None),
+ ResolveState::CanResolve(server_id, resolve_data) => (
+ 1,
+ 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::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<Project>,
+ cx: &mut AsyncAppContext,
+ ) -> anyhow::Result<InlayHint> {
+ 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::<Option<lsp::LSPAny>>(&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,
+ })
+ }
+
+ pub fn project_to_lsp_hint(
+ hint: InlayHint,
+ project: &ModelHandle<Project>,
+ 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, 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(&location_snapshot)
+ ..location.range.end.to_point_utf16(&location_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,
+ },
+ }
+ }
+
+ 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)]
impl LspCommand for InlayHints {
type Response = Vec<InlayHint>;
@@ -1816,8 +2271,9 @@ impl LspCommand for InlayHints {
buffer: ModelHandle<Buffer>,
server_id: LanguageServerId,
mut cx: AsyncAppContext,
- ) -> Result<Vec<InlayHint>> {
- let (lsp_adapter, _) = language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
+ ) -> anyhow::Result<Vec<InlayHint>> {
+ 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.
@@ -1827,93 +2283,34 @@ 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| {
- let origin_buffer = buffer.read(cx);
- Ok(message
- .unwrap_or_default()
- .into_iter()
- .map(|lsp_hint| {
- 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,
- })
- }
- }),
- }
- })
- .collect())
- })
+
+ let hints = message.unwrap_or_default().into_iter().map(|lsp_hint| {
+ 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();
+ 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::<anyhow::Result<_>>()
+ .context("lsp to project inlay hints conversion")
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::InlayHints {
@@ -1954,28 +2351,12 @@ impl LspCommand for InlayHints {
_: &mut Project,
_: PeerId,
buffer_version: &clock::Global,
- _: &mut AppContext,
+ cx: &mut AppContext,
) -> proto::InlayHintsResponse {
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,
- 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(),
- },
- )),
- }),
- })
+ .map(|response_hint| InlayHints::project_to_proto_hint(response_hint, cx))
.collect(),
version: serialize_version(buffer_version),
}
@@ -1987,7 +2368,7 @@ impl LspCommand for InlayHints {
project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>,
mut cx: AsyncAppContext,
- ) -> Result<Vec<InlayHint>> {
+ ) -> anyhow::Result<Vec<InlayHint>> {
buffer
.update(&mut cx, |buffer, _| {
buffer.wait_for_version(deserialize_version(&message.version))
@@ -1996,82 +2377,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 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,
- })
- }
- })
- }),
- };
-
- hints.push(hint);
+ hints.push(InlayHints::proto_to_project_hint(message_hint, &project, &mut cx).await?);
}
Ok(hints)
@@ -333,15 +333,22 @@ pub struct Location {
pub range: Range<language::Anchor>,
}
-#[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<InlayHintKind>,
pub padding_left: bool,
pub padding_right: bool,
pub tooltip: Option<InlayHintTooltip>,
+ pub resolve_state: ResolveState,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ResolveState {
+ Resolved,
+ CanResolve(LanguageServerId, Option<lsp::LSPAny>),
+ Resolving,
}
impl InlayHint {
@@ -353,34 +360,34 @@ impl InlayHint {
}
}
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InlayHintLabel {
String(String),
LabelParts(Vec<InlayHintLabelPart>),
}
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InlayHintLabelPart {
pub value: String,
pub tooltip: Option<InlayHintLabelPartTooltip>,
pub location: Option<Location>,
}
-#[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,
}
@@ -414,7 +421,7 @@ pub struct HoverBlock {
pub kind: HoverBlockKind,
}
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
pub enum HoverBlockKind {
PlainText,
Markdown,
@@ -551,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);
@@ -4969,7 +4977,7 @@ impl Project {
buffer_handle: ModelHandle<Buffer>,
range: Range<T>,
cx: &mut ModelContext<Self>,
- ) -> Task<Result<Vec<InlayHint>>> {
+ ) -> Task<anyhow::Result<Vec<InlayHint>>> {
let buffer = buffer_handle.read(cx);
let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
let range_start = range.start;
@@ -5019,6 +5027,73 @@ impl Project {
}
}
+ pub fn resolve_inlay_hint(
+ &self,
+ hint: InlayHint,
+ buffer_handle: ModelHandle<Buffer>,
+ server_id: LanguageServerId,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<anyhow::Result<InlayHint>> {
+ 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(hint));
+ };
+ if !InlayHints::can_resolve_inlays(lang_server.capabilities()) {
+ return Task::ready(Ok(hint));
+ }
+
+ let buffer_snapshot = buffer.snapshot();
+ cx.spawn(|project, mut cx| async move {
+ let resolve_task = lang_server.request::<lsp::request::InlayHintResolveRequest>(
+ 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 = InlayHints::lsp_to_project_hint(
+ resolved_hint,
+ &project,
+ &buffer_handle,
+ server_id,
+ ResolveState::Resolved,
+ false,
+ &mut cx,
+ )
+ .await?;
+ Ok(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.clone(), 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
+ .context("inlay hints proto resolve response conversion")
+ }
+ None => Ok(hint),
+ }
+ })
+ } else {
+ Task::ready(Err(anyhow!("project does not have a remote id")))
+ }
+ }
+
#[allow(clippy::type_complexity)]
pub fn search(
&self,
@@ -6816,6 +6891,43 @@ impl Project {
}))
}
+ async fn handle_resolve_inlay_hint(
+ this: ModelHandle<Self>,
+ envelope: TypedEnvelope<proto::ResolveInlayHint>,
+ _: Arc<Client>,
+ mut cx: AsyncAppContext,
+ ) -> Result<proto::ResolveInlayHintResponse> {
+ 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 response_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")?;
+ let resolved_hint = cx.read(|cx| InlayHints::project_to_proto_hint(response_hint, cx));
+
+ Ok(proto::ResolveInlayHintResponse {
+ hint: Some(resolved_hint),
+ })
+ }
+
async fn handle_refresh_inlay_hints(
this: ModelHandle<Self>,
_: TypedEnvelope<proto::RefreshInlayHints>,
@@ -128,6 +128,8 @@ message Envelope {
InlayHints inlay_hints = 116;
InlayHintsResponse inlay_hints_response = 117;
+ ResolveInlayHint resolve_inlay_hint = 137;
+ ResolveInlayHintResponse resolve_inlay_hint_response = 138;
RefreshInlayHints refresh_inlay_hints = 118;
CreateChannel create_channel = 119;
@@ -754,6 +756,7 @@ message InlayHint {
bool padding_left = 4;
bool padding_right = 5;
InlayHintTooltip tooltip = 6;
+ ResolveState resolve_state = 7;
}
message InlayHintLabel {
@@ -787,12 +790,39 @@ 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;
+ 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;
}
@@ -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,
@@ -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;