Skip to main content

gpui_base/input/editor/lsp/
definitions.rs

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