Skip to repository content1877 lines · 69.6 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:30:10.678Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
code_lens.rs
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