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, RenderBlock},
  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 CodeLensBlock {
  35    block_id: CustomBlockId,
  36    anchor: Anchor,
  37    line: CodeLensLine,
  38}
  39
  40pub(super) struct CodeLensState {
  41    pub(super) blocks: HashMap<BufferId, Vec<CodeLensBlock>>,
  42    actions: HashMap<BufferId, Vec<CodeAction>>,
  43    resolve_task: Task<()>,
  44}
  45
  46impl Default for CodeLensState {
  47    fn default() -> Self {
  48        Self {
  49            blocks: HashMap::default(),
  50            actions: HashMap::default(),
  51            resolve_task: Task::ready(()),
  52        }
  53    }
  54}
  55
  56pub(super) fn try_handle_client_command(
  57    action: &CodeAction,
  58    editor: &mut Editor,
  59    workspace: &gpui::Entity<workspace::Workspace>,
  60    window: &mut Window,
  61    cx: &mut Context<Editor>,
  62) -> bool {
  63    let Some(command) = action.lsp_action.command() else {
  64        return false;
  65    };
  66
  67    let arguments = command.arguments.as_deref().unwrap_or_default();
  68    let project = workspace.read(cx).project().clone();
  69    let client_command = project
  70        .read(cx)
  71        .lsp_store()
  72        .read(cx)
  73        .language_server_adapter_for_id(action.server_id)
  74        .and_then(|adapter| adapter.adapter.client_command(&command.command, arguments))
  75        .or_else(|| match command.command.as_str() {
  76            "editor.action.showReferences"
  77            | "editor.action.goToLocations"
  78            | "editor.action.peekLocations" => Some(ClientCommand::ShowLocations),
  79            _ => None,
  80        });
  81
  82    match client_command {
  83        Some(ClientCommand::ScheduleTask(task_template)) => {
  84            schedule_task(task_template, action, editor, workspace, window, cx)
  85        }
  86        Some(ClientCommand::ShowLocations) => {
  87            try_show_references(arguments, action, editor, window, cx)
  88        }
  89        None => false,
  90    }
  91}
  92
  93fn schedule_task(
  94    task_template: task::TaskTemplate,
  95    action: &CodeAction,
  96    editor: &Editor,
  97    workspace: &gpui::Entity<workspace::Workspace>,
  98    window: &mut Window,
  99    cx: &mut Context<Editor>,
 100) -> bool {
 101    let task_context = TaskContext {
 102        cwd: task_template.cwd.as_ref().map(std::path::PathBuf::from),
 103        ..TaskContext::default()
 104    };
 105    let language_name = editor
 106        .buffer()
 107        .read(cx)
 108        .buffer(action.range.start.buffer_id)
 109        .and_then(|buffer| buffer.read(cx).language())
 110        .map(|language| language.name());
 111    let task_source_kind = match language_name {
 112        Some(language_name) => TaskSourceKind::Lsp {
 113            server: action.server_id,
 114            language_name: SharedString::from(language_name),
 115        },
 116        None => TaskSourceKind::AbsPath {
 117            id_base: "code-lens".into(),
 118            abs_path: task_template
 119                .cwd
 120                .as_ref()
 121                .map(std::path::PathBuf::from)
 122                .unwrap_or_default(),
 123        },
 124    };
 125
 126    workspace.update(cx, |workspace, cx| {
 127        workspace.schedule_task(
 128            task_source_kind,
 129            &task_template,
 130            &task_context,
 131            false,
 132            window,
 133            cx,
 134        );
 135    });
 136    true
 137}
 138
 139fn try_show_references(
 140    arguments: &[serde_json::Value],
 141    action: &CodeAction,
 142    editor: &mut Editor,
 143    window: &mut Window,
 144    cx: &mut Context<Editor>,
 145) -> bool {
 146    if arguments.len() < 3 {
 147        return false;
 148    }
 149    let Ok(locations) = serde_json::from_value::<Vec<lsp::Location>>(arguments[2].clone()) else {
 150        return false;
 151    };
 152    if locations.is_empty() {
 153        return false;
 154    }
 155
 156    let server_id = action.server_id;
 157    let nav_entry = editor.navigation_entry(editor.selections.newest_anchor().head(), cx);
 158    let links = locations
 159        .into_iter()
 160        .map(|location| HoverLink::InlayHint(location, server_id))
 161        .collect();
 162    editor
 163        .navigate_to_hover_links(None, links, nav_entry, false, window, cx)
 164        .detach_and_log_err(cx);
 165
 166    true
 167}
 168
 169impl Editor {
 170    pub(super) fn refresh_code_lenses(
 171        &mut self,
 172        for_buffer: Option<BufferId>,
 173        _window: &Window,
 174        cx: &mut Context<Self>,
 175    ) {
 176        if !self.lsp_data_enabled() || self.code_lens.is_none() {
 177            return;
 178        }
 179        let Some(project) = self.project.clone() else {
 180            return;
 181        };
 182
 183        let buffers_to_query = self
 184            .visible_buffers(cx)
 185            .into_iter()
 186            .filter(|buffer| self.is_lsp_relevant(buffer.read(cx).file(), cx))
 187            .chain(for_buffer.and_then(|buffer_id| self.buffer.read(cx).buffer(buffer_id)))
 188            .filter(|editor_buffer| {
 189                let editor_buffer_id = editor_buffer.read(cx).remote_id();
 190                for_buffer.is_none_or(|buffer_id| buffer_id == editor_buffer_id)
 191                    && self.registered_buffers.contains_key(&editor_buffer_id)
 192            })
 193            .unique_by(|buffer| buffer.read(cx).remote_id())
 194            .collect::<Vec<_>>();
 195
 196        if buffers_to_query.is_empty() {
 197            return;
 198        }
 199
 200        let project = project.downgrade();
 201        self.refresh_code_lens_task = cx.spawn(async move |editor, cx| {
 202            cx.background_executor()
 203                .timer(LSP_REQUEST_DEBOUNCE_TIMEOUT)
 204                .await;
 205
 206            let Some(tasks) = project
 207                .update(cx, |project, cx| {
 208                    project.lsp_store().update(cx, |lsp_store, cx| {
 209                        buffers_to_query
 210                            .into_iter()
 211                            .map(|buffer| {
 212                                let buffer_id = buffer.read(cx).remote_id();
 213                                let task = lsp_store.code_lens_actions(&buffer, cx);
 214                                async move { (buffer_id, task.await) }
 215                            })
 216                            .collect::<Vec<_>>()
 217                    })
 218                })
 219                .ok()
 220            else {
 221                return;
 222            };
 223
 224            let results = join_all(tasks).await;
 225            if results.is_empty() {
 226                return;
 227            }
 228
 229            editor
 230                .update(cx, |editor, cx| {
 231                    let snapshot = editor.buffer().read(cx).snapshot(cx);
 232                    for (buffer_id, result) in results {
 233                        let actions = match result {
 234                            Ok(Some(actions)) => actions,
 235                            Ok(None) => continue,
 236                            Err(e) => {
 237                                log::error!(
 238                                    "Failed to fetch code lenses for buffer {buffer_id:?}: {e:#}"
 239                                );
 240                                continue;
 241                            }
 242                        };
 243                        editor.apply_lens_actions_for_buffer(buffer_id, actions, &snapshot, cx);
 244                    }
 245                    editor.resolve_visible_code_lenses(cx);
 246                })
 247                .ok();
 248        });
 249    }
 250
 251    /// Reconciles the set of blocks for `buffer_id` with `actions`. For each
 252    /// existing block at row `R`:
 253    /// - if the new fetch has no lens at `R` → remove the block (the lens is
 254    ///   gone, e.g. the function was deleted);
 255    /// - if the new fetch has a titled lens at `R` whose rendered text
 256    ///   differs from the block's current line → swap the renderer in place
 257    ///   via [`Editor::replace_blocks`];
 258    /// - if the new fetch has a titled lens at `R` with the same rendered
 259    ///   text → keep the block as-is;
 260    /// - if the new fetch has a lens at `R` but no `command` yet (the server
 261    ///   sent a shallow response that needs a separate `resolve`) → keep the
 262    ///   block as-is. The previously rendered (resolved) content stays on
 263    ///   screen until the next viewport-driven `resolve` produces a new
 264    ///   title; only then does the comparison-and-replace happen. This is
 265    ///   what keeps the post-edit screen from flickering for shallow servers
 266    ///   like `rust-analyzer`.
 267    ///
 268    /// Rows present in the new fetch with a title but no existing block get
 269    /// a fresh block inserted.
 270    fn apply_lens_actions_for_buffer(
 271        &mut self,
 272        buffer_id: BufferId,
 273        actions: Vec<CodeAction>,
 274        snapshot: &MultiBufferSnapshot,
 275        cx: &mut Context<Self>,
 276    ) {
 277        let mut rows_with_any_lens = HashSet::default();
 278        let mut titled_lenses = Vec::new();
 279        for action in &actions {
 280            let Some(position) = snapshot.anchor_in_excerpt(action.range.start) else {
 281                continue;
 282            };
 283
 284            rows_with_any_lens.insert(MultiBufferRow(position.to_point(snapshot).row));
 285            if let project::LspAction::CodeLens(lens) = &action.lsp_action {
 286                if let Some(title) = lens
 287                    .command
 288                    .as_ref()
 289                    .map(|cmd| SharedString::from(&cmd.title))
 290                {
 291                    titled_lenses.push((
 292                        position,
 293                        CodeLensItem {
 294                            title,
 295                            action: action.clone(),
 296                        },
 297                    ));
 298                }
 299            }
 300        }
 301
 302        let mut new_lines_by_row = group_lenses_by_row(titled_lenses, snapshot)
 303            .map(|line| (MultiBufferRow(line.position.to_point(snapshot).row), line))
 304            .collect::<HashMap<_, _>>();
 305
 306        let editor_handle = cx.entity().downgrade();
 307        let code_lens = self.code_lens.get_or_insert_with(CodeLensState::default);
 308        let old_blocks = code_lens.blocks.remove(&buffer_id).unwrap_or_default();
 309
 310        let mut kept_blocks = Vec::new();
 311        let mut renderers_to_replace = HashMap::default();
 312        let mut blocks_to_remove = HashSet::default();
 313        let mut covered_rows = HashSet::default();
 314
 315        for old in old_blocks {
 316            let row = MultiBufferRow(old.anchor.to_point(snapshot).row);
 317            if !rows_with_any_lens.contains(&row) {
 318                blocks_to_remove.insert(old.block_id);
 319                continue;
 320            }
 321            covered_rows.insert(row);
 322            let Some(new_line) = new_lines_by_row.remove(&row) else {
 323                kept_blocks.push(old);
 324                continue;
 325            };
 326            if rendered_text_matches(&old.line, &new_line) {
 327                kept_blocks.push(old);
 328            } else {
 329                let mut updated = old;
 330                updated.line = new_line.clone();
 331                renderers_to_replace.insert(
 332                    updated.block_id,
 333                    build_code_lens_renderer(new_line, editor_handle.clone()),
 334                );
 335                kept_blocks.push(updated);
 336            }
 337        }
 338
 339        let mut to_insert = Vec::new();
 340        for (row, new_line) in new_lines_by_row {
 341            if covered_rows.contains(&row) {
 342                continue;
 343            }
 344            let anchor = new_line.position;
 345            let props = BlockProperties {
 346                placement: BlockPlacement::Above(anchor),
 347                height: Some(1),
 348                style: BlockStyle::Flex,
 349                render: build_code_lens_renderer(new_line.clone(), editor_handle.clone()),
 350                priority: 0,
 351            };
 352            to_insert.push((props, anchor, new_line));
 353        }
 354
 355        if !blocks_to_remove.is_empty() {
 356            self.remove_blocks(blocks_to_remove, None, cx);
 357        }
 358        if !renderers_to_replace.is_empty() {
 359            self.replace_blocks(renderers_to_replace, None, cx);
 360        }
 361        if !to_insert.is_empty() {
 362            let mut props = Vec::with_capacity(to_insert.len());
 363            let mut metadata = Vec::with_capacity(to_insert.len());
 364            for (p, anchor, line) in to_insert {
 365                props.push(p);
 366                metadata.push((anchor, line));
 367            }
 368            let block_ids = self.insert_blocks(props, None, cx);
 369            for (block_id, (anchor, line)) in block_ids.into_iter().zip(metadata) {
 370                kept_blocks.push(CodeLensBlock {
 371                    block_id,
 372                    anchor,
 373                    line,
 374                });
 375            }
 376        }
 377
 378        let code_lens = self.code_lens.get_or_insert_with(CodeLensState::default);
 379        if actions.is_empty() {
 380            code_lens.actions.remove(&buffer_id);
 381        } else {
 382            code_lens.actions.insert(buffer_id, actions);
 383        }
 384        if kept_blocks.is_empty() {
 385            code_lens.blocks.remove(&buffer_id);
 386        } else {
 387            code_lens.blocks.insert(buffer_id, kept_blocks);
 388        }
 389        cx.notify();
 390    }
 391
 392    pub fn supports_code_lens(&self, cx: &ui::App) -> bool {
 393        let Some(project) = self.project.as_ref() else {
 394            return false;
 395        };
 396        let lsp_store = project.read(cx).lsp_store().read(cx);
 397        lsp_store
 398            .lsp_server_capabilities
 399            .values()
 400            .any(|caps| caps.code_lens_provider.is_some())
 401    }
 402
 403    pub fn code_lens_enabled(&self) -> bool {
 404        self.code_lens.is_some()
 405    }
 406
 407    pub fn toggle_code_lens_action(
 408        &mut self,
 409        _: &ToggleCodeLens,
 410        window: &mut Window,
 411        cx: &mut Context<Self>,
 412    ) {
 413        let currently_enabled = self.code_lens.is_some();
 414        self.toggle_code_lens(!currently_enabled, window, cx);
 415    }
 416
 417    pub(super) fn toggle_code_lens(
 418        &mut self,
 419        enabled: bool,
 420        window: &mut Window,
 421        cx: &mut Context<Self>,
 422    ) {
 423        if enabled {
 424            self.code_lens.get_or_insert_with(CodeLensState::default);
 425            self.refresh_code_lenses(None, window, cx);
 426        } else {
 427            self.clear_code_lenses(cx);
 428        }
 429    }
 430
 431    pub(super) fn resolve_visible_code_lenses(&mut self, cx: &mut Context<Self>) {
 432        if !self.lsp_data_enabled() || self.code_lens.is_none() {
 433            return;
 434        }
 435        let Some(project) = self.project.clone() else {
 436            return;
 437        };
 438
 439        let resolve_tasks = self
 440            .visible_buffer_ranges(cx)
 441            .into_iter()
 442            .filter_map(|(snapshot, visible_range, _)| {
 443                let buffer_id = snapshot.remote_id();
 444                let buffer = self.buffer.read(cx).buffer(buffer_id)?;
 445                let visible_anchor_range = snapshot.anchor_before(visible_range.start)
 446                    ..snapshot.anchor_after(visible_range.end);
 447                let task = project.update(cx, |project, cx| {
 448                    project.lsp_store().update(cx, |lsp_store, cx| {
 449                        lsp_store.resolve_visible_code_lenses(&buffer, visible_anchor_range, cx)
 450                    })
 451                });
 452                Some((buffer_id, task))
 453            })
 454            .collect::<Vec<_>>();
 455        if resolve_tasks.is_empty() {
 456            return;
 457        }
 458
 459        let code_lens = self.code_lens.get_or_insert_with(CodeLensState::default);
 460        code_lens.resolve_task = cx.spawn(async move |editor, cx| {
 461            let resolved_per_buffer = join_all(
 462                resolve_tasks
 463                    .into_iter()
 464                    .map(|(buffer_id, task)| async move { (buffer_id, task.await) }),
 465            )
 466            .await;
 467            editor
 468                .update(cx, |editor, cx| {
 469                    let snapshot = editor.buffer().read(cx).snapshot(cx);
 470                    for (buffer_id, newly_resolved) in resolved_per_buffer {
 471                        if newly_resolved.is_empty() {
 472                            continue;
 473                        }
 474                        let Some(mut actions) = editor
 475                            .code_lens
 476                            .as_ref()
 477                            .and_then(|state| state.actions.get(&buffer_id))
 478                            .cloned()
 479                        else {
 480                            continue;
 481                        };
 482                        for resolved in newly_resolved {
 483                            if let Some(unresolved) = actions.iter_mut().find(|action| {
 484                                action.server_id == resolved.server_id
 485                                    && action.range == resolved.range
 486                            }) {
 487                                *unresolved = resolved;
 488                            }
 489                        }
 490                        editor.apply_lens_actions_for_buffer(buffer_id, actions, &snapshot, cx);
 491                    }
 492                })
 493                .ok();
 494        });
 495    }
 496
 497    pub(super) fn clear_code_lenses(&mut self, cx: &mut Context<Self>) {
 498        if let Some(code_lens) = self.code_lens.take() {
 499            let all_blocks = code_lens
 500                .blocks
 501                .into_values()
 502                .flatten()
 503                .map(|block| block.block_id)
 504                .collect::<HashSet<_>>();
 505            if !all_blocks.is_empty() {
 506                self.remove_blocks(all_blocks, None, cx);
 507            }
 508            cx.notify();
 509        }
 510        self.refresh_code_lens_task = Task::ready(());
 511    }
 512}
 513
 514/// Whether two lens lines would render the same on screen — same indent
 515/// and same titles in the same order. Used to skip recreating a renderer
 516/// (and thus a click handler) when nothing about the displayed line
 517/// changed; the captured [`CodeAction`] inside the existing renderer keeps
 518/// pointing at the right spot because its anchors track buffer edits.
 519fn rendered_text_matches(a: &CodeLensLine, b: &CodeLensLine) -> bool {
 520    a.indent_column == b.indent_column
 521        && a.items.len() == b.items.len()
 522        && a.items
 523            .iter()
 524            .zip(&b.items)
 525            .all(|(x, y)| x.title == y.title)
 526}
 527
 528fn group_lenses_by_row(
 529    lenses: Vec<(Anchor, CodeLensItem)>,
 530    snapshot: &MultiBufferSnapshot,
 531) -> impl Iterator<Item = CodeLensLine> {
 532    lenses
 533        .into_iter()
 534        .into_group_map_by(|(position, _)| {
 535            let row = position.to_point(snapshot).row;
 536            MultiBufferRow(row)
 537        })
 538        .into_iter()
 539        .sorted_by_key(|(row, _)| *row)
 540        .filter_map(|(row, entries)| {
 541            let position = entries.first()?.0;
 542            let items = entries.into_iter().map(|(_, item)| item).collect();
 543            let indent_column = snapshot.indent_size_for_line(row).len;
 544            Some(CodeLensLine {
 545                position,
 546                indent_column,
 547                items,
 548            })
 549        })
 550}
 551
 552fn build_code_lens_renderer(line: CodeLensLine, editor: WeakEntity<Editor>) -> RenderBlock {
 553    Arc::new(move |cx| {
 554        let mut children = Vec::with_capacity((2 * line.items.len()).saturating_sub(1));
 555        let text_style = &cx.editor_style.text;
 556        let font = text_style.font();
 557        let font_size = text_style.font_size.to_pixels(cx.window.rem_size()) * 0.9;
 558
 559        for (i, item) in line.items.iter().enumerate() {
 560            if i > 0 {
 561                children.push(
 562                    div()
 563                        .font(font.clone())
 564                        .text_size(font_size)
 565                        .text_color(cx.app.theme().colors().text_muted)
 566                        .child(" | ")
 567                        .into_any_element(),
 568                );
 569            }
 570
 571            let title = item.title.clone();
 572            let action = item.action.clone();
 573            let position = line.position;
 574            let editor_handle = editor.clone();
 575
 576            children.push(
 577                div()
 578                    .id(ElementId::from(i))
 579                    .font(font.clone())
 580                    .text_size(font_size)
 581                    .text_color(cx.app.theme().colors().text_muted)
 582                    .cursor_pointer()
 583                    .hover(|style| style.text_color(cx.app.theme().colors().text))
 584                    .child(title)
 585                    .on_mouse_down(MouseButton::Left, |_, _, cx| {
 586                        cx.stop_propagation();
 587                    })
 588                    .on_mouse_down(MouseButton::Right, |_, _, cx| {
 589                        cx.stop_propagation();
 590                    })
 591                    .on_click({
 592                        move |_event, window, cx| {
 593                            if let Some(editor) = editor_handle.upgrade() {
 594                                editor.update(cx, |editor, cx| {
 595                                    editor.change_selections(
 596                                        SelectionEffects::default(),
 597                                        window,
 598                                        cx,
 599                                        |s| {
 600                                            s.select_anchor_ranges([position..position]);
 601                                        },
 602                                    );
 603
 604                                    let action = action.clone();
 605                                    if let Some(workspace) = editor.workspace() {
 606                                        if try_handle_client_command(
 607                                            &action, editor, &workspace, window, cx,
 608                                        ) {
 609                                            return;
 610                                        }
 611
 612                                        let project = workspace.read(cx).project().clone();
 613                                        if let Some(buffer) = editor
 614                                            .buffer()
 615                                            .read(cx)
 616                                            .buffer(action.range.start.buffer_id)
 617                                        {
 618                                            project
 619                                                .update(cx, |project, cx| {
 620                                                    project
 621                                                        .apply_code_action(buffer, action, true, cx)
 622                                                })
 623                                                .detach_and_log_err(cx);
 624                                        }
 625                                    }
 626                                });
 627                            }
 628                        }
 629                    })
 630                    .into_any_element(),
 631            );
 632        }
 633
 634        div()
 635            .id(cx.block_id)
 636            .pl(cx.margins.gutter.full_width() + cx.em_width * (line.indent_column as f32 + 0.5))
 637            .h_full()
 638            .flex()
 639            .flex_row()
 640            .items_end()
 641            .children(children)
 642            .into_any_element()
 643    })
 644}
 645
 646#[cfg(test)]
 647mod tests {
 648    use std::{
 649        sync::{Arc, Mutex},
 650        time::Duration,
 651    };
 652
 653    use collections::HashSet;
 654    use futures::StreamExt;
 655    use gpui::TestAppContext;
 656    use settings::CodeLens;
 657    use util::path;
 658
 659    use crate::{
 660        Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT,
 661        editor_tests::{init_test, update_test_editor_settings},
 662        test::editor_lsp_test_context::EditorLspTestContext,
 663    };
 664
 665    #[gpui::test]
 666    async fn test_code_lens_blocks(cx: &mut TestAppContext) {
 667        init_test(cx, |_| {});
 668        update_test_editor_settings(cx, &|settings| {
 669            settings.code_lens = Some(CodeLens::On);
 670        });
 671
 672        let mut cx = EditorLspTestContext::new_typescript(
 673            lsp::ServerCapabilities {
 674                code_lens_provider: Some(lsp::CodeLensOptions {
 675                    resolve_provider: None,
 676                }),
 677                execute_command_provider: Some(lsp::ExecuteCommandOptions {
 678                    commands: vec!["lens_cmd".to_string()],
 679                    ..lsp::ExecuteCommandOptions::default()
 680                }),
 681                ..lsp::ServerCapabilities::default()
 682            },
 683            cx,
 684        )
 685        .await;
 686
 687        let mut code_lens_request =
 688            cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
 689                Ok(Some(vec![
 690                    lsp::CodeLens {
 691                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
 692                        command: Some(lsp::Command {
 693                            title: "2 references".to_owned(),
 694                            command: "lens_cmd".to_owned(),
 695                            arguments: None,
 696                        }),
 697                        data: None,
 698                    },
 699                    lsp::CodeLens {
 700                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 19)),
 701                        command: Some(lsp::Command {
 702                            title: "0 references".to_owned(),
 703                            command: "lens_cmd".to_owned(),
 704                            arguments: None,
 705                        }),
 706                        data: None,
 707                    },
 708                ]))
 709            });
 710
 711        cx.set_state("ˇfunction hello() {}\nfunction world() {}");
 712
 713        assert!(
 714            code_lens_request.next().await.is_some(),
 715            "should have received a code lens request"
 716        );
 717        cx.run_until_parked();
 718
 719        cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
 720            assert_eq!(
 721                editor.code_lens_enabled(),
 722                true,
 723                "code lens should be enabled"
 724            );
 725            let total_blocks: usize = editor
 726                .code_lens
 727                .as_ref()
 728                .map(|s| s.blocks.values().map(|v| v.len()).sum())
 729                .unwrap_or(0);
 730            assert_eq!(total_blocks, 2, "Should have inserted two code lens blocks");
 731        });
 732    }
 733
 734    #[gpui::test]
 735    async fn test_code_lens_blocks_kept_across_refresh(cx: &mut TestAppContext) {
 736        init_test(cx, |_| {});
 737        update_test_editor_settings(cx, &|settings| {
 738            settings.code_lens = Some(CodeLens::On);
 739        });
 740
 741        let mut cx = EditorLspTestContext::new_typescript(
 742            lsp::ServerCapabilities {
 743                code_lens_provider: Some(lsp::CodeLensOptions {
 744                    resolve_provider: None,
 745                }),
 746                execute_command_provider: Some(lsp::ExecuteCommandOptions {
 747                    commands: vec!["lens_cmd".to_string()],
 748                    ..lsp::ExecuteCommandOptions::default()
 749                }),
 750                ..lsp::ServerCapabilities::default()
 751            },
 752            cx,
 753        )
 754        .await;
 755
 756        let mut code_lens_request =
 757            cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
 758                Ok(Some(vec![lsp::CodeLens {
 759                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
 760                    command: Some(lsp::Command {
 761                        title: "1 reference".to_owned(),
 762                        command: "lens_cmd".to_owned(),
 763                        arguments: None,
 764                    }),
 765                    data: None,
 766                }]))
 767            });
 768
 769        cx.set_state("ˇfunction hello() {}\nfunction world() {}");
 770
 771        assert!(
 772            code_lens_request.next().await.is_some(),
 773            "should have received the initial code lens request"
 774        );
 775        cx.run_until_parked();
 776
 777        let initial_block_ids = cx.editor.read_with(&cx.cx.cx, |editor, _| {
 778            editor
 779                .code_lens
 780                .as_ref()
 781                .map(|s| {
 782                    s.blocks
 783                        .values()
 784                        .flatten()
 785                        .map(|b| b.block_id)
 786                        .collect::<HashSet<_>>()
 787                })
 788                .unwrap_or_default()
 789        });
 790        assert_eq!(
 791            initial_block_ids.len(),
 792            1,
 793            "Should have one initial code lens block"
 794        );
 795
 796        cx.update_editor(|editor, window, cx| {
 797            editor.move_to_end(&crate::actions::MoveToEnd, window, cx);
 798            editor.handle_input("\n// trailing comment", window, cx);
 799        });
 800        cx.executor()
 801            .advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT + Duration::from_millis(50));
 802        assert!(
 803            code_lens_request.next().await.is_some(),
 804            "should have received another code lens request after edit"
 805        );
 806        cx.run_until_parked();
 807
 808        let refreshed_block_ids = cx.editor.read_with(&cx.cx.cx, |editor, _| {
 809            editor
 810                .code_lens
 811                .as_ref()
 812                .map(|s| {
 813                    s.blocks
 814                        .values()
 815                        .flatten()
 816                        .map(|b| b.block_id)
 817                        .collect::<HashSet<_>>()
 818                })
 819                .unwrap_or_default()
 820        });
 821        assert_eq!(
 822            refreshed_block_ids, initial_block_ids,
 823            "Code lens blocks should be preserved across refreshes when their content is unchanged"
 824        );
 825    }
 826
 827    #[gpui::test]
 828    async fn test_code_lens_blocks_kept_when_only_resolve_fills_titles(cx: &mut TestAppContext) {
 829        init_test(cx, |_| {});
 830        update_test_editor_settings(cx, &|settings| {
 831            settings.code_lens = Some(CodeLens::On);
 832        });
 833
 834        let mut cx = EditorLspTestContext::new_typescript(
 835            lsp::ServerCapabilities {
 836                code_lens_provider: Some(lsp::CodeLensOptions {
 837                    resolve_provider: Some(true),
 838                }),
 839                ..lsp::ServerCapabilities::default()
 840            },
 841            cx,
 842        )
 843        .await;
 844
 845        // The LSP returns shallow code lenses on every fetch; only `resolve`
 846        // populates the command/title. This is the realistic flow with
 847        // servers like rust-analyzer and exercises the path where each
 848        // post-edit refresh comes back unresolved before the resolve catches
 849        // up.
 850        let mut code_lens_request =
 851            cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
 852                Ok(Some(vec![lsp::CodeLens {
 853                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
 854                    command: None,
 855                    data: Some(serde_json::json!({"id": "lens_1"})),
 856                }]))
 857            });
 858
 859        cx.lsp
 860            .set_request_handler::<lsp::request::CodeLensResolve, _, _>(|lens, _| async move {
 861                Ok(lsp::CodeLens {
 862                    command: Some(lsp::Command {
 863                        title: "1 reference".to_owned(),
 864                        command: "resolved_cmd".to_owned(),
 865                        arguments: None,
 866                    }),
 867                    ..lens
 868                })
 869            });
 870
 871        cx.set_state("ˇfunction hello() {}\nfunction world() {}");
 872
 873        assert!(
 874            code_lens_request.next().await.is_some(),
 875            "should have received the initial code lens request"
 876        );
 877        cx.run_until_parked();
 878
 879        let initial = cx.editor.read_with(&cx.cx.cx, |editor, _| {
 880            editor
 881                .code_lens
 882                .as_ref()
 883                .map(|s| {
 884                    s.blocks
 885                        .values()
 886                        .flatten()
 887                        .map(|b| b.block_id)
 888                        .collect::<HashSet<_>>()
 889                })
 890                .unwrap_or_default()
 891        });
 892        assert_eq!(
 893            initial.len(),
 894            1,
 895            "resolve should have inserted exactly one block from the shallow lens"
 896        );
 897
 898        for keystroke in [" ", "x", "y"] {
 899            cx.update_editor(|editor, window, cx| {
 900                editor.move_to_end(&crate::actions::MoveToEnd, window, cx);
 901                editor.handle_input(keystroke, window, cx);
 902            });
 903            cx.executor()
 904                .advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT + Duration::from_millis(50));
 905            assert!(
 906                code_lens_request.next().await.is_some(),
 907                "should have received another (shallow) code lens request after edit"
 908            );
 909            cx.run_until_parked();
 910
 911            let after = cx.editor.read_with(&cx.cx.cx, |editor, _| {
 912                editor
 913                    .code_lens
 914                    .as_ref()
 915                    .map(|s| {
 916                        s.blocks
 917                            .values()
 918                            .flatten()
 919                            .map(|b| b.block_id)
 920                            .collect::<HashSet<_>>()
 921                    })
 922                    .unwrap_or_default()
 923            });
 924            assert_eq!(
 925                after, initial,
 926                "Block IDs must survive the unresolved-fetch → resolve cycle without churn"
 927            );
 928        }
 929    }
 930
 931    #[gpui::test]
 932    async fn test_code_lens_disabled_by_default(cx: &mut TestAppContext) {
 933        init_test(cx, |_| {});
 934
 935        let mut cx = EditorLspTestContext::new_typescript(
 936            lsp::ServerCapabilities {
 937                code_lens_provider: Some(lsp::CodeLensOptions {
 938                    resolve_provider: None,
 939                }),
 940                execute_command_provider: Some(lsp::ExecuteCommandOptions {
 941                    commands: vec!["lens_cmd".to_string()],
 942                    ..lsp::ExecuteCommandOptions::default()
 943                }),
 944                ..lsp::ServerCapabilities::default()
 945            },
 946            cx,
 947        )
 948        .await;
 949
 950        cx.lsp
 951            .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
 952                panic!("Should not request code lenses when disabled");
 953            });
 954
 955        cx.set_state("ˇfunction hello() {}");
 956        cx.run_until_parked();
 957
 958        cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
 959            assert_eq!(
 960                editor.code_lens_enabled(),
 961                false,
 962                "code lens should not be enabled when setting is off"
 963            );
 964        });
 965    }
 966
 967    #[gpui::test]
 968    async fn test_code_lens_toggling(cx: &mut TestAppContext) {
 969        init_test(cx, |_| {});
 970        update_test_editor_settings(cx, &|settings| {
 971            settings.code_lens = Some(CodeLens::On);
 972        });
 973
 974        let mut cx = EditorLspTestContext::new_typescript(
 975            lsp::ServerCapabilities {
 976                code_lens_provider: Some(lsp::CodeLensOptions {
 977                    resolve_provider: None,
 978                }),
 979                execute_command_provider: Some(lsp::ExecuteCommandOptions {
 980                    commands: vec!["lens_cmd".to_string()],
 981                    ..lsp::ExecuteCommandOptions::default()
 982                }),
 983                ..lsp::ServerCapabilities::default()
 984            },
 985            cx,
 986        )
 987        .await;
 988
 989        let mut code_lens_request =
 990            cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
 991                Ok(Some(vec![lsp::CodeLens {
 992                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
 993                    command: Some(lsp::Command {
 994                        title: "1 reference".to_owned(),
 995                        command: "lens_cmd".to_owned(),
 996                        arguments: None,
 997                    }),
 998                    data: None,
 999                }]))
1000            });
1001
1002        cx.set_state("ˇfunction hello() {}");
1003
1004        assert!(
1005            code_lens_request.next().await.is_some(),
1006            "should have received a code lens request"
1007        );
1008        cx.run_until_parked();
1009
1010        cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
1011            assert_eq!(
1012                editor.code_lens_enabled(),
1013                true,
1014                "code lens should be enabled"
1015            );
1016            let total_blocks: usize = editor
1017                .code_lens
1018                .as_ref()
1019                .map(|s| s.blocks.values().map(|v| v.len()).sum())
1020                .unwrap_or(0);
1021            assert_eq!(total_blocks, 1, "Should have one code lens block");
1022        });
1023
1024        cx.update_editor(|editor, _window, cx| {
1025            editor.clear_code_lenses(cx);
1026        });
1027
1028        cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
1029            assert_eq!(
1030                editor.code_lens_enabled(),
1031                false,
1032                "code lens should be disabled after clearing"
1033            );
1034        });
1035    }
1036
1037    #[gpui::test]
1038    async fn test_code_lens_resolve(cx: &mut TestAppContext) {
1039        init_test(cx, |_| {});
1040        update_test_editor_settings(cx, &|settings| {
1041            settings.code_lens = Some(CodeLens::On);
1042        });
1043
1044        let mut cx = EditorLspTestContext::new_typescript(
1045            lsp::ServerCapabilities {
1046                code_lens_provider: Some(lsp::CodeLensOptions {
1047                    resolve_provider: Some(true),
1048                }),
1049                ..lsp::ServerCapabilities::default()
1050            },
1051            cx,
1052        )
1053        .await;
1054
1055        let mut code_lens_request =
1056            cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
1057                Ok(Some(vec![
1058                    lsp::CodeLens {
1059                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
1060                        command: None,
1061                        data: Some(serde_json::json!({"id": "lens_1"})),
1062                    },
1063                    lsp::CodeLens {
1064                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 19)),
1065                        command: None,
1066                        data: Some(serde_json::json!({"id": "lens_2"})),
1067                    },
1068                ]))
1069            });
1070
1071        cx.lsp
1072            .set_request_handler::<lsp::request::CodeLensResolve, _, _>(|lens, _| async move {
1073                let id = lens
1074                    .data
1075                    .as_ref()
1076                    .and_then(|d| d.get("id"))
1077                    .and_then(|v| v.as_str())
1078                    .unwrap_or("unknown");
1079                let title = match id {
1080                    "lens_1" => "3 references",
1081                    "lens_2" => "1 implementation",
1082                    _ => "unknown",
1083                };
1084                Ok(lsp::CodeLens {
1085                    command: Some(lsp::Command {
1086                        title: title.to_owned(),
1087                        command: format!("resolved_{id}"),
1088                        arguments: None,
1089                    }),
1090                    ..lens
1091                })
1092            });
1093
1094        cx.set_state("ˇfunction hello() {}\nfunction world() {}");
1095
1096        assert!(
1097            code_lens_request.next().await.is_some(),
1098            "should have received a code lens request"
1099        );
1100        cx.run_until_parked();
1101
1102        cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
1103            let total_blocks: usize = editor
1104                .code_lens
1105                .as_ref()
1106                .map(|s| s.blocks.values().map(|v| v.len()).sum())
1107                .unwrap_or(0);
1108            assert_eq!(
1109                total_blocks, 2,
1110                "Unresolved lenses should have been resolved and displayed"
1111            );
1112        });
1113    }
1114
1115    #[gpui::test]
1116    async fn test_code_lens_resolve_only_visible(cx: &mut TestAppContext) {
1117        init_test(cx, |_| {});
1118        update_test_editor_settings(cx, &|settings| {
1119            settings.code_lens = Some(CodeLens::On);
1120        });
1121
1122        let line_count: u32 = 100;
1123        let lens_every: u32 = 10;
1124        let lines = (0..line_count)
1125            .map(|i| format!("function func_{i}() {{}}"))
1126            .collect::<Vec<_>>()
1127            .join("\n");
1128
1129        let lens_lines = (0..line_count)
1130            .filter(|i| i % lens_every == 0)
1131            .collect::<Vec<_>>();
1132
1133        let resolved_lines = Arc::new(Mutex::new(Vec::<u32>::new()));
1134
1135        let fs = project::FakeFs::new(cx.executor());
1136        fs.insert_tree(path!("/dir"), serde_json::json!({ "main.ts": lines }))
1137            .await;
1138
1139        let project = project::Project::test(fs, [path!("/dir").as_ref()], cx).await;
1140        let (multi_workspace, cx) = cx.add_window_view(|window, cx| {
1141            workspace::MultiWorkspace::test_new(project.clone(), window, cx)
1142        });
1143        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1144
1145        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1146        language_registry.add(Arc::new(language::Language::new(
1147            language::LanguageConfig {
1148                name: "TypeScript".into(),
1149                matcher: language::LanguageMatcher {
1150                    path_suffixes: vec!["ts".to_string()],
1151                    ..language::LanguageMatcher::default()
1152                },
1153                ..language::LanguageConfig::default()
1154            },
1155            Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
1156        )));
1157
1158        let mut fake_servers = language_registry.register_fake_lsp(
1159            "TypeScript",
1160            language::FakeLspAdapter {
1161                capabilities: lsp::ServerCapabilities {
1162                    code_lens_provider: Some(lsp::CodeLensOptions {
1163                        resolve_provider: Some(true),
1164                    }),
1165                    ..lsp::ServerCapabilities::default()
1166                },
1167                ..language::FakeLspAdapter::default()
1168            },
1169        );
1170
1171        let editor = workspace
1172            .update_in(cx, |workspace, window, cx| {
1173                workspace.open_abs_path(
1174                    std::path::PathBuf::from(path!("/dir/main.ts")),
1175                    workspace::OpenOptions::default(),
1176                    window,
1177                    cx,
1178                )
1179            })
1180            .await
1181            .unwrap()
1182            .downcast::<Editor>()
1183            .unwrap();
1184        let fake_server = fake_servers.next().await.unwrap();
1185
1186        let lens_lines_for_handler = lens_lines.clone();
1187        fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _| {
1188            let lens_lines = lens_lines_for_handler.clone();
1189            async move {
1190                Ok(Some(
1191                    lens_lines
1192                        .iter()
1193                        .map(|&line| lsp::CodeLens {
1194                            range: lsp::Range::new(
1195                                lsp::Position::new(line, 0),
1196                                lsp::Position::new(line, 10),
1197                            ),
1198                            command: None,
1199                            data: Some(serde_json::json!({ "line": line })),
1200                        })
1201                        .collect(),
1202                ))
1203            }
1204        });
1205
1206        {
1207            let resolved_lines = resolved_lines.clone();
1208            fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
1209                move |lens, _| {
1210                    let resolved_lines = resolved_lines.clone();
1211                    async move {
1212                        let line = lens
1213                            .data
1214                            .as_ref()
1215                            .and_then(|d| d.get("line"))
1216                            .and_then(|v| v.as_u64())
1217                            .unwrap() as u32;
1218                        resolved_lines.lock().unwrap().push(line);
1219                        Ok(lsp::CodeLens {
1220                            command: Some(lsp::Command {
1221                                title: format!("{line} references"),
1222                                command: format!("show_refs_{line}"),
1223                                arguments: None,
1224                            }),
1225                            ..lens
1226                        })
1227                    }
1228                },
1229            );
1230        }
1231
1232        cx.executor().advance_clock(Duration::from_millis(500));
1233        cx.run_until_parked();
1234
1235        let initial_resolved = resolved_lines
1236            .lock()
1237            .unwrap()
1238            .drain(..)
1239            .collect::<HashSet<_>>();
1240        assert_eq!(
1241            initial_resolved,
1242            HashSet::from_iter([0, 10, 20, 30, 40]),
1243            "Only lenses visible at the top should be resolved"
1244        );
1245
1246        editor.update_in(cx, |editor, window, cx| {
1247            editor.move_to_end(&crate::actions::MoveToEnd, window, cx);
1248        });
1249        cx.executor().advance_clock(Duration::from_millis(500));
1250        cx.run_until_parked();
1251
1252        let after_scroll_resolved = resolved_lines
1253            .lock()
1254            .unwrap()
1255            .drain(..)
1256            .collect::<HashSet<_>>();
1257        assert_eq!(
1258            after_scroll_resolved,
1259            HashSet::from_iter([60, 70, 80, 90]),
1260            "Only newly visible lenses at the bottom should be resolved, not middle ones"
1261        );
1262    }
1263}