on master help sign in
popup: wip work on completion popups
commitd4b85ce18d8a9bb535eaeae9e2c7421ef81c81e9 authorBlaž Hrastnik <blaz@mxxn.io> dateTue 9 Feb 2021, 15:40 (6 years ago) parent755632f
10 files changed+272−67
helix-term/src/application.rs+2−2 helix-term/src/commands.rs+63−27 helix-term/src/compositor.rs+9−33 helix-term/src/ui/editor.rs+2−2 helix-term/src/ui/menu.rs+189 helix-term/src/ui/mod.rs+2 helix-term/src/ui/picker.rs+2−2 helix-term/src/ui/prompt.rs+1−1 helix-view/src/theme.rs+1 helix-view/src/view.rs+1
helix-term/src/application.rs+2−2
@@ -70,7 +70,7 @@ impl Application {
7070 let area = self.terminal.size().unwrap();
7171
7272 compositor.render(area, self.terminal.current_buffer_mut(), &mut cx);
73− let pos = compositor.cursor_position(area, &mut cx);
73+ let pos = compositor.cursor_position(area, &editor);
7474
7575 self.terminal.draw();
7676 self.terminal.set_cursor(pos.col as u16, pos.row as u16);
@@ -112,7 +112,7 @@ impl Application {
112112 .handle_event(Event::Resize(width, height), &mut cx)
113113 }
114114 Some(Ok(event)) => self.compositor.handle_event(event, &mut cx),
115− Some(Err(x)) => panic!(x),
115+ Some(Err(x)) => panic!("{}", x),
116116 None => panic!(),
117117 };
118118
helix-term/src/commands.rs+63−27
@@ -910,7 +910,8 @@ pub fn completion(cx: &mut Context) {
910910
911911 // TODO: if no completion, show some message or something
912912 if !res.is_empty() {
913− let picker = ui::Picker::new(
913+ let snapshot = cx.doc().state.clone();
914+ let mut menu = ui::Menu::new(
914915 res,
915916 |item| {
916917 // format_fn
@@ -918,40 +919,75 @@ pub fn completion(cx: &mut Context) {
918919
919920 // TODO: use item.filter_text for filtering
920921 },
921− |editor: &mut Editor, item| {
922− use helix_lsp::{lsp, util};
923− // determine what to insert: text_edit | insert_text | label
924− let edit = if let Some(edit) = &item.text_edit {
925− match edit {
926− lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
927− lsp::CompletionTextEdit::InsertAndReplace(item) => {
928− unimplemented!("completion: insert_and_replace {:?}", item)
929− }
922+ move |editor: &mut Editor, item, event| {
923+ match event {
924+ PromptEvent::Abort => {
925+ // revert state
926+ let doc = &mut editor.view_mut().doc;
927+ doc.state = snapshot.clone();
930928 }
931− } else {
932− item.insert_text.as_ref().unwrap_or(&item.label);
933− unimplemented!();
934− // lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text
935− // and we insert at position.
936− };
929+ PromptEvent::Validate => {
930+ let doc = &mut editor.view_mut().doc;
931+
932+ // revert state to what it was before the last update
933+ doc.state = snapshot.clone();
934+
935+ // extract as fn(doc, item):
936+
937+ // TODO: need to apply without composing state...
938+ // TODO: need to update lsp on accept/cancel by diffing the snapshot with
939+ // the final state?
940+ // -> on update simply update the snapshot, then on accept redo the call,
941+ // finally updating doc.changes + notifying lsp.
942+ //
943+ // or we could simply use doc.undo + apply when changing between options
944+
945+ let item = item.unwrap();
946+
947+ use helix_lsp::{lsp, util};
948+ // determine what to insert: text_edit | insert_text | label
949+ let edit = if let Some(edit) = &item.text_edit {
950+ match edit {
951+ lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
952+ lsp::CompletionTextEdit::InsertAndReplace(item) => {
953+ unimplemented!("completion: insert_and_replace {:?}", item)
954+ }
955+ }
956+ } else {
957+ item.insert_text.as_ref().unwrap_or(&item.label);
958+ unimplemented!();
959+ // lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text
960+ // and we insert at position.
961+ };
962+
963+ // TODO: merge edit with additional_text_edits
964+ if let Some(additional_edits) = &item.additional_text_edits {
965+ if !additional_edits.is_empty() {
966+ unimplemented!(
967+ "completion: additional_text_edits: {:?}",
968+ additional_edits
969+ );
970+ }
971+ }
937972
938− // TODO: merge edit with additional_text_edits
939− if let Some(additional_edits) = &item.additional_text_edits {
940− if !additional_edits.is_empty() {
941− unimplemented!("completion: additional_text_edits: {:?}", additional_edits);
973+ let transaction =
974+ util::generate_transaction_from_edits(&doc.state, vec![edit]);
975+ doc.apply(&transaction);
976+ // TODO: append_changes_to_history(cx); if not in insert mode?
942977 }
943− }
944−
945− let doc = &mut editor.view_mut().doc;
946− let transaction = util::generate_transaction_from_edits(&doc.state, vec![edit]);
947− doc.apply(&transaction);
948− // TODO: append_changes_to_history(cx); if not in insert mode?
978+ _ => (),
979+ };
949980 },
950981 );
951982
952983 cx.callback = Some(Box::new(
953984 move |compositor: &mut Compositor, editor: &mut Editor| {
954− compositor.push(Box::new(picker));
985+ let area = tui::layout::Rect::default(); // TODO: unused remove from cursor_position
986+ let mut pos = compositor.cursor_position(area, editor);
987+ pos.row += 1; // shift down by one row
988+ menu.set_position(pos);
989+
990+ compositor.push(Box::new(menu));
955991 },
956992 ));
957993
helix-term/src/compositor.rs+9−33
@@ -54,13 +54,11 @@ pub trait Component {
5454
5555 fn render(&self, area: Rect, frame: &mut Surface, ctx: &mut Context);
5656
57− fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> {
57+ fn cursor_position(&self, area: Rect, ctx: &Editor) -> Option<Position> {
5858 None
5959 }
6060}
6161
62−// struct Editor { };
63−
6462// For v1:
6563// Child views are something each view needs to handle on it's own for now, positioning and sizing
6664// options, focus tracking. In practice this is simple: we only will need special solving for
@@ -83,29 +81,6 @@ pub trait Component {
8381// - a popup panel / dialog with it's own interactions
8482// - an autocomplete popup that doesn't change focus
8583
86−//fn main() {
87−// let root = Editor::new();
88−// let compositor = Compositor::new();
89−
90−// compositor.push(root);
91−
92−// // pos: clip to bottom of screen
93−// compositor.push_at(pos, Prompt::new(
94−// ":",
95−// (),
96−// |input: &str| match input {}
97−// )); // TODO: this Prompt needs to somehow call compositor.pop() on close, but it can't refer to parent
98−// // Cursive solves this by allowing to return a special result on process_event
99−// // that's either Ignore | Consumed(Opt<C>) where C: fn (Compositor) -> ()
100−
101−// // TODO: solve popup focus: we want to push autocomplete popups on top of the current layer
102−// // but retain the focus where it was. The popup will also need to update as we type into the
103−// // textarea. It should also capture certain input, such as tab presses etc
104−// //
105−// // 1) This could be faked by the top layer pushing down edits into the previous layer.
106−// // 2) Alternatively,
107−//}
108−
10984pub struct Compositor {
11085 layers: Vec<Box<dyn Component>>,
11186}
@@ -124,14 +99,15 @@ impl Compositor {
12499 }
125100
126101 pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool {
127− // TODO: custom focus
128− if let Some(layer) = self.layers.last_mut() {
129− return match layer.handle_event(event, cx) {
102+ // propagate events through the layers until we either find a layer that consumes it or we
103+ // run out of layers (event bubbling)
104+ for layer in self.layers.iter_mut().rev() {
105+ match layer.handle_event(event, cx) {
130106 EventResult::Consumed(Some(callback)) => {
131107 callback(self, cx.editor);
132− true
108+ return true;
133109 }
134− EventResult::Consumed(None) => true,
110+ EventResult::Consumed(None) => return true,
135111 EventResult::Ignored => false,
136112 };
137113 }
@@ -144,9 +120,9 @@ impl Compositor {
144120 }
145121 }
146122
147− pub fn cursor_position(&self, area: Rect, cx: &mut Context) -> Position {
123+ pub fn cursor_position(&self, area: Rect, editor: &Editor) -> Position {
148124 for layer in self.layers.iter().rev() {
149− if let Some(pos) = layer.cursor_position(area, cx) {
125+ if let Some(pos) = layer.cursor_position(area, editor) {
150126 return pos;
151127 }
152128 }
helix-term/src/ui/editor.rs+2−2
@@ -365,12 +365,12 @@ impl Component for EditorView {
365365 // TODO: drop unwrap
366366 }
367367
368− fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> {
368+ fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
369369 // match view.doc.mode() {
370370 // Mode::Insert => write!(stdout, "\x1B[6 q"),
371371 // mode => write!(stdout, "\x1B[2 q"),
372372 // };
373− let view = ctx.editor.view();
373+ let view = editor.view();
374374 let cursor = view.doc.state.selection().cursor();
375375
376376 let mut pos = view
helix-term/src/ui/menu.rsnew file+189
@@ -0,0 +1,189 @@
1+use crate::compositor::{Component, Compositor, Context, EventResult};
2+use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
3+use tui::buffer::Buffer as Surface;
4+use tui::{
5+ layout::Rect,
6+ style::{Color, Style},
7+ widgets::{Block, Borders},
8+};
9+
10+use std::borrow::Cow;
11+
12+use helix_core::Position;
13+use helix_view::Editor;
14+
15+// TODO: factor out a popup component that we can reuse for displaying docs on autocomplete,
16+// diagnostics popups, etc.
17+
18+pub struct Menu<T> {
19+ options: Vec<T>,
20+
21+ cursor: usize,
22+
23+ position: Position,
24+
25+ format_fn: Box<dyn Fn(&T) -> Cow<str>>,
26+ callback_fn: Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>,
27+}
28+
29+impl<T> Menu<T> {
30+ // TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different
31+ // rendering)
32+ pub fn new(
33+ options: Vec<T>,
34+ format_fn: impl Fn(&T) -> Cow<str> + 'static,
35+ callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
36+ ) -> Self {
37+ Self {
38+ options,
39+ cursor: 0,
40+ position: Position::default(),
41+ format_fn: Box::new(format_fn),
42+ callback_fn: Box::new(callback_fn),
43+ }
44+ }
45+
46+ pub fn set_position(&mut self, pos: Position) {
47+ self.position = pos;
48+ }
49+
50+ pub fn move_up(&mut self) {
51+ self.cursor = self.cursor.saturating_sub(1);
52+ }
53+
54+ pub fn move_down(&mut self) {
55+ // TODO: len - 1
56+ if self.cursor < self.options.len() {
57+ self.cursor += 1;
58+ }
59+ }
60+
61+ pub fn selection(&self) -> Option<&T> {
62+ self.options.get(self.cursor)
63+ }
64+}
65+
66+use super::PromptEvent as MenuEvent;
67+
68+impl<T> Component for Menu<T> {
69+ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
70+ let event = match event {
71+ Event::Key(event) => event,
72+ _ => return EventResult::Ignored,
73+ };
74+
75+ let close_fn = EventResult::Consumed(Some(Box::new(
76+ |compositor: &mut Compositor, editor: &mut Editor| {
77+ // remove the layer
78+ compositor.pop();
79+ },
80+ )));
81+
82+ match event {
83+ // esc or ctrl-c aborts the completion and closes the menu
84+ KeyEvent {
85+ code: KeyCode::Esc, ..
86+ }
87+ | KeyEvent {
88+ code: KeyCode::Char('c'),
89+ modifiers: KeyModifiers::CONTROL,
90+ } => {
91+ (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Abort);
92+ return close_fn;
93+ }
94+ // arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc)
95+ KeyEvent {
96+ code: KeyCode::Tab,
97+ modifiers: KeyModifiers::SHIFT,
98+ }
99+ | KeyEvent {
100+ code: KeyCode::Up, ..
101+ }
102+ | KeyEvent {
103+ code: KeyCode::Char('p'),
104+ modifiers: KeyModifiers::CONTROL,
105+ } => {
106+ self.move_up();
107+ (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
108+ return EventResult::Consumed(None);
109+ }
110+ // arrow down/ctrl-n/tab advances completion choice (including updating the doc)
111+ KeyEvent {
112+ code: KeyCode::Tab,
113+ modifiers: KeyModifiers::NONE,
114+ }
115+ | KeyEvent {
116+ code: KeyCode::Down,
117+ ..
118+ }
119+ | KeyEvent {
120+ code: KeyCode::Char('n'),
121+ modifiers: KeyModifiers::CONTROL,
122+ } => {
123+ self.move_down();
124+ (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
125+ return EventResult::Consumed(None);
126+ }
127+ KeyEvent {
128+ code: KeyCode::Enter,
129+ ..
130+ } => {
131+ (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Validate);
132+ return close_fn;
133+ }
134+ // KeyEvent {
135+ // code: KeyCode::Char(c),
136+ // modifiers: KeyModifiers::NONE,
137+ // } => {
138+ // self.insert_char(c);
139+ // (self.callback_fn)(cx.editor, &self.line, MenuEvent::Update);
140+ // }
141+
142+ // / -> edit_filter?
143+ //
144+ // enter confirms the match and closes the menu
145+ // typing filters the menu
146+ // if we run out of options the menu closes itself
147+ _ => (),
148+ }
149+ // for some events, we want to process them but send ignore, specifically all input except
150+ // tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll.
151+ // EventResult::Consumed(None)
152+ EventResult::Ignored
153+ }
154+ fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
155+ // render a box at x, y. Width equal to max width of item.
156+ // initially limit to n items, add support for scrolling
157+ //
158+ const MAX: usize = 5;
159+ let rows = std::cmp::min(self.options.len(), MAX) as u16;
160+ let area = Rect::new(self.position.col as u16, self.position.row as u16, 30, rows);
161+
162+ // clear area
163+ let background = cx.editor.theme.get("ui.popup");
164+ for y in area.top()..area.bottom() {
165+ for x in area.left()..area.right() {
166+ let cell = surface.get_mut(x, y);
167+ cell.reset();
168+ // cell.symbol.clear();
169+ cell.set_style(background);
170+ }
171+ }
172+
173+ // -- Render the contents:
174+
175+ let style = Style::default().fg(Color::Rgb(164, 160, 232)); // lavender
176+ let selected = Style::default().fg(Color::Rgb(255, 255, 255));
177+
178+ for (i, option) in self.options.iter().take(rows as usize).enumerate() {
179+ // TODO: set bg for the whole row if selected
180+ surface.set_stringn(
181+ area.x,
182+ area.y + i as u16,
183+ (self.format_fn)(option),
184+ area.width as usize - 1,
185+ if i == self.cursor { selected } else { style },
186+ );
187+ }
188+ }
189+}
helix-term/src/ui/mod.rs+2
@@ -1,8 +1,10 @@
11mod editor;
2+mod menu;
23mod picker;
34mod prompt;
45
56pub use editor::EditorView;
7+pub use menu::Menu;
68pub use picker::Picker;
79pub use prompt::{Prompt, PromptEvent};
810
helix-term/src/ui/picker.rs+2−2
@@ -257,7 +257,7 @@ impl<T> Component for Picker<T> {
257257 }
258258 }
259259
260− fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> {
261− self.prompt.cursor_position(area, ctx)
260+ fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
261+ self.prompt.cursor_position(area, editor)
262262 }
263263}
helix-term/src/ui/prompt.rs+1−1
@@ -235,7 +235,7 @@ impl Component for Prompt {
235235 self.render_prompt(area, surface, &cx.editor.theme)
236236 }
237237
238− fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> {
238+ fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
239239 Some(Position::new(
240240 area.height as usize,
241241 area.x as usize + self.prompt.len() + self.cursor,
helix-view/src/theme.rs+1
@@ -157,6 +157,7 @@ impl Default for Theme {
157157 "ui.background" => Style::default().bg(Color::Rgb(59, 34, 76)), // midnight
158158 "ui.linenr" => Style::default().fg(Color::Rgb(90, 89, 119)), // comet
159159 "ui.statusline" => Style::default().bg(Color::Rgb(40, 23, 51)), // revolver
160+ "ui.popup" => Style::default().bg(Color::Rgb(40, 23, 51)), // revolver
160161
161162 "warning" => Style::default().fg(Color::Rgb(255, 205, 28)),
162163 };
helix-view/src/view.rs+1
@@ -19,6 +19,7 @@ pub struct View {
1919 pub first_line: usize,
2020 pub area: Rect,
2121}
22+// TODO: popups should be a thing on the view with a rect + text
2223
2324impl View {
2425 pub fn new(doc: Document) -> Result<Self, Error> {