mod generated; use expect_test::expect; use hir::Semantics; use ide_db::{ base_db::{FileId, FileRange, SourceDatabaseExt}, imports::insert_use::{ImportGranularity, InsertUseConfig}, source_change::FileSystemEdit, RootDatabase, SnippetCap, }; use stdx::{format_to, trim_indent}; use syntax::TextRange; use test_fixture::WithFixture; use test_utils::{assert_eq_text, extract_offset}; use crate::{ assists, handlers::Handler, Assist, AssistConfig, AssistContext, AssistKind, AssistResolveStrategy, Assists, SingleResolve, }; pub(crate) const TEST_CONFIG: AssistConfig = AssistConfig { snippet_cap: SnippetCap::new(true), allowed: None, insert_use: InsertUseConfig { granularity: ImportGranularity::Crate, prefix_kind: hir::PrefixKind::Plain, enforce_granularity: true, group: true, skip_glob_imports: true, }, prefer_no_std: false, prefer_prelude: true, assist_emit_must_use: false, }; pub(crate) const TEST_CONFIG_NO_SNIPPET_CAP: AssistConfig = AssistConfig { snippet_cap: None, allowed: None, insert_use: InsertUseConfig { granularity: ImportGranularity::Crate, prefix_kind: hir::PrefixKind::Plain, enforce_granularity: true, group: true, skip_glob_imports: true, }, prefer_no_std: false, prefer_prelude: true, assist_emit_must_use: false, }; pub(crate) const TEST_CONFIG_IMPORT_ONE: AssistConfig = AssistConfig { snippet_cap: SnippetCap::new(true), allowed: None, insert_use: InsertUseConfig { granularity: ImportGranularity::One, prefix_kind: hir::PrefixKind::Plain, enforce_granularity: true, group: true, skip_glob_imports: true, }, prefer_no_std: false, prefer_prelude: true, assist_emit_must_use: false, }; pub(crate) fn with_single_file(text: &str) -> (RootDatabase, FileId) { RootDatabase::with_single_file(text) } #[track_caller] pub(crate) fn check_assist(assist: Handler, ra_fixture_before: &str, ra_fixture_after: &str) { let ra_fixture_after = trim_indent(ra_fixture_after); check(assist, ra_fixture_before, ExpectedResult::After(&ra_fixture_after), None); } #[track_caller] pub(crate) fn check_assist_no_snippet_cap( assist: Handler, ra_fixture_before: &str, ra_fixture_after: &str, ) { let ra_fixture_after = trim_indent(ra_fixture_after); check_with_config( TEST_CONFIG_NO_SNIPPET_CAP, assist, ra_fixture_before, ExpectedResult::After(&ra_fixture_after), None, ); } #[track_caller] pub(crate) fn check_assist_import_one( assist: Handler, ra_fixture_before: &str, ra_fixture_after: &str, ) { let ra_fixture_after = trim_indent(ra_fixture_after); check_with_config( TEST_CONFIG_IMPORT_ONE, assist, ra_fixture_before, ExpectedResult::After(&ra_fixture_after), None, ); } // There is no way to choose what assist within a group you want to test against, // so this is here to allow you choose. pub(crate) fn check_assist_by_label( assist: Handler, ra_fixture_before: &str, ra_fixture_after: &str, label: &str, ) { let ra_fixture_after = trim_indent(ra_fixture_after); check(assist, ra_fixture_before, ExpectedResult::After(&ra_fixture_after), Some(label)); } // FIXME: instead of having a separate function here, maybe use // `extract_ranges` and mark the target as ` ` in the // fixture? #[track_caller] pub(crate) fn check_assist_target(assist: Handler, ra_fixture: &str, target: &str) { check(assist, ra_fixture, ExpectedResult::Target(target), None); } #[track_caller] pub(crate) fn check_assist_not_applicable(assist: Handler, ra_fixture: &str) { check(assist, ra_fixture, ExpectedResult::NotApplicable, None); } #[track_caller] pub(crate) fn check_assist_not_applicable_by_label(assist: Handler, ra_fixture: &str, label: &str) { check(assist, ra_fixture, ExpectedResult::NotApplicable, Some(label)); } #[track_caller] pub(crate) fn check_assist_not_applicable_for_import_one(assist: Handler, ra_fixture: &str) { check_with_config( TEST_CONFIG_IMPORT_ONE, assist, ra_fixture, ExpectedResult::NotApplicable, None, ); } /// Check assist in unresolved state. Useful to check assists for lazy computation. #[track_caller] pub(crate) fn check_assist_unresolved(assist: Handler, ra_fixture: &str) { check(assist, ra_fixture, ExpectedResult::Unresolved, None); } #[track_caller] fn check_doc_test(assist_id: &str, before: &str, after: &str) { let after = trim_indent(after); let (db, file_id, selection) = RootDatabase::with_range_or_offset(before); let before = db.file_text(file_id).to_string(); let frange = FileRange { file_id, range: selection.into() }; let assist = assists(&db, &TEST_CONFIG, AssistResolveStrategy::All, frange) .into_iter() .find(|assist| assist.id.0 == assist_id) .unwrap_or_else(|| { panic!( "\n\nAssist is not applicable: {}\nAvailable assists: {}", assist_id, assists(&db, &TEST_CONFIG, AssistResolveStrategy::None, frange) .into_iter() .map(|assist| assist.id.0) .collect::>() .join(", ") ) }); let actual = { let source_change = assist .source_change .filter(|it| !it.source_file_edits.is_empty() || !it.file_system_edits.is_empty()) .expect("Assist did not contain any source changes"); let mut actual = before; if let Some((source_file_edit, snippet_edit)) = source_change.get_source_and_snippet_edit(file_id) { source_file_edit.apply(&mut actual); if let Some(snippet_edit) = snippet_edit { snippet_edit.apply(&mut actual); } } actual }; assert_eq_text!(&after, &actual); } enum ExpectedResult<'a> { NotApplicable, Unresolved, After(&'a str), Target(&'a str), } #[track_caller] fn check(handler: Handler, before: &str, expected: ExpectedResult<'_>, assist_label: Option<&str>) { check_with_config(TEST_CONFIG, handler, before, expected, assist_label); } #[track_caller] fn check_with_config( config: AssistConfig, handler: Handler, before: &str, expected: ExpectedResult<'_>, assist_label: Option<&str>, ) { let (mut db, file_with_caret_id, range_or_offset) = RootDatabase::with_range_or_offset(before); db.enable_proc_attr_macros(); let text_without_caret = db.file_text(file_with_caret_id).to_string(); let frange = FileRange { file_id: file_with_caret_id, range: range_or_offset.into() }; let sema = Semantics::new(&db); let ctx = AssistContext::new(sema, &config, frange); let resolve = match expected { ExpectedResult::Unresolved => AssistResolveStrategy::None, _ => AssistResolveStrategy::All, }; let mut acc = Assists::new(&ctx, resolve); handler(&mut acc, &ctx); let mut res = acc.finish(); let assist = match assist_label { Some(label) => res.into_iter().find(|resolved| resolved.label == label), None => res.pop(), }; match (assist, expected) { (Some(assist), ExpectedResult::After(after)) => { let source_change = assist .source_change .filter(|it| !it.source_file_edits.is_empty() || !it.file_system_edits.is_empty()) .expect("Assist did not contain any source changes"); let skip_header = source_change.source_file_edits.len() == 1 && source_change.file_system_edits.is_empty(); let mut buf = String::new(); for (file_id, (edit, snippet_edit)) in source_change.source_file_edits { let mut text = db.file_text(file_id).as_ref().to_owned(); edit.apply(&mut text); if let Some(snippet_edit) = snippet_edit { snippet_edit.apply(&mut text); } if !skip_header { let sr = db.file_source_root(file_id); let sr = db.source_root(sr); let path = sr.path_for_file(&file_id).unwrap(); format_to!(buf, "//- {}\n", path) } buf.push_str(&text); } for file_system_edit in source_change.file_system_edits { let (dst, contents) = match file_system_edit { FileSystemEdit::CreateFile { dst, initial_contents } => (dst, initial_contents), FileSystemEdit::MoveFile { src, dst } => { (dst, db.file_text(src).as_ref().to_owned()) } FileSystemEdit::MoveDir { src, src_id, dst } => { // temporary placeholder for MoveDir since we are not using MoveDir in ide assists yet. (dst, format!("{src_id:?}\n{src:?}")) } }; let sr = db.file_source_root(dst.anchor); let sr = db.source_root(sr); let mut base = sr.path_for_file(&dst.anchor).unwrap().clone(); base.pop(); let created_file_path = base.join(&dst.path).unwrap(); format_to!(buf, "//- {}\n", created_file_path); buf.push_str(&contents); } assert_eq_text!(after, &buf); } (Some(assist), ExpectedResult::Target(target)) => { let range = assist.target; assert_eq_text!(&text_without_caret[range], target); } (Some(assist), ExpectedResult::Unresolved) => assert!( assist.source_change.is_none(), "unresolved assist should not contain source changes" ), (Some(_), ExpectedResult::NotApplicable) => panic!("assist should not be applicable!"), ( None, ExpectedResult::After(_) | ExpectedResult::Target(_) | ExpectedResult::Unresolved, ) => { panic!("code action is not applicable") } (None, ExpectedResult::NotApplicable) => (), }; } fn labels(assists: &[Assist]) -> String { let mut labels = assists .iter() .map(|assist| { let mut label = match &assist.group { Some(g) => g.0.clone(), None => assist.label.to_string(), }; label.push('\n'); label }) .collect::>(); labels.dedup(); labels.into_iter().collect::() } #[test] fn assist_order_field_struct() { let before = "struct Foo { $0bar: u32 }"; let (before_cursor_pos, before) = extract_offset(before); let (db, file_id) = with_single_file(&before); let frange = FileRange { file_id, range: TextRange::empty(before_cursor_pos) }; let assists = assists(&db, &TEST_CONFIG, AssistResolveStrategy::None, frange); let mut assists = assists.iter(); assert_eq!(assists.next().expect("expected assist").label, "Change visibility to pub(crate)"); assert_eq!(assists.next().expect("expected assist").label, "Generate a getter method"); assert_eq!(assists.next().expect("expected assist").label, "Generate a mut getter method"); assert_eq!(assists.next().expect("expected assist").label, "Generate a setter method"); assert_eq!(assists.next().expect("expected assist").label, "Add `#[derive]`"); assert_eq!(assists.next().expect("expected assist").label, "Generate `new`"); assert_eq!(assists.next().map(|it| it.label.to_string()), None); } #[test] fn assist_order_if_expr() { let (db, frange) = RootDatabase::with_range( r#" pub fn test_some_range(a: int) -> bool { if let 2..6 = $05$0 { true } else { false } } "#, ); let assists = assists(&db, &TEST_CONFIG, AssistResolveStrategy::None, frange); let expected = labels(&assists); expect![[r#" Convert integer base Extract into variable Extract into function Replace if let with match "#]] .assert_eq(&expected); } #[test] fn assist_filter_works() { let (db, frange) = RootDatabase::with_range( r#" pub fn test_some_range(a: int) -> bool { if let 2..6 = $05$0 { true } else { false } } "#, ); { let mut cfg = TEST_CONFIG; cfg.allowed = Some(vec![AssistKind::Refactor]); let assists = assists(&db, &cfg, AssistResolveStrategy::None, frange); let expected = labels(&assists); expect![[r#" Convert integer base Extract into variable Extract into function Replace if let with match "#]] .assert_eq(&expected); } { let mut cfg = TEST_CONFIG; cfg.allowed = Some(vec![AssistKind::RefactorExtract]); let assists = assists(&db, &cfg, AssistResolveStrategy::None, frange); let expected = labels(&assists); expect![[r#" Extract into variable Extract into function "#]] .assert_eq(&expected); } { let mut cfg = TEST_CONFIG; cfg.allowed = Some(vec![AssistKind::QuickFix]); let assists = assists(&db, &cfg, AssistResolveStrategy::None, frange); let expected = labels(&assists); expect![[r#""#]].assert_eq(&expected); } } #[test] fn various_resolve_strategies() { let (db, frange) = RootDatabase::with_range( r#" pub fn test_some_range(a: int) -> bool { if let 2..6 = $05$0 { true } else { false } } "#, ); let mut cfg = TEST_CONFIG; cfg.allowed = Some(vec![AssistKind::RefactorExtract]); { let assists = assists(&db, &cfg, AssistResolveStrategy::None, frange); assert_eq!(2, assists.len()); let mut assists = assists.into_iter(); let extract_into_variable_assist = assists.next().unwrap(); expect![[r#" Assist { id: AssistId( "extract_variable", RefactorExtract, ), label: "Extract into variable", group: None, target: 59..60, source_change: None, trigger_signature_help: false, } "#]] .assert_debug_eq(&extract_into_variable_assist); let extract_into_function_assist = assists.next().unwrap(); expect![[r#" Assist { id: AssistId( "extract_function", RefactorExtract, ), label: "Extract into function", group: None, target: 59..60, source_change: None, trigger_signature_help: false, } "#]] .assert_debug_eq(&extract_into_function_assist); } { let assists = assists( &db, &cfg, AssistResolveStrategy::Single(SingleResolve { assist_id: "SOMETHING_MISMATCHING".to_owned(), assist_kind: AssistKind::RefactorExtract, }), frange, ); assert_eq!(2, assists.len()); let mut assists = assists.into_iter(); let extract_into_variable_assist = assists.next().unwrap(); expect![[r#" Assist { id: AssistId( "extract_variable", RefactorExtract, ), label: "Extract into variable", group: None, target: 59..60, source_change: None, trigger_signature_help: false, } "#]] .assert_debug_eq(&extract_into_variable_assist); let extract_into_function_assist = assists.next().unwrap(); expect![[r#" Assist { id: AssistId( "extract_function", RefactorExtract, ), label: "Extract into function", group: None, target: 59..60, source_change: None, trigger_signature_help: false, } "#]] .assert_debug_eq(&extract_into_function_assist); } { let assists = assists( &db, &cfg, AssistResolveStrategy::Single(SingleResolve { assist_id: "extract_variable".to_owned(), assist_kind: AssistKind::RefactorExtract, }), frange, ); assert_eq!(2, assists.len()); let mut assists = assists.into_iter(); let extract_into_variable_assist = assists.next().unwrap(); expect![[r#" Assist { id: AssistId( "extract_variable", RefactorExtract, ), label: "Extract into variable", group: None, target: 59..60, source_change: Some( SourceChange { source_file_edits: { FileId( 0, ): ( TextEdit { indels: [ Indel { insert: "let", delete: 45..47, }, Indel { insert: "var_name", delete: 48..60, }, Indel { insert: "=", delete: 61..81, }, Indel { insert: "5;\n if let 2..6 = var_name {\n true\n } else {\n false\n }", delete: 82..108, }, ], }, Some( SnippetEdit( [ ( 0, 49..49, ), ], ), ), ), }, file_system_edits: [], is_snippet: true, }, ), trigger_signature_help: false, } "#]] .assert_debug_eq(&extract_into_variable_assist); let extract_into_function_assist = assists.next().unwrap(); expect![[r#" Assist { id: AssistId( "extract_function", RefactorExtract, ), label: "Extract into function", group: None, target: 59..60, source_change: None, trigger_signature_help: false, } "#]] .assert_debug_eq(&extract_into_function_assist); } { let assists = assists(&db, &cfg, AssistResolveStrategy::All, frange); assert_eq!(2, assists.len()); let mut assists = assists.into_iter(); let extract_into_variable_assist = assists.next().unwrap(); expect![[r#" Assist { id: AssistId( "extract_variable", RefactorExtract, ), label: "Extract into variable", group: None, target: 59..60, source_change: Some( SourceChange { source_file_edits: { FileId( 0, ): ( TextEdit { indels: [ Indel { insert: "let", delete: 45..47, }, Indel { insert: "var_name", delete: 48..60, }, Indel { insert: "=", delete: 61..81, }, Indel { insert: "5;\n if let 2..6 = var_name {\n true\n } else {\n false\n }", delete: 82..108, }, ], }, Some( SnippetEdit( [ ( 0, 49..49, ), ], ), ), ), }, file_system_edits: [], is_snippet: true, }, ), trigger_signature_help: false, } "#]] .assert_debug_eq(&extract_into_variable_assist); let extract_into_function_assist = assists.next().unwrap(); expect![[r#" Assist { id: AssistId( "extract_function", RefactorExtract, ), label: "Extract into function", group: None, target: 59..60, source_change: Some( SourceChange { source_file_edits: { FileId( 0, ): ( TextEdit { indels: [ Indel { insert: "fun_name()", delete: 59..60, }, Indel { insert: "\n\nfn fun_name() -> i32 {\n 5\n}", delete: 110..110, }, ], }, Some( SnippetEdit( [ ( 0, 124..124, ), ], ), ), ), }, file_system_edits: [], is_snippet: true, }, ), trigger_signature_help: false, } "#]] .assert_debug_eq(&extract_into_function_assist); } }