Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: better display of prompts on long inputs #12036

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions helix-term/src/ui/picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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)
}
Expand Down
78 changes: 67 additions & 11 deletions helix-term/src/ui/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ pub type DocFn = Box<dyn Fn(&str) -> Option<Cow<str>>>;
pub struct Prompt {
prompt: Cow<'static, str>,
line: String,
line_area: Rect,
cursor: usize,
anchor: usize,
truncate_start: bool,
truncate_end: bool,
completion: Vec<Completion>,
selection: Option<usize>,
history_register: Option<char>,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -329,6 +337,7 @@ impl Prompt {
pub fn clear(&mut self, editor: &Editor) {
self.line.clear();
self.cursor = 0;

self.recalculate_completion(editor);
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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,
);
}
}
}
Expand Down Expand Up @@ -688,14 +732,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),
)
}
Expand Down
57 changes: 57 additions & 0 deletions helix-tui/src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading