diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 970787946..c3afbd922 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1691,8 +1691,12 @@ pub fn completion(cx: &mut Context) { // TODO: if no completion, show some message or something if !items.is_empty() { - let completion = Completion::new(items, trigger_offset); - compositor.push(Box::new(completion)); + use crate::compositor::AnyComponent; + let size = compositor.size(); + let ui = compositor.find("hx::ui::editor::EditorView").unwrap(); + if let Some(ui) = ui.as_any_mut().downcast_mut::() { + ui.set_completion(items, trigger_offset, size); + }; } }, ); diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 4869032b1..6e81cc81b 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -34,7 +34,7 @@ pub struct Context<'a> { pub callbacks: &'a mut LspCallbacks, } -pub trait Component { +pub trait Component: Any + AnyComponent { /// Process input events, return true if handled. fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult { EventResult::Ignored @@ -60,6 +60,10 @@ pub trait Component { // that way render can use it None } + + fn type_name(&self) -> &'static str { + std::any::type_name::() + } } use anyhow::Error; @@ -142,4 +146,84 @@ impl Compositor { } None } + + pub fn find(&mut self, type_name: &str) -> Option<&mut dyn Component> { + self.layers + .iter_mut() + .find(|component| component.type_name() == type_name) + .map(|component| component.as_mut()) + } +} + +// View casting, taken straight from Cursive + +use std::any::Any; + +/// A view that can be downcasted to its concrete type. +/// +/// This trait is automatically implemented for any `T: Component`. +pub trait AnyComponent { + /// Downcast self to a `Any`. + fn as_any(&self) -> &dyn Any; + + /// Downcast self to a mutable `Any`. + fn as_any_mut(&mut self) -> &mut dyn Any; + + /// Returns a boxed any from a boxed self. + /// + /// Can be used before `Box::downcast()`. + /// + /// # Examples + /// + /// ```rust + /// # use cursive_core::views::TextComponent; + /// # use cursive_core::view::Component; + /// let boxed: Box = Box::new(TextComponent::new("text")); + /// let text: Box = boxed.as_boxed_any().downcast().unwrap(); + /// ``` + fn as_boxed_any(self: Box) -> Box; +} + +impl AnyComponent for T { + /// Downcast self to a `Any`. + fn as_any(&self) -> &dyn Any { + self + } + + /// Downcast self to a mutable `Any`. + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn as_boxed_any(self: Box) -> Box { + self + } +} + +impl dyn AnyComponent { + /// Attempts to downcast `self` to a concrete type. + pub fn downcast_ref(&self) -> Option<&T> { + self.as_any().downcast_ref() + } + + /// Attempts to downcast `self` to a concrete type. + pub fn downcast_mut(&mut self) -> Option<&mut T> { + self.as_any_mut().downcast_mut() + } + + /// Attempts to downcast `Box` to a concrete type. + pub fn downcast(self: Box) -> Result, Box> { + // Do the check here + unwrap, so the error + // value is `Self` and not `dyn Any`. + if self.as_any().is::() { + Ok(self.as_boxed_any().downcast().unwrap()) + } else { + Err(self) + } + } + + /// Checks if this view is of type `T`. + pub fn is(&mut self) -> bool { + self.as_any().is::() + } } diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 53241c57d..637fc5f45 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -12,6 +12,7 @@ use std::borrow::Cow; use helix_core::{Position, Transaction}; use helix_view::Editor; +use crate::commands; use crate::ui::{Menu, Popup, PromptEvent}; use helix_lsp::lsp; @@ -112,44 +113,50 @@ impl Completion { trigger_offset, } } + + pub fn update(&mut self, cx: &mut commands::Context) { + // recompute menu based on matches + let menu = self.popup.contents_mut(); + let (view, doc) = cx.editor.current(); + + // cx.hooks() + // cx.add_hook(enum type, ||) + // cx.trigger_hook(enum type, &str, ...) <-- there has to be enough to identify doc/view + // callback with editor & compositor + // + // trigger_hook sends event into channel, that's consumed in the global loop and + // triggers all registered callbacks + // TODO: hooks should get processed immediately so maybe do it after select!(), before + // looping? + + let cursor = doc.selection(view.id).cursor(); + if self.trigger_offset <= cursor { + let fragment = doc.text().slice(self.trigger_offset..=cursor); + let text = Cow::from(fragment); + // TODO: logic is same as ui/picker + menu.score(&text); + } + } + + pub fn is_empty(&self) -> bool { + self.popup.contents().is_empty() + } } +// need to: +// - trigger on the right trigger char +// - detect previous open instance and recycle +// - update after input, but AFTER the document has changed +// - if no more matches, need to auto close +// +// missing bits: +// - a more robust hook system: emit to a channel, process in main loop +// - a way to find specific layers in compositor +// - components register for hooks, then unregister when terminated +// ... since completion is a special case, maybe just build it into doc/render? + impl Component for Completion { fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { - // input - if let Event::Key(KeyEvent { - code: KeyCode::Char(ch), - .. - }) = event - { - // recompute menu based on matches - let menu = self.popup.contents(); - let (view, doc) = cx.editor.current(); - - // cx.hooks() - // cx.add_hook(enum type, ||) - // cx.trigger_hook(enum type, &str, ...) <-- there has to be enough to identify doc/view - // callback with editor & compositor - // - // trigger_hook sends event into channel, that's consumed in the global loop and - // triggers all registered callbacks - // TODO: hooks should get processed immediately so maybe do it after select!(), before - // looping? - - let cursor = doc.selection(view.id).cursor(); - if self.trigger_offset <= cursor { - let fragment = doc.text().slice(self.trigger_offset..cursor); - // ^ problem seems to be that we handle events here before the editor layer, so the - // keypress isn't included in the editor layer yet... - // so we can't use ..= for now. - let text = Cow::from(fragment); - // TODO: logic is same as ui/picker - menu.score(&text); - - // TODO: if after scoring the selection is 0 items, remove popup - } - } - self.popup.handle_event(event, cx) } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 24c46bde6..16a77b6c2 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -3,7 +3,7 @@ use crate::{ compositor::{Component, Compositor, Context, EventResult}, key, keymap::{self, Keymaps}, - ui::text_color, + ui::{text_color, Completion}, }; use helix_core::{ @@ -29,6 +29,7 @@ pub struct EditorView { on_next_key: Option>, status_msg: Option, last_insert: (commands::Command, Vec), + completion: Option, } const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter @@ -40,6 +41,7 @@ impl EditorView { on_next_key: None, status_msg: None, last_insert: (commands::normal_mode, Vec::new()), + completion: None, } } @@ -435,15 +437,15 @@ impl EditorView { ); } - fn insert_mode(&self, cxt: &mut commands::Context, event: KeyEvent) { + fn insert_mode(&self, cx: &mut commands::Context, event: KeyEvent) { if let Some(command) = self.keymap[&Mode::Insert].get(&event) { - command(cxt); + command(cx); } else if let KeyEvent { code: KeyCode::Char(ch), .. } = event { - commands::insert::insert_char(cxt, ch); + commands::insert::insert_char(cx, ch); } } @@ -476,6 +478,18 @@ impl EditorView { } } } + + pub fn set_completion( + &mut self, + items: Vec, + trigger_offset: usize, + size: Rect, + ) { + let mut completion = Completion::new(items, trigger_offset); + // TODO : propagate required size on resize to completion too + completion.required_size((size.width, size.height)); + self.completion = Some(completion); + } } impl Component for EditorView { @@ -512,7 +526,15 @@ impl Component for EditorView { // record last_insert key self.last_insert.1.push(event); - self.insert_mode(&mut cxt, event) + self.insert_mode(&mut cxt, event); + + if let Some(completion) = &mut self.completion { + completion.update(&mut cxt); + if completion.is_empty() { + self.completion = None; + } + // TODO: if exiting InsertMode, remove completion + } } mode => self.command_mode(mode, &mut cxt, event), } @@ -547,6 +569,11 @@ impl Component for EditorView { let doc = cx.editor.document(view.doc).unwrap(); self.render_view(doc, view, view.area, surface, &cx.editor.theme, is_focused); } + + if let Some(completion) = &self.completion { + completion.render(area, surface, cx) + // render completion here + } } fn cursor_position(&self, area: Rect, editor: &Editor) -> Option { diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index fbd25a6de..30ac044ce 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -123,11 +123,19 @@ impl Menu { .map(|(index, _score)| &self.options[*index]) }) } + + pub fn is_empty(&self) -> bool { + self.matches.is_empty() + } + + pub fn len(&self) -> usize { + self.matches.len() + } } use super::PromptEvent as MenuEvent; -impl Component for Menu { +impl Component for Menu { fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { let event = match event { Event::Key(event) => event, diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 6ac35fba6..ef2fe2a86 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -118,7 +118,7 @@ impl Picker { // - on input change: // - score all the names in relation to input -impl Component for Picker { +impl Component for Picker { fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { let key_event = match event { Event::Key(event) => event, diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index f1666451d..44e79c4f0 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -46,7 +46,11 @@ impl Popup { } } - pub fn contents(&mut self) -> &mut T { + pub fn contents(&self) -> &T { + &self.contents + } + + pub fn contents_mut(&mut self) -> &mut T { &mut self.contents } }