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