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, default_client_command};
8use multi_buffer::{Anchor, MultiBufferRow, MultiBufferSnapshot, ToPoint as _};
9use project::{CodeAction, TaskSourceKind};
10use task::TaskContext;
11use text::Point;
12
13use ui::{Context, Window, div, prelude::*};
14
15use crate::{
16 Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT, MultibufferSelectionMode, SelectionEffects,
17 actions::ToggleCodeLens,
18 display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
19};
20
21#[derive(Clone, Debug)]
22struct CodeLensLine {
23 position: Anchor,
24 items: Vec<CodeLensItem>,
25}
26
27#[derive(Clone, Debug)]
28struct CodeLensItem {
29 title: SharedString,
30 action: Option<CodeAction>,
31}
32
33#[derive(Default)]
34pub(super) struct CodeLensState {
35 pub(super) block_ids: HashMap<BufferId, Vec<CustomBlockId>>,
36}
37
38impl CodeLensState {
39 fn all_block_ids(&self) -> HashSet<CustomBlockId> {
40 self.block_ids.values().flatten().copied().collect()
41 }
42}
43
44fn group_lenses_by_row(
45 lenses: Vec<(Anchor, CodeLensItem)>,
46 snapshot: &MultiBufferSnapshot,
47) -> Vec<CodeLensLine> {
48 let mut grouped: HashMap<MultiBufferRow, (Anchor, Vec<CodeLensItem>)> = HashMap::default();
49
50 for (position, item) in lenses {
51 let row = position.to_point(snapshot).row;
52 let indent = snapshot.indent_size_for_line(MultiBufferRow(row)).len;
53 let indent_anchor = snapshot.anchor_before(Point::new(row, indent));
54 grouped
55 .entry(MultiBufferRow(row))
56 .or_insert_with(|| (indent_anchor, Vec::new()))
57 .1
58 .push(item);
59 }
60
61 let mut result: Vec<CodeLensLine> = grouped
62 .into_iter()
63 .map(|(_, (position, items))| CodeLensLine { position, items })
64 .collect();
65
66 result.sort_by_key(|lens| lens.position.to_point(snapshot).row);
67 result
68}
69
70fn render_code_lens_line(
71 line_number: usize,
72 lens: CodeLensLine,
73 editor: WeakEntity<Editor>,
74) -> impl Fn(&mut crate::display_map::BlockContext) -> gpui::AnyElement {
75 move |cx| {
76 let mut children = Vec::new();
77 let text_style = &cx.editor_style.text;
78 let font = text_style.font();
79 let font_size = text_style.font_size.to_pixels(cx.window.rem_size()) * 0.9;
80
81 for (i, item) in lens.items.iter().enumerate() {
82 if i > 0 {
83 children.push(
84 div()
85 .font(font.clone())
86 .text_size(font_size)
87 .text_color(cx.app.theme().colors().text_muted)
88 .child(" | ")
89 .into_any_element(),
90 );
91 }
92
93 let id = (line_number as u64) << 32 | (i as u64);
94 let editor_handle = editor.clone();
95 let element = div()
96 .id(ElementId::Integer(id))
97 .font(font.clone())
98 .text_size(font_size)
99 .text_color(cx.app.theme().colors().text_muted)
100 .child(item.title.clone())
101 .when_some(item.action.clone(), |lens_element, code_action| {
102 let position = lens.position;
103 lens_element
104 .cursor_pointer()
105 .hover(|style| style.text_color(cx.app.theme().colors().text))
106 .on_mouse_down(MouseButton::Left, |_, _, cx| {
107 cx.stop_propagation();
108 })
109 .on_mouse_down(MouseButton::Right, |_, _, cx| {
110 cx.stop_propagation();
111 })
112 .on_click({
113 move |_event, window, cx| {
114 if let Some(editor) = editor_handle.upgrade() {
115 editor.update(cx, |editor, cx| {
116 editor.change_selections(
117 SelectionEffects::default(),
118 window,
119 cx,
120 |s| {
121 s.select_anchor_ranges([position..position]);
122 },
123 );
124
125 if let Some(workspace) = editor.workspace() {
126 if try_handle_client_command(
127 &code_action,
128 editor,
129 &workspace,
130 window,
131 cx,
132 ) {
133 return;
134 }
135
136 let project = workspace.read(cx).project().clone();
137 let buffer = editor.buffer().clone();
138 if let Some(excerpt_buffer) =
139 buffer.read(cx).as_singleton()
140 {
141 project
142 .update(cx, |project, cx| {
143 project.apply_code_action(
144 excerpt_buffer.clone(),
145 code_action.clone(),
146 true,
147 cx,
148 )
149 })
150 .detach_and_log_err(cx);
151 }
152 }
153 });
154 }
155 }
156 })
157 });
158 children.push(element.into_any_element());
159 }
160
161 div()
162 .pl(cx.anchor_x)
163 .h_full()
164 .flex()
165 .flex_row()
166 .items_end()
167 .children(children)
168 .into_any_element()
169 }
170}
171
172pub(super) fn try_handle_client_command(
173 action: &CodeAction,
174 editor: &mut Editor,
175 workspace: &gpui::Entity<workspace::Workspace>,
176 window: &mut Window,
177 cx: &mut Context<Editor>,
178) -> bool {
179 let Some(command) = action.lsp_action.command() else {
180 return false;
181 };
182
183 let project = workspace.read(cx).project().clone();
184 let client_command = project
185 .read(cx)
186 .lsp_store()
187 .read(cx)
188 .language_server_adapter_for_id(action.server_id)
189 .and_then(|adapter| adapter.adapter.client_command(&command.command))
190 .or_else(|| default_client_command(&command.command));
191
192 match client_command {
193 Some(ClientCommand::ScheduleTask) => {
194 try_schedule_task(command, action, editor, workspace, window, cx)
195 }
196 Some(ClientCommand::ShowLocations) => {
197 let arguments = command.arguments.as_deref().unwrap_or_default();
198 try_show_references(arguments, action, workspace, window, cx)
199 }
200 None => false,
201 }
202}
203
204fn try_schedule_task(
205 command: &lsp::Command,
206 action: &CodeAction,
207 editor: &Editor,
208 workspace: &gpui::Entity<workspace::Workspace>,
209 window: &mut Window,
210 cx: &mut Context<Editor>,
211) -> bool {
212 let project = workspace.read(cx).project().clone();
213 let task_template = project
214 .read(cx)
215 .lsp_store()
216 .read(cx)
217 .language_server_adapter_for_id(action.server_id)
218 .and_then(|adapter| adapter.adapter.command_to_task(command));
219
220 let Some(task_template) = task_template else {
221 return false;
222 };
223
224 let task_context = TaskContext {
225 cwd: task_template.cwd.as_ref().map(std::path::PathBuf::from),
226 ..TaskContext::default()
227 };
228 let language_name = editor
229 .buffer()
230 .read(cx)
231 .as_singleton()
232 .and_then(|buffer| buffer.read(cx).language())
233 .map(|language| language.name());
234 let task_source_kind = match language_name {
235 Some(language_name) => TaskSourceKind::Lsp {
236 server: action.server_id,
237 language_name: SharedString::from(language_name),
238 },
239 None => TaskSourceKind::AbsPath {
240 id_base: "code-lens".into(),
241 abs_path: task_template
242 .cwd
243 .as_ref()
244 .map(std::path::PathBuf::from)
245 .unwrap_or_default(),
246 },
247 };
248
249 workspace.update(cx, |workspace, cx| {
250 workspace.schedule_task(
251 task_source_kind,
252 &task_template,
253 &task_context,
254 false,
255 window,
256 cx,
257 );
258 });
259 true
260}
261
262fn try_show_references(
263 arguments: &[serde_json::Value],
264 action: &CodeAction,
265 workspace: &gpui::Entity<workspace::Workspace>,
266 window: &mut Window,
267 cx: &mut Context<Editor>,
268) -> bool {
269 if arguments.len() < 3 {
270 return false;
271 }
272 let Ok(locations) = serde_json::from_value::<Vec<lsp::Location>>(arguments[2].clone()) else {
273 return false;
274 };
275 if locations.is_empty() {
276 return false;
277 }
278
279 let fallback_title = action.lsp_action.title().to_owned();
280 let server_id = action.server_id;
281 let project = workspace.read(cx).project().clone();
282 let workspace = workspace.clone();
283
284 cx.spawn_in(window, async move |_editor, cx| {
285 let mut buffer_locations: StdHashMap<gpui::Entity<language::Buffer>, Vec<Range<Point>>> =
286 StdHashMap::default();
287
288 for location in &locations {
289 let open_task = cx.update(|_, cx| {
290 project.update(cx, |project, cx| {
291 let uri: lsp::Uri = location.uri.clone();
292 project.open_local_buffer_via_lsp(uri, server_id, cx)
293 })
294 })?;
295 let buffer = open_task.await?;
296
297 let range = range_from_lsp(location.range);
298 buffer_locations.entry(buffer).or_default().push(range);
299 }
300
301 workspace.update_in(cx, |workspace, window, cx| {
302 let target = buffer_locations
303 .iter()
304 .flat_map(|(buffer, ranges)| iter::repeat(buffer.clone()).zip(ranges))
305 .map(|(buffer, range)| {
306 buffer
307 .read(cx)
308 .text_for_range(range.clone())
309 .collect::<String>()
310 })
311 .filter(|text| !text.contains('\n'))
312 .unique()
313 .take(3)
314 .join(", ");
315 let title = if target.is_empty() {
316 fallback_title
317 } else {
318 format!("References to {target}")
319 };
320 Editor::open_locations_in_multibuffer(
321 workspace,
322 buffer_locations,
323 title,
324 false,
325 true,
326 MultibufferSelectionMode::First,
327 window,
328 cx,
329 );
330 })?;
331 anyhow::Ok(())
332 })
333 .detach_and_log_err(cx);
334
335 true
336}
337
338fn range_from_lsp(range: lsp::Range) -> Range<Point> {
339 let start = Point::new(range.start.line, range.start.character);
340 let end = Point::new(range.end.line, range.end.character);
341 start..end
342}
343
344impl Editor {
345 pub(super) fn refresh_code_lenses(
346 &mut self,
347 for_buffer: Option<BufferId>,
348 _window: &Window,
349 cx: &mut Context<Self>,
350 ) {
351 if !self.lsp_data_enabled() || self.code_lens.is_none() {
352 return;
353 }
354 let Some(project) = self.project.clone() else {
355 return;
356 };
357
358 let buffers_to_query = self
359 .visible_buffers(cx)
360 .into_iter()
361 .filter(|buffer| self.is_lsp_relevant(buffer.read(cx).file(), cx))
362 .chain(for_buffer.and_then(|buffer_id| self.buffer.read(cx).buffer(buffer_id)))
363 .filter(|editor_buffer| {
364 let editor_buffer_id = editor_buffer.read(cx).remote_id();
365 for_buffer.is_none_or(|buffer_id| buffer_id == editor_buffer_id)
366 && self.registered_buffers.contains_key(&editor_buffer_id)
367 })
368 .unique_by(|buffer| buffer.read(cx).remote_id())
369 .collect::<Vec<_>>();
370
371 if buffers_to_query.is_empty() {
372 return;
373 }
374
375 let project = project.downgrade();
376 self.refresh_code_lens_task = cx.spawn(async move |editor, cx| {
377 cx.background_executor()
378 .timer(LSP_REQUEST_DEBOUNCE_TIMEOUT)
379 .await;
380
381 let Some(tasks) = project
382 .update(cx, |project, cx| {
383 project.lsp_store().update(cx, |lsp_store, cx| {
384 buffers_to_query
385 .into_iter()
386 .map(|buffer| {
387 let buffer_id = buffer.read(cx).remote_id();
388 let task = lsp_store.code_lens_actions(&buffer, cx);
389 async move { (buffer_id, task.await) }
390 })
391 .collect::<Vec<_>>()
392 })
393 })
394 .ok()
395 else {
396 return;
397 };
398
399 let results = join_all(tasks).await;
400 if results.is_empty() {
401 return;
402 }
403
404 let Ok(multi_buffer_snapshot) =
405 editor.update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx))
406 else {
407 return;
408 };
409
410 let mut new_lenses_per_buffer: HashMap<BufferId, Vec<CodeLensLine>> =
411 HashMap::default();
412
413 for (buffer_id, result) in results {
414 match result {
415 Ok(Some(actions)) => {
416 let individual_lenses: Vec<(Anchor, CodeLensItem)> = actions
417 .into_iter()
418 .filter_map(|action| {
419 let (title, is_actionable) = match &action.lsp_action {
420 project::LspAction::CodeLens(lens) => match &lens.command {
421 Some(cmd) => {
422 let actionable = !cmd.command.is_empty();
423 (SharedString::from(&cmd.title), actionable)
424 }
425 None => return None,
426 },
427 _ => return None,
428 };
429 let position =
430 multi_buffer_snapshot.anchor_in_excerpt(action.range.start)?;
431 let action = if is_actionable { Some(action) } else { None };
432 Some((position, CodeLensItem { title, action }))
433 })
434 .collect();
435
436 let grouped =
437 group_lenses_by_row(individual_lenses, &multi_buffer_snapshot);
438 new_lenses_per_buffer.insert(buffer_id, grouped);
439 }
440 Ok(None) => {}
441 Err(e) => {
442 log::error!("Failed to fetch code lenses for buffer {buffer_id:?}: {e:#}");
443 }
444 }
445 }
446
447 editor
448 .update(cx, |editor, cx| {
449 let code_lens = editor.code_lens.get_or_insert_with(CodeLensState::default);
450
451 let mut blocks_to_remove: HashSet<CustomBlockId> = HashSet::default();
452 for (buffer_id, _) in &new_lenses_per_buffer {
453 if let Some(old_ids) = code_lens.block_ids.remove(buffer_id) {
454 blocks_to_remove.extend(old_ids);
455 }
456 }
457
458 if !blocks_to_remove.is_empty() {
459 editor.remove_blocks(blocks_to_remove, None, cx);
460 }
461
462 let editor_handle = cx.entity().downgrade();
463
464 let mut all_new_blocks: Vec<(BufferId, Vec<BlockProperties<Anchor>>)> =
465 Vec::new();
466 for (buffer_id, lense_lines) in new_lenses_per_buffer {
467 if lense_lines.is_empty() {
468 continue;
469 }
470 let blocks: Vec<BlockProperties<Anchor>> = lense_lines
471 .into_iter()
472 .enumerate()
473 .map(|(line_number, lens_line)| {
474 let position = lens_line.position;
475 let render_fn = render_code_lens_line(
476 line_number,
477 lens_line,
478 editor_handle.clone(),
479 );
480 BlockProperties {
481 placement: BlockPlacement::Above(position),
482 height: Some(1),
483 style: BlockStyle::Flex,
484 render: Arc::new(render_fn),
485 priority: 0,
486 }
487 })
488 .collect();
489 all_new_blocks.push((buffer_id, blocks));
490 }
491
492 for (buffer_id, blocks) in all_new_blocks {
493 let block_ids = editor.insert_blocks(blocks, None, cx);
494 editor
495 .code_lens
496 .get_or_insert_with(CodeLensState::default)
497 .block_ids
498 .insert(buffer_id, block_ids);
499 }
500
501 cx.notify();
502 })
503 .ok();
504 });
505 }
506
507 pub fn supports_code_lens(&self, cx: &ui::App) -> bool {
508 let Some(project) = self.project.as_ref() else {
509 return false;
510 };
511 let lsp_store = project.read(cx).lsp_store().read(cx);
512 lsp_store
513 .lsp_server_capabilities
514 .values()
515 .any(|caps| caps.code_lens_provider.is_some())
516 }
517
518 pub fn code_lens_enabled(&self) -> bool {
519 self.code_lens.is_some()
520 }
521
522 pub fn toggle_code_lens_action(
523 &mut self,
524 _: &ToggleCodeLens,
525 window: &mut Window,
526 cx: &mut Context<Self>,
527 ) {
528 let currently_enabled = self.code_lens.is_some();
529 self.toggle_code_lens(!currently_enabled, window, cx);
530 }
531
532 pub(super) fn toggle_code_lens(
533 &mut self,
534 enabled: bool,
535 window: &mut Window,
536 cx: &mut Context<Self>,
537 ) {
538 if enabled {
539 self.code_lens.get_or_insert_with(CodeLensState::default);
540 self.refresh_code_lenses(None, window, cx);
541 } else {
542 self.clear_code_lenses(cx);
543 }
544 }
545
546 pub(super) fn clear_code_lenses(&mut self, cx: &mut Context<Self>) {
547 if let Some(code_lens) = self.code_lens.take() {
548 let all_blocks = code_lens.all_block_ids();
549 if !all_blocks.is_empty() {
550 self.remove_blocks(all_blocks, None, cx);
551 }
552 cx.notify();
553 }
554 self.refresh_code_lens_task = Task::ready(());
555 }
556}
557
558#[cfg(test)]
559mod tests {
560 use futures::StreamExt;
561 use gpui::TestAppContext;
562
563 use settings::CodeLens;
564
565 use crate::{
566 editor_tests::{init_test, update_test_editor_settings},
567 test::editor_lsp_test_context::EditorLspTestContext,
568 };
569
570 #[gpui::test]
571 async fn test_code_lens_blocks(cx: &mut TestAppContext) {
572 init_test(cx, |_| {});
573 update_test_editor_settings(cx, &|settings| {
574 settings.code_lens = Some(CodeLens::On);
575 });
576
577 let mut cx = EditorLspTestContext::new_typescript(
578 lsp::ServerCapabilities {
579 code_lens_provider: Some(lsp::CodeLensOptions {
580 resolve_provider: None,
581 }),
582 execute_command_provider: Some(lsp::ExecuteCommandOptions {
583 commands: vec!["lens_cmd".to_string()],
584 ..lsp::ExecuteCommandOptions::default()
585 }),
586 ..lsp::ServerCapabilities::default()
587 },
588 cx,
589 )
590 .await;
591
592 let mut code_lens_request =
593 cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
594 Ok(Some(vec![
595 lsp::CodeLens {
596 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
597 command: Some(lsp::Command {
598 title: "2 references".to_owned(),
599 command: "lens_cmd".to_owned(),
600 arguments: None,
601 }),
602 data: None,
603 },
604 lsp::CodeLens {
605 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 19)),
606 command: Some(lsp::Command {
607 title: "0 references".to_owned(),
608 command: "lens_cmd".to_owned(),
609 arguments: None,
610 }),
611 data: None,
612 },
613 ]))
614 });
615
616 cx.set_state("ˇfunction hello() {}\nfunction world() {}");
617
618 assert!(
619 code_lens_request.next().await.is_some(),
620 "should have received a code lens request"
621 );
622 cx.run_until_parked();
623
624 cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
625 assert_eq!(
626 editor.code_lens_enabled(),
627 true,
628 "code lens should be enabled"
629 );
630 let total_blocks: usize = editor
631 .code_lens
632 .as_ref()
633 .map(|s| s.block_ids.values().map(|v| v.len()).sum())
634 .unwrap_or(0);
635 assert_eq!(total_blocks, 2, "Should have inserted two code lens blocks");
636 });
637 }
638
639 #[gpui::test]
640 async fn test_code_lens_disabled_by_default(cx: &mut TestAppContext) {
641 init_test(cx, |_| {});
642
643 let mut cx = EditorLspTestContext::new_typescript(
644 lsp::ServerCapabilities {
645 code_lens_provider: Some(lsp::CodeLensOptions {
646 resolve_provider: None,
647 }),
648 execute_command_provider: Some(lsp::ExecuteCommandOptions {
649 commands: vec!["lens_cmd".to_string()],
650 ..lsp::ExecuteCommandOptions::default()
651 }),
652 ..lsp::ServerCapabilities::default()
653 },
654 cx,
655 )
656 .await;
657
658 cx.lsp
659 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
660 panic!("Should not request code lenses when disabled");
661 });
662
663 cx.set_state("ˇfunction hello() {}");
664 cx.run_until_parked();
665
666 cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
667 assert_eq!(
668 editor.code_lens_enabled(),
669 false,
670 "code lens should not be enabled when setting is off"
671 );
672 });
673 }
674
675 #[gpui::test]
676 async fn test_code_lens_toggling(cx: &mut TestAppContext) {
677 init_test(cx, |_| {});
678 update_test_editor_settings(cx, &|settings| {
679 settings.code_lens = Some(CodeLens::On);
680 });
681
682 let mut cx = EditorLspTestContext::new_typescript(
683 lsp::ServerCapabilities {
684 code_lens_provider: Some(lsp::CodeLensOptions {
685 resolve_provider: None,
686 }),
687 execute_command_provider: Some(lsp::ExecuteCommandOptions {
688 commands: vec!["lens_cmd".to_string()],
689 ..lsp::ExecuteCommandOptions::default()
690 }),
691 ..lsp::ServerCapabilities::default()
692 },
693 cx,
694 )
695 .await;
696
697 let mut code_lens_request =
698 cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
699 Ok(Some(vec![lsp::CodeLens {
700 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
701 command: Some(lsp::Command {
702 title: "1 reference".to_owned(),
703 command: "lens_cmd".to_owned(),
704 arguments: None,
705 }),
706 data: None,
707 }]))
708 });
709
710 cx.set_state("ˇfunction hello() {}");
711
712 assert!(
713 code_lens_request.next().await.is_some(),
714 "should have received a code lens request"
715 );
716 cx.run_until_parked();
717
718 cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
719 assert_eq!(
720 editor.code_lens_enabled(),
721 true,
722 "code lens should be enabled"
723 );
724 let total_blocks: usize = editor
725 .code_lens
726 .as_ref()
727 .map(|s| s.block_ids.values().map(|v| v.len()).sum())
728 .unwrap_or(0);
729 assert_eq!(total_blocks, 1, "Should have one code lens block");
730 });
731
732 cx.update_editor(|editor, _window, cx| {
733 editor.clear_code_lenses(cx);
734 });
735
736 cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
737 assert_eq!(
738 editor.code_lens_enabled(),
739 false,
740 "code lens should be disabled after clearing"
741 );
742 });
743 }
744
745 #[gpui::test]
746 async fn test_code_lens_resolve(cx: &mut TestAppContext) {
747 init_test(cx, |_| {});
748 update_test_editor_settings(cx, &|settings| {
749 settings.code_lens = Some(CodeLens::On);
750 });
751
752 let mut cx = EditorLspTestContext::new_typescript(
753 lsp::ServerCapabilities {
754 code_lens_provider: Some(lsp::CodeLensOptions {
755 resolve_provider: Some(true),
756 }),
757 ..lsp::ServerCapabilities::default()
758 },
759 cx,
760 )
761 .await;
762
763 let mut code_lens_request =
764 cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
765 Ok(Some(vec![
766 lsp::CodeLens {
767 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
768 command: None,
769 data: Some(serde_json::json!({"id": "lens_1"})),
770 },
771 lsp::CodeLens {
772 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 19)),
773 command: None,
774 data: Some(serde_json::json!({"id": "lens_2"})),
775 },
776 ]))
777 });
778
779 cx.lsp
780 .set_request_handler::<lsp::request::CodeLensResolve, _, _>(|lens, _| async move {
781 let id = lens
782 .data
783 .as_ref()
784 .and_then(|d| d.get("id"))
785 .and_then(|v| v.as_str())
786 .unwrap_or("unknown");
787 let title = match id {
788 "lens_1" => "3 references",
789 "lens_2" => "1 implementation",
790 _ => "unknown",
791 };
792 Ok(lsp::CodeLens {
793 command: Some(lsp::Command {
794 title: title.to_owned(),
795 command: format!("resolved_{id}"),
796 arguments: None,
797 }),
798 ..lens
799 })
800 });
801
802 cx.set_state("ˇfunction hello() {}\nfunction world() {}");
803
804 assert!(
805 code_lens_request.next().await.is_some(),
806 "should have received a code lens request"
807 );
808 cx.run_until_parked();
809
810 cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
811 let total_blocks: usize = editor
812 .code_lens
813 .as_ref()
814 .map(|s| s.block_ids.values().map(|v| v.len()).sum())
815 .unwrap_or(0);
816 assert_eq!(
817 total_blocks, 2,
818 "Unresolved lenses should have been resolved and displayed"
819 );
820 });
821 }
822}