From dd788255b454b93bc8ca8699bac7f28cd49af744 Mon Sep 17 00:00:00 2001 From: roife Date: Wed, 25 Dec 2024 15:50:35 +0800 Subject: [PATCH 01/10] feat: Add TestDefs to find usage of Expect, Insta and Snapbox --- crates/hir/src/lib.rs | 6 ++ crates/ide/src/runnables.rs | 183 +++++++++++++++++++++++++++++++----- 2 files changed, 168 insertions(+), 21 deletions(-) diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs index ac8a62ee8506..be424c2cd9c3 100644 --- a/crates/hir/src/lib.rs +++ b/crates/hir/src/lib.rs @@ -5917,6 +5917,12 @@ impl HasCrate for Adt { } } +impl HasCrate for Impl { + fn krate(&self, db: &dyn HirDatabase) -> Crate { + self.module(db).krate() + } +} + impl HasCrate for Module { fn krate(&self, _: &dyn HirDatabase) -> Crate { Module::krate(*self) diff --git a/crates/ide/src/runnables.rs b/crates/ide/src/runnables.rs index d385e453e213..487cd6daeba8 100644 --- a/crates/ide/src/runnables.rs +++ b/crates/ide/src/runnables.rs @@ -1,4 +1,4 @@ -use std::fmt; +use std::{fmt, ops::Not}; use ast::HasName; use cfg::{CfgAtom, CfgExpr}; @@ -15,6 +15,7 @@ use ide_db::{ FilePosition, FxHashMap, FxHashSet, RootDatabase, SymbolKind, }; use itertools::Itertools; +use smallvec::SmallVec; use span::{Edition, TextSize}; use stdx::format_to; use syntax::{ @@ -30,6 +31,7 @@ pub struct Runnable { pub nav: NavigationTarget, pub kind: RunnableKind, pub cfg: Option, + pub update_test: UpdateTest, } #[derive(Debug, Clone, Hash, PartialEq, Eq)] @@ -334,14 +336,19 @@ pub(crate) fn runnable_fn( } }; + let fn_source = def.source(sema.db)?; let nav = NavigationTarget::from_named( sema.db, - def.source(sema.db)?.as_ref().map(|it| it as &dyn ast::HasName), + fn_source.as_ref().map(|it| it as &dyn ast::HasName), SymbolKind::Function, ) .call_site(); + + let file_range = fn_source.syntax().original_file_range_with_macro_call_body(sema.db); + let update_test = TestDefs::new(sema, def.krate(sema.db), file_range).update_test(); + let cfg = def.attrs(sema.db).cfg(); - Some(Runnable { use_name_in_title: false, nav, kind, cfg }) + Some(Runnable { use_name_in_title: false, nav, kind, cfg, update_test }) } pub(crate) fn runnable_mod( @@ -366,7 +373,22 @@ pub(crate) fn runnable_mod( let attrs = def.attrs(sema.db); let cfg = attrs.cfg(); let nav = NavigationTarget::from_module_to_decl(sema.db, def).call_site(); - Some(Runnable { use_name_in_title: false, nav, kind: RunnableKind::TestMod { path }, cfg }) + + let file_range = { + let src = def.definition_source(sema.db); + let file_id = src.file_id.original_file(sema.db); + let range = src.file_syntax(sema.db).text_range(); + hir::FileRange { file_id, range } + }; + let update_test = TestDefs::new(sema, def.krate(), file_range).update_test(); + + Some(Runnable { + use_name_in_title: false, + nav, + kind: RunnableKind::TestMod { path }, + cfg, + update_test, + }) } pub(crate) fn runnable_impl( @@ -392,7 +414,17 @@ pub(crate) fn runnable_impl( test_id.retain(|c| c != ' '); let test_id = TestId::Path(test_id); - Some(Runnable { use_name_in_title: false, nav, kind: RunnableKind::DocTest { test_id }, cfg }) + let impl_source = + def.source(sema.db)?.syntax().original_file_range_with_macro_call_body(sema.db); + let update_test = TestDefs::new(sema, def.krate(sema.db), impl_source).update_test(); + + Some(Runnable { + use_name_in_title: false, + nav, + kind: RunnableKind::DocTest { test_id }, + cfg, + update_test, + }) } fn has_cfg_test(attrs: AttrsWithOwner) -> bool { @@ -404,6 +436,8 @@ fn runnable_mod_outline_definition( sema: &Semantics<'_, RootDatabase>, def: hir::Module, ) -> Option { + def.as_source_file_id(sema.db)?; + if !has_test_function_or_multiple_test_submodules(sema, &def, has_cfg_test(def.attrs(sema.db))) { return None; @@ -421,16 +455,22 @@ fn runnable_mod_outline_definition( let attrs = def.attrs(sema.db); let cfg = attrs.cfg(); - if def.as_source_file_id(sema.db).is_some() { - Some(Runnable { - use_name_in_title: false, - nav: def.to_nav(sema.db).call_site(), - kind: RunnableKind::TestMod { path }, - cfg, - }) - } else { - None - } + + let file_range = { + let src = def.definition_source(sema.db); + let file_id = src.file_id.original_file(sema.db); + let range = src.file_syntax(sema.db).text_range(); + hir::FileRange { file_id, range } + }; + let update_test = TestDefs::new(sema, def.krate(), file_range).update_test(); + + Some(Runnable { + use_name_in_title: false, + nav: def.to_nav(sema.db).call_site(), + kind: RunnableKind::TestMod { path }, + cfg, + update_test, + }) } fn module_def_doctest(db: &RootDatabase, def: Definition) -> Option { @@ -495,6 +535,7 @@ fn module_def_doctest(db: &RootDatabase, def: Definition) -> Option { nav, kind: RunnableKind::DocTest { test_id }, cfg: attrs.cfg(), + update_test: UpdateTest::default(), }; Some(res) } @@ -575,6 +616,106 @@ fn has_test_function_or_multiple_test_submodules( number_of_test_submodules > 1 } +struct TestDefs<'a, 'b>(&'a Semantics<'b, RootDatabase>, hir::Crate, hir::FileRange); + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub struct UpdateTest { + pub expect_test: bool, + pub insta: bool, + pub snapbox: bool, +} + +impl UpdateTest { + pub fn label(&self) -> Option { + let mut builder: SmallVec<[_; 3]> = SmallVec::new(); + if self.expect_test { + builder.push("Expect"); + } + if self.insta { + builder.push("Insta"); + } + if self.snapbox { + builder.push("Snapbox"); + } + + let res: SmolStr = builder.join(" + ").into(); + res.is_empty().not().then_some(res) + } +} + +impl<'a, 'b> TestDefs<'a, 'b> { + fn new( + sema: &'a Semantics<'b, RootDatabase>, + current_krate: hir::Crate, + file_range: hir::FileRange, + ) -> Self { + Self(sema, current_krate, file_range) + } + + fn update_test(&self) -> UpdateTest { + UpdateTest { expect_test: self.expect_test(), insta: self.insta(), snapbox: self.snapbox() } + } + + fn expect_test(&self) -> bool { + self.find_macro("expect_test:expect") || self.find_macro("expect_test::expect_file") + } + + fn insta(&self) -> bool { + self.find_macro("insta:assert_snapshot") + || self.find_macro("insta:assert_debug_snapshot") + || self.find_macro("insta:assert_display_snapshot") + || self.find_macro("insta:assert_json_snapshot") + || self.find_macro("insta:assert_yaml_snapshot") + || self.find_macro("insta:assert_ron_snapshot") + || self.find_macro("insta:assert_toml_snapshot") + || self.find_macro("insta:assert_csv_snapshot") + || self.find_macro("insta:assert_compact_json_snapshot") + || self.find_macro("insta:assert_compact_debug_snapshot") + || self.find_macro("insta:assert_binary_snapshot") + } + + fn snapbox(&self) -> bool { + self.find_macro("snapbox:assert_data_eq") + || self.find_macro("snapbox:file") + || self.find_macro("snapbox:str") + } + + fn find_macro(&self, path: &str) -> bool { + let Some(hir::ScopeDef::ModuleDef(hir::ModuleDef::Macro(it))) = self.find_def(path) else { + return false; + }; + + Definition::Macro(it) + .usages(self.0) + .in_scope(&SearchScope::file_range(self.2)) + .at_least_one() + } + + fn find_def(&self, path: &str) -> Option { + let db = self.0.db; + + let mut path = path.split(':'); + let item = path.next_back()?; + let krate = path.next()?; + let dep = self.1.dependencies(db).into_iter().find(|dep| dep.name.eq_ident(krate))?; + + let mut module = dep.krate.root_module(); + for segment in path { + module = module.children(db).find_map(|child| { + let name = child.name(db)?; + if name.eq_ident(segment) { + Some(child) + } else { + None + } + })?; + } + + let (_, def) = module.scope(db, None).into_iter().find(|(name, _)| name.eq_ident(item))?; + Some(def) + } +} + #[cfg(test)] mod tests { use expect_test::{expect, Expect}; @@ -1337,18 +1478,18 @@ mod tests { file_id: FileId( 0, ), - full_range: 52..115, - focus_range: 67..75, - name: "foo_test", + full_range: 121..185, + focus_range: 136..145, + name: "foo2_test", kind: Function, }, NavigationTarget { file_id: FileId( 0, ), - full_range: 121..185, - focus_range: 136..145, - name: "foo2_test", + full_range: 52..115, + focus_range: 67..75, + name: "foo_test", kind: Function, }, ] From edb61b10ab49359dfdd82aafeef6cb4c5c1531ae Mon Sep 17 00:00:00 2001 From: roife Date: Wed, 25 Dec 2024 15:56:06 +0800 Subject: [PATCH 02/10] feat: support UpdateTest in codelens --- crates/ide/src/annotations.rs | 68 +++++++++++++++++++----- crates/ide/src/hover/tests.rs | 52 ++++++++++-------- crates/rust-analyzer/src/config.rs | 10 +++- crates/rust-analyzer/src/lsp/ext.rs | 12 ++--- crates/rust-analyzer/src/lsp/to_proto.rs | 38 ++++++++++++- docs/dev/lsp-extensions.md | 2 +- docs/user/generated_config.adoc | 6 +++ editors/code/package.json | 15 ++++++ editors/code/src/client.ts | 17 +++--- editors/code/src/commands.ts | 26 +++++++++ editors/code/src/config.ts | 7 +++ editors/code/src/main.ts | 8 +-- 12 files changed, 204 insertions(+), 57 deletions(-) diff --git a/crates/ide/src/annotations.rs b/crates/ide/src/annotations.rs index 121a463c9f15..6a4e5ba290ec 100644 --- a/crates/ide/src/annotations.rs +++ b/crates/ide/src/annotations.rs @@ -316,6 +316,11 @@ fn main() { }, kind: Bin, cfg: None, + update_test: UpdateTest { + expect_test: false, + insta: false, + snapbox: false, + }, }, ), }, @@ -401,6 +406,11 @@ fn main() { }, kind: Bin, cfg: None, + update_test: UpdateTest { + expect_test: false, + insta: false, + snapbox: false, + }, }, ), }, @@ -537,6 +547,11 @@ fn main() { }, kind: Bin, cfg: None, + update_test: UpdateTest { + expect_test: false, + insta: false, + snapbox: false, + }, }, ), }, @@ -597,6 +612,11 @@ fn main() {} }, kind: Bin, cfg: None, + update_test: UpdateTest { + expect_test: false, + insta: false, + snapbox: false, + }, }, ), }, @@ -709,6 +729,11 @@ fn main() { }, kind: Bin, cfg: None, + update_test: UpdateTest { + expect_test: false, + insta: false, + snapbox: false, + }, }, ), }, @@ -744,6 +769,20 @@ mod tests { "#, expect![[r#" [ + Annotation { + range: 3..7, + kind: HasReferences { + pos: FilePositionWrapper { + file_id: FileId( + 0, + ), + offset: 3, + }, + data: Some( + [], + ), + }, + }, Annotation { range: 3..7, kind: Runnable( @@ -760,23 +799,14 @@ mod tests { }, kind: Bin, cfg: None, + update_test: UpdateTest { + expect_test: false, + insta: false, + snapbox: false, + }, }, ), }, - Annotation { - range: 3..7, - kind: HasReferences { - pos: FilePositionWrapper { - file_id: FileId( - 0, - ), - offset: 3, - }, - data: Some( - [], - ), - }, - }, Annotation { range: 18..23, kind: Runnable( @@ -796,6 +826,11 @@ mod tests { path: "tests", }, cfg: None, + update_test: UpdateTest { + expect_test: false, + insta: false, + snapbox: false, + }, }, ), }, @@ -822,6 +857,11 @@ mod tests { }, }, cfg: None, + update_test: UpdateTest { + expect_test: false, + insta: false, + snapbox: false, + }, }, ), }, diff --git a/crates/ide/src/hover/tests.rs b/crates/ide/src/hover/tests.rs index ed8cd64cdbee..a7a5b8fb5a1d 100644 --- a/crates/ide/src/hover/tests.rs +++ b/crates/ide/src/hover/tests.rs @@ -3213,6 +3213,11 @@ fn foo_$0test() {} }, }, cfg: None, + update_test: UpdateTest { + expect_test: false, + insta: false, + snapbox: false, + }, }, ), ] @@ -3230,28 +3235,33 @@ mod tests$0 { } "#, expect![[r#" - [ - Runnable( - Runnable { - use_name_in_title: false, - nav: NavigationTarget { - file_id: FileId( - 0, - ), - full_range: 0..46, - focus_range: 4..9, - name: "tests", - kind: Module, - description: "mod tests", - }, - kind: TestMod { - path: "tests", - }, - cfg: None, + [ + Runnable( + Runnable { + use_name_in_title: false, + nav: NavigationTarget { + file_id: FileId( + 0, + ), + full_range: 0..46, + focus_range: 4..9, + name: "tests", + kind: Module, + description: "mod tests", }, - ), - ] - "#]], + kind: TestMod { + path: "tests", + }, + cfg: None, + update_test: UpdateTest { + expect_test: false, + insta: false, + snapbox: false, + }, + }, + ), + ] + "#]], ); } diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index c182952c731d..1b37ab0aab2d 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -243,6 +243,9 @@ config_data! { /// Whether to show `Run` lens. Only applies when /// `#rust-analyzer.lens.enable#` is set. lens_run_enable: bool = true, + /// Whether to show `Update Test` lens. Only applies when + /// `#rust-analyzer.lens.enable#` and `#rust-analyzer.lens.run.enable#` are set. + lens_update_test_enable: bool = true, /// Disable project auto-discovery in favor of explicitly specified set /// of projects. @@ -1161,6 +1164,7 @@ pub struct LensConfig { // runnables pub run: bool, pub debug: bool, + pub update_test: bool, pub interpret: bool, // implementations @@ -1196,6 +1200,7 @@ impl LensConfig { pub fn any(&self) -> bool { self.run || self.debug + || self.update_test || self.implementations || self.method_refs || self.refs_adt @@ -1208,7 +1213,7 @@ impl LensConfig { } pub fn runnable(&self) -> bool { - self.run || self.debug + self.run || self.debug || self.update_test } pub fn references(&self) -> bool { @@ -2120,6 +2125,9 @@ impl Config { LensConfig { run: *self.lens_enable() && *self.lens_run_enable(), debug: *self.lens_enable() && *self.lens_debug_enable(), + update_test: *self.lens_enable() + && *self.lens_update_test_enable() + && *self.lens_run_enable(), interpret: *self.lens_enable() && *self.lens_run_enable() && *self.interpret_tests(), implementations: *self.lens_enable() && *self.lens_implementations_enable(), method_refs: *self.lens_enable() && *self.lens_references_method_enable(), diff --git a/crates/rust-analyzer/src/lsp/ext.rs b/crates/rust-analyzer/src/lsp/ext.rs index c0173d9c2470..e1677cbcda80 100644 --- a/crates/rust-analyzer/src/lsp/ext.rs +++ b/crates/rust-analyzer/src/lsp/ext.rs @@ -427,14 +427,14 @@ impl Request for Runnables { const METHOD: &'static str = "experimental/runnables"; } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct RunnablesParams { pub text_document: TextDocumentIdentifier, pub position: Option, } -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Runnable { pub label: String, @@ -444,7 +444,7 @@ pub struct Runnable { pub args: RunnableArgs, } -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] #[serde(untagged)] pub enum RunnableArgs { @@ -452,14 +452,14 @@ pub enum RunnableArgs { Shell(ShellRunnableArgs), } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "lowercase")] pub enum RunnableKind { Cargo, Shell, } -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct CargoRunnableArgs { #[serde(skip_serializing_if = "FxHashMap::is_empty")] @@ -475,7 +475,7 @@ pub struct CargoRunnableArgs { pub executable_args: Vec, } -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct ShellRunnableArgs { #[serde(skip_serializing_if = "FxHashMap::is_empty")] diff --git a/crates/rust-analyzer/src/lsp/to_proto.rs b/crates/rust-analyzer/src/lsp/to_proto.rs index 05e93b4e6acf..091ac773323a 100644 --- a/crates/rust-analyzer/src/lsp/to_proto.rs +++ b/crates/rust-analyzer/src/lsp/to_proto.rs @@ -20,6 +20,7 @@ use itertools::Itertools; use paths::{Utf8Component, Utf8Prefix}; use semver::VersionReq; use serde_json::to_value; +use syntax::SmolStr; use vfs::AbsPath; use crate::{ @@ -1567,6 +1568,7 @@ pub(crate) fn code_lens( let line_index = snap.file_line_index(run.nav.file_id)?; let annotation_range = range(&line_index, annotation.range); + let update_test = run.update_test; let title = run.title(); let can_debug = match run.kind { ide::RunnableKind::DocTest { .. } => false, @@ -1602,6 +1604,17 @@ pub(crate) fn code_lens( data: None, }) } + if lens_config.update_test && client_commands_config.run_single { + let label = update_test.label(); + if let Some(r) = make_update_runnable(&r, &label) { + let command = command::run_single(&r, label.unwrap().as_str()); + acc.push(lsp_types::CodeLens { + range: annotation_range, + command: Some(command), + data: None, + }) + } + } } if lens_config.interpret { @@ -1786,7 +1799,7 @@ pub(crate) mod command { pub(crate) fn debug_single(runnable: &lsp_ext::Runnable) -> lsp_types::Command { lsp_types::Command { - title: "Debug".into(), + title: "⚙\u{fe0e} Debug".into(), command: "rust-analyzer.debugSingle".into(), arguments: Some(vec![to_value(runnable).unwrap()]), } @@ -1838,6 +1851,29 @@ pub(crate) mod command { } } +fn make_update_runnable( + runnable: &lsp_ext::Runnable, + label: &Option, +) -> Option { + if !matches!(runnable.args, lsp_ext::RunnableArgs::Cargo(_)) { + return None; + } + let label = label.as_ref()?; + + let mut runnable = runnable.clone(); + runnable.label = format!("{} + {}", runnable.label, label); + + let lsp_ext::RunnableArgs::Cargo(r) = &mut runnable.args else { + unreachable!(); + }; + + let environment_vars = + [("UPDATE_EXPECT", "1"), ("INSTA_UPDATE", "always"), ("SNAPSHOTS", "overwrite")]; + r.environment.extend(environment_vars.into_iter().map(|(k, v)| (k.to_owned(), v.to_owned()))); + + Some(runnable) +} + pub(crate) fn implementation_title(count: usize) -> String { if count == 1 { "1 implementation".into() diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md index 0e37611a5493..826ce1124486 100644 --- a/docs/dev/lsp-extensions.md +++ b/docs/dev/lsp-extensions.md @@ -1,5 +1,5 @@