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