aboutsummaryrefslogtreecommitdiff
path: root/crates/ide/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide/src')
-rw-r--r--crates/ide/src/lib.rs11
-rw-r--r--crates/ide/src/references/rename.rs54
-rw-r--r--crates/ide/src/runnables.rs399
3 files changed, 446 insertions, 18 deletions
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index d1a250d48..a8b169e87 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -87,7 +87,7 @@ pub use crate::{
87pub use hir::{Documentation, Semantics}; 87pub use hir::{Documentation, Semantics};
88pub use ide_assists::{Assist, AssistConfig, AssistId, AssistKind}; 88pub use ide_assists::{Assist, AssistConfig, AssistId, AssistKind};
89pub use ide_completion::{ 89pub use ide_completion::{
90 CompletionConfig, CompletionItem, CompletionItemKind, CompletionScore, ImportEdit, 90 CompletionConfig, CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit,
91 InsertTextFormat, 91 InsertTextFormat,
92}; 92};
93pub use ide_db::{ 93pub use ide_db::{
@@ -447,6 +447,15 @@ impl Analysis {
447 self.with_db(|db| runnables::runnables(db, file_id)) 447 self.with_db(|db| runnables::runnables(db, file_id))
448 } 448 }
449 449
450 /// Returns the set of tests for the given file position.
451 pub fn related_tests(
452 &self,
453 position: FilePosition,
454 search_scope: Option<SearchScope>,
455 ) -> Cancelable<Vec<Runnable>> {
456 self.with_db(|db| runnables::related_tests(db, position, search_scope))
457 }
458
450 /// Computes syntax highlighting for the given file 459 /// Computes syntax highlighting for the given file
451 pub fn highlight(&self, file_id: FileId) -> Cancelable<Vec<HlRange>> { 460 pub fn highlight(&self, file_id: FileId) -> Cancelable<Vec<HlRange>> {
452 self.with_db(|db| syntax_highlighting::highlight(db, file_id, None, false)) 461 self.with_db(|db| syntax_highlighting::highlight(db, file_id, None, false))
diff --git a/crates/ide/src/references/rename.rs b/crates/ide/src/references/rename.rs
index bb68bcc78..1e378279d 100644
--- a/crates/ide/src/references/rename.rs
+++ b/crates/ide/src/references/rename.rs
@@ -21,7 +21,7 @@ use crate::{display::TryToNav, FilePosition, FileSystemEdit, RangeInfo, SourceCh
21 21
22type RenameResult<T> = Result<T, RenameError>; 22type RenameResult<T> = Result<T, RenameError>;
23#[derive(Debug)] 23#[derive(Debug)]
24pub struct RenameError(pub(crate) String); 24pub struct RenameError(String);
25 25
26impl fmt::Display for RenameError { 26impl fmt::Display for RenameError {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -47,16 +47,15 @@ pub(crate) fn prepare_rename(
47 let sema = Semantics::new(db); 47 let sema = Semantics::new(db);
48 let source_file = sema.parse(position.file_id); 48 let source_file = sema.parse(position.file_id);
49 let syntax = source_file.syntax(); 49 let syntax = source_file.syntax();
50 let range = match &sema 50 let name_like = sema
51 .find_node_at_offset_with_descend(&syntax, position.offset) 51 .find_node_at_offset_with_descend(&syntax, position.offset)
52 .ok_or_else(|| format_err!("No references found at position"))? 52 .ok_or_else(|| format_err!("No references found at position"))?;
53 { 53 let node = match &name_like {
54 ast::NameLike::Name(it) => it.syntax(), 54 ast::NameLike::Name(it) => it.syntax(),
55 ast::NameLike::NameRef(it) => it.syntax(), 55 ast::NameLike::NameRef(it) => it.syntax(),
56 ast::NameLike::Lifetime(it) => it.syntax(), 56 ast::NameLike::Lifetime(it) => it.syntax(),
57 } 57 };
58 .text_range(); 58 Ok(RangeInfo::new(sema.original_range(node).range, ()))
59 Ok(RangeInfo::new(range, ()))
60} 59}
61 60
62// Feature: Rename 61// Feature: Rename
@@ -546,6 +545,8 @@ mod tests {
546 545
547 use crate::{fixture, FileId}; 546 use crate::{fixture, FileId};
548 547
548 use super::{RangeInfo, RenameError};
549
549 fn check(new_name: &str, ra_fixture_before: &str, ra_fixture_after: &str) { 550 fn check(new_name: &str, ra_fixture_before: &str, ra_fixture_after: &str) {
550 let ra_fixture_after = &trim_indent(ra_fixture_after); 551 let ra_fixture_after = &trim_indent(ra_fixture_after);
551 let (analysis, position) = fixture::position(ra_fixture_before); 552 let (analysis, position) = fixture::position(ra_fixture_before);
@@ -591,6 +592,45 @@ mod tests {
591 expect.assert_debug_eq(&source_change) 592 expect.assert_debug_eq(&source_change)
592 } 593 }
593 594
595 fn check_prepare(ra_fixture: &str, expect: Expect) {
596 let (analysis, position) = fixture::position(ra_fixture);
597 let result = analysis
598 .prepare_rename(position)
599 .unwrap_or_else(|err| panic!("PrepareRename was cancelled: {}", err));
600 match result {
601 Ok(RangeInfo { range, info: () }) => {
602 let source = analysis.file_text(position.file_id).unwrap();
603 expect.assert_eq(&format!("{:?}: {}", range, &source[range]))
604 }
605 Err(RenameError(err)) => expect.assert_eq(&err),
606 };
607 }
608
609 #[test]
610 fn test_prepare_rename_namelikes() {
611 check_prepare(r"fn name$0<'lifetime>() {}", expect![[r#"3..7: name"#]]);
612 check_prepare(r"fn name<'lifetime$0>() {}", expect![[r#"8..17: 'lifetime"#]]);
613 check_prepare(r"fn name<'lifetime>() { name$0(); }", expect![[r#"23..27: name"#]]);
614 }
615
616 #[test]
617 fn test_prepare_rename_in_macro() {
618 check_prepare(
619 r"macro_rules! foo {
620 ($ident:ident) => {
621 pub struct $ident;
622 }
623}
624foo!(Foo$0);",
625 expect![[r#"83..86: Foo"#]],
626 );
627 }
628
629 #[test]
630 fn test_prepare_rename_keyword() {
631 check_prepare(r"struct$0 Foo;", expect![[r#"No references found at position"#]]);
632 }
633
594 #[test] 634 #[test]
595 fn test_rename_to_underscore() { 635 fn test_rename_to_underscore() {
596 check("_", r#"fn main() { let i$0 = 1; }"#, r#"fn main() { let _ = 1; }"#); 636 check("_", r#"fn main() { let i$0 = 1; }"#, r#"fn main() { let _ = 1; }"#);
diff --git a/crates/ide/src/runnables.rs b/crates/ide/src/runnables.rs
index 280565563..27d35de5b 100644
--- a/crates/ide/src/runnables.rs
+++ b/crates/ide/src/runnables.rs
@@ -1,10 +1,17 @@
1use std::fmt; 1use std::fmt;
2 2
3use ast::NameOwner;
3use cfg::CfgExpr; 4use cfg::CfgExpr;
4use hir::{AsAssocItem, HasAttrs, HasSource, Semantics}; 5use hir::{AsAssocItem, HasAttrs, HasSource, HirDisplay, Semantics};
5use ide_assists::utils::test_related_attribute; 6use ide_assists::utils::test_related_attribute;
6use ide_db::{defs::Definition, RootDatabase, SymbolKind}; 7use ide_db::{
8 base_db::{FilePosition, FileRange},
9 defs::Definition,
10 search::SearchScope,
11 RootDatabase, SymbolKind,
12};
7use itertools::Itertools; 13use itertools::Itertools;
14use rustc_hash::FxHashSet;
8use syntax::{ 15use syntax::{
9 ast::{self, AstNode, AttrsOwner}, 16 ast::{self, AstNode, AttrsOwner},
10 match_ast, SyntaxNode, 17 match_ast, SyntaxNode,
@@ -12,17 +19,17 @@ use syntax::{
12 19
13use crate::{ 20use crate::{
14 display::{ToNav, TryToNav}, 21 display::{ToNav, TryToNav},
15 FileId, NavigationTarget, 22 references, FileId, NavigationTarget,
16}; 23};
17 24
18#[derive(Debug, Clone)] 25#[derive(Debug, Clone, Hash, PartialEq, Eq)]
19pub struct Runnable { 26pub struct Runnable {
20 pub nav: NavigationTarget, 27 pub nav: NavigationTarget,
21 pub kind: RunnableKind, 28 pub kind: RunnableKind,
22 pub cfg: Option<CfgExpr>, 29 pub cfg: Option<CfgExpr>,
23} 30}
24 31
25#[derive(Debug, Clone)] 32#[derive(Debug, Clone, Hash, PartialEq, Eq)]
26pub enum TestId { 33pub enum TestId {
27 Name(String), 34 Name(String),
28 Path(String), 35 Path(String),
@@ -37,7 +44,7 @@ impl fmt::Display for TestId {
37 } 44 }
38} 45}
39 46
40#[derive(Debug, Clone)] 47#[derive(Debug, Clone, Hash, PartialEq, Eq)]
41pub enum RunnableKind { 48pub enum RunnableKind {
42 Test { test_id: TestId, attr: TestAttr }, 49 Test { test_id: TestId, attr: TestAttr },
43 TestMod { path: String }, 50 TestMod { path: String },
@@ -105,6 +112,105 @@ pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Vec<Runnable> {
105 res 112 res
106} 113}
107 114
115// Feature: Related Tests
116//
117// Provides a sneak peek of all tests where the current item is used.
118//
119// The simplest way to use this feature is via the context menu:
120// - Right-click on the selected item. The context menu opens.
121// - Select **Peek related tests**
122//
123// |===
124// | Editor | Action Name
125//
126// | VS Code | **Rust Analyzer: Peek related tests**
127// |===
128pub(crate) fn related_tests(
129 db: &RootDatabase,
130 position: FilePosition,
131 search_scope: Option<SearchScope>,
132) -> Vec<Runnable> {
133 let sema = Semantics::new(db);
134 let mut res: FxHashSet<Runnable> = FxHashSet::default();
135
136 find_related_tests(&sema, position, search_scope, &mut res);
137
138 res.into_iter().collect_vec()
139}
140
141fn find_related_tests(
142 sema: &Semantics<RootDatabase>,
143 position: FilePosition,
144 search_scope: Option<SearchScope>,
145 tests: &mut FxHashSet<Runnable>,
146) {
147 if let Some(refs) = references::find_all_refs(&sema, position, search_scope) {
148 for (file_id, refs) in refs.references {
149 let file = sema.parse(file_id);
150 let file = file.syntax();
151 let functions = refs.iter().filter_map(|(range, _)| {
152 let token = file.token_at_offset(range.start()).next()?;
153 let token = sema.descend_into_macros(token);
154 let syntax = token.parent();
155 syntax.ancestors().find_map(ast::Fn::cast)
156 });
157
158 for fn_def in functions {
159 if let Some(runnable) = as_test_runnable(&sema, &fn_def) {
160 // direct test
161 tests.insert(runnable);
162 } else if let Some(module) = parent_test_module(&sema, &fn_def) {
163 // indirect test
164 find_related_tests_in_module(sema, &fn_def, &module, tests);
165 }
166 }
167 }
168 }
169}
170
171fn find_related_tests_in_module(
172 sema: &Semantics<RootDatabase>,
173 fn_def: &ast::Fn,
174 parent_module: &hir::Module,
175 tests: &mut FxHashSet<Runnable>,
176) {
177 if let Some(fn_name) = fn_def.name() {
178 let mod_source = parent_module.definition_source(sema.db);
179 let range = match mod_source.value {
180 hir::ModuleSource::Module(m) => m.syntax().text_range(),
181 hir::ModuleSource::BlockExpr(b) => b.syntax().text_range(),
182 hir::ModuleSource::SourceFile(f) => f.syntax().text_range(),
183 };
184
185 let file_id = mod_source.file_id.original_file(sema.db);
186 let mod_scope = SearchScope::file_range(FileRange { file_id, range });
187 let fn_pos = FilePosition { file_id, offset: fn_name.syntax().text_range().start() };
188 find_related_tests(sema, fn_pos, Some(mod_scope), tests)
189 }
190}
191
192fn as_test_runnable(sema: &Semantics<RootDatabase>, fn_def: &ast::Fn) -> Option<Runnable> {
193 if test_related_attribute(&fn_def).is_some() {
194 let function = sema.to_def(fn_def)?;
195 runnable_fn(sema, function)
196 } else {
197 None
198 }
199}
200
201fn parent_test_module(sema: &Semantics<RootDatabase>, fn_def: &ast::Fn) -> Option<hir::Module> {
202 fn_def.syntax().ancestors().find_map(|node| {
203 let module = ast::Module::cast(node)?;
204 let module = sema.to_def(&module)?;
205
206 if has_test_function_or_multiple_test_submodules(sema, &module) {
207 Some(module)
208 } else {
209 None
210 }
211 })
212}
213
108fn runnables_mod(sema: &Semantics<RootDatabase>, acc: &mut Vec<Runnable>, module: hir::Module) { 214fn runnables_mod(sema: &Semantics<RootDatabase>, acc: &mut Vec<Runnable>, module: hir::Module) {
109 acc.extend(module.declarations(sema.db).into_iter().filter_map(|def| { 215 acc.extend(module.declarations(sema.db).into_iter().filter_map(|def| {
110 let runnable = match def { 216 let runnable = match def {
@@ -234,11 +340,21 @@ fn module_def_doctest(sema: &Semantics<RootDatabase>, def: hir::ModuleDef) -> Op
234 // FIXME: this also looks very wrong 340 // FIXME: this also looks very wrong
235 if let Some(assoc_def) = assoc_def { 341 if let Some(assoc_def) = assoc_def {
236 if let hir::AssocItemContainer::Impl(imp) = assoc_def.container(sema.db) { 342 if let hir::AssocItemContainer::Impl(imp) = assoc_def.container(sema.db) {
237 if let Some(adt) = imp.target_ty(sema.db).as_adt() { 343 let ty = imp.target_ty(sema.db);
238 let name = adt.name(sema.db).to_string(); 344 if let Some(adt) = ty.as_adt() {
345 let name = adt.name(sema.db);
239 let idx = path.rfind(':').map_or(0, |idx| idx + 1); 346 let idx = path.rfind(':').map_or(0, |idx| idx + 1);
240 let (prefix, suffix) = path.split_at(idx); 347 let (prefix, suffix) = path.split_at(idx);
241 return format!("{}{}::{}", prefix, name, suffix); 348 let mut ty_params = ty.type_parameters().peekable();
349 let params = if ty_params.peek().is_some() {
350 format!(
351 "<{}>",
352 ty_params.format_with(", ", |ty, cb| cb(&ty.display(sema.db)))
353 )
354 } else {
355 String::new()
356 };
357 return format!("{}{}{}::{}", prefix, name, params, suffix);
242 } 358 }
243 } 359 }
244 } 360 }
@@ -256,7 +372,7 @@ fn module_def_doctest(sema: &Semantics<RootDatabase>, def: hir::ModuleDef) -> Op
256 Some(res) 372 Some(res)
257} 373}
258 374
259#[derive(Debug, Copy, Clone)] 375#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
260pub struct TestAttr { 376pub struct TestAttr {
261 pub ignore: bool, 377 pub ignore: bool,
262} 378}
@@ -349,6 +465,12 @@ mod tests {
349 ); 465 );
350 } 466 }
351 467
468 fn check_tests(ra_fixture: &str, expect: Expect) {
469 let (analysis, position) = fixture::position(ra_fixture);
470 let tests = analysis.related_tests(position, None).unwrap();
471 expect.assert_debug_eq(&tests);
472 }
473
352 #[test] 474 #[test]
353 fn test_runnables() { 475 fn test_runnables() {
354 check( 476 check(
@@ -1074,4 +1196,261 @@ mod tests {
1074 "#]], 1196 "#]],
1075 ); 1197 );
1076 } 1198 }
1199
1200 #[test]
1201 fn find_no_tests() {
1202 check_tests(
1203 r#"
1204//- /lib.rs
1205fn foo$0() { };
1206"#,
1207 expect![[r#"
1208 []
1209 "#]],
1210 );
1211 }
1212
1213 #[test]
1214 fn find_direct_fn_test() {
1215 check_tests(
1216 r#"
1217//- /lib.rs
1218fn foo$0() { };
1219
1220mod tests {
1221 #[test]
1222 fn foo_test() {
1223 super::foo()
1224 }
1225}
1226"#,
1227 expect![[r#"
1228 [
1229 Runnable {
1230 nav: NavigationTarget {
1231 file_id: FileId(
1232 0,
1233 ),
1234 full_range: 31..85,
1235 focus_range: 46..54,
1236 name: "foo_test",
1237 kind: Function,
1238 },
1239 kind: Test {
1240 test_id: Path(
1241 "tests::foo_test",
1242 ),
1243 attr: TestAttr {
1244 ignore: false,
1245 },
1246 },
1247 cfg: None,
1248 },
1249 ]
1250 "#]],
1251 );
1252 }
1253
1254 #[test]
1255 fn find_direct_struct_test() {
1256 check_tests(
1257 r#"
1258//- /lib.rs
1259struct Fo$0o;
1260fn foo(arg: &Foo) { };
1261
1262mod tests {
1263 use super::*;
1264
1265 #[test]
1266 fn foo_test() {
1267 foo(Foo);
1268 }
1269}
1270"#,
1271 expect![[r#"
1272 [
1273 Runnable {
1274 nav: NavigationTarget {
1275 file_id: FileId(
1276 0,
1277 ),
1278 full_range: 71..122,
1279 focus_range: 86..94,
1280 name: "foo_test",
1281 kind: Function,
1282 },
1283 kind: Test {
1284 test_id: Path(
1285 "tests::foo_test",
1286 ),
1287 attr: TestAttr {
1288 ignore: false,
1289 },
1290 },
1291 cfg: None,
1292 },
1293 ]
1294 "#]],
1295 );
1296 }
1297
1298 #[test]
1299 fn find_indirect_fn_test() {
1300 check_tests(
1301 r#"
1302//- /lib.rs
1303fn foo$0() { };
1304
1305mod tests {
1306 use super::foo;
1307
1308 fn check1() {
1309 check2()
1310 }
1311
1312 fn check2() {
1313 foo()
1314 }
1315
1316 #[test]
1317 fn foo_test() {
1318 check1()
1319 }
1320}
1321"#,
1322 expect![[r#"
1323 [
1324 Runnable {
1325 nav: NavigationTarget {
1326 file_id: FileId(
1327 0,
1328 ),
1329 full_range: 133..183,
1330 focus_range: 148..156,
1331 name: "foo_test",
1332 kind: Function,
1333 },
1334 kind: Test {
1335 test_id: Path(
1336 "tests::foo_test",
1337 ),
1338 attr: TestAttr {
1339 ignore: false,
1340 },
1341 },
1342 cfg: None,
1343 },
1344 ]
1345 "#]],
1346 );
1347 }
1348
1349 #[test]
1350 fn tests_are_unique() {
1351 check_tests(
1352 r#"
1353//- /lib.rs
1354fn foo$0() { };
1355
1356mod tests {
1357 use super::foo;
1358
1359 #[test]
1360 fn foo_test() {
1361 foo();
1362 foo();
1363 }
1364
1365 #[test]
1366 fn foo2_test() {
1367 foo();
1368 foo();
1369 }
1370
1371}
1372"#,
1373 expect![[r#"
1374 [
1375 Runnable {
1376 nav: NavigationTarget {
1377 file_id: FileId(
1378 0,
1379 ),
1380 full_range: 52..115,
1381 focus_range: 67..75,
1382 name: "foo_test",
1383 kind: Function,
1384 },
1385 kind: Test {
1386 test_id: Path(
1387 "tests::foo_test",
1388 ),
1389 attr: TestAttr {
1390 ignore: false,
1391 },
1392 },
1393 cfg: None,
1394 },
1395 Runnable {
1396 nav: NavigationTarget {
1397 file_id: FileId(
1398 0,
1399 ),
1400 full_range: 121..185,
1401 focus_range: 136..145,
1402 name: "foo2_test",
1403 kind: Function,
1404 },
1405 kind: Test {
1406 test_id: Path(
1407 "tests::foo2_test",
1408 ),
1409 attr: TestAttr {
1410 ignore: false,
1411 },
1412 },
1413 cfg: None,
1414 },
1415 ]
1416 "#]],
1417 );
1418 }
1419
1420 #[test]
1421 fn doc_test_type_params() {
1422 check(
1423 r#"
1424//- /lib.rs
1425$0
1426struct Foo<T, U>;
1427
1428impl<T, U> Foo<T, U> {
1429 /// ```rust
1430 /// ````
1431 fn t() {}
1432}
1433"#,
1434 &[&DOCTEST],
1435 expect![[r#"
1436 [
1437 Runnable {
1438 nav: NavigationTarget {
1439 file_id: FileId(
1440 0,
1441 ),
1442 full_range: 47..85,
1443 name: "t",
1444 },
1445 kind: DocTest {
1446 test_id: Path(
1447 "Foo<T, U>::t",
1448 ),
1449 },
1450 cfg: None,
1451 },
1452 ]
1453 "#]],
1454 );
1455 }
1077} 1456}