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