diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs index c62498ae..563b8801 100644 --- a/helix-term/src/ui/document.rs +++ b/helix-term/src/ui/document.rs @@ -335,6 +335,7 @@ impl<'a> TextRenderer<'a> { style = style.patch(grapheme_style.overlay_style); let width = grapheme.width(); + let mut is_tab = false; let space = if is_virtual { " " } else { &self.space }; let nbsp = if is_virtual { " " } else { &self.nbsp }; let nnbsp = if is_virtual { " " } else { &self.nnbsp }; @@ -345,6 +346,7 @@ impl<'a> TextRenderer<'a> { }; let grapheme = match grapheme.raw { Grapheme::Tab { width } => { + is_tab = true; let grapheme_tab_width = char_to_byte_idx(tab, width); &tab[..grapheme_tab_width] } @@ -359,13 +361,18 @@ impl<'a> TextRenderer<'a> { let in_bounds = self.column_in_bounds(position.col, width); if in_bounds { - self.surface.set_grapheme( - self.viewport.x + (position.col - self.offset.col) as u16, - self.viewport.y + position.row as u16, - grapheme, - width, - style, - ); + let x = self.viewport.x + (position.col - self.offset.col) as u16; + let y = self.viewport.y + position.row as u16; + if is_tab { + // A tab expands to `width` single-column cells; writing them + // individually keeps background styles (selection, cursorline) + // across the whole tab and avoids the redraw diff clipping + // `render-whitespace` pads. A single `set_grapheme` would pack + // them into one wide cell and leave the rest unstyled. + self.surface.set_tab(x, y, grapheme, style); + } else { + self.surface.set_grapheme(x, y, grapheme, width, style); + } } else if cut_off_start != 0 && cut_off_start < width { // partially on screen let rect = Rect::new( diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs index 893a4ab2..a2ccc3a1 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -391,6 +391,24 @@ impl Buffer { } } + /// Fast path for tab expansion: write each char of `tab` into its own + /// single-column cell starting at (x, y), all sharing `style`. + /// + /// Unlike [`Self::set_grapheme`], the columns are written as independent + /// width-1 cells rather than one wide cell, so background styles (selection, + /// cursorline) cover every column. Caller must guarantee each char is one column + /// and that the whole run fits inside the buffer area. + #[inline] + pub fn set_tab(&mut self, x: u16, y: u16, tab: &str, style: Style) { + let mut index = self.index_of(x, y); + for (i, ch) in tab.char_indices() { + let cell = &mut self.content[index]; + cell.set_symbol_with_width(&tab[i..i + ch.len_utf8()], 1); + cell.set_style(style); + index += 1; + } + } + /// 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.