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