mirror of
https://github.com/ilyakooo0/helix.git
synced 2024-11-30 00:35:26 +03:00
ui: Improve completion state handling.
This commit is contained in:
parent
59a0fc7b59
commit
95d0bba81a
@ -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);
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@ -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>()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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> {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user