ra_ap_ide_assists 0.0.258

Code assists for rust-analyzer.
Documentation
use syntax::{ast, ast::Radix, AstToken};

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

const MIN_NUMBER_OF_DIGITS_TO_FORMAT: usize = 5;

// Assist: reformat_number_literal
//
// Adds or removes separators from integer literal.
//
// ```
// const _: i32 = 1012345$0;
// ```
// ->
// ```
// const _: i32 = 1_012_345;
// ```
pub(crate) fn reformat_number_literal(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
    let literal = ctx.find_node_at_offset::<ast::Literal>()?;
    let literal = match literal.kind() {
        ast::LiteralKind::IntNumber(it) => it,
        _ => return None,
    };

    let text = literal.text();
    if text.contains('_') {
        return remove_separators(acc, literal);
    }

    let (prefix, value, suffix) = literal.split_into_parts();
    if value.len() < MIN_NUMBER_OF_DIGITS_TO_FORMAT {
        return None;
    }

    let radix = literal.radix();
    let mut converted = prefix.to_owned();
    converted.push_str(&add_group_separators(value, group_size(radix)));
    converted.push_str(suffix);

    let group_id = GroupLabel("Reformat number literal".into());
    let label = format!("Convert {literal} to {converted}");
    let range = literal.syntax().text_range();
    acc.add_group(
        &group_id,
        AssistId("reformat_number_literal", AssistKind::RefactorInline),
        label,
        range,
        |builder| builder.replace(range, converted),
    )
}

fn remove_separators(acc: &mut Assists, literal: ast::IntNumber) -> Option<()> {
    let group_id = GroupLabel("Reformat number literal".into());
    let range = literal.syntax().text_range();
    acc.add_group(
        &group_id,
        AssistId("reformat_number_literal", AssistKind::RefactorInline),
        "Remove digit separators",
        range,
        |builder| builder.replace(range, literal.text().replace('_', "")),
    )
}

const fn group_size(r: Radix) -> usize {
    match r {
        Radix::Binary => 4,
        Radix::Octal => 3,
        Radix::Decimal => 3,
        Radix::Hexadecimal => 4,
    }
}

fn add_group_separators(s: &str, group_size: usize) -> String {
    let mut chars = Vec::new();
    for (i, ch) in s.chars().filter(|&ch| ch != '_').rev().enumerate() {
        if i > 0 && i % group_size == 0 {
            chars.push('_');
        }
        chars.push(ch);
    }

    chars.into_iter().rev().collect()
}

#[cfg(test)]
mod tests {
    use crate::tests::{check_assist_by_label, check_assist_not_applicable, check_assist_target};

    use super::*;

    #[test]
    fn group_separators() {
        let cases = vec![
            ("", 4, ""),
            ("1", 4, "1"),
            ("12", 4, "12"),
            ("123", 4, "123"),
            ("1234", 4, "1234"),
            ("12345", 4, "1_2345"),
            ("123456", 4, "12_3456"),
            ("1234567", 4, "123_4567"),
            ("12345678", 4, "1234_5678"),
            ("123456789", 4, "1_2345_6789"),
            ("1234567890", 4, "12_3456_7890"),
            ("1_2_3_4_5_6_7_8_9_0_", 4, "12_3456_7890"),
            ("1234567890", 3, "1_234_567_890"),
            ("1234567890", 2, "12_34_56_78_90"),
            ("1234567890", 1, "1_2_3_4_5_6_7_8_9_0"),
        ];

        for case in cases {
            let (input, group_size, expected) = case;
            assert_eq!(add_group_separators(input, group_size), expected)
        }
    }

    #[test]
    fn good_targets() {
        let cases = vec![
            ("const _: i32 = 0b11111$0", "0b11111"),
            ("const _: i32 = 0o77777$0;", "0o77777"),
            ("const _: i32 = 10000$0;", "10000"),
            ("const _: i32 = 0xFFFFF$0;", "0xFFFFF"),
            ("const _: i32 = 10000i32$0;", "10000i32"),
            ("const _: i32 = 0b_10_0i32$0;", "0b_10_0i32"),
        ];

        for case in cases {
            check_assist_target(reformat_number_literal, case.0, case.1);
        }
    }

    #[test]
    fn bad_targets() {
        let cases = vec![
            "const _: i32 = 0b111$0",
            "const _: i32 = 0b1111$0",
            "const _: i32 = 0o77$0;",
            "const _: i32 = 0o777$0;",
            "const _: i32 = 10$0;",
            "const _: i32 = 999$0;",
            "const _: i32 = 0xFF$0;",
            "const _: i32 = 0xFFFF$0;",
        ];

        for case in cases {
            check_assist_not_applicable(reformat_number_literal, case);
        }
    }

    #[test]
    fn labels() {
        let cases = vec![
            ("const _: i32 = 10000$0", "const _: i32 = 10_000", "Convert 10000 to 10_000"),
            (
                "const _: i32 = 0xFF0000$0;",
                "const _: i32 = 0xFF_0000;",
                "Convert 0xFF0000 to 0xFF_0000",
            ),
            (
                "const _: i32 = 0b11111111$0;",
                "const _: i32 = 0b1111_1111;",
                "Convert 0b11111111 to 0b1111_1111",
            ),
            (
                "const _: i32 = 0o377211$0;",
                "const _: i32 = 0o377_211;",
                "Convert 0o377211 to 0o377_211",
            ),
            (
                "const _: i32 = 10000i32$0;",
                "const _: i32 = 10_000i32;",
                "Convert 10000i32 to 10_000i32",
            ),
            ("const _: i32 = 1_0_0_0_i32$0;", "const _: i32 = 1000i32;", "Remove digit separators"),
        ];

        for case in cases {
            let (before, after, label) = case;
            check_assist_by_label(reformat_number_literal, before, after, label);
        }
    }
}