Unnamed repository; edit this file 'description' to name the repository.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
; https://github.com/connorlay/tree-sitter-heex/blob/592e22292a367312c35e13de7fdb888f029981d6/queries/injections.scm
; directives are standalone tags like '<%= @x %>'
;
; partial_expression_values are elixir code that is part of an expression that
; spans multiple directive nodes, so they must be combined. For example:
;     <%= if true do %>
;       <p>hello, tree-sitter!</p>
;     <% end %>
((directive
    [
      (partial_expression_value)
      (ending_expression_value)
    ] @injection.content)
 (#set! injection.language "elixir")
 (#set! injection.include-children)
 (#set! injection.combined))

; Regular expression_values do not need to be combined
((directive (expression_value) @injection.content)
 (#set! injection.language "elixir"))

; expressions live within HTML tags, and do not need to be combined
;     <link href={ Routes.static_path(..) } />
((expression (expression_value) @injection.content)
 (#set! injection.language "elixir"))

((comment) @injection.content 
 (#set! injection.language "comment"))
='n103' href='#n103'>103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
use syntax::{AstToken, ast, ast::Radix};

use crate::{AssistContext, AssistId, Assists, GroupLabel, utils::add_group_separators};

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::refactor_inline("reformat_number_literal"),
        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::refactor_inline("reformat_number_literal"),
        "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,
    }
}

#[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);
        }
    }
}