aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src/ui
diff options
context:
space:
mode:
authorBlaž Hrastnik2021-02-09 06:40:30 +0000
committerBlaž Hrastnik2021-02-09 06:40:47 +0000
commitd4b85ce18d8a9bb535eaeae9e2c7421ef81c81e9 (patch)
treef68c8fc1d4996f861af2c7966ac8c49dba4aa174 /helix-term/src/ui
parent755632f23113ccff66390de1e2c3476a51886b00 (diff)
popup: wip work on completion popups
Diffstat (limited to 'helix-term/src/ui')
-rw-r--r--helix-term/src/ui/editor.rs4
-rw-r--r--helix-term/src/ui/menu.rs189
-rw-r--r--helix-term/src/ui/mod.rs2
-rw-r--r--helix-term/src/ui/picker.rs4
-rw-r--r--helix-term/src/ui/prompt.rs2
5 files changed, 196 insertions, 5 deletions
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index b841dff4..773bc44d 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -365,12 +365,12 @@ impl Component for EditorView {
// TODO: drop unwrap
}
- fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> {
+ fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
// match view.doc.mode() {
// Mode::Insert => write!(stdout, "\x1B[6 q"),
// mode => write!(stdout, "\x1B[2 q"),
// };
- let view = ctx.editor.view();
+ let view = editor.view();
let cursor = view.doc.state.selection().cursor();
let mut pos = view
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs
new file mode 100644
index 00000000..7053a179
--- /dev/null
+++ b/helix-term/src/ui/menu.rs
@@ -0,0 +1,189 @@
+use crate::compositor::{Component, Compositor, Context, EventResult};
+use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
+use tui::buffer::Buffer as Surface;
+use tui::{
+ layout::Rect,
+ style::{Color, Style},
+ widgets::{Block, Borders},
+};
+
+use std::borrow::Cow;
+
+use helix_core::Position;
+use helix_view::Editor;
+
+// TODO: factor out a popup component that we can reuse for displaying docs on autocomplete,
+// diagnostics popups, etc.
+
+pub struct Menu<T> {
+ options: Vec<T>,
+
+ cursor: usize,
+
+ position: Position,
+
+ format_fn: Box<dyn Fn(&T) -> Cow<str>>,
+ callback_fn: Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>,
+}
+
+impl<T> Menu<T> {
+ // TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different
+ // rendering)
+ pub fn new(
+ options: Vec<T>,
+ format_fn: impl Fn(&T) -> Cow<str> + 'static,
+ callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
+ ) -> Self {
+ Self {
+ options,
+ cursor: 0,
+ position: Position::default(),
+ format_fn: Box::new(format_fn),
+ callback_fn: Box::new(callback_fn),
+ }
+ }
+
+ pub fn set_position(&mut self, pos: Position) {
+ self.position = pos;
+ }
+
+ pub fn move_up(&mut self) {
+ self.cursor = self.cursor.saturating_sub(1);
+ }
+
+ pub fn move_down(&mut self) {
+ // TODO: len - 1
+ if self.cursor < self.options.len() {
+ self.cursor += 1;
+ }
+ }
+
+ pub fn selection(&self) -> Option<&T> {
+ self.options.get(self.cursor)
+ }
+}
+
+use super::PromptEvent as MenuEvent;
+
+impl<T> Component for Menu<T> {
+ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
+ let event = match event {
+ Event::Key(event) => event,
+ _ => return EventResult::Ignored,
+ };
+
+ let close_fn = EventResult::Consumed(Some(Box::new(
+ |compositor: &mut Compositor, editor: &mut Editor| {
+ // remove the layer
+ compositor.pop();
+ },
+ )));
+
+ match event {
+ // esc or ctrl-c aborts the completion and closes the menu
+ KeyEvent {
+ code: KeyCode::Esc, ..
+ }
+ | KeyEvent {
+ code: KeyCode::Char('c'),
+ modifiers: KeyModifiers::CONTROL,
+ } => {
+ (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Abort);
+ return close_fn;
+ }
+ // arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc)
+ KeyEvent {
+ code: KeyCode::Tab,
+ modifiers: KeyModifiers::SHIFT,
+ }
+ | KeyEvent {
+ code: KeyCode::Up, ..
+ }
+ | KeyEvent {
+ code: KeyCode::Char('p'),
+ modifiers: KeyModifiers::CONTROL,
+ } => {
+ self.move_up();
+ (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
+ return EventResult::Consumed(None);
+ }
+ // arrow down/ctrl-n/tab advances completion choice (including updating the doc)
+ KeyEvent {
+ code: KeyCode::Tab,
+ modifiers: KeyModifiers::NONE,
+ }
+ | KeyEvent {
+ code: KeyCode::Down,
+ ..
+ }
+ | KeyEvent {
+ code: KeyCode::Char('n'),
+ modifiers: KeyModifiers::CONTROL,
+ } => {
+ self.move_down();
+ (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
+ return EventResult::Consumed(None);
+ }
+ KeyEvent {
+ code: KeyCode::Enter,
+ ..
+ } => {
+ (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Validate);
+ return close_fn;
+ }
+ // KeyEvent {
+ // code: KeyCode::Char(c),
+ // modifiers: KeyModifiers::NONE,
+ // } => {
+ // self.insert_char(c);
+ // (self.callback_fn)(cx.editor, &self.line, MenuEvent::Update);
+ // }
+
+ // / -> edit_filter?
+ //
+ // enter confirms the match and closes the menu
+ // typing filters the menu
+ // if we run out of options the menu closes itself
+ _ => (),
+ }
+ // for some events, we want to process them but send ignore, specifically all input except
+ // tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll.
+ // EventResult::Consumed(None)
+ EventResult::Ignored
+ }
+ fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
+ // render a box at x, y. Width equal to max width of item.
+ // initially limit to n items, add support for scrolling
+ //
+ const MAX: usize = 5;
+ let rows = std::cmp::min(self.options.len(), MAX) as u16;
+ let area = Rect::new(self.position.col as u16, self.position.row as u16, 30, rows);
+
+ // clear area
+ let background = cx.editor.theme.get("ui.popup");
+ for y in area.top()..area.bottom() {
+ for x in area.left()..area.right() {
+ let cell = surface.get_mut(x, y);
+ cell.reset();
+ // cell.symbol.clear();
+ cell.set_style(background);
+ }
+ }
+
+ // -- Render the contents:
+
+ let style = Style::default().fg(Color::Rgb(164, 160, 232)); // lavender
+ let selected = Style::default().fg(Color::Rgb(255, 255, 255));
+
+ for (i, option) in self.options.iter().take(rows as usize).enumerate() {
+ // TODO: set bg for the whole row if selected
+ surface.set_stringn(
+ area.x,
+ area.y + i as u16,
+ (self.format_fn)(option),
+ area.width as usize - 1,
+ if i == self.cursor { selected } else { style },
+ );
+ }
+ }
+}
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index 7c12b918..29483705 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -1,8 +1,10 @@
mod editor;
+mod menu;
mod picker;
mod prompt;
pub use editor::EditorView;
+pub use menu::Menu;
pub use picker::Picker;
pub use prompt::{Prompt, PromptEvent};
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index 60828b6f..d8da052a 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -257,7 +257,7 @@ impl<T> Component for Picker<T> {
}
}
- fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> {
- self.prompt.cursor_position(area, ctx)
+ fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
+ self.prompt.cursor_position(area, editor)
}
}
diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs
index 5a47bf12..7228b38c 100644
--- a/helix-term/src/ui/prompt.rs
+++ b/helix-term/src/ui/prompt.rs
@@ -235,7 +235,7 @@ impl Component for Prompt {
self.render_prompt(area, surface, &cx.editor.theme)
}
- fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> {
+ fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
Some(Position::new(
area.height as usize,
area.x as usize + self.prompt.len() + self.cursor,