1use std::{collections::HashMap as StdHashMap, ops::Range, sync::Arc};
2
3use collections::{HashMap, HashSet};
4use futures::future::join_all;
5use gpui::{MouseButton, SharedString, Task, WeakEntity};
6use itertools::Itertools;
7use language::{BufferId, ClientCommand, default_client_command};
8use multi_buffer::{Anchor, MultiBufferRow, MultiBufferSnapshot, ToPoint as _};
9use project::{CodeAction, TaskSourceKind, lsp_store::lsp_ext_command};
10use task::TaskContext;
11use text::Point;
12
13use ui::{Context, Window, div, prelude::*};
14
15use crate::{
16 Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT, MultibufferSelectionMode, SelectionEffects,
17 actions::ToggleCodeLens,
18 display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
19};
20
21#[derive(Clone, Debug)]
22struct CodeLensLine {
23 position: Anchor,
24 items: Vec<CodeLensItem>,
25}
26
27#[derive(Clone, Debug)]
28struct CodeLensItem {
29 title: SharedString,
30 action: CodeAction,
31}
32
33#[derive(Default)]
34pub(super) struct CodeLensState {
35 pub(super) block_ids: HashMap<BufferId, Vec<CustomBlockId>>,
36}
37
38impl CodeLensState {
39 fn all_block_ids(&self) -> HashSet<CustomBlockId> {
40 self.block_ids.values().flatten().copied().collect()
41 }
42}
43
44fn group_lenses_by_row(
45 lenses: Vec<(Anchor, CodeLensItem)>,
46 snapshot: &MultiBufferSnapshot,
47) -> Vec<CodeLensLine> {
48 let mut grouped: HashMap<MultiBufferRow, (Anchor, Vec<CodeLensItem>)> = HashMap::default();
49
50 for (position, item) in lenses {
51 let row = position.to_point(snapshot).row;
52 grouped
53 .entry(MultiBufferRow(row))
54 .or_insert_with(|| (position, Vec::new()))
55 .1
56 .push(item);
57 }
58
59 let mut result: Vec<CodeLensLine> = grouped
60 .into_iter()
61 .map(|(_, (position, items))| CodeLensLine { position, items })
62 .collect();
63
64 result.sort_by_key(|lens| lens.position.to_point(snapshot).row);
65 result
66}
67
68fn render_code_lens_line(
69 line_number: usize,
70 lens: CodeLensLine,
71 editor: WeakEntity<Editor>,
72) -> impl Fn(&mut crate::display_map::BlockContext) -> gpui::AnyElement {
73 move |cx| {
74 let mut children: Vec<gpui::AnyElement> = Vec::new();
75 let text_style = &cx.editor_style.text;
76 let font = text_style.font();
77 let font_size = text_style.font_size.to_pixels(cx.window.rem_size()) * 0.9;
78
79 for (i, item) in lens.items.iter().enumerate() {
80 if i > 0 {
81 children.push(
82 div()
83 .font(font.clone())
84 .text_size(font_size)
85 .text_color(cx.app.theme().colors().text_muted)
86 .child(" | ")
87 .into_any_element(),
88 );
89 }
90
91 let title = item.title.clone();
92 let action = item.action.clone();
93 let editor_handle = editor.clone();
94 let position = lens.position;
95 let id = (line_number as u64) << 32 | (i as u64);
96
97 children.push(
98 div()
99 .id(ElementId::Integer(id))
100 .font(font.clone())
101 .text_size(font_size)
102 .text_color(cx.app.theme().colors().text_muted)
103 .cursor_pointer()
104 .hover(|style| style.text_color(cx.app.theme().colors().text))
105 .child(title.clone())
106 .on_mouse_down(MouseButton::Left, |_, _, cx| {
107 cx.stop_propagation();
108 })
109 .on_mouse_down(MouseButton::Right, |_, _, cx| {
110 cx.stop_propagation();
111 })
112 .on_click({
113 move |_event, window, cx| {
114 if let Some(editor) = editor_handle.upgrade() {
115 editor.update(cx, |editor, cx| {
116 editor.change_selections(
117 SelectionEffects::default(),
118 window,
119 cx,
120 |s| {
121 s.select_anchor_ranges([position..position]);
122 },
123 );
124
125 let action = action.clone();
126 if let Some(workspace) = editor.workspace() {
127 if try_handle_client_command(
128 &action, editor, &workspace, window, cx,
129 ) {
130 return;
131 }
132
133 let project = workspace.read(cx).project().clone();
134 let buffer = editor.buffer().clone();
135 if let Some(excerpt_buffer) = buffer.read(cx).as_singleton()
136 {
137 project
138 .update(cx, |project, cx| {
139 project.apply_code_action(
140 excerpt_buffer.clone(),
141 action,
142 true,
143 cx,
144 )
145 })
146 .detach_and_log_err(cx);
147 }
148 }
149 });
150 }
151 }
152 })
153 .into_any_element(),
154 );
155 }
156
157 div()
158 .pl(cx.margins.gutter.full_width())
159 .h_full()
160 .flex()
161 .flex_row()
162 .items_end()
163 .children(children)
164 .into_any_element()
165 }
166}
167
168pub(super) fn try_handle_client_command(
169 action: &CodeAction,
170 editor: &mut Editor,
171 workspace: &gpui::Entity<workspace::Workspace>,
172 window: &mut Window,
173 cx: &mut Context<Editor>,
174) -> bool {
175 let Some(command) = action.lsp_action.command() else {
176 return false;
177 };
178
179 let project = workspace.read(cx).project().clone();
180 let client_command = project
181 .read(cx)
182 .lsp_store()
183 .read(cx)
184 .language_server_adapter_for_id(action.server_id)
185 .and_then(|adapter| adapter.adapter.client_command(&command.command))
186 .or_else(|| default_client_command(&command.command));
187
188 let arguments = command.arguments.as_deref().unwrap_or_default();
189 match client_command {
190 Some(ClientCommand::ScheduleRunnable) => {
191 try_schedule_runnable(arguments, action, editor, workspace, window, cx)
192 }
193 Some(ClientCommand::ShowLocations) => {
194 try_show_references(arguments, action, workspace, window, cx)
195 }
196 None => false,
197 }
198}
199
200fn try_schedule_runnable(
201 arguments: &[serde_json::Value],
202 action: &CodeAction,
203 editor: &Editor,
204 workspace: &gpui::Entity<workspace::Workspace>,
205 window: &mut Window,
206 cx: &mut Context<Editor>,
207) -> bool {
208 let Some(first_arg) = arguments.first() else {
209 return false;
210 };
211 let Ok(runnable) = serde_json::from_value::<lsp_ext_command::Runnable>(first_arg.clone())
212 else {
213 return false;
214 };
215
216 let task_template = lsp_ext_command::runnable_to_task_template(runnable.label, runnable.args);
217 let task_context = TaskContext {
218 cwd: task_template.cwd.as_ref().map(std::path::PathBuf::from),
219 ..TaskContext::default()
220 };
221 let language_name = editor
222 .buffer()
223 .read(cx)
224 .as_singleton()
225 .and_then(|buffer| buffer.read(cx).language())
226 .map(|language| language.name());
227 let task_source_kind = match language_name {
228 Some(language_name) => TaskSourceKind::Lsp {
229 server: action.server_id,
230 language_name: SharedString::from(language_name),
231 },
232 None => TaskSourceKind::AbsPath {
233 id_base: "code-lens".into(),
234 abs_path: task_template
235 .cwd
236 .as_ref()
237 .map(std::path::PathBuf::from)
238 .unwrap_or_default(),
239 },
240 };
241
242 workspace.update(cx, |workspace, cx| {
243 workspace.schedule_task(
244 task_source_kind,
245 &task_template,
246 &task_context,
247 false,
248 window,
249 cx,
250 );
251 });
252 true
253}
254
255fn try_show_references(
256 arguments: &[serde_json::Value],
257 action: &CodeAction,
258 workspace: &gpui::Entity<workspace::Workspace>,
259 window: &mut Window,
260 cx: &mut Context<Editor>,
261) -> bool {
262 if arguments.len() < 3 {
263 return false;
264 }
265 let Ok(locations) = serde_json::from_value::<Vec<lsp::Location>>(arguments[2].clone()) else {
266 return false;
267 };
268 if locations.is_empty() {
269 return false;
270 }
271
272 let title = action.lsp_action.title().to_owned();
273 let server_id = action.server_id;
274 let project = workspace.read(cx).project().clone();
275 let workspace = workspace.clone();
276
277 cx.spawn_in(window, async move |_editor, cx| {
278 let mut buffer_locations: StdHashMap<gpui::Entity<language::Buffer>, Vec<Range<Point>>> =
279 StdHashMap::default();
280
281 for location in &locations {
282 let open_task = cx.update(|_, cx| {
283 project.update(cx, |project, cx| {
284 let uri: lsp::Uri = location.uri.clone();
285 project.open_local_buffer_via_lsp(uri, server_id, cx)
286 })
287 })?;
288 let buffer = open_task.await?;
289
290 let range = range_from_lsp(location.range);
291 buffer_locations.entry(buffer).or_default().push(range);
292 }
293
294 workspace.update_in(cx, |workspace, window, cx| {
295 Editor::open_locations_in_multibuffer(
296 workspace,
297 buffer_locations,
298 title,
299 false,
300 true,
301 MultibufferSelectionMode::First,
302 window,
303 cx,
304 );
305 })?;
306 anyhow::Ok(())
307 })
308 .detach_and_log_err(cx);
309
310 true
311}
312
313fn range_from_lsp(range: lsp::Range) -> Range<Point> {
314 let start = Point::new(range.start.line, range.start.character);
315 let end = Point::new(range.end.line, range.end.character);
316 start..end
317}
318
319impl Editor {
320 pub(super) fn refresh_code_lenses(
321 &mut self,
322 for_buffer: Option<BufferId>,
323 _window: &Window,
324 cx: &mut Context<Self>,
325 ) {
326 if !self.lsp_data_enabled() || self.code_lens.is_none() {
327 return;
328 }
329 let Some(project) = self.project.clone() else {
330 return;
331 };
332
333 let buffers_to_query = self
334 .visible_buffers(cx)
335 .into_iter()
336 .filter(|buffer| self.is_lsp_relevant(buffer.read(cx).file(), cx))
337 .chain(for_buffer.and_then(|buffer_id| self.buffer.read(cx).buffer(buffer_id)))
338 .filter(|editor_buffer| {
339 let editor_buffer_id = editor_buffer.read(cx).remote_id();
340 for_buffer.is_none_or(|buffer_id| buffer_id == editor_buffer_id)
341 && self.registered_buffers.contains_key(&editor_buffer_id)
342 })
343 .unique_by(|buffer| buffer.read(cx).remote_id())
344 .collect::<Vec<_>>();
345
346 if buffers_to_query.is_empty() {
347 return;
348 }
349
350 let project = project.downgrade();
351 self.refresh_code_lens_task = cx.spawn(async move |editor, cx| {
352 cx.background_executor()
353 .timer(LSP_REQUEST_DEBOUNCE_TIMEOUT)
354 .await;
355
356 let Some(tasks) = project
357 .update(cx, |project, cx| {
358 project.lsp_store().update(cx, |lsp_store, cx| {
359 buffers_to_query
360 .into_iter()
361 .map(|buffer| {
362 let buffer_id = buffer.read(cx).remote_id();
363 let task = lsp_store.code_lens_actions(&buffer, cx);
364 async move { (buffer_id, task.await) }
365 })
366 .collect::<Vec<_>>()
367 })
368 })
369 .ok()
370 else {
371 return;
372 };
373
374 let results = join_all(tasks).await;
375 if results.is_empty() {
376 return;
377 }
378
379 let Ok(multi_buffer_snapshot) =
380 editor.update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx))
381 else {
382 return;
383 };
384
385 let mut new_lenses_per_buffer: HashMap<BufferId, Vec<CodeLensLine>> =
386 HashMap::default();
387
388 for (buffer_id, result) in results {
389 match result {
390 Ok(Some(actions)) => {
391 let individual_lenses: Vec<(Anchor, CodeLensItem)> = actions
392 .into_iter()
393 .filter_map(|action| {
394 let title = match &action.lsp_action {
395 project::LspAction::CodeLens(lens) => lens
396 .command
397 .as_ref()
398 .map(|cmd| SharedString::from(&cmd.title)),
399 _ => None,
400 }?;
401 let position =
402 multi_buffer_snapshot.anchor_in_excerpt(action.range.start)?;
403 Some((position, CodeLensItem { title, action }))
404 })
405 .collect();
406
407 let grouped =
408 group_lenses_by_row(individual_lenses, &multi_buffer_snapshot);
409 new_lenses_per_buffer.insert(buffer_id, grouped);
410 }
411 Ok(None) => {}
412 Err(e) => {
413 log::error!("Failed to fetch code lenses for buffer {buffer_id:?}: {e:#}");
414 }
415 }
416 }
417
418 editor
419 .update(cx, |editor, cx| {
420 let code_lens = editor.code_lens.get_or_insert_with(CodeLensState::default);
421
422 let mut blocks_to_remove: HashSet<CustomBlockId> = HashSet::default();
423 for (buffer_id, _) in &new_lenses_per_buffer {
424 if let Some(old_ids) = code_lens.block_ids.remove(buffer_id) {
425 blocks_to_remove.extend(old_ids);
426 }
427 }
428
429 if !blocks_to_remove.is_empty() {
430 editor.remove_blocks(blocks_to_remove, None, cx);
431 }
432
433 let editor_handle = cx.entity().downgrade();
434
435 let mut all_new_blocks: Vec<(BufferId, Vec<BlockProperties<Anchor>>)> =
436 Vec::new();
437 for (buffer_id, lense_lines) in new_lenses_per_buffer {
438 if lense_lines.is_empty() {
439 continue;
440 }
441 let blocks: Vec<BlockProperties<Anchor>> = lense_lines
442 .into_iter()
443 .enumerate()
444 .map(|(line_number, lens_line)| {
445 let position = lens_line.position;
446 let render_fn = render_code_lens_line(
447 line_number,
448 lens_line,
449 editor_handle.clone(),
450 );
451 BlockProperties {
452 placement: BlockPlacement::Above(position),
453 height: Some(1),
454 style: BlockStyle::Flex,
455 render: Arc::new(render_fn),
456 priority: 0,
457 }
458 })
459 .collect();
460 all_new_blocks.push((buffer_id, blocks));
461 }
462
463 for (buffer_id, blocks) in all_new_blocks {
464 let block_ids = editor.insert_blocks(blocks, None, cx);
465 editor
466 .code_lens
467 .get_or_insert_with(CodeLensState::default)
468 .block_ids
469 .insert(buffer_id, block_ids);
470 }
471
472 cx.notify();
473 })
474 .ok();
475 });
476 }
477
478 pub fn supports_code_lens(&self, cx: &ui::App) -> bool {
479 let Some(project) = self.project.as_ref() else {
480 return false;
481 };
482 let lsp_store = project.read(cx).lsp_store().read(cx);
483 lsp_store
484 .lsp_server_capabilities
485 .values()
486 .any(|caps| caps.code_lens_provider.is_some())
487 }
488
489 pub fn code_lens_enabled(&self) -> bool {
490 self.code_lens.is_some()
491 }
492
493 pub fn toggle_code_lens_action(
494 &mut self,
495 _: &ToggleCodeLens,
496 window: &mut Window,
497 cx: &mut Context<Self>,
498 ) {
499 let currently_enabled = self.code_lens.is_some();
500 self.toggle_code_lens(!currently_enabled, window, cx);
501 }
502
503 pub(super) fn toggle_code_lens(
504 &mut self,
505 enabled: bool,
506 window: &mut Window,
507 cx: &mut Context<Self>,
508 ) {
509 if enabled {
510 self.code_lens.get_or_insert_with(CodeLensState::default);
511 self.refresh_code_lenses(None, window, cx);
512 } else {
513 self.clear_code_lenses(cx);
514 }
515 }
516
517 pub(super) fn clear_code_lenses(&mut self, cx: &mut Context<Self>) {
518 if let Some(code_lens) = self.code_lens.take() {
519 let all_blocks = code_lens.all_block_ids();
520 if !all_blocks.is_empty() {
521 self.remove_blocks(all_blocks, None, cx);
522 }
523 cx.notify();
524 }
525 self.refresh_code_lens_task = Task::ready(());
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use futures::StreamExt;
532 use gpui::TestAppContext;
533
534 use settings::CodeLens;
535
536 use crate::{
537 editor_tests::{init_test, update_test_editor_settings},
538 test::editor_lsp_test_context::EditorLspTestContext,
539 };
540
541 #[gpui::test]
542 async fn test_code_lens_blocks(cx: &mut TestAppContext) {
543 init_test(cx, |_| {});
544 update_test_editor_settings(cx, &|settings| {
545 settings.code_lens = Some(CodeLens::On);
546 });
547
548 let mut cx = EditorLspTestContext::new_typescript(
549 lsp::ServerCapabilities {
550 code_lens_provider: Some(lsp::CodeLensOptions {
551 resolve_provider: None,
552 }),
553 execute_command_provider: Some(lsp::ExecuteCommandOptions {
554 commands: vec!["lens_cmd".to_string()],
555 ..lsp::ExecuteCommandOptions::default()
556 }),
557 ..lsp::ServerCapabilities::default()
558 },
559 cx,
560 )
561 .await;
562
563 let mut code_lens_request =
564 cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
565 Ok(Some(vec![
566 lsp::CodeLens {
567 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
568 command: Some(lsp::Command {
569 title: "2 references".to_owned(),
570 command: "lens_cmd".to_owned(),
571 arguments: None,
572 }),
573 data: None,
574 },
575 lsp::CodeLens {
576 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 19)),
577 command: Some(lsp::Command {
578 title: "0 references".to_owned(),
579 command: "lens_cmd".to_owned(),
580 arguments: None,
581 }),
582 data: None,
583 },
584 ]))
585 });
586
587 cx.set_state("ˇfunction hello() {}\nfunction world() {}");
588
589 assert!(
590 code_lens_request.next().await.is_some(),
591 "should have received a code lens request"
592 );
593 cx.run_until_parked();
594
595 cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
596 assert_eq!(
597 editor.code_lens_enabled(),
598 true,
599 "code lens should be enabled"
600 );
601 let total_blocks: usize = editor
602 .code_lens
603 .as_ref()
604 .map(|s| s.block_ids.values().map(|v| v.len()).sum())
605 .unwrap_or(0);
606 assert_eq!(total_blocks, 2, "Should have inserted two code lens blocks");
607 });
608 }
609
610 #[gpui::test]
611 async fn test_code_lens_disabled_by_default(cx: &mut TestAppContext) {
612 init_test(cx, |_| {});
613
614 let mut cx = EditorLspTestContext::new_typescript(
615 lsp::ServerCapabilities {
616 code_lens_provider: Some(lsp::CodeLensOptions {
617 resolve_provider: None,
618 }),
619 execute_command_provider: Some(lsp::ExecuteCommandOptions {
620 commands: vec!["lens_cmd".to_string()],
621 ..lsp::ExecuteCommandOptions::default()
622 }),
623 ..lsp::ServerCapabilities::default()
624 },
625 cx,
626 )
627 .await;
628
629 cx.lsp
630 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
631 panic!("Should not request code lenses when disabled");
632 });
633
634 cx.set_state("ˇfunction hello() {}");
635 cx.run_until_parked();
636
637 cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
638 assert_eq!(
639 editor.code_lens_enabled(),
640 false,
641 "code lens should not be enabled when setting is off"
642 );
643 });
644 }
645
646 #[gpui::test]
647 async fn test_code_lens_toggling(cx: &mut TestAppContext) {
648 init_test(cx, |_| {});
649 update_test_editor_settings(cx, &|settings| {
650 settings.code_lens = Some(CodeLens::On);
651 });
652
653 let mut cx = EditorLspTestContext::new_typescript(
654 lsp::ServerCapabilities {
655 code_lens_provider: Some(lsp::CodeLensOptions {
656 resolve_provider: None,
657 }),
658 execute_command_provider: Some(lsp::ExecuteCommandOptions {
659 commands: vec!["lens_cmd".to_string()],
660 ..lsp::ExecuteCommandOptions::default()
661 }),
662 ..lsp::ServerCapabilities::default()
663 },
664 cx,
665 )
666 .await;
667
668 let mut code_lens_request =
669 cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
670 Ok(Some(vec![lsp::CodeLens {
671 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
672 command: Some(lsp::Command {
673 title: "1 reference".to_owned(),
674 command: "lens_cmd".to_owned(),
675 arguments: None,
676 }),
677 data: None,
678 }]))
679 });
680
681 cx.set_state("ˇfunction hello() {}");
682
683 assert!(
684 code_lens_request.next().await.is_some(),
685 "should have received a code lens request"
686 );
687 cx.run_until_parked();
688
689 cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
690 assert_eq!(
691 editor.code_lens_enabled(),
692 true,
693 "code lens should be enabled"
694 );
695 let total_blocks: usize = editor
696 .code_lens
697 .as_ref()
698 .map(|s| s.block_ids.values().map(|v| v.len()).sum())
699 .unwrap_or(0);
700 assert_eq!(total_blocks, 1, "Should have one code lens block");
701 });
702
703 cx.update_editor(|editor, _window, cx| {
704 editor.clear_code_lenses(cx);
705 });
706
707 cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
708 assert_eq!(
709 editor.code_lens_enabled(),
710 false,
711 "code lens should be disabled after clearing"
712 );
713 });
714 }
715
716 #[gpui::test]
717 async fn test_code_lens_resolve(cx: &mut TestAppContext) {
718 init_test(cx, |_| {});
719 update_test_editor_settings(cx, &|settings| {
720 settings.code_lens = Some(CodeLens::On);
721 });
722
723 let mut cx = EditorLspTestContext::new_typescript(
724 lsp::ServerCapabilities {
725 code_lens_provider: Some(lsp::CodeLensOptions {
726 resolve_provider: Some(true),
727 }),
728 ..lsp::ServerCapabilities::default()
729 },
730 cx,
731 )
732 .await;
733
734 let mut code_lens_request =
735 cx.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _, _| async {
736 Ok(Some(vec![
737 lsp::CodeLens {
738 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 19)),
739 command: None,
740 data: Some(serde_json::json!({"id": "lens_1"})),
741 },
742 lsp::CodeLens {
743 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 19)),
744 command: None,
745 data: Some(serde_json::json!({"id": "lens_2"})),
746 },
747 ]))
748 });
749
750 cx.lsp
751 .set_request_handler::<lsp::request::CodeLensResolve, _, _>(|lens, _| async move {
752 let id = lens
753 .data
754 .as_ref()
755 .and_then(|d| d.get("id"))
756 .and_then(|v| v.as_str())
757 .unwrap_or("unknown");
758 let title = match id {
759 "lens_1" => "3 references",
760 "lens_2" => "1 implementation",
761 _ => "unknown",
762 };
763 Ok(lsp::CodeLens {
764 command: Some(lsp::Command {
765 title: title.to_owned(),
766 command: format!("resolved_{id}"),
767 arguments: None,
768 }),
769 ..lens
770 })
771 });
772
773 cx.set_state("ˇfunction hello() {}\nfunction world() {}");
774
775 assert!(
776 code_lens_request.next().await.is_some(),
777 "should have received a code lens request"
778 );
779 cx.run_until_parked();
780
781 cx.editor.read_with(&cx.cx.cx, |editor, _cx| {
782 let total_blocks: usize = editor
783 .code_lens
784 .as_ref()
785 .map(|s| s.block_ids.values().map(|v| v.len()).sum())
786 .unwrap_or(0);
787 assert_eq!(
788 total_blocks, 2,
789 "Unresolved lenses should have been resolved and displayed"
790 );
791 });
792 }
793}