1use crate::{
2 display_map::InlayOffset,
3 link_go_to_definition::{InlayHighlight, RangeInEditor},
4 Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
5 ExcerptId, RangeToAnchorExt,
6};
7use futures::FutureExt;
8use gpui::{AnyElement, AppContext, Model, Task, ViewContext, WeakView};
9use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
10use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
11use settings::Settings;
12use std::{ops::Range, sync::Arc, time::Duration};
13use util::TryFutureExt;
14use workspace::Workspace;
15
16pub const HOVER_DELAY_MILLIS: u64 = 350;
17pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
18
19pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
20pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
21pub const HOVER_POPOVER_GAP: f32 = 10.;
22
23// actions!(editor, [Hover]);
24
25pub fn init(cx: &mut AppContext) {
26 // cx.add_action(hover);
27}
28
29// todo!()
30// /// Bindable action which uses the most recent selection head to trigger a hover
31// pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
32// let head = editor.selections.newest_display(cx).head();
33// show_hover(editor, head, true, cx);
34// }
35
36/// The internal hover action dispatches between `show_hover` or `hide_hover`
37/// depending on whether a point to hover over is provided.
38pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) {
39 if EditorSettings::get_global(cx).hover_popover_enabled {
40 if let Some(point) = point {
41 show_hover(editor, point, false, cx);
42 } else {
43 hide_hover(editor, cx);
44 }
45 }
46}
47
48pub struct InlayHover {
49 pub excerpt: ExcerptId,
50 pub range: InlayHighlight,
51 pub tooltip: HoverBlock,
52}
53
54pub fn find_hovered_hint_part(
55 label_parts: Vec<InlayHintLabelPart>,
56 hint_start: InlayOffset,
57 hovered_offset: InlayOffset,
58) -> Option<(InlayHintLabelPart, Range<InlayOffset>)> {
59 if hovered_offset >= hint_start {
60 let mut hovered_character = (hovered_offset - hint_start).0;
61 let mut part_start = hint_start;
62 for part in label_parts {
63 let part_len = part.value.chars().count();
64 if hovered_character > part_len {
65 hovered_character -= part_len;
66 part_start.0 += part_len;
67 } else {
68 let part_end = InlayOffset(part_start.0 + part_len);
69 return Some((part, part_start..part_end));
70 }
71 }
72 }
73 None
74}
75
76pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext<Editor>) {
77 todo!()
78 // if EditorSettings::get_global(cx).hover_popover_enabled {
79 // if editor.pending_rename.is_some() {
80 // return;
81 // }
82
83 // let Some(project) = editor.project.clone() else {
84 // return;
85 // };
86
87 // if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
88 // if let RangeInEditor::Inlay(range) = symbol_range {
89 // if range == &inlay_hover.range {
90 // // Hover triggered from same location as last time. Don't show again.
91 // return;
92 // }
93 // }
94 // hide_hover(editor, cx);
95 // }
96
97 // let task = cx.spawn(|this, mut cx| {
98 // async move {
99 // cx.background_executor()
100 // .timer(Duration::from_millis(HOVER_DELAY_MILLIS))
101 // .await;
102 // this.update(&mut cx, |this, _| {
103 // this.hover_state.diagnostic_popover = None;
104 // })?;
105
106 // let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?;
107 // let blocks = vec![inlay_hover.tooltip];
108 // let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
109
110 // let hover_popover = InfoPopover {
111 // project: project.clone(),
112 // symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
113 // blocks,
114 // parsed_content,
115 // };
116
117 // this.update(&mut cx, |this, cx| {
118 // // Highlight the selected symbol using a background highlight
119 // this.highlight_inlay_background::<HoverState>(
120 // vec![inlay_hover.range],
121 // |theme| theme.editor.hover_popover.highlight,
122 // cx,
123 // );
124 // this.hover_state.info_popover = Some(hover_popover);
125 // cx.notify();
126 // })?;
127
128 // anyhow::Ok(())
129 // }
130 // .log_err()
131 // });
132
133 // editor.hover_state.info_task = Some(task);
134 // }
135}
136
137/// Hides the type information popup.
138/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
139/// selections changed.
140pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
141 let did_hide = editor.hover_state.info_popover.take().is_some()
142 | editor.hover_state.diagnostic_popover.take().is_some();
143
144 editor.hover_state.info_task = None;
145 editor.hover_state.triggered_from = None;
146
147 editor.clear_background_highlights::<HoverState>(cx);
148
149 if did_hide {
150 cx.notify();
151 }
152
153 did_hide
154}
155
156/// Queries the LSP and shows type info and documentation
157/// about the symbol the mouse is currently hovering over.
158/// Triggered by the `Hover` action when the cursor may be over a symbol.
159fn show_hover(
160 editor: &mut Editor,
161 point: DisplayPoint,
162 ignore_timeout: bool,
163 cx: &mut ViewContext<Editor>,
164) {
165 if editor.pending_rename.is_some() {
166 return;
167 }
168
169 let snapshot = editor.snapshot(cx);
170 let multibuffer_offset = point.to_offset(&snapshot.display_snapshot, Bias::Left);
171
172 let (buffer, buffer_position) = if let Some(output) = editor
173 .buffer
174 .read(cx)
175 .text_anchor_for_position(multibuffer_offset, cx)
176 {
177 output
178 } else {
179 return;
180 };
181
182 let excerpt_id = if let Some((excerpt_id, _, _)) = editor
183 .buffer()
184 .read(cx)
185 .excerpt_containing(multibuffer_offset, cx)
186 {
187 excerpt_id
188 } else {
189 return;
190 };
191
192 let project = if let Some(project) = editor.project.clone() {
193 project
194 } else {
195 return;
196 };
197
198 if !ignore_timeout {
199 if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
200 if symbol_range
201 .as_text_range()
202 .map(|range| {
203 range
204 .to_offset(&snapshot.buffer_snapshot)
205 .contains(&multibuffer_offset)
206 })
207 .unwrap_or(false)
208 {
209 // Hover triggered from same location as last time. Don't show again.
210 return;
211 } else {
212 hide_hover(editor, cx);
213 }
214 }
215 }
216
217 // Get input anchor
218 let anchor = snapshot
219 .buffer_snapshot
220 .anchor_at(multibuffer_offset, Bias::Left);
221
222 // Don't request again if the location is the same as the previous request
223 if let Some(triggered_from) = &editor.hover_state.triggered_from {
224 if triggered_from
225 .cmp(&anchor, &snapshot.buffer_snapshot)
226 .is_eq()
227 {
228 return;
229 }
230 }
231
232 let task = cx.spawn(|this, mut cx| {
233 async move {
234 // If we need to delay, delay a set amount initially before making the lsp request
235 let delay = if !ignore_timeout {
236 // Construct delay task to wait for later
237 let total_delay = Some(
238 cx.background_executor()
239 .timer(Duration::from_millis(HOVER_DELAY_MILLIS)),
240 );
241
242 cx.background_executor()
243 .timer(Duration::from_millis(HOVER_REQUEST_DELAY_MILLIS))
244 .await;
245 total_delay
246 } else {
247 None
248 };
249
250 // query the LSP for hover info
251 let hover_request = cx.update(|_, cx| {
252 project.update(cx, |project, cx| {
253 project.hover(&buffer, buffer_position, cx)
254 })
255 })?;
256
257 if let Some(delay) = delay {
258 delay.await;
259 }
260
261 // If there's a diagnostic, assign it on the hover state and notify
262 let local_diagnostic = snapshot
263 .buffer_snapshot
264 .diagnostics_in_range::<_, usize>(multibuffer_offset..multibuffer_offset, false)
265 // Find the entry with the most specific range
266 .min_by_key(|entry| entry.range.end - entry.range.start)
267 .map(|entry| DiagnosticEntry {
268 diagnostic: entry.diagnostic,
269 range: entry.range.to_anchors(&snapshot.buffer_snapshot),
270 });
271
272 // Pull the primary diagnostic out so we can jump to it if the popover is clicked
273 let primary_diagnostic = local_diagnostic.as_ref().and_then(|local_diagnostic| {
274 snapshot
275 .buffer_snapshot
276 .diagnostic_group::<usize>(local_diagnostic.diagnostic.group_id)
277 .find(|diagnostic| diagnostic.diagnostic.is_primary)
278 .map(|entry| DiagnosticEntry {
279 diagnostic: entry.diagnostic,
280 range: entry.range.to_anchors(&snapshot.buffer_snapshot),
281 })
282 });
283
284 this.update(&mut cx, |this, _| {
285 this.hover_state.diagnostic_popover =
286 local_diagnostic.map(|local_diagnostic| DiagnosticPopover {
287 local_diagnostic,
288 primary_diagnostic,
289 });
290 })?;
291
292 let hover_result = hover_request.await.ok().flatten();
293 let hover_popover = match hover_result {
294 Some(hover_result) if !hover_result.is_empty() => {
295 // Create symbol range of anchors for highlighting and filtering of future requests.
296 let range = if let Some(range) = hover_result.range {
297 let start = snapshot
298 .buffer_snapshot
299 .anchor_in_excerpt(excerpt_id.clone(), range.start);
300 let end = snapshot
301 .buffer_snapshot
302 .anchor_in_excerpt(excerpt_id.clone(), range.end);
303
304 start..end
305 } else {
306 anchor..anchor
307 };
308
309 let language_registry =
310 project.update(&mut cx, |p, _| p.languages().clone())?;
311 let blocks = hover_result.contents;
312 let language = hover_result.language;
313 let parsed_content = parse_blocks(&blocks, &language_registry, language).await;
314
315 Some(InfoPopover {
316 project: project.clone(),
317 symbol_range: RangeInEditor::Text(range),
318 blocks,
319 parsed_content,
320 })
321 }
322
323 _ => None,
324 };
325
326 this.update(&mut cx, |this, cx| {
327 if let Some(symbol_range) = hover_popover
328 .as_ref()
329 .and_then(|hover_popover| hover_popover.symbol_range.as_text_range())
330 {
331 // Highlight the selected symbol using a background highlight
332 this.highlight_background::<HoverState>(
333 vec![symbol_range],
334 |theme| theme.element_hover, // todo! update theme
335 cx,
336 );
337 } else {
338 this.clear_background_highlights::<HoverState>(cx);
339 }
340
341 this.hover_state.info_popover = hover_popover;
342 cx.notify();
343 })?;
344
345 Ok::<_, anyhow::Error>(())
346 }
347 .log_err()
348 });
349
350 editor.hover_state.info_task = Some(task);
351}
352
353async fn parse_blocks(
354 blocks: &[HoverBlock],
355 language_registry: &Arc<LanguageRegistry>,
356 language: Option<Arc<Language>>,
357) -> markdown::ParsedMarkdown {
358 let mut text = String::new();
359 let mut highlights = Vec::new();
360 let mut region_ranges = Vec::new();
361 let mut regions = Vec::new();
362
363 for block in blocks {
364 match &block.kind {
365 HoverBlockKind::PlainText => {
366 markdown::new_paragraph(&mut text, &mut Vec::new());
367 text.push_str(&block.text);
368 }
369
370 HoverBlockKind::Markdown => {
371 markdown::parse_markdown_block(
372 &block.text,
373 language_registry,
374 language.clone(),
375 &mut text,
376 &mut highlights,
377 &mut region_ranges,
378 &mut regions,
379 )
380 .await
381 }
382
383 HoverBlockKind::Code { language } => {
384 if let Some(language) = language_registry
385 .language_for_name(language)
386 .now_or_never()
387 .and_then(Result::ok)
388 {
389 markdown::highlight_code(&mut text, &mut highlights, &block.text, &language);
390 } else {
391 text.push_str(&block.text);
392 }
393 }
394 }
395 }
396
397 ParsedMarkdown {
398 text: text.trim().to_string(),
399 highlights,
400 region_ranges,
401 regions,
402 }
403}
404
405#[derive(Default)]
406pub struct HoverState {
407 pub info_popover: Option<InfoPopover>,
408 pub diagnostic_popover: Option<DiagnosticPopover>,
409 pub triggered_from: Option<Anchor>,
410 pub info_task: Option<Task<Option<()>>>,
411}
412
413impl HoverState {
414 pub fn visible(&self) -> bool {
415 self.info_popover.is_some() || self.diagnostic_popover.is_some()
416 }
417
418 pub fn render(
419 &mut self,
420 snapshot: &EditorSnapshot,
421 style: &EditorStyle,
422 visible_rows: Range<u32>,
423 workspace: Option<WeakView<Workspace>>,
424 cx: &mut ViewContext<Editor>,
425 ) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> {
426 todo!("old version below")
427 }
428 // // If there is a diagnostic, position the popovers based on that.
429 // // Otherwise use the start of the hover range
430 // let anchor = self
431 // .diagnostic_popover
432 // .as_ref()
433 // .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start)
434 // .or_else(|| {
435 // self.info_popover
436 // .as_ref()
437 // .map(|info_popover| match &info_popover.symbol_range {
438 // RangeInEditor::Text(range) => &range.start,
439 // RangeInEditor::Inlay(range) => &range.inlay_position,
440 // })
441 // })?;
442 // let point = anchor.to_display_point(&snapshot.display_snapshot);
443
444 // // Don't render if the relevant point isn't on screen
445 // if !self.visible() || !visible_rows.contains(&point.row()) {
446 // return None;
447 // }
448
449 // let mut elements = Vec::new();
450
451 // if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
452 // elements.push(diagnostic_popover.render(style, cx));
453 // }
454 // if let Some(info_popover) = self.info_popover.as_mut() {
455 // elements.push(info_popover.render(style, workspace, cx));
456 // }
457
458 // Some((point, elements))
459 // }
460}
461
462#[derive(Debug, Clone)]
463pub struct InfoPopover {
464 pub project: Model<Project>,
465 symbol_range: RangeInEditor,
466 pub blocks: Vec<HoverBlock>,
467 parsed_content: ParsedMarkdown,
468}
469
470// impl InfoPopover {
471// pub fn render(
472// &mut self,
473// style: &EditorStyle,
474// workspace: Option<WeakView<Workspace>>,
475// cx: &mut ViewContext<Editor>,
476// ) -> AnyElement<Editor> {
477// MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
478// Flex::column()
479// .scrollable::<HoverBlock>(0, None, cx)
480// .with_child(crate::render_parsed_markdown::<HoverBlock>(
481// &self.parsed_content,
482// style,
483// workspace,
484// cx,
485// ))
486// .contained()
487// .with_style(style.hover_popover.container)
488// })
489// .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath.
490// .with_cursor_style(CursorStyle::Arrow)
491// .with_padding(Padding {
492// bottom: HOVER_POPOVER_GAP,
493// top: HOVER_POPOVER_GAP,
494// ..Default::default()
495// })
496// .into_any()
497// }
498// }
499
500#[derive(Debug, Clone)]
501pub struct DiagnosticPopover {
502 local_diagnostic: DiagnosticEntry<Anchor>,
503 primary_diagnostic: Option<DiagnosticEntry<Anchor>>,
504}
505
506impl DiagnosticPopover {
507 pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement<Editor> {
508 todo!()
509 // enum PrimaryDiagnostic {}
510
511 // let mut text_style = style.hover_popover.prose.clone();
512 // text_style.font_size = style.text.font_size;
513 // let diagnostic_source_style = style.hover_popover.diagnostic_source_highlight.clone();
514
515 // let text = match &self.local_diagnostic.diagnostic.source {
516 // Some(source) => Text::new(
517 // format!("{source}: {}", self.local_diagnostic.diagnostic.message),
518 // text_style,
519 // )
520 // .with_highlights(vec![(0..source.len(), diagnostic_source_style)]),
521
522 // None => Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style),
523 // };
524
525 // let container_style = match self.local_diagnostic.diagnostic.severity {
526 // DiagnosticSeverity::HINT => style.hover_popover.info_container,
527 // DiagnosticSeverity::INFORMATION => style.hover_popover.info_container,
528 // DiagnosticSeverity::WARNING => style.hover_popover.warning_container,
529 // DiagnosticSeverity::ERROR => style.hover_popover.error_container,
530 // _ => style.hover_popover.container,
531 // };
532
533 // let tooltip_style = theme::current(cx).tooltip.clone();
534
535 // MouseEventHandler::new::<DiagnosticPopover, _>(0, cx, |_, _| {
536 // text.with_soft_wrap(true)
537 // .contained()
538 // .with_style(container_style)
539 // })
540 // .with_padding(Padding {
541 // top: HOVER_POPOVER_GAP,
542 // bottom: HOVER_POPOVER_GAP,
543 // ..Default::default()
544 // })
545 // .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath.
546 // .on_click(MouseButton::Left, |_, this, cx| {
547 // this.go_to_diagnostic(&Default::default(), cx)
548 // })
549 // .with_cursor_style(CursorStyle::PointingHand)
550 // .with_tooltip::<PrimaryDiagnostic>(
551 // 0,
552 // "Go To Diagnostic".to_string(),
553 // Some(Box::new(crate::GoToDiagnostic)),
554 // tooltip_style,
555 // cx,
556 // )
557 // .into_any()
558 }
559
560 pub fn activation_info(&self) -> (usize, Anchor) {
561 let entry = self
562 .primary_diagnostic
563 .as_ref()
564 .unwrap_or(&self.local_diagnostic);
565
566 (entry.diagnostic.group_id, entry.range.start.clone())
567 }
568}
569
570// #[cfg(test)]
571// mod tests {
572// use super::*;
573// use crate::{
574// editor_tests::init_test,
575// element::PointForPosition,
576// inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
577// link_go_to_definition::update_inlay_link_and_hover_points,
578// test::editor_lsp_test_context::EditorLspTestContext,
579// InlayId,
580// };
581// use collections::BTreeSet;
582// use gpui::fonts::{HighlightStyle, Underline, Weight};
583// use indoc::indoc;
584// use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
585// use lsp::LanguageServerId;
586// use project::{HoverBlock, HoverBlockKind};
587// use smol::stream::StreamExt;
588// use unindent::Unindent;
589// use util::test::marked_text_ranges;
590
591// #[gpui::test]
592// async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
593// init_test(cx, |_| {});
594
595// let mut cx = EditorLspTestContext::new_rust(
596// lsp::ServerCapabilities {
597// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
598// ..Default::default()
599// },
600// cx,
601// )
602// .await;
603
604// // Basic hover delays and then pops without moving the mouse
605// cx.set_state(indoc! {"
606// fn ˇtest() { println!(); }
607// "});
608// let hover_point = cx.display_point(indoc! {"
609// fn test() { printˇln!(); }
610// "});
611
612// cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
613// assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
614
615// // After delay, hover should be visible.
616// let symbol_range = cx.lsp_range(indoc! {"
617// fn test() { «println!»(); }
618// "});
619// let mut requests =
620// cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
621// Ok(Some(lsp::Hover {
622// contents: lsp::HoverContents::Markup(lsp::MarkupContent {
623// kind: lsp::MarkupKind::Markdown,
624// value: "some basic docs".to_string(),
625// }),
626// range: Some(symbol_range),
627// }))
628// });
629// cx.foreground()
630// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
631// requests.next().await;
632
633// cx.editor(|editor, _| {
634// assert!(editor.hover_state.visible());
635// assert_eq!(
636// editor.hover_state.info_popover.clone().unwrap().blocks,
637// vec![HoverBlock {
638// text: "some basic docs".to_string(),
639// kind: HoverBlockKind::Markdown,
640// },]
641// )
642// });
643
644// // Mouse moved with no hover response dismisses
645// let hover_point = cx.display_point(indoc! {"
646// fn teˇst() { println!(); }
647// "});
648// let mut request = cx
649// .lsp
650// .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
651// cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
652// cx.foreground()
653// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
654// request.next().await;
655// cx.editor(|editor, _| {
656// assert!(!editor.hover_state.visible());
657// });
658// }
659
660// #[gpui::test]
661// async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
662// init_test(cx, |_| {});
663
664// let mut cx = EditorLspTestContext::new_rust(
665// lsp::ServerCapabilities {
666// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
667// ..Default::default()
668// },
669// cx,
670// )
671// .await;
672
673// // Hover with keyboard has no delay
674// cx.set_state(indoc! {"
675// fˇn test() { println!(); }
676// "});
677// cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
678// let symbol_range = cx.lsp_range(indoc! {"
679// «fn» test() { println!(); }
680// "});
681// cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
682// Ok(Some(lsp::Hover {
683// contents: lsp::HoverContents::Markup(lsp::MarkupContent {
684// kind: lsp::MarkupKind::Markdown,
685// value: "some other basic docs".to_string(),
686// }),
687// range: Some(symbol_range),
688// }))
689// })
690// .next()
691// .await;
692
693// cx.condition(|editor, _| editor.hover_state.visible()).await;
694// cx.editor(|editor, _| {
695// assert_eq!(
696// editor.hover_state.info_popover.clone().unwrap().blocks,
697// vec![HoverBlock {
698// text: "some other basic docs".to_string(),
699// kind: HoverBlockKind::Markdown,
700// }]
701// )
702// });
703// }
704
705// #[gpui::test]
706// async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
707// init_test(cx, |_| {});
708
709// let mut cx = EditorLspTestContext::new_rust(
710// lsp::ServerCapabilities {
711// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
712// ..Default::default()
713// },
714// cx,
715// )
716// .await;
717
718// // Hover with keyboard has no delay
719// cx.set_state(indoc! {"
720// fˇn test() { println!(); }
721// "});
722// cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
723// let symbol_range = cx.lsp_range(indoc! {"
724// «fn» test() { println!(); }
725// "});
726// cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
727// Ok(Some(lsp::Hover {
728// contents: lsp::HoverContents::Array(vec![
729// lsp::MarkedString::String("regular text for hover to show".to_string()),
730// lsp::MarkedString::String("".to_string()),
731// lsp::MarkedString::LanguageString(lsp::LanguageString {
732// language: "Rust".to_string(),
733// value: "".to_string(),
734// }),
735// ]),
736// range: Some(symbol_range),
737// }))
738// })
739// .next()
740// .await;
741
742// cx.condition(|editor, _| editor.hover_state.visible()).await;
743// cx.editor(|editor, _| {
744// assert_eq!(
745// editor.hover_state.info_popover.clone().unwrap().blocks,
746// vec![HoverBlock {
747// text: "regular text for hover to show".to_string(),
748// kind: HoverBlockKind::Markdown,
749// }],
750// "No empty string hovers should be shown"
751// );
752// });
753// }
754
755// #[gpui::test]
756// async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
757// init_test(cx, |_| {});
758
759// let mut cx = EditorLspTestContext::new_rust(
760// lsp::ServerCapabilities {
761// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
762// ..Default::default()
763// },
764// cx,
765// )
766// .await;
767
768// // Hover with keyboard has no delay
769// cx.set_state(indoc! {"
770// fˇn test() { println!(); }
771// "});
772// cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
773// let symbol_range = cx.lsp_range(indoc! {"
774// «fn» test() { println!(); }
775// "});
776
777// let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
778// let markdown_string = format!("\n```rust\n{code_str}```");
779
780// let closure_markdown_string = markdown_string.clone();
781// cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
782// let future_markdown_string = closure_markdown_string.clone();
783// async move {
784// Ok(Some(lsp::Hover {
785// contents: lsp::HoverContents::Markup(lsp::MarkupContent {
786// kind: lsp::MarkupKind::Markdown,
787// value: future_markdown_string,
788// }),
789// range: Some(symbol_range),
790// }))
791// }
792// })
793// .next()
794// .await;
795
796// cx.condition(|editor, _| editor.hover_state.visible()).await;
797// cx.editor(|editor, _| {
798// let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
799// assert_eq!(
800// blocks,
801// vec![HoverBlock {
802// text: markdown_string,
803// kind: HoverBlockKind::Markdown,
804// }],
805// );
806
807// let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
808// assert_eq!(
809// rendered.text,
810// code_str.trim(),
811// "Should not have extra line breaks at end of rendered hover"
812// );
813// });
814// }
815
816// #[gpui::test]
817// async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
818// init_test(cx, |_| {});
819
820// let mut cx = EditorLspTestContext::new_rust(
821// lsp::ServerCapabilities {
822// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
823// ..Default::default()
824// },
825// cx,
826// )
827// .await;
828
829// // Hover with just diagnostic, pops DiagnosticPopover immediately and then
830// // info popover once request completes
831// cx.set_state(indoc! {"
832// fn teˇst() { println!(); }
833// "});
834
835// // Send diagnostic to client
836// let range = cx.text_anchor_range(indoc! {"
837// fn «test»() { println!(); }
838// "});
839// cx.update_buffer(|buffer, cx| {
840// let snapshot = buffer.text_snapshot();
841// let set = DiagnosticSet::from_sorted_entries(
842// vec![DiagnosticEntry {
843// range,
844// diagnostic: Diagnostic {
845// message: "A test diagnostic message.".to_string(),
846// ..Default::default()
847// },
848// }],
849// &snapshot,
850// );
851// buffer.update_diagnostics(LanguageServerId(0), set, cx);
852// });
853
854// // Hover pops diagnostic immediately
855// cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
856// cx.foreground().run_until_parked();
857
858// cx.editor(|Editor { hover_state, .. }, _| {
859// assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none())
860// });
861
862// // Info Popover shows after request responded to
863// let range = cx.lsp_range(indoc! {"
864// fn «test»() { println!(); }
865// "});
866// cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
867// Ok(Some(lsp::Hover {
868// contents: lsp::HoverContents::Markup(lsp::MarkupContent {
869// kind: lsp::MarkupKind::Markdown,
870// value: "some new docs".to_string(),
871// }),
872// range: Some(range),
873// }))
874// });
875// cx.foreground()
876// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
877
878// cx.foreground().run_until_parked();
879// cx.editor(|Editor { hover_state, .. }, _| {
880// hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
881// });
882// }
883
884// #[gpui::test]
885// fn test_render_blocks(cx: &mut gpui::TestAppContext) {
886// init_test(cx, |_| {});
887
888// cx.add_window(|cx| {
889// let editor = Editor::single_line(None, cx);
890// let style = editor.style(cx);
891
892// struct Row {
893// blocks: Vec<HoverBlock>,
894// expected_marked_text: String,
895// expected_styles: Vec<HighlightStyle>,
896// }
897
898// let rows = &[
899// // Strong emphasis
900// Row {
901// blocks: vec![HoverBlock {
902// text: "one **two** three".to_string(),
903// kind: HoverBlockKind::Markdown,
904// }],
905// expected_marked_text: "one «two» three".to_string(),
906// expected_styles: vec![HighlightStyle {
907// weight: Some(Weight::BOLD),
908// ..Default::default()
909// }],
910// },
911// // Links
912// Row {
913// blocks: vec three".to_string(),
915// kind: HoverBlockKind::Markdown,
916// }],
917// expected_marked_text: "one «two» three".to_string(),
918// expected_styles: vec![HighlightStyle {
919// underline: Some(Underline {
920// thickness: 1.0.into(),
921// ..Default::default()
922// }),
923// ..Default::default()
924// }],
925// },
926// // Lists
927// Row {
928// blocks: vec
936// - d"
937// .unindent(),
938// kind: HoverBlockKind::Markdown,
939// }],
940// expected_marked_text: "
941// lists:
942// - one
943// - a
944// - b
945// - two
946// - «c»
947// - d"
948// .unindent(),
949// expected_styles: vec![HighlightStyle {
950// underline: Some(Underline {
951// thickness: 1.0.into(),
952// ..Default::default()
953// }),
954// ..Default::default()
955// }],
956// },
957// // Multi-paragraph list items
958// Row {
959// blocks: vec![HoverBlock {
960// text: "
961// * one two
962// three
963
964// * four five
965// * six seven
966// eight
967
968// nine
969// * ten
970// * six"
971// .unindent(),
972// kind: HoverBlockKind::Markdown,
973// }],
974// expected_marked_text: "
975// - one two three
976// - four five
977// - six seven eight
978
979// nine
980// - ten
981// - six"
982// .unindent(),
983// expected_styles: vec![HighlightStyle {
984// underline: Some(Underline {
985// thickness: 1.0.into(),
986// ..Default::default()
987// }),
988// ..Default::default()
989// }],
990// },
991// ];
992
993// for Row {
994// blocks,
995// expected_marked_text,
996// expected_styles,
997// } in &rows[0..]
998// {
999// let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
1000
1001// let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
1002// let expected_highlights = ranges
1003// .into_iter()
1004// .zip(expected_styles.iter().cloned())
1005// .collect::<Vec<_>>();
1006// assert_eq!(
1007// rendered.text, expected_text,
1008// "wrong text for input {blocks:?}"
1009// );
1010
1011// let rendered_highlights: Vec<_> = rendered
1012// .highlights
1013// .iter()
1014// .filter_map(|(range, highlight)| {
1015// let highlight = highlight.to_highlight_style(&style.syntax)?;
1016// Some((range.clone(), highlight))
1017// })
1018// .collect();
1019
1020// assert_eq!(
1021// rendered_highlights, expected_highlights,
1022// "wrong highlights for input {blocks:?}"
1023// );
1024// }
1025
1026// editor
1027// });
1028// }
1029
1030// #[gpui::test]
1031// async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
1032// init_test(cx, |settings| {
1033// settings.defaults.inlay_hints = Some(InlayHintSettings {
1034// enabled: true,
1035// show_type_hints: true,
1036// show_parameter_hints: true,
1037// show_other_hints: true,
1038// })
1039// });
1040
1041// let mut cx = EditorLspTestContext::new_rust(
1042// lsp::ServerCapabilities {
1043// inlay_hint_provider: Some(lsp::OneOf::Right(
1044// lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
1045// resolve_provider: Some(true),
1046// ..Default::default()
1047// }),
1048// )),
1049// ..Default::default()
1050// },
1051// cx,
1052// )
1053// .await;
1054
1055// cx.set_state(indoc! {"
1056// struct TestStruct;
1057
1058// // ==================
1059
1060// struct TestNewType<T>(T);
1061
1062// fn main() {
1063// let variableˇ = TestNewType(TestStruct);
1064// }
1065// "});
1066
1067// let hint_start_offset = cx.ranges(indoc! {"
1068// struct TestStruct;
1069
1070// // ==================
1071
1072// struct TestNewType<T>(T);
1073
1074// fn main() {
1075// let variableˇ = TestNewType(TestStruct);
1076// }
1077// "})[0]
1078// .start;
1079// let hint_position = cx.to_lsp(hint_start_offset);
1080// let new_type_target_range = cx.lsp_range(indoc! {"
1081// struct TestStruct;
1082
1083// // ==================
1084
1085// struct «TestNewType»<T>(T);
1086
1087// fn main() {
1088// let variable = TestNewType(TestStruct);
1089// }
1090// "});
1091// let struct_target_range = cx.lsp_range(indoc! {"
1092// struct «TestStruct»;
1093
1094// // ==================
1095
1096// struct TestNewType<T>(T);
1097
1098// fn main() {
1099// let variable = TestNewType(TestStruct);
1100// }
1101// "});
1102
1103// let uri = cx.buffer_lsp_url.clone();
1104// let new_type_label = "TestNewType";
1105// let struct_label = "TestStruct";
1106// let entire_hint_label = ": TestNewType<TestStruct>";
1107// let closure_uri = uri.clone();
1108// cx.lsp
1109// .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
1110// let task_uri = closure_uri.clone();
1111// async move {
1112// assert_eq!(params.text_document.uri, task_uri);
1113// Ok(Some(vec![lsp::InlayHint {
1114// position: hint_position,
1115// label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
1116// value: entire_hint_label.to_string(),
1117// ..Default::default()
1118// }]),
1119// kind: Some(lsp::InlayHintKind::TYPE),
1120// text_edits: None,
1121// tooltip: None,
1122// padding_left: Some(false),
1123// padding_right: Some(false),
1124// data: None,
1125// }]))
1126// }
1127// })
1128// .next()
1129// .await;
1130// cx.foreground().run_until_parked();
1131// cx.update_editor(|editor, cx| {
1132// let expected_layers = vec![entire_hint_label.to_string()];
1133// assert_eq!(expected_layers, cached_hint_labels(editor));
1134// assert_eq!(expected_layers, visible_hint_labels(editor, cx));
1135// });
1136
1137// let inlay_range = cx
1138// .ranges(indoc! {"
1139// struct TestStruct;
1140
1141// // ==================
1142
1143// struct TestNewType<T>(T);
1144
1145// fn main() {
1146// let variable« »= TestNewType(TestStruct);
1147// }
1148// "})
1149// .get(0)
1150// .cloned()
1151// .unwrap();
1152// let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| {
1153// let snapshot = editor.snapshot(cx);
1154// let previous_valid = inlay_range.start.to_display_point(&snapshot);
1155// let next_valid = inlay_range.end.to_display_point(&snapshot);
1156// assert_eq!(previous_valid.row(), next_valid.row());
1157// assert!(previous_valid.column() < next_valid.column());
1158// let exact_unclipped = DisplayPoint::new(
1159// previous_valid.row(),
1160// previous_valid.column()
1161// + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
1162// as u32,
1163// );
1164// PointForPosition {
1165// previous_valid,
1166// next_valid,
1167// exact_unclipped,
1168// column_overshoot_after_line_end: 0,
1169// }
1170// });
1171// cx.update_editor(|editor, cx| {
1172// update_inlay_link_and_hover_points(
1173// &editor.snapshot(cx),
1174// new_type_hint_part_hover_position,
1175// editor,
1176// true,
1177// false,
1178// cx,
1179// );
1180// });
1181
1182// let resolve_closure_uri = uri.clone();
1183// cx.lsp
1184// .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
1185// move |mut hint_to_resolve, _| {
1186// let mut resolved_hint_positions = BTreeSet::new();
1187// let task_uri = resolve_closure_uri.clone();
1188// async move {
1189// let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
1190// assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
1191
1192// // `: TestNewType<TestStruct>`
1193// hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
1194// lsp::InlayHintLabelPart {
1195// value: ": ".to_string(),
1196// ..Default::default()
1197// },
1198// lsp::InlayHintLabelPart {
1199// value: new_type_label.to_string(),
1200// location: Some(lsp::Location {
1201// uri: task_uri.clone(),
1202// range: new_type_target_range,
1203// }),
1204// tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
1205// "A tooltip for `{new_type_label}`"
1206// ))),
1207// ..Default::default()
1208// },
1209// lsp::InlayHintLabelPart {
1210// value: "<".to_string(),
1211// ..Default::default()
1212// },
1213// lsp::InlayHintLabelPart {
1214// value: struct_label.to_string(),
1215// location: Some(lsp::Location {
1216// uri: task_uri,
1217// range: struct_target_range,
1218// }),
1219// tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
1220// lsp::MarkupContent {
1221// kind: lsp::MarkupKind::Markdown,
1222// value: format!("A tooltip for `{struct_label}`"),
1223// },
1224// )),
1225// ..Default::default()
1226// },
1227// lsp::InlayHintLabelPart {
1228// value: ">".to_string(),
1229// ..Default::default()
1230// },
1231// ]);
1232
1233// Ok(hint_to_resolve)
1234// }
1235// },
1236// )
1237// .next()
1238// .await;
1239// cx.foreground().run_until_parked();
1240
1241// cx.update_editor(|editor, cx| {
1242// update_inlay_link_and_hover_points(
1243// &editor.snapshot(cx),
1244// new_type_hint_part_hover_position,
1245// editor,
1246// true,
1247// false,
1248// cx,
1249// );
1250// });
1251// cx.foreground()
1252// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1253// cx.foreground().run_until_parked();
1254// cx.update_editor(|editor, cx| {
1255// let hover_state = &editor.hover_state;
1256// assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
1257// let popover = hover_state.info_popover.as_ref().unwrap();
1258// let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1259// assert_eq!(
1260// popover.symbol_range,
1261// RangeInEditor::Inlay(InlayHighlight {
1262// inlay: InlayId::Hint(0),
1263// inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1264// range: ": ".len()..": ".len() + new_type_label.len(),
1265// }),
1266// "Popover range should match the new type label part"
1267// );
1268// assert_eq!(
1269// popover.parsed_content.text,
1270// format!("A tooltip for `{new_type_label}`"),
1271// "Rendered text should not anyhow alter backticks"
1272// );
1273// });
1274
1275// let struct_hint_part_hover_position = cx.update_editor(|editor, cx| {
1276// let snapshot = editor.snapshot(cx);
1277// let previous_valid = inlay_range.start.to_display_point(&snapshot);
1278// let next_valid = inlay_range.end.to_display_point(&snapshot);
1279// assert_eq!(previous_valid.row(), next_valid.row());
1280// assert!(previous_valid.column() < next_valid.column());
1281// let exact_unclipped = DisplayPoint::new(
1282// previous_valid.row(),
1283// previous_valid.column()
1284// + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
1285// as u32,
1286// );
1287// PointForPosition {
1288// previous_valid,
1289// next_valid,
1290// exact_unclipped,
1291// column_overshoot_after_line_end: 0,
1292// }
1293// });
1294// cx.update_editor(|editor, cx| {
1295// update_inlay_link_and_hover_points(
1296// &editor.snapshot(cx),
1297// struct_hint_part_hover_position,
1298// editor,
1299// true,
1300// false,
1301// cx,
1302// );
1303// });
1304// cx.foreground()
1305// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
1306// cx.foreground().run_until_parked();
1307// cx.update_editor(|editor, cx| {
1308// let hover_state = &editor.hover_state;
1309// assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
1310// let popover = hover_state.info_popover.as_ref().unwrap();
1311// let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
1312// assert_eq!(
1313// popover.symbol_range,
1314// RangeInEditor::Inlay(InlayHighlight {
1315// inlay: InlayId::Hint(0),
1316// inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
1317// range: ": ".len() + new_type_label.len() + "<".len()
1318// ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
1319// }),
1320// "Popover range should match the struct label part"
1321// );
1322// assert_eq!(
1323// popover.parsed_content.text,
1324// format!("A tooltip for {struct_label}"),
1325// "Rendered markdown element should remove backticks from text"
1326// );
1327// });
1328// }
1329// }