code_lens.rs

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