Merge pull request #18757 from roife/fix-17812

feat: support updating snapshot tests with codelens/hovering/runnables
This commit is contained in:
Lukas Wirth 2025-01-01 12:44:55 +00:00 committed by GitHub
commit a612fc9a16
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 540 additions and 97 deletions

View file

@ -5933,6 +5933,12 @@ impl HasCrate for Adt {
} }
} }
impl HasCrate for Impl {
fn krate(&self, db: &dyn HirDatabase) -> Crate {
self.module(db).krate()
}
}
impl HasCrate for Module { impl HasCrate for Module {
fn krate(&self, _: &dyn HirDatabase) -> Crate { fn krate(&self, _: &dyn HirDatabase) -> Crate {
Module::krate(*self) Module::krate(*self)

View file

@ -316,6 +316,11 @@ fn main() {
}, },
kind: Bin, kind: Bin,
cfg: None, cfg: None,
update_test: UpdateTest {
expect_test: false,
insta: false,
snapbox: false,
},
}, },
), ),
}, },
@ -401,6 +406,11 @@ fn main() {
}, },
kind: Bin, kind: Bin,
cfg: None, cfg: None,
update_test: UpdateTest {
expect_test: false,
insta: false,
snapbox: false,
},
}, },
), ),
}, },
@ -537,6 +547,11 @@ fn main() {
}, },
kind: Bin, kind: Bin,
cfg: None, cfg: None,
update_test: UpdateTest {
expect_test: false,
insta: false,
snapbox: false,
},
}, },
), ),
}, },
@ -597,6 +612,11 @@ fn main() {}
}, },
kind: Bin, kind: Bin,
cfg: None, cfg: None,
update_test: UpdateTest {
expect_test: false,
insta: false,
snapbox: false,
},
}, },
), ),
}, },
@ -709,6 +729,11 @@ fn main() {
}, },
kind: Bin, kind: Bin,
cfg: None, cfg: None,
update_test: UpdateTest {
expect_test: false,
insta: false,
snapbox: false,
},
}, },
), ),
}, },
@ -744,6 +769,20 @@ mod tests {
"#, "#,
expect![[r#" expect![[r#"
[ [
Annotation {
range: 3..7,
kind: HasReferences {
pos: FilePositionWrapper {
file_id: FileId(
0,
),
offset: 3,
},
data: Some(
[],
),
},
},
Annotation { Annotation {
range: 3..7, range: 3..7,
kind: Runnable( kind: Runnable(
@ -760,23 +799,14 @@ mod tests {
}, },
kind: Bin, kind: Bin,
cfg: None, 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 { Annotation {
range: 18..23, range: 18..23,
kind: Runnable( kind: Runnable(
@ -796,6 +826,11 @@ mod tests {
path: "tests", path: "tests",
}, },
cfg: None, cfg: None,
update_test: UpdateTest {
expect_test: false,
insta: false,
snapbox: false,
},
}, },
), ),
}, },
@ -822,6 +857,11 @@ mod tests {
}, },
}, },
cfg: None, cfg: None,
update_test: UpdateTest {
expect_test: false,
insta: false,
snapbox: false,
},
}, },
), ),
}, },

View file

@ -3260,6 +3260,11 @@ fn foo_$0test() {}
}, },
}, },
cfg: None, cfg: None,
update_test: UpdateTest {
expect_test: false,
insta: false,
snapbox: false,
},
}, },
), ),
] ]
@ -3295,6 +3300,11 @@ mod tests$0 {
path: "tests", path: "tests",
}, },
cfg: None, cfg: None,
update_test: UpdateTest {
expect_test: false,
insta: false,
snapbox: false,
},
}, },
), ),
] ]
@ -10029,3 +10039,99 @@ fn bar() {
"#]], "#]],
); );
} }
#[test]
fn test_runnables_with_snapshot_tests() {
check_actions(
r#"
//- /lib.rs crate:foo deps:expect_test,insta,snapbox
use expect_test::expect;
use insta::assert_debug_snapshot;
use snapbox::Assert;
#[test]
fn test$0() {
let actual = "new25";
expect!["new25"].assert_eq(&actual);
Assert::new()
.action_env("SNAPSHOTS")
.eq(actual, snapbox::str!["new25"]);
assert_debug_snapshot!(actual);
}
//- /lib.rs crate:expect_test
struct Expect;
impl Expect {
fn assert_eq(&self, actual: &str) {}
}
#[macro_export]
macro_rules! expect {
($e:expr) => Expect; // dummy
}
//- /lib.rs crate:insta
#[macro_export]
macro_rules! assert_debug_snapshot {
($e:expr) => {}; // dummy
}
//- /lib.rs crate:snapbox
pub struct Assert;
impl Assert {
pub fn new() -> Self { Assert }
pub fn action_env(&self, env: &str) -> &Self { self }
pub fn eq(&self, actual: &str, expected: &str) {}
}
#[macro_export]
macro_rules! str {
($e:expr) => ""; // dummy
}
"#,
expect![[r#"
[
Reference(
FilePositionWrapper {
file_id: FileId(
0,
),
offset: 92,
},
),
Runnable(
Runnable {
use_name_in_title: false,
nav: NavigationTarget {
file_id: FileId(
0,
),
full_range: 81..301,
focus_range: 92..96,
name: "test",
kind: Function,
},
kind: Test {
test_id: Path(
"test",
),
attr: TestAttr {
ignore: false,
},
},
cfg: None,
update_test: UpdateTest {
expect_test: true,
insta: true,
snapbox: true,
},
},
),
]
"#]],
);
}

View file

@ -1,10 +1,11 @@
use std::fmt; use std::{fmt, sync::OnceLock};
use arrayvec::ArrayVec;
use ast::HasName; use ast::HasName;
use cfg::{CfgAtom, CfgExpr}; use cfg::{CfgAtom, CfgExpr};
use hir::{ use hir::{
db::HirDatabase, sym, AsAssocItem, AttrsWithOwner, HasAttrs, HasCrate, HasSource, HirFileIdExt, db::HirDatabase, sym, AsAssocItem, AttrsWithOwner, HasAttrs, HasCrate, HasSource, HirFileIdExt,
Semantics, ModPath, Name, PathKind, Semantics, Symbol,
}; };
use ide_assists::utils::{has_test_related_attribute, test_related_attribute_syn}; use ide_assists::utils::{has_test_related_attribute, test_related_attribute_syn};
use ide_db::{ use ide_db::{
@ -15,11 +16,12 @@ use ide_db::{
FilePosition, FxHashMap, FxHashSet, RootDatabase, SymbolKind, FilePosition, FxHashMap, FxHashSet, RootDatabase, SymbolKind,
}; };
use itertools::Itertools; use itertools::Itertools;
use smallvec::SmallVec;
use span::{Edition, TextSize}; use span::{Edition, TextSize};
use stdx::format_to; use stdx::format_to;
use syntax::{ use syntax::{
ast::{self, AstNode}, ast::{self, AstNode},
SmolStr, SyntaxNode, ToSmolStr, format_smolstr, SmolStr, SyntaxNode, ToSmolStr,
}; };
use crate::{references, FileId, NavigationTarget, ToNav, TryToNav}; use crate::{references, FileId, NavigationTarget, ToNav, TryToNav};
@ -30,6 +32,7 @@ pub struct Runnable {
pub nav: NavigationTarget, pub nav: NavigationTarget,
pub kind: RunnableKind, pub kind: RunnableKind,
pub cfg: Option<CfgExpr>, pub cfg: Option<CfgExpr>,
pub update_test: UpdateTest,
} }
#[derive(Debug, Clone, Hash, PartialEq, Eq)] #[derive(Debug, Clone, Hash, PartialEq, Eq)]
@ -334,14 +337,20 @@ pub(crate) fn runnable_fn(
} }
}; };
let fn_source = sema.source(def)?;
let nav = NavigationTarget::from_named( let nav = NavigationTarget::from_named(
sema.db, 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, SymbolKind::Function,
) )
.call_site(); .call_site();
let file_range = fn_source.syntax().original_file_range_with_macro_call_body(sema.db);
let update_test =
UpdateTest::find_snapshot_macro(sema, &fn_source.file_syntax(sema.db), file_range);
let cfg = def.attrs(sema.db).cfg(); 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( pub(crate) fn runnable_mod(
@ -366,7 +375,22 @@ pub(crate) fn runnable_mod(
let attrs = def.attrs(sema.db); let attrs = def.attrs(sema.db);
let cfg = attrs.cfg(); let cfg = attrs.cfg();
let nav = NavigationTarget::from_module_to_decl(sema.db, def).call_site(); 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 module_source = sema.module_definition_node(def);
let module_syntax = module_source.file_syntax(sema.db);
let file_range = hir::FileRange {
file_id: module_source.file_id.original_file(sema.db),
range: module_syntax.text_range(),
};
let update_test = UpdateTest::find_snapshot_macro(sema, &module_syntax, file_range);
Some(Runnable {
use_name_in_title: false,
nav,
kind: RunnableKind::TestMod { path },
cfg,
update_test,
})
} }
pub(crate) fn runnable_impl( pub(crate) fn runnable_impl(
@ -392,7 +416,19 @@ pub(crate) fn runnable_impl(
test_id.retain(|c| c != ' '); test_id.retain(|c| c != ' ');
let test_id = TestId::Path(test_id); let test_id = TestId::Path(test_id);
Some(Runnable { use_name_in_title: false, nav, kind: RunnableKind::DocTest { test_id }, cfg }) let impl_source = sema.source(*def)?;
let impl_syntax = impl_source.syntax();
let file_range = impl_syntax.original_file_range_with_macro_call_body(sema.db);
let update_test =
UpdateTest::find_snapshot_macro(sema, &impl_syntax.file_syntax(sema.db), file_range);
Some(Runnable {
use_name_in_title: false,
nav,
kind: RunnableKind::DocTest { test_id },
cfg,
update_test,
})
} }
fn has_cfg_test(attrs: AttrsWithOwner) -> bool { fn has_cfg_test(attrs: AttrsWithOwner) -> bool {
@ -404,6 +440,8 @@ fn runnable_mod_outline_definition(
sema: &Semantics<'_, RootDatabase>, sema: &Semantics<'_, RootDatabase>,
def: hir::Module, def: hir::Module,
) -> Option<Runnable> { ) -> Option<Runnable> {
def.as_source_file_id(sema.db)?;
if !has_test_function_or_multiple_test_submodules(sema, &def, has_cfg_test(def.attrs(sema.db))) if !has_test_function_or_multiple_test_submodules(sema, &def, has_cfg_test(def.attrs(sema.db)))
{ {
return None; return None;
@ -421,16 +459,22 @@ fn runnable_mod_outline_definition(
let attrs = def.attrs(sema.db); let attrs = def.attrs(sema.db);
let cfg = attrs.cfg(); let cfg = attrs.cfg();
if def.as_source_file_id(sema.db).is_some() {
let mod_source = sema.module_definition_node(def);
let mod_syntax = mod_source.file_syntax(sema.db);
let file_range = hir::FileRange {
file_id: mod_source.file_id.original_file(sema.db),
range: mod_syntax.text_range(),
};
let update_test = UpdateTest::find_snapshot_macro(sema, &mod_syntax, file_range);
Some(Runnable { Some(Runnable {
use_name_in_title: false, use_name_in_title: false,
nav: def.to_nav(sema.db).call_site(), nav: def.to_nav(sema.db).call_site(),
kind: RunnableKind::TestMod { path }, kind: RunnableKind::TestMod { path },
cfg, cfg,
update_test,
}) })
} else {
None
}
} }
fn module_def_doctest(db: &RootDatabase, def: Definition) -> Option<Runnable> { fn module_def_doctest(db: &RootDatabase, def: Definition) -> Option<Runnable> {
@ -495,6 +539,7 @@ fn module_def_doctest(db: &RootDatabase, def: Definition) -> Option<Runnable> {
nav, nav,
kind: RunnableKind::DocTest { test_id }, kind: RunnableKind::DocTest { test_id },
cfg: attrs.cfg(), cfg: attrs.cfg(),
update_test: UpdateTest::default(),
}; };
Some(res) Some(res)
} }
@ -575,6 +620,128 @@ fn has_test_function_or_multiple_test_submodules(
number_of_test_submodules > 1 number_of_test_submodules > 1
} }
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UpdateTest {
pub expect_test: bool,
pub insta: bool,
pub snapbox: bool,
}
static SNAPSHOT_TEST_MACROS: OnceLock<FxHashMap<&str, Vec<ModPath>>> = OnceLock::new();
impl UpdateTest {
const EXPECT_CRATE: &str = "expect_test";
const EXPECT_MACROS: &[&str] = &["expect", "expect_file"];
const INSTA_CRATE: &str = "insta";
const INSTA_MACROS: &[&str] = &[
"assert_snapshot",
"assert_debug_snapshot",
"assert_display_snapshot",
"assert_json_snapshot",
"assert_yaml_snapshot",
"assert_ron_snapshot",
"assert_toml_snapshot",
"assert_csv_snapshot",
"assert_compact_json_snapshot",
"assert_compact_debug_snapshot",
"assert_binary_snapshot",
];
const SNAPBOX_CRATE: &str = "snapbox";
const SNAPBOX_MACROS: &[&str] = &["assert_data_eq", "file", "str"];
fn find_snapshot_macro(
sema: &Semantics<'_, RootDatabase>,
scope: &SyntaxNode,
file_range: hir::FileRange,
) -> Self {
fn init<'a>(
krate_name: &'a str,
paths: &[&str],
map: &mut FxHashMap<&'a str, Vec<ModPath>>,
) {
let mut res = Vec::with_capacity(paths.len());
let krate = Name::new_symbol_root(Symbol::intern(krate_name));
for path in paths {
let segments = [krate.clone(), Name::new_symbol_root(Symbol::intern(path))];
let mod_path = ModPath::from_segments(PathKind::Abs, segments);
res.push(mod_path);
}
map.insert(krate_name, res);
}
let mod_paths = SNAPSHOT_TEST_MACROS.get_or_init(|| {
let mut map = FxHashMap::default();
init(Self::EXPECT_CRATE, Self::EXPECT_MACROS, &mut map);
init(Self::INSTA_CRATE, Self::INSTA_MACROS, &mut map);
init(Self::SNAPBOX_CRATE, Self::SNAPBOX_MACROS, &mut map);
map
});
let search_scope = SearchScope::file_range(file_range);
let find_macro = |paths: &[ModPath]| {
for path in paths {
let Some(items) = sema.resolve_mod_path(scope, path) else {
continue;
};
for item in items {
if let hir::ItemInNs::Macros(makro) = item {
if Definition::Macro(makro)
.usages(sema)
.in_scope(&search_scope)
.at_least_one()
{
return true;
}
}
}
}
false
};
UpdateTest {
expect_test: find_macro(mod_paths.get(Self::EXPECT_CRATE).unwrap()),
insta: find_macro(mod_paths.get(Self::INSTA_CRATE).unwrap()),
snapbox: find_macro(mod_paths.get(Self::SNAPBOX_CRATE).unwrap()),
}
}
pub fn label(&self) -> Option<SmolStr> {
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();
if res.is_empty() {
None
} else {
Some(format_smolstr!("\u{fe0e} Update Tests ({res})"))
}
}
pub fn env(&self) -> ArrayVec<(&str, &str), 3> {
let mut env = ArrayVec::new();
if self.expect_test {
env.push(("UPDATE_EXPECT", "1"));
}
if self.insta {
env.push(("INSTA_UPDATE", "always"));
}
if self.snapbox {
env.push(("SNAPSHOTS", "overwrite"));
}
env
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use expect_test::{expect, Expect}; use expect_test::{expect, Expect};
@ -1337,18 +1504,18 @@ mod tests {
file_id: FileId( file_id: FileId(
0, 0,
), ),
full_range: 52..115, full_range: 121..185,
focus_range: 67..75, focus_range: 136..145,
name: "foo_test", name: "foo2_test",
kind: Function, kind: Function,
}, },
NavigationTarget { NavigationTarget {
file_id: FileId( file_id: FileId(
0, 0,
), ),
full_range: 121..185, full_range: 52..115,
focus_range: 136..145, focus_range: 67..75,
name: "foo2_test", name: "foo_test",
kind: Function, kind: Function,
}, },
] ]

View file

@ -119,6 +119,9 @@ config_data! {
/// Whether to show `Run` action. Only applies when /// Whether to show `Run` action. Only applies when
/// `#rust-analyzer.hover.actions.enable#` is set. /// `#rust-analyzer.hover.actions.enable#` is set.
hover_actions_run_enable: bool = true, hover_actions_run_enable: bool = true,
/// Whether to show `Update Test` action. Only applies when
/// `#rust-analyzer.hover.actions.enable#` and `#rust-analyzer.hover.actions.run.enable#` are set.
hover_actions_updateTest_enable: bool = true,
/// Whether to show documentation on hover. /// Whether to show documentation on hover.
hover_documentation_enable: bool = true, hover_documentation_enable: bool = true,
@ -243,6 +246,9 @@ config_data! {
/// Whether to show `Run` lens. Only applies when /// Whether to show `Run` lens. Only applies when
/// `#rust-analyzer.lens.enable#` is set. /// `#rust-analyzer.lens.enable#` is set.
lens_run_enable: bool = true, 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_updateTest_enable: bool = true,
/// Disable project auto-discovery in favor of explicitly specified set /// Disable project auto-discovery in favor of explicitly specified set
/// of projects. /// of projects.
@ -1161,6 +1167,7 @@ pub struct LensConfig {
// runnables // runnables
pub run: bool, pub run: bool,
pub debug: bool, pub debug: bool,
pub update_test: bool,
pub interpret: bool, pub interpret: bool,
// implementations // implementations
@ -1196,6 +1203,7 @@ impl LensConfig {
pub fn any(&self) -> bool { pub fn any(&self) -> bool {
self.run self.run
|| self.debug || self.debug
|| self.update_test
|| self.implementations || self.implementations
|| self.method_refs || self.method_refs
|| self.refs_adt || self.refs_adt
@ -1208,7 +1216,7 @@ impl LensConfig {
} }
pub fn runnable(&self) -> bool { pub fn runnable(&self) -> bool {
self.run || self.debug self.run || self.debug || self.update_test
} }
pub fn references(&self) -> bool { pub fn references(&self) -> bool {
@ -1222,6 +1230,7 @@ pub struct HoverActionsConfig {
pub references: bool, pub references: bool,
pub run: bool, pub run: bool,
pub debug: bool, pub debug: bool,
pub update_test: bool,
pub goto_type_def: bool, pub goto_type_def: bool,
} }
@ -1231,6 +1240,7 @@ impl HoverActionsConfig {
references: false, references: false,
run: false, run: false,
debug: false, debug: false,
update_test: false,
goto_type_def: false, goto_type_def: false,
}; };
@ -1243,7 +1253,7 @@ impl HoverActionsConfig {
} }
pub fn runnable(&self) -> bool { pub fn runnable(&self) -> bool {
self.run || self.debug self.run || self.debug || self.update_test
} }
} }
@ -1517,6 +1527,9 @@ impl Config {
references: enable && self.hover_actions_references_enable().to_owned(), references: enable && self.hover_actions_references_enable().to_owned(),
run: enable && self.hover_actions_run_enable().to_owned(), run: enable && self.hover_actions_run_enable().to_owned(),
debug: enable && self.hover_actions_debug_enable().to_owned(), debug: enable && self.hover_actions_debug_enable().to_owned(),
update_test: enable
&& self.hover_actions_run_enable().to_owned()
&& self.hover_actions_updateTest_enable().to_owned(),
goto_type_def: enable && self.hover_actions_gotoTypeDef_enable().to_owned(), goto_type_def: enable && self.hover_actions_gotoTypeDef_enable().to_owned(),
} }
} }
@ -2120,6 +2133,9 @@ impl Config {
LensConfig { LensConfig {
run: *self.lens_enable() && *self.lens_run_enable(), run: *self.lens_enable() && *self.lens_run_enable(),
debug: *self.lens_enable() && *self.lens_debug_enable(), debug: *self.lens_enable() && *self.lens_debug_enable(),
update_test: *self.lens_enable()
&& *self.lens_updateTest_enable()
&& *self.lens_run_enable(),
interpret: *self.lens_enable() && *self.lens_run_enable() && *self.interpret_tests(), interpret: *self.lens_enable() && *self.lens_run_enable() && *self.interpret_tests(),
implementations: *self.lens_enable() && *self.lens_implementations_enable(), implementations: *self.lens_enable() && *self.lens_implementations_enable(),
method_refs: *self.lens_enable() && *self.lens_references_method_enable(), method_refs: *self.lens_enable() && *self.lens_references_method_enable(),

View file

@ -27,7 +27,7 @@ use paths::Utf8PathBuf;
use project_model::{CargoWorkspace, ManifestPath, ProjectWorkspaceKind, TargetKind}; use project_model::{CargoWorkspace, ManifestPath, ProjectWorkspaceKind, TargetKind};
use serde_json::json; use serde_json::json;
use stdx::{format_to, never}; use stdx::{format_to, never};
use syntax::{algo, ast, AstNode, TextRange, TextSize}; use syntax::{TextRange, TextSize};
use triomphe::Arc; use triomphe::Arc;
use vfs::{AbsPath, AbsPathBuf, FileId, VfsPath}; use vfs::{AbsPath, AbsPathBuf, FileId, VfsPath};
@ -928,39 +928,32 @@ pub(crate) fn handle_runnables(
let offset = params.position.and_then(|it| from_proto::offset(&line_index, it).ok()); let offset = params.position.and_then(|it| from_proto::offset(&line_index, it).ok());
let target_spec = TargetSpec::for_file(&snap, file_id)?; let target_spec = TargetSpec::for_file(&snap, file_id)?;
let expect_test = match offset {
Some(offset) => {
let source_file = snap.analysis.parse(file_id)?;
algo::find_node_at_offset::<ast::MacroCall>(source_file.syntax(), offset)
.and_then(|it| it.path()?.segment()?.name_ref())
.map_or(false, |it| it.text() == "expect" || it.text() == "expect_file")
}
None => false,
};
let mut res = Vec::new(); let mut res = Vec::new();
for runnable in snap.analysis.runnables(file_id)? { for runnable in snap.analysis.runnables(file_id)? {
if should_skip_for_offset(&runnable, offset) { if should_skip_for_offset(&runnable, offset)
continue; || should_skip_target(&runnable, target_spec.as_ref())
} {
if should_skip_target(&runnable, target_spec.as_ref()) {
continue; continue;
} }
let update_test = runnable.update_test;
if let Some(mut runnable) = to_proto::runnable(&snap, runnable)? { if let Some(mut runnable) = to_proto::runnable(&snap, runnable)? {
if expect_test { if let Some(runnable) =
to_proto::make_update_runnable(&runnable, &update_test.label(), &update_test.env())
{
res.push(runnable);
}
if let lsp_ext::RunnableArgs::Cargo(r) = &mut runnable.args { if let lsp_ext::RunnableArgs::Cargo(r) = &mut runnable.args {
runnable.label = format!("{} + expect", runnable.label);
r.environment.insert("UPDATE_EXPECT".to_owned(), "1".to_owned());
if let Some(TargetSpec::Cargo(CargoTargetSpec { if let Some(TargetSpec::Cargo(CargoTargetSpec {
sysroot_root: Some(sysroot_root), sysroot_root: Some(sysroot_root),
.. ..
})) = &target_spec })) = &target_spec
{ {
r.environment r.environment.insert("RUSTC_TOOLCHAIN".to_owned(), sysroot_root.to_string());
.insert("RUSTC_TOOLCHAIN".to_owned(), sysroot_root.to_string());
}
}
} }
};
res.push(runnable); res.push(runnable);
} }
} }
@ -2142,6 +2135,7 @@ fn runnable_action_links(
} }
let title = runnable.title(); let title = runnable.title();
let update_test = runnable.update_test;
let r = to_proto::runnable(snap, runnable).ok()??; let r = to_proto::runnable(snap, runnable).ok()??;
let mut group = lsp_ext::CommandLinkGroup::default(); let mut group = lsp_ext::CommandLinkGroup::default();
@ -2153,7 +2147,15 @@ fn runnable_action_links(
if hover_actions_config.debug && client_commands_config.debug_single { if hover_actions_config.debug && client_commands_config.debug_single {
let dbg_command = to_proto::command::debug_single(&r); let dbg_command = to_proto::command::debug_single(&r);
group.commands.push(to_command_link(dbg_command, r.label)); group.commands.push(to_command_link(dbg_command, r.label.clone()));
}
if hover_actions_config.update_test && client_commands_config.run_single {
let label = update_test.label();
if let Some(r) = to_proto::make_update_runnable(&r, &label, &update_test.env()) {
let update_command = to_proto::command::run_single(&r, label.unwrap().as_str());
group.commands.push(to_command_link(update_command, r.label.clone()));
}
} }
Some(group) Some(group)

View file

@ -427,14 +427,14 @@ impl Request for Runnables {
const METHOD: &'static str = "experimental/runnables"; const METHOD: &'static str = "experimental/runnables";
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct RunnablesParams { pub struct RunnablesParams {
pub text_document: TextDocumentIdentifier, pub text_document: TextDocumentIdentifier,
pub position: Option<Position>, pub position: Option<Position>,
} }
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Runnable { pub struct Runnable {
pub label: String, pub label: String,
@ -444,7 +444,7 @@ pub struct Runnable {
pub args: RunnableArgs, pub args: RunnableArgs,
} }
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[serde(untagged)] #[serde(untagged)]
pub enum RunnableArgs { pub enum RunnableArgs {
@ -452,14 +452,14 @@ pub enum RunnableArgs {
Shell(ShellRunnableArgs), Shell(ShellRunnableArgs),
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum RunnableKind { pub enum RunnableKind {
Cargo, Cargo,
Shell, Shell,
} }
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CargoRunnableArgs { pub struct CargoRunnableArgs {
#[serde(skip_serializing_if = "FxHashMap::is_empty")] #[serde(skip_serializing_if = "FxHashMap::is_empty")]
@ -475,7 +475,7 @@ pub struct CargoRunnableArgs {
pub executable_args: Vec<String>, pub executable_args: Vec<String>,
} }
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ShellRunnableArgs { pub struct ShellRunnableArgs {
#[serde(skip_serializing_if = "FxHashMap::is_empty")] #[serde(skip_serializing_if = "FxHashMap::is_empty")]

View file

@ -20,6 +20,7 @@ use itertools::Itertools;
use paths::{Utf8Component, Utf8Prefix}; use paths::{Utf8Component, Utf8Prefix};
use semver::VersionReq; use semver::VersionReq;
use serde_json::to_value; use serde_json::to_value;
use syntax::SmolStr;
use vfs::AbsPath; use vfs::AbsPath;
use crate::{ use crate::{
@ -1567,6 +1568,7 @@ pub(crate) fn code_lens(
let line_index = snap.file_line_index(run.nav.file_id)?; let line_index = snap.file_line_index(run.nav.file_id)?;
let annotation_range = range(&line_index, annotation.range); let annotation_range = range(&line_index, annotation.range);
let update_test = run.update_test;
let title = run.title(); let title = run.title();
let can_debug = match run.kind { let can_debug = match run.kind {
ide::RunnableKind::DocTest { .. } => false, ide::RunnableKind::DocTest { .. } => false,
@ -1602,6 +1604,18 @@ pub(crate) fn code_lens(
data: None, data: None,
}) })
} }
if lens_config.update_test && client_commands_config.run_single {
let label = update_test.label();
let env = update_test.env();
if let Some(r) = make_update_runnable(&r, &label, &env) {
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 { if lens_config.interpret {
@ -1786,7 +1800,7 @@ pub(crate) mod command {
pub(crate) fn debug_single(runnable: &lsp_ext::Runnable) -> lsp_types::Command { pub(crate) fn debug_single(runnable: &lsp_ext::Runnable) -> lsp_types::Command {
lsp_types::Command { lsp_types::Command {
title: "Debug".into(), title: "\u{fe0e} Debug".into(),
command: "rust-analyzer.debugSingle".into(), command: "rust-analyzer.debugSingle".into(),
arguments: Some(vec![to_value(runnable).unwrap()]), arguments: Some(vec![to_value(runnable).unwrap()]),
} }
@ -1838,6 +1852,28 @@ pub(crate) mod command {
} }
} }
pub(crate) fn make_update_runnable(
runnable: &lsp_ext::Runnable,
label: &Option<SmolStr>,
env: &[(&str, &str)],
) -> Option<lsp_ext::Runnable> {
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!();
};
r.environment.extend(env.iter().map(|(k, v)| (k.to_string(), v.to_string())));
Some(runnable)
}
pub(crate) fn implementation_title(count: usize) -> String { pub(crate) fn implementation_title(count: usize) -> String {
if count == 1 { if count == 1 {
"1 implementation".into() "1 implementation".into()

View file

@ -1,5 +1,5 @@
<!--- <!---
lsp/ext.rs hash: 9790509d87670c22 lsp/ext.rs hash: 512c06cd8b46a21d
If you need to change the above hash to make the test pass, please check if you If you need to change the above hash to make the test pass, please check if you
need to adjust this doc as well and ping this issue: need to adjust this doc as well and ping this issue:

View file

@ -497,6 +497,12 @@ Whether to show `References` action. Only applies when
Whether to show `Run` action. Only applies when Whether to show `Run` action. Only applies when
`#rust-analyzer.hover.actions.enable#` is set. `#rust-analyzer.hover.actions.enable#` is set.
-- --
[[rust-analyzer.hover.actions.updateTest.enable]]rust-analyzer.hover.actions.updateTest.enable (default: `true`)::
+
--
Whether to show `Update Test` action. Only applies when
`#rust-analyzer.hover.actions.enable#` and `#rust-analyzer.hover.actions.run.enable#` are set.
--
[[rust-analyzer.hover.documentation.enable]]rust-analyzer.hover.documentation.enable (default: `true`):: [[rust-analyzer.hover.documentation.enable]]rust-analyzer.hover.documentation.enable (default: `true`)::
+ +
-- --
@ -808,6 +814,12 @@ Only applies when `#rust-analyzer.lens.enable#` is set.
Whether to show `Run` lens. Only applies when Whether to show `Run` lens. Only applies when
`#rust-analyzer.lens.enable#` is set. `#rust-analyzer.lens.enable#` is set.
-- --
[[rust-analyzer.lens.updateTest.enable]]rust-analyzer.lens.updateTest.enable (default: `true`)::
+
--
Whether to show `Update Test` lens. Only applies when
`#rust-analyzer.lens.enable#` and `#rust-analyzer.lens.run.enable#` are set.
--
[[rust-analyzer.linkedProjects]]rust-analyzer.linkedProjects (default: `[]`):: [[rust-analyzer.linkedProjects]]rust-analyzer.linkedProjects (default: `[]`)::
+ +
-- --

View file

@ -407,6 +407,11 @@
"$rustc" "$rustc"
], ],
"markdownDescription": "Problem matchers to use for `rust-analyzer.run` command, eg `[\"$rustc\", \"$rust-panic\"]`." "markdownDescription": "Problem matchers to use for `rust-analyzer.run` command, eg `[\"$rustc\", \"$rust-panic\"]`."
},
"rust-analyzer.runnables.askBeforeUpdateTest": {
"type": "boolean",
"default": true,
"markdownDescription": "Ask before updating the test when running it."
} }
} }
}, },
@ -1515,6 +1520,16 @@
} }
} }
}, },
{
"title": "hover",
"properties": {
"rust-analyzer.hover.actions.updateTest.enable": {
"markdownDescription": "Whether to show `Update Test` action. Only applies when\n`#rust-analyzer.hover.actions.enable#` and `#rust-analyzer.hover.actions.run.enable#` are set.",
"default": true,
"type": "boolean"
}
}
},
{ {
"title": "hover", "title": "hover",
"properties": { "properties": {
@ -2295,6 +2310,16 @@
} }
} }
}, },
{
"title": "lens",
"properties": {
"rust-analyzer.lens.updateTest.enable": {
"markdownDescription": "Whether to show `Update Test` lens. Only applies when\n`#rust-analyzer.lens.enable#` and `#rust-analyzer.lens.run.enable#` are set.",
"default": true,
"type": "boolean"
}
}
},
{ {
"title": "general", "title": "general",
"properties": { "properties": {

View file

@ -1139,11 +1139,37 @@ export function peekTests(ctx: CtxInit): Cmd {
}; };
} }
function isUpdatingTest(runnable: ra.Runnable): boolean {
if (!isCargoRunnableArgs(runnable.args)) {
return false;
}
const env = runnable.args.environment;
return env ? ["UPDATE_EXPECT", "INSTA_UPDATE", "SNAPSHOTS"].some((key) => key in env) : false;
}
export function runSingle(ctx: CtxInit): Cmd { export function runSingle(ctx: CtxInit): Cmd {
return async (runnable: ra.Runnable) => { return async (runnable: ra.Runnable) => {
const editor = ctx.activeRustEditor; const editor = ctx.activeRustEditor;
if (!editor) return; if (!editor) return;
if (isUpdatingTest(runnable) && ctx.config.askBeforeUpdateTest) {
const selection = await vscode.window.showInformationMessage(
"rust-analyzer",
{ detail: "Do you want to update tests?", modal: true },
"Update Now",
"Update (and Don't ask again)",
);
if (selection !== "Update Now" && selection !== "Update (and Don't ask again)") {
return;
}
if (selection === "Update (and Don't ask again)") {
await ctx.config.setAskBeforeUpdateTest(false);
}
}
const task = await createTaskFromRunnable(runnable, ctx.config); const task = await createTaskFromRunnable(runnable, ctx.config);
task.group = vscode.TaskGroup.Build; task.group = vscode.TaskGroup.Build;
task.presentationOptions = { task.presentationOptions = {

View file

@ -362,6 +362,13 @@ export class Config {
get initializeStopped() { get initializeStopped() {
return this.get<boolean>("initializeStopped"); return this.get<boolean>("initializeStopped");
} }
get askBeforeUpdateTest() {
return this.get<boolean>("runnables.askBeforeUpdateTest");
}
async setAskBeforeUpdateTest(value: boolean) {
await this.cfg.update("runnables.askBeforeUpdateTest", value, true);
}
} }
export function prepareVSCodeConfig<T>(resp: T): T { export function prepareVSCodeConfig<T>(resp: T): T {