Handle inlay hints resolve, support dynamic hints (#2890)

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
This commit is contained in:
Kirill Bulatov 2023-08-25 15:16:13 +03:00 committed by GitHub
commit b50762c821
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 2267 additions and 594 deletions

2
Cargo.lock generated
View File

@ -1220,7 +1220,7 @@ dependencies = [
"tempfile",
"text",
"thiserror",
"time 0.3.24",
"time 0.3.27",
"tiny_http",
"url",
"util",

View File

@ -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 text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range<Anchor>])> {
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, &[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()
}

View File

@ -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,

View File

@ -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,

View File

@ -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_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 = {
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, highlights) in text_highlights.iter() {
let style = highlights.0;
let ranges = &highlights.1;
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 = 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,
});
let start_ix = match ranges.binary_search_by(|probe| {
let cmp = self
.document_to_inlay_range(probe)
.end
.cmp(&transform_start);
if cmp.is_gt() {
cmp::Ordering::Greater
} else {
cmp::Ordering::Less
}
}) {
Ok(i) | Err(i) => i,
};
for range in &ranges[start_ix..] {
let range = self.document_to_inlay_range(range);
if range.start.cmp(&transform_end).is_ge() {
break;
}
highlight_endpoints.push(HighlightEndpoint {
offset: range.start,
is_start: true,
tag: *tag,
style,
});
highlight_endpoints.push(HighlightEndpoint {
offset: range.end,
is_start: false,
tag: *tag,
style,
});
}
}
@ -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);

View File

@ -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,

View File

@ -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,

View File

@ -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,63 +7617,18 @@ impl Editor {
Ok(i) | Err(i) => i,
};
for range in &ranges[start_ix..] {
if range.start.cmp(&search_range.end, buffer).is_ge() {
let range = document_to_inlay_range(range, display_snapshot);
if range.start.cmp(&search_range.end).is_ge() {
break;
}
let start = range
.start
.to_point(buffer)
.to_display_point(display_snapshot);
let end = range
.end
.to_point(buffer)
.to_display_point(display_snapshot);
let start = display_snapshot.inlay_offset_to_display_point(range.start, Bias::Left);
let end = display_snapshot.inlay_offset_to_display_point(range.end, Bias::Right);
results.push((start..end, color))
}
}
results
}
pub fn background_highlights_in_range_for<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 = range
.start
.to_point(buffer)
.to_display_point(display_snapshot);
let end = range
.end
.to_point(buffer)
.to_display_point(display_snapshot);
results.push((start..end, color))
}
results
}
pub fn background_highlight_row_ranges<T: 'static>(
&self,
@ -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)
}

View File

@ -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;
let mut exact_unclipped = DisplayPoint::new(row, column);
let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left);
let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right);
(point, target_point)
let column_overshoot_after_line_end = (x_overshoot_after_line_end / self.em_advance) as u32;
*exact_unclipped.column_mut() += column_overshoot_after_line_end;
PointForPosition {
previous_valid,
next_valid,
exact_unclipped,
column_overshoot_after_line_end,
}
}
}
@ -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,

View File

@ -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"
);
});
}
}

View File

@ -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()

View File

@ -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 {

View File

@ -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),
let result = match &trigger_point {
TriggerPoint::Text(_) => {
// query the LSP for definition info
cx.update(|cx| {
project.update(cx, |project, cx| match definition_kind {
LinkDefinitionKind::Symbol => {
project.definition(&buffer, buffer_position, cx)
}
LinkDefinitionKind::Type => {
project.type_definition(&buffer, buffer_position, cx)
}
})
});
let result = definition_request.await.ok().map(|definition_result| {
(
definition_result.iter().find_map(|link| {
link.origin.as_ref().map(|origin| {
let start = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id.clone(), origin.range.start);
let end = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id.clone(), origin.range.end);
start..end
LinkDefinitionKind::Type => {
project.type_definition(&buffer, buffer_position, cx)
}
})
}),
definition_result,
)
});
})
.await
.ok()
.map(|definition_result| {
(
definition_result.iter().find_map(|link| {
link.origin.as_ref().map(|origin| {
let start = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id.clone(), origin.range.start);
let end = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id.clone(), origin.range.end);
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;
}
"});
}
}

View File

@ -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);

View File

@ -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)

View File

@ -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>,

View File

@ -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;
}

View File

@ -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,

View File

@ -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;