on master help sign in
ui: Improve completion state handling.
commit95d0bba81ae8ed035399b2cb362d2f65481d4781 authorBlaž Hrastnik <blaz@mxxn.io> dateMon 5 Apr 2021, 18:23 (6 years ago) parent59a0fc7
7 files changed+179−45
helix-term/src/commands.rs+6−2 helix-term/src/compositor.rs+85−1 helix-term/src/ui/completion.rs+41−34 helix-term/src/ui/editor.rs+32−5 helix-term/src/ui/menu.rs+9−1 helix-term/src/ui/picker.rs+1−1 helix-term/src/ui/popup.rs+5−1
helix-term/src/commands.rs+6−2
@@ -1691,8 +1691,12 @@ pub fn completion(cx: &mut Context) {
16911691
16921692 // TODO: if no completion, show some message or something
16931693 if !items.is_empty() {
1694− let completion = Completion::new(items, trigger_offset);
1695− compositor.push(Box::new(completion));
1694+ use crate::compositor::AnyComponent;
1695+ let size = compositor.size();
1696+ let ui = compositor.find("hx::ui::editor::EditorView").unwrap();
1697+ if let Some(ui) = ui.as_any_mut().downcast_mut::<ui::EditorView>() {
1698+ ui.set_completion(items, trigger_offset, size);
1699+ };
16961700 }
16971701 },
16981702 );
helix-term/src/compositor.rs+85−1
@@ -34,7 +34,7 @@ pub struct Context<'a> {
3434 pub callbacks: &'a mut LspCallbacks,
3535}
3636
37−pub trait Component {
37+pub trait Component: Any + AnyComponent {
3838 /// Process input events, return true if handled.
3939 fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult {
4040 EventResult::Ignored
@@ -60,6 +60,10 @@ pub trait Component {
6060 // that way render can use it
6161 None
6262 }
63+
64+ fn type_name(&self) -> &'static str {
65+ std::any::type_name::<Self>()
66+ }
6367}
6468
6569use anyhow::Error;
@@ -142,4 +146,84 @@ impl Compositor {
142146 }
143147 None
144148 }
149+
150+ pub fn find(&mut self, type_name: &str) -> Option<&mut dyn Component> {
151+ self.layers
152+ .iter_mut()
153+ .find(|component| component.type_name() == type_name)
154+ .map(|component| component.as_mut())
155+ }
156+}
157+
158+// View casting, taken straight from Cursive
159+
160+use std::any::Any;
161+
162+/// A view that can be downcasted to its concrete type.
163+///
164+/// This trait is automatically implemented for any `T: Component`.
165+pub trait AnyComponent {
166+ /// Downcast self to a `Any`.
167+ fn as_any(&self) -> &dyn Any;
168+
169+ /// Downcast self to a mutable `Any`.
170+ fn as_any_mut(&mut self) -> &mut dyn Any;
171+
172+ /// Returns a boxed any from a boxed self.
173+ ///
174+ /// Can be used before `Box::downcast()`.
175+ ///
176+ /// # Examples
177+ ///
178+ /// ```rust
179+ /// # use cursive_core::views::TextComponent;
180+ /// # use cursive_core::view::Component;
181+ /// let boxed: Box<Component> = Box::new(TextComponent::new("text"));
182+ /// let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
183+ /// ```
184+ fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>;
185+}
186+
187+impl<T: Component> AnyComponent for T {
188+ /// Downcast self to a `Any`.
189+ fn as_any(&self) -> &dyn Any {
190+ self
191+ }
192+
193+ /// Downcast self to a mutable `Any`.
194+ fn as_any_mut(&mut self) -> &mut dyn Any {
195+ self
196+ }
197+
198+ fn as_boxed_any(self: Box<Self>) -> Box<dyn Any> {
199+ self
200+ }
201+}
202+
203+impl dyn AnyComponent {
204+ /// Attempts to downcast `self` to a concrete type.
205+ pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
206+ self.as_any().downcast_ref()
207+ }
208+
209+ /// Attempts to downcast `self` to a concrete type.
210+ pub fn downcast_mut<T: Any>(&mut self) -> Option<&mut T> {
211+ self.as_any_mut().downcast_mut()
212+ }
213+
214+ /// Attempts to downcast `Box<Self>` to a concrete type.
215+ pub fn downcast<T: Any>(self: Box<Self>) -> Result<Box<T>, Box<Self>> {
216+ // Do the check here + unwrap, so the error
217+ // value is `Self` and not `dyn Any`.
218+ if self.as_any().is::<T>() {
219+ Ok(self.as_boxed_any().downcast().unwrap())
220+ } else {
221+ Err(self)
222+ }
223+ }
224+
225+ /// Checks if this view is of type `T`.
226+ pub fn is<T: Any>(&mut self) -> bool {
227+ self.as_any().is::<T>()
228+ }
145229}
helix-term/src/ui/completion.rs+41−34
@@ -12,6 +12,7 @@ use std::borrow::Cow;
1212use helix_core::{Position, Transaction};
1313use helix_view::Editor;
1414
15+use crate::commands;
1516use crate::ui::{Menu, Popup, PromptEvent};
1617
1718use helix_lsp::lsp;
@@ -112,44 +113,50 @@ impl Completion {
112113 trigger_offset,
113114 }
114115 }
116+
117+ pub fn update(&mut self, cx: &mut commands::Context) {
118+ // recompute menu based on matches
119+ let menu = self.popup.contents_mut();
120+ let (view, doc) = cx.editor.current();
121+
122+ // cx.hooks()
123+ // cx.add_hook(enum type, ||)
124+ // cx.trigger_hook(enum type, &str, ...) <-- there has to be enough to identify doc/view
125+ // callback with editor & compositor
126+ //
127+ // trigger_hook sends event into channel, that's consumed in the global loop and
128+ // triggers all registered callbacks
129+ // TODO: hooks should get processed immediately so maybe do it after select!(), before
130+ // looping?
131+
132+ let cursor = doc.selection(view.id).cursor();
133+ if self.trigger_offset <= cursor {
134+ let fragment = doc.text().slice(self.trigger_offset..=cursor);
135+ let text = Cow::from(fragment);
136+ // TODO: logic is same as ui/picker
137+ menu.score(&text);
138+ }
139+ }
140+
141+ pub fn is_empty(&self) -> bool {
142+ self.popup.contents().is_empty()
143+ }
115144}
116145
146+// need to:
147+// - trigger on the right trigger char
148+// - detect previous open instance and recycle
149+// - update after input, but AFTER the document has changed
150+// - if no more matches, need to auto close
151+//
152+// missing bits:
153+// - a more robust hook system: emit to a channel, process in main loop
154+// - a way to find specific layers in compositor
155+// - components register for hooks, then unregister when terminated
156+// ... since completion is a special case, maybe just build it into doc/render?
157+
117158impl Component for Completion {
118159 fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
119− // input
120− if let Event::Key(KeyEvent {
121− code: KeyCode::Char(ch),
122− ..
123− }) = event
124− {
125− // recompute menu based on matches
126− let menu = self.popup.contents();
127− let (view, doc) = cx.editor.current();
128−
129− // cx.hooks()
130− // cx.add_hook(enum type, ||)
131− // cx.trigger_hook(enum type, &str, ...) <-- there has to be enough to identify doc/view
132− // callback with editor & compositor
133− //
134− // trigger_hook sends event into channel, that's consumed in the global loop and
135− // triggers all registered callbacks
136− // TODO: hooks should get processed immediately so maybe do it after select!(), before
137− // looping?
138−
139− let cursor = doc.selection(view.id).cursor();
140− if self.trigger_offset <= cursor {
141− let fragment = doc.text().slice(self.trigger_offset..cursor);
142− // ^ problem seems to be that we handle events here before the editor layer, so the
143− // keypress isn't included in the editor layer yet...
144− // so we can't use ..= for now.
145− let text = Cow::from(fragment);
146− // TODO: logic is same as ui/picker
147− menu.score(&text);
148−
149− // TODO: if after scoring the selection is 0 items, remove popup
150− }
151− }
152−
153160 self.popup.handle_event(event, cx)
154161 }
155162
helix-term/src/ui/editor.rs+32−5
@@ -3,7 +3,7 @@ use crate::{
33 compositor::{Component, Compositor, Context, EventResult},
44 key,
55 keymap::{self, Keymaps},
6− ui::text_color,
6+ ui::{text_color, Completion},
77};
88
99use helix_core::{
@@ -29,6 +29,7 @@ pub struct EditorView {
2929 on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
3030 status_msg: Option<String>,
3131 last_insert: (commands::Command, Vec<KeyEvent>),
32+ completion: Option<Completion>,
3233}
3334
3435const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
@@ -40,6 +41,7 @@ impl EditorView {
4041 on_next_key: None,
4142 status_msg: None,
4243 last_insert: (commands::normal_mode, Vec::new()),
44+ completion: None,
4345 }
4446 }
4547
@@ -435,15 +437,15 @@ impl EditorView {
435437 );
436438 }
437439
438− fn insert_mode(&self, cxt: &mut commands::Context, event: KeyEvent) {
440+ fn insert_mode(&self, cx: &mut commands::Context, event: KeyEvent) {
439441 if let Some(command) = self.keymap[&Mode::Insert].get(&event) {
440− command(cxt);
442+ command(cx);
441443 } else if let KeyEvent {
442444 code: KeyCode::Char(ch),
443445 ..
444446 } = event
445447 {
446− commands::insert::insert_char(cxt, ch);
448+ commands::insert::insert_char(cx, ch);
447449 }
448450 }
449451
@@ -476,6 +478,18 @@ impl EditorView {
476478 }
477479 }
478480 }
481+
482+ pub fn set_completion(
483+ &mut self,
484+ items: Vec<helix_lsp::lsp::CompletionItem>,
485+ trigger_offset: usize,
486+ size: Rect,
487+ ) {
488+ let mut completion = Completion::new(items, trigger_offset);
489+ // TODO : propagate required size on resize to completion too
490+ completion.required_size((size.width, size.height));
491+ self.completion = Some(completion);
492+ }
479493}
480494
481495impl Component for EditorView {
@@ -512,7 +526,15 @@ impl Component for EditorView {
512526 // record last_insert key
513527 self.last_insert.1.push(event);
514528
515− self.insert_mode(&mut cxt, event)
529+ self.insert_mode(&mut cxt, event);
530+
531+ if let Some(completion) = &mut self.completion {
532+ completion.update(&mut cxt);
533+ if completion.is_empty() {
534+ self.completion = None;
535+ }
536+ // TODO: if exiting InsertMode, remove completion
537+ }
516538 }
517539 mode => self.command_mode(mode, &mut cxt, event),
518540 }
@@ -547,6 +569,11 @@ impl Component for EditorView {
547569 let doc = cx.editor.document(view.doc).unwrap();
548570 self.render_view(doc, view, view.area, surface, &cx.editor.theme, is_focused);
549571 }
572+
573+ if let Some(completion) = &self.completion {
574+ completion.render(area, surface, cx)
575+ // render completion here
576+ }
550577 }
551578
552579 fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
helix-term/src/ui/menu.rs+9−1
@@ -123,11 +123,19 @@ impl<T> Menu<T> {
123123 .map(|(index, _score)| &self.options[*index])
124124 })
125125 }
126+
127+ pub fn is_empty(&self) -> bool {
128+ self.matches.is_empty()
129+ }
130+
131+ pub fn len(&self) -> usize {
132+ self.matches.len()
133+ }
126134}
127135
128136use super::PromptEvent as MenuEvent;
129137
130−impl<T> Component for Menu<T> {
138+impl<T: 'static> Component for Menu<T> {
131139 fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
132140 let event = match event {
133141 Event::Key(event) => event,
helix-term/src/ui/picker.rs+1−1
@@ -118,7 +118,7 @@ impl<T> Picker<T> {
118118// - on input change:
119119// - score all the names in relation to input
120120
121−impl<T> Component for Picker<T> {
121+impl<T: 'static> Component for Picker<T> {
122122 fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
123123 let key_event = match event {
124124 Event::Key(event) => event,
helix-term/src/ui/popup.rs+5−1
@@ -46,7 +46,11 @@ impl<T: Component> Popup<T> {
4646 }
4747 }
4848
49− pub fn contents(&mut self) -> &mut T {
49+ pub fn contents(&self) -> &T {
50+ &self.contents
51+ }
52+
53+ pub fn contents_mut(&mut self) -> &mut T {
5054 &mut self.contents
5155 }
5256}