<commit_before>use syntax::ast::{edit::AstNodeEdit, AstNode, HasName, LetStmt, Pat};
use syntax::T;

use crate::{AssistContext, AssistId, AssistKind, Assists};

/// Gets a list of binders in a pattern, and whether they are mut.
fn binders_in_pat(pat: &Pat) -> Option<Vec<(String, bool)>> {
    use Pat::*;
    match pat {
        IdentPat(p) => {
            let ident = p.name()?.text().to_string();
            let ismut = p.ref_token().is_none() && p.mut_token().is_some();
            let mut res = vec![(ident, ismut)];
            if let Some(inner) = p.pat() {
                res.extend(binders_in_pat(&inner)?);
            }
            Some(res)
        }
        BoxPat(p) => p.pat().and_then(|p| binders_in_pat(&p)),
        RestPat(_) | LiteralPat(_) | PathPat(_) | WildcardPat(_) | ConstBlockPat(_) => Some(vec![]),
        OrPat(p) => {
            let mut v = vec![];
            for p in p.pats() {
                v.extend(binders_in_pat(&p)?);
            }
            Some(v)
        }
        ParenPat(p) => p.pat().and_then(|p| binders_in_pat(&p)),
        RangePat(p) => {
            let mut start = if let Some(st) = p.start() { binders_in_pat(&st)? } else { vec![] };
            let end = if let Some(st) = p.end() { binders_in_pat(&st)? } else { vec![] };
            start.extend(end);
            Some(start)
        }
        RecordPat(p) => {
            let mut v = vec![];
            for f in p.record_pat_field_list()?.fields() {
                let pat = f.pat()?;
                v.extend(binders_in_pat(&pat)?);
            }
            Some(v)
        }
        RefPat(p) => p.pat().and_then(|p| binders_in_pat(&p)),
        SlicePat(p) => {
            let mut v = vec![];
            for p in p.pats() {
                v.extend(binders_in_pat(&p)?);
            }
            Some(v)
        }
        TuplePat(p) => {
            let mut v = vec![];
            for p in p.fields() {
                v.extend(binders_in_pat(&p)?);
            }
            Some(v)
        }
        TupleStructPat(p) => {
            let mut v = vec![];
            for p in p.fields() {
                v.extend(binders_in_pat(&p)?);
            }
            Some(v)
        }
        // don't support macro pat yet
        MacroPat(_) => None,
    }
}

fn binders_to_str(binders: &[(String, bool)], addmut: bool) -> String {
    let vars = binders
        .iter()
        .map(
            |(ident, ismut)| {
                if *ismut && addmut {
                    format!("mut {}", ident)
                } else {
                    ident.to_string()
                }
            },
        )
        .collect::<Vec<_>>()
        .join(", ");
    if binders.is_empty() {
        String::from("{}")
    } else if binders.len() == 1 {
        vars
    } else {
        format!("({})", vars)
    }
}

// Assist: convert_let_else_to_match
//
// Converts let-else statement to let statement and match expression.
//
// ```
// fn main() {
//     let Ok(mut x) = f() else$0 { return };
// }
// ```
// ->
// ```
// fn main() {
//     let mut x = match f() {
//         Ok(x) => x,
//         _ => return,
//     };
// }
// ```
pub(crate) fn convert_let_else_to_match(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
    // should focus on else token to trigger
    let else_token = ctx.find_token_syntax_at_offset(T![else])?;
    let let_stmt = LetStmt::cast(else_token.parent()?.parent()?)?;
    let let_else_block = let_stmt.let_else()?.block_expr()?;
    let let_init = let_stmt.initializer()?;
    if let_stmt.ty().is_some() {
        // don't support let with type annotation
        return None;
    }
    let pat = let_stmt.pat()?;
    let binders = binders_in_pat(&pat)?;

    let target = let_stmt.syntax().text_range();
    acc.add(
        AssistId("convert_let_else_to_match", AssistKind::RefactorRewrite),
        "Convert let-else to let and match",
        target,
        |edit| {
            let indent_level = let_stmt.indent_level().0 as usize;
            let indent = "    ".repeat(indent_level);
            let indent1 = "    ".repeat(indent_level + 1);

            let binders_str = binders_to_str(&binders, false);
            let binders_str_mut = binders_to_str(&binders, true);

            let init_expr = let_init.syntax().text();
            let mut pat_no_mut = pat.syntax().text().to_string();
            // remove the mut from the pattern
            for (b, ismut) in binders.iter() {
                if *ismut {
                    pat_no_mut = pat_no_mut.replace(&format!("mut {b}"), b);
                }
            }

            let only_expr = let_else_block.statements().next().is_none();
            let branch2 = match &let_else_block.tail_expr() {
                Some(tail) if only_expr => format!("{},", tail.syntax().text()),
                _ => let_else_block.syntax().text().to_string(),
            };
            let replace = if binders.is_empty() {
                format!(
                    "match {init_expr} {{
{indent1}{pat_no_mut} => {binders_str}
{indent1}_ => {branch2}
{indent}}}"
                )
            } else {
                format!(
                    "let {binders_str_mut} = match {init_expr} {{
{indent1}{pat_no_mut} => {binders_str},<commit_msg>Avoid allocations<commit_after>use hir::Semantics;
use ide_db::RootDatabase;
use syntax::ast::{edit::AstNodeEdit, AstNode, HasName, LetStmt, Name, Pat};
use syntax::T;

use crate::{AssistContext, AssistId, AssistKind, Assists};

/// Gets a list of binders in a pattern, and whether they are mut.
fn binders_in_pat(
    acc: &mut Vec<(Name, bool)>,
    pat: &Pat,
    sem: &Semantics<RootDatabase>,
) -> Option<()> {
    use Pat::*;
    match pat {
        IdentPat(p) => {
            let ident = p.name()?;
            let ismut = p.ref_token().is_none() && p.mut_token().is_some();
            acc.push((ident, ismut));
            if let Some(inner) = p.pat() {
                binders_in_pat(acc, &inner, sem)?;
            }
            Some(())
        }
        BoxPat(p) => p.pat().and_then(|p| binders_in_pat(acc, &p, sem)),
        RestPat(_) | LiteralPat(_) | PathPat(_) | WildcardPat(_) | ConstBlockPat(_) => Some(()),
        OrPat(p) => {
            for p in p.pats() {
                binders_in_pat(acc, &p, sem)?;
            }
            Some(())
        }
        ParenPat(p) => p.pat().and_then(|p| binders_in_pat(acc, &p, sem)),
        RangePat(p) => {
            if let Some(st) = p.start() {
                binders_in_pat(acc, &st, sem)?
            }
            if let Some(ed) = p.end() {
                binders_in_pat(acc, &ed, sem)?
            }
            Some(())
        }
        RecordPat(p) => {
            for f in p.record_pat_field_list()?.fields() {
                let pat = f.pat()?;
                binders_in_pat(acc, &pat, sem)?;
            }
            Some(())
        }
        RefPat(p) => p.pat().and_then(|p| binders_in_pat(acc, &p, sem)),
        SlicePat(p) => {
            for p in p.pats() {
                binders_in_pat(acc, &p, sem)?;
            }
            Some(())
        }
        TuplePat(p) => {
            for p in p.fields() {
                binders_in_pat(acc, &p, sem)?;
            }
            Some(())
        }
        TupleStructPat(p) => {
            for p in p.fields() {
                binders_in_pat(acc, &p, sem)?;
            }
            Some(())
        }
        // don't support macro pat yet
        MacroPat(_) => None,
    }
}

fn binders_to_str(binders: &[(Name, bool)], addmut: bool) -> String {
    let vars = binders
        .iter()
        .map(
            |(ident, ismut)| {
                if *ismut && addmut {
                    format!("mut {}", ident)
                } else {
                    ident.to_string()
                }
            },
        )
        .collect::<Vec<_>>()
        .join(", ");
    if binders.is_empty() {
        String::from("{}")
    } else if binders.len() == 1 {
        vars
    } else {
        format!("({})", vars)
    }
}

// Assist: convert_let_else_to_match
//
// Converts let-else statement to let statement and match expression.
//
// ```
// fn main() {
//     let Ok(mut x) = f() else$0 { return };
// }
// ```
// ->
// ```
// fn main() {
//     let mut x = match f() {
//         Ok(x) => x,
//         _ => return,
//     };
// }
// ```
pub(crate) fn convert_let_else_to_match(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
    // should focus on else token to trigger
    let else_token = ctx.find_token_syntax_at_offset(T![else])?;
    let let_stmt = LetStmt::cast(else_token.parent()?.parent()?)?;
    let let_else_block = let_stmt.let_else()?.block_expr()?;
    let let_init = let_stmt.initializer()?;
    if let_stmt.ty().is_some() {
        // don't support let with type annotation
        return None;
    }
    let pat = let_stmt.pat()?;
    let mut binders = Vec::new();
    binders_in_pat(&mut binders, &pat, &ctx.sema)?;

    let target = let_stmt.syntax().text_range();
    acc.add(
        AssistId("convert_let_else_to_match", AssistKind::RefactorRewrite),
        "Convert let-else to let and match",
        target,
        |edit| {
            let indent_level = let_stmt.indent_level().0 as usize;
            let indent = "    ".repeat(indent_level);
            let indent1 = "    ".repeat(indent_level + 1);

            let binders_str = binders_to_str(&binders, false);
            let binders_str_mut = binders_to_str(&binders, true);

            let init_expr = let_init.syntax().text();
            let mut pat_no_mut = pat.syntax().text().to_string();
            // remove the mut from the pattern
            for (b, ismut) in binders.iter() {
                if *ismut {
                    pat_no_mut = pat_no_mut.replace(&format!("mut {b}"), &b.to_string());
                }
            }

            let only_expr = let_else_block.statements().next().is_none();
            let branch2 = match &let_else_block.tail_expr() {
                Some(tail) if only_expr => format!("{},", tail.syntax().text()),
                _ => let_else_block.syntax().text().to_string(),
            };
            let replace = if binders.is_empty() {
                format!(
                    "match {init_expr} {{
{indent1}{pat_no_mut} => {binders_str}
{indent1}_ => {branch2}
{indent}}}"
                )
            } else {
                format!(
                    "let {binders_str_mut} = match {init_expr} {{
{indent1}{pat_no_mut} => {binders_str},
{'fraction_non_alphanumeric': 0.1617744846584972, 'fraction_numerical': 0.0018998765080269783, 'mean_word_length': 1.6653164556962026, 'pattern_counts': {'":': 0, '<': 14, '<?xml version=': 0, '>': 60, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6887280', 'n_tokens_mistral': 3462, 'n_tokens_neox': 3419, 'n_words': 711}