Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
heartwood crates radicle-term src textarea.rs
use crate::{Constraint, Element, Line, Paint, Size, cell::Cell};

/// Default text wrap width.
pub const DEFAULT_WRAP: usize = 80;
/// Soft tab replacement for '\t'.
pub const SOFT_TAB: &str = "  ";

/// Text area.
///
/// A block of text that can contain multiple lines.
#[derive(Debug)]
pub struct TextArea {
    body: Paint<String>,
    wrap: usize,
}

impl TextArea {
    /// Create a new text area.
    pub fn new(body: impl Into<Paint<String>>) -> Self {
        Self {
            body: body.into(),
            wrap: DEFAULT_WRAP,
        }
    }

    /// Set wrap width.
    pub fn wrap(mut self, cols: usize) -> Self {
        self.wrap = cols;
        self
    }

    /// Get the lines of text in this text area.
    pub fn lines(&self) -> impl Iterator<Item = String> + use<> {
        let mut lines: Vec<String> = Vec::new();
        let mut fenced = false;

        for line in self
            .body
            .content()
            .lines()
            // Replace tabs as their visual width cannot be calculated.
            .map(|l| l.replace('\t', SOFT_TAB))
        {
            // Fenced code block support.
            if line.starts_with("```") {
                fenced = !fenced;
            }
            // Code blocks are not wrapped, they are truncated.
            if fenced || line.starts_with('\t') || line.starts_with(' ') {
                lines.push(line.truncate(self.wrap, "…"));
                continue;
            }
            let mut current = String::new();

            for word in line.split_whitespace() {
                if current.width() + word.width() > self.wrap {
                    lines.push(current.trim_end().to_owned());
                    current = word.to_owned();
                } else {
                    current.push_str(word);
                }
                current.push(' ');
            }
            lines.push(current.trim_end().to_owned());
        }
        lines.into_iter()
    }

    /// Box the text area.
    pub fn boxed(self) -> Box<dyn Element> {
        Box::new(self)
    }
}

impl Element for TextArea {
    fn size(&self, _parent: Constraint) -> Size {
        let cols = self.lines().map(|l| l.width()).max().unwrap_or(0);
        let rows = self.lines().count();

        Size::new(cols, rows)
    }

    fn render(&self, _parent: Constraint) -> Vec<Line> {
        self.lines()
            .map(|l| Line::new(Paint::new(l).with_style(self.body.style)))
            .collect()
    }
}

/// Create a new text area.
pub fn textarea(content: impl Into<Paint<String>>) -> TextArea {
    TextArea::new(content)
}

#[cfg(test)]
mod test {
    use super::*;
    use pretty_assertions::assert_eq;

    #[test]
    fn test_wrapping() {
        let t = TextArea::new(
            "Radicle enables users to run their own nodes, \
            ensuring censorship-resistant code collaboration \
            and fostering a resilient network without reliance \
            on third-parties.",
        )
        .wrap(50);
        let wrapped = t.lines().collect::<Vec<_>>();

        assert_eq!(
            wrapped,
            vec![
                "Radicle enables users to run their own nodes,".to_owned(),
                "ensuring censorship-resistant code collaboration".to_owned(),
                "and fostering a resilient network without reliance".to_owned(),
                "on third-parties.".to_owned(),
            ]
        );
    }

    #[test]
    fn test_wrapping_paragraphs() {
        let t = TextArea::new(
            "Radicle enables users to run their own nodes, \
            ensuring censorship-resistant code collaboration \
            and fostering a resilient network without reliance \
            on third-parties.\n\n\
            All social artifacts are stored in git, and signed \
            using public-key cryptography. Radicle verifies \
            the authenticity and authorship of all data \
            automatically.",
        )
        .wrap(50);
        let wrapped = t.lines().collect::<Vec<_>>();

        assert_eq!(
            wrapped,
            vec![
                "Radicle enables users to run their own nodes,".to_owned(),
                "ensuring censorship-resistant code collaboration".to_owned(),
                "and fostering a resilient network without reliance".to_owned(),
                "on third-parties.".to_owned(),
                "".to_owned(),
                "All social artifacts are stored in git, and signed".to_owned(),
                "using public-key cryptography. Radicle verifies".to_owned(),
                "the authenticity and authorship of all data".to_owned(),
                "automatically.".to_owned(),
            ]
        );
    }

    #[test]
    fn test_wrapping_code_block() {
        let t = TextArea::new(
            "\
Here's an example:

  $ git push rad://z3gqcJUoA1n9HaHKufZs5FCSGazv5/z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
  $ rad sync

Run the above and wait for your project to sync.\
        ",
        )
        .wrap(50);
        let wrapped = t.lines().collect::<Vec<_>>();

        assert_eq!(
            wrapped,
            vec![
                "Here's an example:".to_owned(),
                "".to_owned(),
                "  $ git push rad://z3gqcJUoA1n9HaHKufZs5FCSGazv5/…".to_owned(),
                "  $ rad sync".to_owned(),
                "".to_owned(),
                "Run the above and wait for your project to sync.".to_owned()
            ]
        );
    }

    #[test]
    fn test_wrapping_fenced_block() {
        let t = TextArea::new(
            "\
Here's an example:
```
$ git push rad://z3gqcJUoA1n9HaHKufZs5FCSGazv5/z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
$ rad sync
```
Run the above and wait for your project to sync.\
        ",
        )
        .wrap(40);
        let wrapped = t.lines().collect::<Vec<_>>();

        assert_eq!(
            wrapped,
            vec![
                "Here's an example:".to_owned(),
                "```".to_owned(),
                "$ git push rad://z3gqcJUoA1n9HaHKufZs5F…".to_owned(),
                "$ rad sync".to_owned(),
                "```".to_owned(),
                "Run the above and wait for your project".to_owned(),
                "to sync.".to_owned()
            ]
        );
    }
}