1use crate::CodeEditor;
7use iced::widget::{
8 Id, Space, button, column, container, markdown, mouse_area, row,
9 scrollable, stack, text,
10};
11use iced::{Background, Border, Color, Element, Length, Point, Shadow, Theme};
12
13const MAX_COMPLETION_ITEMS: usize = 8;
15const COMPLETION_ITEM_HEIGHT: f32 = 20.0;
17const COMPLETION_HEADER_HEIGHT: f32 = 24.0;
19const COMPLETION_PADDING: f32 = 4.0;
21const COMPLETION_MENU_WIDTH: f32 = 250.0;
23const SCROLLABLE_BORDER_RADIUS: f32 = 4.0;
25
26pub struct LspOverlayState {
41 pub hover_text: Option<String>,
43 pub hover_items: Vec<iced::widget::markdown::Item>,
45 pub hover_visible: bool,
47 pub hover_position: Option<Point>,
49 pub hover_interactive: bool,
51 pub all_completions: Vec<String>,
53 pub completion_filter: String,
55 pub completion_items: Vec<String>,
57 pub completion_visible: bool,
59 pub completion_selected: usize,
61 pub completion_suppressed: bool,
63 pub completion_position: Option<Point>,
65}
66
67impl LspOverlayState {
68 pub fn new() -> Self {
80 Self {
81 hover_text: None,
82 hover_items: Vec::new(),
83 hover_visible: false,
84 hover_position: None,
85 hover_interactive: false,
86 all_completions: Vec::new(),
87 completion_filter: String::new(),
88 completion_items: Vec::new(),
89 completion_visible: false,
90 completion_selected: 0,
91 completion_suppressed: false,
92 completion_position: None,
93 }
94 }
95
96 pub fn set_hover_position(&mut self, point: Point) {
111 self.hover_position = Some(point);
112 }
113
114 pub fn show_hover(&mut self, text: String) {
129 self.hover_items = iced::widget::markdown::parse(&text).collect();
130 self.hover_text = Some(text);
131 self.hover_visible = true;
132 }
133
134 pub fn clear_hover(&mut self) {
150 self.hover_text = None;
151 self.hover_items.clear();
152 self.hover_visible = false;
153 self.hover_position = None;
154 self.hover_interactive = false;
155 }
156
157 pub fn set_completions(&mut self, items: Vec<String>, position: Point) {
175 self.all_completions = items;
176 self.completion_selected = 0;
177 self.completion_position = Some(position);
178 self.filter_completions();
179 }
180
181 pub fn clear_completions(&mut self) {
196 self.all_completions.clear();
197 self.completion_items.clear();
198 self.completion_filter.clear();
199 self.completion_visible = false;
200 self.completion_suppressed = false;
201 }
202
203 pub fn filter_completions(&mut self) {
223 let filter = self.completion_filter.to_lowercase();
224 if filter.is_empty() {
225 self.completion_items = self.all_completions.clone();
226 } else {
227 self.completion_items = self
228 .all_completions
229 .iter()
230 .filter(|item| item.to_lowercase().contains(&filter))
231 .cloned()
232 .collect();
233 }
234 self.completion_visible = !self.completion_items.is_empty();
235 if self.completion_selected >= self.completion_items.len() {
236 self.completion_selected =
237 self.completion_items.len().saturating_sub(1);
238 }
239 }
240
241 pub fn navigate(&mut self, delta: i32) {
260 if self.completion_items.is_empty() {
261 return;
262 }
263 let len = self.completion_items.len();
264 let current = self.completion_selected as i32;
265 self.completion_selected =
266 ((current + delta).rem_euclid(len as i32)) as usize;
267 }
268
269 pub fn selected_item(&self) -> Option<&str> {
282 self.completion_items.get(self.completion_selected).map(String::as_str)
283 }
284
285 pub fn scroll_offset_for_selected(&self) -> f32 {
305 self.completion_selected as f32 * COMPLETION_ITEM_HEIGHT
306 }
307}
308
309impl Default for LspOverlayState {
310 fn default() -> Self {
311 Self::new()
312 }
313}
314
315#[derive(Debug, Clone)]
320pub enum LspOverlayMessage {
321 HoverEntered,
323 HoverExited,
325 CompletionSelected(usize),
327 CompletionClosed,
329 CompletionNavigateUp,
331 CompletionNavigateDown,
333 CompletionConfirm,
335}
336
337fn measure_hover_width(editor: &CodeEditor, text: &str) -> f32 {
339 text.lines().map(|line| editor.measure_text_width(line)).fold(0.0, f32::max)
340}
341
342pub fn view_lsp_overlay<'a, M: Clone + 'a>(
385 state: &'a LspOverlayState,
386 editor: &'a CodeEditor,
387 theme: &'a Theme,
388 font_size: f32,
389 line_height: f32,
390 f: impl Fn(LspOverlayMessage) -> M + 'a,
391) -> Element<'a, M> {
392 let msg_hover_entered = f(LspOverlayMessage::HoverEntered);
394 let msg_hover_exited = f(LspOverlayMessage::HoverExited);
395 let msg_completion_closed = f(LspOverlayMessage::CompletionClosed);
396 let msg_completion_selected: Vec<M> = (0..state.completion_items.len())
397 .map(|i| f(LspOverlayMessage::CompletionSelected(i)))
398 .collect();
399
400 let mut has_overlay = false;
401
402 let hover_layer: Element<'a, M> = build_hover_layer(
404 state,
405 editor,
406 theme,
407 (font_size, line_height),
408 msg_hover_entered,
409 msg_hover_exited,
410 &mut has_overlay,
411 );
412
413 let completion_layer: Element<'a, M> = build_completion_layer(
415 state,
416 editor,
417 line_height,
418 msg_completion_closed,
419 msg_completion_selected,
420 &mut has_overlay,
421 );
422
423 if !has_overlay {
424 return container(
425 Space::new().width(Length::Shrink).height(Length::Shrink),
426 )
427 .into();
428 }
429
430 let base = container(Space::new().width(Length::Fill).height(Length::Fill))
431 .width(Length::Fill)
432 .height(Length::Fill);
433
434 stack![base, completion_layer, hover_layer].into()
436}
437
438fn build_hover_layer<'a, M: Clone + 'a>(
440 state: &'a LspOverlayState,
441 editor: &'a CodeEditor,
442 theme: &'a Theme,
443 text_metrics: (f32, f32),
444 msg_entered: M,
445 msg_exited: M,
446 has_overlay: &mut bool,
447) -> Element<'a, M> {
448 let (font_size, line_height) = text_metrics;
449 if !state.hover_visible {
450 return empty_overlay();
451 }
452
453 let Some(hover) =
454 state.hover_text.as_ref().filter(|t| !t.trim().is_empty())
455 else {
456 return empty_overlay();
457 };
458
459 let line_count = hover.lines().count().max(1);
460 let visible_lines = line_count.min(10);
461 let hover_padding = 8.0;
462 let scroll_height = line_height * visible_lines as f32
463 + (line_height * 0.75).max(10.0)
464 + hover_padding * 2.0;
465
466 let viewport_width = editor.viewport_width();
467 let max_line_width = measure_hover_width(editor, hover);
468 let max_width = (viewport_width - 24.0).max(0.0);
469 let content_max_width = if max_width > hover_padding * 2.0 {
470 max_width - hover_padding * 2.0
471 } else {
472 max_width
473 };
474 let content_width = if content_max_width > 0.0 {
475 max_line_width.min(content_max_width)
476 } else {
477 max_line_width
478 };
479 let hover_width = content_width + hover_padding * 2.0;
480
481 let palette = theme.palette();
482 let markdown_settings = markdown::Settings::with_text_size(
483 font_size,
484 markdown::Style::from_palette(palette),
485 );
486
487 let entered_for_map = msg_entered.clone();
488 let entered_for_enter = msg_entered.clone();
489 let entered_for_move = msg_entered;
490
491 let hover_content = scrollable(
492 container(
493 markdown::view(&state.hover_items, markdown_settings)
494 .map(move |_| entered_for_map.clone()),
495 )
496 .width(Length::Fixed(hover_width))
497 .padding(hover_padding),
498 )
499 .height(Length::Fixed(scroll_height))
500 .width(Length::Fixed(hover_width))
501 .style(|theme: &Theme, _status| {
502 let palette = theme.extended_palette();
503 scrollable::Style {
504 container: container::Style {
505 background: Some(Background::Color(Color::TRANSPARENT)),
506 ..container::Style::default()
507 },
508 vertical_rail: lsp_scrollable_rail(palette),
509 horizontal_rail: lsp_scrollable_rail(palette),
510 gap: None,
511 auto_scroll: scrollable::AutoScroll {
512 background: Color::TRANSPARENT.into(),
513 border: Border::default(),
514 shadow: Shadow::default(),
515 icon: Color::TRANSPARENT,
516 },
517 }
518 });
519
520 let hover_box = container(column![hover_content])
521 .width(Length::Shrink)
522 .style(|theme: &Theme| {
523 let palette = theme.extended_palette();
524 container::Style {
525 background: Some(iced::Background::Color(
526 palette.background.weak.color,
527 )),
528 border: iced::Border {
529 color: palette.primary.weak.color,
530 width: 1.0,
531 radius: 6.0.into(),
532 },
533 ..Default::default()
534 }
535 });
536
537 let hover_box: Element<'_, M> = mouse_area(hover_box)
538 .on_enter(entered_for_enter)
539 .on_move(move |_| entered_for_move.clone())
540 .on_exit(msg_exited)
541 .into();
542
543 let hover_pos = state.hover_position.unwrap_or(Point::new(4.0, 4.0));
544 let viewport_scroll = editor.viewport_scroll();
545 let hover_pos =
546 Point::new(hover_pos.x, (hover_pos.y - viewport_scroll).max(0.0));
547 let viewport_height = editor.viewport_height();
548 let gap = 1.0;
549 let show_above = hover_pos.y >= scroll_height + gap;
550
551 let gap_x = (editor.char_width() * 0.5).max(2.0);
552 let right_x = hover_pos.x + gap_x;
553 let left_x = hover_pos.x - hover_width - gap_x;
554 let max_x = (viewport_width - hover_width - 4.0).max(0.0);
555
556 let offset_x = if right_x <= max_x {
557 right_x
558 } else if left_x >= 0.0 {
559 left_x
560 } else {
561 right_x.clamp(0.0, max_x)
562 };
563
564 let offset_y = if show_above {
565 (hover_pos.y - scroll_height - gap).max(0.0)
566 } else {
567 (hover_pos.y + line_height + gap).max(0.0).min(viewport_height)
568 };
569
570 *has_overlay = true;
571
572 container(
573 column![
574 Space::new().height(Length::Fixed(offset_y)),
575 row![Space::new().width(Length::Fixed(offset_x)), hover_box]
576 ]
577 .spacing(0)
578 .width(Length::Fill)
579 .height(Length::Fill),
580 )
581 .width(Length::Fill)
582 .height(Length::Fill)
583 .into()
584}
585
586fn build_completion_layer<'a, M: Clone + 'a>(
588 state: &'a LspOverlayState,
589 editor: &'a CodeEditor,
590 line_height: f32,
591 msg_closed: M,
592 msg_selected: Vec<M>,
593 has_overlay: &mut bool,
594) -> Element<'a, M> {
595 if !state.completion_visible
596 || state.completion_items.is_empty()
597 || state.completion_suppressed
598 {
599 return empty_overlay();
600 }
601
602 let visible_count = state.completion_items.len().min(MAX_COMPLETION_ITEMS);
603 let menu_height = COMPLETION_HEADER_HEIGHT
604 + (visible_count as f32 * COMPLETION_ITEM_HEIGHT)
605 + (COMPLETION_PADDING * 2.0);
606
607 let cursor_pos = state.completion_position.unwrap_or(Point::new(4.0, 4.0));
608 let viewport_width = editor.viewport_width();
609 let viewport_height = editor.viewport_height();
610 let viewport_scroll = editor.viewport_scroll();
611
612 let menu_width = COMPLETION_MENU_WIDTH.min(viewport_width - 8.0);
613 let adjusted_y = (cursor_pos.y - viewport_scroll).max(0.0);
614 let space_below = viewport_height - adjusted_y - line_height;
615 let show_above =
616 space_below < menu_height + 4.0 && adjusted_y >= menu_height + 4.0;
617
618 let offset_x = cursor_pos.x.min(viewport_width - menu_width - 4.0).max(4.0);
619 let offset_y = if show_above {
620 (adjusted_y - menu_height - 4.0).max(0.0)
621 } else {
622 adjusted_y + line_height + 4.0
623 };
624
625 let completion_elements: Vec<Element<'_, M>> = state
626 .completion_items
627 .iter()
628 .enumerate()
629 .zip(msg_selected)
630 .map(|((index, item), msg)| {
631 let is_selected = index == state.completion_selected;
632 button(
633 text(item.clone())
634 .size(12)
635 .line_height(iced::widget::text::LineHeight::Relative(1.5)),
636 )
637 .padding([2, 8])
638 .width(Length::Fill)
639 .on_press(msg)
640 .style(move |theme: &Theme, _status| {
641 let palette = theme.extended_palette();
642 if is_selected {
643 button::Style {
644 background: Some(iced::Background::Color(
645 palette.primary.weak.color,
646 )),
647 text_color: Color::WHITE,
648 ..Default::default()
649 }
650 } else {
651 button::Style {
652 background: Some(iced::Background::Color(
653 palette.background.weak.color,
654 )),
655 text_color: Color::WHITE,
656 ..Default::default()
657 }
658 }
659 })
660 .into()
661 })
662 .collect();
663
664 let completion_box = scrollable(column(completion_elements).spacing(0))
665 .height(Length::Fixed(menu_height))
666 .width(Length::Fixed(menu_width))
667 .id(Id::new("completion_scrollable"))
668 .style(|theme: &Theme, _status| {
669 let palette = theme.extended_palette();
670 scrollable::Style {
671 container: container::Style {
672 background: Some(iced::Background::Color(
673 palette.background.weak.color,
674 )),
675 border: iced::Border {
676 color: palette.primary.weak.color,
677 width: 1.0,
678 radius: SCROLLABLE_BORDER_RADIUS.into(),
679 },
680 ..Default::default()
681 },
682 vertical_rail: lsp_scrollable_rail(palette),
683 horizontal_rail: lsp_scrollable_rail(palette),
684 gap: None,
685 auto_scroll: scrollable::AutoScroll {
686 background: Color::TRANSPARENT.into(),
687 border: Border::default(),
688 shadow: Shadow::default(),
689 icon: Color::TRANSPARENT,
690 },
691 }
692 });
693
694 *has_overlay = true;
695
696 let click_outside =
697 button(Space::new().width(Length::Fill).height(Length::Fill))
698 .width(Length::Fill)
699 .height(Length::Fill)
700 .on_press(msg_closed)
701 .style(|_theme: &Theme, _status| button::Style {
702 background: Some(Background::Color(Color::TRANSPARENT)),
703 ..Default::default()
704 });
705
706 let completion_content = container(
707 column![
708 Space::new().height(Length::Fixed(offset_y)),
709 row![Space::new().width(Length::Fixed(offset_x)), completion_box]
710 ]
711 .spacing(0)
712 .width(Length::Fill)
713 .height(Length::Fill),
714 )
715 .width(Length::Fill)
716 .height(Length::Fill);
717
718 stack![click_outside, completion_content].into()
719}
720
721fn lsp_scrollable_rail(
726 palette: &iced::theme::palette::Extended,
727) -> scrollable::Rail {
728 scrollable::Rail {
729 background: Some(palette.background.weak.color.into()),
730 border: Border {
731 radius: SCROLLABLE_BORDER_RADIUS.into(),
732 width: 0.0,
733 color: Color::TRANSPARENT,
734 },
735 scroller: scrollable::Scroller {
736 background: palette.primary.weak.color.into(),
737 border: Border {
738 radius: SCROLLABLE_BORDER_RADIUS.into(),
739 width: 0.0,
740 color: Color::TRANSPARENT,
741 },
742 },
743 }
744}
745
746fn empty_overlay<'a, M: 'a>() -> Element<'a, M> {
748 container(Space::new().width(Length::Shrink).height(Length::Shrink)).into()
749}
750
751#[cfg(test)]
752mod tests {
753 use super::*;
754 use iced::Point;
755
756 #[test]
757 fn test_lsp_overlay_state_new() {
758 let state = LspOverlayState::new();
759 assert!(!state.hover_visible);
760 assert!(!state.completion_visible);
761 assert!(state.hover_text.is_none());
762 assert!(state.completion_items.is_empty());
763 }
764
765 #[test]
766 fn test_show_hover() {
767 let mut state = LspOverlayState::new();
768 state.show_hover("hello".to_string());
769 assert!(state.hover_visible);
770 assert_eq!(state.hover_text, Some("hello".to_string()));
771 }
772
773 #[test]
774 fn test_clear_hover() {
775 let mut state = LspOverlayState::new();
776 state.show_hover("hello".to_string());
777 state.hover_interactive = true;
778 state.hover_position = Some(Point::ORIGIN);
779 state.clear_hover();
780 assert!(!state.hover_visible);
781 assert!(state.hover_text.is_none());
782 assert!(!state.hover_interactive);
783 assert!(state.hover_position.is_none());
784 }
785
786 #[test]
787 fn test_set_hover_position() {
788 let mut state = LspOverlayState::new();
789 state.set_hover_position(Point::new(10.0, 20.0));
790 assert_eq!(state.hover_position, Some(Point::new(10.0, 20.0)));
791 }
792
793 #[test]
794 fn test_set_completions() {
795 let mut state = LspOverlayState::new();
796 state.set_completions(
797 vec!["foo".to_string(), "bar".to_string()],
798 Point::ORIGIN,
799 );
800 assert_eq!(state.completion_items.len(), 2);
801 assert!(state.completion_visible);
802 assert_eq!(state.completion_selected, 0);
803 }
804
805 #[test]
806 fn test_clear_completions() {
807 let mut state = LspOverlayState::new();
808 state.set_completions(vec!["foo".to_string()], Point::ORIGIN);
809 state.clear_completions();
810 assert!(!state.completion_visible);
811 assert!(state.all_completions.is_empty());
812 assert!(state.completion_items.is_empty());
813 }
814
815 #[test]
816 fn test_filter_completions() {
817 let mut state = LspOverlayState::new();
818 state.set_completions(
819 vec!["foo".to_string(), "bar".to_string(), "baz".to_string()],
820 Point::ORIGIN,
821 );
822 state.completion_filter = "ba".to_string();
823 state.filter_completions();
824 assert_eq!(state.completion_items.len(), 2);
825 assert!(state.completion_items.contains(&"bar".to_string()));
826 assert!(state.completion_items.contains(&"baz".to_string()));
827 }
828
829 #[test]
830 fn test_navigate() {
831 let mut state = LspOverlayState::new();
832 state.set_completions(
833 vec!["a".to_string(), "b".to_string(), "c".to_string()],
834 Point::ORIGIN,
835 );
836 state.navigate(1);
837 assert_eq!(state.completion_selected, 1);
838 state.navigate(-1);
839 assert_eq!(state.completion_selected, 0);
840 state.navigate(-1);
842 assert_eq!(state.completion_selected, 2);
843 state.navigate(1);
845 assert_eq!(state.completion_selected, 0);
846 }
847
848 #[test]
849 fn test_scroll_offset_for_selected() {
850 let mut state = LspOverlayState::new();
851 assert_eq!(state.scroll_offset_for_selected(), 0.0);
852 state.set_completions(
853 vec!["a".to_string(), "b".to_string(), "c".to_string()],
854 Point::ORIGIN,
855 );
856 assert_eq!(state.scroll_offset_for_selected(), 0.0);
857 state.navigate(1);
858 assert_eq!(state.scroll_offset_for_selected(), COMPLETION_ITEM_HEIGHT);
859 state.navigate(1);
860 assert_eq!(
861 state.scroll_offset_for_selected(),
862 2.0 * COMPLETION_ITEM_HEIGHT
863 );
864 }
865
866 #[test]
867 fn test_selected_item() {
868 let mut state = LspOverlayState::new();
869 assert_eq!(state.selected_item(), None);
870 state.set_completions(
871 vec!["first".to_string(), "second".to_string()],
872 Point::ORIGIN,
873 );
874 assert_eq!(state.selected_item(), Some("first"));
875 state.navigate(1);
876 assert_eq!(state.selected_item(), Some("second"));
877 }
878}