Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/ui/prompt.rs')
| -rw-r--r-- | helix-term/src/ui/prompt.rs | 316 |
1 files changed, 84 insertions, 232 deletions
diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index d2448335..b19b9a9f 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -1,52 +1,34 @@ use crate::compositor::{Component, Compositor, Context, Event, EventResult}; use crate::{alt, ctrl, key, shift, ui}; -use arc_swap::ArcSwap; -use helix_core::syntax; -use helix_view::document::Mode; use helix_view::input::KeyEvent; use helix_view::keyboard::KeyCode; -use std::sync::Arc; use std::{borrow::Cow, ops::RangeFrom}; use tui::buffer::Buffer as Surface; -use tui::text::Span; -use tui::widgets::{Block, Widget}; +use tui::widgets::{Block, Borders, Widget}; use helix_core::{ - unicode::segmentation::{GraphemeCursor, UnicodeSegmentation}, - unicode::width::UnicodeWidthStr, - Position, + unicode::segmentation::GraphemeCursor, unicode::width::UnicodeWidthStr, Position, }; use helix_view::{ graphics::{CursorKind, Margin, Rect}, Editor, }; +pub type Completion = (RangeFrom<usize>, Cow<'static, str>); type PromptCharHandler = Box<dyn Fn(&mut Prompt, char, &Context)>; -pub type Completion = (RangeFrom<usize>, Span<'static>); -type CompletionFn = Box<dyn FnMut(&Editor, &str) -> Vec<Completion>>; -type CallbackFn = Box<dyn FnMut(&mut Context, &str, PromptEvent)>; -pub type DocFn = Box<dyn Fn(&str) -> Option<Cow<str>>>; - pub struct Prompt { prompt: Cow<'static, str>, line: String, cursor: usize, - // Fields used for Component callbacks and rendering: - line_area: Rect, - anchor: usize, - truncate_start: bool, - truncate_end: bool, - // --- completion: Vec<Completion>, selection: Option<usize>, history_register: Option<char>, history_pos: Option<usize>, - completion_fn: CompletionFn, - callback_fn: CallbackFn, - pub doc_fn: DocFn, + completion_fn: Box<dyn FnMut(&Editor, &str) -> Vec<Completion>>, + callback_fn: Box<dyn FnMut(&mut Context, &str, PromptEvent)>, + pub doc_fn: Box<dyn Fn(&str) -> Option<Cow<str>>>, next_char_handler: Option<PromptCharHandler>, - language: Option<(&'static str, Arc<ArcSwap<syntax::Loader>>)>, } #[derive(Clone, Copy, PartialEq, Eq)] @@ -90,10 +72,6 @@ impl Prompt { prompt, line: String::new(), cursor: 0, - line_area: Rect::default(), - anchor: 0, - truncate_start: false, - truncate_end: false, completion: Vec::new(), selection: None, history_register, @@ -102,34 +80,14 @@ impl Prompt { callback_fn: Box::new(callback_fn), doc_fn: Box::new(|_| None), next_char_handler: None, - language: None, } } - /// Gets the byte index in the input representing the current cursor location. - #[inline] - pub(crate) fn position(&self) -> usize { - self.cursor - } - pub fn with_line(mut self, line: String, editor: &Editor) -> Self { - self.set_line(line, editor); - self - } - - pub fn set_line(&mut self, line: String, editor: &Editor) { let cursor = line.len(); self.line = line; self.cursor = cursor; self.recalculate_completion(editor); - } - - pub fn with_language( - mut self, - language: &'static str, - loader: Arc<ArcSwap<syntax::Loader>>, - ) -> Self { - self.language = Some((language, loader)); self } @@ -137,23 +95,6 @@ impl Prompt { &self.line } - pub fn with_history_register(&mut self, history_register: Option<char>) -> &mut Self { - self.history_register = history_register; - self - } - - pub(crate) fn history_register(&self) -> Option<char> { - self.history_register - } - - pub(crate) fn first_history_completion<'a>( - &'a self, - editor: &'a Editor, - ) -> Option<Cow<'a, str>> { - self.history_register - .and_then(|reg| editor.registers.first(reg, editor)) - } - pub fn recalculate_completion(&mut self, editor: &Editor) { self.exit_selection(); self.completion = (self.completion_fn)(editor, &self.line); @@ -247,7 +188,15 @@ impl Prompt { position } Movement::StartOfLine => 0, - Movement::EndOfLine => self.line.len(), + Movement::EndOfLine => { + let mut cursor = + GraphemeCursor::new(self.line.len().saturating_sub(1), self.line.len(), false); + if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) { + pos + } else { + self.cursor + } + } Movement::None => self.cursor, } } @@ -345,8 +294,8 @@ impl Prompt { direction: CompletionDirection, ) { (self.callback_fn)(cx, &self.line, PromptEvent::Abort); - let mut values = match cx.editor.registers.read(register, cx.editor) { - Some(values) if values.len() > 0 => values.rev(), + let values = match cx.editor.registers.read(register) { + Some(values) if !values.is_empty() => values, _ => return, }; @@ -354,16 +303,13 @@ impl Prompt { let index = match direction { CompletionDirection::Forward => self.history_pos.map_or(0, |i| i + 1), - CompletionDirection::Backward => self - .history_pos - .unwrap_or_else(|| values.len()) - .saturating_sub(1), + CompletionDirection::Backward => { + self.history_pos.unwrap_or(values.len()).saturating_sub(1) + } } .min(end); - self.line = values.nth(index).unwrap().to_string(); - // Appease the borrow checker. - drop(values); + self.line = values[index].clone(); self.history_pos = Some(index); @@ -388,7 +334,7 @@ impl Prompt { let (range, item) = &self.completion[index]; - self.line.replace_range(range.clone(), &item.content); + self.line.replace_range(range.clone(), item); self.move_end(); } @@ -401,19 +347,17 @@ impl Prompt { const BASE_WIDTH: u16 = 30; impl Prompt { - pub fn render_prompt(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + pub fn render_prompt(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { let theme = &cx.editor.theme; let prompt_color = theme.get("ui.text"); let completion_color = theme.get("ui.menu"); let selected_color = theme.get("ui.menu.selected"); - let suggestion_color = theme.get("ui.text.inactive"); - let background = theme.get("ui.background"); // completion let max_len = self .completion .iter() - .map(|(_, completion)| completion.content.len() as u16) + .map(|(_, completion)| completion.len() as u16) .max() .unwrap_or(BASE_WIDTH) .max(BASE_WIDTH); @@ -421,8 +365,7 @@ impl Prompt { let cols = std::cmp::max(1, area.width / max_len); let col_width = (area.width.saturating_sub(cols)) / cols; - let height = (self.completion.len() as u16) - .div_ceil(cols) + let height = ((self.completion.len() as u16 + cols - 1) / cols) .min(10) // at most 10 rows (or less) .min(area.height.saturating_sub(1)); @@ -433,7 +376,7 @@ impl Prompt { height, ); - if completion_area.height > 0 && !self.completion.is_empty() { + if !self.completion.is_empty() { let area = completion_area; let background = theme.get("ui.menu"); @@ -452,22 +395,18 @@ impl Prompt { for (i, (_range, completion)) in self.completion.iter().enumerate().skip(offset).take(items) { - let is_selected = Some(i) == self.selection; - - let completion_item_style = if is_selected { - selected_color + let color = if Some(i) == self.selection { + selected_color // TODO: just invert bg } else { - completion_color.patch(completion.style) + completion_color }; - surface.set_stringn( area.x + col * (1 + col_width), area.y + row, - &completion.content, + completion, col_width.saturating_sub(1) as usize, - completion_item_style, + color, ); - row += 1; if row > area.height - 1 { row = 0; @@ -496,104 +435,37 @@ impl Prompt { let background = theme.get("ui.help"); surface.clear_with(area, background); - let block = Block::bordered() + let block = Block::default() // .title(self.title.as_str()) + .borders(Borders::ALL) .border_style(background); - let inner = block.inner(area).inner(Margin::horizontal(1)); + let inner = block.inner(area).inner(&Margin::horizontal(1)); block.render(area, surface); text.render(inner, surface, cx); } let line = area.height - 1; - surface.clear_with(area.clip_top(line), background); // render buffer text surface.set_string(area.x, area.y + line, &self.prompt, prompt_color); - self.line_area = area - .clip_left(self.prompt.len() as u16) - .clip_top(line) - .clip_right(2); - - if self.line.is_empty() { - // Show the most recently entered value as a suggestion. - if let Some(suggestion) = self.first_history_completion(cx.editor) { - surface.set_string( - self.line_area.x, - self.line_area.y, - suggestion, - suggestion_color, - ); - } - } else if let Some((language, loader)) = self.language.as_ref() { - let mut text: ui::text::Text = crate::ui::markdown::highlighted_code_block( - &self.line, - language, - Some(&cx.editor.theme), - &loader.load(), - None, - ) - .into(); - text.render(self.line_area, surface, cx); + let input: Cow<str> = if self.line.is_empty() { + // latest value in the register list + self.history_register + .and_then(|reg| cx.editor.registers.last(reg)) + .map(|entry| entry.into()) + .unwrap_or_else(|| Cow::from("")) } else { - let line_width = self.line_area.width as usize; - - if self.line.width() < line_width { - self.anchor = 0; - } else if self.cursor <= self.anchor { - // Ensure the grapheme under the cursor is in view. - self.anchor = self.line[..self.cursor] - .grapheme_indices(true) - .next_back() - .map(|(i, _)| i) - .unwrap_or_default(); - } else if self.line[self.anchor..self.cursor].width() > line_width { - // Set the anchor to the last grapheme cluster before the width is exceeded. - let mut width = 0; - self.anchor = self.line[..self.cursor] - .grapheme_indices(true) - .rev() - .find_map(|(idx, g)| { - width += g.width(); - if width > line_width { - Some(idx + g.len()) - } else { - None - } - }) - .unwrap(); - } - - self.truncate_start = self.anchor > 0; - self.truncate_end = self.line[self.anchor..].width() > line_width; - - // if we keep inserting characters just before the end elipsis, we move the anchor - // so that those new characters are displayed - if self.truncate_end && self.line[self.anchor..self.cursor].width() >= line_width { - // Move the anchor forward by one non-zero-width grapheme. - self.anchor += self.line[self.anchor..] - .grapheme_indices(true) - .find_map(|(idx, g)| { - if g.width() > 0 { - Some(idx + g.len()) - } else { - None - } - }) - .unwrap(); - } + self.line.as_str().into() + }; - surface.set_string_anchored( - self.line_area.x, - self.line_area.y, - self.truncate_start, - self.truncate_end, - &self.line.as_str()[self.anchor..], - line_width, - |_| prompt_color, - ); - } + surface.set_string( + area.x + self.prompt.len() as u16, + area.y + line, + &input, + prompt_color, + ); } } @@ -627,22 +499,12 @@ impl Component for Prompt { ctrl!('e') | key!(End) => self.move_end(), ctrl!('a') | key!(Home) => self.move_start(), ctrl!('w') | alt!(Backspace) | ctrl!(Backspace) => { - self.delete_word_backwards(cx.editor); - (self.callback_fn)(cx, &self.line, PromptEvent::Update); - } - alt!('d') | alt!(Delete) | ctrl!(Delete) => { - self.delete_word_forwards(cx.editor); - (self.callback_fn)(cx, &self.line, PromptEvent::Update); + self.delete_word_backwards(cx.editor) } - ctrl!('k') => { - self.kill_to_end_of_line(cx.editor); - (self.callback_fn)(cx, &self.line, PromptEvent::Update); - } - ctrl!('u') => { - self.kill_to_start_of_line(cx.editor); - (self.callback_fn)(cx, &self.line, PromptEvent::Update); - } - ctrl!('h') | key!(Backspace) | shift!(Backspace) => { + alt!('d') | alt!(Delete) | ctrl!(Delete) => self.delete_word_forwards(cx.editor), + ctrl!('k') => self.kill_to_end_of_line(cx.editor), + ctrl!('u') => self.kill_to_start_of_line(cx.editor), + ctrl!('h') | key!(Backspace) => { self.delete_char_backwards(cx.editor); (self.callback_fn)(cx, &self.line, PromptEvent::Update); } @@ -673,29 +535,26 @@ impl Component for Prompt { self.recalculate_completion(cx.editor); } else { let last_item = self - .first_history_completion(cx.editor) - .map(|entry| entry.to_string()) - .unwrap_or_else(|| String::from("")); + .history_register + .and_then(|reg| cx.editor.registers.last(reg).cloned()) + .map(|entry| entry.into()) + .unwrap_or_else(|| Cow::from("")); // handle executing with last command in history if nothing entered - let input = if self.line.is_empty() { - &last_item + let input: Cow<str> = if self.line.is_empty() { + last_item } else { if last_item != self.line { // store in history if let Some(register) = self.history_register { - if let Err(err) = - cx.editor.registers.push(register, self.line.clone()) - { - cx.editor.set_error(err.to_string()); - } + cx.editor.registers.push(register, self.line.clone()); }; } - &self.line + self.line.as_str().into() }; - (self.callback_fn)(cx, input, PromptEvent::Validate); + (self.callback_fn)(cx, &input, PromptEvent::Validate); return close_fn; } @@ -727,16 +586,25 @@ impl Component for Prompt { self.completion = cx .editor .registers - .iter_preview() - .map(|(ch, preview)| (0.., format!("{} {}", ch, &preview).into())) + .inner() + .iter() + .map(|(ch, reg)| { + let content = reg + .read() + .get(0) + .and_then(|s| s.lines().next().to_owned()) + .unwrap_or_default(); + (0.., format!("{} {}", ch, &content).into()) + }) .collect(); self.next_char_handler = Some(Box::new(|prompt, c, context| { prompt.insert_str( - &context + context .editor .registers - .first(c, context.editor) - .unwrap_or_default(), + .read(c) + .and_then(|r| r.first()) + .map_or("", |r| r.as_str()), context.editor, ); })); @@ -761,32 +629,16 @@ impl Component for Prompt { self.render_prompt(area, surface, cx) } - fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) { - let area = area - .clip_left(self.prompt.len() as u16) - .clip_right(if self.prompt.is_empty() { 2 } else { 0 }); - - let mut col = area.left() as usize + self.line[self.anchor..self.cursor].width(); - - // ensure the cursor does not go beyond elipses - if self.truncate_end - && self.line[self.anchor..self.cursor].width() >= self.line_area.width as usize - { - col -= 1; - } - - if self.truncate_start && self.cursor == self.anchor { - col += self.line[self.cursor..] - .graphemes(true) - .next() - .map_or(0, |g| g.width()); - } - + fn cursor(&self, area: Rect, _editor: &Editor) -> (Option<Position>, CursorKind) { let line = area.height as usize - 1; - ( - Some(Position::new(area.y as usize + line, col)), - editor.config().cursor_shape.from_mode(Mode::Insert), + Some(Position::new( + area.y as usize + line, + area.x as usize + + self.prompt.len() + + UnicodeWidthStr::width(&self.line[..self.cursor]), + )), + CursorKind::Block, ) } } |