gpui_base/input/editor/lsp/
definitions.rs1use 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
10pub trait DefinitionProvider {
14 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 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 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 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 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(¶ms, window, cx) {
178 return;
179 }
180 }
181
182 if external {
183 cx.open_url(&location.target_uri.to_string());
184 } else {
185 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
196impl InputBaseState<EditorMode> {
199 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 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}