gpui_component/input/lsp/
definitions.rs1use 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
13pub trait DefinitionProvider {
17 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 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 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 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 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}