1use std::ops::Range;
2
3use gpui::{impl_internal_actions, MutableAppContext, Task, ViewContext};
4use language::{Bias, ToOffset};
5use project::LocationLink;
6use settings::Settings;
7use util::TryFutureExt;
8use workspace::Workspace;
9
10use crate::{
11 Anchor, DisplayPoint, Editor, EditorSnapshot, Event, GoToDefinition, Select, SelectPhase,
12};
13
14#[derive(Clone, PartialEq)]
15pub struct UpdateGoToDefinitionLink {
16 pub point: Option<DisplayPoint>,
17 pub cmd_held: bool,
18}
19
20#[derive(Clone, PartialEq)]
21pub struct CmdChanged {
22 pub cmd_down: bool,
23}
24
25#[derive(Clone, PartialEq)]
26pub struct GoToFetchedDefinition {
27 pub point: DisplayPoint,
28}
29
30impl_internal_actions!(
31 editor,
32 [UpdateGoToDefinitionLink, CmdChanged, GoToFetchedDefinition]
33);
34
35pub fn init(cx: &mut MutableAppContext) {
36 cx.add_action(update_go_to_definition_link);
37 cx.add_action(cmd_changed);
38 cx.add_action(go_to_fetched_definition);
39}
40
41#[derive(Default)]
42pub struct LinkGoToDefinitionState {
43 pub last_mouse_location: Option<Anchor>,
44 pub symbol_range: Option<Range<Anchor>>,
45 pub definitions: Vec<LocationLink>,
46 pub task: Option<Task<Option<()>>>,
47}
48
49pub fn update_go_to_definition_link(
50 editor: &mut Editor,
51 &UpdateGoToDefinitionLink { point, cmd_held }: &UpdateGoToDefinitionLink,
52 cx: &mut ViewContext<Editor>,
53) {
54 // Store new mouse point as an anchor
55 let snapshot = editor.snapshot(cx);
56 let point = point.map(|point| {
57 snapshot
58 .buffer_snapshot
59 .anchor_before(point.to_offset(&snapshot.display_snapshot, Bias::Left))
60 });
61
62 // If the new point is the same as the previously stored one, return early
63 if let (Some(a), Some(b)) = (
64 &point,
65 &editor.link_go_to_definition_state.last_mouse_location,
66 ) {
67 if a.cmp(&b, &snapshot.buffer_snapshot).is_eq() {
68 return;
69 }
70 }
71
72 editor.link_go_to_definition_state.last_mouse_location = point.clone();
73 if cmd_held {
74 if let Some(point) = point {
75 show_link_definition(editor, point, snapshot, cx);
76 return;
77 }
78 }
79
80 hide_link_definition(editor, cx);
81}
82
83pub fn cmd_changed(
84 editor: &mut Editor,
85 &CmdChanged { cmd_down }: &CmdChanged,
86 cx: &mut ViewContext<Editor>,
87) {
88 if let Some(point) = editor
89 .link_go_to_definition_state
90 .last_mouse_location
91 .clone()
92 {
93 if cmd_down {
94 let snapshot = editor.snapshot(cx);
95 show_link_definition(editor, point.clone(), snapshot, cx);
96 } else {
97 hide_link_definition(editor, cx)
98 }
99 }
100}
101
102pub fn show_link_definition(
103 editor: &mut Editor,
104 trigger_point: Anchor,
105 snapshot: EditorSnapshot,
106 cx: &mut ViewContext<Editor>,
107) {
108 if editor.pending_rename.is_some() {
109 return;
110 }
111
112 let (buffer, buffer_position) = if let Some(output) = editor
113 .buffer
114 .read(cx)
115 .text_anchor_for_position(trigger_point.clone(), cx)
116 {
117 output
118 } else {
119 return;
120 };
121
122 let excerpt_id = if let Some((excerpt_id, _, _)) = editor
123 .buffer()
124 .read(cx)
125 .excerpt_containing(trigger_point.clone(), cx)
126 {
127 excerpt_id
128 } else {
129 return;
130 };
131
132 let project = if let Some(project) = editor.project.clone() {
133 project
134 } else {
135 return;
136 };
137
138 // Don't request again if the location is within the symbol region of a previous request
139 if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range {
140 if symbol_range
141 .start
142 .cmp(&trigger_point, &snapshot.buffer_snapshot)
143 .is_le()
144 && symbol_range
145 .end
146 .cmp(&trigger_point, &snapshot.buffer_snapshot)
147 .is_ge()
148 {
149 return;
150 }
151 }
152
153 let task = cx.spawn_weak(|this, mut cx| {
154 async move {
155 // query the LSP for definition info
156 let definition_request = cx.update(|cx| {
157 project.update(cx, |project, cx| {
158 project.definition(&buffer, buffer_position.clone(), cx)
159 })
160 });
161
162 let result = definition_request.await.ok().map(|definition_result| {
163 (
164 definition_result.iter().find_map(|link| {
165 link.origin.as_ref().map(|origin| {
166 let start = snapshot
167 .buffer_snapshot
168 .anchor_in_excerpt(excerpt_id.clone(), origin.range.start);
169 let end = snapshot
170 .buffer_snapshot
171 .anchor_in_excerpt(excerpt_id.clone(), origin.range.end);
172
173 start..end
174 })
175 }),
176 definition_result,
177 )
178 });
179
180 if let Some(this) = this.upgrade(&cx) {
181 this.update(&mut cx, |this, cx| {
182 // Clear any existing highlights
183 this.clear_text_highlights::<LinkGoToDefinitionState>(cx);
184 this.link_go_to_definition_state.symbol_range = result
185 .as_ref()
186 .and_then(|(symbol_range, _)| symbol_range.clone());
187
188 if let Some((symbol_range, definitions)) = result {
189 this.link_go_to_definition_state.definitions = definitions.clone();
190
191 let buffer_snapshot = buffer.read(cx).snapshot();
192 // Only show highlight if there exists a definition to jump to that doesn't contain
193 // the current location.
194 if definitions.iter().any(|definition| {
195 let target = &definition.target;
196 if target.buffer == buffer {
197 let range = &target.range;
198 // Expand range by one character as lsp definition ranges include positions adjacent
199 // but not contained by the symbol range
200 let start = buffer_snapshot.clip_offset(
201 range.start.to_offset(&buffer_snapshot).saturating_sub(1),
202 Bias::Left,
203 );
204 let end = buffer_snapshot.clip_offset(
205 range.end.to_offset(&buffer_snapshot) + 1,
206 Bias::Right,
207 );
208 let offset = buffer_position.to_offset(&buffer_snapshot);
209 !(start <= offset && end >= offset)
210 } else {
211 true
212 }
213 }) {
214 // If no symbol range returned from language server, use the surrounding word.
215 let highlight_range = symbol_range.unwrap_or_else(|| {
216 let snapshot = &snapshot.buffer_snapshot;
217 let (offset_range, _) = snapshot.surrounding_word(trigger_point);
218
219 snapshot.anchor_before(offset_range.start)
220 ..snapshot.anchor_after(offset_range.end)
221 });
222
223 // Highlight symbol using theme link definition highlight style
224 let style = cx.global::<Settings>().theme.editor.link_definition;
225 this.highlight_text::<LinkGoToDefinitionState>(
226 vec![highlight_range],
227 style,
228 cx,
229 )
230 } else {
231 hide_link_definition(this, cx);
232 }
233 }
234 })
235 }
236
237 Ok::<_, anyhow::Error>(())
238 }
239 .log_err()
240 });
241
242 editor.link_go_to_definition_state.task = Some(task);
243}
244
245pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
246 if editor.link_go_to_definition_state.symbol_range.is_some()
247 || !editor.link_go_to_definition_state.definitions.is_empty()
248 {
249 editor.link_go_to_definition_state.symbol_range.take();
250 editor.link_go_to_definition_state.definitions.clear();
251 cx.notify();
252 }
253
254 editor.link_go_to_definition_state.task = None;
255
256 editor.clear_text_highlights::<LinkGoToDefinitionState>(cx);
257}
258
259pub fn go_to_fetched_definition(
260 workspace: &mut Workspace,
261 GoToFetchedDefinition { point }: &GoToFetchedDefinition,
262 cx: &mut ViewContext<Workspace>,
263) {
264 let active_item = workspace.active_item(cx);
265 let editor_handle = if let Some(editor) = active_item
266 .as_ref()
267 .and_then(|item| item.act_as::<Editor>(cx))
268 {
269 editor
270 } else {
271 return;
272 };
273
274 let definitions = editor_handle.update(cx, |editor, cx| {
275 let definitions = editor.link_go_to_definition_state.definitions.clone();
276 hide_link_definition(editor, cx);
277 definitions
278 });
279
280 if !definitions.is_empty() {
281 editor_handle.update(cx, |editor, cx| {
282 if !editor.focused {
283 cx.focus_self();
284 cx.emit(Event::Activate);
285 }
286 });
287
288 Editor::navigate_to_definitions(workspace, editor_handle, definitions, cx);
289 } else {
290 editor_handle.update(cx, |editor, cx| {
291 editor.select(
292 &Select(SelectPhase::Begin {
293 position: point.clone(),
294 add: false,
295 click_count: 1,
296 }),
297 cx,
298 );
299 });
300
301 Editor::go_to_definition(workspace, &GoToDefinition, cx);
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use futures::StreamExt;
308 use indoc::indoc;
309
310 use crate::test::EditorLspTestContext;
311
312 use super::*;
313
314 #[gpui::test]
315 async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
316 let mut cx = EditorLspTestContext::new_rust(
317 lsp::ServerCapabilities {
318 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
319 ..Default::default()
320 },
321 cx,
322 )
323 .await;
324
325 cx.set_state(indoc! {"
326 fn |test()
327 do_work();
328
329 fn do_work()
330 test();"});
331
332 // Basic hold cmd, expect highlight in region if response contains definition
333 let hover_point = cx.display_point(indoc! {"
334 fn test()
335 do_w|ork();
336
337 fn do_work()
338 test();"});
339
340 let symbol_range = cx.lsp_range(indoc! {"
341 fn test()
342 [do_work]();
343
344 fn do_work()
345 test();"});
346 let target_range = cx.lsp_range(indoc! {"
347 fn test()
348 do_work();
349
350 fn [do_work]()
351 test();"});
352
353 let mut requests =
354 cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
355 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
356 lsp::LocationLink {
357 origin_selection_range: Some(symbol_range),
358 target_uri: url.clone(),
359 target_range,
360 target_selection_range: target_range,
361 },
362 ])))
363 });
364 cx.update_editor(|editor, cx| {
365 update_go_to_definition_link(
366 editor,
367 &UpdateGoToDefinitionLink {
368 point: Some(hover_point),
369 cmd_held: true,
370 },
371 cx,
372 );
373 });
374 requests.next().await;
375 cx.foreground().run_until_parked();
376 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
377 fn test()
378 [do_work]();
379
380 fn do_work()
381 test();"});
382
383 // Unpress cmd causes highlight to go away
384 cx.update_editor(|editor, cx| {
385 cmd_changed(editor, &CmdChanged { cmd_down: false }, cx);
386 });
387 // Assert no link highlights
388 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
389 fn test()
390 do_work();
391
392 fn do_work()
393 test();"});
394
395 // Response without source range still highlights word
396 cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None);
397 let mut requests =
398 cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
399 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
400 lsp::LocationLink {
401 // No origin range
402 origin_selection_range: None,
403 target_uri: url.clone(),
404 target_range,
405 target_selection_range: target_range,
406 },
407 ])))
408 });
409 cx.update_editor(|editor, cx| {
410 update_go_to_definition_link(
411 editor,
412 &UpdateGoToDefinitionLink {
413 point: Some(hover_point),
414 cmd_held: true,
415 },
416 cx,
417 );
418 });
419 requests.next().await;
420 cx.foreground().run_until_parked();
421
422 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
423 fn test()
424 [do_work]();
425
426 fn do_work()
427 test();"});
428
429 // Moving mouse to location with no response dismisses highlight
430 let hover_point = cx.display_point(indoc! {"
431 f|n test()
432 do_work();
433
434 fn do_work()
435 test();"});
436 let mut requests =
437 cx.lsp
438 .handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
439 // No definitions returned
440 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
441 });
442 cx.update_editor(|editor, cx| {
443 update_go_to_definition_link(
444 editor,
445 &UpdateGoToDefinitionLink {
446 point: Some(hover_point),
447 cmd_held: true,
448 },
449 cx,
450 );
451 });
452 requests.next().await;
453 cx.foreground().run_until_parked();
454
455 // Assert no link highlights
456 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
457 fn test()
458 do_work();
459
460 fn do_work()
461 test();"});
462
463 // Move mouse without cmd and then pressing cmd triggers highlight
464 let hover_point = cx.display_point(indoc! {"
465 fn test()
466 do_work();
467
468 fn do_work()
469 te|st();"});
470 cx.update_editor(|editor, cx| {
471 update_go_to_definition_link(
472 editor,
473 &UpdateGoToDefinitionLink {
474 point: Some(hover_point),
475 cmd_held: false,
476 },
477 cx,
478 );
479 });
480 cx.foreground().run_until_parked();
481
482 // Assert no link highlights
483 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
484 fn test()
485 do_work();
486
487 fn do_work()
488 test();"});
489
490 let symbol_range = cx.lsp_range(indoc! {"
491 fn test()
492 do_work();
493
494 fn do_work()
495 [test]();"});
496 let target_range = cx.lsp_range(indoc! {"
497 fn [test]()
498 do_work();
499
500 fn do_work()
501 test();"});
502
503 let mut requests =
504 cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
505 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
506 lsp::LocationLink {
507 origin_selection_range: Some(symbol_range),
508 target_uri: url,
509 target_range,
510 target_selection_range: target_range,
511 },
512 ])))
513 });
514 cx.update_editor(|editor, cx| {
515 cmd_changed(editor, &CmdChanged { cmd_down: true }, cx);
516 });
517 requests.next().await;
518 cx.foreground().run_until_parked();
519
520 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
521 fn test()
522 do_work();
523
524 fn do_work()
525 [test]();"});
526
527 // Moving within symbol range doesn't re-request
528 let hover_point = cx.display_point(indoc! {"
529 fn test()
530 do_work();
531
532 fn do_work()
533 tes|t();"});
534 cx.update_editor(|editor, cx| {
535 update_go_to_definition_link(
536 editor,
537 &UpdateGoToDefinitionLink {
538 point: Some(hover_point),
539 cmd_held: true,
540 },
541 cx,
542 );
543 });
544 cx.foreground().run_until_parked();
545 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
546 fn test()
547 do_work();
548
549 fn do_work()
550 [test]();"});
551
552 // Cmd click with existing definition doesn't re-request and dismisses highlight
553 cx.update_workspace(|workspace, cx| {
554 go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
555 });
556 // Assert selection moved to to definition
557 cx.lsp
558 .handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
559 // Empty definition response to make sure we aren't hitting the lsp and using
560 // the cached location instead
561 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
562 });
563 cx.assert_editor_state(indoc! {"
564 fn [test}()
565 do_work();
566
567 fn do_work()
568 test();"});
569 // Assert no link highlights after jump
570 cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
571 fn test()
572 do_work();
573
574 fn do_work()
575 test();"});
576
577 // Cmd click without existing definition requests and jumps
578 let hover_point = cx.display_point(indoc! {"
579 fn test()
580 do_w|ork();
581
582 fn do_work()
583 test();"});
584 let target_range = cx.lsp_range(indoc! {"
585 fn test()
586 do_work();
587
588 fn [do_work]()
589 test();"});
590
591 let mut requests =
592 cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
593 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
594 lsp::LocationLink {
595 origin_selection_range: None,
596 target_uri: url,
597 target_range,
598 target_selection_range: target_range,
599 },
600 ])))
601 });
602 cx.update_workspace(|workspace, cx| {
603 go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
604 });
605 requests.next().await;
606 cx.foreground().run_until_parked();
607
608 cx.assert_editor_state(indoc! {"
609 fn test()
610 do_work();
611
612 fn [do_work}()
613 test();"});
614 }
615}