gpui_component/input/lsp/
definitions.rs

1use anyhow::Result;
2use gpui::{
3    px, App, Context, HighlightStyle, Hitbox, MouseDownEvent, Task, UnderlineStyle, Window,
4};
5use ropey::Rope;
6use std::{ops::Range, rc::Rc};
7
8use crate::{
9    input::{element::TextElement, GoToDefinition, InputState, RopeExt},
10    ActiveTheme,
11};
12
13/// Definition provider
14///
15/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition
16pub trait DefinitionProvider {
17    /// textDocument/definition
18    ///
19    /// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition
20    fn definitions(
21        &self,
22        _text: &Rope,
23        _offset: usize,
24        _window: &mut Window,
25        _cx: &mut App,
26    ) -> Task<Result<Vec<lsp_types::LocationLink>>>;
27}
28
29#[derive(Clone, Default)]
30pub(crate) struct HoverDefinition {
31    /// The range of the symbol that triggered the hover.
32    symbol_range: Range<usize>,
33    pub(crate) locations: Rc<Vec<lsp_types::LocationLink>>,
34    last_location: Option<(Range<usize>, Rc<Vec<lsp_types::LocationLink>>)>,
35}
36
37impl HoverDefinition {
38    pub(crate) fn update(
39        &mut self,
40        symbol_range: Range<usize>,
41        locations: Vec<lsp_types::LocationLink>,
42    ) {
43        self.clear();
44        self.symbol_range = symbol_range;
45        self.locations = Rc::new(locations);
46    }
47
48    pub(crate) fn is_empty(&self) -> bool {
49        self.locations.is_empty()
50    }
51
52    pub(crate) fn clear(&mut self) {
53        if !self.locations.is_empty() {
54            self.last_location = Some((self.symbol_range.clone(), self.locations.clone()));
55        }
56
57        self.symbol_range = 0..0;
58        self.locations = Rc::new(vec![]);
59    }
60
61    pub(crate) fn is_same(&self, offset: usize) -> bool {
62        self.symbol_range.contains(&offset)
63    }
64}
65
66impl InputState {
67    pub(crate) fn handle_hover_definition(
68        &mut self,
69        offset: usize,
70        window: &mut Window,
71        cx: &mut Context<Self>,
72    ) {
73        let Some(provider) = self.lsp.definition_provider.clone() else {
74            return;
75        };
76
77        if self.hover_definition.is_same(offset) {
78            return;
79        }
80
81        // Currently not implemented.
82        let task = provider.definitions(&self.text, offset, window, cx);
83        let mut symbol_range = self.text.word_range(offset).unwrap_or(offset..offset);
84        let editor = cx.entity();
85        self.lsp._hover_task = cx.spawn_in(window, async move |_, cx| {
86            let locations = task.await?;
87
88            _ = editor.update(cx, |editor, cx| {
89                if locations.is_empty() {
90                    editor.hover_definition.clear();
91                } else {
92                    if let Some(location) = locations.first() {
93                        if let Some(range) = location.origin_selection_range {
94                            let start = editor.text.position_to_offset(&range.start);
95                            let end = editor.text.position_to_offset(&range.end);
96                            symbol_range = start..end;
97                        }
98                    }
99
100                    editor
101                        .hover_definition
102                        .update(symbol_range.clone(), locations.clone());
103                }
104                cx.notify();
105            });
106
107            Ok(())
108        });
109    }
110
111    pub(crate) fn on_action_go_to_definition(
112        &mut self,
113        _: &GoToDefinition,
114        _: &mut Window,
115        cx: &mut Context<Self>,
116    ) {
117        let offset = self.cursor();
118        if let Some((symbol_range, locations)) = self.hover_definition.last_location.clone() {
119            if !(symbol_range.start..=symbol_range.end).contains(&offset) {
120                return;
121            }
122
123            if let Some(location) = locations.first().cloned() {
124                self.go_to_definition(&location, cx);
125            }
126        }
127    }
128
129    /// Return true if handled.
130    pub(crate) fn handle_click_hover_definition(
131        &mut self,
132        event: &MouseDownEvent,
133        offset: usize,
134        _: &mut Window,
135        cx: &mut Context<InputState>,
136    ) -> bool {
137        if !event.modifiers.secondary() {
138            return false;
139        }
140
141        if self.hover_definition.is_empty() {
142            return false;
143        };
144        if !self.hover_definition.is_same(offset) {
145            return false;
146        }
147
148        let Some(location) = self.hover_definition.locations.first().cloned() else {
149            return false;
150        };
151
152        self.go_to_definition(&location, cx);
153
154        true
155    }
156
157    pub(crate) fn go_to_definition(
158        &mut self,
159        location: &lsp_types::LocationLink,
160        cx: &mut Context<Self>,
161    ) {
162        if location
163            .target_uri
164            .scheme()
165            .map(|s| s.as_str() == "https" || s.as_str() == "http")
166            == Some(true)
167        {
168            cx.open_url(&location.target_uri.to_string());
169        } else {
170            // Move to the location.
171            let target_range = location.target_range;
172            let start = self.text.position_to_offset(&target_range.start);
173            let end = self.text.position_to_offset(&target_range.end);
174
175            self.move_to(start, cx);
176            self.select_to(end, cx);
177        }
178    }
179}
180
181impl TextElement {
182    pub(crate) fn layout_hover_definition(
183        &self,
184        cx: &App,
185    ) -> Option<(Range<usize>, HighlightStyle)> {
186        let editor = self.state.read(cx);
187        if !editor.mode.is_code_editor() {
188            return None;
189        }
190
191        if editor.hover_definition.is_empty() {
192            return None;
193        };
194
195        let mut highlight_style: HighlightStyle = cx
196            .theme()
197            .highlight_theme
198            .link_text
199            .map(|style| style.into())
200            .unwrap_or_default();
201
202        highlight_style.underline = Some(UnderlineStyle {
203            thickness: px(1.),
204            ..UnderlineStyle::default()
205        });
206
207        Some((
208            editor.hover_definition.symbol_range.clone(),
209            highlight_style,
210        ))
211    }
212
213    pub(crate) fn layout_hover_definition_hitbox(
214        &self,
215        editor: &InputState,
216        window: &mut Window,
217        _cx: &App,
218    ) -> Option<Hitbox> {
219        if !editor.mode.is_code_editor() {
220            return None;
221        }
222
223        if editor.hover_definition.is_empty() {
224            return None;
225        };
226
227        let Some(bounds) = editor.range_to_bounds(&editor.hover_definition.symbol_range) else {
228            return None;
229        };
230
231        Some(window.insert_hitbox(bounds, gpui::HitboxBehavior::Normal))
232    }
233}