@@ -6164,6 +6164,26 @@ dependencies = [
"workspace",
]
+[[package]]
+name = "outline2"
+version = "0.1.0"
+dependencies = [
+ "editor2",
+ "fuzzy2",
+ "gpui2",
+ "language2",
+ "ordered-float 2.10.0",
+ "picker2",
+ "postage",
+ "settings2",
+ "smol",
+ "text2",
+ "theme2",
+ "ui2",
+ "util",
+ "workspace2",
+]
+
[[package]]
name = "overload"
version = "0.1.1"
@@ -11818,6 +11838,7 @@ dependencies = [
"menu2",
"node_runtime",
"num_cpus",
+ "outline2",
"parking_lot 0.11.2",
"postage",
"project2",
@@ -0,0 +1,276 @@
+use editor::{
+ display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, AnchorRangeExt,
+ DisplayPoint, Editor, ToPoint,
+};
+use fuzzy::StringMatch;
+use gpui::{
+ actions, div, rems, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
+ FontWeight, ParentElement, Point, Render, Styled, StyledText, Task, TextStyle, View,
+ ViewContext, VisualContext, WeakView, WindowContext,
+};
+use language::Outline;
+use ordered_float::OrderedFloat;
+use picker::{Picker, PickerDelegate};
+use std::{
+ cmp::{self, Reverse},
+ sync::Arc,
+};
+use theme::ActiveTheme;
+use ui::{v_stack, ListItem, Selectable};
+use util::ResultExt;
+use workspace::Workspace;
+
+actions!(Toggle);
+
+pub fn init(cx: &mut AppContext) {
+ cx.observe_new_views(OutlineView::register).detach();
+}
+
+pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
+ if let Some(editor) = workspace
+ .active_item(cx)
+ .and_then(|item| item.downcast::<Editor>())
+ {
+ let outline = editor
+ .read(cx)
+ .buffer()
+ .read(cx)
+ .snapshot(cx)
+ .outline(Some(&cx.theme().syntax()));
+
+ if let Some(outline) = outline {
+ workspace.toggle_modal(cx, |cx| OutlineView::new(outline, editor, cx));
+ }
+ }
+}
+
+pub struct OutlineView {
+ picker: View<Picker<OutlineViewDelegate>>,
+}
+
+impl FocusableView for OutlineView {
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+ self.picker.focus_handle(cx)
+ }
+}
+
+impl EventEmitter<DismissEvent> for OutlineView {}
+
+impl Render for OutlineView {
+ type Element = Div;
+
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+ v_stack().min_w_96().child(self.picker.clone())
+ }
+}
+
+impl OutlineView {
+ fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
+ workspace.register_action(toggle);
+ }
+
+ fn new(
+ outline: Outline<Anchor>,
+ editor: View<Editor>,
+ cx: &mut ViewContext<Self>,
+ ) -> OutlineView {
+ let delegate = OutlineViewDelegate::new(cx.view().downgrade(), outline, editor, cx);
+ let picker = cx.build_view(|cx| Picker::new(delegate, cx));
+ OutlineView { picker }
+ }
+}
+
+struct OutlineViewDelegate {
+ outline_view: WeakView<OutlineView>,
+ active_editor: View<Editor>,
+ outline: Outline<Anchor>,
+ selected_match_index: usize,
+ prev_scroll_position: Option<Point<f32>>,
+ matches: Vec<StringMatch>,
+ last_query: String,
+}
+
+impl OutlineViewDelegate {
+ fn new(
+ outline_view: WeakView<OutlineView>,
+ outline: Outline<Anchor>,
+ editor: View<Editor>,
+ cx: &mut ViewContext<OutlineView>,
+ ) -> Self {
+ Self {
+ outline_view,
+ last_query: Default::default(),
+ matches: Default::default(),
+ selected_match_index: 0,
+ prev_scroll_position: Some(editor.update(cx, |editor, cx| editor.scroll_position(cx))),
+ active_editor: editor,
+ outline,
+ }
+ }
+
+ fn restore_active_editor(&mut self, cx: &mut WindowContext) {
+ self.active_editor.update(cx, |editor, cx| {
+ editor.highlight_rows(None);
+ if let Some(scroll_position) = self.prev_scroll_position {
+ editor.set_scroll_position(scroll_position, cx);
+ }
+ })
+ }
+
+ fn set_selected_index(
+ &mut self,
+ ix: usize,
+ navigate: bool,
+ cx: &mut ViewContext<Picker<OutlineViewDelegate>>,
+ ) {
+ self.selected_match_index = ix;
+
+ if navigate && !self.matches.is_empty() {
+ let selected_match = &self.matches[self.selected_match_index];
+ let outline_item = &self.outline.items[selected_match.candidate_id];
+
+ self.active_editor.update(cx, |active_editor, cx| {
+ let snapshot = active_editor.snapshot(cx).display_snapshot;
+ let buffer_snapshot = &snapshot.buffer_snapshot;
+ let start = outline_item.range.start.to_point(buffer_snapshot);
+ let end = outline_item.range.end.to_point(buffer_snapshot);
+ let display_rows = start.to_display_point(&snapshot).row()
+ ..end.to_display_point(&snapshot).row() + 1;
+ active_editor.highlight_rows(Some(display_rows));
+ active_editor.request_autoscroll(Autoscroll::center(), cx);
+ });
+ }
+ }
+}
+
+impl PickerDelegate for OutlineViewDelegate {
+ type ListItem = ListItem;
+
+ fn placeholder_text(&self) -> Arc<str> {
+ "Search buffer symbols...".into()
+ }
+
+ fn match_count(&self) -> usize {
+ self.matches.len()
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_match_index
+ }
+
+ fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<OutlineViewDelegate>>) {
+ self.set_selected_index(ix, true, cx);
+ }
+
+ fn update_matches(
+ &mut self,
+ query: String,
+ cx: &mut ViewContext<Picker<OutlineViewDelegate>>,
+ ) -> Task<()> {
+ let selected_index;
+ if query.is_empty() {
+ self.restore_active_editor(cx);
+ self.matches = self
+ .outline
+ .items
+ .iter()
+ .enumerate()
+ .map(|(index, _)| StringMatch {
+ candidate_id: index,
+ score: Default::default(),
+ positions: Default::default(),
+ string: Default::default(),
+ })
+ .collect();
+
+ let editor = self.active_editor.read(cx);
+ let cursor_offset = editor.selections.newest::<usize>(cx).head();
+ let buffer = editor.buffer().read(cx).snapshot(cx);
+ selected_index = self
+ .outline
+ .items
+ .iter()
+ .enumerate()
+ .map(|(ix, item)| {
+ let range = item.range.to_offset(&buffer);
+ let distance_to_closest_endpoint = cmp::min(
+ (range.start as isize - cursor_offset as isize).abs(),
+ (range.end as isize - cursor_offset as isize).abs(),
+ );
+ let depth = if range.contains(&cursor_offset) {
+ Some(item.depth)
+ } else {
+ None
+ };
+ (ix, depth, distance_to_closest_endpoint)
+ })
+ .max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance)))
+ .map(|(ix, _, _)| ix)
+ .unwrap_or(0);
+ } else {
+ self.matches = smol::block_on(
+ self.outline
+ .search(&query, cx.background_executor().clone()),
+ );
+ selected_index = self
+ .matches
+ .iter()
+ .enumerate()
+ .max_by_key(|(_, m)| OrderedFloat(m.score))
+ .map(|(ix, _)| ix)
+ .unwrap_or(0);
+ }
+ self.last_query = query;
+ self.set_selected_index(selected_index, !self.last_query.is_empty(), cx);
+ Task::ready(())
+ }
+
+ fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<OutlineViewDelegate>>) {
+ self.prev_scroll_position.take();
+
+ self.active_editor.update(cx, |active_editor, cx| {
+ if let Some(rows) = active_editor.highlighted_rows() {
+ let snapshot = active_editor.snapshot(cx).display_snapshot;
+ let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
+ active_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
+ s.select_ranges([position..position])
+ });
+ active_editor.highlight_rows(None);
+ }
+ });
+
+ self.dismissed(cx);
+ }
+
+ fn dismissed(&mut self, cx: &mut ViewContext<Picker<OutlineViewDelegate>>) {
+ self.outline_view
+ .update(cx, |_, cx| cx.emit(DismissEvent))
+ .log_err();
+ self.restore_active_editor(cx);
+ }
+
+ fn render_match(
+ &self,
+ ix: usize,
+ selected: bool,
+ _: &mut ViewContext<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ let mat = &self.matches[ix];
+ let outline_item = &self.outline.items[mat.candidate_id];
+
+ let highlights = gpui::combine_highlights(
+ mat.ranges().map(|range| (range, FontWeight::BOLD.into())),
+ outline_item.highlight_ranges.iter().cloned(),
+ );
+
+ let styled_text = StyledText::new(outline_item.text.clone())
+ .with_highlights(&TextStyle::default(), highlights);
+
+ Some(
+ ListItem::new(ix)
+ .inset(true)
+ .selected(selected)
+ .child(div().pl(rems(outline_item.depth as f32)).child(styled_text)),
+ )
+ }
+}