1use std::ops::Range;
2use std::{
3 cmp::{self, Reverse},
4 sync::Arc,
5};
6
7use editor::scroll::ScrollOffset;
8use editor::{Anchor, AnchorRangeExt, Editor, scroll::Autoscroll};
9use editor::{MultiBufferOffset, RowHighlightOptions, SelectionEffects};
10use fuzzy::StringMatch;
11use gpui::{
12 App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle,
13 ParentElement, Point, Render, Styled, StyledText, Task, TextStyle, WeakEntity, Window, div,
14 rems,
15};
16use language::{Outline, OutlineItem};
17use ordered_float::OrderedFloat;
18use picker::{Picker, PickerDelegate};
19use settings::Settings;
20use theme::{ActiveTheme, ThemeSettings};
21use ui::{ListItem, ListItemSpacing, prelude::*};
22use util::ResultExt;
23use workspace::{DismissDecision, ModalView};
24
25pub fn init(cx: &mut App) {
26 cx.observe_new(OutlineView::register).detach();
27 zed_actions::outline::TOGGLE_OUTLINE
28 .set(|view, window, cx| {
29 let Ok(editor) = view.downcast::<Editor>() else {
30 return;
31 };
32
33 toggle(editor, &Default::default(), window, cx);
34 })
35 .ok();
36}
37
38pub fn toggle(
39 editor: Entity<Editor>,
40 _: &zed_actions::outline::ToggleOutline,
41 window: &mut Window,
42 cx: &mut App,
43) {
44 let Some(workspace) = editor.read(cx).workspace() else {
45 return;
46 };
47 if workspace.read(cx).active_modal::<OutlineView>(cx).is_some() {
48 workspace.update(cx, |workspace, cx| {
49 workspace.toggle_modal(window, cx, |window, cx| {
50 OutlineView::new(Outline::new(Vec::new()), editor.clone(), window, cx)
51 });
52 });
53 return;
54 }
55
56 let Some(task) = outline_for_editor(&editor, cx) else {
57 return;
58 };
59 let editor = editor.clone();
60 window
61 .spawn(cx, async move |cx| {
62 let items = task.await;
63 if items.is_empty() {
64 return;
65 }
66 cx.update(|window, cx| {
67 let outline = Outline::new(items);
68 workspace.update(cx, |workspace, cx| {
69 workspace.toggle_modal(window, cx, |window, cx| {
70 OutlineView::new(outline, editor, window, cx)
71 });
72 });
73 })
74 .ok();
75 })
76 .detach();
77}
78
79fn outline_for_editor(
80 editor: &Entity<Editor>,
81 cx: &mut App,
82) -> Option<Task<Vec<OutlineItem<Anchor>>>> {
83 let multibuffer = editor.read(cx).buffer().read(cx).snapshot(cx);
84 let (excerpt_id, _, buffer_snapshot) = multibuffer.as_singleton()?;
85 let buffer_id = buffer_snapshot.remote_id();
86 let task = editor.update(cx, |editor, cx| editor.buffer_outline_items(buffer_id, cx));
87
88 Some(cx.background_executor().spawn(async move {
89 task.await
90 .into_iter()
91 .map(|item| OutlineItem {
92 depth: item.depth,
93 range: Anchor::range_in_buffer(excerpt_id, item.range),
94 source_range_for_text: Anchor::range_in_buffer(
95 excerpt_id,
96 item.source_range_for_text,
97 ),
98 text: item.text,
99 highlight_ranges: item.highlight_ranges,
100 name_ranges: item.name_ranges,
101 body_range: item
102 .body_range
103 .map(|r| Anchor::range_in_buffer(excerpt_id, r)),
104 annotation_range: item
105 .annotation_range
106 .map(|r| Anchor::range_in_buffer(excerpt_id, r)),
107 })
108 .collect()
109 }))
110}
111
112pub struct OutlineView {
113 picker: Entity<Picker<OutlineViewDelegate>>,
114}
115
116impl Focusable for OutlineView {
117 fn focus_handle(&self, cx: &App) -> FocusHandle {
118 self.picker.focus_handle(cx)
119 }
120}
121
122impl EventEmitter<DismissEvent> for OutlineView {}
123impl ModalView for OutlineView {
124 fn on_before_dismiss(
125 &mut self,
126 window: &mut Window,
127 cx: &mut Context<Self>,
128 ) -> DismissDecision {
129 self.picker.update(cx, |picker, cx| {
130 picker.delegate.restore_active_editor(window, cx)
131 });
132 DismissDecision::Dismiss(true)
133 }
134}
135
136impl Render for OutlineView {
137 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
138 v_flex()
139 .w(rems(34.))
140 .on_action(cx.listener(
141 |_this: &mut OutlineView,
142 _: &zed_actions::outline::ToggleOutline,
143 _window: &mut Window,
144 cx: &mut Context<OutlineView>| {
145 // When outline::Toggle is triggered while the outline is open, dismiss it
146 cx.emit(DismissEvent);
147 },
148 ))
149 .child(self.picker.clone())
150 }
151}
152
153impl OutlineView {
154 fn register(editor: &mut Editor, _: Option<&mut Window>, cx: &mut Context<Editor>) {
155 if editor.mode().is_full() {
156 let handle = cx.entity().downgrade();
157 editor
158 .register_action(move |action, window, cx| {
159 if let Some(editor) = handle.upgrade() {
160 toggle(editor, action, window, cx);
161 }
162 })
163 .detach();
164 }
165 }
166
167 fn new(
168 outline: Outline<Anchor>,
169 editor: Entity<Editor>,
170 window: &mut Window,
171 cx: &mut Context<Self>,
172 ) -> OutlineView {
173 let delegate = OutlineViewDelegate::new(cx.entity().downgrade(), outline, editor, cx);
174 let picker = cx.new(|cx| {
175 Picker::uniform_list(delegate, window, cx)
176 .max_height(Some(vh(0.75, window)))
177 .show_scrollbar(true)
178 });
179 OutlineView { picker }
180 }
181}
182
183struct OutlineViewDelegate {
184 outline_view: WeakEntity<OutlineView>,
185 active_editor: Entity<Editor>,
186 outline: Outline<Anchor>,
187 selected_match_index: usize,
188 prev_scroll_position: Option<Point<ScrollOffset>>,
189 matches: Vec<StringMatch>,
190 last_query: String,
191}
192
193enum OutlineRowHighlights {}
194
195impl OutlineViewDelegate {
196 fn new(
197 outline_view: WeakEntity<OutlineView>,
198 outline: Outline<Anchor>,
199 editor: Entity<Editor>,
200
201 cx: &mut Context<OutlineView>,
202 ) -> Self {
203 Self {
204 outline_view,
205 last_query: Default::default(),
206 matches: Default::default(),
207 selected_match_index: 0,
208 prev_scroll_position: Some(editor.update(cx, |editor, cx| editor.scroll_position(cx))),
209 active_editor: editor,
210 outline,
211 }
212 }
213
214 fn restore_active_editor(&mut self, window: &mut Window, cx: &mut App) {
215 self.active_editor.update(cx, |editor, cx| {
216 editor.clear_row_highlights::<OutlineRowHighlights>();
217 if let Some(scroll_position) = self.prev_scroll_position {
218 editor.set_scroll_position(scroll_position, window, cx);
219 }
220 })
221 }
222
223 fn set_selected_index(
224 &mut self,
225 ix: usize,
226 navigate: bool,
227
228 cx: &mut Context<Picker<OutlineViewDelegate>>,
229 ) {
230 self.selected_match_index = ix;
231
232 if navigate && !self.matches.is_empty() {
233 let selected_match = &self.matches[self.selected_match_index];
234 let outline_item = &self.outline.items[selected_match.candidate_id];
235
236 self.active_editor.update(cx, |active_editor, cx| {
237 active_editor.clear_row_highlights::<OutlineRowHighlights>();
238 active_editor.highlight_rows::<OutlineRowHighlights>(
239 outline_item.range.start..outline_item.range.end,
240 cx.theme().colors().editor_highlighted_line_background,
241 RowHighlightOptions {
242 autoscroll: true,
243 ..Default::default()
244 },
245 cx,
246 );
247 active_editor.request_autoscroll(Autoscroll::center(), cx);
248 });
249 }
250 }
251}
252
253impl PickerDelegate for OutlineViewDelegate {
254 type ListItem = ListItem;
255
256 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
257 "Search buffer symbols...".into()
258 }
259
260 fn match_count(&self) -> usize {
261 self.matches.len()
262 }
263
264 fn selected_index(&self) -> usize {
265 self.selected_match_index
266 }
267
268 fn set_selected_index(
269 &mut self,
270 ix: usize,
271 _: &mut Window,
272 cx: &mut Context<Picker<OutlineViewDelegate>>,
273 ) {
274 self.set_selected_index(ix, true, cx);
275 }
276
277 fn update_matches(
278 &mut self,
279 query: String,
280 window: &mut Window,
281 cx: &mut Context<Picker<OutlineViewDelegate>>,
282 ) -> Task<()> {
283 let selected_index;
284 if query.is_empty() {
285 self.restore_active_editor(window, cx);
286 self.matches = self
287 .outline
288 .items
289 .iter()
290 .enumerate()
291 .map(|(index, _)| StringMatch {
292 candidate_id: index,
293 score: Default::default(),
294 positions: Default::default(),
295 string: Default::default(),
296 })
297 .collect();
298
299 let (buffer, cursor_offset) = self.active_editor.update(cx, |editor, cx| {
300 let buffer = editor.buffer().read(cx).snapshot(cx);
301 let cursor_offset = editor
302 .selections
303 .newest::<MultiBufferOffset>(&editor.display_snapshot(cx))
304 .head();
305 (buffer, cursor_offset)
306 });
307 selected_index = self
308 .outline
309 .items
310 .iter()
311 .enumerate()
312 .map(|(ix, item)| {
313 let range = item.range.to_offset(&buffer);
314 let distance_to_closest_endpoint = cmp::min(
315 (range.start.0 as isize - cursor_offset.0 as isize).abs(),
316 (range.end.0 as isize - cursor_offset.0 as isize).abs(),
317 );
318 let depth = if range.contains(&cursor_offset) {
319 Some(item.depth)
320 } else {
321 None
322 };
323 (ix, depth, distance_to_closest_endpoint)
324 })
325 .max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance)))
326 .map(|(ix, _, _)| ix)
327 .unwrap_or(0);
328 } else {
329 self.matches = smol::block_on(
330 self.outline
331 .search(&query, cx.background_executor().clone()),
332 );
333 selected_index = self
334 .matches
335 .iter()
336 .enumerate()
337 .max_by_key(|(_, m)| OrderedFloat(m.score))
338 .map(|(ix, _)| ix)
339 .unwrap_or(0);
340 }
341 self.last_query = query;
342 self.set_selected_index(selected_index, !self.last_query.is_empty(), cx);
343 Task::ready(())
344 }
345
346 fn confirm(
347 &mut self,
348 _: bool,
349 window: &mut Window,
350 cx: &mut Context<Picker<OutlineViewDelegate>>,
351 ) {
352 self.prev_scroll_position.take();
353 self.set_selected_index(self.selected_match_index, true, cx);
354
355 self.active_editor.update(cx, |active_editor, cx| {
356 let highlight = active_editor
357 .highlighted_rows::<OutlineRowHighlights>()
358 .next();
359 if let Some((rows, _)) = highlight {
360 active_editor.change_selections(
361 SelectionEffects::scroll(Autoscroll::center()),
362 window,
363 cx,
364 |s| s.select_ranges([rows.start..rows.start]),
365 );
366 active_editor.clear_row_highlights::<OutlineRowHighlights>();
367 window.focus(&active_editor.focus_handle(cx), cx);
368 }
369 });
370
371 self.dismissed(window, cx);
372 }
373
374 fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<OutlineViewDelegate>>) {
375 self.outline_view
376 .update(cx, |_, cx| cx.emit(DismissEvent))
377 .log_err();
378 self.restore_active_editor(window, cx);
379 }
380
381 fn render_match(
382 &self,
383 ix: usize,
384 selected: bool,
385 _: &mut Window,
386 cx: &mut Context<Picker<Self>>,
387 ) -> Option<Self::ListItem> {
388 let mat = self.matches.get(ix)?;
389 let outline_item = self.outline.items.get(mat.candidate_id)?;
390
391 Some(
392 ListItem::new(ix)
393 .inset(true)
394 .spacing(ListItemSpacing::Sparse)
395 .toggle_state(selected)
396 .child(
397 div()
398 .text_ui(cx)
399 .pl(rems(outline_item.depth as f32))
400 .child(render_item(outline_item, mat.ranges(), cx)),
401 ),
402 )
403 }
404}
405
406pub fn render_item<T>(
407 outline_item: &OutlineItem<T>,
408 match_ranges: impl IntoIterator<Item = Range<usize>>,
409 cx: &App,
410) -> StyledText {
411 let highlight_style = HighlightStyle {
412 background_color: Some(cx.theme().colors().text_accent.alpha(0.3)),
413 ..Default::default()
414 };
415 let custom_highlights = match_ranges
416 .into_iter()
417 .map(|range| (range, highlight_style));
418
419 let settings = ThemeSettings::get_global(cx);
420
421 // TODO: We probably shouldn't need to build a whole new text style here
422 // but I'm not sure how to get the current one and modify it.
423 // Before this change TextStyle::default() was used here, which was giving us the wrong font and text color.
424 let text_style = TextStyle {
425 color: cx.theme().colors().text,
426 font_family: settings.buffer_font.family.clone(),
427 font_features: settings.buffer_font.features.clone(),
428 font_fallbacks: settings.buffer_font.fallbacks.clone(),
429 font_size: settings.buffer_font_size(cx).into(),
430 font_weight: settings.buffer_font.weight,
431 line_height: relative(1.),
432 ..Default::default()
433 };
434 let highlights = gpui::combine_highlights(
435 custom_highlights,
436 outline_item.highlight_ranges.iter().cloned(),
437 );
438
439 StyledText::new(outline_item.text.clone()).with_default_highlights(&text_style, highlights)
440}
441
442#[cfg(test)]
443mod tests {
444 use std::time::Duration;
445
446 use super::*;
447 use gpui::{TestAppContext, UpdateGlobal, VisualTestContext};
448 use indoc::indoc;
449 use language::FakeLspAdapter;
450 use project::{FakeFs, Project};
451 use serde_json::json;
452 use settings::SettingsStore;
453 use smol::stream::StreamExt as _;
454 use util::{path, rel_path::rel_path};
455 use workspace::{AppState, MultiWorkspace, Workspace};
456
457 #[gpui::test]
458 async fn test_outline_view_row_highlights(cx: &mut TestAppContext) {
459 init_test(cx);
460 let fs = FakeFs::new(cx.executor());
461 fs.insert_tree(
462 path!("/dir"),
463 json!({
464 "a.rs": indoc!{"
465 // display line 0
466 struct SingleLine; // display line 1
467 // display line 2
468 struct MultiLine { // display line 3
469 field_1: i32, // display line 4
470 field_2: i32, // display line 5
471 } // display line 6
472 "}
473 }),
474 )
475 .await;
476
477 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
478 project.read_with(cx, |project, _| {
479 project.languages().add(language::rust_lang())
480 });
481
482 let (workspace, cx) =
483 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
484
485 let workspace = cx.read(|cx| workspace.read(cx).workspace().clone());
486 let worktree_id = workspace.update(cx, |workspace, cx| {
487 workspace.project().update(cx, |project, cx| {
488 project.worktrees(cx).next().unwrap().read(cx).id()
489 })
490 });
491 let _buffer = project
492 .update(cx, |project, cx| {
493 project.open_local_buffer(path!("/dir/a.rs"), cx)
494 })
495 .await
496 .unwrap();
497 let editor = workspace
498 .update_in(cx, |workspace, window, cx| {
499 workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
500 })
501 .await
502 .unwrap()
503 .downcast::<Editor>()
504 .unwrap();
505 let ensure_outline_view_contents =
506 |outline_view: &Entity<Picker<OutlineViewDelegate>>, cx: &mut VisualTestContext| {
507 assert_eq!(query(outline_view, cx), "");
508 assert_eq!(
509 outline_names(outline_view, cx),
510 vec![
511 "struct SingleLine",
512 "struct MultiLine",
513 "field_1",
514 "field_2"
515 ],
516 );
517 };
518
519 let outline_view = open_outline_view(&workspace, cx);
520 ensure_outline_view_contents(&outline_view, cx);
521 assert_eq!(
522 highlighted_display_rows(&editor, cx),
523 Vec::<u32>::new(),
524 "Initially opened outline view should have no highlights"
525 );
526 assert_single_caret_at_row(&editor, 0, cx);
527
528 cx.dispatch_action(menu::Confirm);
529 // Ensures that outline still goes to entry even if no queries have been made
530 assert_single_caret_at_row(&editor, 1, cx);
531
532 let outline_view = open_outline_view(&workspace, cx);
533
534 cx.dispatch_action(menu::SelectNext);
535 ensure_outline_view_contents(&outline_view, cx);
536 assert_eq!(
537 highlighted_display_rows(&editor, cx),
538 vec![3, 4, 5, 6],
539 "Second struct's rows should be highlighted"
540 );
541 assert_single_caret_at_row(&editor, 1, cx);
542
543 cx.dispatch_action(menu::SelectPrevious);
544 ensure_outline_view_contents(&outline_view, cx);
545 assert_eq!(
546 highlighted_display_rows(&editor, cx),
547 vec![1],
548 "First struct's row should be highlighted"
549 );
550 assert_single_caret_at_row(&editor, 1, cx);
551
552 cx.dispatch_action(menu::Cancel);
553 ensure_outline_view_contents(&outline_view, cx);
554 assert_eq!(
555 highlighted_display_rows(&editor, cx),
556 Vec::<u32>::new(),
557 "No rows should be highlighted after outline view is cancelled and closed"
558 );
559 assert_single_caret_at_row(&editor, 1, cx);
560
561 let outline_view = open_outline_view(&workspace, cx);
562 ensure_outline_view_contents(&outline_view, cx);
563 assert_eq!(
564 highlighted_display_rows(&editor, cx),
565 Vec::<u32>::new(),
566 "Reopened outline view should have no highlights"
567 );
568 assert_single_caret_at_row(&editor, 1, cx);
569
570 let expected_first_highlighted_row = 3;
571 cx.dispatch_action(menu::SelectNext);
572 ensure_outline_view_contents(&outline_view, cx);
573 assert_eq!(
574 highlighted_display_rows(&editor, cx),
575 vec![expected_first_highlighted_row, 4, 5, 6]
576 );
577 assert_single_caret_at_row(&editor, 1, cx);
578 cx.dispatch_action(menu::Confirm);
579 ensure_outline_view_contents(&outline_view, cx);
580 assert_eq!(
581 highlighted_display_rows(&editor, cx),
582 Vec::<u32>::new(),
583 "No rows should be highlighted after outline view is confirmed and closed"
584 );
585 // On confirm, should place the caret on the first row of the highlighted rows range.
586 assert_single_caret_at_row(&editor, expected_first_highlighted_row, cx);
587 }
588
589 fn open_outline_view(
590 workspace: &Entity<Workspace>,
591 cx: &mut VisualTestContext,
592 ) -> Entity<Picker<OutlineViewDelegate>> {
593 cx.dispatch_action(zed_actions::outline::ToggleOutline);
594 cx.executor().advance_clock(Duration::from_millis(200));
595 workspace.update(cx, |workspace, cx| {
596 workspace
597 .active_modal::<OutlineView>(cx)
598 .unwrap()
599 .read(cx)
600 .picker
601 .clone()
602 })
603 }
604
605 fn query(
606 outline_view: &Entity<Picker<OutlineViewDelegate>>,
607 cx: &mut VisualTestContext,
608 ) -> String {
609 outline_view.update(cx, |outline_view, cx| outline_view.query(cx))
610 }
611
612 fn outline_names(
613 outline_view: &Entity<Picker<OutlineViewDelegate>>,
614 cx: &mut VisualTestContext,
615 ) -> Vec<String> {
616 outline_view.read_with(cx, |outline_view, _| {
617 let items = &outline_view.delegate.outline.items;
618 outline_view
619 .delegate
620 .matches
621 .iter()
622 .map(|hit| items[hit.candidate_id].text.clone())
623 .collect::<Vec<_>>()
624 })
625 }
626
627 fn highlighted_display_rows(editor: &Entity<Editor>, cx: &mut VisualTestContext) -> Vec<u32> {
628 editor.update_in(cx, |editor, window, cx| {
629 editor
630 .highlighted_display_rows(window, cx)
631 .into_keys()
632 .map(|r| r.0)
633 .collect()
634 })
635 }
636
637 fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
638 cx.update(|cx| {
639 let state = AppState::test(cx);
640 crate::init(cx);
641 editor::init(cx);
642 state
643 })
644 }
645
646 #[gpui::test]
647 async fn test_outline_modal_lsp_document_symbols(cx: &mut TestAppContext) {
648 init_test(cx);
649
650 let fs = FakeFs::new(cx.executor());
651 fs.insert_tree(
652 path!("/dir"),
653 json!({
654 "a.rs": indoc!{"
655 struct Foo {
656 bar: u32,
657 baz: String,
658 }
659 "}
660 }),
661 )
662 .await;
663
664 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
665 let language_registry = project.read_with(cx, |project, _| {
666 project.languages().add(language::rust_lang());
667 project.languages().clone()
668 });
669
670 let mut fake_language_servers = language_registry.register_fake_lsp(
671 "Rust",
672 FakeLspAdapter {
673 capabilities: lsp::ServerCapabilities {
674 document_symbol_provider: Some(lsp::OneOf::Left(true)),
675 ..lsp::ServerCapabilities::default()
676 },
677 initializer: Some(Box::new(|fake_language_server| {
678 #[allow(deprecated)]
679 fake_language_server
680 .set_request_handler::<lsp::request::DocumentSymbolRequest, _, _>(
681 move |_, _| async move {
682 Ok(Some(lsp::DocumentSymbolResponse::Nested(vec![
683 lsp::DocumentSymbol {
684 name: "Foo".to_string(),
685 detail: None,
686 kind: lsp::SymbolKind::STRUCT,
687 tags: None,
688 deprecated: None,
689 range: lsp::Range::new(
690 lsp::Position::new(0, 0),
691 lsp::Position::new(3, 1),
692 ),
693 selection_range: lsp::Range::new(
694 lsp::Position::new(0, 7),
695 lsp::Position::new(0, 10),
696 ),
697 children: Some(vec![
698 lsp::DocumentSymbol {
699 name: "bar".to_string(),
700 detail: None,
701 kind: lsp::SymbolKind::FIELD,
702 tags: None,
703 deprecated: None,
704 range: lsp::Range::new(
705 lsp::Position::new(1, 4),
706 lsp::Position::new(1, 13),
707 ),
708 selection_range: lsp::Range::new(
709 lsp::Position::new(1, 4),
710 lsp::Position::new(1, 7),
711 ),
712 children: None,
713 },
714 lsp::DocumentSymbol {
715 name: "lsp_only_field".to_string(),
716 detail: None,
717 kind: lsp::SymbolKind::FIELD,
718 tags: None,
719 deprecated: None,
720 range: lsp::Range::new(
721 lsp::Position::new(2, 4),
722 lsp::Position::new(2, 15),
723 ),
724 selection_range: lsp::Range::new(
725 lsp::Position::new(2, 4),
726 lsp::Position::new(2, 7),
727 ),
728 children: None,
729 },
730 ]),
731 },
732 ])))
733 },
734 );
735 })),
736 ..FakeLspAdapter::default()
737 },
738 );
739
740 let (multi_workspace, cx) =
741 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
742 let workspace = cx.read(|cx| multi_workspace.read(cx).workspace().clone());
743 let worktree_id = workspace.update(cx, |workspace, cx| {
744 workspace.project().update(cx, |project, cx| {
745 project.worktrees(cx).next().unwrap().read(cx).id()
746 })
747 });
748 let _buffer = project
749 .update(cx, |project, cx| {
750 project.open_local_buffer(path!("/dir/a.rs"), cx)
751 })
752 .await
753 .unwrap();
754 let editor = workspace
755 .update_in(cx, |workspace, window, cx| {
756 workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
757 })
758 .await
759 .unwrap()
760 .downcast::<Editor>()
761 .unwrap();
762
763 let _fake_language_server = fake_language_servers.next().await.unwrap();
764 cx.run_until_parked();
765
766 // Step 1: tree-sitter outlines by default
767 let outline_view = open_outline_view(&workspace, cx);
768 let tree_sitter_names = outline_names(&outline_view, cx);
769 assert_eq!(
770 tree_sitter_names,
771 vec!["struct Foo", "bar", "baz"],
772 "Step 1: tree-sitter outlines should be displayed by default"
773 );
774 cx.dispatch_action(menu::Cancel);
775 cx.run_until_parked();
776
777 // Step 2: Switch to LSP document symbols
778 cx.update(|_, cx| {
779 SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
780 store.update_user_settings(cx, |settings| {
781 settings.project.all_languages.defaults.document_symbols =
782 Some(settings::DocumentSymbols::On);
783 });
784 });
785 });
786 let outline_view = open_outline_view(&workspace, cx);
787 let lsp_names = outline_names(&outline_view, cx);
788 assert_eq!(
789 lsp_names,
790 vec!["struct Foo", "bar", "lsp_only_field"],
791 "Step 2: LSP-provided symbols should be displayed"
792 );
793 assert_eq!(
794 highlighted_display_rows(&editor, cx),
795 Vec::<u32>::new(),
796 "Step 2: initially opened outline view should have no highlights"
797 );
798 assert_single_caret_at_row(&editor, 0, cx);
799
800 cx.dispatch_action(menu::SelectNext);
801 assert_eq!(
802 highlighted_display_rows(&editor, cx),
803 vec![1],
804 "Step 2: bar's row should be highlighted after SelectNext"
805 );
806 assert_single_caret_at_row(&editor, 0, cx);
807
808 cx.dispatch_action(menu::Confirm);
809 cx.run_until_parked();
810 assert_single_caret_at_row(&editor, 1, cx);
811
812 // Step 3: Switch back to tree-sitter
813 cx.update(|_, cx| {
814 SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
815 store.update_user_settings(cx, |settings| {
816 settings.project.all_languages.defaults.document_symbols =
817 Some(settings::DocumentSymbols::Off);
818 });
819 });
820 });
821
822 let outline_view = open_outline_view(&workspace, cx);
823 let restored_names = outline_names(&outline_view, cx);
824 assert_eq!(
825 restored_names,
826 vec!["struct Foo", "bar", "baz"],
827 "Step 3: tree-sitter outlines should be restored after switching back"
828 );
829 }
830
831 #[track_caller]
832 fn assert_single_caret_at_row(
833 editor: &Entity<Editor>,
834 buffer_row: u32,
835 cx: &mut VisualTestContext,
836 ) {
837 let selections = editor.update(cx, |editor, cx| {
838 editor
839 .selections
840 .all::<rope::Point>(&editor.display_snapshot(cx))
841 .into_iter()
842 .map(|s| s.start..s.end)
843 .collect::<Vec<_>>()
844 });
845 assert!(
846 selections.len() == 1,
847 "Expected one caret selection but got: {selections:?}"
848 );
849 let selection = &selections[0];
850 assert!(
851 selection.start == selection.end,
852 "Expected a single caret selection, but got: {selection:?}"
853 );
854 assert_eq!(selection.start.row, buffer_row);
855 }
856}