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