<commit_before>//! FIXME: write short doc here

use itertools::Itertools;
use ra_fmt::{compute_ws, extract_trivial_expression};
use ra_syntax::{
    algo::{find_covering_element, non_trivia_sibling},
    ast::{self, AstNode, AstToken},
    Direction, NodeOrToken, SourceFile,
    SyntaxKind::{self, WHITESPACE},
    SyntaxNode, SyntaxToken, TextRange, TextUnit, T,
};
use ra_text_edit::{TextEdit, TextEditBuilder};

pub fn join_lines(file: &SourceFile, range: TextRange) -> TextEdit {
    let range = if range.is_empty() {
        let syntax = file.syntax();
        let text = syntax.text().slice(range.start()..);
        let pos = match text.find_char('\n') {
            None => return TextEditBuilder::default().finish(),
            Some(pos) => pos,
        };
        TextRange::offset_len(range.start() + pos, TextUnit::of_char('\n'))
    } else {
        range
    };

    let node = match find_covering_element(file.syntax(), range) {
        NodeOrToken::Node(node) => node,
        NodeOrToken::Token(token) => token.parent(),
    };
    let mut edit = TextEditBuilder::default();
    for token in node.descendants_with_tokens().filter_map(|it| it.into_token()) {
        let range = match range.intersection(&token.text_range()) {
            Some(range) => range,
            None => continue,
        } - token.text_range().start();
        let text = token.text();
        for (pos, _) in text[range].bytes().enumerate().filter(|&(_, b)| b == b'\n') {
            let pos: TextUnit = (pos as u32).into();
            let off = token.text_range().start() + range.start() + pos;
            if !edit.invalidates_offset(off) {
                remove_newline(&mut edit, &token, off);
            }
        }
    }

    edit.finish()
}

fn remove_newline(edit: &mut TextEditBuilder, token: &SyntaxToken, offset: TextUnit) {
    if token.kind() != WHITESPACE || token.text().bytes().filter(|&b| b == b'\n').count() != 1 {
        // The node is either the first or the last in the file
        let suff = &token.text()[TextRange::from_to(
            offset - token.text_range().start() + TextUnit::of_char('\n'),
            TextUnit::of_str(token.text()),
        )];
        let spaces = suff.bytes().take_while(|&b| b == b' ').count();

        edit.replace(TextRange::offset_len(offset, ((spaces + 1) as u32).into()), " ".to_string());
        return;
    }

    // Special case that turns something like:
    //
    // ```
    // my_function({<|>
    //    <some-expr>
    // })
    // ```
    //
    // into `my_function(<some-expr>)`
    if join_single_expr_block(edit, token).is_some() {
        return;
    }
    // ditto for
    //
    // ```
    // use foo::{<|>
    //    bar
    // };
    // ```
    if join_single_use_tree(edit, token).is_some() {
        return;
    }

    // The node is between two other nodes
    let prev = token.prev_sibling_or_token().unwrap();
    let next = token.next_sibling_or_token().unwrap();
    if is_trailing_comma(prev.kind(), next.kind()) {
        // Removes: trailing comma, newline (incl. surrounding whitespace)
        edit.delete(TextRange::from_to(prev.text_range().start(), token.text_range().end()));
    } else if prev.kind() == T![,] && next.kind() == T!['}'] {
        // Removes: comma, newline (incl. surrounding whitespace)
        let space = if let Some(left) = prev.prev_sibling_or_token() {
            compute_ws(left.kind(), next.kind())
        } else {
            " "
        };
        edit.replace(
            TextRange::from_to(prev.text_range().start(), token.text_range().end()),
            space.to_string(),
        );
    } else if let (Some(_), Some(next)) = (
        prev.as_token().cloned().and_then(ast::Comment::cast),
        next.as_token().cloned().and_then(ast::Comment::cast),
    ) {
        // Removes: newline (incl. surrounding whitespace), start of the next comment
        edit.delete(TextRange::from_to(
            token.text_range().start(),
            next.syntax().text_range().start() + TextUnit::of_str(next.prefix()),
        ));
    } else {
        // Remove newline but add a computed amount of whitespace characters
        edit.replace(token.text_range(), compute_ws(prev.kind(), next.kind()).to_string());
    }
}

fn has_comma_after(node: &SyntaxNode) -> bool {
    match non_trivia_sibling(node.clone().into(), Direction::Next) {
        Some(n) => n.kind() == T![,],
        _ => false,
    }
}

fn join_single_expr_block(edit: &mut TextEditBuilder, token: &SyntaxToken) -> Option<()> {
    let block = ast::Block::cast(token.parent())?;
    let block_expr = ast::BlockExpr::cast(block.syntax().parent()?)?;
    let expr = extract_trivial_expression(&block_expr)?;

    let block_range = block_expr.syntax().text_range();
    let mut buf = expr.syntax().text().to_string();

    // Match block needs to have a comma after the block
    if let Some(match_arm) = block_expr.syntax().parent().and_then(ast::MatchArm::cast) {
        if !has_comma_after(match_arm.syntax()) {
            buf.push(',');
        }
    }

    edit.replace(block_range, buf);

    Some(())
}

fn join_single_use_tree(edit: &mut TextEditBuilder, token: &SyntaxToken) -> Option<()> {
    let use_tree_list = ast::UseTreeList::cast(token.parent())?;
    let (tree,) = use_tree_list.use_trees().collect_tuple()?;
    edit.replace(use_tree_list.syntax().text_range(), tree.syntax().text().to_string());
    Some(())
}

fn is_trailing_comma(left: SyntaxKind, right: SyntaxKind) -> bool {
    match (left, right) {
        (T![,], T![')']) | (T![,], T![']']) => true,
        _ => false,
    }
}

#[cfg(test)]
mod tests {
    use crate::test_utils::{assert_eq_text, check_action, extract_range};

    use super::*;

    fn check_join_lines(before: &str, after: &str) {
        check_action(before, after, |file, offset| {
            let range = TextRange::offset_len(offset, 0.into());
            let res = join_lines(file, range);
            Some(res)
        })
    }

    #[test]
    fn test_join_lines_comma() {
        check_join_lines(
            r"
fn foo() {
    <|>foo(1,
    )
}
",
            r"
fn foo() {
    <|>foo(1)
}
",
        );
    }

    #[test]
    fn test_join_lines_lambda_block() {
        check_join_lines(
            r"
pub fn reparse(&self, edit: &AtomTextEdit) -> File {
    <|>self.incremental_reparse(edit).unwrap_or_else(|| {
        self.full_reparse(edit)
    })
}
",
            r"
pub fn reparse(&self, edit: &AtomTextEdit) -> File {
    <|>self.incremental_reparse(edit).unwrap_or_else(|| self.full_reparse(edit))
}
",
        );
    }

    #[test]
    fn test_join_lines_block() {
        check_join_lines(
            r"
fn foo() {
    foo(<|>{
        92
    })
}",
            r"
fn foo() {
    foo(<|>92)
}",
        );
    }

    #[test]
    fn join_lines_adds_comma_for_block_in_match_arm() {
        check_join_lines(
            r"
fn foo(e: Result<U, V>) {
    match e {
        Ok(u) => <|>{
            u.foo()
        }
        Err(v) => v,
    }
}",
            r"
fn foo(e: Result<U, V>) {
    match e {
        Ok(u) => <|>u.foo(),
        Err(v) => v,
    }
}",
        );
    }

    #[test]
    fn join_lines_multiline_in_block() {
        check_join_lines(
            r"
fn foo() {
    match ty {
        <|> Some(ty) => {
            match ty {
                _ => false,
            }
        }
        _ => true,
    }
}
",
            r"
fn foo() {
    match ty {
        <|> Some(ty) => match ty {
                _ => false,
            },
        _ => true,
    }
}
",
        );
    }

    #[test]
    fn join_lines_keeps_comma_for_block_in_match_arm() {
        // We already have a comma
        check_join_lines(
            r"
fn foo(e: Result<U, V>) {
    match e {
        Ok(u) => <|>{
            u.foo()
        },
        Err(v) => v,
    }
}",
            r"
fn foo(e: Result<U, V>) {
    match e {
        Ok(u) => <|>u.foo(),
        Err(v) => v,
    }
}",
        );

        // comma with whitespace between brace and ,
        check_join_lines(
            r"
fn foo(e: Result<U, V>) {
    match e {
        Ok(u) => <|>{
            u.foo()
        }    ,
        Err(v) => v,
    }
}",
            r"
fn foo(e: Result<U, V>) {
    match e {
        Ok(u) => <|>u.foo()    ,
        Err(v) => v,
    }
}",
        );

        // comma with newline between brace and ,
        check_join_lines(
            r"
fn foo(e: Result<U, V>) {
    match e {
        Ok(u) => <|>{
            u.foo()
        }
        ,
        Err(v) => v,
    }
}",
            r"
fn foo(e: Result<U, V>) {
    match e {
        Ok(u) => <|>u.foo()
        ,
        Err(v) => v,
    }
}",
        );
    }

    #[test]
    fn join_lines_keeps_comma_with_single_arg_tuple() {
        // A single arg tuple
        check_join_lines(
            r"
fn foo() {
    let x = (<|>{
       4
    },);
}",
            r"
fn foo() {
    let x = (<|>4,);
}",
        );

        // single arg tuple with whitespace between brace and comma
        check_join_lines(
            r"
fn foo() {
    let x = (<|>{
       4
    }   ,);
}",
            r"
fn foo() {
    let x = (<|>4   ,);
}",
        );

        // single arg tuple with newline between brace and comma
        check_join_lines(
            r"
fn foo() {
    let x = (<|>{
       4
    }
    ,);
}",
            r"
fn foo() {
    let x = (<|>4
    ,);
}",
        );
    }

    #[test]
    fn test_join_lines_use_items_left() {
        // No space after the '{'
        check_join_lines(
            r"
<|>use ra_syntax::{
    TextUnit, TextRange,
};",
            r"
<|>use ra_syntax::{TextUnit, TextRange,
};",
        );
    }

    #[test]
    fn test_join_lines_use_items_right() {
        // No space after the '}'
        check_join_lines(
            r"
use ra_syntax::{
<|>    TextUnit, TextRange
};",
            r"
use ra_syntax::{
<|>    TextUnit, TextRange};",
        );
    }

    #[test]
    fn test_join_lines_use_items_right_comma() {
        // No space after the '}'
        check_join_lines(
            r"
use ra_syntax::{
<|>    TextUnit, TextRange,
};",
            r"
use ra_syntax::{
<|>    TextUnit, TextRange};",
        );
    }

    #[test]
    fn test_join_lines_use_tree() {
        check_join_lines(
            r"
use ra_syntax::{
    algo::<|>{
        find_token_at_offset,
    },
    ast,
};",
            r"
use ra_syntax::{
    algo::<|>find_token_at_offset,
    ast,
};",
        );
    }

    #[test]
    fn test_join_lines_normal_comments() {
        check_join_lines(
            r"
fn foo() {
    // Hello<|>
    // world!
}
",
            r"
fn foo() {
    // Hello<|> world!
}
",
        );
    }

    #[test]
    fn test_join_lines_doc_comments() {
        check_join_lines(
            r"
fn foo() {
    /// Hello<|>
    /// world!
}
",
            r"
fn foo() {
    /// Hello<|> world!
}
",
        );
    }

    #[test]
    fn test_join_lines_mod_comments() {
        check_join_lines(
            r"
fn foo() {
    //! Hello<|>
    //! world!
}
",
            r"
fn foo() {
    //! Hello<|> world!
}
",
        );
    }

    #[test]
    fn test_join_lines_multiline_comments_1() {
        check_join_lines(
            r"
fn foo() {
    // Hello<|>
    /* world! */
}
",
            r"
fn foo() {
    // Hello<|> world! */
}
",
        );
    }

    #[test]
    fn test_join_lines_multiline_comments_2() {
        check_join_lines(
            r"
fn foo() {
    // The<|>
    /* quick
    brown
    fox! */
}
",
            r"
fn foo() {
    // The<|> quick
    brown
    fox! */
}
",
        );
    }

    fn check_join_lines_sel(before: &str, after: &str) {
        let (sel, before) = extract_range(before);
        let parse = SourceFile::parse(&before);
        let result = join_lines(&parse.tree(), sel);
        let actual = result.apply(&before);
        assert_eq_text!(after, &actual);
    }

    #[test]
    fn test_join_lines_selection_fn_args() {
        check_join_lines_sel(
            r"
fn foo() {
    <|>foo(1,
        2,
        3,
    <|>)
}
    ",
            r"
fn foo() {
    foo(1, 2, 3)
}
    ",
        );
    }

    #[test]
    fn test_join_lines_selection_struct() {
        check_join_lines_sel(
            r"
struct Foo <|>{
    f: u32,
}<|>
    ",
            r"
struct Foo { f: u32 }
    ",
        );
    }

    #[test]
    fn test_join_lines_selection_dot_chain() {
        check_join_lines_sel(
            r"
fn foo() {
    join(<|>type_params.type_params()
            .filter_map(|it| it.name())
            .map(|it| it.text())<|>)
}",
            r"
fn foo() {
    join(type_params.type_params().filter_map(|it| it.name()).map(|it| it.text()))
}",
        );
    }

    #[test]
    fn test_join_lines_selection_lambda_block_body() {
        check_join_lines_sel(
            r"
pub fn handle_find_matching_brace() {
    params.offsets
        .map(|offset| <|>{
            world.analysis().matching_brace(&file, offset).unwrap_or(offset)
        }<|>)
        .collect();
}",
            r"
pub fn handle_find_matching_brace() {
    params.offsets
        .map(|offset| world.analysis().matching_brace(&file, offset).unwrap_or(offset))
        .collect();
}",
        );
    }
}
<commit_msg>Fix join lines when two rules match<commit_after>//! FIXME: write short doc here

use itertools::Itertools;
use ra_fmt::{compute_ws, extract_trivial_expression};
use ra_syntax::{
    algo::{find_covering_element, non_trivia_sibling},
    ast::{self, AstNode, AstToken},
    Direction, NodeOrToken, SourceFile,
    SyntaxKind::{self, WHITESPACE},
    SyntaxNode, SyntaxToken, TextRange, TextUnit, T,
};
use ra_text_edit::{TextEdit, TextEditBuilder};

pub fn join_lines(file: &SourceFile, range: TextRange) -> TextEdit {
    let range = if range.is_empty() {
        let syntax = file.syntax();
        let text = syntax.text().slice(range.start()..);
        let pos = match text.find_char('\n') {
            None => return TextEditBuilder::default().finish(),
            Some(pos) => pos,
        };
        TextRange::offset_len(range.start() + pos, TextUnit::of_char('\n'))
    } else {
        range
    };

    let node = match find_covering_element(file.syntax(), range) {
        NodeOrToken::Node(node) => node,
        NodeOrToken::Token(token) => token.parent(),
    };
    let mut edit = TextEditBuilder::default();
    for token in node.descendants_with_tokens().filter_map(|it| it.into_token()) {
        let range = match range.intersection(&token.text_range()) {
            Some(range) => range,
            None => continue,
        } - token.text_range().start();
        let text = token.text();
        for (pos, _) in text[range].bytes().enumerate().filter(|&(_, b)| b == b'\n') {
            let pos: TextUnit = (pos as u32).into();
            let off = token.text_range().start() + range.start() + pos;
            if !edit.invalidates_offset(off) {
                remove_newline(&mut edit, &token, off);
            }
        }
    }

    edit.finish()
}

fn remove_newline(edit: &mut TextEditBuilder, token: &SyntaxToken, offset: TextUnit) {
    if token.kind() != WHITESPACE || token.text().bytes().filter(|&b| b == b'\n').count() != 1 {
        // The node is either the first or the last in the file
        let suff = &token.text()[TextRange::from_to(
            offset - token.text_range().start() + TextUnit::of_char('\n'),
            TextUnit::of_str(token.text()),
        )];
        let spaces = suff.bytes().take_while(|&b| b == b' ').count();

        edit.replace(TextRange::offset_len(offset, ((spaces + 1) as u32).into()), " ".to_string());
        return;
    }

    // The node is between two other nodes
    let prev = token.prev_sibling_or_token().unwrap();
    let next = token.next_sibling_or_token().unwrap();
    if is_trailing_comma(prev.kind(), next.kind()) {
        // Removes: trailing comma, newline (incl. surrounding whitespace)
        edit.delete(TextRange::from_to(prev.text_range().start(), token.text_range().end()));
    } else if prev.kind() == T![,] && next.kind() == T!['}'] {
        // Removes: comma, newline (incl. surrounding whitespace)
        let space = if let Some(left) = prev.prev_sibling_or_token() {
            compute_ws(left.kind(), next.kind())
        } else {
            " "
        };
        edit.replace(
            TextRange::from_to(prev.text_range().start(), token.text_range().end()),
            space.to_string(),
        );
    } else if let (Some(_), Some(next)) = (
        prev.as_token().cloned().and_then(ast::Comment::cast),
        next.as_token().cloned().and_then(ast::Comment::cast),
    ) {
        // Removes: newline (incl. surrounding whitespace), start of the next comment
        edit.delete(TextRange::from_to(
            token.text_range().start(),
            next.syntax().text_range().start() + TextUnit::of_str(next.prefix()),
        ));
    } else {
        // Special case that turns something like:
        //
        // ```
        // my_function({<|>
        //    <some-expr>
        // })
        // ```
        //
        // into `my_function(<some-expr>)`
        if join_single_expr_block(edit, token).is_some() {
            return;
        }
        // ditto for
        //
        // ```
        // use foo::{<|>
        //    bar
        // };
        // ```
        if join_single_use_tree(edit, token).is_some() {
            return;
        }

        // Remove newline but add a computed amount of whitespace characters
        edit.replace(token.text_range(), compute_ws(prev.kind(), next.kind()).to_string());
    }
}

fn has_comma_after(node: &SyntaxNode) -> bool {
    match non_trivia_sibling(node.clone().into(), Direction::Next) {
        Some(n) => n.kind() == T![,],
        _ => false,
    }
}

fn join_single_expr_block(edit: &mut TextEditBuilder, token: &SyntaxToken) -> Option<()> {
    let block = ast::Block::cast(token.parent())?;
    let block_expr = ast::BlockExpr::cast(block.syntax().parent()?)?;
    let expr = extract_trivial_expression(&block_expr)?;

    let block_range = block_expr.syntax().text_range();
    let mut buf = expr.syntax().text().to_string();

    // Match block needs to have a comma after the block
    if let Some(match_arm) = block_expr.syntax().parent().and_then(ast::MatchArm::cast) {
        if !has_comma_after(match_arm.syntax()) {
            buf.push(',');
        }
    }

    edit.replace(block_range, buf);

    Some(())
}

fn join_single_use_tree(edit: &mut TextEditBuilder, token: &SyntaxToken) -> Option<()> {
    let use_tree_list = ast::UseTreeList::cast(token.parent())?;
    let (tree,) = use_tree_list.use_trees().collect_tuple()?;
    edit.replace(use_tree_list.syntax().text_range(), tree.syntax().text().to_string());
    Some(())
}

fn is_trailing_comma(left: SyntaxKind, right: SyntaxKind) -> bool {
    match (left, right) {
        (T![,], T![')']) | (T![,], T![']']) => true,
        _ => false,
    }
}

#[cfg(test)]
mod tests {
    use crate::test_utils::{assert_eq_text, check_action, extract_range};

    use super::*;

    fn check_join_lines(before: &str, after: &str) {
        check_action(before, after, |file, offset| {
            let range = TextRange::offset_len(offset, 0.into());
            let res = join_lines(file, range);
            Some(res)
        })
    }

    #[test]
    fn test_join_lines_comma() {
        check_join_lines(
            r"
fn foo() {
    <|>foo(1,
    )
}
",
            r"
fn foo() {
    <|>foo(1)
}
",
        );
    }

    #[test]
    fn test_join_lines_lambda_block() {
        check_join_lines(
            r"
pub fn reparse(&self, edit: &AtomTextEdit) -> File {
    <|>self.incremental_reparse(edit).unwrap_or_else(|| {
        self.full_reparse(edit)
    })
}
",
            r"
pub fn reparse(&self, edit: &AtomTextEdit) -> File {
    <|>self.incremental_reparse(edit).unwrap_or_else(|| self.full_reparse(edit))
}
",
        );
    }

    #[test]
    fn test_join_lines_block() {
        check_join_lines(
            r"
fn foo() {
    foo(<|>{
        92
    })
}",
            r"
fn foo() {
    foo(<|>92)
}",
        );
    }

    #[test]
    fn join_lines_adds_comma_for_block_in_match_arm() {
        check_join_lines(
            r"
fn foo(e: Result<U, V>) {
    match e {
        Ok(u) => <|>{
            u.foo()
        }
        Err(v) => v,
    }
}",
            r"
fn foo(e: Result<U, V>) {
    match e {
        Ok(u) => <|>u.foo(),
        Err(v) => v,
    }
}",
        );
    }

    #[test]
    fn join_lines_multiline_in_block() {
        check_join_lines(
            r"
fn foo() {
    match ty {
        <|> Some(ty) => {
            match ty {
                _ => false,
            }
        }
        _ => true,
    }
}
",
            r"
fn foo() {
    match ty {
        <|> Some(ty) => match ty {
                _ => false,
            },
        _ => true,
    }
}
",
        );
    }

    #[test]
    fn join_lines_keeps_comma_for_block_in_match_arm() {
        // We already have a comma
        check_join_lines(
            r"
fn foo(e: Result<U, V>) {
    match e {
        Ok(u) => <|>{
            u.foo()
        },
        Err(v) => v,
    }
}",
            r"
fn foo(e: Result<U, V>) {
    match e {
        Ok(u) => <|>u.foo(),
        Err(v) => v,
    }
}",
        );

        // comma with whitespace between brace and ,
        check_join_lines(
            r"
fn foo(e: Result<U, V>) {
    match e {
        Ok(u) => <|>{
            u.foo()
        }    ,
        Err(v) => v,
    }
}",
            r"
fn foo(e: Result<U, V>) {
    match e {
        Ok(u) => <|>u.foo()    ,
        Err(v) => v,
    }
}",
        );

        // comma with newline between brace and ,
        check_join_lines(
            r"
fn foo(e: Result<U, V>) {
    match e {
        Ok(u) => <|>{
            u.foo()
        }
        ,
        Err(v) => v,
    }
}",
            r"
fn foo(e: Result<U, V>) {
    match e {
        Ok(u) => <|>u.foo()
        ,
        Err(v) => v,
    }
}",
        );
    }

    #[test]
    fn join_lines_keeps_comma_with_single_arg_tuple() {
        // A single arg tuple
        check_join_lines(
            r"
fn foo() {
    let x = (<|>{
       4
    },);
}",
            r"
fn foo() {
    let x = (<|>4,);
}",
        );

        // single arg tuple with whitespace between brace and comma
        check_join_lines(
            r"
fn foo() {
    let x = (<|>{
       4
    }   ,);
}",
            r"
fn foo() {
    let x = (<|>4   ,);
}",
        );

        // single arg tuple with newline between brace and comma
        check_join_lines(
            r"
fn foo() {
    let x = (<|>{
       4
    }
    ,);
}",
            r"
fn foo() {
    let x = (<|>4
    ,);
}",
        );
    }

    #[test]
    fn test_join_lines_use_items_left() {
        // No space after the '{'
        check_join_lines(
            r"
<|>use ra_syntax::{
    TextUnit, TextRange,
};",
            r"
<|>use ra_syntax::{TextUnit, TextRange,
};",
        );
    }

    #[test]
    fn test_join_lines_use_items_right() {
        // No space after the '}'
        check_join_lines(
            r"
use ra_syntax::{
<|>    TextUnit, TextRange
};",
            r"
use ra_syntax::{
<|>    TextUnit, TextRange};",
        );
    }

    #[test]
    fn test_join_lines_use_items_right_comma() {
        // No space after the '}'
        check_join_lines(
            r"
use ra_syntax::{
<|>    TextUnit, TextRange,
};",
            r"
use ra_syntax::{
<|>    TextUnit, TextRange};",
        );
    }

    #[test]
    fn test_join_lines_use_tree() {
        check_join_lines(
            r"
use ra_syntax::{
    algo::<|>{
        find_token_at_offset,
    },
    ast,
};",
            r"
use ra_syntax::{
    algo::<|>find_token_at_offset,
    ast,
};",
        );
    }

    #[test]
    fn test_join_lines_normal_comments() {
        check_join_lines(
            r"
fn foo() {
    // Hello<|>
    // world!
}
",
            r"
fn foo() {
    // Hello<|> world!
}
",
        );
    }

    #[test]
    fn test_join_lines_doc_comments() {
        check_join_lines(
            r"
fn foo() {
    /// Hello<|>
    /// world!
}
",
            r"
fn foo() {
    /// Hello<|> world!
}
",
        );
    }

    #[test]
    fn test_join_lines_mod_comments() {
        check_join_lines(
            r"
fn foo() {
    //! Hello<|>
    //! world!
}
",
            r"
fn foo() {
    //! Hello<|> world!
}
",
        );
    }

    #[test]
    fn test_join_lines_multiline_comments_1() {
        check_join_lines(
            r"
fn foo() {
    // Hello<|>
    /* world! */
}
",
            r"
fn foo() {
    // Hello<|> world! */
}
",
        );
    }

    #[test]
    fn test_join_lines_multiline_comments_2() {
        check_join_lines(
            r"
fn foo() {
    // The<|>
    /* quick
    brown
    fox! */
}
",
            r"
fn foo() {
    // The<|> quick
    brown
    fox! */
}
",
        );
    }

    fn check_join_lines_sel(before: &str, after: &str) {
        let (sel, before) = extract_range(before);
        let parse = SourceFile::parse(&before);
        let result = join_lines(&parse.tree(), sel);
        let actual = result.apply(&before);
        assert_eq_text!(after, &actual);
    }

    #[test]
    fn test_join_lines_selection_fn_args() {
        check_join_lines_sel(
            r"
fn foo() {
    <|>foo(1,
        2,
        3,
    <|>)
}
    ",
            r"
fn foo() {
    foo(1, 2, 3)
}
    ",
        );
    }

    #[test]
    fn test_join_lines_selection_struct() {
        check_join_lines_sel(
            r"
struct Foo <|>{
    f: u32,
}<|>
    ",
            r"
struct Foo { f: u32 }
    ",
        );
    }

    #[test]
    fn test_join_lines_selection_dot_chain() {
        check_join_lines_sel(
            r"
fn foo() {
    join(<|>type_params.type_params()
            .filter_map(|it| it.name())
            .map(|it| it.text())<|>)
}",
            r"
fn foo() {
    join(type_params.type_params().filter_map(|it| it.name()).map(|it| it.text()))
}",
        );
    }

    #[test]
    fn test_join_lines_selection_lambda_block_body() {
        check_join_lines_sel(
            r"
pub fn handle_find_matching_brace() {
    params.offsets
        .map(|offset| <|>{
            world.analysis().matching_brace(&file, offset).unwrap_or(offset)
        }<|>)
        .collect();
}",
            r"
pub fn handle_find_matching_brace() {
    params.offsets
        .map(|offset| world.analysis().matching_brace(&file, offset).unwrap_or(offset))
        .collect();
}",
        );
    }

    #[test]
    fn test_join_lines_commented_block() {
        check_join_lines(
            r"
fn main() {
    let _ = {
        // <|>foo
        // bar
        92
    };
}
        ",
            r"
fn main() {
    let _ = {
        // <|>foo bar
        92
    };
}
        ",
        )
    }
}
<|endoftext|>


<commit_before>#[macro_use]
extern crate enum_map;

use enum_map::{EnumMap, IntoIter};

use std::cell::RefCell;
use std::collections::HashSet;

#[derive(Copy, Clone, Debug, EnumMap, PartialEq)]
enum Example {
    A,
    B,
    C,
}

#[test]
fn test_bool() {
    let mut map = enum_map! { false => 24, true => 42 };
    assert_eq!(map[false], 24);
    assert_eq!(map[true], 42);
    map[false] += 1;
    assert_eq!(map[false], 25);
    for (key, item) in &mut map {
        if !key {
            *item += 1;
        }
    }
    assert_eq!(map[false], 26);
    assert_eq!(map[true], 42);
}

#[test]
fn test_option_bool() {
    let mut map = enum_map! { None => 1, Some(false) => 2, Some(true) => 3};
    assert_eq!(map[None], 1);
    assert_eq!(map[Some(false)], 2);
    assert_eq!(map[Some(true)], 3);
    map[None] = 4;
    map[Some(false)] = 5;
    map[Some(true)] = 6;
    assert_eq!(map[None], 4);
    assert_eq!(map[Some(false)], 5);
    assert_eq!(map[Some(true)], 6);
    assert_eq!(map.as_slice(), [4, 5, 6]);
    assert_eq!(
        map.into_iter().collect::<Vec<_>>(),
        [(None, 4), (Some(false), 5), (Some(true), 6)]
    );
}

#[test]
fn test_clone() {
    let map = enum_map! { false => 3, true => 5 };
    assert_eq!(map.clone(), map);
}

#[test]
fn test_debug() {
    let map = enum_map! { false => 3, true => 5 };
    assert_eq!(format!("{:?}", map), "{false: 3, true: 5}");
}

#[test]
fn test_hash() {
    let map = enum_map! { false => 3, true => 5 };
    let mut set = HashSet::new();
    set.insert(map);
    assert!(set.contains(&map));
}

#[test]
fn discriminants() {
    #[derive(Debug, EnumMap, PartialEq)]
    enum Discriminants {
        A = 2000,
        B = 3000,
        C = 1000,
    }
    let mut map = EnumMap::new();
    map[Discriminants::A] = 3;
    map[Discriminants::B] = 2;
    map[Discriminants::C] = 1;
    let mut pairs = map.iter();
    assert_eq!(pairs.next(), Some((Discriminants::A, &3)));
    assert_eq!(pairs.next(), Some((Discriminants::B, &2)));
    assert_eq!(pairs.next(), Some((Discriminants::C, &1)));
    assert_eq!(pairs.next(), None);
}

#[test]
fn extend() {
    let mut map = enum_map! { _ => 0 };
    map.extend(vec![(Example::A, 3)]);
    map.extend(vec![(&Example::B, &4)]);
    assert_eq!(
        map,
        enum_map! { Example:: A => 3, Example:: B => 4, Example::C => 0 }
    );
}

#[test]
fn huge_enum() {
    #[derive(EnumMap)]
    enum Example {
        A,
        B,
        C,
        D,
        E,
        F,
        G,
        H,
        I,
        J,
        K,
        L,
        M,
        N,
        O,
        P,
        Q,
        R,
        S,
        T,
        U,
        V,
        W,
        X,
        Y,
        Z,
        Aa,
        Bb,
        Cc,
        Dd,
        Ee,
        Ff,
        Gg,
        Hh,
        Ii,
        Jj,
        Kk,
        Ll,
        Mm,
        Nn,
        Oo,
        Pp,
        Qq,
        Rr,
        Ss,
        Tt,
        Uu,
        Vv,
        Ww,
        Xx,
        Yy,
        Zz,
    }

    let map = enum_map! { _ => 2 };
    assert_eq!(map[Example::Xx], 2);
}

#[test]
fn iterator_len() {
    assert_eq!(
        enum_map! { Example::A | Example::B | Example::C => 0 }
            .iter()
            .len(),
        3
    );
}

#[test]
fn iterator_next_back() {
    assert_eq!(
        enum_map! { Example::A => 1, Example::B => 2, Example::C => 3 }
            .iter()
            .next_back(),
        Some((Example::C, &3))
    );
}

struct DropReporter<'a> {
    into: &'a RefCell<Vec<usize>>,
    value: usize,
}

impl<'a> Drop for DropReporter<'a> {
    fn drop(&mut self) {
        self.into.borrow_mut().push(self.value);
    }
}

#[test]
fn into_iter_drop() {
    let dropped = RefCell::new(Vec::new());
    let mut a: IntoIter<Example, _> = enum_map! {
        k => DropReporter {
            into: &dropped,
            value: k as usize,
        },
    }.into_iter();
    assert_eq!(a.next().unwrap().0, Example::A);
    assert_eq!(*dropped.borrow(), &[0]);
    drop(a);
    assert_eq!(*dropped.borrow(), &[0, 1, 2]);
}

#[test]
fn test_u8() {
    let map = enum_map! { b'a' => 4, _ => 0 };
    assert_eq!(map[b'a'], 4);
    assert_eq!(map[b'b'], 0);
}

#[derive(EnumMap)]
enum Void {}

#[test]
fn empty_map() {
    let void: EnumMap<Void, Void> = enum_map!{};
    assert!(void.is_empty());
}

#[test]
#[should_panic]
fn empty_value() {
    let _void: EnumMap<bool, Void> = enum_map! { _ => unreachable!() };
}
<commit_msg>Test whether IntoIter next method can return None<commit_after>#[macro_use]
extern crate enum_map;

use enum_map::{EnumMap, IntoIter};

use std::cell::RefCell;
use std::collections::HashSet;

#[derive(Copy, Clone, Debug, EnumMap, PartialEq)]
enum Example {
    A,
    B,
    C,
}

#[test]
fn test_bool() {
    let mut map = enum_map! { false => 24, true => 42 };
    assert_eq!(map[false], 24);
    assert_eq!(map[true], 42);
    map[false] += 1;
    assert_eq!(map[false], 25);
    for (key, item) in &mut map {
        if !key {
            *item += 1;
        }
    }
    assert_eq!(map[false], 26);
    assert_eq!(map[true], 42);
}

#[test]
fn test_option_bool() {
    let mut map = enum_map! { None => 1, Some(false) => 2, Some(true) => 3};
    assert_eq!(map[None], 1);
    assert_eq!(map[Some(false)], 2);
    assert_eq!(map[Some(true)], 3);
    map[None] = 4;
    map[Some(false)] = 5;
    map[Some(true)] = 6;
    assert_eq!(map[None], 4);
    assert_eq!(map[Some(false)], 5);
    assert_eq!(map[Some(true)], 6);
    assert_eq!(map.as_slice(), [4, 5, 6]);
    assert_eq!(
        map.into_iter().collect::<Vec<_>>(),
        [(None, 4), (Some(false), 5), (Some(true), 6)]
    );
}

#[test]
fn test_clone() {
    let map = enum_map! { false => 3, true => 5 };
    assert_eq!(map.clone(), map);
}

#[test]
fn test_debug() {
    let map = enum_map! { false => 3, true => 5 };
    assert_eq!(format!("{:?}", map), "{false: 3, true: 5}");
}

#[test]
fn test_hash() {
    let map = enum_map! { false => 3, true => 5 };
    let mut set = HashSet::new();
    set.insert(map);
    assert!(set.contains(&map));
}

#[test]
fn discriminants() {
    #[derive(Debug, EnumMap, PartialEq)]
    enum Discriminants {
        A = 2000,
        B = 3000,
        C = 1000,
    }
    let mut map = EnumMap::new();
    map[Discriminants::A] = 3;
    map[Discriminants::B] = 2;
    map[Discriminants::C] = 1;
    let mut pairs = map.iter();
    assert_eq!(pairs.next(), Some((Discriminants::A, &3)));
    assert_eq!(pairs.next(), Some((Discriminants::B, &2)));
    assert_eq!(pairs.next(), Some((Discriminants::C, &1)));
    assert_eq!(pairs.next(), None);
}

#[test]
fn extend() {
    let mut map = enum_map! { _ => 0 };
    map.extend(vec![(Example::A, 3)]);
    map.extend(vec![(&Example::B, &4)]);
    assert_eq!(
        map,
        enum_map! { Example:: A => 3, Example:: B => 4, Example::C => 0 }
    );
}

#[test]
fn huge_enum() {
    #[derive(EnumMap)]
    enum Example {
        A,
        B,
        C,
        D,
        E,
        F,
        G,
        H,
        I,
        J,
        K,
        L,
        M,
        N,
        O,
        P,
        Q,
        R,
        S,
        T,
        U,
        V,
        W,
        X,
        Y,
        Z,
        Aa,
        Bb,
        Cc,
        Dd,
        Ee,
        Ff,
        Gg,
        Hh,
        Ii,
        Jj,
        Kk,
        Ll,
        Mm,
        Nn,
        Oo,
        Pp,
        Qq,
        Rr,
        Ss,
        Tt,
        Uu,
        Vv,
        Ww,
        Xx,
        Yy,
        Zz,
    }

    let map = enum_map! { _ => 2 };
    assert_eq!(map[Example::Xx], 2);
}

#[test]
fn iterator_len() {
    assert_eq!(
        enum_map! { Example::A | Example::B | Example::C => 0 }
            .iter()
            .len(),
        3
    );
}

#[test]
fn iterator_next_back() {
    assert_eq!(
        enum_map! { Example::A => 1, Example::B => 2, Example::C => 3 }
            .iter()
            .next_back(),
        Some((Example::C, &3))
    );
}

#[test]
fn into_iter() {
    let mut iter = enum_map! { true => 5, false => 7 }.into_iter();
    assert_eq!(iter.next(), Some((false, 7)));
    assert_eq!(iter.next(), Some((true, 5)));
    assert_eq!(iter.next(), None);
    assert_eq!(iter.next(), None);
}

struct DropReporter<'a> {
    into: &'a RefCell<Vec<usize>>,
    value: usize,
}

impl<'a> Drop for DropReporter<'a> {
    fn drop(&mut self) {
        self.into.borrow_mut().push(self.value);
    }
}

#[test]
fn into_iter_drop() {
    let dropped = RefCell::new(Vec::new());
    let mut a: IntoIter<Example, _> = enum_map! {
        k => DropReporter {
            into: &dropped,
            value: k as usize,
        },
    }.into_iter();
    assert_eq!(a.next().unwrap().0, Example::A);
    assert_eq!(*dropped.borrow(), &[0]);
    drop(a);
    assert_eq!(*dropped.borrow(), &[0, 1, 2]);
}

#[test]
fn test_u8() {
    let map = enum_map! { b'a' => 4, _ => 0 };
    assert_eq!(map[b'a'], 4);
    assert_eq!(map[b'b'], 0);
}

#[derive(EnumMap)]
enum Void {}

#[test]
fn empty_map() {
    let void: EnumMap<Void, Void> = enum_map!{};
    assert!(void.is_empty());
}

#[test]
#[should_panic]
fn empty_value() {
    let _void: EnumMap<bool, Void> = enum_map! { _ => unreachable!() };
}
<|endoftext|>


<commit_before>use cairo;
use cairo::{MatrixTrait, PatternTrait};
use std::cell::{Cell, RefCell};

use allowed_url::Href;
use aspect_ratio::AspectRatio;
use attributes::Attribute;
use bbox::BoundingBox;
use drawing_ctx::DrawingCtx;
use error::{NodeError, RenderingError};
use float_eq_cairo::ApproxEqCairo;
use length::*;
use node::*;
use parsers::ParseValue;
use property_bag::PropertyBag;

pub struct NodeImage {
    aspect: Cell<AspectRatio>,
    x: Cell<Length>,
    y: Cell<Length>,
    w: Cell<Length>,
    h: Cell<Length>,
    href: RefCell<Option<Href>>,
}

impl NodeImage {
    pub fn new() -> NodeImage {
        NodeImage {
            aspect: Cell::new(AspectRatio::default()),
            x: Cell::new(Length::default()),
            y: Cell::new(Length::default()),
            w: Cell::new(Length::default()),
            h: Cell::new(Length::default()),
            href: RefCell::new(None),
        }
    }
}

impl NodeTrait for NodeImage {
    fn set_atts(&self, node: &RsvgNode, pbag: &PropertyBag<'_>) -> NodeResult {
        // SVG element has overflow:hidden
        // https://www.w3.org/TR/SVG/styling.html#UAStyleSheet
        node.set_overflow_hidden();

        for (attr, value) in pbag.iter() {
            match attr {
                Attribute::X => self.x.set(attr.parse(value, LengthDir::Horizontal)?),
                Attribute::Y => self.y.set(attr.parse(value, LengthDir::Vertical)?),
                Attribute::Width => self.w.set(attr.parse_and_validate(
                    value,
                    LengthDir::Horizontal,
                    Length::check_nonnegative,
                )?),
                Attribute::Height => self.h.set(attr.parse_and_validate(
                    value,
                    LengthDir::Vertical,
                    Length::check_nonnegative,
                )?),

                Attribute::PreserveAspectRatio => self.aspect.set(attr.parse(value, ())?),

                // "path" is used by some older Adobe Illustrator versions
                Attribute::XlinkHref | Attribute::Path => {
                    // FIXME: use better errors here; these should be loading errors
                    *self.href.borrow_mut() =
                        Some(Href::without_fragment(value).map_err(|_| {
                            NodeError::value_error(attr, "fragment not allowed here")
                        })?);
                }

                _ => (),
            }
        }

        Ok(())
    }

    fn draw(
        &self,
        node: &RsvgNode,
        cascaded: &CascadedValues<'_>,
        draw_ctx: &mut DrawingCtx,
        clipping: bool,
    ) -> Result<(), RenderingError> {
        let surface = if let Some(Href::PlainUrl(ref url)) = *self.href.borrow() {
            draw_ctx.lookup_image(&url)?
        } else {
            return Ok(());
        };

        let values = cascaded.get();
        let params = draw_ctx.get_view_params();

        let x = self.x.get().normalize(values, &params);
        let y = self.y.get().normalize(values, &params);
        let w = self.w.get().normalize(values, &params);
        let h = self.h.get().normalize(values, &params);

        if w.approx_eq_cairo(&0.0) || h.approx_eq_cairo(&0.0) {
            return Ok(());
        }

        draw_ctx.with_discrete_layer(node, values, clipping, &mut |dc| {
            let aspect = self.aspect.get();

            if !values.is_overflow() && aspect.is_slice() {
                dc.clip(x, y, w, h);
            }

            let width = surface.width();
            let height = surface.height();
            if clipping || width == 0 || height == 0 {
                return Ok(());
            }

            // The bounding box for <image> is decided by the values of x, y, w, h and not by
            // the final computed image bounds.
            let bbox = BoundingBox::new(&dc.get_cairo_context().get_matrix()).with_rect(Some(
                cairo::Rectangle {
                    x,
                    y,
                    width: w,
                    height: h,
                },
            ));

            let width = f64::from(width);
            let height = f64::from(height);

            let (x, y, w, h) = aspect.compute(width, height, x, y, w, h);

            let cr = dc.get_cairo_context();

            cr.save();

            dc.set_affine_on_cr(&cr);
            cr.scale(w / width, h / height);
            let x = x * width / w;
            let y = y * height / h;

            // We need to set extend appropriately, so can't use cr.set_source_surface().
            //
            // If extend is left at its default value (None), then bilinear scaling uses
            // transparency outside of the image producing incorrect results.
            // For example, in svg1.1/filters-blend-01-b.svgthere's a completely
            // opaque 100×1 image of a gradient scaled to 100×98 which ends up
            // transparent almost everywhere without this fix (which it shouldn't).
            let ptn = surface.to_cairo_pattern();
            let mut matrix = cairo::Matrix::identity();
            matrix.translate(-x, -y);
            ptn.set_matrix(matrix);
            ptn.set_extend(cairo::Extend::Pad);
            cr.set_source(&ptn);

            // Clip is needed due to extend being set to pad.
            cr.rectangle(x, y, width, height);
            cr.clip();

            cr.paint();

            cr.restore();

            dc.insert_bbox(&bbox);
            Ok(())
        })
    }
}
<commit_msg>image: move lookup to an inner scope<commit_after>use cairo;
use cairo::{MatrixTrait, PatternTrait};
use std::cell::{Cell, RefCell};

use allowed_url::Href;
use aspect_ratio::AspectRatio;
use attributes::Attribute;
use bbox::BoundingBox;
use drawing_ctx::DrawingCtx;
use error::{NodeError, RenderingError};
use float_eq_cairo::ApproxEqCairo;
use length::*;
use node::*;
use parsers::ParseValue;
use property_bag::PropertyBag;

pub struct NodeImage {
    aspect: Cell<AspectRatio>,
    x: Cell<Length>,
    y: Cell<Length>,
    w: Cell<Length>,
    h: Cell<Length>,
    href: RefCell<Option<Href>>,
}

impl NodeImage {
    pub fn new() -> NodeImage {
        NodeImage {
            aspect: Cell::new(AspectRatio::default()),
            x: Cell::new(Length::default()),
            y: Cell::new(Length::default()),
            w: Cell::new(Length::default()),
            h: Cell::new(Length::default()),
            href: RefCell::new(None),
        }
    }
}

impl NodeTrait for NodeImage {
    fn set_atts(&self, node: &RsvgNode, pbag: &PropertyBag<'_>) -> NodeResult {
        // SVG element has overflow:hidden
        // https://www.w3.org/TR/SVG/styling.html#UAStyleSheet
        node.set_overflow_hidden();

        for (attr, value) in pbag.iter() {
            match attr {
                Attribute::X => self.x.set(attr.parse(value, LengthDir::Horizontal)?),
                Attribute::Y => self.y.set(attr.parse(value, LengthDir::Vertical)?),
                Attribute::Width => self.w.set(attr.parse_and_validate(
                    value,
                    LengthDir::Horizontal,
                    Length::check_nonnegative,
                )?),
                Attribute::Height => self.h.set(attr.parse_and_validate(
                    value,
                    LengthDir::Vertical,
                    Length::check_nonnegative,
                )?),

                Attribute::PreserveAspectRatio => self.aspect.set(attr.parse(value, ())?),

                // "path" is used by some older Adobe Illustrator versions
                Attribute::XlinkHref | Attribute::Path => {
                    // FIXME: use better errors here; these should be loading errors
                    *self.href.borrow_mut() =
                        Some(Href::without_fragment(value).map_err(|_| {
                            NodeError::value_error(attr, "fragment not allowed here")
                        })?);
                }

                _ => (),
            }
        }

        Ok(())
    }

    fn draw(
        &self,
        node: &RsvgNode,
        cascaded: &CascadedValues<'_>,
        draw_ctx: &mut DrawingCtx,
        clipping: bool,
    ) -> Result<(), RenderingError> {
        let values = cascaded.get();
        let params = draw_ctx.get_view_params();

        let x = self.x.get().normalize(values, &params);
        let y = self.y.get().normalize(values, &params);
        let w = self.w.get().normalize(values, &params);
        let h = self.h.get().normalize(values, &params);

        if w.approx_eq_cairo(&0.0) || h.approx_eq_cairo(&0.0) {
            return Ok(());
        }

        draw_ctx.with_discrete_layer(node, values, clipping, &mut |dc| {
            let surface = if let Some(Href::PlainUrl(ref url)) = *self.href.borrow() {
                dc.lookup_image(&url)?
            } else {
                return Ok(());
            };

            let aspect = self.aspect.get();

            if !values.is_overflow() && aspect.is_slice() {
                dc.clip(x, y, w, h);
            }

            let width = surface.width();
            let height = surface.height();
            if clipping || width == 0 || height == 0 {
                return Ok(());
            }

            // The bounding box for <image> is decided by the values of x, y, w, h and not by
            // the final computed image bounds.
            let bbox = BoundingBox::new(&dc.get_cairo_context().get_matrix()).with_rect(Some(
                cairo::Rectangle {
                    x,
                    y,
                    width: w,
                    height: h,
                },
            ));

            let width = f64::from(width);
            let height = f64::from(height);

            let (x, y, w, h) = aspect.compute(width, height, x, y, w, h);

            let cr = dc.get_cairo_context();

            cr.save();

            dc.set_affine_on_cr(&cr);
            cr.scale(w / width, h / height);
            let x = x * width / w;
            let y = y * height / h;

            // We need to set extend appropriately, so can't use cr.set_source_surface().
            //
            // If extend is left at its default value (None), then bilinear scaling uses
            // transparency outside of the image producing incorrect results.
            // For example, in svg1.1/filters-blend-01-b.svgthere's a completely
            // opaque 100×1 image of a gradient scaled to 100×98 which ends up
            // transparent almost everywhere without this fix (which it shouldn't).
            let ptn = surface.to_cairo_pattern();
            let mut matrix = cairo::Matrix::identity();
            matrix.translate(-x, -y);
            ptn.set_matrix(matrix);
            ptn.set_extend(cairo::Extend::Pad);
            cr.set_source(&ptn);

            // Clip is needed due to extend being set to pad.
            cr.rectangle(x, y, width, height);
            cr.clip();

            cr.paint();

            cr.restore();

            dc.insert_bbox(&bbox);
            Ok(())
        })
    }
}
<|endoftext|>


<commit_before>extern crate sapper;
extern crate sapper_tmpl;
extern crate sapper_query;
extern crate sapper_body;
extern crate sapper_session;
extern crate sapper_logger;

extern crate serde;
extern crate serde_json;

use sapper::{Request, Response, Result, Error as SapperError};

pub use sapper::PathParams;
pub use sapper_query::QueryParams;
pub use sapper_body::FormParams;
pub use sapper_body::JsonParams;
pub use sapper_session::{ SessionVal, set_cookie };
pub use sapper_tmpl::{
    Context as WebContext,
    TERA,
    TeraResult,
    TeraValue,
    to_value,
    render,
};
pub use sapper_logger::splog;

pub fn init(req: &mut Request, cookie_key: Option<&'static str>) -> Result<()> {
    sapper_logger::init(req)?;
    sapper_query::parse(req)?;
    sapper_body::parse(req)?;
    sapper_session::session_val(req, cookie_key)?;

    Ok(())
}

pub fn finish(req: &Request, res: &mut Response) -> Result<()> {
    sapper_logger::splog(req, res.status())?;

    Ok(())
}

// ============ Status Code ============

#[macro_export]
macro_rules! res_redirect {
    ($redirect_uri:expr) => ({
        Err(SapperError::Found($redirect_uri.to_string()))
    })
}

#[macro_export]
macro_rules! res_400 {
    ($info:expr) => ({
        Err(SapperError::Break($info.to_string()))
    })
}

#[macro_export]
macro_rules! res_404 {
    () => ({
        Err(SapperError::NotFound)
    })
}

#[macro_export]
macro_rules! res_500 {
    ($info:expr) => ({
        Err(SapperError::InternalServerError($info.to_string()))
    })
}

#[macro_export]
macro_rules! set_response_redirect {
    ($response:expr, $redirect_uri:expr) => ({
        use sapper::status;
        use sapper::header::Location;

        $response.set_status(status::Found);
        //response.set_status(status::TemporaryRedirect);
        $response.headers_mut().set(Location($redirect_uri.to_owned()));
        $response.write_body(format!("redirect to {}", $redirect_uri));
    })
}

// ============ Json ============

#[macro_export]
macro_rules! res_json {
    ($json:expr) => ({
        use serde_json;
        use sapper::Response;
        use sapper::header::ContentType;

        let mut response = Response::new();
        response.headers_mut().set(ContentType::json());
        response.write_body(serde_json::to_string(&$json).unwrap());

        Ok(response)
    })
}

#[macro_export]
macro_rules! res_json_ok {
    ($info:expr) => ({
        use sapper::Response;
        use serde_json;

        let json2ret = json!({
            "success": true,
            "info": $info
        });

        res_json!(json2ret)
    })
}

#[macro_export]
macro_rules! res_json_error {
    ($info:expr) => ({
        use sapper::Response;
        use serde_json;

        let json2ret = json!({
            "success": false,
            "info": $info
        });

        res_json!(json2ret)
    })
}

// ============ Page Render ============

#[macro_export]
macro_rules! res_html {
    ($html:expr, $context:expr) => ({
        use sapper::Response;
        use sapper::header::ContentType;

        let res_str = render($html, $context);

        let mut response = Response::new();
        response.headers_mut().set(ContentType::html());
        response.write_body(res_str);

        Ok(response)
    })
}

#[macro_export]
macro_rules! res_html_before {
    ($context:expr) => ({
        Err(SapperError::CustomHtml($context))
    })
}

#[macro_export]
macro_rules! res_xml_string {
    ($context:expr) => ({
        use sapper::Response;
        use sapper::status::StatusCode;

        let mut res = Response::new();
        res.set_status(StatusCode::Ok);
        res.headers_mut().set_raw("Content-Type", vec!["text/xml".as_bytes().to_vec()]);
        res.write_body($context);

        Ok(res)
    })
}

// ============ Params ============

#[macro_export]
macro_rules! get_params {
    ($req:expr, $tykey:ty) => ({
        match $req.ext().get::<$tykey>() {
            Some(params) => {
                params
            },
            None => {
                let info = "no params";
                info!("{}", info);
                return res_400!(info);
            }
        }
    })
}

#[macro_export]
macro_rules! get_path_params {
    ($req:expr) => ({
        get_params!($req, PathParams)
    })
}

#[macro_export]
macro_rules! get_query_params {
    ($req:expr) => ({
        get_params!($req, QueryParams)
    })
}

#[macro_export]
macro_rules! get_form_params {
    ($req:expr) => ({
        get_params!($req, FormParams)
    })
}

#[macro_export]
macro_rules! get_json_params {
    ($req:expr) => ({
        match serde_json::from_value(get_params!($req, JsonParams).clone()) {
            Ok(val) => val,
            Err(_) => {
                let info = "Json parameter not match to struct.";
                info!("{}", info);
                return res_400!(info);
            }
        }
    })
}

#[macro_export]
macro_rules! t_cond {
    ($bool:expr, $prompt:expr) => ({
        match $bool {
            true => (),
            false => {
                let info = format!("test param condition result: {}", $prompt);
                info!("{}", info);
                return res_400!(info);
            }
        }
    })
}

#[macro_export]
macro_rules! _missing_or_unrecognized {
    ($field:expr) => ({
        let info = format!("missing or unrecognized parameter {}.", $field);
        info!("{}",info);
        return res_400!(info);
    })
}

#[macro_export]
macro_rules! _using_default {
    ($field:expr, $default:expr) => ({
        info!("missing or unrecognized parameter {}, using default {}.", $field, $default);
        // return default
        $default
    })
}

// for PathParams, QueryParams and FormParams
#[macro_export]
macro_rules! t_param {
    ($params:expr, $field:expr) => ({
        match $params.get($field) {
            Some(ref astr) => &astr[0],
            None =>  _missing_or_unrecognized! ($field)
        }
    })
}

// for PathParams, QueryParams and FormParams, default version
#[macro_export]
macro_rules! t_param_default {
    ($params:expr, $field:expr, $default:expr) => ({
        match $params.get($field) {
            Some(ref astr) => &astr[0],
            None =>  _using_default! ($field, $default)
        }
    })
}

#[macro_export]
macro_rules! t_param_parse {
    ($params:expr, $field:expr, $tykey:ty) => ({
        match $params.get($field) {
            Some(ref astr) => {
                let _t = &astr[0];
                match _t.parse::<$tykey>() {
                    Ok(output) => output,
                    Err(_) => {
                        let info = format!("parse parameter type error {}.", $field);
                        info!("{}", info);
                        return res_400!(info);
                    }
                }
            },
            None =>  _missing_or_unrecognized! ($field)
        }
    })
}

#[macro_export]
macro_rules! t_param_parse_default {
    ($params:expr, $field:expr, $tykey:ty, $default:expr) => ({
        match $params.get($field) {
            Some(ref astr) => {
                let _t = &astr[0];
                match _t.parse::<$tykey>() {
                    Ok(output) => output,
                    Err(_) => {
                        _using_default! ($field, $default)
                    }
                }
            },
            None =>  _using_default! ($field, $default)
        }
    })
}

#[macro_export]
macro_rules! t_arr_param {
    ($params:expr, $field:expr) => ({
        match $params.get($field) {
            Some(ref arr) => *arr,
            None =>  _missing_or_unrecognized! ($field)
        }
    })
}

#[macro_export]
macro_rules! t_arr_param_default {
    ($params:expr, $field:expr, $default:expr) => ({
        match $params.get($field) {
            Some(ref arr) => *arr,
            None =>  _using_default! ($field, $default)
        }
    })
}

#[macro_export]
macro_rules! t_has_param {
    ($params:expr, $field:expr) => ({
        match $params.get($field) {
            Some(_) => true,
            None => false
        }
    })
}

// ============ Ext Type Params ============
#[macro_export]
macro_rules! ext_type {
    ($req:expr, $tykey:ty) => ({
        match $req.ext().get::<$tykey>() {
            Some(entity) => {
                Some(entity)
            },
            None => None
        }
    })
}

#[macro_export]
macro_rules! ext_type_owned {
    ($req:expr, $tykey:ty) => ({
        match $req.ext().get::<$tykey>() {
            Some(entity) => {
                Some(entity.clone())
            },
            None => None
        }
    })
}

<commit_msg>remove unused import in sapper_std.<commit_after>extern crate sapper;
extern crate sapper_tmpl;
extern crate sapper_query;
extern crate sapper_body;
extern crate sapper_session;
extern crate sapper_logger;

extern crate serde;
extern crate serde_json;

use sapper::{Request, Response, Result};

pub use sapper::PathParams;
pub use sapper_query::QueryParams;
pub use sapper_body::FormParams;
pub use sapper_body::JsonParams;
pub use sapper_session::{ SessionVal, set_cookie };
pub use sapper_tmpl::{
    Context as WebContext,
    TERA,
    TeraResult,
    TeraValue,
    to_value,
    render,
};
pub use sapper_logger::splog;

pub fn init(req: &mut Request, cookie_key: Option<&'static str>) -> Result<()> {
    sapper_logger::init(req)?;
    sapper_query::parse(req)?;
    sapper_body::parse(req)?;
    sapper_session::session_val(req, cookie_key)?;

    Ok(())
}

pub fn finish(req: &Request, res: &mut Response) -> Result<()> {
    sapper_logger::splog(req, res.status())?;

    Ok(())
}

// ============ Status Code ============

#[macro_export]
macro_rules! res_redirect {
    ($redirect_uri:expr) => ({
        Err(SapperError::Found($redirect_uri.to_string()))
    })
}

#[macro_export]
macro_rules! res_400 {
    ($info:expr) => ({
        Err(SapperError::Break($info.to_string()))
    })
}

#[macro_export]
macro_rules! res_404 {
    () => ({
        Err(SapperError::NotFound)
    })
}

#[macro_export]
macro_rules! res_500 {
    ($info:expr) => ({
        Err(SapperError::InternalServerError($info.to_string()))
    })
}

#[macro_export]
macro_rules! set_response_redirect {
    ($response:expr, $redirect_uri:expr) => ({
        use sapper::status;
        use sapper::header::Location;

        $response.set_status(status::Found);
        //response.set_status(status::TemporaryRedirect);
        $response.headers_mut().set(Location($redirect_uri.to_owned()));
        $response.write_body(format!("redirect to {}", $redirect_uri));
    })
}

// ============ Json ============

#[macro_export]
macro_rules! res_json {
    ($json:expr) => ({
        use serde_json;
        use sapper::Response;
        use sapper::header::ContentType;

        let mut response = Response::new();
        response.headers_mut().set(ContentType::json());
        response.write_body(serde_json::to_string(&$json).unwrap());

        Ok(response)
    })
}

#[macro_export]
macro_rules! res_json_ok {
    ($info:expr) => ({
        use sapper::Response;
        use serde_json;

        let json2ret = json!({
            "success": true,
            "info": $info
        });

        res_json!(json2ret)
    })
}

#[macro_export]
macro_rules! res_json_error {
    ($info:expr) => ({
        use sapper::Response;
        use serde_json;

        let json2ret = json!({
            "success": false,
            "info": $info
        });

        res_json!(json2ret)
    })
}

// ============ Page Render ============

#[macro_export]
macro_rules! res_html {
    ($html:expr, $context:expr) => ({
        use sapper::Response;
        use sapper::header::ContentType;

        let res_str = render($html, $context);

        let mut response = Response::new();
        response.headers_mut().set(ContentType::html());
        response.write_body(res_str);

        Ok(response)
    })
}

#[macro_export]
macro_rules! res_html_before {
    ($context:expr) => ({
        Err(SapperError::CustomHtml($context))
    })
}

#[macro_export]
macro_rules! res_xml_string {
    ($context:expr) => ({
        use sapper::Response;
        use sapper::status::StatusCode;

        let mut res = Response::new();
        res.set_status(StatusCode::Ok);
        res.headers_mut().set_raw("Content-Type", vec!["text/xml".as_bytes().to_vec()]);
        res.write_body($context);

        Ok(res)
    })
}

// ============ Params ============

#[macro_export]
macro_rules! get_params {
    ($req:expr, $tykey:ty) => ({
        match $req.ext().get::<$tykey>() {
            Some(params) => {
                params
            },
            None => {
                let info = "no params";
                info!("{}", info);
                return res_400!(info);
            }
        }
    })
}

#[macro_export]
macro_rules! get_path_params {
    ($req:expr) => ({
        get_params!($req, PathParams)
    })
}

#[macro_export]
macro_rules! get_query_params {
    ($req:expr) => ({
        get_params!($req, QueryParams)
    })
}

#[macro_export]
macro_rules! get_form_params {
    ($req:expr) => ({
        get_params!($req, FormParams)
    })
}

#[macro_export]
macro_rules! get_json_params {
    ($req:expr) => ({
        match serde_json::from_value(get_params!($req, JsonParams).clone()) {
            Ok(val) => val,
            Err(_) => {
                let info = "Json parameter not match to struct.";
                info!("{}", info);
                return res_400!(info);
            }
        }
    })
}

#[macro_export]
macro_rules! t_cond {
    ($bool:expr, $prompt:expr) => ({
        match $bool {
            true => (),
            false => {
                let info = format!("test param condition result: {}", $prompt);
                info!("{}", info);
                return res_400!(info);
            }
        }
    })
}

#[macro_export]
macro_rules! _missing_or_unrecognized {
    ($field:expr) => ({
        let info = format!("missing or unrecognized parameter {}.", $field);
        info!("{}",info);
        return res_400!(info);
    })
}

#[macro_export]
macro_rules! _using_default {
    ($field:expr, $default:expr) => ({
        info!("missing or unrecognized parameter {}, using default {}.", $field, $default);
        // return default
        $default
    })
}

// for PathParams, QueryParams and FormParams
#[macro_export]
macro_rules! t_param {
    ($params:expr, $field:expr) => ({
        match $params.get($field) {
            Some(ref astr) => &astr[0],
            None =>  _missing_or_unrecognized! ($field)
        }
    })
}

// for PathParams, QueryParams and FormParams, default version
#[macro_export]
macro_rules! t_param_default {
    ($params:expr, $field:expr, $default:expr) => ({
        match $params.get($field) {
            Some(ref astr) => &astr[0],
            None =>  _using_default! ($field, $default)
        }
    })
}

#[macro_export]
macro_rules! t_param_parse {
    ($params:expr, $field:expr, $tykey:ty) => ({
        match $params.get($field) {
            Some(ref astr) => {
                let _t = &astr[0];
                match _t.parse::<$tykey>() {
                    Ok(output) => output,
                    Err(_) => {
                        let info = format!("parse parameter type error {}.", $field);
                        info!("{}", info);
                        return res_400!(info);
                    }
                }
            },
            None =>  _missing_or_unrecognized! ($field)
        }
    })
}

#[macro_export]
macro_rules! t_param_parse_default {
    ($params:expr, $field:expr, $tykey:ty, $default:expr) => ({
        match $params.get($field) {
            Some(ref astr) => {
                let _t = &astr[0];
                match _t.parse::<$tykey>() {
                    Ok(output) => output,
                    Err(_) => {
                        _using_default! ($field, $default)
                    }
                }
            },
            None =>  _using_default! ($field, $default)
        }
    })
}

#[macro_export]
macro_rules! t_arr_param {
    ($params:expr, $field:expr) => ({
        match $params.get($field) {
            Some(ref arr) => *arr,
            None =>  _missing_or_unrecognized! ($field)
        }
    })
}

#[macro_export]
macro_rules! t_arr_param_default {
    ($params:expr, $field:expr, $default:expr) => ({
        match $params.get($field) {
            Some(ref arr) => *arr,
            None =>  _using_default! ($field, $default)
        }
    })
}

#[macro_export]
macro_rules! t_has_param {
    ($params:expr, $field:expr) => ({
        match $params.get($field) {
            Some(_) => true,
            None => false
        }
    })
}

// ============ Ext Type Params ============
#[macro_export]
macro_rules! ext_type {
    ($req:expr, $tykey:ty) => ({
        match $req.ext().get::<$tykey>() {
            Some(entity) => {
                Some(entity)
            },
            None => None
        }
    })
}

#[macro_export]
macro_rules! ext_type_owned {
    ($req:expr, $tykey:ty) => ({
        match $req.ext().get::<$tykey>() {
            Some(entity) => {
                Some(entity.clone())
            },
            None => None
        }
    })
}

<|endoftext|>


<commit_before>mod component;
mod ui;

use super::{CurrentWatchInfo, SeriesPlayerArgs};
use crate::config::Config;
use crate::err::{self, Result};
use crate::file::{SaveDir, SaveFile};
use crate::track::SeriesTracker;
use anime::remote::RemoteService;
use anime::{SeasonInfoList, Series};
use chrono::{DateTime, Duration, Utc};
use clap::ArgMatches;
use component::command_prompt::{Command, CommandPrompt};
use component::log::{LogItem, StatusLog};
use snafu::{OptionExt, ResultExt};
use std::mem;
use std::process;
use termion::event::Key;
use ui::{Event, Events, UI};

pub fn run(args: &ArgMatches) -> Result<()> {
    let cstate = {
        let config = super::get_config()?;
        let remote = super::get_remote(args, true)?;

        CommonState {
            args,
            config,
            remote,
        }
    };

    let mut ui = UI::init()?;

    // Due to the way series selection works, we can't select a saved series that no longer
    // has matching episodes on disk, so we might as well just remove the series data.
    //
    // Series data could be deleted simply by renaming the folder episodes are in to something
    // the program can't recognize; however, the risk is small enough for this to be worth it.
    super::remove_orphaned_data(&cstate.config, |removed| {
        ui.status_log.push(format!("Removing {}", removed))
    })?;

    let mut ui_state = {
        let watch_info = super::get_watch_info(args)?;
        let series = SeriesState::new(&cstate, watch_info, true)?;

        let series_names = {
            let mut names = SaveDir::LocalData.get_subdirs()?;
            names.sort_unstable();
            names
        };

        let selected_series = series_names
            .iter()
            .position(|s| *s == series.watch_info.name)
            .unwrap_or(0);

        UIState {
            series,
            series_names,
            selected_series,
            selection: Selection::Series,
            status_bar_state: StatusBarState::default(),
            last_used_command: None,
        }
    };

    let events = Events::new(Duration::seconds(1));

    loop {
        ui.draw(&ui_state, cstate.remote.as_ref())?;

        match events.next()? {
            Event::Input(key) => match key {
                // Exit
                Key::Char('q') if !ui_state.status_bar_state.in_input_dialog() => {
                    // Prevent ruining the user's terminal
                    ui.clear().ok();
                    break Ok(());
                }
                key => match ui_state.process_key(&cstate, &mut ui, key) {
                    Ok(_) => (),
                    Err(err) => {
                        ui.status_log.push(LogItem::failed("Processing key", err));
                    }
                },
            },
            Event::Tick => match ui_state.process_tick(&cstate, &mut ui) {
                Ok(_) => (),
                Err(err) => ui.status_log.push(LogItem::failed("Processing tick", err)),
            },
        }
    }
}

/// Items that are not tied to the UI and are commonly used together.
struct CommonState<'a> {
    args: &'a ArgMatches<'a>,
    config: Config,
    remote: Box<dyn RemoteService>,
}

/// Current state of the UI.
pub struct UIState<'a> {
    series: SeriesState<'a>,
    series_names: Vec<String>,
    selected_series: usize,
    selection: Selection,
    status_bar_state: StatusBarState<'a>,
    last_used_command: Option<Command>,
}

impl<'a> UIState<'a> {
    fn process_key<B>(&mut self, state: &'a CommonState, ui: &mut UI<B>, key: Key) -> Result<()>
    where
        B: tui::backend::Backend,
    {
        if !self.is_idle() {
            return Ok(());
        }

        if self.status_bar_state.in_input_dialog() {
            return self.process_input_dialog_key(state, ui, key);
        }

        match key {
            // Play next episode
            Key::Char(ch) if ch == state.config.tui.keys.play_next_episode => {
                self.series.set_as_last_watched(ui);

                ui.status_log.capture_status("Playing next episode", || {
                    self.series.season.play_next_episode_async(&state)
                });
            }
            // Command prompt
            Key::Char(':') => {
                self.status_bar_state.set_to_command_prompt();
            }
            // Run last used command
            Key::Char(ch) if ch == state.config.tui.keys.run_last_command => {
                let cmd = match &self.last_used_command {
                    Some(cmd) => cmd.clone(),
                    None => return Ok(()),
                };

                self.process_command(cmd, state, &mut ui.status_log)?;
            }
            // Switch between series and season selection
            Key::Left | Key::Right => {
                self.selection.set_opposite();
            }
            // Select series / season
            Key::Up | Key::Down => {
                let next_value = |value: usize| match key {
                    Key::Up => value.saturating_sub(1),
                    Key::Down => value + 1,
                    _ => value,
                };

                match self.selection {
                    Selection::Series => {
                        let series_index = next_value(self.selected_series);
                        let new_name = match self.series_names.get(series_index) {
                            Some(new_name) => new_name,
                            None => return Ok(()),
                        };

                        let watch_info = CurrentWatchInfo::new(new_name, 0);

                        self.series = SeriesState::new(state, watch_info, false)?;
                        self.selected_series = series_index;
                    }
                    Selection::Season => {
                        let season_index = next_value(self.series.watch_info.season);
                        self.series.set_season(season_index)?;
                    }
                }
            }
            _ => (),
        }

        Ok(())
    }

    fn process_input_dialog_key<B>(
        &mut self,
        state: &'a CommonState,
        ui: &mut UI<B>,
        key: Key,
    ) -> Result<()>
    where
        B: tui::backend::Backend,
    {
        match &mut self.status_bar_state {
            StatusBarState::Log => Ok(()),
            StatusBarState::CommandPrompt(prompt) => {
                use component::command_prompt::PromptResult;

                match prompt.process_key(key) {
                    Ok(PromptResult::Command(command)) => {
                        self.status_bar_state.reset();
                        self.last_used_command = Some(command.clone());
                        self.process_command(command, state, &mut ui.status_log)
                    }
                    Ok(PromptResult::Done) => {
                        self.status_bar_state.reset();
                        Ok(())
                    }
                    Ok(PromptResult::NotDone) => Ok(()),
                    // We need to set the status bar state back before propagating errors,
                    // otherwise we'll be stuck in the prompt
                    Err(err) => {
                        self.status_bar_state.reset();
                        Err(err)
                    }
                }
            }
        }
    }

    fn process_command(
        &mut self,
        command: Command,
        cstate: &CommonState,
        log: &mut StatusLog,
    ) -> Result<()> {
        let remote = cstate.remote.as_ref();

        match command {
            Command::SyncFromRemote => {
                let season = &mut self.series.season;

                log.capture_status("Syncing entry from remote", || {
                    season.tracker.force_sync_changes_from_remote(remote)
                });

                Ok(())
            }
            Command::SyncToRemote => {
                let season = &mut self.series.season;

                log.capture_status("Syncing entry to remote", || {
                    season.tracker.force_sync_changes_to_remote(remote)
                });

                Ok(())
            }
            Command::Status(status) => {
                let season = &mut self.series.season;
                let entry = &mut season.tracker.entry;

                entry.set_status(status);

                log.capture_status(format!("Setting series status to \"{}\"", status), || {
                    season.tracker.sync_changes_to_remote(remote)
                });

                Ok(())
            }
            Command::Progress(direction) => {
                use component::command_prompt::ProgressDirection;

                let tracker = &mut self.series.season.tracker;

                match direction {
                    ProgressDirection::Forwards => {
                        log.capture_status("Forcing forward watch progress", || {
                            tracker.episode_completed(remote, &cstate.config)
                        });
                    }
                    ProgressDirection::Backwards => {
                        log.capture_status("Forcing backwards watch progress", || {
                            tracker.episode_regressed(remote)
                        });
                    }
                }

                Ok(())
            }
            Command::Score(raw_score) => {
                let score = match cstate.remote.parse_score(&raw_score) {
                    Some(score) if score == 0 => None,
                    Some(score) => Some(score),
                    None => {
                        log.push(LogItem::failed("Parsing score", None));
                        return Ok(());
                    }
                };

                let tracker = &mut self.series.season.tracker;
                tracker.entry.set_score(score);

                log.capture_status("Setting score", || {
                    tracker.sync_changes_to_remote(cstate.remote.as_ref())
                });

                Ok(())
            }
            Command::PlayerArgs(args) => {
                let name = self.series.watch_info.name.as_ref();

                log.capture_status("Saving player args for series", || {
                    SeriesPlayerArgs::new(args).save(name)
                });

                Ok(())
            }
        }
    }

    fn process_tick<B>(&mut self, state: &'a CommonState, ui: &mut UI<B>) -> Result<()>
    where
        B: tui::backend::Backend,
    {
        self.series.season.process_tick(state, ui)
    }

    fn is_idle(&self) -> bool {
        self.series.season.watch_state == WatchState::Idle
    }
}

#[derive(Copy, Clone)]
enum Selection {
    Series,
    Season,
}

impl Selection {
    fn opposite(self) -> Selection {
        match self {
            Selection::Series => Selection::Season,
            Selection::Season => Selection::Series,
        }
    }

    fn set_opposite(&mut self) {
        *self = self.opposite();
    }
}

enum StatusBarState<'a> {
    Log,
    CommandPrompt(CommandPrompt<'a>),
}

impl<'a> StatusBarState<'a> {
    fn set_to_command_prompt(&mut self) {
        *self = StatusBarState::CommandPrompt(CommandPrompt::new());
    }

    fn reset(&mut self) {
        *self = StatusBarState::default();
    }

    fn in_input_dialog(&self) -> bool {
        match self {
            StatusBarState::Log => false,
            StatusBarState::CommandPrompt(_) => true,
        }
    }
}

impl<'a> Default for StatusBarState<'a> {
    fn default() -> StatusBarState<'a> {
        StatusBarState::Log
    }
}

struct SeriesState<'a> {
    watch_info: CurrentWatchInfo,
    season: SeasonState<'a>,
    seasons: SeasonInfoList,
    num_seasons: usize,
    is_last_watched: bool,
}

impl<'a> SeriesState<'a> {
    fn new(
        state: &'a CommonState,
        watch_info: CurrentWatchInfo,
        is_last_watched: bool,
    ) -> Result<SeriesState<'a>> {
        let remote = state.remote.as_ref();
        let name = &watch_info.name;

        let episodes = super::get_episodes(&state.args, name, &state.config, is_last_watched)?;
        let seasons = super::get_season_list(name, remote, &episodes)?;
        let num_seasons = seasons.len();
        let series = Series::from_season_list(&seasons, watch_info.season, episodes)?;
        let season = SeasonState::new(name, series)?;

        Ok(SeriesState {
            watch_info,
            season,
            seasons,
            num_seasons,
            is_last_watched,
        })
    }

    /// Loads the season specified by `season_num` and points `season` to it.
    fn set_season(&mut self, season_num: usize) -> Result<()> {
        if !self.seasons.has(season_num) {
            return Ok(());
        }

        let episodes = self.season.series.episodes.clone();
        let series = Series::from_season_list(&self.seasons, season_num, episodes)?;

        self.season = SeasonState::new(&self.watch_info.name, series)?;
        self.watch_info.season = season_num;

        Ok(())
    }

    /// Sets the current series as the last watched one if it isn't already.
    fn set_as_last_watched<B>(&mut self, ui: &mut UI<B>)
    where
        B: tui::backend::Backend,
    {
        if self.is_last_watched {
            return;
        }

        ui.status_log
            .capture_status("Marking as the last watched series", || {
                self.watch_info.save(None)
            });

        self.is_last_watched = true;
    }
}

struct SeasonState<'a> {
    series: Series<'a>,
    tracker: SeriesTracker<'a>,
    watch_state: WatchState,
}

impl<'a> SeasonState<'a> {
    fn new<S>(name: S, series: Series<'a>) -> Result<SeasonState<'a>>
    where
        S: Into<String>,
    {
        let tracker = SeriesTracker::init(series.info.clone(), name)?;

        Ok(SeasonState {
            series,
            tracker,
            watch_state: WatchState::Idle,
        })
    }

    fn play_next_episode_async(&mut self, state: &CommonState) -> Result<()> {
        let remote = state.remote.as_ref();
        let config = &state.config;

        self.tracker.begin_watching(remote, config)?;
        let next_ep = self.tracker.entry.watched_eps() + 1;

        let episode = self
            .series
            .get_episode(next_ep)
            .context(err::EpisodeNotFound { episode: next_ep })?;

        let child = super::prepare_episode_cmd(&self.tracker.name, &state.config, episode)?
            .spawn()
            .context(err::FailedToPlayEpisode { episode: next_ep })?;

        let progress_time = {
            let secs_must_watch =
                (self.series.info.episode_length as f32 * config.episode.pcnt_must_watch) * 60.0;
            let time_must_watch = Duration::seconds(secs_must_watch as i64);

            Utc::now() + time_must_watch
        };

        self.watch_state = WatchState::Watching(progress_time, child);

        Ok(())
    }

    fn process_tick<B>(&mut self, state: &'a CommonState, ui: &mut UI<B>) -> Result<()>
    where
        B: tui::backend::Backend,
    {
        match &mut self.watch_state {
            WatchState::Idle => (),
            WatchState::Watching(_, child) => {
                let status = match child.try_wait().context(err::IO)? {
                    Some(status) => status,
                    None => return Ok(()),
                };

                // The watch state should be set to idle immediately to avoid a potential infinite loop.
                let progress_time = match mem::replace(&mut self.watch_state, WatchState::Idle) {
                    WatchState::Watching(progress_time, _) => progress_time,
                    WatchState::Idle => unreachable!(),
                };

                if !status.success() {
                    ui.status_log.push("Player did not exit properly");
                    return Ok(());
                }

                if Utc::now() >= progress_time {
                    ui.status_log
                        .capture_status("Marking episode as completed", || {
                            self.tracker
                                .episode_completed(state.remote.as_ref(), &state.config)
                        });
                } else {
                    ui.status_log.push("Not marking episode as completed");
                }
            }
        }

        Ok(())
    }
}

type ProgressTime = DateTime<Utc>;

enum WatchState {
    Idle,
    Watching(ProgressTime, process::Child),
}

impl PartialEq for WatchState {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (WatchState::Idle, WatchState::Idle) => true,
            (WatchState::Watching(_, _), WatchState::Watching(_, _)) => true,
            _ => false,
        }
    }
}
<commit_msg>[anup] Replace "&mut UI<_>" parameters with "&mut StatusLog"<commit_after>mod component;
mod ui;

use super::{CurrentWatchInfo, SeriesPlayerArgs};
use crate::config::Config;
use crate::err::{self, Result};
use crate::file::{SaveDir, SaveFile};
use crate::track::SeriesTracker;
use anime::remote::RemoteService;
use anime::{SeasonInfoList, Series};
use chrono::{DateTime, Duration, Utc};
use clap::ArgMatches;
use component::command_prompt::{Command, CommandPrompt};
use component::log::{LogItem, StatusLog};
use snafu::{OptionExt, ResultExt};
use std::mem;
use std::process;
use termion::event::Key;
use ui::{Event, Events, UI};

pub fn run(args: &ArgMatches) -> Result<()> {
    let cstate = {
        let config = super::get_config()?;
        let remote = super::get_remote(args, true)?;

        CommonState {
            args,
            config,
            remote,
        }
    };

    let mut ui = UI::init()?;

    // Due to the way series selection works, we can't select a saved series that no longer
    // has matching episodes on disk, so we might as well just remove the series data.
    //
    // Series data could be deleted simply by renaming the folder episodes are in to something
    // the program can't recognize; however, the risk is small enough for this to be worth it.
    super::remove_orphaned_data(&cstate.config, |removed| {
        ui.status_log.push(format!("Removing {}", removed))
    })?;

    let mut ui_state = {
        let watch_info = super::get_watch_info(args)?;
        let series = SeriesState::new(&cstate, watch_info, true)?;

        let series_names = {
            let mut names = SaveDir::LocalData.get_subdirs()?;
            names.sort_unstable();
            names
        };

        let selected_series = series_names
            .iter()
            .position(|s| *s == series.watch_info.name)
            .unwrap_or(0);

        UIState {
            series,
            series_names,
            selected_series,
            selection: Selection::Series,
            status_bar_state: StatusBarState::default(),
            last_used_command: None,
        }
    };

    let events = Events::new(Duration::seconds(1));

    loop {
        ui.draw(&ui_state, cstate.remote.as_ref())?;

        match events.next()? {
            Event::Input(key) => match key {
                // Exit
                Key::Char('q') if !ui_state.status_bar_state.in_input_dialog() => {
                    // Prevent ruining the user's terminal
                    ui.clear().ok();
                    break Ok(());
                }
                key => match ui_state.process_key(&cstate, &mut ui.status_log, key) {
                    Ok(_) => (),
                    Err(err) => {
                        ui.status_log.push(LogItem::failed("Processing key", err));
                    }
                },
            },
            Event::Tick => match ui_state.process_tick(&cstate, &mut ui.status_log) {
                Ok(_) => (),
                Err(err) => ui.status_log.push(LogItem::failed("Processing tick", err)),
            },
        }
    }
}

/// Items that are not tied to the UI and are commonly used together.
struct CommonState<'a> {
    args: &'a ArgMatches<'a>,
    config: Config,
    remote: Box<dyn RemoteService>,
}

/// Current state of the UI.
pub struct UIState<'a> {
    series: SeriesState<'a>,
    series_names: Vec<String>,
    selected_series: usize,
    selection: Selection,
    status_bar_state: StatusBarState<'a>,
    last_used_command: Option<Command>,
}

impl<'a> UIState<'a> {
    fn process_key(&mut self, state: &'a CommonState, log: &mut StatusLog, key: Key) -> Result<()> {
        if !self.is_idle() {
            return Ok(());
        }

        if self.status_bar_state.in_input_dialog() {
            return self.process_input_dialog_key(state, log, key);
        }

        match key {
            // Play next episode
            Key::Char(ch) if ch == state.config.tui.keys.play_next_episode => {
                self.series.set_as_last_watched(log);

                log.capture_status("Playing next episode", || {
                    self.series.season.play_next_episode_async(&state)
                });
            }
            // Command prompt
            Key::Char(':') => {
                self.status_bar_state.set_to_command_prompt();
            }
            // Run last used command
            Key::Char(ch) if ch == state.config.tui.keys.run_last_command => {
                let cmd = match &self.last_used_command {
                    Some(cmd) => cmd.clone(),
                    None => return Ok(()),
                };

                self.process_command(cmd, state, log)?;
            }
            // Switch between series and season selection
            Key::Left | Key::Right => {
                self.selection.set_opposite();
            }
            // Select series / season
            Key::Up | Key::Down => {
                let next_value = |value: usize| match key {
                    Key::Up => value.saturating_sub(1),
                    Key::Down => value + 1,
                    _ => value,
                };

                match self.selection {
                    Selection::Series => {
                        let series_index = next_value(self.selected_series);
                        let new_name = match self.series_names.get(series_index) {
                            Some(new_name) => new_name,
                            None => return Ok(()),
                        };

                        let watch_info = CurrentWatchInfo::new(new_name, 0);

                        self.series = SeriesState::new(state, watch_info, false)?;
                        self.selected_series = series_index;
                    }
                    Selection::Season => {
                        let season_index = next_value(self.series.watch_info.season);
                        self.series.set_season(season_index)?;
                    }
                }
            }
            _ => (),
        }

        Ok(())
    }

    fn process_input_dialog_key(
        &mut self,
        state: &'a CommonState,
        log: &mut StatusLog,
        key: Key,
    ) -> Result<()> {
        match &mut self.status_bar_state {
            StatusBarState::Log => Ok(()),
            StatusBarState::CommandPrompt(prompt) => {
                use component::command_prompt::PromptResult;

                match prompt.process_key(key) {
                    Ok(PromptResult::Command(command)) => {
                        self.status_bar_state.reset();
                        self.last_used_command = Some(command.clone());
                        self.process_command(command, state, log)
                    }
                    Ok(PromptResult::Done) => {
                        self.status_bar_state.reset();
                        Ok(())
                    }
                    Ok(PromptResult::NotDone) => Ok(()),
                    // We need to set the status bar state back before propagating errors,
                    // otherwise we'll be stuck in the prompt
                    Err(err) => {
                        self.status_bar_state.reset();
                        Err(err)
                    }
                }
            }
        }
    }

    fn process_command(
        &mut self,
        command: Command,
        cstate: &CommonState,
        log: &mut StatusLog,
    ) -> Result<()> {
        let remote = cstate.remote.as_ref();

        match command {
            Command::SyncFromRemote => {
                let season = &mut self.series.season;

                log.capture_status("Syncing entry from remote", || {
                    season.tracker.force_sync_changes_from_remote(remote)
                });

                Ok(())
            }
            Command::SyncToRemote => {
                let season = &mut self.series.season;

                log.capture_status("Syncing entry to remote", || {
                    season.tracker.force_sync_changes_to_remote(remote)
                });

                Ok(())
            }
            Command::Status(status) => {
                let season = &mut self.series.season;
                let entry = &mut season.tracker.entry;

                entry.set_status(status);

                log.capture_status(format!("Setting series status to \"{}\"", status), || {
                    season.tracker.sync_changes_to_remote(remote)
                });

                Ok(())
            }
            Command::Progress(direction) => {
                use component::command_prompt::ProgressDirection;

                let tracker = &mut self.series.season.tracker;

                match direction {
                    ProgressDirection::Forwards => {
                        log.capture_status("Forcing forward watch progress", || {
                            tracker.episode_completed(remote, &cstate.config)
                        });
                    }
                    ProgressDirection::Backwards => {
                        log.capture_status("Forcing backwards watch progress", || {
                            tracker.episode_regressed(remote)
                        });
                    }
                }

                Ok(())
            }
            Command::Score(raw_score) => {
                let score = match cstate.remote.parse_score(&raw_score) {
                    Some(score) if score == 0 => None,
                    Some(score) => Some(score),
                    None => {
                        log.push(LogItem::failed("Parsing score", None));
                        return Ok(());
                    }
                };

                let tracker = &mut self.series.season.tracker;
                tracker.entry.set_score(score);

                log.capture_status("Setting score", || {
                    tracker.sync_changes_to_remote(cstate.remote.as_ref())
                });

                Ok(())
            }
            Command::PlayerArgs(args) => {
                let name = self.series.watch_info.name.as_ref();

                log.capture_status("Saving player args for series", || {
                    SeriesPlayerArgs::new(args).save(name)
                });

                Ok(())
            }
        }
    }

    fn process_tick(&mut self, state: &'a CommonState, log: &mut StatusLog) -> Result<()> {
        self.series.season.process_tick(state, log)
    }

    fn is_idle(&self) -> bool {
        self.series.season.watch_state == WatchState::Idle
    }
}

#[derive(Copy, Clone)]
enum Selection {
    Series,
    Season,
}

impl Selection {
    fn opposite(self) -> Selection {
        match self {
            Selection::Series => Selection::Season,
            Selection::Season => Selection::Series,
        }
    }

    fn set_opposite(&mut self) {
        *self = self.opposite();
    }
}

enum StatusBarState<'a> {
    Log,
    CommandPrompt(CommandPrompt<'a>),
}

impl<'a> StatusBarState<'a> {
    fn set_to_command_prompt(&mut self) {
        *self = StatusBarState::CommandPrompt(CommandPrompt::new());
    }

    fn reset(&mut self) {
        *self = StatusBarState::default();
    }

    fn in_input_dialog(&self) -> bool {
        match self {
            StatusBarState::Log => false,
            StatusBarState::CommandPrompt(_) => true,
        }
    }
}

impl<'a> Default for StatusBarState<'a> {
    fn default() -> StatusBarState<'a> {
        StatusBarState::Log
    }
}

struct SeriesState<'a> {
    watch_info: CurrentWatchInfo,
    season: SeasonState<'a>,
    seasons: SeasonInfoList,
    num_seasons: usize,
    is_last_watched: bool,
}

impl<'a> SeriesState<'a> {
    fn new(
        state: &'a CommonState,
        watch_info: CurrentWatchInfo,
        is_last_watched: bool,
    ) -> Result<SeriesState<'a>> {
        let remote = state.remote.as_ref();
        let name = &watch_info.name;

        let episodes = super::get_episodes(&state.args, name, &state.config, is_last_watched)?;
        let seasons = super::get_season_list(name, remote, &episodes)?;
        let num_seasons = seasons.len();
        let series = Series::from_season_list(&seasons, watch_info.season, episodes)?;
        let season = SeasonState::new(name, series)?;

        Ok(SeriesState {
            watch_info,
            season,
            seasons,
            num_seasons,
            is_last_watched,
        })
    }

    /// Loads the season specified by `season_num` and points `season` to it.
    fn set_season(&mut self, season_num: usize) -> Result<()> {
        if !self.seasons.has(season_num) {
            return Ok(());
        }

        let episodes = self.season.series.episodes.clone();
        let series = Series::from_season_list(&self.seasons, season_num, episodes)?;

        self.season = SeasonState::new(&self.watch_info.name, series)?;
        self.watch_info.season = season_num;

        Ok(())
    }

    /// Sets the current series as the last watched one if it isn't already.
    fn set_as_last_watched(&mut self, log: &mut StatusLog) {
        if self.is_last_watched {
            return;
        }

        log.capture_status("Marking as the last watched series", || {
            self.watch_info.save(None)
        });

        self.is_last_watched = true;
    }
}

struct SeasonState<'a> {
    series: Series<'a>,
    tracker: SeriesTracker<'a>,
    watch_state: WatchState,
}

impl<'a> SeasonState<'a> {
    fn new<S>(name: S, series: Series<'a>) -> Result<SeasonState<'a>>
    where
        S: Into<String>,
    {
        let tracker = SeriesTracker::init(series.info.clone(), name)?;

        Ok(SeasonState {
            series,
            tracker,
            watch_state: WatchState::Idle,
        })
    }

    fn play_next_episode_async(&mut self, state: &CommonState) -> Result<()> {
        let remote = state.remote.as_ref();
        let config = &state.config;

        self.tracker.begin_watching(remote, config)?;
        let next_ep = self.tracker.entry.watched_eps() + 1;

        let episode = self
            .series
            .get_episode(next_ep)
            .context(err::EpisodeNotFound { episode: next_ep })?;

        let child = super::prepare_episode_cmd(&self.tracker.name, &state.config, episode)?
            .spawn()
            .context(err::FailedToPlayEpisode { episode: next_ep })?;

        let progress_time = {
            let secs_must_watch =
                (self.series.info.episode_length as f32 * config.episode.pcnt_must_watch) * 60.0;
            let time_must_watch = Duration::seconds(secs_must_watch as i64);

            Utc::now() + time_must_watch
        };

        self.watch_state = WatchState::Watching(progress_time, child);

        Ok(())
    }

    fn process_tick(&mut self, state: &'a CommonState, log: &mut StatusLog) -> Result<()> {
        match &mut self.watch_state {
            WatchState::Idle => (),
            WatchState::Watching(_, child) => {
                let status = match child.try_wait().context(err::IO)? {
                    Some(status) => status,
                    None => return Ok(()),
                };

                // The watch state should be set to idle immediately to avoid a potential infinite loop.
                let progress_time = match mem::replace(&mut self.watch_state, WatchState::Idle) {
                    WatchState::Watching(progress_time, _) => progress_time,
                    WatchState::Idle => unreachable!(),
                };

                if !status.success() {
                    log.push("Player did not exit properly");
                    return Ok(());
                }

                if Utc::now() >= progress_time {
                    log.capture_status("Marking episode as completed", || {
                        self.tracker
                            .episode_completed(state.remote.as_ref(), &state.config)
                    });
                } else {
                    log.push("Not marking episode as completed");
                }
            }
        }

        Ok(())
    }
}

type ProgressTime = DateTime<Utc>;

enum WatchState {
    Idle,
    Watching(ProgressTime, process::Child),
}

impl PartialEq for WatchState {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (WatchState::Idle, WatchState::Idle) => true,
            (WatchState::Watching(_, _), WatchState::Watching(_, _)) => true,
            _ => false,
        }
    }
}
<|endoftext|>