diff --git a/crates/ra_assists/src/assist_ctx.rs b/crates/ra_assists/src/assist_ctx.rs index 62182cf03f22..1d8e58463dac 100644 --- a/crates/ra_assists/src/assist_ctx.rs +++ b/crates/ra_assists/src/assist_ctx.rs @@ -118,6 +118,10 @@ impl<'a> AssistCtx<'a> { AssistGroup { ctx: self, group_name: group_name.into(), assists: Vec::new() } } + pub(crate) fn add_assists(self) -> AssistVec<'a> { + AssistVec { ctx: self, assists: Vec::new() } + } + pub(crate) fn token_at_offset(&self) -> TokenAtOffset { self.source_file.syntax().token_at_offset(self.frange.range.start()) } @@ -129,6 +133,16 @@ impl<'a> AssistCtx<'a> { pub(crate) fn find_node_at_offset(&self) -> Option { find_node_at_offset(self.source_file.syntax(), self.frange.range.start()) } + + pub(crate) fn find_covering_node_at_offset(&self) -> Option { + let node = find_node_at_offset::(self.source_file.syntax(), self.frange.range.start())?; + if self.frange.range.is_subrange(&node.syntax().text_range()) { + Some(node) + } else { + None + } + } + pub(crate) fn covering_element(&self) -> SyntaxElement { find_covering_element(self.source_file.syntax(), self.frange.range) } @@ -174,6 +188,42 @@ impl<'a> AssistGroup<'a> { } } +pub(crate) struct AssistVec<'a> { + ctx: AssistCtx<'a>, + assists: Vec, +} + +impl<'a> AssistVec<'a> { + pub(crate) fn add_assist( + &mut self, + id: AssistId, + label: impl Into, + f: impl FnOnce(&mut ActionBuilder), + ) { + let label = AssistLabel::new(label.into(), id); + + let mut info = AssistInfo::new(label); + if self.ctx.should_compute_edit { + let action = { + let mut edit = ActionBuilder::default(); + f(&mut edit); + edit.build() + }; + info = info.resolved(action) + }; + + self.assists.push(info) + } + + pub(crate) fn finish(self) -> Option { + if self.assists.is_empty() { + None + } else { + Some(Assist(self.assists)) + } + } +} + #[derive(Default)] pub(crate) struct ActionBuilder { edit: TextEditBuilder, diff --git a/crates/ra_assists/src/handlers/number_representation.rs b/crates/ra_assists/src/handlers/number_representation.rs new file mode 100644 index 000000000000..9cbd2055ed30 --- /dev/null +++ b/crates/ra_assists/src/handlers/number_representation.rs @@ -0,0 +1,597 @@ +use ra_syntax::{ast, ast::LiteralKind, AstNode, SmolStr}; +use std::fmt; + +use crate::{Assist, AssistCtx, AssistId}; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +enum NumberLiteralType { + /// A literal without prefix, '42' + Decimal, + /// Hexadecimal literal, '0x2A' + Hexadecimal, + /// Octal literal, '0o52' + Octal, + /// Binary literal, '0b00101010' + Binary, +} + +#[derive(Clone, Debug)] +struct NumberLiteral { + /// The type of literal (no prefix, hex, octal or binary) + number_type: NumberLiteralType, + /// The suffix as a string, for example 'i32' + suffix: Option, + /// The prefix as string, for example '0x' + prefix: Option, + /// Text of the literal + text: String, +} + +impl fmt::Display for NumberLiteral { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(prefix) = &self.prefix { + f.write_str(prefix)?; + } + + f.write_str(&self.text)?; + + if let Some(suffix) = &self.suffix { + f.write_str(suffix)?; + } + + Ok(()) + } +} + +fn identify_number_literal(literal: &ast::Literal) -> Option { + match literal.kind() { + LiteralKind::IntNumber { suffix } => { + let token = literal.token(); + let full_text = token.text().as_str(); + let suffix_clone = suffix.clone(); + let suffix_len = suffix.map(|s| s.len()).unwrap_or_default(); + let non_suffix = &full_text[0..full_text.len() - suffix_len]; + let maybe_prefix = if non_suffix.len() < 2 { None } else { Some(&non_suffix[0..2]) }; + let (prefix, number_type) = match maybe_prefix { + Some("0x") => (maybe_prefix, NumberLiteralType::Hexadecimal), + Some("0b") => (maybe_prefix, NumberLiteralType::Binary), + Some("0o") => (maybe_prefix, NumberLiteralType::Octal), + _ => (None, NumberLiteralType::Decimal), + }; + let prefix_len = prefix.map(|s| s.len()).unwrap_or_default(); + let text = &non_suffix[prefix_len..]; + + let result = NumberLiteral { + number_type, + suffix: suffix_clone, + prefix: prefix.map(SmolStr::new), + text: text.to_string(), + }; + Some(result) + } + _ => None, + } +} + +fn is_int_number(literal: &ast::Literal) -> bool { + match literal.kind() { + LiteralKind::IntNumber { .. } => true, + _ => false, + } +} + +fn remove_separator_from_string(str: &str) -> String { + str.replace("_", "") +} + +pub(crate) fn remove_digit_separators(ctx: AssistCtx) -> Option { + let literal = ctx.find_covering_node_at_offset::()?; + if !is_int_number(&literal) { + return None; + } + + if !literal.syntax().text().contains_char('_') { + return None; + } + + ctx.add_assist(AssistId("remove_digit_separators"), "Remove digit separators", |edit| { + edit.target(literal.syntax().text_range()); + let new_text = remove_separator_from_string(&literal.syntax().text().to_string()); + edit.replace(literal.syntax().text_range(), new_text); + }) +} + +fn len_without_separators(text: &str) -> usize { + text.chars().filter(|&c| c != '_').count() +} + +fn separate_number(text: &str, every: usize, digits_len: usize) -> String { + let mut result = String::with_capacity(digits_len + digits_len / every); + let offset = every - (digits_len % every); + let mut i = 0; + for c in text.chars() { + if c != '_' { + if (i != 0) && ((i + offset) % every == 0) { + result.push('_'); + } + result.push(c); + i += 1; + } + } + + return result; +} + +#[derive(Clone, Debug)] +struct PossibleSeparateNumberAssist { + id: AssistId, + label: String, + every: usize, +} + +const SEPARATE_DECIMAL_THOUSANDS_ID: AssistId = AssistId("separate_decimal_thousands"); +const SEPARATE_HEXADECIMAL_WORDS_ID: AssistId = AssistId("separate_hexadecimal_words"); +const SEPARATE_HEXADECIMAL_BYTES_ID: AssistId = AssistId("separate_hexadecimal_bytes"); +const SEPARATE_BINARY_BYTES_ID: AssistId = AssistId("separate_binary_bytes"); +const SEPARATE_BINARY_NIBBLES_ID: AssistId = AssistId("separate_binary_nibbles"); + +fn get_possible_separate_number_assist( + literal: &NumberLiteral, +) -> Vec { + match literal.number_type { + NumberLiteralType::Decimal => vec![PossibleSeparateNumberAssist { + id: SEPARATE_DECIMAL_THOUSANDS_ID, + label: "Separate thousands".to_string(), + every: 3, + }], + NumberLiteralType::Hexadecimal => vec![ + PossibleSeparateNumberAssist { + id: SEPARATE_HEXADECIMAL_WORDS_ID, + label: "Separate 16-bits words".to_string(), + every: 4, + }, + PossibleSeparateNumberAssist { + id: SEPARATE_HEXADECIMAL_BYTES_ID, + label: "Separate bytes".to_string(), + every: 2, + }, + ], + NumberLiteralType::Binary => vec![ + PossibleSeparateNumberAssist { + id: SEPARATE_BINARY_BYTES_ID, + label: "Separate bytes".to_string(), + every: 8, + }, + PossibleSeparateNumberAssist { + id: SEPARATE_BINARY_NIBBLES_ID, + label: "Separate nibbles".to_string(), + every: 4, + }, + ], + _ => Vec::default(), + } +} + +pub(crate) fn separate_number_literal(ctx: AssistCtx) -> Option { + let literal = ctx.find_covering_node_at_offset::()?; + let number_literal = identify_number_literal(&literal)?; + let possible_assists = get_possible_separate_number_assist(&number_literal); + if possible_assists.len() == 0 { + return None; + } + + let mut assists = ctx.add_assists(); + for possible_assist in possible_assists { + let digits_len = len_without_separators(number_literal.text.as_str()); + if digits_len <= possible_assist.every { + continue; + } + + let result = + separate_number(number_literal.text.as_str(), possible_assist.every, digits_len); + if result == number_literal.text.as_str() { + continue; + } + + assists.add_assist(possible_assist.id, possible_assist.label, |edit| { + edit.target(literal.syntax().text_range()); + let new_literal = NumberLiteral { text: result, ..number_literal.clone() }; + let new_text = new_literal.to_string(); + edit.replace(literal.syntax().text_range(), new_text); + }) + } + + assists.finish() +} + +#[cfg(test)] +mod test { + use super::*; + use crate::helpers::{ + check_assist, check_assist_not_applicable, check_assist_not_applicable_with_id, + check_assist_target, check_assist_target_with_id, check_assist_with_id, + }; + + #[test] + fn remove_digit_separators_target() { + check_assist_target( + remove_digit_separators, + r#"fn f() { let x = <|>42_420; }"#, + r#"42_420"#, + ); + } + + #[test] + fn remove_digit_separators_target_range_inside() { + check_assist_target( + remove_digit_separators, + r#"fn f() { let x = 42<|>_<|>420; }"#, + r#"42_420"#, + ); + } + + #[test] + fn remove_digit_separators_not_applicable_no_separator() { + check_assist_not_applicable(remove_digit_separators, r#"fn f() { let x = <|>42420; }"#); + } + + #[test] + fn remove_digit_separators_not_applicable_range_ends_after() { + check_assist_not_applicable(remove_digit_separators, r#"fn f() { let x = <|>42_420; <|>}"#); + } + + #[test] + fn remove_digit_separators_works_decimal() { + check_assist( + remove_digit_separators, + r#"fn f() { let x = <|>42_420; }"#, + r#"fn f() { let x = <|>42420; }"#, + ) + } + + #[test] + fn remove_digit_separators_works_hex() { + check_assist( + remove_digit_separators, + r#"fn f() { let x = <|>0x42_420; }"#, + r#"fn f() { let x = <|>0x42420; }"#, + ) + } + + #[test] + fn remove_digit_separators_works_octal() { + check_assist( + remove_digit_separators, + r#"fn f() { let x = <|>0o42_420; }"#, + r#"fn f() { let x = <|>0o42420; }"#, + ) + } + + #[test] + fn remove_digit_separators_works_binary() { + check_assist( + remove_digit_separators, + r#"fn f() { let x = <|>0b0010_1010; }"#, + r#"fn f() { let x = <|>0b00101010; }"#, + ) + } + + #[test] + fn remove_digit_separators_works_suffix() { + check_assist( + remove_digit_separators, + r#"fn f() { let x = <|>42_420u32; }"#, + r#"fn f() { let x = <|>42420u32; }"#, + ) + } + + // --- + + fn separate_number_for_test(text: &str, every: usize) -> String { + separate_number(text, every, len_without_separators(text)) + } + + #[test] + fn test_separate_number() { + assert_eq!(separate_number_for_test("", 2), ""); + assert_eq!(separate_number_for_test("1", 2), "1"); + assert_eq!(separate_number_for_test("12", 2), "12"); + assert_eq!(separate_number_for_test("12345678", 2), "12_34_56_78"); + assert_eq!(separate_number_for_test("123456789", 2), "1_23_45_67_89"); + assert_eq!(separate_number_for_test("1_2_3_4_5_6_7_8_9", 2), "1_23_45_67_89"); + + assert_eq!(separate_number_for_test("", 4), ""); + assert_eq!(separate_number_for_test("1", 4), "1"); + assert_eq!(separate_number_for_test("1212", 4), "1212"); + assert_eq!(separate_number_for_test("24204242420", 4), "242_0424_2420"); + assert_eq!(separate_number_for_test("024204242420", 4), "0242_0424_2420"); + assert_eq!(separate_number_for_test("_0_2_4_2_04242_420", 4), "0242_0424_2420"); + } + + // --- + + #[test] + fn separate_number_literal_decimal_target() { + check_assist_target(separate_number_literal, r#"fn f() { let x = <|>42420; }"#, r#"42420"#); + } + + #[test] + fn separate_number_literal_decimal_already_split_not_applicable() { + check_assist_not_applicable(separate_number_literal, r#"fn f() { let x = <|>42_420;}"#); + } + + #[test] + fn separate_number_literal_decimal_too_small_not_applicable() { + check_assist_not_applicable(separate_number_literal, r#"fn f() { let x = <|>420;}"#); + } + + #[test] + fn separate_number_literal_decimal_too_small_separator_not_applicable() { + check_assist_not_applicable(separate_number_literal, r#"fn f() { let x = <|>4_2_0;}"#); + } + + #[test] + fn separate_number_literal_decimal() { + check_assist( + separate_number_literal, + r#"fn f() { let x = <|>2420420; }"#, + r#"fn f() { let x = <|>2_420_420; }"#, + ) + } + + #[test] + fn separate_number_literal_decimal_badly_split() { + check_assist( + separate_number_literal, + r#"fn f() { let x = <|>4_2_4_2_0420; }"#, + r#"fn f() { let x = <|>42_420_420; }"#, + ) + } + + // --- + + #[test] + fn separate_number_literal_hex_words_target() { + check_assist_target_with_id( + separate_number_literal, + SEPARATE_HEXADECIMAL_WORDS_ID, + r#"fn f() { let x = <|>0x04242420; }"#, + r#"0x04242420"#, + ); + } + + #[test] + fn separate_number_literal_hex_words_already_split_not_applicable() { + check_assist_not_applicable_with_id( + separate_number_literal, + SEPARATE_HEXADECIMAL_WORDS_ID, + r#"fn f() { let x = <|>0x0424_2420; <|>}"#, + ); + } + + #[test] + fn separate_number_literal_hex_words_too_small_not_applicable() { + check_assist_not_applicable_with_id( + separate_number_literal, + SEPARATE_HEXADECIMAL_WORDS_ID, + r#"fn f() { let x = <|>0x2420;}"#, + ); + } + + #[test] + fn separate_number_literal_hex_words_too_small_separator_not_applicable() { + check_assist_not_applicable_with_id( + separate_number_literal, + SEPARATE_HEXADECIMAL_WORDS_ID, + r#"fn f() { let x = <|>0x2_4_2_0;}"#, + ); + } + + #[test] + fn separate_number_literal_hex_words() { + check_assist_with_id( + separate_number_literal, + SEPARATE_HEXADECIMAL_WORDS_ID, + r#"fn f() { let x = <|>0x24204242420; }"#, + r#"fn f() { let x = <|>0x242_0424_2420; }"#, + ) + } + + #[test] + fn separate_number_literal_hex_words_badly_split() { + check_assist_with_id( + separate_number_literal, + SEPARATE_HEXADECIMAL_WORDS_ID, + r#"fn f() { let x = <|>0x2_4204_24_2420; }"#, + r#"fn f() { let x = <|>0x242_0424_2420; }"#, + ) + } + + // --- + + #[test] + fn separate_number_literal_hex_bytes_target() { + check_assist_target_with_id( + separate_number_literal, + SEPARATE_HEXADECIMAL_BYTES_ID, + r#"fn f() { let x = <|>0x04242420; }"#, + r#"0x04242420"#, + ); + } + + #[test] + fn separate_number_literal_hex_bytes_already_split_not_applicable() { + check_assist_not_applicable_with_id( + separate_number_literal, + SEPARATE_HEXADECIMAL_BYTES_ID, + r#"fn f() { let x = <|>0x04_24_24_20; <|>}"#, + ); + } + + #[test] + fn separate_number_literal_hex_bytes_too_small_not_applicable() { + check_assist_not_applicable_with_id( + separate_number_literal, + SEPARATE_HEXADECIMAL_BYTES_ID, + r#"fn f() { let x = <|>0x20;}"#, + ); + } + + #[test] + fn separate_number_literal_hex_bytes_too_small_separator_not_applicable() { + check_assist_not_applicable_with_id( + separate_number_literal, + SEPARATE_HEXADECIMAL_BYTES_ID, + r#"fn f() { let x = <|>0x2_0;}"#, + ); + } + + #[test] + fn separate_number_literal_hex_bytes() { + check_assist_with_id( + separate_number_literal, + SEPARATE_HEXADECIMAL_BYTES_ID, + r#"fn f() { let x = <|>0x24204242420; }"#, + r#"fn f() { let x = <|>0x2_42_04_24_24_20; }"#, + ) + } + + #[test] + fn separate_number_literal_hex_bytes_badly_split() { + check_assist_with_id( + separate_number_literal, + SEPARATE_HEXADECIMAL_BYTES_ID, + r#"fn f() { let x = <|>0x2_4_2_04242420; }"#, + r#"fn f() { let x = <|>0x2_42_04_24_24_20; }"#, + ) + } + + // --- + + #[test] + fn separate_number_literal_octal_not_applicable() { + check_assist_not_applicable( + separate_number_literal, + r#"fn f() { let x = <|>0o01234567; }"#, + ); + } + + // --- + + #[test] + fn separate_number_literal_binary_nibbles_target() { + check_assist_target_with_id( + separate_number_literal, + SEPARATE_BINARY_NIBBLES_ID, + //r#"fn f() { let x = <|>0b00101010; }"#, + r#"fn f() { let x = <|>0b00101010; }"#, + r#"0b00101010"#, + ); + } + + #[test] + fn separate_number_literal_binary_nibbles_already_split_not_applicable() { + check_assist_not_applicable_with_id( + separate_number_literal, + SEPARATE_BINARY_NIBBLES_ID, + r#"fn f() { let x = <|>0b0010_1010_0010_1010; <|>}"#, + ); + } + + #[test] + fn separate_number_literal_binary_nibbles_too_small_not_applicable() { + check_assist_not_applicable_with_id( + separate_number_literal, + SEPARATE_BINARY_NIBBLES_ID, + r#"fn f() { let x = <|>0b1010;}"#, + ); + } + + #[test] + fn separate_number_literal_binary_nibbles_too_small_separator_not_applicable() { + check_assist_not_applicable_with_id( + separate_number_literal, + SEPARATE_BINARY_NIBBLES_ID, + r#"fn f() { let x = <|>0b1_01_0;}"#, + ); + } + + #[test] + fn separate_number_literal_binary_nibbles() { + check_assist_with_id( + separate_number_literal, + SEPARATE_BINARY_NIBBLES_ID, + r#"fn f() { let x = <|>0b0010101000101010; }"#, + r#"fn f() { let x = <|>0b0010_1010_0010_1010; }"#, + ) + } + + #[test] + fn separate_number_literal_binary_nibbles_badly_split() { + check_assist_with_id( + separate_number_literal, + SEPARATE_BINARY_NIBBLES_ID, + r#"fn f() { let x = <|>0b001_0101_000_101_010; }"#, + r#"fn f() { let x = <|>0b0010_1010_0010_1010; }"#, + ) + } + + // --- + + #[test] + fn separate_number_literal_binary_bytes_target() { + check_assist_target_with_id( + separate_number_literal, + SEPARATE_BINARY_BYTES_ID, + r#"fn f() { let x = <|>0b0010101000101010; }"#, + r#"0b0010101000101010"#, + ); + } + + #[test] + fn separate_number_literal_binary_bytes_already_split_not_applicable() { + check_assist_not_applicable_with_id( + separate_number_literal, + SEPARATE_BINARY_BYTES_ID, + r#"fn f() { let x = <|>0b00101010_00101010; <|>}"#, + ); + } + + #[test] + fn separate_number_literal_binary_bytes_too_small_not_applicable() { + check_assist_not_applicable_with_id( + separate_number_literal, + SEPARATE_BINARY_BYTES_ID, + r#"fn f() { let x = <|>0b00101010;}"#, + ); + } + + #[test] + fn separate_number_literal_binary_bytes_too_small_separator_not_applicable() { + check_assist_not_applicable_with_id( + separate_number_literal, + SEPARATE_BINARY_BYTES_ID, + r#"fn f() { let x = <|>0b0_0_101_01_0;}"#, + ); + } + + #[test] + fn separate_number_literal_binary_bytes() { + check_assist_with_id( + separate_number_literal, + SEPARATE_BINARY_BYTES_ID, + r#"fn f() { let x = <|>0b0010101000101010; }"#, + r#"fn f() { let x = <|>0b00101010_00101010; }"#, + ) + } + + #[test] + fn separate_number_literal_binary_bytes_badly_split() { + check_assist_with_id( + separate_number_literal, + SEPARATE_BINARY_BYTES_ID, + r#"fn f() { let x = <|>0b001_0101_000_101_010; }"#, + r#"fn f() { let x = <|>0b00101010_00101010; }"#, + ) + } +} diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs index b8704ea7d2f8..3110a5591def 100644 --- a/crates/ra_assists/src/lib.rs +++ b/crates/ra_assists/src/lib.rs @@ -120,6 +120,7 @@ mod handlers { mod replace_if_let_with_match; mod replace_qualified_name_with_use; mod split_import; + mod number_representation; pub(crate) fn all() -> &'static [AssistHandler] { &[ @@ -155,6 +156,8 @@ mod handlers { replace_if_let_with_match::replace_if_let_with_match, replace_qualified_name_with_use::replace_qualified_name_with_use, split_import::split_import, + number_representation::remove_digit_separators, + number_representation::separate_number_literal, ] } } @@ -168,7 +171,7 @@ mod helpers { use ra_syntax::TextRange; use test_utils::{add_cursor, assert_eq_text, extract_range_or_offset, RangeOrOffset}; - use crate::{AssistCtx, AssistHandler}; + use crate::{assist_ctx::AssistInfo, AssistCtx, AssistHandler, AssistId}; use hir::Semantics; pub(crate) fn with_single_file(text: &str) -> (RootDatabase, FileId) { @@ -185,18 +188,44 @@ mod helpers { ra_fixture_before: &str, ra_fixture_after: &str, ) { - check(assist, ra_fixture_before, ExpectedResult::After(ra_fixture_after)); + check(assist, None, ra_fixture_before, ExpectedResult::After(ra_fixture_after)); + } + + pub(crate) fn check_assist_with_id( + assist: AssistHandler, + assist_id: AssistId, + ra_fixture_before: &str, + ra_fixture_after: &str, + ) { + check(assist, Some(assist_id), ra_fixture_before, ExpectedResult::After(ra_fixture_after)); } // FIXME: instead of having a separate function here, maybe use // `extract_ranges` and mark the target as ` ` in the // fixuture? pub(crate) fn check_assist_target(assist: AssistHandler, ra_fixture: &str, target: &str) { - check(assist, ra_fixture, ExpectedResult::Target(target)); + check(assist, None, ra_fixture, ExpectedResult::Target(target)); + } + + pub(crate) fn check_assist_target_with_id( + assist: AssistHandler, + assist_id: AssistId, + ra_fixture: &str, + target: &str, + ) { + check(assist, Some(assist_id), ra_fixture, ExpectedResult::Target(target)); } pub(crate) fn check_assist_not_applicable(assist: AssistHandler, ra_fixture: &str) { - check(assist, ra_fixture, ExpectedResult::NotApplicable); + check(assist, None, ra_fixture, ExpectedResult::NotApplicable); + } + + pub(crate) fn check_assist_not_applicable_with_id( + assist: AssistHandler, + assist_id: AssistId, + ra_fixture: &str, + ) { + check(assist, Some(assist_id), ra_fixture, ExpectedResult::NotApplicable); } enum ExpectedResult<'a> { @@ -205,7 +234,12 @@ mod helpers { Target(&'a str), } - fn check(assist: AssistHandler, before: &str, expected: ExpectedResult) { + fn check( + assist_handler: AssistHandler, + assist_id: Option, + before: &str, + expected: ExpectedResult, + ) { let (range_or_offset, before) = extract_range_or_offset(before); let range: TextRange = range_or_offset.into(); @@ -214,9 +248,17 @@ mod helpers { let sema = Semantics::new(&db); let assist_ctx = AssistCtx::new(&sema, frange, true); - match (assist(assist_ctx), expected) { + let assist_result = assist_handler(assist_ctx); + let assist: Option = assist_result.clone().and_then(|assist| match assist_id { + None => Some(assist.0[0].clone()), + Some(assist_id) => { + assist.0.iter().find(|info| info.label.id == assist_id).map(AssistInfo::to_owned) + } + }); + + match (assist, expected) { (Some(assist), ExpectedResult::After(after)) => { - let action = assist.0[0].action.clone().unwrap(); + let action = assist.action.clone().unwrap(); let mut actual = action.edit.apply(&before); match action.cursor_position { @@ -235,14 +277,25 @@ mod helpers { assert_eq_text!(after, &actual); } (Some(assist), ExpectedResult::Target(target)) => { - let action = assist.0[0].action.clone().unwrap(); + let action = assist.action.clone().unwrap(); let range = action.target.expect("expected target on action"); assert_eq_text!(&before[range], target); } (Some(_), ExpectedResult::NotApplicable) => panic!("assist should not be applicable!"), - (None, ExpectedResult::After(_)) | (None, ExpectedResult::Target(_)) => { - panic!("code action is not applicable") - } + (None, ExpectedResult::After(_)) | (None, ExpectedResult::Target(_)) => match assist_id + { + None => panic!("No code action is applicable"), + Some(assist_id) => { + let applicable_actions: Vec = assist_result + .map(|r| r.0.iter().map(|i| i.label.id.0.to_string()).collect()) + .unwrap_or_default(); + panic!( + "Code action '{}' is not applicable. Applicables actions: [{}]", + assist_id.0, + applicable_actions.join(", ") + ); + } + }, (None, ExpectedResult::NotApplicable) => (), }; }