Files
Ely-GPUI-Components/src/terminal/frame.rs
T

253 lines
7.6 KiB
Rust

use std::ops::Range;
use alacritty_terminal::{
event::EventListener,
grid::Dimensions,
term::{Term, cell::Cell, cell::Flags, color::Colors, search::Match},
vte::ansi::CursorShape,
};
use gpui::Hsla;
use super::colors::Ink;
/// How a run of cells paints.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Style {
pub fg: Hsla,
/// None where the terminal's ground shows.
pub bg: Option<Hsla>,
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub strike: bool,
}
/// One screen line: a char per cell, and style runs over its bytes.
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct Row {
pub text: String,
pub runs: Vec<(usize, Style)>,
/// Cells with combining marks, painted on their own at their column: the column, the cluster and its style.
pub clusters: Vec<(usize, String, Style)>,
}
/// Why cells are lit: selected, or a find match, the current one stronger.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Lit {
Selected,
Found,
Current,
}
/// A terminal's screen as it paints.
#[derive(Clone, Debug, Default)]
pub(crate) struct Frame {
pub rows: Vec<Row>,
/// Lit cells: the row, its columns, and why.
pub lit: Vec<(usize, Range<usize>, Lit)>,
/// The cursor's row, column and shape, when it shows.
pub cursor: Option<(usize, usize, CursorShape)>,
}
impl Row {
/// Each cell's text, a combining cluster whole.
pub(crate) fn cells(&self) -> Vec<String> {
let mut cells: Vec<String> = self.text.chars().map(String::from).collect();
for (column, cluster, _) in &self.clusters {
cells[*column] = cluster.clone();
}
cells
}
}
impl Frame {
/// The screen's text, trailing spaces gone.
pub(crate) fn text(&self) -> String {
let lines: Vec<String> = self
.rows
.iter()
.map(|row| row.cells().concat().trim_end().to_string())
.collect();
lines.join("\n").trim_end().to_string()
}
}
/// The screen as it shows now, scrolled back or not, with `found` lit and match `current` stronger.
pub(crate) fn frame<T: EventListener>(
term: &Term<T>,
ink: &Ink,
found: &[Match],
current: Option<usize>,
) -> Frame {
let content = term.renderable_content();
let offset = content.display_offset as i32;
let mut rows = vec![Row::default(); term.screen_lines()];
let mut lit: Vec<(usize, Range<usize>, Lit)> = Vec::new();
let (top, bottom) = (-offset, rows.len() as i32 - 1 - offset);
let near: Vec<(usize, &Match)> = found
.iter()
.enumerate()
.filter(|(_, hit)| hit.end().line.0 >= top && hit.start().line.0 <= bottom)
.collect();
for indexed in content.display_iter {
let (point, cell) = (indexed.point, indexed.cell);
let row = (point.line.0 + offset) as usize;
let spacer = cell
.flags
.intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER);
let look = style(cell, ink, content.colors);
let cluster = cell
.zerowidth()
.filter(|marks| !marks.is_empty())
.map(|marks| {
std::iter::once(cell.c)
.chain(marks.iter().copied())
.collect::<String>()
});
push(
&mut rows[row],
if spacer || cluster.is_some() {
' '
} else {
cell.c
},
look,
);
if let Some(cluster) = cluster {
rows[row].clusters.push((point.column.0, cluster, look));
}
let why = if content.selection.is_some_and(|range| range.contains(point)) {
Some(Lit::Selected)
} else {
near.iter()
.find(|(_, hit)| hit.contains(&point))
.map(|(ix, _)| {
if Some(*ix) == current {
Lit::Current
} else {
Lit::Found
}
})
};
if let Some(why) = why {
light(&mut lit, row, point.column.0, why);
}
}
let at = content.cursor.point.line.0 + offset;
let shows = content.cursor.shape != CursorShape::Hidden && (0..rows.len() as i32).contains(&at);
let cursor = shows.then_some((
at as usize,
content.cursor.point.column.0,
content.cursor.shape,
));
Frame { rows, lit, cursor }
}
fn push(row: &mut Row, ch: char, style: Style) {
let len = ch.len_utf8();
row.text.push(ch);
match row.runs.last_mut() {
Some((bytes, last)) if *last == style => *bytes += len,
_ => row.runs.push((len, style)),
}
}
fn light(lit: &mut Vec<(usize, Range<usize>, Lit)>, row: usize, column: usize, why: Lit) {
match lit.last_mut() {
Some((at, columns, last)) if *at == row && columns.end == column && *last == why => {
columns.end += 1
}
_ => lit.push((row, column..column + 1, why)),
}
}
fn style(cell: &Cell, ink: &Ink, set: &Colors) -> Style {
let flags = cell.flags;
let (mut fg, mut bg) = (ink.resolve(cell.fg, set), ink.resolve(cell.bg, set));
if flags.contains(Flags::INVERSE) {
std::mem::swap(&mut fg, &mut bg);
}
if flags.contains(Flags::DIM) {
fg = fg.opacity(0.7);
}
if flags.contains(Flags::HIDDEN) {
fg = bg;
}
Style {
fg,
bg: (bg != ink.bg).then_some(bg),
bold: flags.contains(Flags::BOLD),
italic: flags.contains(Flags::ITALIC),
underline: flags.intersects(Flags::ALL_UNDERLINES),
strike: flags.contains(Flags::STRIKEOUT),
}
}
#[cfg(test)]
mod tests {
use alacritty_terminal::{
event::VoidListener,
term::{Config, test::TermSize},
vte::ansi::Processor,
};
use gpui::{black, white};
use super::*;
fn replayed(bytes: &[u8]) -> Term<VoidListener> {
let mut term = Term::new(Config::default(), &TermSize::new(12, 3), VoidListener);
let mut parser: Processor = Processor::new();
parser.advance(&mut term, bytes);
term
}
fn ink() -> Ink {
let mut ansi = [black(); 16];
ansi[1] = gpui::red();
Ink {
ansi,
fg: black(),
bg: white(),
}
}
#[test]
fn combining_marks_stay_on_their_cell() {
let term = replayed("e\u{301}x".as_bytes());
let shown = frame(&term, &ink(), &[], None);
assert_eq!(shown.text(), "e\u{301}x");
assert_eq!(
&shown.rows[0].text[..2],
" x",
"the cluster's cell keeps its column"
);
assert_eq!(shown.rows[0].clusters[0].1, "e\u{301}");
}
#[test]
fn cells_join_into_runs_of_one_style() {
let term = replayed(b"\x1b[31mred\x1b[0m ok\r\n\x1b[7minv\x1b[0m");
let shown = frame(&term, &ink(), &[], None);
assert_eq!(shown.text(), "red ok\ninv");
let (bytes, red) = shown.rows[0].runs[0];
assert_eq!((bytes, red.fg, red.bg), (3, gpui::red(), None));
let (_, inverse) = shown.rows[1].runs[0];
assert_eq!(
(inverse.fg, inverse.bg),
(white(), Some(black())),
"inverse swaps"
);
assert_eq!(shown.cursor, Some((1, 3, CursorShape::Block)));
}
#[test]
fn a_wide_char_keeps_the_columns_after_it() {
let term = replayed("中a".as_bytes());
let shown = frame(&term, &ink(), &[], None);
assert_eq!(
shown.rows[0].text.chars().take(3).collect::<String>(),
"中 a"
);
}
}