code_lens.rs

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