mirror of
https://github.com/wez/wezterm.git
synced 2024-12-24 13:52:55 +03:00
termwiz: refactor: split line into sub-modules
This commit is contained in:
parent
d3ef36dd5f
commit
c32db29474
68
termwiz/src/surface/line/cellref.rs
Normal file
68
termwiz/src/surface/line/cellref.rs
Normal file
@ -0,0 +1,68 @@
|
||||
use crate::cell::{Cell, CellAttributes};
|
||||
use crate::emoji::Presentation;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CellRef<'a> {
|
||||
CellRef {
|
||||
cell_index: usize,
|
||||
cell: &'a Cell,
|
||||
},
|
||||
ClusterRef {
|
||||
cell_index: usize,
|
||||
text: &'a str,
|
||||
width: usize,
|
||||
attrs: &'a CellAttributes,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> CellRef<'a> {
|
||||
pub fn cell_index(&self) -> usize {
|
||||
match self {
|
||||
Self::ClusterRef { cell_index, .. } | Self::CellRef { cell_index, .. } => *cell_index,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn str(&self) -> &str {
|
||||
match self {
|
||||
Self::CellRef { cell, .. } => cell.str(),
|
||||
Self::ClusterRef { text, .. } => text,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
match self {
|
||||
Self::CellRef { cell, .. } => cell.width(),
|
||||
Self::ClusterRef { width, .. } => *width,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attrs(&self) -> &CellAttributes {
|
||||
match self {
|
||||
Self::CellRef { cell, .. } => cell.attrs(),
|
||||
Self::ClusterRef { attrs, .. } => attrs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn presentation(&self) -> Presentation {
|
||||
match self {
|
||||
Self::CellRef { cell, .. } => cell.presentation(),
|
||||
Self::ClusterRef { text, .. } => match Presentation::for_grapheme(text) {
|
||||
(_, Some(variation)) => variation,
|
||||
(presentation, None) => presentation,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_cell(&self) -> Cell {
|
||||
match self {
|
||||
Self::CellRef { cell, .. } => (*cell).clone(),
|
||||
Self::ClusterRef {
|
||||
text, width, attrs, ..
|
||||
} => Cell::new_grapheme_with_width(text, *width, (*attrs).clone()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn same_contents(&self, other: &Self) -> bool {
|
||||
self.str() == other.str() && self.width() == other.width() && self.attrs() == other.attrs()
|
||||
}
|
||||
}
|
343
termwiz/src/surface/line/clusterline.rs
Normal file
343
termwiz/src/surface/line/clusterline.rs
Normal file
@ -0,0 +1,343 @@
|
||||
use crate::cell::{Cell, CellAttributes};
|
||||
use crate::surface::line::CellRef;
|
||||
use fixedbitset::FixedBitSet;
|
||||
#[cfg(feature = "use_serde")]
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::num::NonZeroU8;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
#[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct Cluster {
|
||||
cell_width: usize,
|
||||
attrs: CellAttributes,
|
||||
}
|
||||
|
||||
/// Stores line data as a contiguous string and a series of
|
||||
/// clusters of attribute data describing attributed ranges
|
||||
/// within the line
|
||||
#[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct ClusteredLine {
|
||||
pub text: String,
|
||||
#[cfg_attr(
|
||||
feature = "use_serde",
|
||||
serde(
|
||||
deserialize_with = "deserialize_bitset",
|
||||
serialize_with = "serialize_bitset"
|
||||
)
|
||||
)]
|
||||
is_double_wide: Option<FixedBitSet>,
|
||||
clusters: Vec<Cluster>,
|
||||
len: usize,
|
||||
last_cell_width: Option<NonZeroU8>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "use_serde")]
|
||||
fn deserialize_bitset<'de, D>(deserializer: D) -> Result<Option<FixedBitSet>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let wide_indices = <Vec<usize>>::deserialize(deserializer)?;
|
||||
if wide_indices.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
let max_idx = wide_indices.iter().max().unwrap_or(&1);
|
||||
let mut bitset = FixedBitSet::with_capacity(max_idx + 1);
|
||||
for idx in wide_indices {
|
||||
bitset.set(idx, true);
|
||||
}
|
||||
Ok(Some(bitset))
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize the bitset as a vector of the indices of just the 1 bits;
|
||||
/// the thesis is that most of the cells on a given line are single width.
|
||||
/// That may not be strictly true for users that heavily use asian scripts,
|
||||
/// but we'll start with this and see if we need to improve it.
|
||||
#[cfg(feature = "use_serde")]
|
||||
fn serialize_bitset<S>(value: &Option<FixedBitSet>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut wide_indices: Vec<usize> = vec![];
|
||||
if let Some(bits) = value {
|
||||
for idx in bits.ones() {
|
||||
wide_indices.push(idx);
|
||||
}
|
||||
}
|
||||
wide_indices.serialize(serializer)
|
||||
}
|
||||
|
||||
impl ClusteredLine {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
text: String::with_capacity(80),
|
||||
is_double_wide: None,
|
||||
clusters: vec![],
|
||||
len: 0,
|
||||
last_cell_width: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_cell_vec(&self) -> Vec<Cell> {
|
||||
let mut cells = vec![];
|
||||
|
||||
for c in self.iter() {
|
||||
cells.push(c.as_cell());
|
||||
for _ in 1..c.width() {
|
||||
cells.push(Cell::blank_with_attrs(c.attrs().clone()));
|
||||
}
|
||||
}
|
||||
|
||||
cells
|
||||
}
|
||||
|
||||
pub fn from_cell_vec<'a>(hint: usize, iter: impl Iterator<Item = CellRef<'a>>) -> Self {
|
||||
let mut last_cluster: Option<Cluster> = None;
|
||||
let mut is_double_wide = FixedBitSet::with_capacity(hint);
|
||||
let mut text = String::new();
|
||||
let mut clusters = vec![];
|
||||
let mut any_double = false;
|
||||
let mut len = 0;
|
||||
let mut last_cell_width = None;
|
||||
|
||||
for cell in iter {
|
||||
len += cell.width();
|
||||
last_cell_width = NonZeroU8::new(1);
|
||||
|
||||
if cell.width() > 1 {
|
||||
any_double = true;
|
||||
is_double_wide.set(cell.cell_index(), true);
|
||||
}
|
||||
|
||||
text.push_str(cell.str());
|
||||
|
||||
last_cluster = match last_cluster.take() {
|
||||
None => Some(Cluster {
|
||||
cell_width: cell.width(),
|
||||
attrs: cell.attrs().clone(),
|
||||
}),
|
||||
Some(cluster) if cluster.attrs != *cell.attrs() => {
|
||||
clusters.push(cluster);
|
||||
Some(Cluster {
|
||||
cell_width: cell.width(),
|
||||
attrs: cell.attrs().clone(),
|
||||
})
|
||||
}
|
||||
Some(mut cluster) => {
|
||||
cluster.cell_width += cell.width();
|
||||
Some(cluster)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(cluster) = last_cluster.take() {
|
||||
clusters.push(cluster);
|
||||
}
|
||||
|
||||
Self {
|
||||
text,
|
||||
is_double_wide: if any_double {
|
||||
Some(is_double_wide)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
clusters,
|
||||
len,
|
||||
last_cell_width,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
|
||||
fn is_double_wide(&self, cell_index: usize) -> bool {
|
||||
match &self.is_double_wide {
|
||||
Some(bitset) => bitset.contains(cell_index),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> ClusterLineCellIter {
|
||||
let mut clusters = self.clusters.iter();
|
||||
let cluster = clusters.next();
|
||||
ClusterLineCellIter {
|
||||
graphemes: self.text.graphemes(true),
|
||||
clusters,
|
||||
cluster,
|
||||
idx: 0,
|
||||
cluster_total: 0,
|
||||
line: self,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_grapheme(&mut self, text: &str, cell_width: usize, attrs: CellAttributes) {
|
||||
let new_cluster = match self.clusters.last() {
|
||||
Some(cluster) => cluster.attrs != attrs,
|
||||
None => true,
|
||||
};
|
||||
let new_cell_index = self.len;
|
||||
if new_cluster {
|
||||
self.clusters.push(Cluster { attrs, cell_width });
|
||||
} else if let Some(cluster) = self.clusters.last_mut() {
|
||||
cluster.cell_width += cell_width;
|
||||
}
|
||||
self.text.push_str(text);
|
||||
|
||||
if cell_width > 1 {
|
||||
let bitset = match self.is_double_wide.take() {
|
||||
Some(mut bitset) => {
|
||||
bitset.grow(new_cell_index + 1);
|
||||
bitset.set(new_cell_index, true);
|
||||
bitset
|
||||
}
|
||||
None => {
|
||||
let mut bitset = FixedBitSet::with_capacity(new_cell_index + 1);
|
||||
bitset.set(new_cell_index, true);
|
||||
bitset
|
||||
}
|
||||
};
|
||||
self.is_double_wide.replace(bitset);
|
||||
}
|
||||
self.last_cell_width = NonZeroU8::new(cell_width as u8);
|
||||
self.len += cell_width;
|
||||
}
|
||||
|
||||
pub fn append(&mut self, cell: Cell) {
|
||||
let new_cluster = match self.clusters.last() {
|
||||
Some(cluster) => cluster.attrs != *cell.attrs(),
|
||||
None => true,
|
||||
};
|
||||
let new_cell_index = self.len;
|
||||
let cell_width = cell.width();
|
||||
if new_cluster {
|
||||
self.clusters.push(Cluster {
|
||||
attrs: (*cell.attrs()).clone(),
|
||||
cell_width,
|
||||
});
|
||||
} else if let Some(cluster) = self.clusters.last_mut() {
|
||||
cluster.cell_width += cell_width;
|
||||
}
|
||||
self.text.push_str(cell.str());
|
||||
|
||||
if cell_width > 1 {
|
||||
let bitset = match self.is_double_wide.take() {
|
||||
Some(mut bitset) => {
|
||||
bitset.grow(new_cell_index + 1);
|
||||
bitset.set(new_cell_index, true);
|
||||
bitset
|
||||
}
|
||||
None => {
|
||||
let mut bitset = FixedBitSet::with_capacity(new_cell_index + 1);
|
||||
bitset.set(new_cell_index, true);
|
||||
bitset
|
||||
}
|
||||
};
|
||||
self.is_double_wide.replace(bitset);
|
||||
}
|
||||
self.last_cell_width = NonZeroU8::new(cell_width as u8);
|
||||
self.len += cell_width;
|
||||
}
|
||||
|
||||
pub fn prune_trailing_blanks(&mut self) -> bool {
|
||||
let num_spaces = self.text.chars().rev().take_while(|&c| c == ' ').count();
|
||||
if num_spaces == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let blank = CellAttributes::blank();
|
||||
let mut pruned = false;
|
||||
for _ in 0..num_spaces {
|
||||
let mut need_pop = false;
|
||||
if let Some(cluster) = self.clusters.last_mut() {
|
||||
if cluster.attrs != blank {
|
||||
break;
|
||||
}
|
||||
cluster.cell_width -= 1;
|
||||
self.text.pop();
|
||||
self.len -= 1;
|
||||
self.last_cell_width.take();
|
||||
pruned = true;
|
||||
if cluster.cell_width == 0 {
|
||||
need_pop = true;
|
||||
}
|
||||
}
|
||||
if need_pop {
|
||||
self.clusters.pop();
|
||||
}
|
||||
}
|
||||
|
||||
pruned
|
||||
}
|
||||
|
||||
fn compute_last_cell_width(&mut self) -> Option<NonZeroU8> {
|
||||
if self.last_cell_width.is_none() {
|
||||
if let Some(last_cell) = self.iter().last() {
|
||||
self.last_cell_width = NonZeroU8::new(last_cell.width() as u8);
|
||||
}
|
||||
}
|
||||
self.last_cell_width
|
||||
}
|
||||
|
||||
pub fn set_last_cell_was_wrapped(&mut self, wrapped: bool) {
|
||||
if let Some(width) = self.compute_last_cell_width() {
|
||||
let width = width.get() as usize;
|
||||
if let Some(last_cluster) = self.clusters.last_mut() {
|
||||
let mut attrs = last_cluster.attrs.clone();
|
||||
attrs.set_wrapped(wrapped);
|
||||
|
||||
if last_cluster.cell_width == width {
|
||||
// Re-purpose final cluster
|
||||
last_cluster.attrs = attrs;
|
||||
} else {
|
||||
last_cluster.cell_width -= width;
|
||||
self.clusters.push(Cluster {
|
||||
cell_width: width,
|
||||
attrs,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ClusterLineCellIter<'a> {
|
||||
graphemes: unicode_segmentation::Graphemes<'a>,
|
||||
clusters: std::slice::Iter<'a, Cluster>,
|
||||
cluster: Option<&'a Cluster>,
|
||||
idx: usize,
|
||||
cluster_total: usize,
|
||||
line: &'a ClusteredLine,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for ClusterLineCellIter<'a> {
|
||||
type Item = CellRef<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<CellRef<'a>> {
|
||||
let text = self.graphemes.next()?;
|
||||
|
||||
let cell_index = self.idx;
|
||||
let width = if self.line.is_double_wide(cell_index) {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
};
|
||||
self.idx += width;
|
||||
self.cluster_total += width;
|
||||
let attrs = &self.cluster.as_ref()?.attrs;
|
||||
|
||||
if self.cluster_total >= self.cluster.as_ref()?.cell_width {
|
||||
self.cluster = self.clusters.next();
|
||||
self.cluster_total = 0;
|
||||
}
|
||||
|
||||
Some(CellRef::ClusterRef {
|
||||
cell_index,
|
||||
width,
|
||||
text,
|
||||
attrs,
|
||||
})
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
56
termwiz/src/surface/line/linebits.rs
Normal file
56
termwiz/src/surface/line/linebits.rs
Normal file
@ -0,0 +1,56 @@
|
||||
use bitflags::bitflags;
|
||||
#[cfg(feature = "use_serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
bitflags! {
|
||||
#[cfg_attr(feature="use_serde", derive(Serialize, Deserialize))]
|
||||
pub(crate) struct LineBits : u16 {
|
||||
const NONE = 0;
|
||||
/// The line contains 1+ cells with explicit hyperlinks set
|
||||
const HAS_HYPERLINK = 1<<1;
|
||||
/// true if we have scanned for implicit hyperlinks
|
||||
const SCANNED_IMPLICIT_HYPERLINKS = 1<<2;
|
||||
/// true if we found implicit hyperlinks in the last scan
|
||||
const HAS_IMPLICIT_HYPERLINKS = 1<<3;
|
||||
|
||||
/// true if this line should be displayed with
|
||||
/// foreground/background colors reversed
|
||||
const REVERSE = 1<<4;
|
||||
|
||||
/// true if this line should be displayed with
|
||||
/// in double-width
|
||||
const DOUBLE_WIDTH = 1<<5;
|
||||
|
||||
/// true if this line should be displayed
|
||||
/// as double-height top-half
|
||||
const DOUBLE_HEIGHT_TOP = 1<<6;
|
||||
|
||||
/// true if this line should be displayed
|
||||
/// as double-height bottom-half
|
||||
const DOUBLE_HEIGHT_BOTTOM = 1<<7;
|
||||
|
||||
const DOUBLE_WIDTH_HEIGHT_MASK =
|
||||
Self::DOUBLE_WIDTH.bits |
|
||||
Self::DOUBLE_HEIGHT_TOP.bits |
|
||||
Self::DOUBLE_HEIGHT_BOTTOM.bits;
|
||||
|
||||
/// true if the line should have the bidi algorithm
|
||||
/// applied as part of presentation.
|
||||
/// This corresponds to the "implicit" bidi modes
|
||||
/// described in
|
||||
/// <https://terminal-wg.pages.freedesktop.org/bidi/recommendation/basic-modes.html>
|
||||
const BIDI_ENABLED = 1<<0;
|
||||
|
||||
/// true if the line base direction is RTL.
|
||||
/// When BIDI_ENABLED is also true, this is passed to the bidi algorithm.
|
||||
/// When rendering, the line will be rendered from RTL.
|
||||
const RTL = 1<<8;
|
||||
|
||||
/// true if the direction for the line should be auto-detected
|
||||
/// when BIDI_ENABLED is also true.
|
||||
/// If false, the direction is taken from the RTL bit only.
|
||||
/// Otherwise, the auto-detect direction is used, falling back
|
||||
/// to the direction specified by the RTL bit.
|
||||
const AUTO_DETECT_DIRECTION = 1<<9;
|
||||
}
|
||||
}
|
10
termwiz/src/surface/line/mod.rs
Normal file
10
termwiz/src/surface/line/mod.rs
Normal file
@ -0,0 +1,10 @@
|
||||
mod cellref;
|
||||
mod clusterline;
|
||||
mod line;
|
||||
mod linebits;
|
||||
mod storage;
|
||||
mod test;
|
||||
mod vecstorage;
|
||||
|
||||
pub use cellref::CellRef;
|
||||
pub use line::{DoubleClickRange, Line};
|
28
termwiz/src/surface/line/storage.rs
Normal file
28
termwiz/src/surface/line/storage.rs
Normal file
@ -0,0 +1,28 @@
|
||||
use crate::surface::line::cellref::CellRef;
|
||||
use crate::surface::line::clusterline::{ClusterLineCellIter, ClusteredLine};
|
||||
use crate::surface::line::vecstorage::{VecStorage, VecStorageIter};
|
||||
#[cfg(feature = "use_serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum CellStorage {
|
||||
V(VecStorage),
|
||||
C(ClusteredLine),
|
||||
}
|
||||
|
||||
pub(crate) enum VisibleCellIter<'a> {
|
||||
V(VecStorageIter<'a>),
|
||||
C(ClusterLineCellIter<'a>),
|
||||
}
|
||||
|
||||
impl<'a> Iterator for VisibleCellIter<'a> {
|
||||
type Item = CellRef<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<CellRef<'a>> {
|
||||
match self {
|
||||
Self::V(iter) => iter.next(),
|
||||
Self::C(iter) => iter.next(),
|
||||
}
|
||||
}
|
||||
}
|
585
termwiz/src/surface/line/test.rs
Normal file
585
termwiz/src/surface/line/test.rs
Normal file
@ -0,0 +1,585 @@
|
||||
#![cfg(test)]
|
||||
|
||||
use super::*;
|
||||
use crate::cell::{Cell, CellAttributes};
|
||||
use crate::hyperlink::{Hyperlink, Rule};
|
||||
use crate::surface::line::clusterline::ClusteredLine;
|
||||
use crate::surface::SEQ_ZERO;
|
||||
use k9::assert_equal as assert_eq;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn hyperlinks() {
|
||||
let text = "❤ 😍🤢 http://example.com \u{1f468}\u{1f3fe}\u{200d}\u{1f9b0} http://example.com";
|
||||
|
||||
let rules = vec![
|
||||
Rule::new(r"\b\w+://(?:[\w.-]+)\.[a-z]{2,15}\S*\b", "$0").unwrap(),
|
||||
Rule::new(r"\b\w+@[\w-]+(\.[\w-]+)+\b", "mailto:$0").unwrap(),
|
||||
];
|
||||
|
||||
let hyperlink = Arc::new(Hyperlink::new_implicit("http://example.com"));
|
||||
let hyperlink_attr = CellAttributes::default()
|
||||
.set_hyperlink(Some(hyperlink.clone()))
|
||||
.clone();
|
||||
|
||||
let mut line: Line = text.into();
|
||||
line.scan_and_create_hyperlinks(&rules);
|
||||
assert!(line.has_hyperlink());
|
||||
assert_eq!(
|
||||
line.coerce_vec_storage().to_vec(),
|
||||
vec![
|
||||
Cell::new_grapheme("❤", CellAttributes::default(), None),
|
||||
Cell::new(' ', CellAttributes::default()), // double width spacer
|
||||
Cell::new_grapheme("😍", CellAttributes::default(), None),
|
||||
Cell::new(' ', CellAttributes::default()), // double width spacer
|
||||
Cell::new_grapheme("🤢", CellAttributes::default(), None),
|
||||
Cell::new(' ', CellAttributes::default()), // double width spacer
|
||||
Cell::new(' ', CellAttributes::default()),
|
||||
Cell::new('h', hyperlink_attr.clone()),
|
||||
Cell::new('t', hyperlink_attr.clone()),
|
||||
Cell::new('t', hyperlink_attr.clone()),
|
||||
Cell::new('p', hyperlink_attr.clone()),
|
||||
Cell::new(':', hyperlink_attr.clone()),
|
||||
Cell::new('/', hyperlink_attr.clone()),
|
||||
Cell::new('/', hyperlink_attr.clone()),
|
||||
Cell::new('e', hyperlink_attr.clone()),
|
||||
Cell::new('x', hyperlink_attr.clone()),
|
||||
Cell::new('a', hyperlink_attr.clone()),
|
||||
Cell::new('m', hyperlink_attr.clone()),
|
||||
Cell::new('p', hyperlink_attr.clone()),
|
||||
Cell::new('l', hyperlink_attr.clone()),
|
||||
Cell::new('e', hyperlink_attr.clone()),
|
||||
Cell::new('.', hyperlink_attr.clone()),
|
||||
Cell::new('c', hyperlink_attr.clone()),
|
||||
Cell::new('o', hyperlink_attr.clone()),
|
||||
Cell::new('m', hyperlink_attr.clone()),
|
||||
Cell::new(' ', CellAttributes::default()),
|
||||
Cell::new_grapheme(
|
||||
// man: dark skin tone, red hair ZWJ emoji grapheme
|
||||
"\u{1f468}\u{1f3fe}\u{200d}\u{1f9b0}",
|
||||
CellAttributes::default(),
|
||||
None,
|
||||
),
|
||||
Cell::new(' ', CellAttributes::default()), // double width spacer
|
||||
Cell::new(' ', CellAttributes::default()),
|
||||
Cell::new('h', hyperlink_attr.clone()),
|
||||
Cell::new('t', hyperlink_attr.clone()),
|
||||
Cell::new('t', hyperlink_attr.clone()),
|
||||
Cell::new('p', hyperlink_attr.clone()),
|
||||
Cell::new(':', hyperlink_attr.clone()),
|
||||
Cell::new('/', hyperlink_attr.clone()),
|
||||
Cell::new('/', hyperlink_attr.clone()),
|
||||
Cell::new('e', hyperlink_attr.clone()),
|
||||
Cell::new('x', hyperlink_attr.clone()),
|
||||
Cell::new('a', hyperlink_attr.clone()),
|
||||
Cell::new('m', hyperlink_attr.clone()),
|
||||
Cell::new('p', hyperlink_attr.clone()),
|
||||
Cell::new('l', hyperlink_attr.clone()),
|
||||
Cell::new('e', hyperlink_attr.clone()),
|
||||
Cell::new('.', hyperlink_attr.clone()),
|
||||
Cell::new('c', hyperlink_attr.clone()),
|
||||
Cell::new('o', hyperlink_attr.clone()),
|
||||
Cell::new('m', hyperlink_attr.clone()),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_click_range_bounds() {
|
||||
let line: Line = "hello".into();
|
||||
let r = line.compute_double_click_range(200, |_| true);
|
||||
assert_eq!(r, DoubleClickRange::Range(200..200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cluster_representation_basic() {
|
||||
let line: Line = "hello".into();
|
||||
let mut compressed = line.clone();
|
||||
compressed.compress_for_scrollback();
|
||||
k9::snapshot!(
|
||||
&compressed.cells,
|
||||
r#"
|
||||
C(
|
||||
ClusteredLine {
|
||||
text: "hello",
|
||||
is_double_wide: None,
|
||||
clusters: [
|
||||
Cluster {
|
||||
cell_width: 5,
|
||||
attrs: CellAttributes {
|
||||
attributes: 0,
|
||||
intensity: Normal,
|
||||
underline: None,
|
||||
blink: None,
|
||||
italic: false,
|
||||
reverse: false,
|
||||
strikethrough: false,
|
||||
invisible: false,
|
||||
wrapped: false,
|
||||
overline: false,
|
||||
semantic_type: Output,
|
||||
foreground: Default,
|
||||
background: Default,
|
||||
fat: None,
|
||||
},
|
||||
},
|
||||
],
|
||||
len: 5,
|
||||
last_cell_width: Some(
|
||||
1,
|
||||
),
|
||||
},
|
||||
)
|
||||
"#
|
||||
);
|
||||
compressed.coerce_vec_storage();
|
||||
assert_eq!(line, compressed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cluster_representation_double_width() {
|
||||
let line: Line = "❤ 😍🤢he❤ 😍🤢llo❤ 😍🤢".into();
|
||||
let mut compressed = line.clone();
|
||||
compressed.compress_for_scrollback();
|
||||
k9::snapshot!(
|
||||
&compressed.cells,
|
||||
r#"
|
||||
C(
|
||||
ClusteredLine {
|
||||
text: "❤ 😍🤢he❤ 😍🤢llo❤ 😍🤢",
|
||||
is_double_wide: Some(
|
||||
FixedBitSet {
|
||||
data: [
|
||||
2626580,
|
||||
],
|
||||
length: 23,
|
||||
},
|
||||
),
|
||||
clusters: [
|
||||
Cluster {
|
||||
cell_width: 23,
|
||||
attrs: CellAttributes {
|
||||
attributes: 0,
|
||||
intensity: Normal,
|
||||
underline: None,
|
||||
blink: None,
|
||||
italic: false,
|
||||
reverse: false,
|
||||
strikethrough: false,
|
||||
invisible: false,
|
||||
wrapped: false,
|
||||
overline: false,
|
||||
semantic_type: Output,
|
||||
foreground: Default,
|
||||
background: Default,
|
||||
fat: None,
|
||||
},
|
||||
},
|
||||
],
|
||||
len: 23,
|
||||
last_cell_width: Some(
|
||||
1,
|
||||
),
|
||||
},
|
||||
)
|
||||
"#
|
||||
);
|
||||
compressed.coerce_vec_storage();
|
||||
assert_eq!(line, compressed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cluster_representation_empty() {
|
||||
let line = Line::from_cells(vec![], SEQ_ZERO);
|
||||
|
||||
let mut compressed = line.clone();
|
||||
compressed.compress_for_scrollback();
|
||||
k9::snapshot!(
|
||||
&compressed.cells,
|
||||
r#"
|
||||
C(
|
||||
ClusteredLine {
|
||||
text: "",
|
||||
is_double_wide: None,
|
||||
clusters: [],
|
||||
len: 0,
|
||||
last_cell_width: None,
|
||||
},
|
||||
)
|
||||
"#
|
||||
);
|
||||
compressed.coerce_vec_storage();
|
||||
assert_eq!(line, compressed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cluster_wrap_last() {
|
||||
let mut line: Line = "hello".into();
|
||||
line.compress_for_scrollback();
|
||||
line.set_last_cell_was_wrapped(true, 1);
|
||||
k9::snapshot!(
|
||||
line,
|
||||
r#"
|
||||
Line {
|
||||
cells: C(
|
||||
ClusteredLine {
|
||||
text: "hello",
|
||||
is_double_wide: None,
|
||||
clusters: [
|
||||
Cluster {
|
||||
cell_width: 4,
|
||||
attrs: CellAttributes {
|
||||
attributes: 0,
|
||||
intensity: Normal,
|
||||
underline: None,
|
||||
blink: None,
|
||||
italic: false,
|
||||
reverse: false,
|
||||
strikethrough: false,
|
||||
invisible: false,
|
||||
wrapped: false,
|
||||
overline: false,
|
||||
semantic_type: Output,
|
||||
foreground: Default,
|
||||
background: Default,
|
||||
fat: None,
|
||||
},
|
||||
},
|
||||
Cluster {
|
||||
cell_width: 1,
|
||||
attrs: CellAttributes {
|
||||
attributes: 2048,
|
||||
intensity: Normal,
|
||||
underline: None,
|
||||
blink: None,
|
||||
italic: false,
|
||||
reverse: false,
|
||||
strikethrough: false,
|
||||
invisible: false,
|
||||
wrapped: true,
|
||||
overline: false,
|
||||
semantic_type: Output,
|
||||
foreground: Default,
|
||||
background: Default,
|
||||
fat: None,
|
||||
},
|
||||
},
|
||||
],
|
||||
len: 5,
|
||||
last_cell_width: Some(
|
||||
1,
|
||||
),
|
||||
},
|
||||
),
|
||||
zones: [],
|
||||
seqno: 1,
|
||||
bits: NONE,
|
||||
}
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
fn bold() -> CellAttributes {
|
||||
use crate::cell::Intensity;
|
||||
let mut attr = CellAttributes::default();
|
||||
attr.set_intensity(Intensity::Bold);
|
||||
attr
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cluster_representation_attributes() {
|
||||
let line = Line::from_cells(
|
||||
vec![
|
||||
Cell::new_grapheme("a", CellAttributes::default(), None),
|
||||
Cell::new_grapheme("b", bold(), None),
|
||||
Cell::new_grapheme("c", CellAttributes::default(), None),
|
||||
Cell::new_grapheme("d", bold(), None),
|
||||
],
|
||||
SEQ_ZERO,
|
||||
);
|
||||
|
||||
let mut compressed = line.clone();
|
||||
compressed.compress_for_scrollback();
|
||||
k9::snapshot!(
|
||||
&compressed.cells,
|
||||
r#"
|
||||
C(
|
||||
ClusteredLine {
|
||||
text: "abcd",
|
||||
is_double_wide: None,
|
||||
clusters: [
|
||||
Cluster {
|
||||
cell_width: 1,
|
||||
attrs: CellAttributes {
|
||||
attributes: 0,
|
||||
intensity: Normal,
|
||||
underline: None,
|
||||
blink: None,
|
||||
italic: false,
|
||||
reverse: false,
|
||||
strikethrough: false,
|
||||
invisible: false,
|
||||
wrapped: false,
|
||||
overline: false,
|
||||
semantic_type: Output,
|
||||
foreground: Default,
|
||||
background: Default,
|
||||
fat: None,
|
||||
},
|
||||
},
|
||||
Cluster {
|
||||
cell_width: 1,
|
||||
attrs: CellAttributes {
|
||||
attributes: 1,
|
||||
intensity: Bold,
|
||||
underline: None,
|
||||
blink: None,
|
||||
italic: false,
|
||||
reverse: false,
|
||||
strikethrough: false,
|
||||
invisible: false,
|
||||
wrapped: false,
|
||||
overline: false,
|
||||
semantic_type: Output,
|
||||
foreground: Default,
|
||||
background: Default,
|
||||
fat: None,
|
||||
},
|
||||
},
|
||||
Cluster {
|
||||
cell_width: 1,
|
||||
attrs: CellAttributes {
|
||||
attributes: 0,
|
||||
intensity: Normal,
|
||||
underline: None,
|
||||
blink: None,
|
||||
italic: false,
|
||||
reverse: false,
|
||||
strikethrough: false,
|
||||
invisible: false,
|
||||
wrapped: false,
|
||||
overline: false,
|
||||
semantic_type: Output,
|
||||
foreground: Default,
|
||||
background: Default,
|
||||
fat: None,
|
||||
},
|
||||
},
|
||||
Cluster {
|
||||
cell_width: 1,
|
||||
attrs: CellAttributes {
|
||||
attributes: 1,
|
||||
intensity: Bold,
|
||||
underline: None,
|
||||
blink: None,
|
||||
italic: false,
|
||||
reverse: false,
|
||||
strikethrough: false,
|
||||
invisible: false,
|
||||
wrapped: false,
|
||||
overline: false,
|
||||
semantic_type: Output,
|
||||
foreground: Default,
|
||||
background: Default,
|
||||
fat: None,
|
||||
},
|
||||
},
|
||||
],
|
||||
len: 4,
|
||||
last_cell_width: Some(
|
||||
1,
|
||||
),
|
||||
},
|
||||
)
|
||||
"#
|
||||
);
|
||||
compressed.coerce_vec_storage();
|
||||
assert_eq!(line, compressed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cluster_append() {
|
||||
let mut cl = ClusteredLine::new();
|
||||
cl.append(Cell::new_grapheme("h", CellAttributes::default(), None));
|
||||
cl.append(Cell::new_grapheme("e", CellAttributes::default(), None));
|
||||
cl.append(Cell::new_grapheme("l", bold(), None));
|
||||
cl.append(Cell::new_grapheme("l", CellAttributes::default(), None));
|
||||
cl.append(Cell::new_grapheme("o", CellAttributes::default(), None));
|
||||
k9::snapshot!(
|
||||
cl,
|
||||
r#"
|
||||
ClusteredLine {
|
||||
text: "hello",
|
||||
is_double_wide: None,
|
||||
clusters: [
|
||||
Cluster {
|
||||
cell_width: 2,
|
||||
attrs: CellAttributes {
|
||||
attributes: 0,
|
||||
intensity: Normal,
|
||||
underline: None,
|
||||
blink: None,
|
||||
italic: false,
|
||||
reverse: false,
|
||||
strikethrough: false,
|
||||
invisible: false,
|
||||
wrapped: false,
|
||||
overline: false,
|
||||
semantic_type: Output,
|
||||
foreground: Default,
|
||||
background: Default,
|
||||
fat: None,
|
||||
},
|
||||
},
|
||||
Cluster {
|
||||
cell_width: 1,
|
||||
attrs: CellAttributes {
|
||||
attributes: 1,
|
||||
intensity: Bold,
|
||||
underline: None,
|
||||
blink: None,
|
||||
italic: false,
|
||||
reverse: false,
|
||||
strikethrough: false,
|
||||
invisible: false,
|
||||
wrapped: false,
|
||||
overline: false,
|
||||
semantic_type: Output,
|
||||
foreground: Default,
|
||||
background: Default,
|
||||
fat: None,
|
||||
},
|
||||
},
|
||||
Cluster {
|
||||
cell_width: 2,
|
||||
attrs: CellAttributes {
|
||||
attributes: 0,
|
||||
intensity: Normal,
|
||||
underline: None,
|
||||
blink: None,
|
||||
italic: false,
|
||||
reverse: false,
|
||||
strikethrough: false,
|
||||
invisible: false,
|
||||
wrapped: false,
|
||||
overline: false,
|
||||
semantic_type: Output,
|
||||
foreground: Default,
|
||||
background: Default,
|
||||
fat: None,
|
||||
},
|
||||
},
|
||||
],
|
||||
len: 5,
|
||||
last_cell_width: Some(
|
||||
1,
|
||||
),
|
||||
}
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cluster_line_new() {
|
||||
let mut line = Line::new(1);
|
||||
line.set_cell(
|
||||
0,
|
||||
Cell::new_grapheme("h", CellAttributes::default(), None),
|
||||
1,
|
||||
);
|
||||
line.set_cell(
|
||||
1,
|
||||
Cell::new_grapheme("e", CellAttributes::default(), None),
|
||||
2,
|
||||
);
|
||||
line.set_cell(2, Cell::new_grapheme("l", bold(), None), 3);
|
||||
line.set_cell(
|
||||
3,
|
||||
Cell::new_grapheme("l", CellAttributes::default(), None),
|
||||
4,
|
||||
);
|
||||
line.set_cell(
|
||||
4,
|
||||
Cell::new_grapheme("o", CellAttributes::default(), None),
|
||||
5,
|
||||
);
|
||||
k9::snapshot!(
|
||||
line,
|
||||
r#"
|
||||
Line {
|
||||
cells: C(
|
||||
ClusteredLine {
|
||||
text: "hello",
|
||||
is_double_wide: None,
|
||||
clusters: [
|
||||
Cluster {
|
||||
cell_width: 2,
|
||||
attrs: CellAttributes {
|
||||
attributes: 0,
|
||||
intensity: Normal,
|
||||
underline: None,
|
||||
blink: None,
|
||||
italic: false,
|
||||
reverse: false,
|
||||
strikethrough: false,
|
||||
invisible: false,
|
||||
wrapped: false,
|
||||
overline: false,
|
||||
semantic_type: Output,
|
||||
foreground: Default,
|
||||
background: Default,
|
||||
fat: None,
|
||||
},
|
||||
},
|
||||
Cluster {
|
||||
cell_width: 1,
|
||||
attrs: CellAttributes {
|
||||
attributes: 1,
|
||||
intensity: Bold,
|
||||
underline: None,
|
||||
blink: None,
|
||||
italic: false,
|
||||
reverse: false,
|
||||
strikethrough: false,
|
||||
invisible: false,
|
||||
wrapped: false,
|
||||
overline: false,
|
||||
semantic_type: Output,
|
||||
foreground: Default,
|
||||
background: Default,
|
||||
fat: None,
|
||||
},
|
||||
},
|
||||
Cluster {
|
||||
cell_width: 2,
|
||||
attrs: CellAttributes {
|
||||
attributes: 0,
|
||||
intensity: Normal,
|
||||
underline: None,
|
||||
blink: None,
|
||||
italic: false,
|
||||
reverse: false,
|
||||
strikethrough: false,
|
||||
invisible: false,
|
||||
wrapped: false,
|
||||
overline: false,
|
||||
semantic_type: Output,
|
||||
foreground: Default,
|
||||
background: Default,
|
||||
fat: None,
|
||||
},
|
||||
},
|
||||
],
|
||||
len: 5,
|
||||
last_cell_width: Some(
|
||||
1,
|
||||
),
|
||||
},
|
||||
),
|
||||
zones: [],
|
||||
seqno: 5,
|
||||
bits: NONE,
|
||||
}
|
||||
"#
|
||||
);
|
||||
}
|
103
termwiz/src/surface/line/vecstorage.rs
Normal file
103
termwiz/src/surface/line/vecstorage.rs
Normal file
@ -0,0 +1,103 @@
|
||||
use crate::cell::Cell;
|
||||
use crate::surface::line::cellref::CellRef;
|
||||
#[cfg(feature = "use_serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
#[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct VecStorage {
|
||||
cells: Vec<Cell>,
|
||||
}
|
||||
|
||||
impl VecStorage {
|
||||
pub(crate) fn new(cells: Vec<Cell>) -> Self {
|
||||
Self { cells }
|
||||
}
|
||||
|
||||
pub(crate) fn set_cell(&mut self, idx: usize, mut cell: Cell, clear_image_placement: bool) {
|
||||
if !clear_image_placement {
|
||||
if let Some(images) = self.cells[idx].attrs().images() {
|
||||
for image in images {
|
||||
if image.has_placement_id() {
|
||||
cell.attrs_mut().attach_image(Box::new(image));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.cells[idx] = cell;
|
||||
}
|
||||
|
||||
pub(crate) fn scan_and_create_hyperlinks(
|
||||
&mut self,
|
||||
line: &str,
|
||||
matches: Vec<crate::hyperlink::RuleMatch>,
|
||||
) -> bool {
|
||||
// The capture range is measured in bytes but we need to translate
|
||||
// that to the index of the column. This is complicated a bit further
|
||||
// because double wide sequences have a blank column cell after them
|
||||
// in the cells array, but the string we match against excludes that
|
||||
// string.
|
||||
let mut cell_idx = 0;
|
||||
let mut has_implicit_hyperlinks = false;
|
||||
for (byte_idx, _grapheme) in line.grapheme_indices(true) {
|
||||
let cell = &mut self.cells[cell_idx];
|
||||
let mut matched = false;
|
||||
for m in &matches {
|
||||
if m.range.contains(&byte_idx) {
|
||||
let attrs = cell.attrs_mut();
|
||||
// Don't replace existing links
|
||||
if attrs.hyperlink().is_none() {
|
||||
attrs.set_hyperlink(Some(Arc::clone(&m.link)));
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
cell_idx += cell.width();
|
||||
if matched {
|
||||
has_implicit_hyperlinks = true;
|
||||
}
|
||||
}
|
||||
|
||||
has_implicit_hyperlinks
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for VecStorage {
|
||||
type Target = Vec<Cell>;
|
||||
|
||||
fn deref(&self) -> &Vec<Cell> {
|
||||
&self.cells
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for VecStorage {
|
||||
fn deref_mut(&mut self) -> &mut Vec<Cell> {
|
||||
&mut self.cells
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterates over a slice of Cell, yielding only visible cells
|
||||
pub(crate) struct VecStorageIter<'a> {
|
||||
pub cells: std::slice::Iter<'a, Cell>,
|
||||
pub idx: usize,
|
||||
pub skip_width: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for VecStorageIter<'a> {
|
||||
type Item = CellRef<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<CellRef<'a>> {
|
||||
while self.skip_width > 0 {
|
||||
self.skip_width -= 1;
|
||||
let _ = self.cells.next()?;
|
||||
self.idx += 1;
|
||||
}
|
||||
let cell = self.cells.next()?;
|
||||
let cell_index = self.idx;
|
||||
self.idx += 1;
|
||||
self.skip_width = cell.width().saturating_sub(1);
|
||||
Some(CellRef::CellRef { cell_index, cell })
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user