diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index ecf8111ab..6ffe1a89d 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -649,10 +649,6 @@ fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) // -- 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 @@ fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) 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 @@ fn cursor(&self, area: Rect, editor: &Editor) -> (Option, CursorKind) 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 6ba2fcb9e..c1c8fe855 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -28,6 +28,7 @@ pub struct Prompt { prompt: Cow<'static, str>, line: String, cursor: usize, + anchor: usize, completion: Vec, selection: Option, history_register: Option, @@ -80,6 +81,7 @@ pub fn new( prompt, line: String::new(), cursor: 0, + anchor: 0, completion: Vec::new(), selection: None, history_register, @@ -329,6 +331,7 @@ pub fn kill_to_end_of_line(&mut self, editor: &Editor) { pub fn clear(&mut self, editor: &Editor) { self.line.clear(); self.cursor = 0; + self.recalculate_completion(editor); } @@ -395,13 +398,14 @@ pub fn exit_selection(&mut self) { 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 @@ -500,7 +504,11 @@ pub fn render_prompt(&self, area: Rect, surface: &mut Surface, cx: &mut Context) // 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); + let 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) { @@ -517,7 +525,22 @@ pub fn render_prompt(&self, area: Rect, surface: &mut Surface, cx: &mut Context) .into(); text.render(line_area, surface, cx); } else { - surface.set_string(line_area.x, line_area.y, self.line.clone(), prompt_color); + if self.line.len() < line_area.width as usize { + self.anchor = 0; + } else if self.cursor < self.anchor { + self.anchor = self.cursor; + } else if self.cursor - self.anchor > line_area.width as usize { + self.anchor = self.cursor - line_area.width as usize; + } + + surface.set_string_anchored( + line_area.x, + line_area.y, + self.anchor, + self.line.as_str(), + line_area.width as usize, + |_| prompt_color, + ); } } } @@ -687,14 +710,24 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { } 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 upbound = area.left() as usize + + UnicodeWidthStr::width(&self.line[self.anchor..self.cursor.max(self.anchor)]); + + if self.anchor > 0 { + upbound += 1; + } + + if self.anchor > 0 && self.cursor > self.anchor && self.line.len() > self.cursor { + upbound -= 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, upbound)), editor.config().cursor_shape.from_mode(Mode::Insert), ) } diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs index d28c32fcc..421ec9c81 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -306,6 +306,72 @@ pub fn set_stringn( self.set_string_truncated_at_end(x, y, string.as_ref(), width, style) } + /// Allow to display a big string in a reduce area. + /// A long string will be truncated, with the first character to display starting at the `anchor`. + /// The start and end of the string are going to be replaced with an ellipsis (`…`), if needed. + #[allow(clippy::too_many_arguments)] + pub fn set_string_anchored( + &mut self, + x: u16, + y: u16, + anchor: usize, + 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 + let mut anchor = anchor; + if !self.in_bounds(x, y) || width == 0 { + return (x, y); + } + + let (ellipsis_start, ellipsis_end) = if string.len() <= width { + (false, false) + } else { + (anchor > 0, string.len() - anchor > width) + }; + + 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 ellipsis_end { + self.content[end_index].set_symbol("…"); + end_index -= 1; + + if ellipsis_start { + anchor += 1; + } + } + + if ellipsis_start { + self.content[start_index].set_symbol("…"); + start_index += 1; + } + + let graphemes = string.grapheme_indices(true); + + for (byte_offset, s) in graphemes.skip(anchor) { + 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