aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src
diff options
context:
space:
mode:
authorBlaž Hrastnik2021-04-05 09:23:37 +0000
committerBlaž Hrastnik2021-04-05 09:23:37 +0000
commit95d0bba81ae8ed035399b2cb362d2f65481d4781 (patch)
treeaf8d35e7de2864abec5d4188bdb7a29fb2ef56c2 /helix-term/src
parent59a0fc7b59186b3bedb01dd5b958d3b97b9fbba2 (diff)
ui: Improve completion state handling.
Diffstat (limited to 'helix-term/src')
-rw-r--r--helix-term/src/commands.rs8
-rw-r--r--helix-term/src/compositor.rs86
-rw-r--r--helix-term/src/ui/completion.rs75
-rw-r--r--helix-term/src/ui/editor.rs37
-rw-r--r--helix-term/src/ui/menu.rs10
-rw-r--r--helix-term/src/ui/picker.rs2
-rw-r--r--helix-term/src/ui/popup.rs6
7 files changed, 179 insertions, 45 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 97078794..c3afbd92 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::EditorView>() {
+ ui.set_completion(items, trigger_offset, size);
+ };
}
},
);
diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs
index 4869032b..6e81cc81 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::<Self>()
+ }
}
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<Component> = Box::new(TextComponent::new("text"));
+ /// let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
+ /// ```
+ fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>;
+}
+
+impl<T: Component> 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<Self>) -> Box<dyn Any> {
+ self
+ }
+}
+
+impl dyn AnyComponent {
+ /// Attempts to downcast `self` to a concrete type.
+ pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
+ self.as_any().downcast_ref()
+ }
+
+ /// Attempts to downcast `self` to a concrete type.
+ pub fn downcast_mut<T: Any>(&mut self) -> Option<&mut T> {
+ self.as_any_mut().downcast_mut()
+ }
+
+ /// Attempts to downcast `Box<Self>` to a concrete type.
+ pub fn downcast<T: Any>(self: Box<Self>) -> Result<Box<T>, Box<Self>> {
+ // Do the check here + unwrap, so the error
+ // value is `Self` and not `dyn Any`.
+ if self.as_any().is::<T>() {
+ Ok(self.as_boxed_any().downcast().unwrap())
+ } else {
+ Err(self)
+ }
+ }
+
+ /// Checks if this view is of type `T`.
+ pub fn is<T: Any>(&mut self) -> bool {
+ self.as_any().is::<T>()
+ }
}
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index 53241c57..637fc5f4 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 24c46bde..16a77b6c 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<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
status_msg: Option<String>,
last_insert: (commands::Command, Vec<KeyEvent>),
+ completion: Option<Completion>,
}
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<helix_lsp::lsp::CompletionItem>,
+ 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<Position> {
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs
index fbd25a6d..30ac044c 100644
--- a/helix-term/src/ui/menu.rs
+++ b/helix-term/src/ui/menu.rs
@@ -123,11 +123,19 @@ impl<T> Menu<T> {
.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<T> Component for Menu<T> {
+impl<T: 'static> Component for Menu<T> {
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 6ac35fba..ef2fe2a8 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -118,7 +118,7 @@ impl<T> Picker<T> {
// - on input change:
// - score all the names in relation to input
-impl<T> Component for Picker<T> {
+impl<T: 'static> Component for Picker<T> {
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 f1666451..44e79c4f 100644
--- a/helix-term/src/ui/popup.rs
+++ b/helix-term/src/ui/popup.rs
@@ -46,7 +46,11 @@ impl<T: Component> Popup<T> {
}
}
- 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
}
}