<commit_before>use std::io::Write;

use itertools;
use liquid_error::{Result, ResultLiquidExt};
use liquid_value::Value;

use compiler::BlockElement;
use compiler::LiquidOptions;
use compiler::TagBlock;
use compiler::TagTokenIter;
use interpreter::Context;
use interpreter::Expression;
use interpreter::Renderable;
use interpreter::Template;

#[derive(Debug)]
struct CaseOption {
    args: Vec<Expression>,
    template: Template,
}

impl CaseOption {
    fn new(args: Vec<Expression>, template: Template) -> CaseOption {
        CaseOption { args, template }
    }

    fn evaluate(&self, value: &Value, context: &Context) -> Result<bool> {
        for a in &self.args {
            let v = a.evaluate(context)?;
            if *v == *value {
                return Ok(true);
            }
        }
        Ok(false)
    }

    fn trace(&self) -> String {
        format!("{{% when {} %}}", itertools::join(self.args.iter(), " or "))
    }
}

#[derive(Debug)]
struct Case {
    target: Expression,
    cases: Vec<CaseOption>,
    else_block: Option<Template>,
}

impl Case {
    fn trace(&self) -> String {
        format!("{{% case {} %}}", self.target)
    }
}

impl Renderable for Case {
    fn render_to(&self, writer: &mut Write, context: &mut Context) -> Result<()> {
        let value = self.target.evaluate(context)?.to_owned();
        for case in &self.cases {
            if case.evaluate(&value, context)? {
                return case
                    .template
                    .render_to(writer, context)
                    .trace_with(|| case.trace())
                    .trace_with(|| self.trace())
                    .context_with(|| (self.target.to_string(), value.to_string()));
            }
        }

        if let Some(ref t) = self.else_block {
            return t
                .render_to(writer, context)
                .trace("{{% else %}}")
                .trace_with(|| self.trace())
                .context_with(|| (self.target.to_string(), value.to_string()));
        }

        Ok(())
    }
}

fn parse_condition(arguments: &mut TagTokenIter) -> Result<Vec<Expression>> {
    let mut values = Vec::new();

    let first_value = arguments
        .expect_next("Value expected")?
        .expect_value()
        .into_result()?;
    values.push(first_value);

    while let Some(token) = arguments.next() {
        token
            .expect_str("or")
            .into_result_custom_msg("\"or\" expected.")?;

        let value = arguments
            .expect_next("Value expected")?
            .expect_value()
            .into_result()?;
        values.push(value);
    }

    // no more arguments should be supplied, trying to supply them is an error
    arguments.expect_nothing()?;
    Ok(values)
}

pub fn case_block(
    _tag_name: &str,
    mut arguments: TagTokenIter,
    mut tokens: TagBlock,
    options: &LiquidOptions,
) -> Result<Box<Renderable>> {
    let target = arguments
        .expect_next("Value expected.")?
        .expect_value()
        .into_result()?;

    // no more arguments should be supplied, trying to supply them is an error
    arguments.expect_nothing()?;

    let mut cases = Vec::new();
    let mut else_block = None;
    let mut current_block = Vec::new();
    let mut current_condition = None;

    while let Some(element) = tokens.next()? {
        match element {
            BlockElement::Tag(mut tag) => match tag.name() {
                "when" => {
                    if let Some(condition) = current_condition {
                        cases.push(CaseOption::new(condition, Template::new(current_block)));
                    }
                    current_block = Vec::new();
                    current_condition = Some(parse_condition(tag.tokens())?);
                }
                "else" => {
                    // no more arguments should be supplied, trying to supply them is an error
                    tag.tokens().expect_nothing()?;
                    else_block = Some(tokens.parse_all(options)?);
                    break;
                }
                _ => current_block.push(tag.parse(&mut tokens, options)?),
            },
            element => current_block.push(element.parse(&mut tokens, options)?),
        }
    }

    if let Some(condition) = current_condition {
        cases.push(CaseOption::new(condition, Template::new(current_block)));
    }

    let else_block = else_block.map(Template::new);

    tokens.assert_empty();
    Ok(Box::new(Case {
        target,
        cases,
        else_block,
    }))
}

#[cfg(test)]
mod test {
    use super::*;
    use compiler;
    use interpreter;

    fn options() -> LiquidOptions {
        let mut options = LiquidOptions::default();
        options
            .blocks
            .insert("case", (case_block as compiler::FnParseBlock).into());
        options
    }

    #[test]
    fn test_case_block() {
        let text = concat!(
            "{% case x %}",
            "{% when 2 %}",
            "two",
            "{% when 3 or 4 %}",
            "three and a half",
            "{% else %}",
            "otherwise",
            "{% endcase %}"
        );
        let options = options();
        let template = compiler::parse(text, &options)
            .map(interpreter::Template::new)
            .unwrap();

        let mut context = Context::new();
        context.stack_mut().set_global("x", Value::scalar(2f64));
        assert_eq!(template.render(&mut context).unwrap(), "two");

        context.stack_mut().set_global("x", Value::scalar(3f64));
        assert_eq!(template.render(&mut context).unwrap(), "three and a half");

        context.stack_mut().set_global("x", Value::scalar(4f64));
        assert_eq!(template.render(&mut context).unwrap(), "three and a half");

        context.stack_mut().set_global("x", Value::scalar("nope"));
        assert_eq!(template.render(&mut context).unwrap(), "otherwise");
    }

    #[test]
    fn test_no_matches_returns_empty_string() {
        let text = concat!(
            "{% case x %}",
            "{% when 2 %}",
            "two",
            "{% when 3 or 4 %}",
            "three and a half",
            "{% endcase %}"
        );
        let options = options();
        let template = compiler::parse(text, &options)
            .map(interpreter::Template::new)
            .unwrap();

        let mut context = Context::new();
        context.stack_mut().set_global("x", Value::scalar("nope"));
        assert_eq!(template.render(&mut context).unwrap(), "");
    }

    #[test]
    fn multiple_else_blocks_is_an_error() {
        let text = concat!(
            "{% case x %}",
            "{% when 2 %}",
            "two",
            "{% else %}",
            "else #1",
            "{% else %}",
            "else # 2",
            "{% endcase %}"
        );
        let options = options();
        let template = compiler::parse(text, &options).map(interpreter::Template::new);
        assert!(template.is_err());
    }
}
<commit_msg>feat(case_block): support comma separated values in `when`<commit_after>use std::io::Write;

use itertools;
use liquid_error::{Result, ResultLiquidExt};
use liquid_value::Value;

use compiler::BlockElement;
use compiler::LiquidOptions;
use compiler::TagBlock;
use compiler::TagTokenIter;
use compiler::TryMatchToken;
use interpreter::Context;
use interpreter::Expression;
use interpreter::Renderable;
use interpreter::Template;

#[derive(Debug)]
struct CaseOption {
    args: Vec<Expression>,
    template: Template,
}

impl CaseOption {
    fn new(args: Vec<Expression>, template: Template) -> CaseOption {
        CaseOption { args, template }
    }

    fn evaluate(&self, value: &Value, context: &Context) -> Result<bool> {
        for a in &self.args {
            let v = a.evaluate(context)?;
            if *v == *value {
                return Ok(true);
            }
        }
        Ok(false)
    }

    fn trace(&self) -> String {
        format!("{{% when {} %}}", itertools::join(self.args.iter(), " or "))
    }
}

#[derive(Debug)]
struct Case {
    target: Expression,
    cases: Vec<CaseOption>,
    else_block: Option<Template>,
}

impl Case {
    fn trace(&self) -> String {
        format!("{{% case {} %}}", self.target)
    }
}

impl Renderable for Case {
    fn render_to(&self, writer: &mut Write, context: &mut Context) -> Result<()> {
        let value = self.target.evaluate(context)?.to_owned();
        for case in &self.cases {
            if case.evaluate(&value, context)? {
                return case
                    .template
                    .render_to(writer, context)
                    .trace_with(|| case.trace())
                    .trace_with(|| self.trace())
                    .context_with(|| (self.target.to_string(), value.to_string()));
            }
        }

        if let Some(ref t) = self.else_block {
            return t
                .render_to(writer, context)
                .trace("{{% else %}}")
                .trace_with(|| self.trace())
                .context_with(|| (self.target.to_string(), value.to_string()));
        }

        Ok(())
    }
}

fn parse_condition(arguments: &mut TagTokenIter) -> Result<Vec<Expression>> {
    let mut values = Vec::new();

    let first_value = arguments
        .expect_next("Value expected")?
        .expect_value()
        .into_result()?;
    values.push(first_value);

    while let Some(token) = arguments.next() {
        if let TryMatchToken::Fails(token) = token.expect_str("or") {
            token
                .expect_str(",")
                .into_result_custom_msg("\"or\" or \",\" expected.")?;
        }

        let value = arguments
            .expect_next("Value expected")?
            .expect_value()
            .into_result()?;
        values.push(value);
    }

    // no more arguments should be supplied, trying to supply them is an error
    arguments.expect_nothing()?;
    Ok(values)
}

pub fn case_block(
    _tag_name: &str,
    mut arguments: TagTokenIter,
    mut tokens: TagBlock,
    options: &LiquidOptions,
) -> Result<Box<Renderable>> {
    let target = arguments
        .expect_next("Value expected.")?
        .expect_value()
        .into_result()?;

    // no more arguments should be supplied, trying to supply them is an error
    arguments.expect_nothing()?;

    let mut cases = Vec::new();
    let mut else_block = None;
    let mut current_block = Vec::new();
    let mut current_condition = None;

    while let Some(element) = tokens.next()? {
        match element {
            BlockElement::Tag(mut tag) => match tag.name() {
                "when" => {
                    if let Some(condition) = current_condition {
                        cases.push(CaseOption::new(condition, Template::new(current_block)));
                    }
                    current_block = Vec::new();
                    current_condition = Some(parse_condition(tag.tokens())?);
                }
                "else" => {
                    // no more arguments should be supplied, trying to supply them is an error
                    tag.tokens().expect_nothing()?;
                    else_block = Some(tokens.parse_all(options)?);
                    break;
                }
                _ => current_block.push(tag.parse(&mut tokens, options)?),
            },
            element => current_block.push(element.parse(&mut tokens, options)?),
        }
    }

    if let Some(condition) = current_condition {
        cases.push(CaseOption::new(condition, Template::new(current_block)));
    }

    let else_block = else_block.map(Template::new);

    tokens.assert_empty();
    Ok(Box::new(Case {
        target,
        cases,
        else_block,
    }))
}

#[cfg(test)]
mod test {
    use super::*;
    use compiler;
    use interpreter;

    fn options() -> LiquidOptions {
        let mut options = LiquidOptions::default();
        options
            .blocks
            .insert("case", (case_block as compiler::FnParseBlock).into());
        options
    }

    #[test]
    fn test_case_block() {
        let text = concat!(
            "{% case x %}",
            "{% when 2 %}",
            "two",
            "{% when 3 or 4 %}",
            "three and a half",
            "{% else %}",
            "otherwise",
            "{% endcase %}"
        );
        let options = options();
        let template = compiler::parse(text, &options)
            .map(interpreter::Template::new)
            .unwrap();

        let mut context = Context::new();
        context.stack_mut().set_global("x", Value::scalar(2f64));
        assert_eq!(template.render(&mut context).unwrap(), "two");

        context.stack_mut().set_global("x", Value::scalar(3f64));
        assert_eq!(template.render(&mut context).unwrap(), "three and a half");

        context.stack_mut().set_global("x", Value::scalar(4f64));
        assert_eq!(template.render(&mut context).unwrap(), "three and a half");

        context.stack_mut().set_global("x", Value::scalar("nope"));
        assert_eq!(template.render(&mut context).unwrap(), "otherwise");
    }

    #[test]
    fn test_no_matches_returns_empty_string() {
        let text = concat!(
            "{% case x %}",
            "{% when 2 %}",
            "two",
            "{% when 3 or 4 %}",
            "three and a half",
            "{% endcase %}"
        );
        let options = options();
        let template = compiler::parse(text, &options)
            .map(interpreter::Template::new)
            .unwrap();

        let mut context = Context::new();
        context.stack_mut().set_global("x", Value::scalar("nope"));
        assert_eq!(template.render(&mut context).unwrap(), "");
    }

    #[test]
    fn multiple_else_blocks_is_an_error() {
        let text = concat!(
            "{% case x %}",
            "{% when 2 %}",
            "two",
            "{% else %}",
            "else #1",
            "{% else %}",
            "else # 2",
            "{% endcase %}"
        );
        let options = options();
        let template = compiler::parse(text, &options).map(interpreter::Template::new);
        assert!(template.is_err());
    }
}
<|endoftext|>


<commit_before>use core::float::consts::pi;

use angle::*;
use mat::Mat4;
use vec::{Vec3, Vec4};

#[test]
fn test_radians() {
    assert Radians(0.0).to_degrees()            == Degrees(0.0);
    assert *Radians(pi / 4.0).to_degrees()      == *Degrees(45.0);   // Removing the unwrap operators causes an ICE.
    assert *Radians(pi / 2.0).to_degrees()      == *Degrees(90.0);   // I think it only happens on operations where
    assert *Radians(pi).to_degrees()            == *Degrees(180.0);  // Rust has to do a bit of type inference.
    assert *Radians(2.0 * pi).to_degrees()      == *Degrees(360.0);
    assert *Radians(5.0 * pi).to_degrees()      == *Degrees(900.0);
    assert *Radians(-pi).to_degrees()           == *Degrees(-180.0);

    assert Radians(0.0).to_radians()            == Radians(0.0);
    assert *Radians(5.0 * pi).to_radians()      == *Radians(5.0 * pi);
    assert *Radians(-pi).to_radians()           == *Radians(-pi);

    assert *Radians(pi).wrap()                  == *Radians(pi);
    assert *Radians(3.0 * pi).wrap()            == *Radians(pi);
    assert *Radians(2.0 * pi).wrap()            == *Radians(0.0);
    assert *Radians(-pi).wrap()                 == *Radians(-pi);   // FIXME: should be in the domain of 0 to two_pi
    assert *Radians(-3.0 * pi).wrap()           == *Radians(-pi);   // FIXME: should be in the domain of 0 to two_pi
    assert *Radians(-2.0 * pi).wrap()           == *Radians(0.0);

    assert *Radians(0.0).opposite()             == *Radians(pi as float);
    assert *Radians(pi / 2.0).opposite()        == *Radians((pi / 2.0) * 3.0);
    assert *Radians(pi * 3.0).opposite()        == *Radians(0.0);
    assert *Radians(-2.0 * pi).opposite()       == *Radians(-pi);   // FIXME: should be in the domain of 0 to two_pi

    assert *(Radians(pi) + Radians(pi))         == *Radians(2.0 * pi);
    assert *(Radians(2.0 * pi) - Radians(pi))   == *Radians(pi);
    assert *(Radians(pi) * 2.0)                 == *Radians(2.0 * pi);
    assert *(Radians(pi) / 2.0)                 == *Radians(pi / 2.0);
    assert *(Radians(3.0 * pi) % (2.0 * pi))    == *Radians(pi);
    assert *(-Radians(pi))                      == *Radians(-pi);

    assert fmt!("%s", Radians(1).to_str())      == ~"1 rad";
}

#[test]
fn test_degrees() {
    assert Degrees(0.0).to_radians()            == Radians(0.0);
    assert Degrees(45.0).to_radians()           == Radians(pi / 4.0 as float);  // Rust can't infer these types
    assert Degrees(90.0).to_radians()           == Radians(pi / 2.0 as float);
    assert Degrees(180.0).to_radians()          == Radians(pi       as float);
    assert Degrees(360.0).to_radians()          == Radians(2.0 * pi as float);
    assert Degrees(900.0).to_radians()          == Radians(5.0 * pi as float);
    assert Degrees(-180.0).to_radians()         == Radians(-pi      as float);

    assert Degrees(0.0).to_degrees()            == Degrees(0.0);
    assert Degrees(900.0).to_degrees()          == Degrees(900.0);
    assert Degrees(-180.0).to_degrees()         == Degrees(-180.0);

    assert Degrees(90.0).wrap()                 == Degrees(90.0);
    assert Degrees(450.0).wrap()                == Degrees(90.0);
    assert Degrees(360.0).wrap()                == Degrees(0.0);
    assert Degrees(-90.0).wrap()                == Degrees(-90.0);   // FIXME: should be in the domain of 0 to 360
    assert Degrees(-450.0).wrap()               == Degrees(-90.0);   // FIXME: should be in the domain of 0 to 360
    assert Degrees(-360.0).wrap()               == Degrees(0.0);

    assert Degrees(0.0).opposite()             == Degrees(180.0);
    assert Degrees(90.0).opposite()            == Degrees(270.0);
    assert Degrees(540.0).opposite()           == Degrees(0.0);
    assert Degrees(-360.0).opposite()          == Degrees(-180.0);   // FIXME: should be in the domain of 0 to 360

    assert (Degrees(180.0) + Degrees(180.0))    == Degrees(360.0);
    assert (Degrees(360.0) - Degrees(180.0))    == Degrees(180.0);
    assert (Degrees(360.0) * 2.0)               == Degrees(720.0);
    assert (Degrees(180.0) / 2.0)               == Degrees(90.0);
    assert (Degrees(540.0) % (360.0))           == Degrees(180.0);
    assert (-Degrees(180.0))                    == Degrees(-180.0);

    assert fmt!("%s", Degrees(180.0).to_str())  == ~"180°";
}

#[test]
fn test_rotation() {
    {
        let pos = Vec4::new(1.0, 0.0, 0.0, 1.0);   // the position to transform
        let rot = Rotation {
            theta: Degrees(180.0).to_radians(),
            axis:  Vec3::new(0.0, 0.0, 1.0),       // unit_z
        };

        let newpos = rot.to_mat4().mul_v(&pos);
        let expected_pos = Vec4::new(-1.0, 0.0, 0.0, 1.0);

        assert newpos == expected_pos;
    }
    {
        let pos = Vec4::new(4f32, 0f32, 0f32, 1f32);

        let rot_a = Rotation {
            theta: Degrees(90f32).to_radians(),
            axis:  Vec3::new(0f32, 1f32, 0f32),     // unit_y
        };

        let rot_b = Rotation {
            theta: Degrees(90f32).to_radians(),
            axis:  -Vec3::new(0f32, 1f32, 0f32),    // -unit_y
        };

        let newpos_a = rot_a.to_mat4().mul_v(&pos);
        let newpos_b = rot_b.to_mat4().mul_v(&pos);

        let expected_pos_a = Vec4::new(0f32, 0f32, -4f32, 1f32);
        let expected_pos_b = Vec4::new(0f32, 0f32,  4f32, 1f32);

        assert newpos_a == expected_pos_a;
        assert newpos_b == expected_pos_b;
    }

    // TODO: test to_quat
}<commit_msg>Remove unwrap operators<commit_after>use core::float::consts::pi;

use angle::*;
use mat::Mat4;
use vec::{Vec3, Vec4};

#[test]
fn test_radians() {
    assert Radians(0.0).to_degrees()            == Degrees(0.0);
    assert Radians(pi / 4.0).to_degrees()       == Degrees(45.0);
    assert Radians(pi / 2.0).to_degrees()       == Degrees(90.0);
    assert Radians(pi).to_degrees()             == Degrees(180.0);
    assert Radians(2.0 * pi).to_degrees()       == Degrees(360.0);
    assert Radians(5.0 * pi).to_degrees()       == Degrees(900.0);
    assert Radians(-pi).to_degrees()            == Degrees(-180.0);

    assert Radians(0.0).to_radians()            == Radians(0.0);
    assert Radians(5.0 * pi).to_radians()       == Radians(5.0 * pi);
    assert Radians(-pi).to_radians()            == Radians(-pi);

    assert Radians(pi).wrap()                   == Radians(pi);
    assert Radians(3.0 * pi).wrap()             == Radians(pi);
    assert Radians(2.0 * pi).wrap()             == Radians(0.0);
    assert Radians(-pi).wrap()                  == Radians(-pi);   // FIXME: should be in the domain of 0 to two_pi
    assert Radians(-3.0 * pi).wrap()            == Radians(-pi);   // FIXME: should be in the domain of 0 to two_pi
    assert Radians(-2.0 * pi).wrap()            == Radians(0.0);

    assert Radians(0.0).opposite()              == Radians(pi as float);
    assert Radians(pi / 2.0).opposite()         == Radians((pi / 2.0) * 3.0);
    assert Radians(pi * 3.0).opposite()         == Radians(0.0);
    assert Radians(-2.0 * pi).opposite()        == Radians(-pi);   // FIXME: should be in the domain of 0 to two_pi

    assert Radians(pi) + Radians(pi)            == Radians(2.0 * pi);
    assert Radians(2.0 * pi) - Radians(pi)      == Radians(pi);
    assert Radians(pi) * 2.0                    == Radians(2.0 * pi);
    assert Radians(pi) / 2.0                    == Radians(pi / 2.0);
    assert Radians(3.0 * pi) % (2.0 * pi)       == Radians(pi);
    assert -Radians(pi)                         == Radians(-pi);

    assert fmt!("%s", Radians(1).to_str())      == ~"1 rad";
}

#[test]
fn test_degrees() {
    assert Degrees(0.0).to_radians()            == Radians(0.0);
    assert Degrees(45.0).to_radians()           == Radians(pi / 4.0 as float);  // Rust can't infer these types
    assert Degrees(90.0).to_radians()           == Radians(pi / 2.0 as float);
    assert Degrees(180.0).to_radians()          == Radians(pi       as float);
    assert Degrees(360.0).to_radians()          == Radians(2.0 * pi as float);
    assert Degrees(900.0).to_radians()          == Radians(5.0 * pi as float);
    assert Degrees(-180.0).to_radians()         == Radians(-pi      as float);

    assert Degrees(0.0).to_degrees()            == Degrees(0.0);
    assert Degrees(900.0).to_degrees()          == Degrees(900.0);
    assert Degrees(-180.0).to_degrees()         == Degrees(-180.0);

    assert Degrees(90.0).wrap()                 == Degrees(90.0);
    assert Degrees(450.0).wrap()                == Degrees(90.0);
    assert Degrees(360.0).wrap()                == Degrees(0.0);
    assert Degrees(-90.0).wrap()                == Degrees(-90.0);   // FIXME: should be in the domain of 0 to 360
    assert Degrees(-450.0).wrap()               == Degrees(-90.0);   // FIXME: should be in the domain of 0 to 360
    assert Degrees(-360.0).wrap()               == Degrees(0.0);

    assert Degrees(0.0).opposite()              == Degrees(180.0);
    assert Degrees(90.0).opposite()             == Degrees(270.0);
    assert Degrees(540.0).opposite()            == Degrees(0.0);
    assert Degrees(-360.0).opposite()           == Degrees(-180.0);   // FIXME: should be in the domain of 0 to 360

    assert Degrees(180.0) + Degrees(180.0)      == Degrees(360.0);
    assert Degrees(360.0) - Degrees(180.0)      == Degrees(180.0);
    assert Degrees(360.0) * 2.0                 == Degrees(720.0);
    assert Degrees(180.0) / 2.0                 == Degrees(90.0);
    assert Degrees(540.0) % (360.0)             == Degrees(180.0);
    assert -Degrees(180.0)                      == Degrees(-180.0);

    assert fmt!("%s", Degrees(180.0).to_str())  == ~"180°";
}

#[test]
fn test_rotation() {
    {
        let pos = Vec4::new(1.0, 0.0, 0.0, 1.0);   // the position to transform
        let rot = Rotation {
            theta: Degrees(180.0).to_radians(),
            axis:  Vec3::new(0.0, 0.0, 1.0),       // unit_z
        };

        let newpos = rot.to_mat4().mul_v(&pos);
        let expected_pos = Vec4::new(-1.0, 0.0, 0.0, 1.0);

        assert newpos == expected_pos;
    }
    {
        let pos = Vec4::new(4f32, 0f32, 0f32, 1f32);

        let rot_a = Rotation {
            theta: Degrees(90f32).to_radians(),
            axis:  Vec3::new(0f32, 1f32, 0f32),     // unit_y
        };

        let rot_b = Rotation {
            theta: Degrees(90f32).to_radians(),
            axis:  -Vec3::new(0f32, 1f32, 0f32),    // -unit_y
        };

        let newpos_a = rot_a.to_mat4().mul_v(&pos);
        let newpos_b = rot_b.to_mat4().mul_v(&pos);

        let expected_pos_a = Vec4::new(0f32, 0f32, -4f32, 1f32);
        let expected_pos_b = Vec4::new(0f32, 0f32,  4f32, 1f32);

        assert newpos_a == expected_pos_a;
        assert newpos_b == expected_pos_b;
    }

    // TODO: test to_quat
}<|endoftext|>


<commit_before>use crate::constants::*;

#[derive(Clone, Debug, PartialEq)]
pub enum Style {
    Old,
    OldInverse,
    New,
    NewInverse,
    Error,
}

#[derive(Clone, Debug, PartialEq)]
pub struct StyledToken {
    token: String,
    style: Style,
}

impl StyledToken {
    pub fn new(token: String, style: Style) -> StyledToken {
        return StyledToken { token, style };
    }

    pub fn is_whitespace(&self) -> bool {
        let mut chars_iterator = self.token.chars();
        let first_char = chars_iterator.next().unwrap();
        if chars_iterator.next().is_some() {
            // Multiple chars found in this token, but whitespace will only be
            // one per token.
            return false;
        }

        return first_char.is_whitespace();
    }
}

pub struct TokenCollector {
    line_prefix: StyledToken,
    tokens: Vec<StyledToken>,
    rendered: bool,
}

impl Style {
    #[must_use]
    pub fn is_inverse(&self) -> bool {
        match self {
            Style::OldInverse | Style::NewInverse | Style::Error => {
                return true;
            }
            _ => {
                return false;
            }
        }
    }

    pub fn inverted(&self) -> Style {
        return match self {
            Style::Old => Style::OldInverse,
            Style::New => Style::NewInverse,
            Style::OldInverse => Style::OldInverse,
            Style::NewInverse => Style::NewInverse,
            Style::Error => Style::Error,
        };
    }

    pub fn not_inverted(&self) -> Style {
        return match self {
            Style::Old => Style::Old,
            Style::New => Style::New,
            Style::OldInverse => Style::Old,
            Style::NewInverse => Style::New,
            Style::Error => Style::Error,
        };
    }

    #[must_use]
    pub fn color<'a>(&self) -> &'a str {
        match self {
            Style::Old => {
                return OLD;
            }
            Style::OldInverse => {
                return OLD;
            }
            Style::New => {
                return NEW;
            }
            Style::NewInverse => {
                return NEW;
            }
            Style::Error => {
                return ERROR;
            }
        }
    }
}

impl TokenCollector {
    #[must_use]
    pub fn create(line_prefix: StyledToken) -> Self {
        return TokenCollector {
            line_prefix,
            tokens: Vec::new(),
            rendered: false,
        };
    }

    pub fn push(&mut self, token: StyledToken) {
        self.tokens.push(token);
    }

    #[must_use]
    fn render_row(&self, row: &mut [StyledToken]) -> String {
        let mut rendered = String::new();

        if self.line_prefix.style == Style::New {
            highlight_trailing_whitespace(row);
            highlight_nonleading_tab(row);
        }

        // Set inverse from prefix
        let mut is_inverse = self.line_prefix.style.is_inverse();
        if is_inverse {
            rendered.push_str(INVERSE_VIDEO);
        }

        // Set line color from prefix
        let mut color = self.line_prefix.style.color();
        rendered.push_str(self.line_prefix.style.color());

        // Render prefix
        rendered.push_str(&self.line_prefix.token);

        for token in row {
            if token.style.is_inverse() && !is_inverse {
                rendered.push_str(INVERSE_VIDEO);
            }
            if is_inverse && !token.style.is_inverse() {
                rendered.push_str(NOT_INVERSE_VIDEO);
            }
            is_inverse = token.style.is_inverse();

            if token.style.color() != color {
                rendered.push_str(token.style.color());
                color = token.style.color();
            }

            rendered.push_str(&token.token);
        }

        rendered.push_str(NORMAL);

        return rendered;
    }

    /// Render all the tokens into a (most of the time multiline) string
    #[must_use]
    pub fn render(&mut self) -> String {
        assert!(!self.rendered);
        let mut current_row: Vec<StyledToken> = Vec::new();
        let mut rendered = String::new();

        let mut tokens = std::mem::take(&mut self.tokens);

        bridge_consecutive_highlighted_tokens(&mut tokens);

        // Otherwise one changed line plus twelve added will stand out too much
        censor_multi_line_highlights(&mut tokens);

        for token in tokens {
            if token.token == "\n" {
                let rendered_row = &self.render_row(&mut current_row);
                rendered.push_str(rendered_row);
                rendered.push('\n');
                current_row.clear();
                continue;
            }

            current_row.push(token);
        }

        if !current_row.is_empty() {
            let rendered_row = &self.render_row(&mut current_row);
            rendered.push_str(rendered_row);
        }

        self.rendered = true;
        return rendered;
    }
}

/// Unhighlight highlights that span multiple lines, or that span one whole
/// line.
fn censor_multi_line_highlights(rows: &mut [StyledToken]) {
    if rows.is_empty() {
        return;
    }

    let mut last_was_highlighted = false;
    let mut last_was_newline = true;

    let mut first_highlighted_index: usize = 0;
    let mut seen_switch_from_newline = false;
    let mut seen_switch_to_newline = false;

    for index in 0..rows.len() {
        if index > 0 {
            last_was_newline = rows[index - 1].token == "\n";

            // Newlines always count as highlighted
            last_was_highlighted = rows[index - 1].style.is_inverse() || last_was_newline;
        }
        let token = &rows[index];
        let is_newline = token.token == "\n";

        // Newlines always count as highlighted
        let is_highlighted = is_newline || token.style.is_inverse();

        if is_highlighted {
            if !last_was_highlighted {
                // Start of new section
                first_highlighted_index = index;
                seen_switch_from_newline = false;
                seen_switch_to_newline = false;
            }
            if last_was_newline && !is_newline {
                seen_switch_from_newline = true;
            }
            if is_newline && !last_was_newline {
                seen_switch_to_newline = true;
            }
            continue;
        }

        assert!(!is_highlighted);
        if !last_was_highlighted {
            // We're in a run of non-highlighted tokens, do nothing
            continue;
        }

        // Found the end
        assert!(last_was_highlighted && !is_highlighted);

        if !(seen_switch_from_newline && seen_switch_to_newline) {
            // No further action needed
            continue;
        }

        // Found end of highlighted section containing newlines or spanning a
        // whole row, unhighlight!
        #[allow(clippy::needless_range_loop)]
        for unhighlight_index in first_highlighted_index..index {
            rows[unhighlight_index].style = rows[unhighlight_index].style.not_inverted();
        }
    }

    let lastindex = rows.len() - 1;
    let last_was_newline = rows[lastindex].token == "\n";

    // Newlines always count as highlighted
    last_was_highlighted = rows[lastindex].style.is_inverse() || last_was_newline;
    if !last_was_highlighted {
        // No ending highlight, nothing more to do
        return;
    }

    // We went past the end while being highlighted
    seen_switch_to_newline = true;

    if seen_switch_from_newline && seen_switch_to_newline {
        #[allow(clippy::needless_range_loop)]
        for unhighlight_index in first_highlighted_index..rows.len() {
            rows[unhighlight_index].style = rows[unhighlight_index].style.not_inverted();
        }
    }
}

fn highlight_trailing_whitespace(row: &mut [StyledToken]) {
    for token in row.iter_mut().rev() {
        if !token.is_whitespace() {
            return;
        }

        token.style = Style::Error;
    }
}

fn highlight_nonleading_tab(row: &mut [StyledToken]) {
    let mut token_iter = row.iter_mut();

    // Skip leading TABs
    loop {
        let next = token_iter.next();
        if next.is_none() {
            // Done!
            return;
        }

        let token = next.unwrap();
        if token.token != "\t" {
            // Not a TAB, this means we're out of skipping the leading TABs
            break;
        }
    }

    // Scan the rest of the line for non-leading TABs
    for token in token_iter {
        if token.token != "\t" {
            // Not a TAB, never mind
            continue;
        }

        // Non-leading TAB, mark it!
        token.style = Style::Error;
    }
}

/// Highlight single space between two highlighted tokens
fn bridge_consecutive_highlighted_tokens(row: &mut [StyledToken]) {
    enum FoundState {
        Nothing,
        HighlightedWord,
        WordSpace,
    }

    let mut found_state = FoundState::Nothing;
    let mut previous_token: Option<&mut StyledToken> = None;
    for token in row.iter_mut() {
        match found_state {
            FoundState::Nothing => {
                if token.style.is_inverse() {
                    // Found "Monkey"
                    found_state = FoundState::HighlightedWord;
                }
            }

            FoundState::HighlightedWord => {
                if token.token.len() == 1 {
                    // Found "Monkey " (note trailing space)
                    found_state = FoundState::WordSpace;
                } else if token.style.is_inverse() {
                    found_state = FoundState::HighlightedWord;
                } else {
                    found_state = FoundState::Nothing;
                }
            }

            FoundState::WordSpace => {
                if token.style.is_inverse() {
                    // Found "Monkey Dance"
                    if let Some(_previous_token) = previous_token {
                        _previous_token.style = _previous_token.style.inverted();
                    }

                    found_state = FoundState::HighlightedWord;
                } else {
                    found_state = FoundState::Nothing;
                }
            }
        }

        previous_token = Some(token);
    }
}

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

    #[cfg(test)]
    use pretty_assertions::assert_eq;

    #[test]
    fn test_basic() {
        let mut test_me = TokenCollector::create(StyledToken {
            token: "+".to_string(),
            style: Style::New,
        });

        test_me.push(StyledToken {
            token: "hej".to_string(),
            style: Style::New,
        });

        test_me.push(StyledToken {
            token: "\n".to_string(),
            style: Style::New,
        });

        let rendered = test_me.render();
        assert_eq!(rendered, format!("{}+hej{}\n", NEW, NORMAL));
    }

    #[test]
    fn test_add_trailing_whitespace() {
        // Just a whitespace
        let mut row = [StyledToken::new(" ".to_string(), Style::New)];
        highlight_trailing_whitespace(&mut row);
        assert_eq!(row, [StyledToken::new(" ".to_string(), Style::Error)]);

        // Trailing whitespace
        let mut row = [
            StyledToken::new("x".to_string(), Style::New),
            StyledToken::new(" ".to_string(), Style::New),
        ];
        highlight_trailing_whitespace(&mut row);
        assert_eq!(
            row,
            [
                StyledToken::new("x".to_string(), Style::New),
                StyledToken::new(" ".to_string(), Style::Error),
            ]
        );

        // Leading whitespace
        let mut row = [
            StyledToken::new(" ".to_string(), Style::New),
            StyledToken::new("x".to_string(), Style::New),
        ];
        highlight_trailing_whitespace(&mut row);
        assert_eq!(
            row,
            [
                StyledToken::new(" ".to_string(), Style::New),
                StyledToken::new("x".to_string(), Style::New),
            ]
        );
    }

    #[test]
    fn test_removed_trailing_whitespace() {
        // It shouldn't be highlighted, just added ones should
        let mut test_me = TokenCollector::create(StyledToken::new("-".to_string(), Style::Old));
        test_me.push(StyledToken::new(" ".to_string(), Style::Old));
        let actual = test_me.render();

        assert_eq!(actual, format!("{}- {}", OLD, NORMAL));
    }

    #[test]
    fn test_add_nonleading_tab() {
        // Trailing TAB
        let mut row = [
            StyledToken::new("x".to_string(), Style::New),
            StyledToken::new("\t".to_string(), Style::New),
        ];
        highlight_nonleading_tab(&mut row);
        assert_eq!(
            row,
            [
                StyledToken::new("x".to_string(), Style::New),
                StyledToken::new("\t".to_string(), Style::Error),
            ]
        );

        // Middle TAB
        let mut row = [
            StyledToken::new("x".to_string(), Style::New),
            StyledToken::new("\t".to_string(), Style::New),
            StyledToken::new("y".to_string(), Style::New),
        ];
        highlight_nonleading_tab(&mut row);
        assert_eq!(
            row,
            [
                StyledToken::new("x".to_string(), Style::New),
                StyledToken::new("\t".to_string(), Style::Error),
                StyledToken::new("y".to_string(), Style::New),
            ]
        );

        // Leading TAB (don't highlight)
        let mut row = [
            StyledToken::new("\t".to_string(), Style::New),
            StyledToken::new("x".to_string(), Style::New),
        ];
        highlight_nonleading_tab(&mut row);
        assert_eq!(
            row,
            [
                StyledToken::new("\t".to_string(), Style::New),
                StyledToken::new("x".to_string(), Style::New),
            ]
        );

        // Single TAB (don't highlight because it is leading)
        let mut row = [StyledToken::new("\t".to_string(), Style::New)];
        highlight_nonleading_tab(&mut row);
        assert_eq!(row, [StyledToken::new("\t".to_string(), Style::New),]);
    }

    #[test]
    fn test_removed_nonleading_tab() {
        // It shouldn't be highlighted, just added ones should
        let mut test_me = TokenCollector::create(StyledToken::new("-".to_string(), Style::Old));
        test_me.push(StyledToken::new("x".to_string(), Style::Old));
        test_me.push(StyledToken::new("\t".to_string(), Style::Old));
        let actual = test_me.render();

        assert_eq!(actual, format!("{}-x\t{}", OLD, NORMAL));
    }

    #[test]
    fn test_highlight_space_between_words() {
        let mut row = [
            StyledToken::new("Monkey".to_string(), Style::NewInverse),
            StyledToken::new(" ".to_string(), Style::New),
            StyledToken::new("Dance".to_string(), Style::NewInverse),
        ];

        bridge_consecutive_highlighted_tokens(&mut row);

        assert_eq!(
            row,
            [
                StyledToken::new("Monkey".to_string(), Style::NewInverse),
                StyledToken::new(" ".to_string(), Style::NewInverse),
                StyledToken::new("Dance".to_string(), Style::NewInverse),
            ]
        );
    }

    #[test]
    fn test_highlight_space_between_random_chars() {
        let mut row = [
            StyledToken::new(">".to_string(), Style::NewInverse),
            StyledToken::new(" ".to_string(), Style::New),
            StyledToken::new("5".to_string(), Style::NewInverse),
        ];

        bridge_consecutive_highlighted_tokens(&mut row);

        assert_eq!(
            row,
            [
                StyledToken::new(">".to_string(), Style::NewInverse),
                StyledToken::new(" ".to_string(), Style::NewInverse),
                StyledToken::new("5".to_string(), Style::NewInverse),
            ]
        );
    }

    /// "_" is an unhighlighted token, "." is a highlighted token, "n" is an
    /// unhighlighted newline and "N" is a highlighted newline.
    fn test_censor_multiline_highlighting(input: &str, expected_output: &str) {
        let mut row = Vec::new();
        for c in input.chars() {
            let mut token = "x".to_string();
            if c == 'n' || c == 'N' {
                token = '\n'.to_string();
            }

            let mut style = Style::New;
            if c == '.' || c == 'N' {
                style = Style::NewInverse;
            }

            row.push(StyledToken { token, style });
        }

        censor_multi_line_highlights(&mut row);

        let mut actual_output = String::new();
        for token in row {
            if token.token != "\n" && !token.style.is_inverse() {
                actual_output.push('_');
            } else if token.token != "\n" && token.style.is_inverse() {
                actual_output.push('.');
            } else if token.token == "\n" && !token.style.is_inverse() {
                actual_output.push('n');
            } else if token.token == "\n" && token.style.is_inverse() {
                actual_output.push('N');
            } else {
                panic!("How did we get here?");
            }
        }

        assert_eq!(actual_output, expected_output);
    }

    #[test]
    fn test_censor_multiline_highlights_start_at_beginning() {
        test_censor_multiline_highlighting("...n___", "___n___");
    }

    #[test]
    fn test_censor_multiline_highlights_at_end() {
        test_censor_multiline_highlighting("___n...", "___n___");
    }

    #[test]
    fn test_censor_multiline_highlights_midline() {
        test_censor_multiline_highlighting("___n...n___", "___n___n___");
    }

    #[test]
    fn test_censor_multiline_highlights_spans_multiline() {
        test_censor_multiline_highlighting("__.n...n.__", "___n___n___");
    }

    #[test]
    fn test_censor_multiline_highlights_trailing_newlines() {
        test_censor_multiline_highlighting("_.nnn", "_.nnn");
    }

    #[test]
    fn test_censor_multiline_highlights_edgecases() {
        test_censor_multiline_highlighting("n.n", "n_n");
        test_censor_multiline_highlighting("...", "___");
        test_censor_multiline_highlighting(".n.", "_n_");
        test_censor_multiline_highlighting("_.n.n._", "__n_n__");
    }

    #[test]
    fn test_censor_multiline_highlights_dont_censor() {
        // Highlighted line parts should not be censored
        test_censor_multiline_highlighting("_.._", "_.._");
        test_censor_multiline_highlighting(".__.", ".__.");
        test_censor_multiline_highlighting(".__.n", ".__.n");
        test_censor_multiline_highlighting("n.__.", "n.__.");
        test_censor_multiline_highlighting("n.__.n", "n.__.n");
    }
}
<commit_msg>Drop existing highlight censoring code<commit_after>use crate::constants::*;

#[derive(Clone, Debug, PartialEq)]
pub enum Style {
    Old,
    OldInverse,
    New,
    NewInverse,
    Error,
}

#[derive(Clone, Debug, PartialEq)]
pub struct StyledToken {
    token: String,
    style: Style,
}

impl StyledToken {
    pub fn new(token: String, style: Style) -> StyledToken {
        return StyledToken { token, style };
    }

    pub fn is_whitespace(&self) -> bool {
        let mut chars_iterator = self.token.chars();
        let first_char = chars_iterator.next().unwrap();
        if chars_iterator.next().is_some() {
            // Multiple chars found in this token, but whitespace will only be
            // one per token.
            return false;
        }

        return first_char.is_whitespace();
    }
}

pub struct TokenCollector {
    line_prefix: StyledToken,
    tokens: Vec<StyledToken>,
    rendered: bool,
}

impl Style {
    #[must_use]
    pub fn is_inverse(&self) -> bool {
        match self {
            Style::OldInverse | Style::NewInverse | Style::Error => {
                return true;
            }
            _ => {
                return false;
            }
        }
    }

    pub fn inverted(&self) -> Style {
        return match self {
            Style::Old => Style::OldInverse,
            Style::New => Style::NewInverse,
            Style::OldInverse => Style::OldInverse,
            Style::NewInverse => Style::NewInverse,
            Style::Error => Style::Error,
        };
    }

    pub fn not_inverted(&self) -> Style {
        return match self {
            Style::Old => Style::Old,
            Style::New => Style::New,
            Style::OldInverse => Style::Old,
            Style::NewInverse => Style::New,
            Style::Error => Style::Error,
        };
    }

    #[must_use]
    pub fn color<'a>(&self) -> &'a str {
        match self {
            Style::Old => {
                return OLD;
            }
            Style::OldInverse => {
                return OLD;
            }
            Style::New => {
                return NEW;
            }
            Style::NewInverse => {
                return NEW;
            }
            Style::Error => {
                return ERROR;
            }
        }
    }
}

impl TokenCollector {
    #[must_use]
    pub fn create(line_prefix: StyledToken) -> Self {
        return TokenCollector {
            line_prefix,
            tokens: Vec::new(),
            rendered: false,
        };
    }

    pub fn push(&mut self, token: StyledToken) {
        self.tokens.push(token);
    }

    #[must_use]
    fn render_row(&self, row: &mut [StyledToken]) -> String {
        let mut rendered = String::new();

        if self.line_prefix.style == Style::New {
            highlight_trailing_whitespace(row);
            highlight_nonleading_tab(row);
        }

        // Set inverse from prefix
        let mut is_inverse = self.line_prefix.style.is_inverse();
        if is_inverse {
            rendered.push_str(INVERSE_VIDEO);
        }

        // Set line color from prefix
        let mut color = self.line_prefix.style.color();
        rendered.push_str(self.line_prefix.style.color());

        // Render prefix
        rendered.push_str(&self.line_prefix.token);

        for token in row {
            if token.style.is_inverse() && !is_inverse {
                rendered.push_str(INVERSE_VIDEO);
            }
            if is_inverse && !token.style.is_inverse() {
                rendered.push_str(NOT_INVERSE_VIDEO);
            }
            is_inverse = token.style.is_inverse();

            if token.style.color() != color {
                rendered.push_str(token.style.color());
                color = token.style.color();
            }

            rendered.push_str(&token.token);
        }

        rendered.push_str(NORMAL);

        return rendered;
    }

    /// Render all the tokens into a (most of the time multiline) string
    #[must_use]
    pub fn render(&mut self) -> String {
        assert!(!self.rendered);
        let mut current_row: Vec<StyledToken> = Vec::new();
        let mut rendered = String::new();

        let mut tokens = std::mem::take(&mut self.tokens);

        bridge_consecutive_highlighted_tokens(&mut tokens);

        for token in tokens {
            if token.token == "\n" {
                let rendered_row = &self.render_row(&mut current_row);
                rendered.push_str(rendered_row);
                rendered.push('\n');
                current_row.clear();
                continue;
            }

            current_row.push(token);
        }

        if !current_row.is_empty() {
            let rendered_row = &self.render_row(&mut current_row);
            rendered.push_str(rendered_row);
        }

        self.rendered = true;
        return rendered;
    }
}

fn highlight_trailing_whitespace(row: &mut [StyledToken]) {
    for token in row.iter_mut().rev() {
        if !token.is_whitespace() {
            return;
        }

        token.style = Style::Error;
    }
}

fn highlight_nonleading_tab(row: &mut [StyledToken]) {
    let mut token_iter = row.iter_mut();

    // Skip leading TABs
    loop {
        let next = token_iter.next();
        if next.is_none() {
            // Done!
            return;
        }

        let token = next.unwrap();
        if token.token != "\t" {
            // Not a TAB, this means we're out of skipping the leading TABs
            break;
        }
    }

    // Scan the rest of the line for non-leading TABs
    for token in token_iter {
        if token.token != "\t" {
            // Not a TAB, never mind
            continue;
        }

        // Non-leading TAB, mark it!
        token.style = Style::Error;
    }
}

/// Highlight single space between two highlighted tokens
fn bridge_consecutive_highlighted_tokens(row: &mut [StyledToken]) {
    enum FoundState {
        Nothing,
        HighlightedWord,
        WordSpace,
    }

    let mut found_state = FoundState::Nothing;
    let mut previous_token: Option<&mut StyledToken> = None;
    for token in row.iter_mut() {
        match found_state {
            FoundState::Nothing => {
                if token.style.is_inverse() {
                    // Found "Monkey"
                    found_state = FoundState::HighlightedWord;
                }
            }

            FoundState::HighlightedWord => {
                if token.token.len() == 1 {
                    // Found "Monkey " (note trailing space)
                    found_state = FoundState::WordSpace;
                } else if token.style.is_inverse() {
                    found_state = FoundState::HighlightedWord;
                } else {
                    found_state = FoundState::Nothing;
                }
            }

            FoundState::WordSpace => {
                if token.style.is_inverse() {
                    // Found "Monkey Dance"
                    if let Some(_previous_token) = previous_token {
                        _previous_token.style = _previous_token.style.inverted();
                    }

                    found_state = FoundState::HighlightedWord;
                } else {
                    found_state = FoundState::Nothing;
                }
            }
        }

        previous_token = Some(token);
    }
}

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

    #[cfg(test)]
    use pretty_assertions::assert_eq;

    #[test]
    fn test_basic() {
        let mut test_me = TokenCollector::create(StyledToken {
            token: "+".to_string(),
            style: Style::New,
        });

        test_me.push(StyledToken {
            token: "hej".to_string(),
            style: Style::New,
        });

        test_me.push(StyledToken {
            token: "\n".to_string(),
            style: Style::New,
        });

        let rendered = test_me.render();
        assert_eq!(rendered, format!("{}+hej{}\n", NEW, NORMAL));
    }

    #[test]
    fn test_add_trailing_whitespace() {
        // Just a whitespace
        let mut row = [StyledToken::new(" ".to_string(), Style::New)];
        highlight_trailing_whitespace(&mut row);
        assert_eq!(row, [StyledToken::new(" ".to_string(), Style::Error)]);

        // Trailing whitespace
        let mut row = [
            StyledToken::new("x".to_string(), Style::New),
            StyledToken::new(" ".to_string(), Style::New),
        ];
        highlight_trailing_whitespace(&mut row);
        assert_eq!(
            row,
            [
                StyledToken::new("x".to_string(), Style::New),
                StyledToken::new(" ".to_string(), Style::Error),
            ]
        );

        // Leading whitespace
        let mut row = [
            StyledToken::new(" ".to_string(), Style::New),
            StyledToken::new("x".to_string(), Style::New),
        ];
        highlight_trailing_whitespace(&mut row);
        assert_eq!(
            row,
            [
                StyledToken::new(" ".to_string(), Style::New),
                StyledToken::new("x".to_string(), Style::New),
            ]
        );
    }

    #[test]
    fn test_removed_trailing_whitespace() {
        // It shouldn't be highlighted, just added ones should
        let mut test_me = TokenCollector::create(StyledToken::new("-".to_string(), Style::Old));
        test_me.push(StyledToken::new(" ".to_string(), Style::Old));
        let actual = test_me.render();

        assert_eq!(actual, format!("{}- {}", OLD, NORMAL));
    }

    #[test]
    fn test_add_nonleading_tab() {
        // Trailing TAB
        let mut row = [
            StyledToken::new("x".to_string(), Style::New),
            StyledToken::new("\t".to_string(), Style::New),
        ];
        highlight_nonleading_tab(&mut row);
        assert_eq!(
            row,
            [
                StyledToken::new("x".to_string(), Style::New),
                StyledToken::new("\t".to_string(), Style::Error),
            ]
        );

        // Middle TAB
        let mut row = [
            StyledToken::new("x".to_string(), Style::New),
            StyledToken::new("\t".to_string(), Style::New),
            StyledToken::new("y".to_string(), Style::New),
        ];
        highlight_nonleading_tab(&mut row);
        assert_eq!(
            row,
            [
                StyledToken::new("x".to_string(), Style::New),
                StyledToken::new("\t".to_string(), Style::Error),
                StyledToken::new("y".to_string(), Style::New),
            ]
        );

        // Leading TAB (don't highlight)
        let mut row = [
            StyledToken::new("\t".to_string(), Style::New),
            StyledToken::new("x".to_string(), Style::New),
        ];
        highlight_nonleading_tab(&mut row);
        assert_eq!(
            row,
            [
                StyledToken::new("\t".to_string(), Style::New),
                StyledToken::new("x".to_string(), Style::New),
            ]
        );

        // Single TAB (don't highlight because it is leading)
        let mut row = [StyledToken::new("\t".to_string(), Style::New)];
        highlight_nonleading_tab(&mut row);
        assert_eq!(row, [StyledToken::new("\t".to_string(), Style::New),]);
    }

    #[test]
    fn test_removed_nonleading_tab() {
        // It shouldn't be highlighted, just added ones should
        let mut test_me = TokenCollector::create(StyledToken::new("-".to_string(), Style::Old));
        test_me.push(StyledToken::new("x".to_string(), Style::Old));
        test_me.push(StyledToken::new("\t".to_string(), Style::Old));
        let actual = test_me.render();

        assert_eq!(actual, format!("{}-x\t{}", OLD, NORMAL));
    }

    #[test]
    fn test_highlight_space_between_words() {
        let mut row = [
            StyledToken::new("Monkey".to_string(), Style::NewInverse),
            StyledToken::new(" ".to_string(), Style::New),
            StyledToken::new("Dance".to_string(), Style::NewInverse),
        ];

        bridge_consecutive_highlighted_tokens(&mut row);

        assert_eq!(
            row,
            [
                StyledToken::new("Monkey".to_string(), Style::NewInverse),
                StyledToken::new(" ".to_string(), Style::NewInverse),
                StyledToken::new("Dance".to_string(), Style::NewInverse),
            ]
        );
    }

    #[test]
    fn test_highlight_space_between_random_chars() {
        let mut row = [
            StyledToken::new(">".to_string(), Style::NewInverse),
            StyledToken::new(" ".to_string(), Style::New),
            StyledToken::new("5".to_string(), Style::NewInverse),
        ];

        bridge_consecutive_highlighted_tokens(&mut row);

        assert_eq!(
            row,
            [
                StyledToken::new(">".to_string(), Style::NewInverse),
                StyledToken::new(" ".to_string(), Style::NewInverse),
                StyledToken::new("5".to_string(), Style::NewInverse),
            ]
        );
    }
}
<|endoftext|>


<commit_before>use Printer;
use With;
use XY;
use align::*;
use direction::Direction;
use event::*;
use owning_ref::{ArcRef, OwningHandle};
use std::ops::Deref;
use std::sync::{Mutex, MutexGuard};
use std::sync::Arc;
use unicode_width::UnicodeWidthStr;
use utils::{LinesIterator, Row};
use vec::Vec2;
use view::{ScrollBase, ScrollStrategy, SizeCache, View};

/// Provides access to the content of a [`TextView`].
///
/// Cloning this object will still point to the same content.
///
/// [`TextView`]: struct.TextView.html
///
/// # Examples
///
/// ```rust
/// # use cursive::views::{TextView, TextContent};
/// let mut content = TextContent::new("content");
/// let view = TextView::new_with_content(content.clone());
///
/// // Later, possibly in a different thread
/// content.set_content("new content");
/// assert!(content.get_content().contains("new"));
/// ```
#[derive(Clone)]
pub struct TextContent {
    content: Arc<Mutex<TextContentInner>>,
}

impl TextContent {
    /// Creates a new text content around the given value.
    pub fn new<S: Into<String>>(content: S) -> Self {
        TextContent {
            content: Arc::new(Mutex::new(TextContentInner {
                content: content.into(),
                size_cache: None,
            })),
        }
    }
}

/// A reference to the text content.
///
/// It implements `Deref<Target=str>`.
///
/// This keeps the content locked. Do not store this!
pub struct TextContentRef {
    handle: OwningHandle<
        ArcRef<Mutex<TextContentInner>>,
        MutexGuard<'static, TextContentInner>,
    >,
}

impl Deref for TextContentRef {
    type Target = str;

    fn deref(&self) -> &str {
        &self.handle.content
    }
}

impl TextContent {
    /// Replaces the content with the given value.
    pub fn set_content<S: Into<String>>(&mut self, content: S) {
        self.with_content(|c| *c = content.into());
    }

    /// Append `content` to the end of a `TextView`.
    pub fn append_content(&mut self, content: &str) {
        self.with_content(|c| c.push_str(content));
    }

    /// Returns a reference to the content.
    ///
    /// This locks the data while the returned value is alive,
    /// so don't keep it too long.
    pub fn get_content(&self) -> TextContentRef {
        TextContentInner::get_content(&self.content)
    }

    fn with_content<F>(&mut self, f: F)
    where
        F: FnOnce(&mut String),
    {
        let mut lock = self.content.lock().unwrap();

        f(&mut lock.content);

        lock.size_cache = None;
    }
}

struct TextContentInner {
    content: String,
    size_cache: Option<XY<SizeCache>>,
}

impl TextContentInner {
    fn get_content(content: &Arc<Mutex<TextContentInner>>) -> TextContentRef {
        let arc_ref: ArcRef<Mutex<TextContentInner>> =
            ArcRef::new(Arc::clone(content));

        TextContentRef {
            handle: OwningHandle::new_with_fn(arc_ref, |mutex| unsafe {
                (*mutex).lock().unwrap()
            }),
        }
    }
}

/// A simple view showing a fixed text.
///
/// # Examples
///
/// ```rust,no_run
/// # use cursive::Cursive;
/// # use cursive::views::TextView;
/// let mut siv = Cursive::new();
///
/// siv.add_layer(TextView::new("Hello world!"));
/// ```
pub struct TextView {
    // content: String,
    content: Arc<Mutex<TextContentInner>>,
    rows: Vec<Row>,

    align: Align,

    // If `false`, disable scrolling.
    scrollable: bool,

    // ScrollBase make many scrolling-related things easier
    scrollbase: ScrollBase,
    scroll_strategy: ScrollStrategy,
    last_size: Vec2,
    width: Option<usize>,
}

// If the last character is a newline, strip it.
fn strip_last_newline(content: &str) -> &str {
    if content.ends_with('\n') {
        &content[..content.len() - 1]
    } else {
        content
    }
}

impl TextView {
    /// Creates a new TextView with the given content.
    pub fn new<S: Into<String>>(content: S) -> Self {
        TextView::new_with_content(TextContent::new(content))
    }

    /// Creates a new TextView using the given `Arc<Mutex<String>>`.
    ///
    /// If you kept a clone of the given content, you'll be able to update it
    /// remotely.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use cursive::views::{TextView, TextContent};
    /// let mut content = TextContent::new("content");
    /// let view = TextView::new_with_content(content.clone());
    ///
    /// // Later, possibly in a different thread
    /// content.set_content("new content");
    /// assert!(content.get_content().contains("new"));
    /// ```
    pub fn new_with_content(content: TextContent) -> Self {
        TextView {
            content: content.content,
            rows: Vec::new(),
            scrollable: true,
            scrollbase: ScrollBase::new(),
            scroll_strategy: ScrollStrategy::KeepRow,
            align: Align::top_left(),
            last_size: Vec2::zero(),
            width: None,
        }
    }

    /// Creates a new empty `TextView`.
    pub fn empty() -> Self {
        TextView::new("")
    }

    /// Enable or disable the view's scrolling capabilities.
    ///
    /// When disabled, the view will never attempt to scroll
    /// (and will always ask for the full height).
    pub fn set_scrollable(&mut self, scrollable: bool) {
        self.scrollable = scrollable;
    }

    /// Enable or disable the view's scrolling capabilities.
    ///
    /// When disabled, the view will never attempt to scroll
    /// (and will always ask for the full height).
    ///
    /// Chainable variant.
    pub fn scrollable(self, scrollable: bool) -> Self {
        self.with(|s| s.set_scrollable(scrollable))
    }

    /// Sets the horizontal alignment for this view.
    pub fn h_align(mut self, h: HAlign) -> Self {
        self.align.h = h;

        self
    }

    /// Sets the vertical alignment for this view.
    pub fn v_align(mut self, v: VAlign) -> Self {
        self.align.v = v;

        self
    }

    /// Sets the alignment for this view.
    pub fn align(mut self, a: Align) -> Self {
        self.align = a;

        self
    }

    /// Center the text horizontally and vertically inside the view.
    pub fn center(mut self) -> Self {
        self.align = Align::center();
        self
    }

    /// Replace the text in this view.
    ///
    /// Chainable variant.
    pub fn content<S: Into<String>>(self, content: S) -> Self {
        self.with(|s| s.set_content(content))
    }

    /// Replace the text in this view.
    pub fn set_content<S: Into<String>>(&mut self, content: S) {
        let content = content.into();
        self.content.lock().unwrap().content = content;
        self.invalidate();
    }

    /// Append `content` to the end of a `TextView`.
    pub fn append_content(&mut self, content: &str) {
        self.content.lock().unwrap().content.push_str(content);
        self.invalidate();
    }

    /// Returns the current text in this view.
    pub fn get_content(&self) -> TextContentRef {
        TextContentInner::get_content(&self.content)
    }

    /// Returns a shared reference to the content, allowing content mutation.
    pub fn get_shared_content(&mut self) -> TextContent {
        // We take &mut here without really needing it,
        // because it sort of "makes sense".

        TextContent {
            content: Arc::clone(&self.content),
        }
    }

    fn is_cache_valid(&self, size: Vec2) -> bool {
        let content = self.content.lock().unwrap();

        match content.size_cache {
            None => false,
            Some(ref last) => last.x.accept(size.x) && last.y.accept(size.y),
        }
    }

    /// Defines the way scrolling is adjusted on content or size change.
    ///
    /// The scroll strategy defines how the scrolling position is adjusted
    /// when the size of the view or the content change.
    ///
    /// It is reset to `ScrollStrategy::KeepRow` whenever the user scrolls
    /// manually.
    pub fn set_scroll_strategy(&mut self, strategy: ScrollStrategy) {
        self.scroll_strategy = strategy;
        self.adjust_scroll();
    }

    /// Defines the way scrolling is adjusted on content or size change.
    ///
    /// Chainable variant.
    pub fn scroll_strategy(self, strategy: ScrollStrategy) -> Self {
        self.with(|s| s.set_scroll_strategy(strategy))
    }

    // Apply the scrolling strategy to the current scroll position.
    //
    // Called when computing rows and when applying a new strategy.
    fn adjust_scroll(&mut self) {
        match self.scroll_strategy {
            ScrollStrategy::StickToTop => self.scrollbase.scroll_top(),
            ScrollStrategy::StickToBottom => self.scrollbase.scroll_bottom(),
            ScrollStrategy::KeepRow => (),
        };
    }

    // This must be non-destructive, as it may be called
    // multiple times during layout.
    fn compute_rows(&mut self, size: Vec2) {
        let mut content = self.content.lock().unwrap();
        if self.is_cache_valid(size) {
            return;
        }

        // Completely bust the cache
        // Just in case we fail, we don't want to leave a bad cache.
        content.size_cache = None;

        if size.x == 0 {
            // Nothing we can do at this point.
            return;
        }

        // First attempt: naively hope that we won't need a scrollbar_width
        // (This means we try to use the entire available width for text).
        self.rows =
            LinesIterator::new(strip_last_newline(&content.content), size.x)
                .collect();

        // Width taken by the scrollbar. Without a scrollbar, it's 0.
        let mut scrollbar_width = 0;

        if self.scrollable && self.rows.len() > size.y {
            // We take 1 column for the bar itself + 1 spacing column
            scrollbar_width = 2;

            // If we're too high, include a scrollbar_width
            let available = match size.x.checked_sub(scrollbar_width) {
                Some(s) => s,
                None => return,
            };

            self.rows =
                LinesIterator::new(&content.content, available).collect();

            if self.rows.is_empty() && !content.content.is_empty() {
                // We have some content, we we didn't find any row for it?
                // This probably means we couldn't even make a single row
                // (for instance we only have 1 column and we have a wide
                // character).
                return;
            }
        }

        // Desired width, including the scrollbar_width.
        self.width = self.rows
            .iter()
            .map(|row| row.width)
            .max()
            .map(|w| w + scrollbar_width);

        // The entire "virtual" size (includes all rows)
        let mut my_size = Vec2::new(self.width.unwrap_or(0), self.rows.len());

        // If we're scrolling, cap the the available size.
        if self.scrollable && my_size.y > size.y {
            my_size.y = size.y;
        }

        // Build a fresh cache.
        content.size_cache = Some(SizeCache::build(my_size, size));
    }

    // Invalidates the cache, so next call will recompute everything.
    fn invalidate(&mut self) {
        let mut content = self.content.lock().unwrap();
        content.size_cache = None;
    }
}

impl View for TextView {
    fn draw(&self, printer: &Printer) {
        let h = self.rows.len();
        // If the content is smaller than the view, align it somewhere.
        let offset = self.align.v.get_offset(h, printer.size.y);
        let printer = &printer.offset((0, offset), true);

        let content = self.content.lock().unwrap();

        self.scrollbase.draw(printer, |printer, i| {
            let row = &self.rows[i];
            let text = &content.content[row.start..row.end];
            let l = text.width();
            let x = self.align.h.get_offset(l, printer.size.x);
            printer.print((x, 0), text);
        });
    }

    fn on_event(&mut self, event: Event) -> EventResult {
        if !self.scrollbase.scrollable() {
            return EventResult::Ignored;
        }

        // We have a scrollbar, otherwise the event would just be ignored.
        match event {
            Event::Key(Key::Home) => self.scrollbase.scroll_top(),
            Event::Key(Key::End) => self.scrollbase.scroll_bottom(),
            Event::Key(Key::Up) if self.scrollbase.can_scroll_up() => {
                self.scrollbase.scroll_up(1)
            }
            Event::Key(Key::Down) if self.scrollbase.can_scroll_down() => {
                self.scrollbase.scroll_down(1)
            }
            Event::Mouse {
                event: MouseEvent::WheelDown,
                ..
            } if self.scrollbase.can_scroll_down() =>
            {
                self.scrollbase.scroll_down(5);
            }
            Event::Mouse {
                event: MouseEvent::WheelUp,
                ..
            } if self.scrollbase.can_scroll_up() =>
            {
                self.scrollbase.scroll_up(5);
            }
            Event::Mouse {
                event: MouseEvent::Press(MouseButton::Left),
                position,
                offset,
            } if position
                .checked_sub(offset)
                .map(|position| {
                    self.scrollbase.start_drag(position, self.last_size.x)
                })
                .unwrap_or(false) =>
            {
                // Only consume the event if the mouse hits the scrollbar.
                // Start scroll drag at the given position.
            }
            Event::Mouse {
                event: MouseEvent::Hold(MouseButton::Left),
                position,
                offset,
            } => {
                // If the mouse is dragged, we always consume the event.
                let position = position.saturating_sub(offset);
                self.scrollbase.drag(position);
            }
            Event::Mouse {
                event: MouseEvent::Release(MouseButton::Left),
                ..
            } => {
                self.scrollbase.release_grab();
            }
            Event::Key(Key::PageDown) => self.scrollbase.scroll_down(10),
            Event::Key(Key::PageUp) => self.scrollbase.scroll_up(10),
            _ => return EventResult::Ignored,
        }

        // We just scrolled manually, so reset the scroll strategy.
        self.scroll_strategy = ScrollStrategy::KeepRow;
        EventResult::Consumed(None)
    }

    fn needs_relayout(&self) -> bool {
        let content = self.content.lock().unwrap();
        content.size_cache.is_none()
    }

    fn required_size(&mut self, size: Vec2) -> Vec2 {
        self.compute_rows(size);

        // This is what we'd like
        let mut ideal = Vec2::new(self.width.unwrap_or(0), self.rows.len());

        if self.scrollable && ideal.y > size.y {
            ideal.y = size.y;
        }

        ideal
    }

    fn take_focus(&mut self, _: Direction) -> bool {
        self.scrollbase.scrollable()
    }

    fn layout(&mut self, size: Vec2) {
        // Compute the text rows.
        self.last_size = size;
        self.compute_rows(size);
        // Adjust scrolling, in case we're sticking to the bottom for instance.
        self.scrollbase.set_heights(size.y, self.rows.len());
        self.adjust_scroll();
    }
}
<commit_msg>Fix TextView deadlock<commit_after>use Printer;
use With;
use XY;
use align::*;
use direction::Direction;
use event::*;
use owning_ref::{ArcRef, OwningHandle};
use std::ops::Deref;
use std::sync::{Mutex, MutexGuard};
use std::sync::Arc;
use unicode_width::UnicodeWidthStr;
use utils::{LinesIterator, Row};
use vec::Vec2;
use view::{ScrollBase, ScrollStrategy, SizeCache, View};

/// Provides access to the content of a [`TextView`].
///
/// Cloning this object will still point to the same content.
///
/// [`TextView`]: struct.TextView.html
///
/// # Examples
///
/// ```rust
/// # use cursive::views::{TextView, TextContent};
/// let mut content = TextContent::new("content");
/// let view = TextView::new_with_content(content.clone());
///
/// // Later, possibly in a different thread
/// content.set_content("new content");
/// assert!(content.get_content().contains("new"));
/// ```
#[derive(Clone)]
pub struct TextContent {
    content: Arc<Mutex<TextContentInner>>,
}

impl TextContent {
    /// Creates a new text content around the given value.
    pub fn new<S: Into<String>>(content: S) -> Self {
        TextContent {
            content: Arc::new(Mutex::new(TextContentInner {
                content: content.into(),
                size_cache: None,
            })),
        }
    }
}

/// A reference to the text content.
///
/// It implements `Deref<Target=str>`.
///
/// This keeps the content locked. Do not store this!
pub struct TextContentRef {
    handle: OwningHandle<
        ArcRef<Mutex<TextContentInner>>,
        MutexGuard<'static, TextContentInner>,
    >,
}

impl Deref for TextContentRef {
    type Target = str;

    fn deref(&self) -> &str {
        &self.handle.content
    }
}

impl TextContent {
    /// Replaces the content with the given value.
    pub fn set_content<S: Into<String>>(&mut self, content: S) {
        self.with_content(|c| *c = content.into());
    }

    /// Append `content` to the end of a `TextView`.
    pub fn append_content(&mut self, content: &str) {
        self.with_content(|c| c.push_str(content));
    }

    /// Returns a reference to the content.
    ///
    /// This locks the data while the returned value is alive,
    /// so don't keep it too long.
    pub fn get_content(&self) -> TextContentRef {
        TextContentInner::get_content(&self.content)
    }

    fn with_content<F>(&mut self, f: F)
    where
        F: FnOnce(&mut String),
    {
        let mut lock = self.content.lock().unwrap();

        f(&mut lock.content);

        lock.size_cache = None;
    }
}

struct TextContentInner {
    content: String,
    size_cache: Option<XY<SizeCache>>,
}

impl TextContentInner {
    fn get_content(content: &Arc<Mutex<TextContentInner>>) -> TextContentRef {
        let arc_ref: ArcRef<Mutex<TextContentInner>> =
            ArcRef::new(Arc::clone(content));

        TextContentRef {
            handle: OwningHandle::new_with_fn(arc_ref, |mutex| unsafe {
                (*mutex).lock().unwrap()
            }),
        }
    }

    fn is_cache_valid(&self, size: Vec2) -> bool {
        match self.size_cache {
            None => false,
            Some(ref last) => last.x.accept(size.x) && last.y.accept(size.y),
        }
    }
}

/// A simple view showing a fixed text.
///
/// # Examples
///
/// ```rust,no_run
/// # use cursive::Cursive;
/// # use cursive::views::TextView;
/// let mut siv = Cursive::new();
///
/// siv.add_layer(TextView::new("Hello world!"));
/// ```
pub struct TextView {
    // content: String,
    content: Arc<Mutex<TextContentInner>>,
    rows: Vec<Row>,

    align: Align,

    // If `false`, disable scrolling.
    scrollable: bool,

    // ScrollBase make many scrolling-related things easier
    scrollbase: ScrollBase,
    scroll_strategy: ScrollStrategy,
    last_size: Vec2,
    width: Option<usize>,
}

// If the last character is a newline, strip it.
fn strip_last_newline(content: &str) -> &str {
    if content.ends_with('\n') {
        &content[..content.len() - 1]
    } else {
        content
    }
}

impl TextView {
    /// Creates a new TextView with the given content.
    pub fn new<S: Into<String>>(content: S) -> Self {
        TextView::new_with_content(TextContent::new(content))
    }

    /// Creates a new TextView using the given `Arc<Mutex<String>>`.
    ///
    /// If you kept a clone of the given content, you'll be able to update it
    /// remotely.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use cursive::views::{TextView, TextContent};
    /// let mut content = TextContent::new("content");
    /// let view = TextView::new_with_content(content.clone());
    ///
    /// // Later, possibly in a different thread
    /// content.set_content("new content");
    /// assert!(content.get_content().contains("new"));
    /// ```
    pub fn new_with_content(content: TextContent) -> Self {
        TextView {
            content: content.content,
            rows: Vec::new(),
            scrollable: true,
            scrollbase: ScrollBase::new(),
            scroll_strategy: ScrollStrategy::KeepRow,
            align: Align::top_left(),
            last_size: Vec2::zero(),
            width: None,
        }
    }

    /// Creates a new empty `TextView`.
    pub fn empty() -> Self {
        TextView::new("")
    }

    /// Enable or disable the view's scrolling capabilities.
    ///
    /// When disabled, the view will never attempt to scroll
    /// (and will always ask for the full height).
    pub fn set_scrollable(&mut self, scrollable: bool) {
        self.scrollable = scrollable;
    }

    /// Enable or disable the view's scrolling capabilities.
    ///
    /// When disabled, the view will never attempt to scroll
    /// (and will always ask for the full height).
    ///
    /// Chainable variant.
    pub fn scrollable(self, scrollable: bool) -> Self {
        self.with(|s| s.set_scrollable(scrollable))
    }

    /// Sets the horizontal alignment for this view.
    pub fn h_align(mut self, h: HAlign) -> Self {
        self.align.h = h;

        self
    }

    /// Sets the vertical alignment for this view.
    pub fn v_align(mut self, v: VAlign) -> Self {
        self.align.v = v;

        self
    }

    /// Sets the alignment for this view.
    pub fn align(mut self, a: Align) -> Self {
        self.align = a;

        self
    }

    /// Center the text horizontally and vertically inside the view.
    pub fn center(mut self) -> Self {
        self.align = Align::center();
        self
    }

    /// Replace the text in this view.
    ///
    /// Chainable variant.
    pub fn content<S: Into<String>>(self, content: S) -> Self {
        self.with(|s| s.set_content(content))
    }

    /// Replace the text in this view.
    pub fn set_content<S: Into<String>>(&mut self, content: S) {
        let content = content.into();
        self.content.lock().unwrap().content = content;
        self.invalidate();
    }

    /// Append `content` to the end of a `TextView`.
    pub fn append_content(&mut self, content: &str) {
        self.content.lock().unwrap().content.push_str(content);
        self.invalidate();
    }

    /// Returns the current text in this view.
    pub fn get_content(&self) -> TextContentRef {
        TextContentInner::get_content(&self.content)
    }

    /// Returns a shared reference to the content, allowing content mutation.
    pub fn get_shared_content(&mut self) -> TextContent {
        // We take &mut here without really needing it,
        // because it sort of "makes sense".

        TextContent {
            content: Arc::clone(&self.content),
        }
    }

    /// Defines the way scrolling is adjusted on content or size change.
    ///
    /// The scroll strategy defines how the scrolling position is adjusted
    /// when the size of the view or the content change.
    ///
    /// It is reset to `ScrollStrategy::KeepRow` whenever the user scrolls
    /// manually.
    pub fn set_scroll_strategy(&mut self, strategy: ScrollStrategy) {
        self.scroll_strategy = strategy;
        self.adjust_scroll();
    }

    /// Defines the way scrolling is adjusted on content or size change.
    ///
    /// Chainable variant.
    pub fn scroll_strategy(self, strategy: ScrollStrategy) -> Self {
        self.with(|s| s.set_scroll_strategy(strategy))
    }

    // Apply the scrolling strategy to the current scroll position.
    //
    // Called when computing rows and when applying a new strategy.
    fn adjust_scroll(&mut self) {
        match self.scroll_strategy {
            ScrollStrategy::StickToTop => self.scrollbase.scroll_top(),
            ScrollStrategy::StickToBottom => self.scrollbase.scroll_bottom(),
            ScrollStrategy::KeepRow => (),
        };
    }

    // This must be non-destructive, as it may be called
    // multiple times during layout.
    fn compute_rows(&mut self, size: Vec2) {
        let mut content = self.content.lock().unwrap();
        if content.is_cache_valid(size) {
            return;
        }

        // Completely bust the cache
        // Just in case we fail, we don't want to leave a bad cache.
        content.size_cache = None;

        if size.x == 0 {
            // Nothing we can do at this point.
            return;
        }

        // First attempt: naively hope that we won't need a scrollbar_width
        // (This means we try to use the entire available width for text).
        self.rows =
            LinesIterator::new(strip_last_newline(&content.content), size.x)
                .collect();

        // Width taken by the scrollbar. Without a scrollbar, it's 0.
        let mut scrollbar_width = 0;

        if self.scrollable && self.rows.len() > size.y {
            // We take 1 column for the bar itself + 1 spacing column
            scrollbar_width = 2;

            // If we're too high, include a scrollbar_width
            let available = match size.x.checked_sub(scrollbar_width) {
                Some(s) => s,
                None => return,
            };

            self.rows =
                LinesIterator::new(&content.content, available).collect();

            if self.rows.is_empty() && !content.content.is_empty() {
                // We have some content, we we didn't find any row for it?
                // This probably means we couldn't even make a single row
                // (for instance we only have 1 column and we have a wide
                // character).
                return;
            }
        }

        // Desired width, including the scrollbar_width.
        self.width = self.rows
            .iter()
            .map(|row| row.width)
            .max()
            .map(|w| w + scrollbar_width);

        // The entire "virtual" size (includes all rows)
        let mut my_size = Vec2::new(self.width.unwrap_or(0), self.rows.len());

        // If we're scrolling, cap the the available size.
        if self.scrollable && my_size.y > size.y {
            my_size.y = size.y;
        }

        // Build a fresh cache.
        content.size_cache = Some(SizeCache::build(my_size, size));
    }

    // Invalidates the cache, so next call will recompute everything.
    fn invalidate(&mut self) {
        let mut content = self.content.lock().unwrap();
        content.size_cache = None;
    }
}

impl View for TextView {
    fn draw(&self, printer: &Printer) {
        let h = self.rows.len();
        // If the content is smaller than the view, align it somewhere.
        let offset = self.align.v.get_offset(h, printer.size.y);
        let printer = &printer.offset((0, offset), true);

        let content = self.content.lock().unwrap();

        self.scrollbase.draw(printer, |printer, i| {
            let row = &self.rows[i];
            let text = &content.content[row.start..row.end];
            let l = text.width();
            let x = self.align.h.get_offset(l, printer.size.x);
            printer.print((x, 0), text);
        });
    }

    fn on_event(&mut self, event: Event) -> EventResult {
        if !self.scrollbase.scrollable() {
            return EventResult::Ignored;
        }

        // We have a scrollbar, otherwise the event would just be ignored.
        match event {
            Event::Key(Key::Home) => self.scrollbase.scroll_top(),
            Event::Key(Key::End) => self.scrollbase.scroll_bottom(),
            Event::Key(Key::Up) if self.scrollbase.can_scroll_up() => {
                self.scrollbase.scroll_up(1)
            }
            Event::Key(Key::Down) if self.scrollbase.can_scroll_down() => {
                self.scrollbase.scroll_down(1)
            }
            Event::Mouse {
                event: MouseEvent::WheelDown,
                ..
            } if self.scrollbase.can_scroll_down() =>
            {
                self.scrollbase.scroll_down(5);
            }
            Event::Mouse {
                event: MouseEvent::WheelUp,
                ..
            } if self.scrollbase.can_scroll_up() =>
            {
                self.scrollbase.scroll_up(5);
            }
            Event::Mouse {
                event: MouseEvent::Press(MouseButton::Left),
                position,
                offset,
            } if position
                .checked_sub(offset)
                .map(|position| {
                    self.scrollbase.start_drag(position, self.last_size.x)
                })
                .unwrap_or(false) =>
            {
                // Only consume the event if the mouse hits the scrollbar.
                // Start scroll drag at the given position.
            }
            Event::Mouse {
                event: MouseEvent::Hold(MouseButton::Left),
                position,
                offset,
            } => {
                // If the mouse is dragged, we always consume the event.
                let position = position.saturating_sub(offset);
                self.scrollbase.drag(position);
            }
            Event::Mouse {
                event: MouseEvent::Release(MouseButton::Left),
                ..
            } => {
                self.scrollbase.release_grab();
            }
            Event::Key(Key::PageDown) => self.scrollbase.scroll_down(10),
            Event::Key(Key::PageUp) => self.scrollbase.scroll_up(10),
            _ => return EventResult::Ignored,
        }

        // We just scrolled manually, so reset the scroll strategy.
        self.scroll_strategy = ScrollStrategy::KeepRow;
        EventResult::Consumed(None)
    }

    fn needs_relayout(&self) -> bool {
        let content = self.content.lock().unwrap();
        content.size_cache.is_none()
    }

    fn required_size(&mut self, size: Vec2) -> Vec2 {
        self.compute_rows(size);

        // This is what we'd like
        let mut ideal = Vec2::new(self.width.unwrap_or(0), self.rows.len());

        if self.scrollable && ideal.y > size.y {
            ideal.y = size.y;
        }

        ideal
    }

    fn take_focus(&mut self, _: Direction) -> bool {
        self.scrollbase.scrollable()
    }

    fn layout(&mut self, size: Vec2) {
        // Compute the text rows.
        self.last_size = size;
        self.compute_rows(size);
        // Adjust scrolling, in case we're sticking to the bottom for instance.
        self.scrollbase.set_heights(size.y, self.rows.len());
        self.adjust_scroll();
    }
}
<|endoftext|>