diff --git a/Cargo.lock b/Cargo.lock index f1761a5daa..289ed1957e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3127,6 +3127,7 @@ version = "0.1.0" dependencies = [ "editor", "gpui", + "language", "postage", "text", "workspace", diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 56bcfbc67c..72b883df0c 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -14,7 +14,7 @@ pub use diagnostic_set::DiagnosticEntry; use gpui::AppContext; use highlight_map::HighlightMap; use lazy_static::lazy_static; -pub use outline::Outline; +pub use outline::{Outline, OutlineItem}; use parking_lot::Mutex; use serde::Deserialize; use std::{ops::Range, path::Path, str, sync::Arc}; diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index c824c6cd87..6aa559f963 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -3,7 +3,7 @@ use std::ops::Range; #[derive(Debug)] pub struct Outline(pub Vec); -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct OutlineItem { pub id: usize, pub depth: usize, diff --git a/crates/outline/Cargo.toml b/crates/outline/Cargo.toml index 9ee0c76f7d..4a6d463088 100644 --- a/crates/outline/Cargo.toml +++ b/crates/outline/Cargo.toml @@ -7,8 +7,9 @@ edition = "2021" path = "src/outline.rs" [dependencies] -text = { path = "../text" } editor = { path = "../editor" } gpui = { path = "../gpui" } +language = { path = "../language" } +text = { path = "../text" } workspace = { path = "../workspace" } postage = { version = "0.4", features = ["futures-traits"] } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index e9507296e7..7ac63f1382 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -1,10 +1,11 @@ use editor::{display_map::ToDisplayPoint, Autoscroll, Editor, EditorSettings}; use gpui::{ action, elements::*, geometry::vector::Vector2F, keymap::Binding, Axis, Entity, - MutableAppContext, RenderContext, View, ViewContext, ViewHandle, + MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle, }; +use language::{Outline, OutlineItem}; use postage::watch; -use std::sync::Arc; +use std::{cmp, sync::Arc}; use text::{Bias, Point, Selection}; use workspace::{Settings, Workspace}; @@ -14,14 +15,21 @@ action!(Confirm); pub fn init(cx: &mut MutableAppContext) { cx.add_bindings([ Binding::new("cmd-shift-O", Toggle, Some("Editor")), - Binding::new("escape", Toggle, Some("GoToLine")), - Binding::new("enter", Confirm, Some("GoToLine")), + Binding::new("escape", Toggle, Some("OutlineView")), + Binding::new("enter", Confirm, Some("OutlineView")), ]); cx.add_action(OutlineView::toggle); cx.add_action(OutlineView::confirm); } -struct OutlineView {} +struct OutlineView { + handle: WeakViewHandle, + outline: Outline, + matches: Vec, + query_editor: ViewHandle, + list_state: UniformListState, + settings: watch::Receiver, +} impl Entity for OutlineView { type Event = (); @@ -33,11 +41,70 @@ impl View for OutlineView { } fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { - todo!() + let settings = self.settings.borrow(); + + Align::new( + ConstrainedBox::new( + Container::new( + Flex::new(Axis::Vertical) + .with_child( + Container::new(ChildView::new(self.query_editor.id()).boxed()) + .with_style(settings.theme.selector.input_editor.container) + .boxed(), + ) + .with_child(Flexible::new(1.0, false, self.render_matches()).boxed()) + .boxed(), + ) + .with_style(settings.theme.selector.container) + .boxed(), + ) + .with_max_width(500.0) + .with_max_height(420.0) + .boxed(), + ) + .top() + .named("outline view") + } + + fn on_focus(&mut self, cx: &mut ViewContext) { + cx.focus(&self.query_editor); } } impl OutlineView { + fn new( + outline: Outline, + settings: watch::Receiver, + cx: &mut ViewContext, + ) -> Self { + let query_editor = cx.add_view(|cx| { + Editor::single_line( + { + let settings = settings.clone(); + Arc::new(move |_| { + let settings = settings.borrow(); + EditorSettings { + style: settings.theme.selector.input_editor.as_editor(), + tab_size: settings.tab_size, + soft_wrap: editor::SoftWrap::None, + } + }) + }, + cx, + ) + }); + cx.subscribe(&query_editor, Self::on_query_editor_event) + .detach(); + Self { + handle: cx.weak_handle(), + matches: outline.0.clone(), + outline, + query_editor, + list_state: Default::default(), + settings, + } + } + fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { let editor = workspace .active_item(cx) @@ -45,9 +112,81 @@ impl OutlineView { .to_any() .downcast::() .unwrap(); - let buffer = editor.read(cx).buffer().read(cx); - dbg!(buffer.read(cx).outline()); + let buffer = editor.read(cx).buffer().read(cx).read(cx).outline(); + if let Some(outline) = buffer { + workspace.toggle_modal(cx, |cx, workspace| { + cx.add_view(|cx| OutlineView::new(outline, workspace.settings(), cx)) + }) + } } fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) {} + + fn on_query_editor_event( + &mut self, + _: ViewHandle, + event: &editor::Event, + cx: &mut ViewContext, + ) { + match event { + editor::Event::Edited => { + let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx)); + } + _ => {} + } + } + + fn render_matches(&self) -> ElementBox { + if self.matches.is_empty() { + let settings = self.settings.borrow(); + return Container::new( + Label::new( + "No matches".into(), + settings.theme.selector.empty.label.clone(), + ) + .boxed(), + ) + .with_style(settings.theme.selector.empty.container) + .named("empty matches"); + } + + let handle = self.handle.clone(); + let list = + UniformList::new( + self.list_state.clone(), + self.matches.len(), + move |mut range, items, cx| { + let cx = cx.as_ref(); + let view = handle.upgrade(cx).unwrap(); + let view = view.read(cx); + let start = range.start; + range.end = cmp::min(range.end, view.matches.len()); + items.extend(view.matches[range].iter().enumerate().map( + move |(i, outline_match)| view.render_match(outline_match, start + i), + )); + }, + ); + + Container::new(list.boxed()) + .with_margin_top(6.0) + .named("matches") + } + + fn render_match(&self, outline_match: &OutlineItem, index: usize) -> ElementBox { + // TODO: maintain selected index. + let selected_index = 0; + let settings = self.settings.borrow(); + let style = if index == selected_index { + &settings.theme.selector.active_item + } else { + &settings.theme.selector.item + }; + + Label::new(outline_match.text.clone(), style.label.clone()) + .contained() + .with_padding_left(20. * outline_match.depth as f32) + .contained() + .with_style(style.container) + .boxed() + } }