Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:32:10.807Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

code_lens.rs

1877 lines · 69.6 KB · rust
1use std::sync::Arc;
2
3use collections::{HashMap, HashSet};
4use futures::{StreamExt as _, future::join_all, stream::FuturesUnordered};
5use gpui::{MouseButton, SharedString, Task, TaskExt, WeakEntity};
6use itertools::Itertools;
7use language::{BufferId, ClientCommand};
8use multi_buffer::{Anchor, BufferOffset, MultiBufferRow, MultiBufferSnapshot, ToPoint as _};
9use project::{CodeAction, TaskSourceKind, lsp_store::code_lens::CodeLensActions};
10use task::TaskContext;
11use text::ToOffset as _;
12
13use ui::{Context, Window, div, prelude::*};
14
15use crate::{
16    Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT, SelectionEffects,
17    actions::ToggleCodeLens,
18    display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock},
19    hover_links::HoverLink,
20    runnables::RunnableTaskStatus,
21};
22
23static EMPTY_LENS_FALLBACK_TITLE: SharedString = SharedString::new_static("0 references");
24const CODE_LENS_SEPARATOR: &str = " | ";
25
26#[derive(Clone, Debug)]
27struct CodeLensLine {
28    position: Anchor,
29    indent_column: u32,
30    items: Vec<CodeLensItem>,
31}
32
33#[derive(Clone, Debug)]
34struct CodeLensItem {
35    title: Option<SharedString>,
36    action: CodeAction,
37}
38
39pub(super) struct CodeLensBlock {
40    block_id: CustomBlockId,
41    anchor: Anchor,
42    line: CodeLensLine,
43}
44
45pub(super) struct CodeLensState {
46    pub(super) blocks: HashMap<BufferId, Vec<CodeLensBlock>>,
47    actions: HashMap<BufferId, CodeLensActions>,
48    resolve_task: Task<()>,
49}
50
51impl Default for CodeLensState {
52    fn default() -> Self {
53        Self {
54            blocks: HashMap::default(),
55            actions: HashMap::default(),
56            resolve_task: Task::ready(()),
57        }
58    }
59}
60
61pub(super) fn try_handle_client_command(
62    action: &CodeAction,
63    editor: &mut Editor,
64    workspace: &gpui::Entity<workspace::Workspace>,
65    window: &mut Window,
66    cx: &mut Context<Editor>,
67) -> bool {
68    let Some(command) = action.lsp_action.command() else {
69        return false;
70    };
71
72    let arguments = command.arguments.as_deref().unwrap_or_default();
73    let project = workspace.read(cx).project().clone();
74    let client_command = project
75        .read(cx)
76        .lsp_store()
77        .read(cx)
78        .language_server_adapter_for_id(action.server_id)
79        .and_then(|adapter| adapter.adapter.client_command(&command.command, arguments))
80        .or_else(|| match command.command.as_str() {
81            "editor.action.showReferences"
82            | "editor.action.goToLocations"
83            | "editor.action.peekLocations" => Some(ClientCommand::ShowLocations),
84            _ => None,
85        });
86
87    match client_command {
88        Some(ClientCommand::ScheduleTask(task_template)) => {
89            schedule_task(task_template, action, editor, workspace, window, cx)
90        }
91        Some(ClientCommand::ShowLocations) => {
92            try_show_references(arguments, action, editor, window, cx)
93        }
94        None => false,
95    }
96}
97
98fn schedule_task(
99    task_template: task::TaskTemplate,
100    action: &CodeAction,
101    editor: &mut Editor,
102    workspace: &gpui::Entity<workspace::Workspace>,
103    window: &mut Window,
104    cx: &mut Context<Editor>,
105) -> bool {
106    let task_context = TaskContext {
107        cwd: task_template.cwd.as_ref().map(std::path::PathBuf::from),
108        ..TaskContext::default()
109    };
110    let language_name = editor
111        .buffer()
112        .read(cx)
113        .buffer(action.range.start.buffer_id)
114        .and_then(|buffer| buffer.read(cx).language())
115        .map(|language| language.name());
116    let task_source_kind = match language_name {
117        Some(language_name) => TaskSourceKind::Lsp {
118            server: action.server_id,
119            language_name: SharedString::from(language_name),
120        },
121        None => TaskSourceKind::AbsPath {
122            id_base: "code-lens".into(),
123            abs_path: task_template
124                .cwd
125                .as_ref()
126                .map(std::path::PathBuf::from)
127                .unwrap_or_default(),
128        },
129    };
130
131    let Some(resolved_task) =
132        task_template.resolve_task(&task_source_kind.to_id_base(), &task_context)
133    else {
134        return true;
135    };
136    let runnable_task_key = editor
137        .buffer()
138        .read(cx)
139        .buffer(action.range.start.buffer_id)
140        .and_then(|buffer| {
141            let buffer_snapshot = buffer.read(cx).snapshot();
142            let buffer_id = buffer_snapshot.remote_id();
143            let offset = BufferOffset(action.range.start.to_offset(&buffer_snapshot));
144            editor.runnable_task_key_for_offset(buffer_id, offset)
145        });
146    if let Some((buffer_id, buffer_row)) = runnable_task_key {
147        editor.set_runnable_task_status(buffer_id, buffer_row, RunnableTaskStatus::Running, cx);
148    }
149    let editor_handle = cx.weak_entity();
150    workspace.update(cx, |workspace, cx| {
151        if let Some((buffer_id, buffer_row)) = runnable_task_key {
152            workspace.schedule_resolved_task_with_completion(
153                task_source_kind,
154                resolved_task,
155                false,
156                move |result, cx| {
157                    editor_handle
158                        .update(cx, |editor, cx| {
159                            editor.set_runnable_task_status(
160                                buffer_id,
161                                buffer_row,
162                                RunnableTaskStatus::from(result),
163                                cx,
164                            );
165                        })
166                        .ok();
167                },
168                window,
169                cx,
170            );
171        } else {
172            workspace.schedule_resolved_task(task_source_kind, resolved_task, false, window, cx);
173        }
174    });
175    true
176}
177
178fn try_show_references(
179    arguments: &[serde_json::Value],
180    action: &CodeAction,
181    editor: &mut Editor,
182    window: &mut Window,
183    cx: &mut Context<Editor>,
184) -> bool {
185    if arguments.len() < 3 {
186        return false;
187    }
188    let Ok(locations) = serde_json::from_value::<Vec<lsp::Location>>(arguments[2].clone()) else {
189        return false;
190    };
191    if locations.is_empty() {
192        return false;
193    }
194
195    let server_id = action.server_id;
196    let nav_entry = editor.navigation_entry(editor.selections.newest_anchor().head(), cx);
197    let links = locations
198        .into_iter()
199        .map(|location| HoverLink::LspLocation(location, server_id))
200        .collect();
201    editor
202        .navigate_to_hover_links(None, links, nav_entry, false, window, cx)
203        .detach_and_log_err(cx);
204
205    true
206}
207
208impl Editor {
209    pub(super) fn refresh_code_lenses(
210        &mut self,
211        for_buffer: Option<BufferId>,
212        _window: &Window,
213        cx: &mut Context<Self>,
214    ) {
215        if !self.lsp_data_enabled() || self.code_lens.is_none() {
216            return;
217        }
218        let Some(project) = self.project.clone() else {
219            return;
220        };
221
222        let buffers_to_query = self
223            .visible_buffers(cx)
224            .into_iter()
225            .filter(|buffer| self.is_lsp_relevant(buffer.read(cx).file(), cx))
226            .chain(for_buffer.and_then(|buffer_id| self.buffer.read(cx).buffer(buffer_id)))
227            .filter(|editor_buffer| {
228                let editor_buffer_id = editor_buffer.read(cx).remote_id();
229                for_buffer.is_none_or(|buffer_id| buffer_id == editor_buffer_id)
230                    && self.registered_buffers.contains_key(&editor_buffer_id)
231            })
232            .unique_by(|buffer| buffer.read(cx).remote_id())
233            .collect::<Vec<_>>();
234
235        if buffers_to_query.is_empty() {
236            return;
237        }
238
239        let project = project.downgrade();
240        self.refresh_code_lens_task = cx.spawn(async move |editor, cx| {
241            cx.background_executor()
242                .timer(LSP_REQUEST_DEBOUNCE_TIMEOUT)
243                .await;
244
245            let Some(tasks_per_buffer) = project
246                .update(cx, |project, cx| {
247                    project.lsp_store().update(cx, |lsp_store, cx| {
248                        buffers_to_query
249                            .into_iter()
250                            .map(|buffer| {
251                                let buffer_id = buffer.read(cx).remote_id();
252                                let task = lsp_store.code_lens_actions(&buffer, cx);
253                                async move { (buffer_id, task.await) }
254                            })
255                            .collect::<Vec<_>>()
256                    })
257                })
258                .ok()
259            else {
260                return;
261            };
262
263            let code_lens_per_buffer = join_all(tasks_per_buffer).await;
264            if code_lens_per_buffer.is_empty() {
265                return;
266            }
267
268            editor
269                .update(cx, |editor, cx| {
270                    let snapshot = editor.buffer().read(cx).snapshot(cx);
271                    for (buffer_id, result) in code_lens_per_buffer {
272                        let actions = match result {
273                            Ok(Some(actions)) => actions,
274                            Ok(None) => continue,
275                            Err(e) => {
276                                log::error!(
277                                    "Failed to fetch code lenses for buffer {buffer_id:?}: {e:#}"
278                                );
279                                continue;
280                            }
281                        };
282                        editor.apply_lens_actions_for_buffer(buffer_id, actions, &snapshot, cx);
283                    }
284                    editor.resolve_visible_code_lenses(cx);
285                })
286                .ok();
287        });
288    }
289
290    /// Reconcile blocks for `buffer_id` against the latest `actions`.
291    ///
292    /// Lenses without a command cannot be rendered, as it's the only textual data in the [`lsp::CodeLens`].
293    /// Worst case, we can know it only after asynchronously issuing a resolve request to the server.
294    /// To avoid flickering during typing, keep a placeholder block for each lens and replace it with a resolved command's block when available,
295    /// or with a synthetic "0 references" title if the server resolve did not return a command.
296    ///
297    /// Also keep the old block until the fresh resolve lands, to avoid flickering during typing.
298    fn apply_lens_actions_for_buffer(
299        &mut self,
300        buffer_id: BufferId,
301        actions: CodeLensActions,
302        snapshot: &MultiBufferSnapshot,
303        cx: &mut Context<Self>,
304    ) {
305        let mut all_lenses = Vec::new();
306        for (_, action) in actions.iter().sorted_by_key(|(id, _)| **id) {
307            let Some(position) = snapshot.anchor_in_excerpt(action.range.start) else {
308                continue;
309            };
310            if let project::LspAction::CodeLens(lens) = &action.lsp_action {
311                let title = lens
312                    .command
313                    .as_ref()
314                    .filter(|cmd| !cmd.title.is_empty())
315                    .map(|cmd| SharedString::from(&cmd.title));
316                all_lenses.push((
317                    position,
318                    CodeLensItem {
319                        title,
320                        action: action.clone(),
321                    },
322                ));
323            }
324        }
325
326        let mut new_lines_by_row = group_lenses_by_row(all_lenses, snapshot)
327            .map(|line| (MultiBufferRow(line.position.to_point(snapshot).row), line))
328            .collect::<HashMap<_, _>>();
329
330        let editor_handle = cx.entity().downgrade();
331        let code_lens = self.code_lens.get_or_insert_with(CodeLensState::default);
332        let old_blocks = code_lens.blocks.remove(&buffer_id).unwrap_or_default();
333
334        let mut kept_blocks = Vec::new();
335        let mut renderers_to_replace = HashMap::default();
336        let mut blocks_to_remove = HashSet::default();
337        let mut covered_rows = HashSet::default();
338
339        for old in old_blocks {
340            let row = MultiBufferRow(old.anchor.to_point(snapshot).row);
341            let Some(new_line) = new_lines_by_row.remove(&row) else {
342                blocks_to_remove.insert(old.block_id);
343                continue;
344            };
345            covered_rows.insert(row);
346            let new_all_pending = new_line
347                .items
348                .iter()
349                .all(|item| item.title.is_none() && !item.action.resolved);
350            let old_has_rendered = old
351                .line
352                .items
353                .iter()
354                .any(|item| displayed_title(item).is_some());
355            if new_all_pending && old_has_rendered {
356                kept_blocks.push(old);
357                continue;
358            }
359            if rendered_text_matches(&old.line, &new_line) {
360                kept_blocks.push(old);
361            } else {
362                let mut updated = old;
363                updated.line = new_line.clone();
364                renderers_to_replace.insert(
365                    updated.block_id,
366                    build_code_lens_renderer(new_line, editor_handle.clone()),
367                );
368                kept_blocks.push(updated);
369            }
370        }
371
372        let mut to_insert = Vec::new();
373        for (row, new_line) in new_lines_by_row {
374            if covered_rows.contains(&row) {
375                continue;
376            }
377            let anchor = new_line.position;
378            let props = BlockProperties {
379                placement: BlockPlacement::Above(anchor),
380                height: Some(1),
381                style: BlockStyle::Spacer,
382                render: build_code_lens_renderer(new_line.clone(), editor_handle.clone()),
383                priority: 0,
384            };
385            to_insert.push((props, anchor, new_line));
386        }
387
388        if !blocks_to_remove.is_empty() {
389            self.remove_blocks(blocks_to_remove, None, cx);
390        }
391        if !renderers_to_replace.is_empty() {
392            self.replace_blocks(renderers_to_replace, None, cx);
393        }
394        if !to_insert.is_empty() {
395            let mut props = Vec::with_capacity(to_insert.len());
396            let mut metadata = Vec::with_capacity(to_insert.len());
397            for (p, anchor, line) in to_insert {
398                props.push(p);
399                metadata.push((anchor, line));
400            }
401            let block_ids = self.insert_blocks(props, None, cx);
402            for (block_id, (anchor, line)) in block_ids.into_iter().zip(metadata) {
403                kept_blocks.push(CodeLensBlock {
404                    block_id,
405                    anchor,
406                    line,
407                });
408            }
409        }
410
411        let code_lens = self.code_lens.get_or_insert_with(CodeLensState::default);
412        if actions.is_empty() {
413            code_lens.actions.remove(&buffer_id);
414        } else {
415            code_lens.actions.insert(buffer_id, actions);
416        }
417        if kept_blocks.is_empty() {
418            code_lens.blocks.remove(&buffer_id);
419        } else {
420            code_lens.blocks.insert(buffer_id, kept_blocks);
421        }
422        cx.notify();
423    }
424
425    pub fn supports_code_lens(&self, cx: &ui::App) -> bool {
426        let Some(project) = self.project.as_ref() else {
427            return false;
428        };
429        let lsp_store = project.read(cx).lsp_store().read(cx);
430        lsp_store
431            .lsp_server_capabilities
432            .values()
433            .any(|caps| caps.code_lens_provider.is_some())
434    }
435
436    pub fn code_lens_enabled(&self) -> bool {
437        self.code_lens.is_some()
438    }
439
440    pub fn toggle_code_lens_action(
441        &mut self,
442        _: &ToggleCodeLens,
443        window: &mut Window,
444        cx: &mut Context<Self>,
445    ) {
446        let currently_enabled = self.code_lens.is_some();
447        self.toggle_code_lens(!currently_enabled, window, cx);
448    }
449
450    pub(super) fn toggle_code_lens(
451        &mut self,
452        enabled: bool,
453        window: &mut Window,
454        cx: &mut Context<Self>,
455    ) {
456        if enabled {
457            self.code_lens.get_or_insert_with(CodeLensState::default);
458            self.refresh_code_lenses(None, window, cx);
459        } else {
460            self.clear_code_lenses(cx);
461        }
462    }
463
464    pub(super) fn resolve_visible_code_lenses(&mut self, cx: &mut Context<Self>) {
465        if !self.lsp_data_enabled() || self.code_lens.is_none() {
466            return;
467        }
468        let Some(project) = self.project.clone() else {
469            return;
470        };
471
472        let lsp_store = project.read(cx).lsp_store();
473
474        let mut pending_resolves = Vec::new();
475        for (buffer_snapshot, visible_range, _) in self.visible_buffer_ranges(cx) {
476            let buffer_id = buffer_snapshot.remote_id();
477            let Some(buffer) = self.buffer.read(cx).buffer(buffer_id) else {
478                continue;
479            };
480            let Some(actions) = self
481                .code_lens
482                .as_ref()
483                .and_then(|state| state.actions.get(&buffer_id))
484            else {
485                continue;
486            };
487            for (lens_id, action) in actions {
488                if action.resolved {
489                    continue;
490                }
491                if let project::LspAction::CodeLens(lens) = &action.lsp_action {
492                    if lens.command.is_some() {
493                        continue;
494                    }
495                }
496                let action_offset = action.range.start.to_offset(&buffer_snapshot);
497                if action_offset < visible_range.start.0 || action_offset > visible_range.end.0 {
498                    continue;
499                }
500                let resolve_task = lsp_store.update(cx, |lsp_store, cx| {
501                    lsp_store.resolve_code_lens(&buffer, action.server_id, *lens_id, cx)
502                });
503                pending_resolves.push((buffer_id, resolve_task));
504            }
505        }
506        if pending_resolves.is_empty() {
507            return;
508        }
509
510        let code_lens = self.code_lens.get_or_insert_with(CodeLensState::default);
511        code_lens.resolve_task = cx.spawn(async move |editor, cx| {
512            let mut resolves_in_progress = pending_resolves
513                .into_iter()
514                .map(|(buffer_id, task)| async move { (buffer_id, task.await) })
515                .collect::<FuturesUnordered<_>>();
516            while let Some((buffer_id, resolve_result)) = resolves_in_progress.next().await {
517                let Some((resolved_id, resolved)) = resolve_result else {
518                    continue;
519                };
520                editor
521                    .update(cx, |editor, cx| {
522                        let snapshot = editor.buffer().read(cx).snapshot(cx);
523                        let Some(mut actions) = editor
524                            .code_lens
525                            .as_ref()
526                            .and_then(|state| state.actions.get(&buffer_id))
527                            .cloned()
528                        else {
529                            return;
530                        };
531                        if let Some(slot) = actions.get_mut(&resolved_id) {
532                            *slot = resolved;
533                        }
534                        editor.apply_lens_actions_for_buffer(buffer_id, actions, &snapshot, cx);
535                    })
536                    .ok();
537            }
538        });
539    }
540
541    pub(super) fn clear_code_lenses(&mut self, cx: &mut Context<Self>) {
542        if let Some(code_lens) = self.code_lens.take() {
543            let all_blocks = code_lens
544                .blocks
545                .into_values()
546                .flatten()
547                .map(|block| block.block_id)
548                .collect::<HashSet<_>>();
549            if !all_blocks.is_empty() {
550                self.remove_blocks(all_blocks, None, cx);
551            }
552            cx.notify();
553        }
554        self.refresh_code_lens_task = Task::ready(());
555    }
556}
557
558/// Whether two lens lines would render the same on screen — same indent
559/// and same titles in the same order. Used to skip recreating a renderer
560/// (and thus a click handler) when nothing about the displayed line
561/// changed; the captured [`CodeAction`] inside the existing renderer keeps
562/// pointing at the right spot because its anchors track buffer edits.
563fn rendered_text_matches(a: &CodeLensLine, b: &CodeLensLine) -> bool {
564    a.indent_column == b.indent_column
565        && a.items.len() == b.items.len()
566        && a.items
567            .iter()
568            .zip(&b.items)
569            .all(|(x, y)| displayed_title(x) == displayed_title(y))
570}
571
572/// Text rendered for a code lens item, or `None` if it should not render
573/// (placeholder while resolve is in flight).
574fn displayed_title(item: &CodeLensItem) -> Option<&SharedString> {
575    item.title
576        .as_ref()
577        .or_else(|| item.action.resolved.then_some(&EMPTY_LENS_FALLBACK_TITLE))
578}
579
580fn group_lenses_by_row(
581    lenses: Vec<(Anchor, CodeLensItem)>,
582    snapshot: &MultiBufferSnapshot,
583) -> impl Iterator<Item = CodeLensLine> {
584    lenses
585        .into_iter()
586        .into_group_map_by(|(position, _)| {
587            let row = position.to_point(snapshot).row;
588            MultiBufferRow(row)
589        })
590        .into_iter()
591        .sorted_by_key(|(row, _)| *row)
592        .filter_map(|(row, entries)| {
593            let position = entries.first()?.0;
594            let items = entries.into_iter().map(|(_, item)| item).collect();
595            let indent_column = snapshot.indent_size_for_line(row).len;
596            Some(CodeLensLine {
597                position,
598                indent_column,
599                items,
600            })
601        })
602}
603
604fn build_code_lens_renderer(line: CodeLensLine, editor: WeakEntity<Editor>) -> RenderBlock {
605    Arc::new(move |cx| {
606        let resolved_items = line
607            .items
608            .iter()
609            .filter_map(|item| {
610                let title = displayed_title(item)?;
611                let action = item.title.is_some().then(|| item.action.clone());
612                Some((title, action))
613            })
614            .collect::<Vec<_>>();
615        let mut children = Vec::with_capacity((2 * resolved_items.len()).saturating_sub(1));
616        let text_style = &cx.editor_style.text;
617        let font = text_style.font();
618        let font_size = text_style.font_size.to_pixels(cx.window.rem_size()) * 0.9;
619
620        for (i, (title, action)) in resolved_items.into_iter().enumerate() {
621            if i > 0 {
622                children.push(
623                    div()
624                        .font(font.clone())
625                        .text_size(font_size)
626                        .text_color(cx.app.theme().colors().text_muted)
627                        .child(CODE_LENS_SEPARATOR)
628                        .into_any_element(),
629                );
630            }
631
632            children.push(
633                div()
634                    .id(ElementId::from(i))
635                    .font(font.clone())
636                    .text_size(font_size)
637                    .text_color(cx.app.theme().colors().text_muted)
638                    .child(title.clone())
639                    .when_some(action, |code_lens_div, action| {
640                        let position = line.position;
641                        let editor_handle = editor.clone();
642
643                        code_lens_div
644                            .cursor_pointer()
645                            .hover(|style| style.text_color(cx.app.theme().colors().text))
646                            .on_mouse_down(MouseButton::Left, |_, _, cx| {
647                                cx.stop_propagation();
648                            })
649                            .on_mouse_down(MouseButton::Right, |_, _, cx| {
650                                cx.stop_propagation();
651                            })
652                            .on_click({
653                                move |_event, window, cx| {
654                                    if let Some(editor) = editor_handle.upgrade() {
655                                        editor.update(cx, |editor, cx| {
656                                            editor.change_selections(
657                                                SelectionEffects::default(),
658                                                window,
659                                                cx,
660                                                |s| {
661                                                    s.select_anchor_ranges([position..position]);
662                                                },
663                                            );
664
665                                            let action = action.clone();
666                                            if let Some(workspace) = editor.workspace() {
667                                                if try_handle_client_command(
668                                                    &action, editor, &workspace, window, cx,
669                                                ) {
670                                                    return;
671                                                }
672
673                                                let project = workspace.read(cx).project().clone();
674                                                if let Some(buffer) = editor
675                                                    .buffer()
676                                                    .read(cx)
677                                                    .buffer(action.range.start.buffer_id)
678                                                {
679                                                    project
680                                                        .update(cx, |project, cx| {
681                                                            project.apply_code_action(
682                                                                buffer, action, true, cx,
683                                                            )
684                                                        })
685                                                        .detach_and_log_err(cx);
686                                                }
687                                            }
688                                        });
689                                    }
690                                }
691                            })
692                    })
693                    .into_any_element(),
694            );
695        }
696
697        div()
698            .id(cx.block_id)
699            .pl(cx.em_width * (line.indent_column as f32 + 0.5))
700            .h_full()
701            .flex()
702            .flex_row()
703            .items_end()
704            .children(children)
705            .into_any_element()
706    })
707}
708
709#[cfg(test)]
710mod tests {
711    use std::{
712        sync::{Arc, Mutex},
713        time::Duration,
714    };
715
716    use collections::HashSet;
717    use futures::StreamExt;
718    use gpui::TestAppContext;
719    use indoc::indoc;
720    use settings::CodeLens;
721    use util::path;
722
723    use multi_buffer::{MultiBufferRow, ToPoint as _};
724    use text::Point;
725
726    use super::{CODE_LENS_SEPARATOR, displayed_title};
727    use crate::{
728        Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT,
729        editor_tests::{init_test, update_test_editor_settings},
730        test::editor_lsp_test_context::EditorLspTestContext,
731    };
732
733    #[gpui::test]
734    async fn test_code_lens_blocks(cx: &mut TestAppContext) {
735        init_test(cx, |_| {});
736        update_test_editor_settings(cx, &|settings| {
737            settings.code_lens = Some(CodeLens::On);
738        });
739
740        let mut cx = EditorLspTestContext::new_typescript(
741            lsp::ServerCapabilities {
742                code_lens_provider: Some(lsp::CodeLensOptions {
743                    resolve_provider: None,
744                }),
745                execute_command_provider: Some(lsp::ExecuteCommandOptions {
746                    commands: vec!["lens_cmd".to_string()],
747                    ..lsp::ExecuteCommandOptions::default()
748                }),
749                ..lsp::ServerCapabilities::default()
750            },
751            cx,
752        )
753        .await;
754
755        let mut code_lens_request =
756            cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
757                Ok(Some(vec![
758                    lsp::CodeLens {
759                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
760                        command: Some(lsp::Command {
761                            title: "2 references".to_owned(),
762                            command: "lens_cmd".to_owned(),
763                            arguments: None,
764                        }),
765                        data: None,
766                    },
767                    lsp::CodeLens {
768                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 19)),
769                        command: Some(lsp::Command {
770                            title: "0 references".to_owned(),
771                            command: "lens_cmd".to_owned(),
772                            arguments: None,
773                        }),
774                        data: None,
775                    },
776                ]))
777            });
778
779        cx.set_state("ˇfunction hello() {}\nfunction world() {}");
780
781        assert!(
782            code_lens_request.next().await.is_some(),
783            "should have received a code lens request"
784        );
785        cx.run_until_parked();
786
787        cx.editor(|editor, _, cx| {
788            assert_eq!(
789                editor.code_lens_enabled(),
790                true,
791                "code lens should be enabled"
792            );
793            assert_eq!(
794                code_lens_assertion_text(editor, cx),
795                indoc! {r#"
796                    Lenses: 2 references
797                    Line 1: function hello() {}
798
799                    Lenses: 0 references
800                    Line 2: function world() {}
801                "#},
802                "both lenses should render their server-provided titles"
803            );
804        });
805    }
806
807    #[gpui::test]
808    async fn test_code_lens_refresh_requeries_open_document(cx: &mut TestAppContext) {
809        init_test(cx, |_| {});
810        update_test_editor_settings(cx, &|settings| {
811            settings.code_lens = Some(CodeLens::On);
812        });
813
814        let mut cx = EditorLspTestContext::new_typescript(
815            lsp::ServerCapabilities {
816                code_lens_provider: Some(lsp::CodeLensOptions {
817                    resolve_provider: None,
818                }),
819                execute_command_provider: Some(lsp::ExecuteCommandOptions {
820                    commands: vec!["lens_cmd".to_string()],
821                    ..lsp::ExecuteCommandOptions::default()
822                }),
823                ..lsp::ServerCapabilities::default()
824            },
825            cx,
826        )
827        .await;
828
829        let lens_title = Arc::new(Mutex::new("Initial lens".to_string()));
830        let mut code_lens_request =
831            cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>({
832                let lens_title = lens_title.clone();
833                move |_, _, _| {
834                    let lens_title = lens_title.clone();
835                    async move {
836                        let title = lens_title.lock().unwrap().clone();
837                        Ok(Some(vec![lsp::CodeLens {
838                            range: lsp::Range::new(
839                                lsp::Position::new(0, 0),
840                                lsp::Position::new(0, 19),
841                            ),
842                            command: Some(lsp::Command {
843                                title,
844                                command: "lens_cmd".to_owned(),
845                                arguments: None,
846                            }),
847                            data: None,
848                        }]))
849                    }
850                }
851            });
852
853        cx.set_state("ˇfunction hello() {}\nfunction world() {}");
854        assert!(
855            code_lens_request.next().await.is_some(),
856            "should have received the initial code lens request"
857        );
858        cx.run_until_parked();
859        cx.editor(|editor, _, cx| {
860            assert_eq!(
861                code_lens_assertion_text(editor, cx),
862                indoc! {r#"
863                    Lenses: Initial lens
864                    Line 1: function hello() {}
865                "#},
866                "initial fetch should render the server title"
867            );
868        });
869
870        *lens_title.lock().unwrap() = "Refreshed lens".to_string();
871        cx.lsp
872            .request::<lsp::request::CodeLensRefresh>((), lsp::DEFAULT_LSP_REQUEST_TIMEOUT)
873            .await
874            .into_response()
875            .expect("code lens refresh request failed");
876        cx.executor()
877            .advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT * 2);
878        cx.run_until_parked();
879        cx.editor(|editor, _, cx| {
880            assert_eq!(
881                code_lens_assertion_text(editor, cx),
882                indoc! {r#"
883                    Lenses: Refreshed lens
884                    Line 1: function hello() {}
885                "#},
886                "refresh should update the displayed lens to the new server title"
887            );
888        });
889    }
890
891    #[gpui::test]
892    async fn test_code_lens_dynamic_registration_requeries_open_document(cx: &mut TestAppContext) {
893        init_test(cx, |_| {});
894        update_test_editor_settings(cx, &|settings| {
895            settings.code_lens = Some(CodeLens::On);
896        });
897
898        // The server advertises no code lens capability up front; it registers
899        // `textDocument/codeLens` dynamically only after the document is open.
900        let mut cx = EditorLspTestContext::new_typescript(
901            lsp::ServerCapabilities {
902                execute_command_provider: Some(lsp::ExecuteCommandOptions {
903                    commands: vec!["lens_cmd".to_string()],
904                    ..lsp::ExecuteCommandOptions::default()
905                }),
906                ..lsp::ServerCapabilities::default()
907            },
908            cx,
909        )
910        .await;
911
912        let _code_lens_request =
913            cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
914                Ok(Some(vec![lsp::CodeLens {
915                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
916                    command: Some(lsp::Command {
917                        title: "Dynamic lens".to_owned(),
918                        command: "lens_cmd".to_owned(),
919                        arguments: None,
920                    }),
921                    data: None,
922                }]))
923            });
924
925        cx.set_state("ˇfunction hello() {}\nfunction world() {}");
926        // Drain any debounced refresh scheduled before the capability exists, so
927        // the post-registration re-query can only come from the dynamic
928        // registration handling itself.
929        cx.executor()
930            .advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT * 2);
931        cx.run_until_parked();
932        cx.editor(|editor, _, cx| {
933            assert_eq!(
934                code_lens_assertion_text(editor, cx),
935                "\n",
936                "no lenses should render before the capability is registered"
937            );
938        });
939
940        cx.lsp
941            .request::<lsp::request::RegisterCapability>(
942                lsp::RegistrationParams {
943                    registrations: vec![lsp::Registration {
944                        id: "code-lens".to_string(),
945                        method: "textDocument/codeLens".to_string(),
946                        register_options: Some(
947                            serde_json::to_value(lsp::CodeLensOptions {
948                                resolve_provider: None,
949                            })
950                            .unwrap(),
951                        ),
952                    }],
953                },
954                lsp::DEFAULT_LSP_REQUEST_TIMEOUT,
955            )
956            .await
957            .into_response()
958            .expect("register capability request failed");
959        cx.executor()
960            .advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT * 2);
961        cx.run_until_parked();
962        cx.editor(|editor, _, cx| {
963            assert_eq!(
964                code_lens_assertion_text(editor, cx),
965                indoc! {r#"
966                    Lenses: Dynamic lens
967                    Line 1: function hello() {}
968                "#},
969                "dynamic textDocument/codeLens registration should re-query and display lenses for the open document"
970            );
971        });
972    }
973
974    #[gpui::test]
975    async fn test_code_lens_blocks_kept_across_refresh(cx: &mut TestAppContext) {
976        init_test(cx, |_| {});
977        update_test_editor_settings(cx, &|settings| {
978            settings.code_lens = Some(CodeLens::On);
979        });
980
981        let mut cx = EditorLspTestContext::new_typescript(
982            lsp::ServerCapabilities {
983                code_lens_provider: Some(lsp::CodeLensOptions {
984                    resolve_provider: None,
985                }),
986                execute_command_provider: Some(lsp::ExecuteCommandOptions {
987                    commands: vec!["lens_cmd".to_string()],
988                    ..lsp::ExecuteCommandOptions::default()
989                }),
990                ..lsp::ServerCapabilities::default()
991            },
992            cx,
993        )
994        .await;
995
996        let mut code_lens_request =
997            cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
998                Ok(Some(vec![lsp::CodeLens {
999                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
1000                    command: Some(lsp::Command {
1001                        title: "1 reference".to_owned(),
1002                        command: "lens_cmd".to_owned(),
1003                        arguments: None,
1004                    }),
1005                    data: None,
1006                }]))
1007            });
1008
1009        cx.set_state("ˇfunction hello() {}\nfunction world() {}");
1010
1011        assert!(
1012            code_lens_request.next().await.is_some(),
1013            "should have received the initial code lens request"
1014        );
1015        cx.run_until_parked();
1016
1017        let initial_block_ids = cx.editor(|editor, _, cx| {
1018            assert_eq!(
1019                code_lens_assertion_text(editor, cx),
1020                indoc! {r#"
1021                    Lenses: 1 reference
1022                    Line 1: function hello() {}
1023                "#},
1024                "initial fetch should render the server title"
1025            );
1026            editor
1027                .code_lens
1028                .as_ref()
1029                .map(|s| {
1030                    s.blocks
1031                        .values()
1032                        .flatten()
1033                        .map(|b| b.block_id)
1034                        .collect::<HashSet<_>>()
1035                })
1036                .unwrap_or_default()
1037        });
1038
1039        cx.update_editor(|editor, window, cx| {
1040            editor.move_to_end(&crate::actions::MoveToEnd, window, cx);
1041            editor.handle_input("\n// trailing comment", window, cx);
1042        });
1043        cx.executor()
1044            .advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT + Duration::from_millis(50));
1045        assert!(
1046            code_lens_request.next().await.is_some(),
1047            "should have received another code lens request after edit"
1048        );
1049        cx.run_until_parked();
1050
1051        let refreshed_block_ids = cx.editor(|editor, _, cx| {
1052            assert_eq!(
1053                code_lens_assertion_text(editor, cx),
1054                indoc! {r#"
1055                    Lenses: 1 reference
1056                    Line 1: function hello() {}
1057                "#},
1058                "refreshed block should keep rendering the same title"
1059            );
1060            editor
1061                .code_lens
1062                .as_ref()
1063                .map(|s| {
1064                    s.blocks
1065                        .values()
1066                        .flatten()
1067                        .map(|b| b.block_id)
1068                        .collect::<HashSet<_>>()
1069                })
1070                .unwrap_or_default()
1071        });
1072        assert_eq!(
1073            refreshed_block_ids, initial_block_ids,
1074            "Code lens blocks should be preserved across refreshes when their content is unchanged"
1075        );
1076    }
1077
1078    #[gpui::test]
1079    async fn test_code_lens_blocks_kept_when_only_resolve_fills_titles(cx: &mut TestAppContext) {
1080        init_test(cx, |_| {});
1081        update_test_editor_settings(cx, &|settings| {
1082            settings.code_lens = Some(CodeLens::On);
1083        });
1084
1085        let mut cx = EditorLspTestContext::new_typescript(
1086            lsp::ServerCapabilities {
1087                code_lens_provider: Some(lsp::CodeLensOptions {
1088                    resolve_provider: Some(true),
1089                }),
1090                ..lsp::ServerCapabilities::default()
1091            },
1092            cx,
1093        )
1094        .await;
1095
1096        // The LSP returns shallow code lenses on every fetch; only `resolve`
1097        // populates the command/title. This is the realistic flow with
1098        // servers like rust-analyzer and exercises the path where each
1099        // post-edit refresh comes back unresolved before the resolve catches
1100        // up.
1101        let mut code_lens_request =
1102            cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
1103                Ok(Some(vec![lsp::CodeLens {
1104                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
1105                    command: None,
1106                    data: Some(serde_json::json!({"id": "lens_1"})),
1107                }]))
1108            });
1109
1110        cx.lsp
1111            .set_request_handler::<lsp::request::CodeLensResolve, _, _>(|lens, _| async move {
1112                Ok(lsp::CodeLens {
1113                    command: Some(lsp::Command {
1114                        title: "1 reference".to_owned(),
1115                        command: "resolved_cmd".to_owned(),
1116                        arguments: None,
1117                    }),
1118                    ..lens
1119                })
1120            });
1121
1122        cx.set_state("ˇfunction hello() {}\nfunction world() {}");
1123
1124        assert!(
1125            code_lens_request.next().await.is_some(),
1126            "should have received the initial code lens request"
1127        );
1128        cx.run_until_parked();
1129
1130        let initial = cx.editor(|editor, _, cx| {
1131            assert_eq!(
1132                code_lens_assertion_text(editor, cx),
1133                indoc! {r#"
1134                    Lenses: 1 reference
1135                    Line 1: function hello() {}
1136                "#},
1137                "resolve should fill the placeholder with the server title"
1138            );
1139            editor
1140                .code_lens
1141                .as_ref()
1142                .map(|s| {
1143                    s.blocks
1144                        .values()
1145                        .flatten()
1146                        .map(|b| b.block_id)
1147                        .collect::<HashSet<_>>()
1148                })
1149                .unwrap_or_default()
1150        });
1151
1152        for keystroke in [" ", "x", "y"] {
1153            cx.update_editor(|editor, window, cx| {
1154                editor.move_to_end(&crate::actions::MoveToEnd, window, cx);
1155                editor.handle_input(keystroke, window, cx);
1156            });
1157            cx.executor()
1158                .advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT + Duration::from_millis(50));
1159            assert!(
1160                code_lens_request.next().await.is_some(),
1161                "should have received another (shallow) code lens request after edit"
1162            );
1163            cx.run_until_parked();
1164
1165            let after = cx.editor(|editor, _, cx| {
1166                assert_eq!(
1167                    code_lens_assertion_text(editor, cx),
1168                    indoc! {r#"
1169                        Lenses: 1 reference
1170                        Line 1: function hello() {}
1171                    "#},
1172                    "refresh+resolve cycle should keep rendering the same title"
1173                );
1174                editor
1175                    .code_lens
1176                    .as_ref()
1177                    .map(|s| {
1178                        s.blocks
1179                            .values()
1180                            .flatten()
1181                            .map(|b| b.block_id)
1182                            .collect::<HashSet<_>>()
1183                    })
1184                    .unwrap_or_default()
1185            });
1186            assert_eq!(
1187                after, initial,
1188                "Block IDs must survive the unresolved-fetch → resolve cycle without churn"
1189            );
1190        }
1191    }
1192
1193    #[gpui::test]
1194    async fn test_code_lens_placeholder_block_before_resolve(cx: &mut TestAppContext) {
1195        init_test(cx, |_| {});
1196        update_test_editor_settings(cx, &|settings| {
1197            settings.code_lens = Some(CodeLens::On);
1198        });
1199
1200        let mut cx = EditorLspTestContext::new_typescript(
1201            lsp::ServerCapabilities {
1202                code_lens_provider: Some(lsp::CodeLensOptions {
1203                    resolve_provider: Some(true),
1204                }),
1205                ..lsp::ServerCapabilities::default()
1206            },
1207            cx,
1208        )
1209        .await;
1210
1211        let mut code_lens_request =
1212            cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
1213                let mut lenses = Vec::new();
1214                lenses.push(lsp::CodeLens {
1215                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
1216                    command: None,
1217                    data: Some(serde_json::json!({"id": "lens_1"})),
1218                });
1219                Ok(Some(lenses))
1220            });
1221
1222        let (resolve_tx, resolve_rx) = futures::channel::oneshot::channel::<()>();
1223        let resolve_rx = std::sync::Mutex::new(Some(resolve_rx));
1224        cx.lsp
1225            .set_request_handler::<lsp::request::CodeLensResolve, _, _>(move |lens, _| {
1226                let rx = resolve_rx.lock().unwrap().take();
1227                async move {
1228                    if let Some(rx) = rx {
1229                        rx.await.ok();
1230                    }
1231                    Ok(lsp::CodeLens {
1232                        command: Some(lsp::Command {
1233                            title: "1 reference".to_owned(),
1234                            command: "resolved_cmd".to_owned(),
1235                            arguments: None,
1236                        }),
1237                        ..lens
1238                    })
1239                }
1240            });
1241
1242        cx.set_state("ˇfunction hello() {}");
1243
1244        assert!(
1245            code_lens_request.next().await.is_some(),
1246            "should have received the initial code lens request"
1247        );
1248        cx.run_until_parked();
1249
1250        cx.editor(|editor, _, cx| {
1251            assert_eq!(
1252                code_lens_assertion_text(editor, cx),
1253                indoc! {r#"
1254                    Lenses: <placeholder>
1255                    Line 1: function hello() {}
1256                "#},
1257                "placeholder spacer should be reserved with no rendered text before resolve"
1258            );
1259        });
1260
1261        resolve_tx.send(()).ok();
1262        cx.run_until_parked();
1263
1264        cx.editor(|editor, _, cx| {
1265            assert_eq!(
1266                code_lens_assertion_text(editor, cx),
1267                indoc! {r#"
1268                    Lenses: 1 reference
1269                    Line 1: function hello() {}
1270                "#},
1271                "after resolve the placeholder should display the server title"
1272            );
1273        });
1274    }
1275
1276    #[gpui::test]
1277    async fn test_code_lens_placeholder_kept_when_resolve_yields_empty_title(
1278        cx: &mut TestAppContext,
1279    ) {
1280        init_test(cx, |_| {});
1281        update_test_editor_settings(cx, &|settings| {
1282            settings.code_lens = Some(CodeLens::On);
1283        });
1284
1285        let mut cx = EditorLspTestContext::new_typescript(
1286            lsp::ServerCapabilities {
1287                code_lens_provider: Some(lsp::CodeLensOptions {
1288                    resolve_provider: Some(true),
1289                }),
1290                ..lsp::ServerCapabilities::default()
1291            },
1292            cx,
1293        )
1294        .await;
1295
1296        let mut code_lens_request =
1297            cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
1298                let mut lenses = Vec::new();
1299                lenses.push(lsp::CodeLens {
1300                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
1301                    command: None,
1302                    data: Some(serde_json::json!({"id": "lens_1"})),
1303                });
1304                Ok(Some(lenses))
1305            });
1306
1307        cx.lsp
1308            .set_request_handler::<lsp::request::CodeLensResolve, _, _>(|lens, _| async move {
1309                Ok(lsp::CodeLens {
1310                    command: Some(lsp::Command {
1311                        title: String::new(),
1312                        command: "noop".to_owned(),
1313                        arguments: None,
1314                    }),
1315                    ..lens
1316                })
1317            });
1318
1319        cx.set_state("ˇfunction hello() {}");
1320
1321        assert!(
1322            code_lens_request.next().await.is_some(),
1323            "should have received the initial code lens request"
1324        );
1325        cx.run_until_parked();
1326
1327        cx.editor(|editor, _, cx| {
1328            assert_eq!(
1329                code_lens_assertion_text(editor, cx),
1330                indoc! {r#"
1331                    Lenses: 0 references
1332                    Line 1: function hello() {}
1333                "#},
1334                "lens resolved to an empty title should fall back to the synthetic label"
1335            );
1336        });
1337    }
1338
1339    #[gpui::test]
1340    async fn test_code_lens_same_range_lenses_resolve_independently(cx: &mut TestAppContext) {
1341        init_test(cx, |_| {});
1342        update_test_editor_settings(cx, &|settings| {
1343            settings.code_lens = Some(CodeLens::On);
1344        });
1345
1346        let mut cx = EditorLspTestContext::new_typescript(
1347            lsp::ServerCapabilities {
1348                code_lens_provider: Some(lsp::CodeLensOptions {
1349                    resolve_provider: Some(true),
1350                }),
1351                ..lsp::ServerCapabilities::default()
1352            },
1353            cx,
1354        )
1355        .await;
1356
1357        // Two shallow lenses on the same range, distinguished only by `data`
1358        // — exactly the shape vtsls/TypeScript-LS uses for the
1359        // "references" + "implementations" pair on the same line.
1360        let mut code_lens_request =
1361            cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
1362                Ok(Some(vec![
1363                    lsp::CodeLens {
1364                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
1365                        command: None,
1366                        data: Some(serde_json::json!({"kind": "references"})),
1367                    },
1368                    lsp::CodeLens {
1369                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
1370                        command: None,
1371                        data: Some(serde_json::json!({"kind": "implementations"})),
1372                    },
1373                ]))
1374            });
1375
1376        let resolve_calls = Arc::new(Mutex::new(Vec::<serde_json::Value>::new()));
1377        cx.lsp
1378            .set_request_handler::<lsp::request::CodeLensResolve, _, _>({
1379                let resolve_calls = resolve_calls.clone();
1380                move |lens, _| {
1381                    let resolve_calls = resolve_calls.clone();
1382                    async move {
1383                        let kind = lens
1384                            .data
1385                            .as_ref()
1386                            .and_then(|d| d.get("kind"))
1387                            .cloned()
1388                            .unwrap_or(serde_json::Value::Null);
1389                        resolve_calls.lock().unwrap().push(kind.clone());
1390                        let title = match kind.as_str() {
1391                            Some("references") => "2 references",
1392                            Some("implementations") => "1 implementation",
1393                            _ => "",
1394                        };
1395                        Ok(lsp::CodeLens {
1396                            command: Some(lsp::Command {
1397                                title: title.to_owned(),
1398                                command: "noop".to_owned(),
1399                                arguments: None,
1400                            }),
1401                            ..lens
1402                        })
1403                    }
1404                }
1405            });
1406
1407        cx.set_state("ˇfunction hello() {}");
1408
1409        assert!(
1410            code_lens_request.next().await.is_some(),
1411            "should have received the initial code lens request"
1412        );
1413        cx.run_until_parked();
1414
1415        let calls = resolve_calls.lock().unwrap().clone();
1416        assert_eq!(
1417            calls.len(),
1418            2,
1419            "both same-range lenses should be resolved independently, got {calls:?}"
1420        );
1421        let kinds: Vec<&str> = calls.iter().filter_map(|v| v.as_str()).collect();
1422        assert_eq!(kinds.contains(&"references"), true);
1423        assert_eq!(kinds.contains(&"implementations"), true);
1424
1425        cx.editor(|editor, _, cx| {
1426            assert_eq!(
1427                code_lens_assertion_text(editor, cx),
1428                indoc! {r#"
1429                    Lenses: 2 references | 1 implementation
1430                    Line 1: function hello() {}
1431                "#},
1432                "both same-range lenses should render their resolved titles"
1433            );
1434        });
1435    }
1436
1437    #[gpui::test]
1438    async fn test_code_lens_placeholder_kept_when_resolve_yields_no_command(
1439        cx: &mut TestAppContext,
1440    ) {
1441        init_test(cx, |_| {});
1442        update_test_editor_settings(cx, &|settings| {
1443            settings.code_lens = Some(CodeLens::On);
1444        });
1445
1446        let mut cx = EditorLspTestContext::new_typescript(
1447            lsp::ServerCapabilities {
1448                code_lens_provider: Some(lsp::CodeLensOptions {
1449                    resolve_provider: Some(true),
1450                }),
1451                ..lsp::ServerCapabilities::default()
1452            },
1453            cx,
1454        )
1455        .await;
1456
1457        let mut code_lens_request =
1458            cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
1459                Ok(Some(vec![lsp::CodeLens {
1460                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
1461                    command: None,
1462                    data: Some(serde_json::json!({"id": "lens_1"})),
1463                }]))
1464            });
1465
1466        cx.lsp
1467            .set_request_handler::<lsp::request::CodeLensResolve, _, _>(|lens, _| async move {
1468                Ok(lsp::CodeLens {
1469                    command: None,
1470                    ..lens
1471                })
1472            });
1473
1474        cx.set_state("ˇfunction hello() {}");
1475
1476        assert!(
1477            code_lens_request.next().await.is_some(),
1478            "should have received the initial code lens request"
1479        );
1480        cx.run_until_parked();
1481
1482        cx.editor(|editor, _, cx| {
1483            assert_eq!(
1484                code_lens_assertion_text(editor, cx),
1485                indoc! {r#"
1486                    Lenses: 0 references
1487                    Line 1: function hello() {}
1488                "#},
1489                "lens resolved without a command should fall back to the synthetic label"
1490            );
1491        });
1492    }
1493
1494    #[gpui::test]
1495    async fn test_code_lens_disabled_by_default(cx: &mut TestAppContext) {
1496        init_test(cx, |_| {});
1497
1498        let mut cx = EditorLspTestContext::new_typescript(
1499            lsp::ServerCapabilities {
1500                code_lens_provider: Some(lsp::CodeLensOptions {
1501                    resolve_provider: None,
1502                }),
1503                execute_command_provider: Some(lsp::ExecuteCommandOptions {
1504                    commands: vec!["lens_cmd".to_string()],
1505                    ..lsp::ExecuteCommandOptions::default()
1506                }),
1507                ..lsp::ServerCapabilities::default()
1508            },
1509            cx,
1510        )
1511        .await;
1512
1513        cx.lsp
1514            .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
1515                panic!("Should not request code lenses when disabled");
1516            });
1517
1518        cx.set_state("ˇfunction hello() {}");
1519        cx.run_until_parked();
1520
1521        cx.editor(|editor, _, _cx| {
1522            assert_eq!(
1523                editor.code_lens_enabled(),
1524                false,
1525                "code lens should not be enabled when setting is off"
1526            );
1527        });
1528    }
1529
1530    #[gpui::test]
1531    async fn test_code_lens_toggling(cx: &mut TestAppContext) {
1532        init_test(cx, |_| {});
1533        update_test_editor_settings(cx, &|settings| {
1534            settings.code_lens = Some(CodeLens::On);
1535        });
1536
1537        let mut cx = EditorLspTestContext::new_typescript(
1538            lsp::ServerCapabilities {
1539                code_lens_provider: Some(lsp::CodeLensOptions {
1540                    resolve_provider: None,
1541                }),
1542                execute_command_provider: Some(lsp::ExecuteCommandOptions {
1543                    commands: vec!["lens_cmd".to_string()],
1544                    ..lsp::ExecuteCommandOptions::default()
1545                }),
1546                ..lsp::ServerCapabilities::default()
1547            },
1548            cx,
1549        )
1550        .await;
1551
1552        let mut code_lens_request =
1553            cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
1554                Ok(Some(vec![lsp::CodeLens {
1555                    range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
1556                    command: Some(lsp::Command {
1557                        title: "1 reference".to_owned(),
1558                        command: "lens_cmd".to_owned(),
1559                        arguments: None,
1560                    }),
1561                    data: None,
1562                }]))
1563            });
1564
1565        cx.set_state("ˇfunction hello() {}");
1566
1567        assert!(
1568            code_lens_request.next().await.is_some(),
1569            "should have received a code lens request"
1570        );
1571        cx.run_until_parked();
1572
1573        cx.editor(|editor, _, _cx| {
1574            assert_eq!(
1575                editor.code_lens_enabled(),
1576                true,
1577                "code lens should be enabled"
1578            );
1579            let total_blocks: usize = editor
1580                .code_lens
1581                .as_ref()
1582                .map(|s| s.blocks.values().map(|v| v.len()).sum())
1583                .unwrap_or(0);
1584            assert_eq!(total_blocks, 1, "Should have one code lens block");
1585        });
1586
1587        cx.update_editor(|editor, _window, cx| {
1588            editor.clear_code_lenses(cx);
1589        });
1590
1591        cx.editor(|editor, _, _cx| {
1592            assert_eq!(
1593                editor.code_lens_enabled(),
1594                false,
1595                "code lens should be disabled after clearing"
1596            );
1597        });
1598    }
1599
1600    #[gpui::test]
1601    async fn test_code_lens_resolve(cx: &mut TestAppContext) {
1602        init_test(cx, |_| {});
1603        update_test_editor_settings(cx, &|settings| {
1604            settings.code_lens = Some(CodeLens::On);
1605        });
1606
1607        let mut cx = EditorLspTestContext::new_typescript(
1608            lsp::ServerCapabilities {
1609                code_lens_provider: Some(lsp::CodeLensOptions {
1610                    resolve_provider: Some(true),
1611                }),
1612                ..lsp::ServerCapabilities::default()
1613            },
1614            cx,
1615        )
1616        .await;
1617
1618        let mut code_lens_request =
1619            cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
1620                Ok(Some(vec![
1621                    lsp::CodeLens {
1622                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
1623                        command: None,
1624                        data: Some(serde_json::json!({"id": "lens_1"})),
1625                    },
1626                    lsp::CodeLens {
1627                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 19)),
1628                        command: None,
1629                        data: Some(serde_json::json!({"id": "lens_2"})),
1630                    },
1631                ]))
1632            });
1633
1634        cx.lsp
1635            .set_request_handler::<lsp::request::CodeLensResolve, _, _>(|lens, _| async move {
1636                let id = lens
1637                    .data
1638                    .as_ref()
1639                    .and_then(|d| d.get("id"))
1640                    .and_then(|v| v.as_str())
1641                    .unwrap_or("unknown");
1642                let title = match id {
1643                    "lens_1" => "3 references",
1644                    "lens_2" => "1 implementation",
1645                    _ => "unknown",
1646                };
1647                Ok(lsp::CodeLens {
1648                    command: Some(lsp::Command {
1649                        title: title.to_owned(),
1650                        command: format!("resolved_{id}"),
1651                        arguments: None,
1652                    }),
1653                    ..lens
1654                })
1655            });
1656
1657        cx.set_state("ˇfunction hello() {}\nfunction world() {}");
1658
1659        assert!(
1660            code_lens_request.next().await.is_some(),
1661            "should have received a code lens request"
1662        );
1663        cx.run_until_parked();
1664
1665        cx.editor(|editor, _, _cx| {
1666            let total_blocks: usize = editor
1667                .code_lens
1668                .as_ref()
1669                .map(|s| s.blocks.values().map(|v| v.len()).sum())
1670                .unwrap_or(0);
1671            assert_eq!(
1672                total_blocks, 2,
1673                "Unresolved lenses should have been resolved and displayed"
1674            );
1675        });
1676    }
1677
1678    #[gpui::test]
1679    async fn test_code_lens_resolve_only_visible(cx: &mut TestAppContext) {
1680        init_test(cx, |_| {});
1681        update_test_editor_settings(cx, &|settings| {
1682            settings.code_lens = Some(CodeLens::On);
1683        });
1684
1685        let line_count: u32 = 100;
1686        let lens_every: u32 = 10;
1687        let lines = (0..line_count)
1688            .map(|i| format!("function func_{i}() {{}}"))
1689            .collect::<Vec<_>>()
1690            .join("\n");
1691
1692        let lens_lines = (0..line_count)
1693            .filter(|i| i % lens_every == 0)
1694            .collect::<Vec<_>>();
1695
1696        let resolved_lines = Arc::new(Mutex::new(Vec::<u32>::new()));
1697
1698        let fs = project::FakeFs::new(cx.executor());
1699        fs.insert_tree(path!("/dir"), serde_json::json!({ "main.ts": lines }))
1700            .await;
1701
1702        let project = project::Project::test(fs, [path!("/dir").as_ref()], cx).await;
1703        let (multi_workspace, cx) = cx.add_window_view(|window, cx| {
1704            workspace::MultiWorkspace::test_new(project.clone(), window, cx)
1705        });
1706        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1707
1708        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1709        language_registry.add(Arc::new(language::Language::new(
1710            language::LanguageConfig {
1711                name: "TypeScript".into(),
1712                matcher: (language::LanguageMatcher {
1713                    path_suffixes: vec!["ts".to_string()],
1714                    ..language::LanguageMatcher::default()
1715                })
1716                .into(),
1717                ..language::LanguageConfig::default()
1718            },
1719            Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
1720        )));
1721
1722        let mut fake_servers = language_registry.register_fake_lsp(
1723            "TypeScript",
1724            language::FakeLspAdapter {
1725                capabilities: lsp::ServerCapabilities {
1726                    code_lens_provider: Some(lsp::CodeLensOptions {
1727                        resolve_provider: Some(true),
1728                    }),
1729                    ..lsp::ServerCapabilities::default()
1730                },
1731                ..language::FakeLspAdapter::default()
1732            },
1733        );
1734
1735        let editor = workspace
1736            .update_in(cx, |workspace, window, cx| {
1737                workspace.open_abs_path(
1738                    std::path::PathBuf::from(path!("/dir/main.ts")),
1739                    workspace::OpenOptions::default(),
1740                    window,
1741                    cx,
1742                )
1743            })
1744            .await
1745            .unwrap()
1746            .downcast::<Editor>()
1747            .unwrap();
1748        let fake_server = fake_servers.next().await.unwrap();
1749
1750        let lens_lines_for_handler = lens_lines.clone();
1751        fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _| {
1752            let lens_lines = lens_lines_for_handler.clone();
1753            async move {
1754                Ok(Some(
1755                    lens_lines
1756                        .iter()
1757                        .map(|&line| lsp::CodeLens {
1758                            range: lsp::Range::new(
1759                                lsp::Position::new(line, 0),
1760                                lsp::Position::new(line, 10),
1761                            ),
1762                            command: None,
1763                            data: Some(serde_json::json!({ "line": line })),
1764                        })
1765                        .collect(),
1766                ))
1767            }
1768        });
1769
1770        {
1771            let resolved_lines = resolved_lines.clone();
1772            fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
1773                move |lens, _| {
1774                    let resolved_lines = resolved_lines.clone();
1775                    async move {
1776                        let line = lens
1777                            .data
1778                            .as_ref()
1779                            .and_then(|d| d.get("line"))
1780                            .and_then(|v| v.as_u64())
1781                            .unwrap() as u32;
1782                        resolved_lines.lock().unwrap().push(line);
1783                        Ok(lsp::CodeLens {
1784                            command: Some(lsp::Command {
1785                                title: format!("{line} references"),
1786                                command: format!("show_refs_{line}"),
1787                                arguments: None,
1788                            }),
1789                            ..lens
1790                        })
1791                    }
1792                },
1793            );
1794        }
1795
1796        cx.executor().advance_clock(Duration::from_millis(500));
1797        cx.run_until_parked();
1798
1799        let initial_resolved = resolved_lines
1800            .lock()
1801            .unwrap()
1802            .drain(..)
1803            .collect::<HashSet<_>>();
1804        assert_eq!(
1805            initial_resolved,
1806            HashSet::from_iter([0, 10, 20, 30, 40]),
1807            "Only lenses visible at the top should be resolved"
1808        );
1809
1810        editor.update_in(cx, |editor, window, cx| {
1811            editor.move_to_end(&crate::actions::MoveToEnd, window, cx);
1812        });
1813        cx.executor().advance_clock(Duration::from_millis(500));
1814        cx.run_until_parked();
1815
1816        let after_scroll_resolved = resolved_lines
1817            .lock()
1818            .unwrap()
1819            .drain(..)
1820            .collect::<HashSet<_>>();
1821        // Once the lenses are first applied we insert a placeholder block per
1822        // lens row so the line is reserved while the resolve is in flight.
1823        // Those placeholder blocks add display height, so after scrolling to
1824        // the end the visible buffer-row range is slightly smaller than it
1825        // would be without them, and lens row 60 is just outside it.
1826        assert_eq!(
1827            after_scroll_resolved,
1828            HashSet::from_iter([70, 80, 90]),
1829            "Only newly visible lenses at the bottom should be resolved, not middle ones"
1830        );
1831    }
1832
1833    fn code_lens_assertion_text(editor: &Editor, cx: &ui::App) -> String {
1834        let snapshot = editor.buffer().read(cx).snapshot(cx);
1835        let mut blocks = editor
1836            .code_lens
1837            .as_ref()
1838            .map(|state| state.blocks.values().flatten().collect::<Vec<_>>())
1839            .unwrap_or_default();
1840        blocks.sort_by_key(|block| block.anchor.to_point(&snapshot).row);
1841
1842        let lens_label = "Lenses";
1843        let line_label = "Line";
1844        let mut text = blocks
1845            .into_iter()
1846            .map(|block| {
1847                let row = block.anchor.to_point(&snapshot).row;
1848                let line_len = snapshot.line_len(MultiBufferRow(row));
1849                let line_text = snapshot
1850                    .text_for_range(Point::new(row, 0)..Point::new(row, line_len))
1851                    .collect::<String>();
1852                let lens_text = block
1853                    .line
1854                    .items
1855                    .iter()
1856                    .map(|item| {
1857                        displayed_title(item)
1858                            .map(|title| title.to_string())
1859                            .unwrap_or_else(|| "<placeholder>".to_string())
1860                    })
1861                    .collect::<Vec<_>>()
1862                    .join(CODE_LENS_SEPARATOR);
1863                let line_number = row + 1;
1864                let line_label = format!("{line_label} {line_number}");
1865                let label_width = line_label.len().max(lens_label.len());
1866                format!(
1867                    "{lens_label:<label_width$}: {lens_text}\n\
1868                     {line_label:<label_width$}: {line_text}"
1869                )
1870            })
1871            .collect::<Vec<_>>()
1872            .join("\n\n");
1873        text.push('\n');
1874        text
1875    }
1876}
1877
Served at tenant.openagents/omega Member data and write actions are omitted.