1use std::sync::Arc;
2
3use collections::{HashMap, HashSet};
4use futures::future::join_all;
5use gpui::{MouseButton, SharedString, Task, WeakEntity};
6use itertools::Itertools;
7use language::{BufferId, ClientCommand};
8use multi_buffer::{Anchor, MultiBufferRow, MultiBufferSnapshot, ToPoint as _};
9use project::{CodeAction, TaskSourceKind};
10use task::TaskContext;
11
12use ui::{Context, Window, div, prelude::*};
13
14use crate::{
15 Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT, SelectionEffects,
16 actions::ToggleCodeLens,
17 display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
18 hover_links::HoverLink,
19};
20
21#[derive(Clone, Debug)]
22struct CodeLensLine {
23 position: Anchor,
24 indent_column: u32,
25 items: Vec<CodeLensItem>,
26}
27
28#[derive(Clone, Debug)]
29struct CodeLensItem {
30 title: SharedString,
31 action: CodeAction,
32}
33
34pub(super) struct CodeLensState {
35 pub(super) block_ids: HashMap<BufferId, Vec<CustomBlockId>>,
36 resolve_task: Task<()>,
37}
38
39impl Default for CodeLensState {
40 fn default() -> Self {
41 Self {
42 block_ids: HashMap::default(),
43 resolve_task: Task::ready(()),
44 }
45 }
46}
47
48impl CodeLensState {
49 fn all_block_ids(&self) -> HashSet<CustomBlockId> {
50 self.block_ids.values().flatten().copied().collect()
51 }
52}
53
54fn group_lenses_by_row(
55 lenses: Vec<(Anchor, CodeLensItem)>,
56 snapshot: &MultiBufferSnapshot,
57) -> impl Iterator<Item = CodeLensLine> {
58 lenses
59 .into_iter()
60 .into_group_map_by(|(position, _)| {
61 let row = position.to_point(snapshot).row;
62 MultiBufferRow(row)
63 })
64 .into_iter()
65 .sorted_by_key(|(row, _)| *row)
66 .filter_map(|(row, entries)| {
67 let position = entries.first()?.0;
68 let items = entries.into_iter().map(|(_, item)| item).collect();
69 let indent_column = snapshot.indent_size_for_line(row).len;
70 Some(CodeLensLine {
71 position,
72 indent_column,
73 items,
74 })
75 })
76}
77
78fn render_code_lens_line(
79 buffer_id: BufferId,
80 line_number: usize,
81 lens: CodeLensLine,
82 editor: WeakEntity<Editor>,
83) -> impl Fn(&mut crate::display_map::BlockContext) -> gpui::AnyElement {
84 move |cx| {
85 let mut children = Vec::with_capacity((2 * lens.items.len()).saturating_sub(1));
86 let text_style = &cx.editor_style.text;
87 let font = text_style.font();
88 let font_size = text_style.font_size.to_pixels(cx.window.rem_size()) * 0.9;
89
90 for (i, item) in lens.items.iter().enumerate() {
91 if i > 0 {
92 children.push(
93 div()
94 .font(font.clone())
95 .text_size(font_size)
96 .text_color(cx.app.theme().colors().text_muted)
97 .child(" | ")
98 .into_any_element(),
99 );
100 }
101
102 let title = item.title.clone();
103 let action = item.action.clone();
104 let editor_handle = editor.clone();
105 let position = lens.position;
106 let id = SharedString::from(format!("{buffer_id}:{line_number}:{i}"));
107
108 children.push(
109 div()
110 .id(ElementId::Name(id))
111 .font(font.clone())
112 .text_size(font_size)
113 .text_color(cx.app.theme().colors().text_muted)
114 .cursor_pointer()
115 .hover(|style| style.text_color(cx.app.theme().colors().text))
116 .child(title.clone())
117 .on_mouse_down(MouseButton::Left, |_, _, cx| {
118 cx.stop_propagation();
119 })
120 .on_mouse_down(MouseButton::Right, |_, _, cx| {
121 cx.stop_propagation();
122 })
123 .on_click({
124 move |_event, window, cx| {
125 if let Some(editor) = editor_handle.upgrade() {
126 editor.update(cx, |editor, cx| {
127 editor.change_selections(
128 SelectionEffects::default(),
129 window,
130 cx,
131 |s| {
132 s.select_anchor_ranges([position..position]);
133 },
134 );
135
136 let action = action.clone();
137 if let Some(workspace) = editor.workspace() {
138 if try_handle_client_command(
139 &action, editor, &workspace, window, cx,
140 ) {
141 return;
142 }
143
144 let project = workspace.read(cx).project().clone();
145 if let Some(buffer) = editor
146 .buffer()
147 .read(cx)
148 .buffer(action.range.start.buffer_id)
149 {
150 project
151 .update(cx, |project, cx| {
152 project
153 .apply_code_action(buffer, action, true, cx)
154 })
155 .detach_and_log_err(cx);
156 }
157 }
158 });
159 }
160 }
161 })
162 .into_any_element(),
163 );
164 }
165
166 div()
167 .pl(cx.margins.gutter.full_width() + cx.em_width * (lens.indent_column as f32 + 0.5))
168 .h_full()
169 .flex()
170 .flex_row()
171 .items_end()
172 .children(children)
173 .into_any_element()
174 }
175}
176
177pub(super) fn try_handle_client_command(
178 action: &CodeAction,
179 editor: &mut Editor,
180 workspace: &gpui::Entity<workspace::Workspace>,
181 window: &mut Window,
182 cx: &mut Context<Editor>,
183) -> bool {
184 let Some(command) = action.lsp_action.command() else {
185 return false;
186 };
187
188 let arguments = command.arguments.as_deref().unwrap_or_default();
189 let project = workspace.read(cx).project().clone();
190 let client_command = project
191 .read(cx)
192 .lsp_store()
193 .read(cx)
194 .language_server_adapter_for_id(action.server_id)
195 .and_then(|adapter| adapter.adapter.client_command(&command.command, arguments))
196 .or_else(|| match command.command.as_str() {
197 "editor.action.showReferences"
198 | "editor.action.goToLocations"
199 | "editor.action.peekLocations" => Some(ClientCommand::ShowLocations),
200 _ => None,
201 });
202
203 match client_command {
204 Some(ClientCommand::ScheduleTask(task_template)) => {
205 schedule_task(task_template, action, editor, workspace, window, cx)
206 }
207 Some(ClientCommand::ShowLocations) => {
208 try_show_references(arguments, action, editor, window, cx)
209 }
210 None => false,
211 }
212}
213
214fn schedule_task(
215 task_template: task::TaskTemplate,
216 action: &CodeAction,
217 editor: &Editor,
218 workspace: &gpui::Entity<workspace::Workspace>,
219 window: &mut Window,
220 cx: &mut Context<Editor>,
221) -> bool {
222 let task_context = TaskContext {
223 cwd: task_template.cwd.as_ref().map(std::path::PathBuf::from),
224 ..TaskContext::default()
225 };
226 let language_name = editor
227 .buffer()
228 .read(cx)
229 .buffer(action.range.start.buffer_id)
230 .and_then(|buffer| buffer.read(cx).language())
231 .map(|language| language.name());
232 let task_source_kind = match language_name {
233 Some(language_name) => TaskSourceKind::Lsp {
234 server: action.server_id,
235 language_name: SharedString::from(language_name),
236 },
237 None => TaskSourceKind::AbsPath {
238 id_base: "code-lens".into(),
239 abs_path: task_template
240 .cwd
241 .as_ref()
242 .map(std::path::PathBuf::from)
243 .unwrap_or_default(),
244 },
245 };
246
247 workspace.update(cx, |workspace, cx| {
248 workspace.schedule_task(
249 task_source_kind,
250 &task_template,
251 &task_context,
252 false,
253 window,
254 cx,
255 );
256 });
257 true
258}
259
260fn try_show_references(
261 arguments: &[serde_json::Value],
262 action: &CodeAction,
263 editor: &mut Editor,
264 window: &mut Window,
265 cx: &mut Context<Editor>,
266) -> bool {
267 if arguments.len() < 3 {
268 return false;
269 }
270 let Ok(locations) = serde_json::from_value::<Vec<lsp::Location>>(arguments[2].clone()) else {
271 return false;
272 };
273 if locations.is_empty() {
274 return false;
275 }
276
277 let server_id = action.server_id;
278 let nav_entry = editor.navigation_entry(editor.selections.newest_anchor().head(), cx);
279 let links = locations
280 .into_iter()
281 .map(|location| HoverLink::InlayHint(location, server_id))
282 .collect();
283 editor
284 .navigate_to_hover_links(None, links, nav_entry, false, window, cx)
285 .detach_and_log_err(cx);
286
287 true
288}
289
290impl Editor {
291 pub(super) fn refresh_code_lenses(
292 &mut self,
293 for_buffer: Option<BufferId>,
294 _window: &Window,
295 cx: &mut Context<Self>,
296 ) {
297 if !self.lsp_data_enabled() || self.code_lens.is_none() {
298 return;
299 }
300 let Some(project) = self.project.clone() else {
301 return;
302 };
303
304 let buffers_to_query = self
305 .visible_buffers(cx)
306 .into_iter()
307 .filter(|buffer| self.is_lsp_relevant(buffer.read(cx).file(), cx))
308 .chain(for_buffer.and_then(|buffer_id| self.buffer.read(cx).buffer(buffer_id)))
309 .filter(|editor_buffer| {
310 let editor_buffer_id = editor_buffer.read(cx).remote_id();
311 for_buffer.is_none_or(|buffer_id| buffer_id == editor_buffer_id)
312 && self.registered_buffers.contains_key(&editor_buffer_id)
313 })
314 .unique_by(|buffer| buffer.read(cx).remote_id())
315 .collect::<Vec<_>>();
316
317 if buffers_to_query.is_empty() {
318 return;
319 }
320
321 let project = project.downgrade();
322 self.refresh_code_lens_task = cx.spawn(async move |editor, cx| {
323 cx.background_executor()
324 .timer(LSP_REQUEST_DEBOUNCE_TIMEOUT)
325 .await;
326
327 let Some(tasks) = project
328 .update(cx, |project, cx| {
329 project.lsp_store().update(cx, |lsp_store, cx| {
330 buffers_to_query
331 .into_iter()
332 .map(|buffer| {
333 let buffer_id = buffer.read(cx).remote_id();
334 let task = lsp_store.code_lens_actions(&buffer, cx);
335 async move { (buffer_id, task.await) }
336 })
337 .collect::<Vec<_>>()
338 })
339 })
340 .ok()
341 else {
342 return;
343 };
344
345 let results = join_all(tasks).await;
346 if results.is_empty() {
347 return;
348 }
349
350 let Ok(multi_buffer_snapshot) =
351 editor.update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx))
352 else {
353 return;
354 };
355
356 let mut new_lenses_per_buffer = HashMap::default();
357 for (buffer_id, result) in results {
358 let actions = match result {
359 Ok(Some(actions)) => actions,
360 Ok(None) => continue,
361 Err(e) => {
362 log::error!("Failed to fetch code lenses for buffer {buffer_id:?}: {e:#}");
363 continue;
364 }
365 };
366 let individual_lenses = actions
367 .into_iter()
368 .filter_map(|action| {
369 let title = match &action.lsp_action {
370 project::LspAction::CodeLens(lens) => lens
371 .command
372 .as_ref()
373 .map(|cmd| SharedString::from(&cmd.title)),
374 _ => None,
375 }?;
376 let position =
377 multi_buffer_snapshot.anchor_in_excerpt(action.range.start)?;
378 Some((position, CodeLensItem { title, action }))
379 })
380 .collect();
381 new_lenses_per_buffer.insert(
382 buffer_id,
383 group_lenses_by_row(individual_lenses, &multi_buffer_snapshot)
384 .collect::<Vec<_>>(),
385 );
386 }
387
388 editor
389 .update(cx, |editor, cx| {
390 let code_lens = editor.code_lens.get_or_insert_with(CodeLensState::default);
391 let mut blocks_to_remove = HashSet::default();
392 for buffer_id in new_lenses_per_buffer.keys() {
393 if let Some(old_ids) = code_lens.block_ids.remove(buffer_id) {
394 blocks_to_remove.extend(old_ids);
395 }
396 }
397 if !blocks_to_remove.is_empty() {
398 editor.remove_blocks(blocks_to_remove, None, cx);
399 }
400
401 let editor_handle = cx.entity().downgrade();
402 for (buffer_id, lens_lines) in new_lenses_per_buffer {
403 if lens_lines.is_empty() {
404 continue;
405 }
406 let blocks = lens_lines
407 .into_iter()
408 .enumerate()
409 .map(|(line_number, lens_line)| {
410 let position = lens_line.position;
411 BlockProperties {
412 placement: BlockPlacement::Above(position),
413 height: Some(1),
414 style: BlockStyle::Flex,
415 render: Arc::new(render_code_lens_line(
416 buffer_id,
417 line_number,
418 lens_line,
419 editor_handle.clone(),
420 )),
421 priority: 0,
422 }
423 })
424 .collect::<Vec<_>>();
425 let block_ids = editor.insert_blocks(blocks, None, cx);
426 editor
427 .code_lens
428 .get_or_insert_with(CodeLensState::default)
429 .block_ids
430 .entry(buffer_id)
431 .or_default()
432 .extend(block_ids);
433 }
434
435 editor.resolve_visible_code_lenses(cx);
436 })
437 .ok();
438 });
439 }
440
441 pub fn supports_code_lens(&self, cx: &ui::App) -> bool {
442 let Some(project) = self.project.as_ref() else {
443 return false;
444 };
445 let lsp_store = project.read(cx).lsp_store().read(cx);
446 lsp_store
447 .lsp_server_capabilities
448 .values()
449 .any(|caps| caps.code_lens_provider.is_some())
450 }
451
452 pub fn code_lens_enabled(&self) -> bool {
453 self.code_lens.is_some()
454 }
455
456 pub fn toggle_code_lens_action(
457 &mut self,
458 _: &ToggleCodeLens,
459 window: &mut Window,
460 cx: &mut Context<Self>,
461 ) {
462 let currently_enabled = self.code_lens.is_some();
463 self.toggle_code_lens(!currently_enabled, window, cx);
464 }
465
466 pub(super) fn toggle_code_lens(
467 &mut self,
468 enabled: bool,
469 window: &mut Window,
470 cx: &mut Context<Self>,
471 ) {
472 if enabled {
473 self.code_lens.get_or_insert_with(CodeLensState::default);
474 self.refresh_code_lenses(None, window, cx);
475 } else {
476 self.clear_code_lenses(cx);
477 }
478 }
479
480 pub(super) fn resolve_visible_code_lenses(&mut self, cx: &mut Context<Self>) {
481 if !self.lsp_data_enabled() || self.code_lens.is_none() {
482 return;
483 }
484 let Some(project) = self.project.clone() else {
485 return;
486 };
487
488 let resolve_tasks = self
489 .visible_buffer_ranges(cx)
490 .into_iter()
491 .filter_map(|(snapshot, visible_range, _)| {
492 let buffer_id = snapshot.remote_id();
493 let buffer = self.buffer.read(cx).buffer(buffer_id)?;
494 let visible_anchor_range = snapshot.anchor_before(visible_range.start)
495 ..snapshot.anchor_after(visible_range.end);
496 let task = project.update(cx, |project, cx| {
497 project.lsp_store().update(cx, |lsp_store, cx| {
498 lsp_store.resolve_visible_code_lenses(&buffer, visible_anchor_range, cx)
499 })
500 });
501 Some((buffer_id, task))
502 })
503 .collect::<Vec<_>>();
504 if resolve_tasks.is_empty() {
505 return;
506 }
507
508 let code_lens = self.code_lens.get_or_insert_with(CodeLensState::default);
509 code_lens.resolve_task = cx.spawn(async move |editor, cx| {
510 let resolved_code_lens = join_all(
511 resolve_tasks
512 .into_iter()
513 .map(|(buffer_id, task)| async move { (buffer_id, task.await) }),
514 )
515 .await;
516 editor
517 .update(cx, |editor, cx| {
518 editor.insert_resolved_code_lens_blocks(resolved_code_lens, cx);
519 })
520 .ok();
521 });
522 }
523
524 fn insert_resolved_code_lens_blocks(
525 &mut self,
526 resolved_code_lens: Vec<(BufferId, Vec<CodeAction>)>,
527 cx: &mut Context<Self>,
528 ) {
529 let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
530 let editor_handle = cx.entity().downgrade();
531
532 for (buffer_id, actions) in resolved_code_lens {
533 let lenses = actions
534 .into_iter()
535 .filter_map(|action| {
536 let title = match &action.lsp_action {
537 project::LspAction::CodeLens(lens) => lens
538 .command
539 .as_ref()
540 .map(|cmd| SharedString::from(&cmd.title)),
541 _ => None,
542 }?;
543 let position = multi_buffer_snapshot.anchor_in_excerpt(action.range.start)?;
544 Some((position, CodeLensItem { title, action }))
545 })
546 .collect();
547
548 let blocks = group_lenses_by_row(lenses, &multi_buffer_snapshot)
549 .enumerate()
550 .map(|(line_number, lens_line)| {
551 let position = lens_line.position;
552 BlockProperties {
553 placement: BlockPlacement::Above(position),
554 height: Some(1),
555 style: BlockStyle::Flex,
556 render: Arc::new(render_code_lens_line(
557 buffer_id,
558 line_number,
559 lens_line,
560 editor_handle.clone(),
561 )),
562 priority: 0,
563 }
564 })
565 .collect::<Vec<_>>();
566
567 if !blocks.is_empty() {
568 let block_ids = self.insert_blocks(blocks, None, cx);
569 self.code_lens
570 .get_or_insert_with(CodeLensState::default)
571 .block_ids
572 .entry(buffer_id)
573 .or_default()
574 .extend(block_ids);
575 }
576 }
577 cx.notify();
578 }
579
580 pub(super) fn clear_code_lenses(&mut self, cx: &mut Context<Self>) {
581 if let Some(code_lens) = self.code_lens.take() {
582 let all_blocks = code_lens.all_block_ids();
583 if !all_blocks.is_empty() {
584 self.remove_blocks(all_blocks, None, cx);
585 }
586 cx.notify();
587 }
588 self.refresh_code_lens_task = Task::ready(());
589 }
590}
591
592#[cfg(test)]
593mod tests {
594 use std::{
595 sync::{Arc, Mutex},
596 time::Duration,
597 };
598
599 use collections::HashSet;
600 use futures::StreamExt;
601 use gpui::TestAppContext;
602 use settings::CodeLens;
603 use util::path;
604
605 use crate::{
606 Editor,
607 editor_tests::{init_test, update_test_editor_settings},
608 test::editor_lsp_test_context::EditorLspTestContext,
609 };
610
611 #[gpui::test]
612 async fn test_code_lens_blocks(cx: &mut TestAppContext) {
613 init_test(cx, |_| {});
614 update_test_editor_settings(cx, &|settings| {
615 settings.code_lens = Some(CodeLens::On);
616 });
617
618 let mut cx = EditorLspTestContext::new_typescript(
619 lsp::ServerCapabilities {
620 code_lens_provider: Some(lsp::CodeLensOptions {
621 resolve_provider: None,
622 }),
623 execute_command_provider: Some(lsp::ExecuteCommandOptions {
624 commands: vec!["lens_cmd".to_string()],
625 ..lsp::ExecuteCommandOptions::default()
626 }),
627 ..lsp::ServerCapabilities::default()
628 },
629 cx,
630 )
631 .await;
632
633 let mut code_lens_request =
634 cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
635 Ok(Some(vec![
636 lsp::CodeLens {
637 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
638 command: Some(lsp::Command {
639 title: "2 references".to_owned(),
640 command: "lens_cmd".to_owned(),
641 arguments: None,
642 }),
643 data: None,
644 },
645 lsp::CodeLens {
646 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 19)),
647 command: Some(lsp::Command {
648 title: "0 references".to_owned(),
649 command: "lens_cmd".to_owned(),
650 arguments: None,
651 }),
652 data: None,
653 },
654 ]))
655 });
656
657 cx.set_state("ˇfunction hello() {}\nfunction world() {}");
658
659 assert!(
660 code_lens_request.next().await.is_some(),
661 "should have received a code lens request"
662 );
663 cx.run_until_parked();
664
665 cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
666 assert_eq!(
667 editor.code_lens_enabled(),
668 true,
669 "code lens should be enabled"
670 );
671 let total_blocks: usize = editor
672 .code_lens
673 .as_ref()
674 .map(|s| s.block_ids.values().map(|v| v.len()).sum())
675 .unwrap_or(0);
676 assert_eq!(total_blocks, 2, "Should have inserted two code lens blocks");
677 });
678 }
679
680 #[gpui::test]
681 async fn test_code_lens_disabled_by_default(cx: &mut TestAppContext) {
682 init_test(cx, |_| {});
683
684 let mut cx = EditorLspTestContext::new_typescript(
685 lsp::ServerCapabilities {
686 code_lens_provider: Some(lsp::CodeLensOptions {
687 resolve_provider: None,
688 }),
689 execute_command_provider: Some(lsp::ExecuteCommandOptions {
690 commands: vec!["lens_cmd".to_string()],
691 ..lsp::ExecuteCommandOptions::default()
692 }),
693 ..lsp::ServerCapabilities::default()
694 },
695 cx,
696 )
697 .await;
698
699 cx.lsp
700 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
701 panic!("Should not request code lenses when disabled");
702 });
703
704 cx.set_state("ˇfunction hello() {}");
705 cx.run_until_parked();
706
707 cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
708 assert_eq!(
709 editor.code_lens_enabled(),
710 false,
711 "code lens should not be enabled when setting is off"
712 );
713 });
714 }
715
716 #[gpui::test]
717 async fn test_code_lens_toggling(cx: &mut TestAppContext) {
718 init_test(cx, |_| {});
719 update_test_editor_settings(cx, &|settings| {
720 settings.code_lens = Some(CodeLens::On);
721 });
722
723 let mut cx = EditorLspTestContext::new_typescript(
724 lsp::ServerCapabilities {
725 code_lens_provider: Some(lsp::CodeLensOptions {
726 resolve_provider: None,
727 }),
728 execute_command_provider: Some(lsp::ExecuteCommandOptions {
729 commands: vec!["lens_cmd".to_string()],
730 ..lsp::ExecuteCommandOptions::default()
731 }),
732 ..lsp::ServerCapabilities::default()
733 },
734 cx,
735 )
736 .await;
737
738 let mut code_lens_request =
739 cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
740 Ok(Some(vec![lsp::CodeLens {
741 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
742 command: Some(lsp::Command {
743 title: "1 reference".to_owned(),
744 command: "lens_cmd".to_owned(),
745 arguments: None,
746 }),
747 data: None,
748 }]))
749 });
750
751 cx.set_state("ˇfunction hello() {}");
752
753 assert!(
754 code_lens_request.next().await.is_some(),
755 "should have received a code lens request"
756 );
757 cx.run_until_parked();
758
759 cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
760 assert_eq!(
761 editor.code_lens_enabled(),
762 true,
763 "code lens should be enabled"
764 );
765 let total_blocks: usize = editor
766 .code_lens
767 .as_ref()
768 .map(|s| s.block_ids.values().map(|v| v.len()).sum())
769 .unwrap_or(0);
770 assert_eq!(total_blocks, 1, "Should have one code lens block");
771 });
772
773 cx.update_editor(|editor, _window, cx| {
774 editor.clear_code_lenses(cx);
775 });
776
777 cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
778 assert_eq!(
779 editor.code_lens_enabled(),
780 false,
781 "code lens should be disabled after clearing"
782 );
783 });
784 }
785
786 #[gpui::test]
787 async fn test_code_lens_resolve(cx: &mut TestAppContext) {
788 init_test(cx, |_| {});
789 update_test_editor_settings(cx, &|settings| {
790 settings.code_lens = Some(CodeLens::On);
791 });
792
793 let mut cx = EditorLspTestContext::new_typescript(
794 lsp::ServerCapabilities {
795 code_lens_provider: Some(lsp::CodeLensOptions {
796 resolve_provider: Some(true),
797 }),
798 ..lsp::ServerCapabilities::default()
799 },
800 cx,
801 )
802 .await;
803
804 let mut code_lens_request =
805 cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
806 Ok(Some(vec![
807 lsp::CodeLens {
808 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
809 command: None,
810 data: Some(serde_json::json!({"id": "lens_1"})),
811 },
812 lsp::CodeLens {
813 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 19)),
814 command: None,
815 data: Some(serde_json::json!({"id": "lens_2"})),
816 },
817 ]))
818 });
819
820 cx.lsp
821 .set_request_handler::<lsp::request::CodeLensResolve, _, _>(|lens, _| async move {
822 let id = lens
823 .data
824 .as_ref()
825 .and_then(|d| d.get("id"))
826 .and_then(|v| v.as_str())
827 .unwrap_or("unknown");
828 let title = match id {
829 "lens_1" => "3 references",
830 "lens_2" => "1 implementation",
831 _ => "unknown",
832 };
833 Ok(lsp::CodeLens {
834 command: Some(lsp::Command {
835 title: title.to_owned(),
836 command: format!("resolved_{id}"),
837 arguments: None,
838 }),
839 ..lens
840 })
841 });
842
843 cx.set_state("ˇfunction hello() {}\nfunction world() {}");
844
845 assert!(
846 code_lens_request.next().await.is_some(),
847 "should have received a code lens request"
848 );
849 cx.run_until_parked();
850
851 cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
852 let total_blocks: usize = editor
853 .code_lens
854 .as_ref()
855 .map(|s| s.block_ids.values().map(|v| v.len()).sum())
856 .unwrap_or(0);
857 assert_eq!(
858 total_blocks, 2,
859 "Unresolved lenses should have been resolved and displayed"
860 );
861 });
862 }
863
864 #[gpui::test]
865 async fn test_code_lens_resolve_only_visible(cx: &mut TestAppContext) {
866 init_test(cx, |_| {});
867 update_test_editor_settings(cx, &|settings| {
868 settings.code_lens = Some(CodeLens::On);
869 });
870
871 let line_count: u32 = 100;
872 let lens_every: u32 = 10;
873 let lines = (0..line_count)
874 .map(|i| format!("function func_{i}() {{}}"))
875 .collect::<Vec<_>>()
876 .join("\n");
877
878 let lens_lines = (0..line_count)
879 .filter(|i| i % lens_every == 0)
880 .collect::<Vec<_>>();
881
882 let resolved_lines = Arc::new(Mutex::new(Vec::<u32>::new()));
883
884 let fs = project::FakeFs::new(cx.executor());
885 fs.insert_tree(path!("/dir"), serde_json::json!({ "main.ts": lines }))
886 .await;
887
888 let project = project::Project::test(fs, [path!("/dir").as_ref()], cx).await;
889 let (multi_workspace, cx) = cx.add_window_view(|window, cx| {
890 workspace::MultiWorkspace::test_new(project.clone(), window, cx)
891 });
892 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
893
894 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
895 language_registry.add(Arc::new(language::Language::new(
896 language::LanguageConfig {
897 name: "TypeScript".into(),
898 matcher: language::LanguageMatcher {
899 path_suffixes: vec!["ts".to_string()],
900 ..language::LanguageMatcher::default()
901 },
902 ..language::LanguageConfig::default()
903 },
904 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
905 )));
906
907 let mut fake_servers = language_registry.register_fake_lsp(
908 "TypeScript",
909 language::FakeLspAdapter {
910 capabilities: lsp::ServerCapabilities {
911 code_lens_provider: Some(lsp::CodeLensOptions {
912 resolve_provider: Some(true),
913 }),
914 ..lsp::ServerCapabilities::default()
915 },
916 ..language::FakeLspAdapter::default()
917 },
918 );
919
920 let editor = workspace
921 .update_in(cx, |workspace, window, cx| {
922 workspace.open_abs_path(
923 std::path::PathBuf::from(path!("/dir/main.ts")),
924 workspace::OpenOptions::default(),
925 window,
926 cx,
927 )
928 })
929 .await
930 .unwrap()
931 .downcast::<Editor>()
932 .unwrap();
933 let fake_server = fake_servers.next().await.unwrap();
934
935 let lens_lines_for_handler = lens_lines.clone();
936 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _| {
937 let lens_lines = lens_lines_for_handler.clone();
938 async move {
939 Ok(Some(
940 lens_lines
941 .iter()
942 .map(|&line| lsp::CodeLens {
943 range: lsp::Range::new(
944 lsp::Position::new(line, 0),
945 lsp::Position::new(line, 10),
946 ),
947 command: None,
948 data: Some(serde_json::json!({ "line": line })),
949 })
950 .collect(),
951 ))
952 }
953 });
954
955 {
956 let resolved_lines = resolved_lines.clone();
957 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
958 move |lens, _| {
959 let resolved_lines = resolved_lines.clone();
960 async move {
961 let line = lens
962 .data
963 .as_ref()
964 .and_then(|d| d.get("line"))
965 .and_then(|v| v.as_u64())
966 .unwrap() as u32;
967 resolved_lines.lock().unwrap().push(line);
968 Ok(lsp::CodeLens {
969 command: Some(lsp::Command {
970 title: format!("{line} references"),
971 command: format!("show_refs_{line}"),
972 arguments: None,
973 }),
974 ..lens
975 })
976 }
977 },
978 );
979 }
980
981 cx.executor().advance_clock(Duration::from_millis(500));
982 cx.run_until_parked();
983
984 let initial_resolved = resolved_lines
985 .lock()
986 .unwrap()
987 .drain(..)
988 .collect::<HashSet<_>>();
989 assert_eq!(
990 initial_resolved,
991 HashSet::from_iter([0, 10, 20, 30, 40]),
992 "Only lenses visible at the top should be resolved"
993 );
994
995 editor.update_in(cx, |editor, window, cx| {
996 editor.move_to_end(&crate::actions::MoveToEnd, window, cx);
997 });
998 cx.executor().advance_clock(Duration::from_millis(500));
999 cx.run_until_parked();
1000
1001 let after_scroll_resolved = resolved_lines
1002 .lock()
1003 .unwrap()
1004 .drain(..)
1005 .collect::<HashSet<_>>();
1006 assert_eq!(
1007 after_scroll_resolved,
1008 HashSet::from_iter([60, 70, 80, 90]),
1009 "Only newly visible lenses at the bottom should be resolved, not middle ones"
1010 );
1011 }
1012}