From b688246f95e26c0af193dc9b239c96a435ae75a6 Mon Sep 17 00:00:00 2001 From: Yomain Date: Tue, 29 Oct 2024 22:49:53 +0100 Subject: [PATCH] Fix: scope prompt line to its are This introduces the notion of anchor in the prompt. The prompt anchor represents the position of the left-side of the input. This anchor is needed so that we are able to correctly display the prompt input when its length is longer than the one of the prompt area itself --- helix-term/src/ui/picker.rs | 21 +++++++--- helix-term/src/ui/prompt.rs | 78 +++++++++++++++++++++++++++++++------ helix-tui/src/buffer.rs | 57 +++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 16 deletions(-) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index df8d52ebd2f8..13daf58bb20d 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -649,10 +649,6 @@ impl Picker { // -- 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 Picker { 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 Component for Picker 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 1e443ce7ff0e..c7cdbdd8d228 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -27,7 +27,11 @@ pub type DocFn = Box Option>>; pub struct Prompt { prompt: Cow<'static, str>, line: String, + line_area: Rect, cursor: usize, + anchor: usize, + truncate_start: bool, + truncate_end: bool, completion: Vec, selection: Option, history_register: Option, @@ -79,7 +83,11 @@ impl Prompt { Self { prompt, line: String::new(), + line_area: Rect::default(), cursor: 0, + anchor: 0, + truncate_start: false, + truncate_end: false, completion: Vec::new(), selection: None, history_register, @@ -329,6 +337,7 @@ impl Prompt { pub fn clear(&mut self, editor: &Editor) { self.line.clear(); self.cursor = 0; + self.recalculate_completion(editor); } @@ -395,13 +404,14 @@ 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"); 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 @@ -501,11 +511,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( @@ -516,9 +535,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, + ); } } } @@ -688,14 +732,26 @@ impl Component for Prompt { } fn cursor(&self, area: Rect, editor: &Editor) -> (Option, 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 d28c32fcca9f..405a0acdce82 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -306,6 +306,63 @@ impl Buffer { self.set_string_truncated_at_end(x, y, string.as_ref(), width, style) } + /// 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