on master help sign in
popup: wip work on completion popups
d4b85ceBlaž Hrastnik
helix-term/src/application.rs+2−2
@@ -70,7 +70,7 @@ impl Application {
70 let area = self.terminal.size().unwrap();
70 let area = self.terminal.size().unwrap();
71
71
72 compositor.render(area, self.terminal.current_buffer_mut(), &mut cx);
72 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);
74
74
75 self.terminal.draw();
75 self.terminal.draw();
76 self.terminal.set_cursor(pos.col as u16, pos.row as u16);
76 self.terminal.set_cursor(pos.col as u16, pos.row as u16);
@@ -112,7 +112,7 @@ impl Application {
112 .handle_event(Event::Resize(width, height), &mut cx)
112 .handle_event(Event::Resize(width, height), &mut cx)
113 }
113 }
114 Some(Ok(event)) => self.compositor.handle_event(event, &mut cx),
114 Some(Ok(event)) => self.compositor.handle_event(event, &mut cx),
115− Some(Err(x)) => panic!(x),
115+ Some(Err(x)) => panic!("{}", x),
116 None => panic!(),
116 None => panic!(),
117 };
117 };
118
118
helix-term/src/commands.rs+63−27
@@ -910,7 +910,8 @@ pub fn completion(cx: &mut Context) {
910
910
911 // TODO: if no completion, show some message or something
911 // TODO: if no completion, show some message or something
912 if !res.is_empty() {
912 if !res.is_empty() {
913− let picker = ui::Picker::new(
913+ let snapshot = cx.doc().state.clone();
914+ let mut menu = ui::Menu::new(
914 res,
915 res,
915 |item| {
916 |item| {
916 // format_fn
917 // format_fn
@@ -918,40 +919,75 @@ pub fn completion(cx: &mut Context) {
918
919
919 // TODO: use item.filter_text for filtering
920 // TODO: use item.filter_text for filtering
920 },
921 },
921− |editor: &mut Editor, item| {
922+ move |editor: &mut Editor, item, event| {
922− use helix_lsp::{lsp, util};
923+ match event {
923− // determine what to insert: text_edit | insert_text | label
924+ PromptEvent::Abort => {
924− let edit = if let Some(edit) = &item.text_edit {
925+ // revert state
925− match edit {
926+ let doc = &mut editor.view_mut().doc;
926− lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
927+ doc.state = snapshot.clone();
927− lsp::CompletionTextEdit::InsertAndReplace(item) => {
928− unimplemented!("completion: insert_and_replace {:?}", item)
929− }
930 }
928 }
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+ }
931− } else {
956+ } else {
932− item.insert_text.as_ref().unwrap_or(&item.label);
957+ item.insert_text.as_ref().unwrap_or(&item.label);
933− unimplemented!();
958+ unimplemented!();
934− // lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text
959+ // lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text
935− // and we insert at position.
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
936− };
969+ );
970+ }
971+ }
937
972
938− // TODO: merge edit with additional_text_edits
973+ let transaction =
939− if let Some(additional_edits) = &item.additional_text_edits {
974+ util::generate_transaction_from_edits(&doc.state, vec![edit]);
940− if !additional_edits.is_empty() {
975+ doc.apply(&transaction);
941− unimplemented!("completion: additional_text_edits: {:?}", additional_edits);
976+ // TODO: append_changes_to_history(cx); if not in insert mode?
942 }
977 }
943− }
978+ _ => (),
944−
979+ };
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?
949 },
980 },
950 );
981 );
951
982
952 cx.callback = Some(Box::new(
983 cx.callback = Some(Box::new(
953 move |compositor: &mut Compositor, editor: &mut Editor| {
984 move |compositor: &mut Compositor, editor: &mut Editor| {
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+
954− compositor.push(Box::new(picker));
990+ compositor.push(Box::new(menu));
955 },
991 },
956 ));
992 ));
957
993
helix-term/src/compositor.rs+9−33
@@ -54,13 +54,11 @@ pub trait Component {
54
54
55 fn render(&self, area: Rect, frame: &mut Surface, ctx: &mut Context);
55 fn render(&self, area: Rect, frame: &mut Surface, ctx: &mut Context);
56
56
57− fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> {
57+ fn cursor_position(&self, area: Rect, ctx: &Editor) -> Option<Position> {
58 None
58 None
59 }
59 }
60}
60}
61
61
62−// struct Editor { };
63−
64// For v1:
62// For v1:
65// Child views are something each view needs to handle on it's own for now, positioning and sizing
63// Child views are something each view needs to handle on it's own for now, positioning and sizing
66// options, focus tracking. In practice this is simple: we only will need special solving for
64// options, focus tracking. In practice this is simple: we only will need special solving for
@@ -83,29 +81,6 @@ pub trait Component {
83// - a popup panel / dialog with it's own interactions
81// - a popup panel / dialog with it's own interactions
84// - an autocomplete popup that doesn't change focus
82// - an autocomplete popup that doesn't change focus
85
83
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−
109pub struct Compositor {
84pub struct Compositor {
110 layers: Vec<Box<dyn Component>>,
85 layers: Vec<Box<dyn Component>>,
111}
86}
@@ -124,14 +99,15 @@ impl Compositor {
124 }
99 }
125
100
126 pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool {
101 pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool {
127− // TODO: custom focus
102+ // propagate events through the layers until we either find a layer that consumes it or we
128− if let Some(layer) = self.layers.last_mut() {
103+ // run out of layers (event bubbling)
104+ for layer in self.layers.iter_mut().rev() {
129− return match layer.handle_event(event, cx) {
105+ match layer.handle_event(event, cx) {
130 EventResult::Consumed(Some(callback)) => {
106 EventResult::Consumed(Some(callback)) => {
131 callback(self, cx.editor);
107 callback(self, cx.editor);
132− true
108+ return true;
133 }
109 }
134− EventResult::Consumed(None) => true,
110+ EventResult::Consumed(None) => return true,
135 EventResult::Ignored => false,
111 EventResult::Ignored => false,
136 };
112 };
137 }
113 }
@@ -144,9 +120,9 @@ impl Compositor {
144 }
120 }
145 }
121 }
146
122
147− pub fn cursor_position(&self, area: Rect, cx: &mut Context) -> Position {
123+ pub fn cursor_position(&self, area: Rect, editor: &Editor) -> Position {
148 for layer in self.layers.iter().rev() {
124 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) {
150 return pos;
126 return pos;
151 }
127 }
152 }
128 }
helix-term/src/ui/editor.rs+2−2
@@ -365,12 +365,12 @@ impl Component for EditorView {
365 // TODO: drop unwrap
365 // TODO: drop unwrap
366 }
366 }
367
367
368− fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> {
368+ fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
369 // match view.doc.mode() {
369 // match view.doc.mode() {
370 // Mode::Insert => write!(stdout, "\x1B[6 q"),
370 // Mode::Insert => write!(stdout, "\x1B[6 q"),
371 // mode => write!(stdout, "\x1B[2 q"),
371 // mode => write!(stdout, "\x1B[2 q"),
372 // };
372 // };
373− let view = ctx.editor.view();
373+ let view = editor.view();
374 let cursor = view.doc.state.selection().cursor();
374 let cursor = view.doc.state.selection().cursor();
375
375
376 let mut pos = view
376 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 @@
1mod editor;
1mod editor;
2+mod menu;
2mod picker;
3mod picker;
3mod prompt;
4mod prompt;
4
5
5pub use editor::EditorView;
6pub use editor::EditorView;
7+pub use menu::Menu;
6pub use picker::Picker;
8pub use picker::Picker;
7pub use prompt::{Prompt, PromptEvent};
9pub use prompt::{Prompt, PromptEvent};
8
10
helix-term/src/ui/picker.rs+2−2
@@ -257,7 +257,7 @@ impl<T> Component for Picker<T> {
257 }
257 }
258 }
258 }
259
259
260− fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> {
260+ fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
261− self.prompt.cursor_position(area, ctx)
261+ self.prompt.cursor_position(area, editor)
262 }
262 }
263}
263}
helix-term/src/ui/prompt.rs+1−1
@@ -235,7 +235,7 @@ impl Component for Prompt {
235 self.render_prompt(area, surface, &cx.editor.theme)
235 self.render_prompt(area, surface, &cx.editor.theme)
236 }
236 }
237
237
238− fn cursor_position(&self, area: Rect, ctx: &mut Context) -> Option<Position> {
238+ fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
239 Some(Position::new(
239 Some(Position::new(
240 area.height as usize,
240 area.height as usize,
241 area.x as usize + self.prompt.len() + self.cursor,
241 area.x as usize + self.prompt.len() + self.cursor,
helix-view/src/theme.rs+1
@@ -157,6 +157,7 @@ impl Default for Theme {
157 "ui.background" => Style::default().bg(Color::Rgb(59, 34, 76)), // midnight
157 "ui.background" => Style::default().bg(Color::Rgb(59, 34, 76)), // midnight
158 "ui.linenr" => Style::default().fg(Color::Rgb(90, 89, 119)), // comet
158 "ui.linenr" => Style::default().fg(Color::Rgb(90, 89, 119)), // comet
159 "ui.statusline" => Style::default().bg(Color::Rgb(40, 23, 51)), // revolver
159 "ui.statusline" => Style::default().bg(Color::Rgb(40, 23, 51)), // revolver
160+ "ui.popup" => Style::default().bg(Color::Rgb(40, 23, 51)), // revolver
160
161
161 "warning" => Style::default().fg(Color::Rgb(255, 205, 28)),
162 "warning" => Style::default().fg(Color::Rgb(255, 205, 28)),
162 };
163 };
helix-view/src/view.rs+1
@@ -19,6 +19,7 @@ pub struct View {
19 pub first_line: usize,
19 pub first_line: usize,
20 pub area: Rect,
20 pub area: Rect,
21}
21}
22+// TODO: popups should be a thing on the view with a rect + text
22
23
23impl View {
24impl View {
24 pub fn new(doc: Document) -> Result<Self, Error> {
25 pub fn new(doc: Document) -> Result<Self, Error> {