outline.rs

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