Unnamed repository; edit this file 'description' to name the repository.
-rw-r--r--helix-term/src/ui/picker.rs21
-rw-r--r--helix-term/src/ui/prompt.rs78
-rw-r--r--helix-tui/src/buffer.rs57
3 files changed, 140 insertions, 16 deletions
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index 9b825583..9ff867c1 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -649,10 +649,6 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
// -- Render the input bar:
- let area = inner.clip_left(1).with_height(1);
- // render the prompt first since it will clear its background
- self.prompt.render(area, surface, cx);
-
let count = format!(
"{}{}/{}",
if status.running || self.matcher.active_injectors() > 0 {
@@ -663,6 +659,13 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
snapshot.matched_item_count(),
snapshot.item_count(),
);
+
+ let area = inner.clip_left(1).with_height(1);
+ let line_area = area.clip_right(count.len() as u16 + 1);
+
+ // render the prompt first since it will clear its background
+ self.prompt.render(line_area, surface, cx);
+
surface.set_stringn(
(area.x + area.width).saturating_sub(count.len() as u16 + 1),
area.y,
@@ -1073,7 +1076,15 @@ impl<I: 'static + Send + Sync, D: 'static + Send + Sync> Component for Picker<I,
let inner = block.inner(area);
// prompt area
- let area = inner.clip_left(1).with_height(1);
+ let render_preview =
+ self.show_preview && self.file_fn.is_some() && area.width > MIN_AREA_WIDTH_FOR_PREVIEW;
+
+ let picker_width = if render_preview {
+ area.width / 2
+ } else {
+ area.width
+ };
+ let area = inner.clip_left(1).with_height(1).with_width(picker_width);
self.prompt.cursor(area, editor)
}
diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs
index f44020c5..5f9745be 100644
--- a/helix-term/src/ui/prompt.rs
+++ b/helix-term/src/ui/prompt.rs
@@ -30,6 +30,12 @@ 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>,
@@ -82,6 +88,10 @@ 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,
@@ -389,7 +399,7 @@ impl Prompt {
const BASE_WIDTH: u16 = 30;
impl Prompt {
- pub fn render_prompt(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
+ pub fn render_prompt(&mut 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");
@@ -499,11 +509,20 @@ impl Prompt {
// render buffer text
surface.set_string(area.x, area.y + line, &self.prompt, prompt_color);
- let line_area = area.clip_left(self.prompt.len() as u16).clip_top(line);
+ 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(line_area.x, line_area.y, suggestion, suggestion_color);
+ 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(
@@ -514,9 +533,34 @@ impl Prompt {
None,
)
.into();
- text.render(line_area, surface, cx);
+ text.render(self.line_area, surface, cx);
} else {
- surface.set_string(line_area.x, line_area.y, self.line.clone(), prompt_color);
+ if self.line.len() < self.line_area.width as usize {
+ self.anchor = 0;
+ } else if self.cursor < self.anchor {
+ self.anchor = self.cursor;
+ } else if self.cursor - self.anchor > self.line_area.width as usize {
+ self.anchor = self.cursor - self.line_area.width as usize;
+ }
+
+ self.truncate_start = self.anchor > 0;
+ self.truncate_end = self.line.len() - self.anchor > self.line_area.width as usize;
+
+ // 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.cursor - self.anchor >= self.line_area.width as usize {
+ self.anchor += 1;
+ }
+
+ surface.set_string_anchored(
+ self.line_area.x,
+ self.line_area.y,
+ self.truncate_start,
+ self.truncate_end,
+ &self.line.as_str()[self.anchor..],
+ self.line_area.width as usize - self.truncate_end as usize,
+ |_| prompt_color,
+ );
}
}
}
@@ -686,14 +730,26 @@ impl Component for Prompt {
}
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.len() > 0 { 0 } else { 2 });
+
+ let mut col = area.left() as usize
+ + UnicodeWidthStr::width(&self.line[self.anchor..self.cursor.max(self.anchor)]);
+
+ // ensure the cursor does not go beyond elipses
+ if self.truncate_end && self.cursor - self.anchor >= self.line_area.width as usize {
+ col -= 1;
+ }
+
+ if self.truncate_start && self.cursor == self.anchor {
+ col += 1;
+ }
+
let line = area.height as usize - 1;
+
(
- Some(Position::new(
- area.y as usize + line,
- area.x as usize
- + self.prompt.len()
- + UnicodeWidthStr::width(&self.line[..self.cursor]),
- )),
+ Some(Position::new(area.y as usize + line, col)),
editor.config().cursor_shape.from_mode(Mode::Insert),
)
}
diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs
index d28c32fc..405a0acd 100644
--- a/helix-tui/src/buffer.rs
+++ b/helix-tui/src/buffer.rs
@@ -307,6 +307,63 @@ impl Buffer {
}
/// Print at most the first `width` characters of a string if enough space is available
+ /// until the end of the line.
+ /// If `ellipsis` is true appends a `…` at the end of truncated lines.
+ /// If `truncate_start` is `true`, adds a `…` at the beginning of truncated lines.
+ #[allow(clippy::too_many_arguments)]
+ pub fn set_string_anchored(
+ &mut self,
+ x: u16,
+ y: u16,
+ truncate_start: bool,
+ truncate_end: bool,
+ string: &str,
+ width: usize,
+ style: impl Fn(usize) -> Style, // Map a grapheme's string offset to a style
+ ) -> (u16, u16) {
+ // prevent panic if out of range
+ if !self.in_bounds(x, y) || width == 0 {
+ return (x, y);
+ }
+
+ let max_offset = min(
+ self.area.right() as usize - 1,
+ width.saturating_add(x as usize),
+ );
+ let mut start_index = self.index_of(x, y);
+ let mut end_index = self.index_of(max_offset as u16, y);
+
+ if truncate_end {
+ self.content[end_index].set_symbol("…");
+ end_index -= 1;
+ }
+
+ if truncate_start {
+ self.content[start_index].set_symbol("…");
+ start_index += 1;
+ }
+
+ let graphemes = string.grapheme_indices(true);
+
+ for (byte_offset, s) in graphemes.skip(truncate_start as usize) {
+ if start_index > end_index {
+ break;
+ }
+
+ self.content[start_index].set_symbol(s);
+ self.content[start_index].set_style(style(byte_offset));
+
+ for i in start_index + 1..end_index {
+ self.content[i].reset();
+ }
+
+ start_index += s.width();
+ }
+
+ (x, y)
+ }
+
+ /// Print at most the first `width` characters of a string if enough space is available
/// until the end of the line. If `ellipsis` is true appends a `…` at the end of
/// truncated lines. If `truncate_start` is `true`, truncate the beginning of the string
/// instead of the end.