aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBlaž Hrastnik2021-02-09 06:40:30 +0000
committerBlaž Hrastnik2021-02-09 06:40:47 +0000
commitd4b85ce18d8a9bb535eaeae9e2c7421ef81c81e9 (patch)
treef68c8fc1d4996f861af2c7966ac8c49dba4aa174
parent755632f23113ccff66390de1e2c3476a51886b00 (diff)
popup: wip work on completion popups
-rw-r--r--helix-term/src/application.rs4
-rw-r--r--helix-term/src/commands.rs90
-rw-r--r--helix-term/src/compositor.rs42
-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
-rw-r--r--helix-view/src/theme.rs1
-rw-r--r--helix-view/src/view.rs1
10 files changed, 272 insertions, 67 deletions
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index d307456e..dd7778dd 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -70,7 +70,7 @@ impl Application {
let area = self.terminal.size().unwrap();
compositor.render(area, self.terminal.current_buffer_mut(), &mut cx);
- let pos = compositor.cursor_position(area, &mut cx);
+ let pos = compositor.cursor_position(area, &editor);
self.terminal.draw();
self.terminal.set_cursor(pos.col as u16, pos.row as u16);
@@ -112,7 +112,7 @@ impl Application {
.handle_event(Event::Resize(width, height), &mut cx)
}
Some(Ok(event)) => self.compositor.handle_event(event, &mut cx),
- Some(Err(x)) => panic!(x),
+ Some(Err(x)) => panic!("{}", x),
None => panic!(),
};
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 3111900d..8570bee1 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -910,7 +910,8 @@ pub fn completion(cx: &mut Context) {
// TODO: if no completion, show some message or something
if !res.is_empty() {
- let picker = ui::Picker::new(
+ let snapshot = cx.doc().state.clone();
+ let mut menu = ui::Menu::new(
res,
|item| {
// format_fn
@@ -918,40 +919,75 @@ pub fn completion(cx: &mut Context) {
// TODO: use item.filter_text for filtering
},
- |editor: &mut Editor, item| {
- use helix_lsp::{lsp, util};
- // determine what to insert: text_edit | insert_text | label
- let edit = if let Some(edit) = &item.text_edit {
- match edit {
- lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
- lsp::CompletionTextEdit::InsertAndReplace(item) => {
- unimplemented!("completion: insert_and_replace {:?}", item)
- }
+ move |editor: &mut Editor, item, event| {
+ match event {
+ PromptEvent::Abort => {
+ // revert state
+ let doc = &mut editor.view_mut().doc;
+ doc.state = snapshot.clone();
}
- } else {
- item.insert_text.as_ref().unwrap_or(&item.label);
- unimplemented!();
- // lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text
- // and we insert at position.
- };
+ PromptEvent::Validate => {
+ let doc = &mut editor.view_mut().doc;
+
+ // revert state to what it was before the last update
+ doc.state = snapshot.clone();
+
+ // extract as fn(doc, item):
+
+ // TODO: need to apply without composing state...
+ // TODO: need to update lsp on accept/cancel by diffing the snapshot with
+ // the final state?
+ // -> on update simply update the snapshot, then on accept redo the call,
+ // finally updating doc.changes + notifying lsp.
+ //
+ // or we could simply use doc.undo + apply when changing between options
+
+ let item = item.unwrap();
+
+ use helix_lsp::{lsp, util};
+ // determine what to insert: text_edit | insert_text | label
+ let edit = if let Some(edit) = &item.text_edit {
+ match edit {
+ lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
+ lsp::CompletionTextEdit::InsertAndReplace(item) => {
+ unimplemented!("completion: insert_and_replace {:?}", item)
+ }
+ }
+ } else {
+ item.insert_text.as_ref().unwrap_or(&item.label);
+ unimplemented!();
+ // lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text
+ // and we insert at position.
+ };
+
+ // TODO: merge edit with additional_text_edits
+ if let Some(additional_edits) = &item.additional_text_edits {
+ if !additional_edits.is_empty() {
+ unimplemented!(
+ "completion: additional_text_edits: {:?}",
+ additional_edits
+ );
+ }
+ }
- // TODO: merge edit with additional_text_edits
- if let Some(additional_edits) = &item.additional_text_edits {
- if !additional_edits.is_empty() {
- unimplemented!("completion: additional_text_edits: {:?}", additional_edits);
+ let transaction =
+ util::generate_transaction_from_edits(&doc.state, vec![edit]);
+ doc.apply(&transaction);
+ // TODO: append_changes_to_history(cx); if not in insert mode?
}
- }
-
- let doc = &mut editor.view_mut().doc;
- let transaction = util::generate_transaction_from_edits(&doc.state, vec![edit]);
- doc.apply(&transaction);
- // TODO: append_changes_to_history(cx); if not in insert mode?
+ _ => (),
+ };
},
);
cx.callback = Some(Box::new(
move |compositor: &mut Compositor, editor: &mut Editor| {
- compositor.push(Box::new(picker));
+ let area = tui::layout::Rect::default(); // TODO: unused remove from cursor_position
+ let mut pos = compositor.cursor_position(area, editor);
+ pos.row += 1; // shift down by one row
+ menu.set_position(pos);
+
+ compositor.push(Box::new(menu));
},
));
diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs
index b1b92a71..3fee1214 100644
--- a/helix-term/src/compositor.rs
+++ b/helix-term/src/compositor.rs
@@ -54,13 +54,11 @@ pub trait Component {
fn render(&self, area: Rect, frame: &mut Surface, ctx: &mut Context);
- fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> {
+ fn cursor_position(&self, area: Rect, ctx: &Editor) -> Option<Position> {
None
}
}
-// struct Editor { };
-
// For v1:
// Child views are something each view needs to handle on it's own for now, positioning and sizing
// options, focus tracking. In practice this is simple: we only will need special solving for
@@ -83,29 +81,6 @@ pub trait Component {
// - a popup panel / dialog with it's own interactions
// - an autocomplete popup that doesn't change focus
-//fn main() {
-// let root = Editor::new();
-// let compositor = Compositor::new();
-
-// compositor.push(root);
-
-// // pos: clip to bottom of screen
-// compositor.push_at(pos, Prompt::new(
-// ":",
-// (),
-// |input: &str| match input {}
-// )); // TODO: this Prompt needs to somehow call compositor.pop() on close, but it can't refer to parent
-// // Cursive solves this by allowing to return a special result on process_event
-// // that's either Ignore | Consumed(Opt<C>) where C: fn (Compositor) -> ()
-
-// // TODO: solve popup focus: we want to push autocomplete popups on top of the current layer
-// // but retain the focus where it was. The popup will also need to update as we type into the
-// // textarea. It should also capture certain input, such as tab presses etc
-// //
-// // 1) This could be faked by the top layer pushing down edits into the previous layer.
-// // 2) Alternatively,
-//}
-
pub struct Compositor {
layers: Vec<Box<dyn Component>>,
}
@@ -124,14 +99,15 @@ impl Compositor {
}
pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool {
- // TODO: custom focus
- if let Some(layer) = self.layers.last_mut() {
- return match layer.handle_event(event, cx) {
+ // propagate events through the layers until we either find a layer that consumes it or we
+ // run out of layers (event bubbling)
+ for layer in self.layers.iter_mut().rev() {
+ match layer.handle_event(event, cx) {
EventResult::Consumed(Some(callback)) => {
callback(self, cx.editor);
- true
+ return true;
}
- EventResult::Consumed(None) => true,
+ EventResult::Consumed(None) => return true,
EventResult::Ignored => false,
};
}
@@ -144,9 +120,9 @@ impl Compositor {
}
}
- pub fn cursor_position(&self, area: Rect, cx: &mut Context) -> Position {
+ pub fn cursor_position(&self, area: Rect, editor: &Editor) -> Position {
for layer in self.layers.iter().rev() {
- if let Some(pos) = layer.cursor_position(area, cx) {
+ if let Some(pos) = layer.cursor_position(area, editor) {
return pos;
}
}
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,
diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs
index 809ec05d..ad15f6f2 100644
--- a/helix-view/src/theme.rs
+++ b/helix-view/src/theme.rs
@@ -157,6 +157,7 @@ impl Default for Theme {
"ui.background" => Style::default().bg(Color::Rgb(59, 34, 76)), // midnight
"ui.linenr" => Style::default().fg(Color::Rgb(90, 89, 119)), // comet
"ui.statusline" => Style::default().bg(Color::Rgb(40, 23, 51)), // revolver
+ "ui.popup" => Style::default().bg(Color::Rgb(40, 23, 51)), // revolver
"warning" => Style::default().fg(Color::Rgb(255, 205, 28)),
};
diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs
index 24b50d81..02eda72f 100644
--- a/helix-view/src/view.rs
+++ b/helix-view/src/view.rs
@@ -19,6 +19,7 @@ pub struct View {
pub first_line: usize,
pub area: Rect,
}
+// TODO: popups should be a thing on the view with a rect + text
impl View {
pub fn new(doc: Document) -> Result<Self, Error> {