From f5b0821860940782567a0baa17280aef04f845e3 Mon Sep 17 00:00:00 2001 From: Mathis Brossier Date: Sun, 16 Jan 2022 02:55:28 +0100 Subject: [PATCH] Fix panics when resizing (#1408) * Change buffer.get & buffer.get_mut to return Option, Implement Trait Index & IndexMut to panic * Prevent FilePicker from drawing outside buffer (rust panics) * apply suggestion * add function in_bounds to avoid useless calculations Co-authored-by: mathis --- helix-term/src/ui/editor.rs | 6 +- helix-term/src/ui/menu.rs | 2 +- helix-term/src/ui/picker.rs | 12 ++-- helix-term/src/ui/prompt.rs | 2 +- helix-tui/src/backend/test.rs | 3 +- helix-tui/src/buffer.rs | 91 +++++++++++++++++++++++------- helix-tui/src/widgets/block.rs | 16 +++--- helix-tui/src/widgets/paragraph.rs | 2 +- helix-view/src/graphics.rs | 8 +-- 9 files changed, 95 insertions(+), 47 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 8d811ed2..c8c94e14 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -97,8 +97,7 @@ impl EditorView { let x = area.right(); let border_style = theme.get("ui.window"); for y in area.top()..area.bottom() { - surface - .get_mut(x, y) + surface[(x, y)] .set_symbol(tui::symbols::line::VERTICAL) //.set_symbol(" ") .set_style(border_style); @@ -393,8 +392,7 @@ impl EditorView { .add_modifier(Modifier::DIM) }); - surface - .get_mut(viewport.x + pos.col as u16, viewport.y + pos.row as u16) + surface[(viewport.x + pos.col as u16, viewport.y + pos.row as u16)] .set_style(style); } } diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 69053db3..9758732c 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -301,7 +301,7 @@ impl Component for Menu { let is_marked = i >= scroll_line && i < scroll_line + scroll_height; if is_marked { - let cell = surface.get_mut(area.x + area.width - 2, area.y + i as u16); + let cell = &mut surface[(area.x + area.width - 2, area.y + i as u16)]; cell.set_symbol("▐ "); // cell.set_style(selected); // cell.set_style(if is_marked { selected } else { style }); diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 1ef94df0..a061513c 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -159,6 +159,7 @@ impl Component for FilePicker { // |picker | | | // | | | | // +---------+ +---------+ + let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW; let area = inner_rect(area); // -- Render the frame: @@ -492,10 +493,9 @@ impl Component for Picker { let sep_style = Style::default().fg(Color::Rgb(90, 89, 119)); let borders = BorderType::line_symbols(BorderType::Plain); for x in inner.left()..inner.right() { - surface - .get_mut(x, inner.y + 1) - .set_symbol(borders.horizontal) - .set_style(sep_style); + if let Some(cell) = surface.get_mut(x, inner.y + 1) { + cell.set_symbol(borders.horizontal).set_style(sep_style); + } } // -- Render the contents: @@ -505,7 +505,7 @@ impl Component for Picker { let selected = cx.editor.theme.get("ui.text.focus"); let rows = inner.height; - let offset = self.cursor / (rows as usize) * (rows as usize); + let offset = self.cursor / std::cmp::max(1, (rows as usize) * (rows as usize)); let files = self.matches.iter().skip(offset).map(|(index, _score)| { (index, self.options.get(*index).unwrap()) // get_unchecked @@ -513,7 +513,7 @@ impl Component for Picker { for (i, (_index, option)) in files.take(rows as usize).enumerate() { if i == (self.cursor - offset) { - surface.set_string(inner.x - 2, inner.y + i as u16, ">", selected); + surface.set_string(inner.x.saturating_sub(2), inner.y + i as u16, ">", selected); } surface.set_string_truncated( diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 0202de23..4c4fef26 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -330,7 +330,7 @@ impl Prompt { .max(BASE_WIDTH); let cols = std::cmp::max(1, area.width / max_len); - let col_width = (area.width - (cols)) / cols; + let col_width = (area.width.saturating_sub(cols)) / cols; let height = ((self.completion.len() as u16 + cols - 1) / cols) .min(10) // at most 10 rows (or less) diff --git a/helix-tui/src/backend/test.rs b/helix-tui/src/backend/test.rs index 3f56b49c..52474148 100644 --- a/helix-tui/src/backend/test.rs +++ b/helix-tui/src/backend/test.rs @@ -111,8 +111,7 @@ impl Backend for TestBackend { I: Iterator, { for (x, y, c) in content { - let cell = self.buffer.get_mut(x, y); - *cell = c.clone(); + self.buffer[(x, y)] = c.clone(); } Ok(()) } diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs index c49a0200..3c5e09ac 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -90,17 +90,17 @@ impl Default for Cell { /// use helix_view::graphics::{Rect, Color, Style, Modifier}; /// /// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5}); -/// buf.get_mut(0, 2).set_symbol("x"); -/// assert_eq!(buf.get(0, 2).symbol, "x"); +/// buf[(0, 2)].set_symbol("x"); +/// assert_eq!(buf[(0, 2)].symbol, "x"); /// buf.set_string(3, 0, "string", Style::default().fg(Color::Red).bg(Color::White)); -/// assert_eq!(buf.get(5, 0), &Cell{ +/// assert_eq!(buf[(5, 0)], Cell{ /// symbol: String::from("r"), /// fg: Color::Red, /// bg: Color::White, /// modifier: Modifier::empty() /// }); -/// buf.get_mut(5, 0).set_char('x'); -/// assert_eq!(buf.get(5, 0).symbol, "x"); +/// buf[(5, 0)].set_char('x'); +/// assert_eq!(buf[(5, 0)].symbol, "x"); /// ``` #[derive(Debug, Default, Clone, PartialEq)] pub struct Buffer { @@ -162,15 +162,38 @@ impl Buffer { } /// Returns a reference to Cell at the given coordinates - pub fn get(&self, x: u16, y: u16) -> &Cell { - let i = self.index_of(x, y); - &self.content[i] + pub fn get(&self, x: u16, y: u16) -> Option<&Cell> { + self.index_of_opt(x, y).map(|i| &self.content[i]) } /// Returns a mutable reference to Cell at the given coordinates - pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell { - let i = self.index_of(x, y); - &mut self.content[i] + pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> { + self.index_of_opt(x, y).map(|i| &mut self.content[i]) + } + + /// Tells whether the global (x, y) coordinates are inside the Buffer's area. + /// + /// Global coordinates are offset by the Buffer's area offset (`x`/`y`). + /// + /// # Examples + /// + /// ``` + /// # use helix_tui::buffer::Buffer; + /// # use helix_view::graphics::Rect; + /// let rect = Rect::new(200, 100, 10, 10); + /// let buffer = Buffer::empty(rect); + /// // Global coordinates inside the Buffer's area + /// assert!(buffer.in_bounds(209, 100)); + /// // Global coordinates outside the Buffer's area + /// assert!(!buffer.in_bounds(210, 100)); + /// ``` + /// + /// Global coordinates are offset by the Buffer's area offset (`x`/`y`). + pub fn in_bounds(&self, x: u16, y: u16) -> bool { + x >= self.area.left() + && x < self.area.right() + && y >= self.area.top() + && y < self.area.bottom() } /// Returns the index in the Vec for the given global (x, y) coordinates. @@ -184,7 +207,7 @@ impl Buffer { /// # use helix_view::graphics::Rect; /// let rect = Rect::new(200, 100, 10, 10); /// let buffer = Buffer::empty(rect); - /// // Global coordinates to the top corner of this buffer's area + /// // Global coordinates to the top corner of this Buffer's area /// assert_eq!(buffer.index_of(200, 100), 0); /// ``` /// @@ -193,10 +216,7 @@ impl Buffer { /// Panics when given an coordinate that is outside of this Buffer's area. pub fn index_of(&self, x: u16, y: u16) -> usize { debug_assert!( - x >= self.area.left() - && x < self.area.right() - && y >= self.area.top() - && y < self.area.bottom(), + self.in_bounds(x, y), "Trying to access position outside the buffer: x={}, y={}, area={:?}", x, y, @@ -205,6 +225,16 @@ impl Buffer { ((y - self.area.y) * self.area.width + (x - self.area.x)) as usize } + /// Returns the index in the Vec for the given global (x, y) coordinates, + /// or `None` if the coordinates are outside the buffer's area. + fn index_of_opt(&self, x: u16, y: u16) -> Option { + if self.in_bounds(x, y) { + Some(self.index_of(x, y)) + } else { + None + } + } + /// Returns the (global) coordinates of a cell given its index /// /// Global coordinates are offset by the Buffer's area offset (`x`/`y`). @@ -278,6 +308,11 @@ impl Buffer { where S: AsRef, { + // prevent panic if out of range + if !self.in_bounds(x, y) || width == 0 { + return (x, y); + } + let mut index = self.index_of(x, y); let mut x_offset = x as usize; let width = if ellipsis { width - 1 } else { width }; @@ -372,7 +407,7 @@ impl Buffer { pub fn set_background(&mut self, area: Rect, color: Color) { for y in area.top()..area.bottom() { for x in area.left()..area.right() { - self.get_mut(x, y).set_bg(color); + self[(x, y)].set_bg(color); } } } @@ -380,7 +415,7 @@ impl Buffer { pub fn set_style(&mut self, area: Rect, style: Style) { for y in area.top()..area.bottom() { for x in area.left()..area.right() { - self.get_mut(x, y).set_style(style); + self[(x, y)].set_style(style); } } } @@ -408,7 +443,7 @@ impl Buffer { pub fn clear(&mut self, area: Rect) { for x in area.left()..area.right() { for y in area.top()..area.bottom() { - self.get_mut(x, y).reset(); + self[(x, y)].reset(); } } } @@ -417,7 +452,7 @@ impl Buffer { pub fn clear_with(&mut self, area: Rect, style: Style) { for x in area.left()..area.right() { for y in area.top()..area.bottom() { - let cell = self.get_mut(x, y); + let cell = &mut self[(x, y)]; cell.reset(); cell.set_style(style); } @@ -509,6 +544,22 @@ impl Buffer { } } +impl std::ops::Index<(u16, u16)> for Buffer { + type Output = Cell; + + fn index(&self, (x, y): (u16, u16)) -> &Self::Output { + let i = self.index_of(x, y); + &self.content[i] + } +} + +impl std::ops::IndexMut<(u16, u16)> for Buffer { + fn index_mut(&mut self, (x, y): (u16, u16)) -> &mut Self::Output { + let i = self.index_of(x, y); + &mut self.content[i] + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/helix-tui/src/widgets/block.rs b/helix-tui/src/widgets/block.rs index 648c2d7e..d43ad41d 100644 --- a/helix-tui/src/widgets/block.rs +++ b/helix-tui/src/widgets/block.rs @@ -140,14 +140,14 @@ impl<'a> Widget for Block<'a> { // Sides if self.borders.intersects(Borders::LEFT) { for y in area.top()..area.bottom() { - buf.get_mut(area.left(), y) + buf[(area.left(), y)] .set_symbol(symbols.vertical) .set_style(self.border_style); } } if self.borders.intersects(Borders::TOP) { for x in area.left()..area.right() { - buf.get_mut(x, area.top()) + buf[(x, area.top())] .set_symbol(symbols.horizontal) .set_style(self.border_style); } @@ -155,7 +155,7 @@ impl<'a> Widget for Block<'a> { if self.borders.intersects(Borders::RIGHT) { let x = area.right() - 1; for y in area.top()..area.bottom() { - buf.get_mut(x, y) + buf[(x, y)] .set_symbol(symbols.vertical) .set_style(self.border_style); } @@ -163,7 +163,7 @@ impl<'a> Widget for Block<'a> { if self.borders.intersects(Borders::BOTTOM) { let y = area.bottom() - 1; for x in area.left()..area.right() { - buf.get_mut(x, y) + buf[(x, y)] .set_symbol(symbols.horizontal) .set_style(self.border_style); } @@ -171,22 +171,22 @@ impl<'a> Widget for Block<'a> { // Corners if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) { - buf.get_mut(area.right() - 1, area.bottom() - 1) + buf[(area.right() - 1, area.bottom() - 1)] .set_symbol(symbols.bottom_right) .set_style(self.border_style); } if self.borders.contains(Borders::RIGHT | Borders::TOP) { - buf.get_mut(area.right() - 1, area.top()) + buf[(area.right() - 1, area.top())] .set_symbol(symbols.top_right) .set_style(self.border_style); } if self.borders.contains(Borders::LEFT | Borders::BOTTOM) { - buf.get_mut(area.left(), area.bottom() - 1) + buf[(area.left(), area.bottom() - 1)] .set_symbol(symbols.bottom_left) .set_style(self.border_style); } if self.borders.contains(Borders::LEFT | Borders::TOP) { - buf.get_mut(area.left(), area.top()) + buf[(area.left(), area.top())] .set_symbol(symbols.top_left) .set_style(self.border_style); } diff --git a/helix-tui/src/widgets/paragraph.rs b/helix-tui/src/widgets/paragraph.rs index fee35d25..8bafbb9a 100644 --- a/helix-tui/src/widgets/paragraph.rs +++ b/helix-tui/src/widgets/paragraph.rs @@ -176,7 +176,7 @@ impl<'a> Widget for Paragraph<'a> { if y >= self.scroll.0 { let mut x = get_line_offset(current_line_width, text_area.width, self.alignment); for StyledGrapheme { symbol, style } in current_line { - buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0) + buf[(text_area.left() + x, text_area.top() + y - self.scroll.0)] .set_symbol(if symbol.is_empty() { // If the symbol is empty, the last char which rendered last time will // leave on the line. It's a quick fix. diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index b8e43ba5..5fd54c8c 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -323,7 +323,7 @@ impl FromStr for Modifier { /// ]; /// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1)); /// for style in &styles { -/// buffer.get_mut(0, 0).set_style(*style); +/// buffer[(0, 0)].set_style(*style); /// } /// assert_eq!( /// Style { @@ -332,7 +332,7 @@ impl FromStr for Modifier { /// add_modifier: Modifier::BOLD, /// sub_modifier: Modifier::empty(), /// }, -/// buffer.get(0, 0).style(), +/// buffer[(0, 0)].style(), /// ); /// ``` /// @@ -348,7 +348,7 @@ impl FromStr for Modifier { /// ]; /// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1)); /// for style in &styles { -/// buffer.get_mut(0, 0).set_style(*style); +/// buffer[(0, 0)].set_style(*style); /// } /// assert_eq!( /// Style { @@ -357,7 +357,7 @@ impl FromStr for Modifier { /// add_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(), /// }, -/// buffer.get(0, 0).style(), +/// buffer[(0, 0)].style(), /// ); /// ``` #[derive(Debug, Clone, Copy, PartialEq)]