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.rs316
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,
)
}
}