//! This module defines multiple types of inlay hints and their visibility

use hir::{Adt, HirDisplay, Semantics, Type};
use ra_ide_db::RootDatabase;
use ra_prof::profile;
use ra_syntax::{
    ast::{self, ArgListOwner, AstNode, TypeAscriptionOwner},
    match_ast, Direction, NodeOrToken, SmolStr, SyntaxKind, TextRange,
};

use crate::{FileId, FunctionSignature};

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InlayHintsConfig {
    pub type_hints: bool,
    pub parameter_hints: bool,
    pub chaining_hints: bool,
    pub max_length: Option<usize>,
}

impl Default for InlayHintsConfig {
    fn default() -> Self {
        Self { type_hints: true, parameter_hints: true, chaining_hints: true, max_length: None }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum InlayKind {
    TypeHint,
    ParameterHint,
    ChainingHint,
}

#[derive(Debug)]
pub struct InlayHint {
    pub range: TextRange,
    pub kind: InlayKind,
    pub label: SmolStr,
}

pub(crate) fn inlay_hints(
    db: &RootDatabase,
    file_id: FileId,
    config: &InlayHintsConfig,
) -> Vec<InlayHint> {
    let _p = profile("inlay_hints");
    let sema = Semantics::new(db);
    let file = sema.parse(file_id);

    let mut res = Vec::new();
    for node in file.syntax().descendants() {
        if let Some(expr) = ast::Expr::cast(node.clone()) {
            get_chaining_hints(&mut res, &sema, config, expr);
        }

        match_ast! {
            match node {
                ast::CallExpr(it) => { get_param_name_hints(&mut res, &sema, config, ast::Expr::from(it)); },
                ast::MethodCallExpr(it) => { get_param_name_hints(&mut res, &sema, config, ast::Expr::from(it)); },
                ast::BindPat(it) => { get_bind_pat_hints(&mut res, &sema, config, it); },
                _ => (),
            }
        }
    }
    res
}

fn get_chaining_hints(
    acc: &mut Vec<InlayHint>,
    sema: &Semantics<RootDatabase>,
    config: &InlayHintsConfig,
    expr: ast::Expr,
) -> Option<()> {
    if !config.chaining_hints {
        return None;
    }

    if matches!(expr, ast::Expr::RecordLit(_)) {
        return None;
    }

    let mut tokens = expr
        .syntax()
        .siblings_with_tokens(Direction::Next)
        .filter_map(NodeOrToken::into_token)
        .filter(|t| match t.kind() {
            SyntaxKind::WHITESPACE if !t.text().contains('\n') => false,
            SyntaxKind::COMMENT => false,
            _ => true,
        });

    // Chaining can be defined as an expression whose next sibling tokens are newline and dot
    // Ignoring extra whitespace and comments
    let next = tokens.next()?.kind();
    let next_next = tokens.next()?.kind();
    if next == SyntaxKind::WHITESPACE && next_next == SyntaxKind::DOT {
        let ty = sema.type_of_expr(&expr)?;
        if ty.is_unknown() {
            return None;
        }
        if matches!(expr, ast::Expr::PathExpr(_)) {
            if let Some(Adt::Struct(st)) = ty.as_adt() {
                if st.fields(sema.db).is_empty() {
                    return None;
                }
            }
        }
        let label = ty.display_truncated(sema.db, config.max_length).to_string();
        acc.push(InlayHint {
            range: expr.syntax().text_range(),
            kind: InlayKind::ChainingHint,
            label: label.into(),
        });
    }
    Some(())
}

fn get_param_name_hints(
    acc: &mut Vec<InlayHint>,
    sema: &Semantics<RootDatabase>,
    config: &InlayHintsConfig,
    expr: ast::Expr,
) -> Option<()> {
    if !config.parameter_hints {
        return None;
    }

    let args = match &expr {
        ast::Expr::CallExpr(expr) => expr.arg_list()?.args(),
        ast::Expr::MethodCallExpr(expr) => expr.arg_list()?.args(),
        _ => return None,
    };
    let args_count = args.clone().count();

    let fn_signature = get_fn_signature(sema, &expr)?;
    let n_params_to_skip =
        if fn_signature.has_self_param && fn_signature.parameter_names.len() > args_count {
            1
        } else {
            0
        };
    let hints = fn_signature
        .parameter_names
        .iter()
        .skip(n_params_to_skip)
        .zip(args)
        .filter(|(param, arg)| should_show_param_hint(&fn_signature, param, &arg))
        .map(|(param_name, arg)| InlayHint {
            range: arg.syntax().text_range(),
            kind: InlayKind::ParameterHint,
            label: param_name.into(),
        });

    acc.extend(hints);
    Some(())
}

fn get_bind_pat_hints(
    acc: &mut Vec<InlayHint>,
    sema: &Semantics<RootDatabase>,
    config: &InlayHintsConfig,
    pat: ast::BindPat,
) -> Option<()> {
    if !config.type_hints {
        return None;
    }

    let ty = sema.type_of_pat(&pat.clone().into())?;

    if should_not_display_type_hint(sema.db, &pat, &ty) {
        return None;
    }

    acc.push(InlayHint {
        range: pat.syntax().text_range(),
        kind: InlayKind::TypeHint,
        label: ty.display_truncated(sema.db, config.max_length).to_string().into(),
    });
    Some(())
}

fn pat_is_enum_variant(db: &RootDatabase, bind_pat: &ast::BindPat, pat_ty: &Type) -> bool {
    if let Some(Adt::Enum(enum_data)) = pat_ty.as_adt() {
        let pat_text = bind_pat.syntax().to_string();
        enum_data
            .variants(db)
            .into_iter()
            .map(|variant| variant.name(db).to_string())
            .any(|enum_name| enum_name == pat_text)
    } else {
        false
    }
}

fn should_not_display_type_hint(db: &RootDatabase, bind_pat: &ast::BindPat, pat_ty: &Type) -> bool {
    if pat_ty.is_unknown() {
        return true;
    }

    if let Some(Adt::Struct(s)) = pat_ty.as_adt() {
        if s.fields(db).is_empty() && s.name(db).to_string() == bind_pat.syntax().to_string() {
            return true;
        }
    }

    for node in bind_pat.syntax().ancestors() {
        match_ast! {
            match node {
                ast::LetStmt(it) => {
                    return it.ascribed_type().is_some()
                },
                ast::Param(it) => {
                    return it.ascribed_type().is_some()
                },
                ast::MatchArm(_it) => {
                    return pat_is_enum_variant(db, bind_pat, pat_ty);
                },
                ast::IfExpr(it) => {
                    return it.condition().and_then(|condition| condition.pat()).is_some()
                        && pat_is_enum_variant(db, bind_pat, pat_ty);
                },
                ast::WhileExpr(it) => {
                    return it.condition().and_then(|condition| condition.pat()).is_some()
                        && pat_is_enum_variant(db, bind_pat, pat_ty);
                },
                _ => (),
            }
        }
    }
    false
}

fn should_show_param_hint(
    fn_signature: &FunctionSignature,
    param_name: &str,
    argument: &ast::Expr,
) -> bool {
    if param_name.is_empty()
        || is_argument_similar_to_param(argument, param_name)
        || Some(param_name.trim_start_matches('_'))
            == fn_signature.name.as_ref().map(|s| s.trim_start_matches('_'))
    {
        return false;
    }

    let parameters_len = if fn_signature.has_self_param {
        fn_signature.parameters.len() - 1
    } else {
        fn_signature.parameters.len()
    };

    // avoid displaying hints for common functions like map, filter, etc.
    // or other obvious words used in std
    parameters_len != 1 || !is_obvious_param(param_name)
}

fn is_argument_similar_to_param(argument: &ast::Expr, param_name: &str) -> bool {
    let argument_string = remove_ref(argument.clone()).syntax().to_string();
    let param_name = param_name.trim_start_matches('_');
    let argument_string = argument_string.trim_start_matches('_');
    argument_string.starts_with(&param_name) || argument_string.ends_with(&param_name)
}

fn remove_ref(expr: ast::Expr) -> ast::Expr {
    if let ast::Expr::RefExpr(ref_expr) = &expr {
        if let Some(inner) = ref_expr.expr() {
            return inner;
        }
    }
    expr
}

fn is_obvious_param(param_name: &str) -> bool {
    let is_obvious_param_name = match param_name {
        "predicate" | "value" | "pat" | "rhs" | "other" => true,
        _ => false,
    };
    param_name.len() == 1 || is_obvious_param_name
}

fn get_fn_signature(sema: &Semantics<RootDatabase>, expr: &ast::Expr) -> Option<FunctionSignature> {
    match expr {
        ast::Expr::CallExpr(expr) => {
            // FIXME: Type::as_callable is broken for closures
            let callable_def = sema.type_of_expr(&expr.expr()?)?.as_callable()?;
            match callable_def {
                hir::CallableDef::FunctionId(it) => {
                    Some(FunctionSignature::from_hir(sema.db, it.into()))
                }
                hir::CallableDef::StructId(it) => {
                    FunctionSignature::from_struct(sema.db, it.into())
                }
                hir::CallableDef::EnumVariantId(it) => {
                    FunctionSignature::from_enum_variant(sema.db, it.into())
                }
            }
        }
        ast::Expr::MethodCallExpr(expr) => {
            let fn_def = sema.resolve_method_call(&expr)?;
            Some(FunctionSignature::from_hir(sema.db, fn_def))
        }
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use crate::inlay_hints::InlayHintsConfig;
    use insta::assert_debug_snapshot;

    use crate::mock_analysis::single_file;

    #[test]
    fn param_hints_only() {
        let (analysis, file_id) = single_file(
            r#"
            fn foo(a: i32, b: i32) -> i32 { a + b }
            fn main() {
                let _x = foo(4, 4);
            }"#,
        );
        assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsConfig{ parameter_hints: true, type_hints: false, chaining_hints: false, max_length: None}).unwrap(), @r###"
        [
            InlayHint {
                range: 106..107,
                kind: ParameterHint,
                label: "a",
            },
            InlayHint {
                range: 109..110,
                kind: ParameterHint,
                label: "b",
            },
        ]"###);
    }

    #[test]
    fn hints_disabled() {
        let (analysis, file_id) = single_file(
            r#"
            fn foo(a: i32, b: i32) -> i32 { a + b }
            fn main() {
                let _x = foo(4, 4);
            }"#,
        );
        assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsConfig{ type_hints: false, parameter_hints: false, chaining_hints: false, max_length: None}).unwrap(), @r###"[]"###);
    }

    #[test]
    fn type_hints_only() {
        let (analysis, file_id) = single_file(
            r#"
            fn foo(a: i32, b: i32) -> i32 { a + b }
            fn main() {
                let _x = foo(4, 4);
            }"#,
        );
        assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsConfig{ type_hints: true, parameter_hints: false, chaining_hints: false, max_length: None}).unwrap(), @r###"
        [
            InlayHint {
                range: 97..99,
                kind: TypeHint,
                label: "i32",
            },
        ]"###);
    }
    #[test]
    fn default_generic_types_should_not_be_displayed() {
        let (analysis, file_id) = single_file(
            r#"
struct Test<K, T = u8> {
    k: K,
    t: T,
}

fn main() {
    let zz = Test { t: 23, k: 33 };
    let zz_ref = &zz;
}"#,
        );

        assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsConfig::default()).unwrap(), @r###"
        [
            InlayHint {
                range: 69..71,
                kind: TypeHint,
                label: "Test<i32>",
            },
            InlayHint {
                range: 105..111,
                kind: TypeHint,
                label: "&Test<i32>",
            },
        ]
        "###
        );
    }

    #[test]
    fn let_statement() {
        let (analysis, file_id) = single_file(
            r#"
#[derive(PartialEq)]
enum CustomOption<T> {
    None,
    Some(T),
}

#[derive(PartialEq)]
struct Test {
    a: CustomOption<u32>,
    b: u8,
}

fn main() {
    struct InnerStruct {}

    let test = 54;
    let test: i32 = 33;
    let mut test = 33;
    let _ = 22;
    let test = "test";
    let test = InnerStruct {};

    let test = vec![222];
    let test: Vec<_> = (0..3).collect();
    let test = (0..3).collect::<Vec<i128>>();
    let test = (0..3).collect::<Vec<_>>();

    let mut test = Vec::new();
    test.push(333);

    let test = (42, 'a');
    let (a, (b, c, (d, e), f)) = (2, (3, 4, (6.6, 7.7), 5));
    let &x = &92;
}"#,
        );

        assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsConfig::default()).unwrap(), @r###"
        [
            InlayHint {
                range: 193..197,
                kind: TypeHint,
                label: "i32",
            },
            InlayHint {
                range: 236..244,
                kind: TypeHint,
                label: "i32",
            },
            InlayHint {
                range: 275..279,
                kind: TypeHint,
                label: "&str",
            },
            InlayHint {
                range: 539..543,
                kind: TypeHint,
                label: "(i32, char)",
            },
            InlayHint {
                range: 566..567,
                kind: TypeHint,
                label: "i32",
            },
            InlayHint {
                range: 570..571,
                kind: TypeHint,
                label: "i32",
            },
            InlayHint {
                range: 573..574,
                kind: TypeHint,
                label: "i32",
            },
            InlayHint {
                range: 577..578,
                kind: TypeHint,
                label: "f64",
            },
            InlayHint {
                range: 580..581,
                kind: TypeHint,
                label: "f64",
            },
            InlayHint {
                range: 584..585,
                kind: TypeHint,
                label: "i32",
            },
            InlayHint {
                range: 627..628,
                kind: TypeHint,
                label: "i32",
            },
        ]
        "###
        );
    }

    #[test]
    fn closure_parameters() {
        let (analysis, file_id) = single_file(
            r#"
fn main() {
    let mut start = 0;
    (0..2).for_each(|increment| {
        start += increment;
    });

    let multiply = |a, b, c, d| a * b * c * d;
    let _: i32 = multiply(1, 2, 3, 4);
    let multiply_ref = &multiply;

    let return_42 = || 42;
}"#,
        );

        assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsConfig::default()).unwrap(), @r###"
        [
            InlayHint {
                range: 21..30,
                kind: TypeHint,
                label: "i32",
            },
            InlayHint {
                range: 57..66,
                kind: TypeHint,
                label: "i32",
            },
            InlayHint {
                range: 115..123,
                kind: TypeHint,
                label: "|…| -> i32",
            },
            InlayHint {
                range: 127..128,
                kind: TypeHint,
                label: "i32",
            },
            InlayHint {
                range: 130..131,
                kind: TypeHint,
                label: "i32",
            },
            InlayHint {
                range: 133..134,
                kind: TypeHint,
                label: "i32",
            },
            InlayHint {
                range: 136..137,
                kind: TypeHint,
                label: "i32",
            },
            InlayHint {
                range: 201..213,
                kind: TypeHint,
                label: "&|…| -> i32",
            },
            InlayHint {
                range: 236..245,
                kind: TypeHint,
                label: "|| -> i32",
            },
        ]
        "###
        );
    }

    #[test]
    fn for_expression() {
        let (analysis, file_id) = single_file(
            r#"
fn main() {
    let mut start = 0;
    for increment in 0..2 {
        start += increment;
    }
}"#,
        );

        assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsConfig::default()).unwrap(), @r###"
        [
            InlayHint {
                range: 21..30,
                kind: TypeHint,
                label: "i32",
            },
            InlayHint {
                range: 44..53,
                kind: TypeHint,
                label: "i32",
            },
        ]
        "###
        );
    }

    #[test]
    fn if_expr() {
        let (analysis, file_id) = single_file(
            r#"
#[derive(PartialEq)]
enum CustomOption<T> {
    None,
    Some(T),
}

#[derive(PartialEq)]
struct Test {
    a: CustomOption<u32>,
    b: u8,
}

use CustomOption::*;

fn main() {
    let test = Some(Test { a: Some(3), b: 1 });
    if let None = &test {};
    if let test = &test {};
    if let Some(test) = &test {};
    if let Some(Test { a, b }) = &test {};
    if let Some(Test { a: x, b: y }) = &test {};
    if let Some(Test { a: Some(x), b: y }) = &test {};
    if let Some(Test { a: None, b: y }) = &test {};
    if let Some(Test { b: y, .. }) = &test {};

    if test == None {}
}"#,
        );

        assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsConfig::default()).unwrap(), @r###"
        [
            InlayHint {
                range: 188..192,
                kind: TypeHint,
                label: "CustomOption<Test>",
            },
            InlayHint {
                range: 267..271,
                kind: TypeHint,
                label: "&CustomOption<Test>",
            },
            InlayHint {
                range: 300..304,
                kind: TypeHint,
                label: "&Test",
            },
            InlayHint {
                range: 341..342,
                kind: TypeHint,
                label: "&CustomOption<u32>",
            },
            InlayHint {
                range: 344..345,
                kind: TypeHint,
                label: "&u8",
            },
            InlayHint {
                range: 387..388,
                kind: TypeHint,
                label: "&CustomOption<u32>",
            },
            InlayHint {
                range: 393..394,
                kind: TypeHint,
                label: "&u8",
            },
            InlayHint {
                range: 441..442,
                kind: TypeHint,
                label: "&u32",
            },
            InlayHint {
                range: 448..449,
                kind: TypeHint,
                label: "&u8",
            },
            InlayHint {
                range: 500..501,
                kind: TypeHint,
                label: "&u8",
            },
            InlayHint {
                range: 543..544,
                kind: TypeHint,
                label: "&u8",
            },
        ]
        "###
        );
    }

    #[test]
    fn while_expr() {
        let (analysis, file_id) = single_file(
            r#"
#[derive(PartialEq)]
enum CustomOption<T> {
    None,
    Some(T),
}

#[derive(PartialEq)]
struct Test {
    a: CustomOption<u32>,
    b: u8,
}

use CustomOption::*;

fn main() {
    let test = Some(Test { a: Some(3), b: 1 });
    while let None = &test {};
    while let test = &test {};
    while let Some(test) = &test {};
    while let Some(Test { a, b }) = &test {};
    while let Some(Test { a: x, b: y }) = &test {};
    while let Some(Test { a: Some(x), b: y }) = &test {};
    while let Some(Test { a: None, b: y }) = &test {};
    while let Some(Test { b: y, .. }) = &test {};

    while test == None {}
}"#,
        );

        assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsConfig::default()).unwrap(), @r###"
        [
            InlayHint {
                range: 188..192,
                kind: TypeHint,
                label: "CustomOption<Test>",
            },
            InlayHint {
                range: 273..277,
                kind: TypeHint,
                label: "&CustomOption<Test>",
            },
            InlayHint {
                range: 309..313,
                kind: TypeHint,
                label: "&Test",
            },
            InlayHint {
                range: 353..354,
                kind: TypeHint,
                label: "&CustomOption<u32>",
            },
            InlayHint {
                range: 356..357,
                kind: TypeHint,
                label: "&u8",
            },
            InlayHint {
                range: 402..403,
                kind: TypeHint,
                label: "&CustomOption<u32>",
            },
            InlayHint {
                range: 408..409,
                kind: TypeHint,
                label: "&u8",
            },
            InlayHint {
                range: 459..460,
                kind: TypeHint,
                label: "&u32",
            },
            InlayHint {
                range: 466..467,
                kind: TypeHint,
                label: "&u8",
            },
            InlayHint {
                range: 521..522,
                kind: TypeHint,
                label: "&u8",
            },
            InlayHint {
                range: 567..568,
                kind: TypeHint,
                label: "&u8",
            },
        ]
        "###
        );
    }

    #[test]
    fn match_arm_list() {
        let (analysis, file_id) = single_file(
            r#"
#[derive(PartialEq)]
enum CustomOption<T> {
    None,
    Some(T),
}

#[derive(PartialEq)]
struct Test {
    a: CustomOption<u32>,
    b: u8,
}

use CustomOption::*;

fn main() {
    match Some(Test { a: Some(3), b: 1 }) {
        None => (),
        test => (),
        Some(test) => (),
        Some(Test { a, b }) => (),
        Some(Test { a: x, b: y }) => (),
        Some(Test { a: Some(x), b: y }) => (),
        Some(Test { a: None, b: y }) => (),
        Some(Test { b: y, .. }) => (),
        _ => {}
    }
}"#,
        );

        assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsConfig::default()).unwrap(), @r###"
        [
            InlayHint {
                range: 252..256,
                kind: TypeHint,
                label: "CustomOption<Test>",
            },
            InlayHint {
                range: 277..281,
                kind: TypeHint,
                label: "Test",
            },
            InlayHint {
                range: 310..311,
                kind: TypeHint,
                label: "CustomOption<u32>",
            },
            InlayHint {
                range: 313..314,
                kind: TypeHint,
                label: "u8",
            },
            InlayHint {
                range: 348..349,
                kind: TypeHint,
                label: "CustomOption<u32>",
            },
            InlayHint {
                range: 354..355,
                kind: TypeHint,
                label: "u8",
            },
            InlayHint {
                range: 394..395,
                kind: TypeHint,
                label: "u32",
            },
            InlayHint {
                range: 401..402,
                kind: TypeHint,
                label: "u8",
            },
            InlayHint {
                range: 445..446,
                kind: TypeHint,
                label: "u8",
            },
            InlayHint {
                range: 480..481,
                kind: TypeHint,
                label: "u8",
            },
        ]
        "###
        );
    }

    #[test]
    fn hint_truncation() {
        let (analysis, file_id) = single_file(
            r#"
struct Smol<T>(T);

struct VeryLongOuterName<T>(T);

fn main() {
    let a = Smol(0u32);
    let b = VeryLongOuterName(0usize);
    let c = Smol(Smol(0u32))
}"#,
        );

        assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsConfig { max_length: Some(8), ..Default::default() }).unwrap(), @r###"
        [
            InlayHint {
                range: 74..75,
                kind: TypeHint,
                label: "Smol<u32>",
            },
            InlayHint {
                range: 98..99,
                kind: TypeHint,
                label: "VeryLongOuterName<…>",
            },
            InlayHint {
                range: 137..138,
                kind: TypeHint,
                label: "Smol<Smol<…>>",
            },
        ]
        "###
        );
    }

    #[test]
    fn function_call_parameter_hint() {
        let (analysis, file_id) = single_file(
            r#"
enum CustomOption<T> {
    None,
    Some(T),
}
use CustomOption::*;

struct FileId {}
struct SmolStr {}

impl From<&str> for SmolStr {
    fn from(_: &str) -> Self {
        unimplemented!()
    }
}

struct TextRange {}
struct SyntaxKind {}
struct NavigationTarget {}

struct Test {}

impl Test {
    fn method(&self, mut param: i32) -> i32 {
        param * 2
    }

    fn from_syntax(
        file_id: FileId,
        name: SmolStr,
        focus_range: CustomOption<TextRange>,
        full_range: TextRange,
        kind: SyntaxKind,
        docs: CustomOption<String>,
        description: CustomOption<String>,
    ) -> NavigationTarget {
        NavigationTarget {}
    }
}

fn test_func(mut foo: i32, bar: i32, msg: &str, _: i32, last: i32) -> i32 {
    foo + bar
}

fn main() {
    let not_literal = 1;
    let _: i32 = test_func(1, 2, "hello", 3, not_literal);
    let t: Test = Test {};
    t.method(123);
    Test::method(&t, 3456);

    Test::from_syntax(
        FileId {},
        "impl".into(),
        None,
        TextRange {},
        SyntaxKind {},
        None,
        None,
    );
}"#,
        );

        assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsConfig::default()).unwrap(), @r###"
        [
            InlayHint {
                range: 798..809,
                kind: TypeHint,
                label: "i32",
            },
            InlayHint {
                range: 842..843,
                kind: ParameterHint,
                label: "foo",
            },
            InlayHint {
                range: 845..846,
                kind: ParameterHint,
                label: "bar",
            },
            InlayHint {
                range: 848..855,
                kind: ParameterHint,
                label: "msg",
            },
            InlayHint {
                range: 860..871,
                kind: ParameterHint,
                label: "last",
            },
            InlayHint {
                range: 914..917,
                kind: ParameterHint,
                label: "param",
            },
            InlayHint {
                range: 937..939,
                kind: ParameterHint,
                label: "&self",
            },
            InlayHint {
                range: 941..945,
                kind: ParameterHint,
                label: "param",
            },
            InlayHint {
                range: 980..989,
                kind: ParameterHint,
                label: "file_id",
            },
            InlayHint {
                range: 999..1012,
                kind: ParameterHint,
                label: "name",
            },
            InlayHint {
                range: 1022..1026,
                kind: ParameterHint,
                label: "focus_range",
            },
            InlayHint {
                range: 1036..1048,
                kind: ParameterHint,
                label: "full_range",
            },
            InlayHint {
                range: 1058..1071,
                kind: ParameterHint,
                label: "kind",
            },
            InlayHint {
                range: 1081..1085,
                kind: ParameterHint,
                label: "docs",
            },
            InlayHint {
                range: 1095..1099,
                kind: ParameterHint,
                label: "description",
            },
        ]
        "###
        );
    }

    #[test]
    fn omitted_parameters_hints_heuristics() {
        let (analysis, file_id) = single_file(
            r#"
fn map(f: i32) {}
fn filter(predicate: i32) {}

struct TestVarContainer {
    test_var: i32,
}

struct Test {}

impl Test {
    fn map(self, f: i32) -> Self {
        self
    }

    fn filter(self, predicate: i32) -> Self {
        self
    }

    fn field(self, value: i32) -> Self {
        self
    }

    fn no_hints_expected(&self, _: i32, test_var: i32) {}

    fn frob(&self, frob: bool) {}
}

struct Param {}

fn different_order(param: &Param) {}
fn different_order_mut(param: &mut Param) {}
fn has_underscore(_param: bool) {}

fn twiddle(twiddle: bool) {}
fn doo(_doo: bool) {}

fn main() {
    let container: TestVarContainer = TestVarContainer { test_var: 42 };
    let test: Test = Test {};

    map(22);
    filter(33);

    let test_processed: Test = test.map(1).filter(2).field(3);

    let test_var: i32 = 55;
    test_processed.no_hints_expected(22, test_var);
    test_processed.no_hints_expected(33, container.test_var);
    test_processed.frob(false);

    twiddle(true);
    doo(true);

    let param_begin: Param = Param {};
    different_order(&param_begin);
    different_order(&mut param_begin);

    let param: bool = true;
    has_underscore(param);

    let a: f64 = 7.0;
    let b: f64 = 4.0;
    let _: f64 = a.div_euclid(b);
    let _: f64 = a.abs_sub(b);
}"#,
        );

        assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsConfig { max_length: Some(8), ..Default::default() }).unwrap(), @r###"
        []
        "###
        );
    }

    #[test]
    fn unit_structs_have_no_type_hints() {
        let (analysis, file_id) = single_file(
            r#"
enum CustomResult<T, E> {
    Ok(T),
    Err(E),
}
use CustomResult::*;

struct SyntheticSyntax;

fn main() {
    match Ok(()) {
        Ok(_) => (),
        Err(SyntheticSyntax) => (),
    }
}"#,
        );

        assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsConfig { max_length: Some(8), ..Default::default() }).unwrap(), @r###"
        []
        "###
        );
    }

    #[test]
    fn chaining_hints_ignore_comments() {
        let (analysis, file_id) = single_file(
            r#"
            struct A(B);
            impl A { fn into_b(self) -> B { self.0 } }
            struct B(C);
            impl B { fn into_c(self) -> C { self.0 } }
            struct C;

            fn main() {
                let c = A(B(C))
                    .into_b() // This is a comment
                    .into_c();
            }"#,
        );
        assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsConfig{ parameter_hints: false, type_hints: false, chaining_hints: true, max_length: None}).unwrap(), @r###"
        [
            InlayHint {
                range: 232..269,
                kind: ChainingHint,
                label: "B",
            },
            InlayHint {
                range: 232..239,
                kind: ChainingHint,
                label: "A",
            },
        ]"###);
    }

    #[test]
    fn chaining_hints_without_newlines() {
        let (analysis, file_id) = single_file(
            r#"
            struct A(B);
            impl A { fn into_b(self) -> B { self.0 } }
            struct B(C);
            impl B { fn into_c(self) -> C { self.0 } }
            struct C;

            fn main() {
                let c = A(B(C)).into_b().into_c();
            }"#,
        );
        assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsConfig{ parameter_hints: false, type_hints: false, chaining_hints: true, max_length: None}).unwrap(), @r###"[]"###);
    }

    #[test]
    fn struct_access_chaining_hints() {
        let (analysis, file_id) = single_file(
            r#"
            struct A { pub b: B }
            struct B { pub c: C }
            struct C(pub bool);
            struct D;

            impl D {
                fn foo(&self) -> i32 { 42 }
            }

            fn main() {
                let x = A { b: B { c: C(true) } }
                    .b
                    .c
                    .0;
                let x = D
                    .foo();
            }"#,
        );
        assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsConfig{ parameter_hints: false, type_hints: false, chaining_hints: true, max_length: None}).unwrap(), @r###"
        [
            InlayHint {
                range: 252..323,
                kind: ChainingHint,
                label: "C",
            },
            InlayHint {
                range: 252..300,
                kind: ChainingHint,
                label: "B",
            },
        ]
        "###);
    }

    #[test]
    fn generic_chaining_hints() {
        let (analysis, file_id) = single_file(
            r#"
            struct A<T>(T);
            struct B<T>(T);
            struct C<T>(T);
            struct X<T,R>(T, R);

            impl<T> A<T> {
                fn new(t: T) -> Self { A(t) }
                fn into_b(self) -> B<T> { B(self.0) }
            }
            impl<T> B<T> {
                fn into_c(self) -> C<T> { C(self.0) }
            }
            fn main() {
                let c = A::new(X(42, true))
                    .into_b()
                    .into_c();
            }"#,
        );
        assert_debug_snapshot!(analysis.inlay_hints(file_id, &InlayHintsConfig{ parameter_hints: false, type_hints: false, chaining_hints: true, max_length: None}).unwrap(), @r###"
        [
            InlayHint {
                range: 403..452,
                kind: ChainingHint,
                label: "B<X<i32, bool>>",
            },
            InlayHint {
                range: 403..422,
                kind: ChainingHint,
                label: "A<X<i32, bool>>",
            },
        ]"###);
    }
}