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, 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}