aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/cfg/src/cfg_expr.rs2
-rw-r--r--crates/ide/src/lib.rs9
-rw-r--r--crates/ide/src/runnables.rs344
-rw-r--r--crates/ide_db/src/search.rs4
-rw-r--r--crates/rust-analyzer/src/handlers.rs30
-rw-r--r--crates/rust-analyzer/src/lsp_ext.rs13
-rw-r--r--crates/rust-analyzer/src/main_loop.rs1
-rw-r--r--crates/rust-analyzer/src/to_proto.rs5
-rw-r--r--docs/dev/lsp-extensions.md18
-rw-r--r--editors/code/package.json14
-rw-r--r--editors/code/src/commands.ts52
-rw-r--r--editors/code/src/lsp_ext.ts6
-rw-r--r--editors/code/src/main.ts1
13 files changed, 471 insertions, 28 deletions
diff --git a/crates/cfg/src/cfg_expr.rs b/crates/cfg/src/cfg_expr.rs
index 42327f1e1..069fc01d0 100644
--- a/crates/cfg/src/cfg_expr.rs
+++ b/crates/cfg/src/cfg_expr.rs
@@ -49,7 +49,7 @@ impl fmt::Display for CfgAtom {
49 } 49 }
50} 50}
51 51
52#[derive(Debug, Clone, PartialEq, Eq)] 52#[derive(Debug, Clone, PartialEq, Eq, Hash)]
53pub enum CfgExpr { 53pub enum CfgExpr {
54 Invalid, 54 Invalid,
55 Atom(CfgAtom), 55 Atom(CfgAtom),
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index 0a493d2f3..a8b169e87 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -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/runnables.rs b/crates/ide/src/runnables.rs
index 280565563..faa91541e 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, 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 {
@@ -256,7 +362,7 @@ fn module_def_doctest(sema: &Semantics<RootDatabase>, def: hir::ModuleDef) -> Op
256 Some(res) 362 Some(res)
257} 363}
258 364
259#[derive(Debug, Copy, Clone)] 365#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
260pub struct TestAttr { 366pub struct TestAttr {
261 pub ignore: bool, 367 pub ignore: bool,
262} 368}
@@ -349,6 +455,12 @@ mod tests {
349 ); 455 );
350 } 456 }
351 457
458 fn check_tests(ra_fixture: &str, expect: Expect) {
459 let (analysis, position) = fixture::position(ra_fixture);
460 let tests = analysis.related_tests(position, None).unwrap();
461 expect.assert_debug_eq(&tests);
462 }
463
352 #[test] 464 #[test]
353 fn test_runnables() { 465 fn test_runnables() {
354 check( 466 check(
@@ -1074,4 +1186,224 @@ mod tests {
1074 "#]], 1186 "#]],
1075 ); 1187 );
1076 } 1188 }
1189
1190 #[test]
1191 fn find_no_tests() {
1192 check_tests(
1193 r#"
1194//- /lib.rs
1195fn foo$0() { };
1196"#,
1197 expect![[r#"
1198 []
1199 "#]],
1200 );
1201 }
1202
1203 #[test]
1204 fn find_direct_fn_test() {
1205 check_tests(
1206 r#"
1207//- /lib.rs
1208fn foo$0() { };
1209
1210mod tests {
1211 #[test]
1212 fn foo_test() {
1213 super::foo()
1214 }
1215}
1216"#,
1217 expect![[r#"
1218 [
1219 Runnable {
1220 nav: NavigationTarget {
1221 file_id: FileId(
1222 0,
1223 ),
1224 full_range: 31..85,
1225 focus_range: 46..54,
1226 name: "foo_test",
1227 kind: Function,
1228 },
1229 kind: Test {
1230 test_id: Path(
1231 "tests::foo_test",
1232 ),
1233 attr: TestAttr {
1234 ignore: false,
1235 },
1236 },
1237 cfg: None,
1238 },
1239 ]
1240 "#]],
1241 );
1242 }
1243
1244 #[test]
1245 fn find_direct_struct_test() {
1246 check_tests(
1247 r#"
1248//- /lib.rs
1249struct Fo$0o;
1250fn foo(arg: &Foo) { };
1251
1252mod tests {
1253 use super::*;
1254
1255 #[test]
1256 fn foo_test() {
1257 foo(Foo);
1258 }
1259}
1260"#,
1261 expect![[r#"
1262 [
1263 Runnable {
1264 nav: NavigationTarget {
1265 file_id: FileId(
1266 0,
1267 ),
1268 full_range: 71..122,
1269 focus_range: 86..94,
1270 name: "foo_test",
1271 kind: Function,
1272 },
1273 kind: Test {
1274 test_id: Path(
1275 "tests::foo_test",
1276 ),
1277 attr: TestAttr {
1278 ignore: false,
1279 },
1280 },
1281 cfg: None,
1282 },
1283 ]
1284 "#]],
1285 );
1286 }
1287
1288 #[test]
1289 fn find_indirect_fn_test() {
1290 check_tests(
1291 r#"
1292//- /lib.rs
1293fn foo$0() { };
1294
1295mod tests {
1296 use super::foo;
1297
1298 fn check1() {
1299 check2()
1300 }
1301
1302 fn check2() {
1303 foo()
1304 }
1305
1306 #[test]
1307 fn foo_test() {
1308 check1()
1309 }
1310}
1311"#,
1312 expect![[r#"
1313 [
1314 Runnable {
1315 nav: NavigationTarget {
1316 file_id: FileId(
1317 0,
1318 ),
1319 full_range: 133..183,
1320 focus_range: 148..156,
1321 name: "foo_test",
1322 kind: Function,
1323 },
1324 kind: Test {
1325 test_id: Path(
1326 "tests::foo_test",
1327 ),
1328 attr: TestAttr {
1329 ignore: false,
1330 },
1331 },
1332 cfg: None,
1333 },
1334 ]
1335 "#]],
1336 );
1337 }
1338
1339 #[test]
1340 fn tests_are_unique() {
1341 check_tests(
1342 r#"
1343//- /lib.rs
1344fn foo$0() { };
1345
1346mod tests {
1347 use super::foo;
1348
1349 #[test]
1350 fn foo_test() {
1351 foo();
1352 foo();
1353 }
1354
1355 #[test]
1356 fn foo2_test() {
1357 foo();
1358 foo();
1359 }
1360
1361}
1362"#,
1363 expect![[r#"
1364 [
1365 Runnable {
1366 nav: NavigationTarget {
1367 file_id: FileId(
1368 0,
1369 ),
1370 full_range: 52..115,
1371 focus_range: 67..75,
1372 name: "foo_test",
1373 kind: Function,
1374 },
1375 kind: Test {
1376 test_id: Path(
1377 "tests::foo_test",
1378 ),
1379 attr: TestAttr {
1380 ignore: false,
1381 },
1382 },
1383 cfg: None,
1384 },
1385 Runnable {
1386 nav: NavigationTarget {
1387 file_id: FileId(
1388 0,
1389 ),
1390 full_range: 121..185,
1391 focus_range: 136..145,
1392 name: "foo2_test",
1393 kind: Function,
1394 },
1395 kind: Test {
1396 test_id: Path(
1397 "tests::foo2_test",
1398 ),
1399 attr: TestAttr {
1400 ignore: false,
1401 },
1402 },
1403 cfg: None,
1404 },
1405 ]
1406 "#]],
1407 );
1408 }
1077} 1409}
diff --git a/crates/ide_db/src/search.rs b/crates/ide_db/src/search.rs
index ddcfbd3f3..fa18703e1 100644
--- a/crates/ide_db/src/search.rs
+++ b/crates/ide_db/src/search.rs
@@ -86,6 +86,10 @@ impl SearchScope {
86 SearchScope::new(std::iter::once((file, None)).collect()) 86 SearchScope::new(std::iter::once((file, None)).collect())
87 } 87 }
88 88
89 pub fn file_range(range: FileRange) -> SearchScope {
90 SearchScope::new(std::iter::once((range.file_id, Some(range.range))).collect())
91 }
92
89 pub fn files(files: &[FileId]) -> SearchScope { 93 pub fn files(files: &[FileId]) -> SearchScope {
90 SearchScope::new(files.iter().map(|f| (*f, None)).collect()) 94 SearchScope::new(files.iter().map(|f| (*f, None)).collect())
91 } 95 }
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs
index 6cc433cb8..706a39dab 100644
--- a/crates/rust-analyzer/src/handlers.rs
+++ b/crates/rust-analyzer/src/handlers.rs
@@ -555,7 +555,7 @@ pub(crate) fn handle_runnables(
555 if should_skip_target(&runnable, cargo_spec.as_ref()) { 555 if should_skip_target(&runnable, cargo_spec.as_ref()) {
556 continue; 556 continue;
557 } 557 }
558 let mut runnable = to_proto::runnable(&snap, file_id, runnable)?; 558 let mut runnable = to_proto::runnable(&snap, runnable)?;
559 if expect_test { 559 if expect_test {
560 runnable.label = format!("{} + expect", runnable.label); 560 runnable.label = format!("{} + expect", runnable.label);
561 runnable.args.expect_test = Some(true); 561 runnable.args.expect_test = Some(true);
@@ -607,6 +607,24 @@ pub(crate) fn handle_runnables(
607 Ok(res) 607 Ok(res)
608} 608}
609 609
610pub(crate) fn handle_related_tests(
611 snap: GlobalStateSnapshot,
612 params: lsp_types::TextDocumentPositionParams,
613) -> Result<Vec<lsp_ext::TestInfo>> {
614 let _p = profile::span("handle_related_tests");
615 let position = from_proto::file_position(&snap, params)?;
616
617 let tests = snap.analysis.related_tests(position, None)?;
618 let mut res = Vec::new();
619 for it in tests {
620 if let Ok(runnable) = to_proto::runnable(&snap, it) {
621 res.push(lsp_ext::TestInfo { runnable })
622 }
623 }
624
625 Ok(res)
626}
627
610pub(crate) fn handle_completion( 628pub(crate) fn handle_completion(
611 snap: GlobalStateSnapshot, 629 snap: GlobalStateSnapshot,
612 params: lsp_types::CompletionParams, 630 params: lsp_types::CompletionParams,
@@ -772,7 +790,7 @@ pub(crate) fn handle_hover(
772 contents: HoverContents::Markup(to_proto::markup_content(info.info.markup)), 790 contents: HoverContents::Markup(to_proto::markup_content(info.info.markup)),
773 range: Some(range), 791 range: Some(range),
774 }, 792 },
775 actions: prepare_hover_actions(&snap, position.file_id, &info.info.actions), 793 actions: prepare_hover_actions(&snap, &info.info.actions),
776 }; 794 };
777 795
778 Ok(Some(hover)) 796 Ok(Some(hover))
@@ -1440,17 +1458,16 @@ fn show_impl_command_link(
1440 1458
1441fn runnable_action_links( 1459fn runnable_action_links(
1442 snap: &GlobalStateSnapshot, 1460 snap: &GlobalStateSnapshot,
1443 file_id: FileId,
1444 runnable: Runnable, 1461 runnable: Runnable,
1445) -> Option<lsp_ext::CommandLinkGroup> { 1462) -> Option<lsp_ext::CommandLinkGroup> {
1446 let cargo_spec = CargoTargetSpec::for_file(&snap, file_id).ok()?; 1463 let cargo_spec = CargoTargetSpec::for_file(&snap, runnable.nav.file_id).ok()?;
1447 let hover_config = snap.config.hover(); 1464 let hover_config = snap.config.hover();
1448 if !hover_config.runnable() || should_skip_target(&runnable, cargo_spec.as_ref()) { 1465 if !hover_config.runnable() || should_skip_target(&runnable, cargo_spec.as_ref()) {
1449 return None; 1466 return None;
1450 } 1467 }
1451 1468
1452 let action: &'static _ = runnable.action(); 1469 let action: &'static _ = runnable.action();
1453 to_proto::runnable(snap, file_id, runnable).ok().map(|r| { 1470 to_proto::runnable(snap, runnable).ok().map(|r| {
1454 let mut group = lsp_ext::CommandLinkGroup::default(); 1471 let mut group = lsp_ext::CommandLinkGroup::default();
1455 1472
1456 if hover_config.run { 1473 if hover_config.run {
@@ -1489,7 +1506,6 @@ fn goto_type_action_links(
1489 1506
1490fn prepare_hover_actions( 1507fn prepare_hover_actions(
1491 snap: &GlobalStateSnapshot, 1508 snap: &GlobalStateSnapshot,
1492 file_id: FileId,
1493 actions: &[HoverAction], 1509 actions: &[HoverAction],
1494) -> Vec<lsp_ext::CommandLinkGroup> { 1510) -> Vec<lsp_ext::CommandLinkGroup> {
1495 if snap.config.hover().none() || !snap.config.hover_actions() { 1511 if snap.config.hover().none() || !snap.config.hover_actions() {
@@ -1500,7 +1516,7 @@ fn prepare_hover_actions(
1500 .iter() 1516 .iter()
1501 .filter_map(|it| match it { 1517 .filter_map(|it| match it {
1502 HoverAction::Implementation(position) => show_impl_command_link(snap, position), 1518 HoverAction::Implementation(position) => show_impl_command_link(snap, position),
1503 HoverAction::Runnable(r) => runnable_action_links(snap, file_id, r.clone()), 1519 HoverAction::Runnable(r) => runnable_action_links(snap, r.clone()),
1504 HoverAction::GoToType(targets) => goto_type_action_links(snap, targets), 1520 HoverAction::GoToType(targets) => goto_type_action_links(snap, targets),
1505 }) 1521 })
1506 .collect() 1522 .collect()
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index 0d2c8f7ff..efcdcd1d9 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -177,6 +177,19 @@ pub struct CargoRunnable {
177 pub expect_test: Option<bool>, 177 pub expect_test: Option<bool>,
178} 178}
179 179
180pub enum RelatedTests {}
181
182impl Request for RelatedTests {
183 type Params = lsp_types::TextDocumentPositionParams;
184 type Result = Vec<TestInfo>;
185 const METHOD: &'static str = "rust-analyzer/relatedTests";
186}
187
188#[derive(Debug, Deserialize, Serialize)]
189pub struct TestInfo {
190 pub runnable: Runnable,
191}
192
180pub enum InlayHints {} 193pub enum InlayHints {}
181 194
182impl Request for InlayHints { 195impl Request for InlayHints {
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs
index f0cb309e4..984790d35 100644
--- a/crates/rust-analyzer/src/main_loop.rs
+++ b/crates/rust-analyzer/src/main_loop.rs
@@ -500,6 +500,7 @@ impl GlobalState {
500 .on::<lsp_ext::ExpandMacro>(handlers::handle_expand_macro) 500 .on::<lsp_ext::ExpandMacro>(handlers::handle_expand_macro)
501 .on::<lsp_ext::ParentModule>(handlers::handle_parent_module) 501 .on::<lsp_ext::ParentModule>(handlers::handle_parent_module)
502 .on::<lsp_ext::Runnables>(handlers::handle_runnables) 502 .on::<lsp_ext::Runnables>(handlers::handle_runnables)
503 .on::<lsp_ext::RelatedTests>(handlers::handle_related_tests)
503 .on::<lsp_ext::InlayHints>(handlers::handle_inlay_hints) 504 .on::<lsp_ext::InlayHints>(handlers::handle_inlay_hints)
504 .on::<lsp_ext::CodeActionRequest>(handlers::handle_code_action) 505 .on::<lsp_ext::CodeActionRequest>(handlers::handle_code_action)
505 .on::<lsp_ext::CodeActionResolveRequest>(handlers::handle_code_action_resolve) 506 .on::<lsp_ext::CodeActionResolveRequest>(handlers::handle_code_action_resolve)
diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs
index 1a8cdadad..9ca0915b9 100644
--- a/crates/rust-analyzer/src/to_proto.rs
+++ b/crates/rust-analyzer/src/to_proto.rs
@@ -838,11 +838,10 @@ pub(crate) fn resolved_code_action(
838 838
839pub(crate) fn runnable( 839pub(crate) fn runnable(
840 snap: &GlobalStateSnapshot, 840 snap: &GlobalStateSnapshot,
841 file_id: FileId,
842 runnable: Runnable, 841 runnable: Runnable,
843) -> Result<lsp_ext::Runnable> { 842) -> Result<lsp_ext::Runnable> {
844 let config = snap.config.runnables(); 843 let config = snap.config.runnables();
845 let spec = CargoTargetSpec::for_file(snap, file_id)?; 844 let spec = CargoTargetSpec::for_file(snap, runnable.nav.file_id)?;
846 let workspace_root = spec.as_ref().map(|it| it.workspace_root.clone()); 845 let workspace_root = spec.as_ref().map(|it| it.workspace_root.clone());
847 let target = spec.as_ref().map(|s| s.target.clone()); 846 let target = spec.as_ref().map(|s| s.target.clone());
848 let (cargo_args, executable_args) = 847 let (cargo_args, executable_args) =
@@ -875,7 +874,7 @@ pub(crate) fn code_lens(
875 let annotation_range = range(&line_index, annotation.range); 874 let annotation_range = range(&line_index, annotation.range);
876 875
877 let action = run.action(); 876 let action = run.action();
878 let r = runnable(&snap, run.nav.file_id, run)?; 877 let r = runnable(&snap, run)?;
879 878
880 let command = if debug { 879 let command = if debug {
881 command::debug_single(&r) 880 command::debug_single(&r)
diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md
index dd3ecc18d..694fafcd5 100644
--- a/docs/dev/lsp-extensions.md
+++ b/docs/dev/lsp-extensions.md
@@ -1,5 +1,5 @@
1<!--- 1<!---
2lsp_ext.rs hash: d279d971d4f62cd7 2lsp_ext.rs hash: 4dfa8d7035f4aee7
3 3
4If you need to change the above hash to make the test pass, please check if you 4If you need to change the above hash to make the test pass, please check if you
5need to adjust this doc as well and ping this issue: 5need to adjust this doc as well and ping this issue:
@@ -579,3 +579,19 @@ This request is sent from client to server to open the current project's Cargo.t
579``` 579```
580 580
581`experimental/openCargoToml` returns a single `Link` to the start of the `[package]` keyword. 581`experimental/openCargoToml` returns a single `Link` to the start of the `[package]` keyword.
582
583## Related tests
584
585This request is sent from client to server to get the list of tests for the specified position.
586
587**Method:** `rust-analyzer/relatedTests`
588
589**Request:** `TextDocumentPositionParams`
590
591**Response:** `TestInfo[]`
592
593```typescript
594interface TestInfo {
595 runnable: Runnable;
596}
597```
diff --git a/editors/code/package.json b/editors/code/package.json
index b29f006f0..923e9b35a 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -203,6 +203,11 @@
203 "command": "rust-analyzer.openCargoToml", 203 "command": "rust-analyzer.openCargoToml",
204 "title": "Open Cargo.toml", 204 "title": "Open Cargo.toml",
205 "category": "Rust Analyzer" 205 "category": "Rust Analyzer"
206 },
207 {
208 "command": "rust-analyzer.peekTests",
209 "title": "Peek related tests",
210 "category": "Rust Analyzer"
206 } 211 }
207 ], 212 ],
208 "keybindings": [ 213 "keybindings": [
@@ -1165,7 +1170,14 @@
1165 "command": "rust-analyzer.openCargoToml", 1170 "command": "rust-analyzer.openCargoToml",
1166 "when": "inRustProject" 1171 "when": "inRustProject"
1167 } 1172 }
1173 ],
1174 "editor/context": [
1175 {
1176 "command": "rust-analyzer.peekTests",
1177 "when": "inRustProject",
1178 "group": "navigation@1000"
1179 }
1168 ] 1180 ]
1169 } 1181 }
1170 } 1182 }
1171} 1183} \ No newline at end of file
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts
index 3e96fbad8..694f445bc 100644
--- a/editors/code/src/commands.ts
+++ b/editors/code/src/commands.ts
@@ -9,6 +9,7 @@ import { RunnableQuickPick, selectRunnable, createTask, createArgs } from './run
9import { AstInspector } from './ast_inspector'; 9import { AstInspector } from './ast_inspector';
10import { isRustDocument, sleep, isRustEditor } from './util'; 10import { isRustDocument, sleep, isRustEditor } from './util';
11import { startDebugSession, makeDebugConfig } from './debug'; 11import { startDebugSession, makeDebugConfig } from './debug';
12import { LanguageClient } from 'vscode-languageclient/node';
12 13
13export * from './ast_inspector'; 14export * from './ast_inspector';
14export * from './run'; 15export * from './run';
@@ -455,17 +456,20 @@ export function reloadWorkspace(ctx: Ctx): Cmd {
455 return async () => ctx.client.sendRequest(ra.reloadWorkspace); 456 return async () => ctx.client.sendRequest(ra.reloadWorkspace);
456} 457}
457 458
459async function showReferencesImpl(client: LanguageClient, uri: string, position: lc.Position, locations: lc.Location[]) {
460 if (client) {
461 await vscode.commands.executeCommand(
462 'editor.action.showReferences',
463 vscode.Uri.parse(uri),
464 client.protocol2CodeConverter.asPosition(position),
465 locations.map(client.protocol2CodeConverter.asLocation),
466 );
467 }
468}
469
458export function showReferences(ctx: Ctx): Cmd { 470export function showReferences(ctx: Ctx): Cmd {
459 return async (uri: string, position: lc.Position, locations: lc.Location[]) => { 471 return async (uri: string, position: lc.Position, locations: lc.Location[]) => {
460 const client = ctx.client; 472 await showReferencesImpl(ctx.client, uri, position, locations);
461 if (client) {
462 await vscode.commands.executeCommand(
463 'editor.action.showReferences',
464 vscode.Uri.parse(uri),
465 client.protocol2CodeConverter.asPosition(position),
466 locations.map(client.protocol2CodeConverter.asLocation),
467 );
468 }
469 }; 473 };
470} 474}
471 475
@@ -554,6 +558,36 @@ export function run(ctx: Ctx): Cmd {
554 }; 558 };
555} 559}
556 560
561export function peekTests(ctx: Ctx): Cmd {
562 const client = ctx.client;
563
564 return async () => {
565 const editor = ctx.activeRustEditor;
566 if (!editor || !client) return;
567
568 await vscode.window.withProgress({
569 location: vscode.ProgressLocation.Notification,
570 title: "Looking for tests...",
571 cancellable: false,
572 }, async (_progress, _token) => {
573 const uri = editor.document.uri.toString();
574 const position = client.code2ProtocolConverter.asPosition(
575 editor.selection.active,
576 );
577
578 const tests = await client.sendRequest(ra.relatedTests, {
579 textDocument: { uri: uri },
580 position: position,
581 });
582 const locations: lc.Location[] = tests.map(it =>
583 lc.Location.create(it.runnable.location!.targetUri, it.runnable.location!.targetSelectionRange));
584
585 await showReferencesImpl(client, uri, position, locations);
586 });
587 };
588}
589
590
557export function runSingle(ctx: Ctx): Cmd { 591export function runSingle(ctx: Ctx): Cmd {
558 return async (runnable: ra.Runnable) => { 592 return async (runnable: ra.Runnable) => {
559 const editor = ctx.activeRustEditor; 593 const editor = ctx.activeRustEditor;
diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts
index 2de1e427d..52de29e04 100644
--- a/editors/code/src/lsp_ext.ts
+++ b/editors/code/src/lsp_ext.ts
@@ -72,6 +72,12 @@ export interface Runnable {
72} 72}
73export const runnables = new lc.RequestType<RunnablesParams, Runnable[], void>("experimental/runnables"); 73export const runnables = new lc.RequestType<RunnablesParams, Runnable[], void>("experimental/runnables");
74 74
75export interface TestInfo {
76 runnable: Runnable;
77}
78
79export const relatedTests = new lc.RequestType<lc.TextDocumentPositionParams, TestInfo[], void>("rust-analyzer/relatedTests");
80
75export type InlayHint = InlayHint.TypeHint | InlayHint.ParamHint | InlayHint.ChainingHint; 81export type InlayHint = InlayHint.TypeHint | InlayHint.ParamHint | InlayHint.ChainingHint;
76 82
77export namespace InlayHint { 83export namespace InlayHint {
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index 1be4f1758..925103f56 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -113,6 +113,7 @@ async function tryActivate(context: vscode.ExtensionContext) {
113 ctx.registerCommand('newDebugConfig', commands.newDebugConfig); 113 ctx.registerCommand('newDebugConfig', commands.newDebugConfig);
114 ctx.registerCommand('openDocs', commands.openDocs); 114 ctx.registerCommand('openDocs', commands.openDocs);
115 ctx.registerCommand('openCargoToml', commands.openCargoToml); 115 ctx.registerCommand('openCargoToml', commands.openCargoToml);
116 ctx.registerCommand('peekTests', commands.peekTests);
116 117
117 defaultOnEnter.dispose(); 118 defaultOnEnter.dispose();
118 ctx.registerCommand('onEnter', commands.onEnter); 119 ctx.registerCommand('onEnter', commands.onEnter);