code_lens.rs

  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}