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