⬆️ rust-analyzer
This commit is contained in:
commit
22a6bc4da0
@ -2,8 +2,8 @@ name: publish
|
||||
on:
|
||||
workflow_dispatch: # We can add version input when 1.0 is released and scheduled releases are removed
|
||||
|
||||
# schedule:
|
||||
# - cron: "0 0 * * *" # midnight UTC
|
||||
# schedule:
|
||||
# - cron: "0 0 * * *" # midnight UTC
|
||||
|
||||
push:
|
||||
branches:
|
||||
@ -50,5 +50,7 @@ jobs:
|
||||
cargo workspaces rename --from test-utils test_utils
|
||||
cargo workspaces rename --from text-edit text_edit
|
||||
cargo workspaces rename ra_ap_%n
|
||||
# Remove library crates from the workspaces so we don't auto-publish them as well
|
||||
sed -i 's/ "lib\/\*",//' ./Cargo.toml
|
||||
find crates/rust-analyzer -type f -name '*.rs' -exec sed -i 's/rust_analyzer/ra_ap_rust_analyzer/g' {} +
|
||||
cargo workspaces publish --yes --force '*' --exact --no-git-commit --allow-dirty --skip-published custom 0.0.$PATCH
|
||||
|
@ -21,6 +21,20 @@
|
||||
DiagnosticSpanMacroExpansion,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum InvocationStrategy {
|
||||
Once,
|
||||
#[default]
|
||||
PerWorkspace,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum InvocationLocation {
|
||||
Root(AbsPathBuf),
|
||||
#[default]
|
||||
Workspace,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum FlycheckConfig {
|
||||
CargoCommand {
|
||||
@ -37,6 +51,8 @@ pub enum FlycheckConfig {
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
extra_env: FxHashMap<String, String>,
|
||||
invocation_strategy: InvocationStrategy,
|
||||
invocation_location: InvocationLocation,
|
||||
},
|
||||
}
|
||||
|
||||
@ -136,11 +152,15 @@ enum Restart {
|
||||
No,
|
||||
}
|
||||
|
||||
/// A [`FlycheckActor`] is a single check instance of a workspace.
|
||||
struct FlycheckActor {
|
||||
/// The workspace id of this flycheck instance.
|
||||
id: usize,
|
||||
sender: Box<dyn Fn(Message) + Send>,
|
||||
config: FlycheckConfig,
|
||||
workspace_root: AbsPathBuf,
|
||||
/// Either the workspace root of the workspace we are flychecking,
|
||||
/// or the project root of the project.
|
||||
root: AbsPathBuf,
|
||||
/// CargoHandle exists to wrap around the communication needed to be able to
|
||||
/// run `cargo check` without blocking. Currently the Rust standard library
|
||||
/// doesn't provide a way to read sub-process output without blocking, so we
|
||||
@ -162,11 +182,13 @@ fn new(
|
||||
workspace_root: AbsPathBuf,
|
||||
) -> FlycheckActor {
|
||||
tracing::info!(%id, ?workspace_root, "Spawning flycheck");
|
||||
FlycheckActor { id, sender, config, workspace_root, cargo_handle: None }
|
||||
FlycheckActor { id, sender, config, root: workspace_root, cargo_handle: None }
|
||||
}
|
||||
fn progress(&self, progress: Progress) {
|
||||
|
||||
fn report_progress(&self, progress: Progress) {
|
||||
self.send(Message::Progress { id: self.id, progress });
|
||||
}
|
||||
|
||||
fn next_event(&self, inbox: &Receiver<Restart>) -> Option<Event> {
|
||||
let check_chan = self.cargo_handle.as_ref().map(|cargo| &cargo.receiver);
|
||||
if let Ok(msg) = inbox.try_recv() {
|
||||
@ -178,6 +200,7 @@ fn next_event(&self, inbox: &Receiver<Restart>) -> Option<Event> {
|
||||
recv(check_chan.unwrap_or(&never())) -> msg => Some(Event::CheckEvent(msg.ok())),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(mut self, inbox: Receiver<Restart>) {
|
||||
'event: while let Some(event) = self.next_event(&inbox) {
|
||||
match event {
|
||||
@ -203,10 +226,10 @@ fn run(mut self, inbox: Receiver<Restart>) {
|
||||
"did restart flycheck"
|
||||
);
|
||||
self.cargo_handle = Some(cargo_handle);
|
||||
self.progress(Progress::DidStart);
|
||||
self.report_progress(Progress::DidStart);
|
||||
}
|
||||
Err(error) => {
|
||||
self.progress(Progress::DidFailToRestart(format!(
|
||||
self.report_progress(Progress::DidFailToRestart(format!(
|
||||
"Failed to run the following command: {:?} error={}",
|
||||
self.check_command(),
|
||||
error
|
||||
@ -226,17 +249,17 @@ fn run(mut self, inbox: Receiver<Restart>) {
|
||||
self.check_command()
|
||||
);
|
||||
}
|
||||
self.progress(Progress::DidFinish(res));
|
||||
self.report_progress(Progress::DidFinish(res));
|
||||
}
|
||||
Event::CheckEvent(Some(message)) => match message {
|
||||
CargoMessage::CompilerArtifact(msg) => {
|
||||
self.progress(Progress::DidCheckCrate(msg.target.name));
|
||||
self.report_progress(Progress::DidCheckCrate(msg.target.name));
|
||||
}
|
||||
|
||||
CargoMessage::Diagnostic(msg) => {
|
||||
self.send(Message::AddDiagnostic {
|
||||
id: self.id,
|
||||
workspace_root: self.workspace_root.clone(),
|
||||
workspace_root: self.root.clone(),
|
||||
diagnostic: msg,
|
||||
});
|
||||
}
|
||||
@ -254,12 +277,12 @@ fn cancel_check_process(&mut self) {
|
||||
"did cancel flycheck"
|
||||
);
|
||||
cargo_handle.cancel();
|
||||
self.progress(Progress::DidCancel);
|
||||
self.report_progress(Progress::DidCancel);
|
||||
}
|
||||
}
|
||||
|
||||
fn check_command(&self) -> Command {
|
||||
let mut cmd = match &self.config {
|
||||
let (mut cmd, args) = match &self.config {
|
||||
FlycheckConfig::CargoCommand {
|
||||
command,
|
||||
target_triple,
|
||||
@ -272,9 +295,7 @@ fn check_command(&self) -> Command {
|
||||
} => {
|
||||
let mut cmd = Command::new(toolchain::cargo());
|
||||
cmd.arg(command);
|
||||
cmd.current_dir(&self.workspace_root);
|
||||
cmd.args(&["--workspace", "--message-format=json", "--manifest-path"])
|
||||
.arg(self.workspace_root.join("Cargo.toml").as_os_str());
|
||||
cmd.args(&["--workspace", "--message-format=json"]);
|
||||
|
||||
if let Some(target) = target_triple {
|
||||
cmd.args(&["--target", target.as_str()]);
|
||||
@ -293,18 +314,41 @@ fn check_command(&self) -> Command {
|
||||
cmd.arg(features.join(" "));
|
||||
}
|
||||
}
|
||||
cmd.args(extra_args);
|
||||
cmd.envs(extra_env);
|
||||
cmd
|
||||
(cmd, extra_args)
|
||||
}
|
||||
FlycheckConfig::CustomCommand { command, args, extra_env } => {
|
||||
FlycheckConfig::CustomCommand {
|
||||
command,
|
||||
args,
|
||||
extra_env,
|
||||
invocation_strategy,
|
||||
invocation_location,
|
||||
} => {
|
||||
let mut cmd = Command::new(command);
|
||||
cmd.args(args);
|
||||
cmd.envs(extra_env);
|
||||
cmd
|
||||
|
||||
match invocation_location {
|
||||
InvocationLocation::Workspace => {
|
||||
match invocation_strategy {
|
||||
InvocationStrategy::Once => {
|
||||
cmd.current_dir(&self.root);
|
||||
}
|
||||
InvocationStrategy::PerWorkspace => {
|
||||
// FIXME: cmd.current_dir(&affected_workspace);
|
||||
cmd.current_dir(&self.root);
|
||||
}
|
||||
}
|
||||
}
|
||||
InvocationLocation::Root(root) => {
|
||||
cmd.current_dir(root);
|
||||
}
|
||||
}
|
||||
|
||||
(cmd, args)
|
||||
}
|
||||
};
|
||||
cmd.current_dir(&self.workspace_root);
|
||||
|
||||
cmd.args(args);
|
||||
cmd
|
||||
}
|
||||
|
||||
|
@ -12,11 +12,11 @@ fn test_copy_expand_simple() {
|
||||
#[derive(Copy)]
|
||||
struct Foo;
|
||||
"#,
|
||||
expect![[r##"
|
||||
expect![[r#"
|
||||
#[derive(Copy)]
|
||||
struct Foo;
|
||||
|
||||
impl < > core::marker::Copy for Foo< > {}"##]],
|
||||
impl < > core::marker::Copy for Foo< > {}"#]],
|
||||
);
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ fn test_copy_expand_in_core() {
|
||||
#[derive(Copy)]
|
||||
struct Foo;
|
||||
"#,
|
||||
expect![[r##"
|
||||
expect![[r#"
|
||||
#[rustc_builtin_macro]
|
||||
macro derive {}
|
||||
#[rustc_builtin_macro]
|
||||
@ -41,7 +41,7 @@ fn test_copy_expand_in_core() {
|
||||
#[derive(Copy)]
|
||||
struct Foo;
|
||||
|
||||
impl < > crate ::marker::Copy for Foo< > {}"##]],
|
||||
impl < > crate ::marker::Copy for Foo< > {}"#]],
|
||||
);
|
||||
}
|
||||
|
||||
@ -53,11 +53,11 @@ fn test_copy_expand_with_type_params() {
|
||||
#[derive(Copy)]
|
||||
struct Foo<A, B>;
|
||||
"#,
|
||||
expect![[r##"
|
||||
expect![[r#"
|
||||
#[derive(Copy)]
|
||||
struct Foo<A, B>;
|
||||
|
||||
impl <T0: core::marker::Copy, T1: core::marker::Copy> core::marker::Copy for Foo<T0, T1> {}"##]],
|
||||
impl <T0: core::marker::Copy, T1: core::marker::Copy, > core::marker::Copy for Foo<T0, T1, > {}"#]],
|
||||
);
|
||||
}
|
||||
|
||||
@ -70,11 +70,11 @@ fn test_copy_expand_with_lifetimes() {
|
||||
#[derive(Copy)]
|
||||
struct Foo<A, B, 'a, 'b>;
|
||||
"#,
|
||||
expect![[r##"
|
||||
expect![[r#"
|
||||
#[derive(Copy)]
|
||||
struct Foo<A, B, 'a, 'b>;
|
||||
|
||||
impl <T0: core::marker::Copy, T1: core::marker::Copy> core::marker::Copy for Foo<T0, T1> {}"##]],
|
||||
impl <T0: core::marker::Copy, T1: core::marker::Copy, > core::marker::Copy for Foo<T0, T1, > {}"#]],
|
||||
);
|
||||
}
|
||||
|
||||
@ -86,10 +86,26 @@ fn test_clone_expand() {
|
||||
#[derive(Clone)]
|
||||
struct Foo<A, B>;
|
||||
"#,
|
||||
expect![[r##"
|
||||
expect![[r#"
|
||||
#[derive(Clone)]
|
||||
struct Foo<A, B>;
|
||||
|
||||
impl <T0: core::clone::Clone, T1: core::clone::Clone> core::clone::Clone for Foo<T0, T1> {}"##]],
|
||||
impl <T0: core::clone::Clone, T1: core::clone::Clone, > core::clone::Clone for Foo<T0, T1, > {}"#]],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clone_expand_with_const_generics() {
|
||||
check(
|
||||
r#"
|
||||
//- minicore: derive, clone
|
||||
#[derive(Clone)]
|
||||
struct Foo<const X: usize, T>(u32);
|
||||
"#,
|
||||
expect![[r#"
|
||||
#[derive(Clone)]
|
||||
struct Foo<const X: usize, T>(u32);
|
||||
|
||||
impl <const T0: usize, T1: core::clone::Clone, > core::clone::Clone for Foo<T0, T1, > {}"#]],
|
||||
);
|
||||
}
|
||||
|
@ -60,7 +60,8 @@ pub fn find_builtin_derive(ident: &name::Name) -> Option<BuiltinDeriveExpander>
|
||||
|
||||
struct BasicAdtInfo {
|
||||
name: tt::Ident,
|
||||
type_or_const_params: usize,
|
||||
/// `Some(ty)` if it's a const param of type `ty`, `None` if it's a type param.
|
||||
param_types: Vec<Option<tt::Subtree>>,
|
||||
}
|
||||
|
||||
fn parse_adt(tt: &tt::Subtree) -> Result<BasicAdtInfo, ExpandError> {
|
||||
@ -92,50 +93,22 @@ fn parse_adt(tt: &tt::Subtree) -> Result<BasicAdtInfo, ExpandError> {
|
||||
let name_token_id =
|
||||
token_map.token_by_range(name.syntax().text_range()).unwrap_or_else(TokenId::unspecified);
|
||||
let name_token = tt::Ident { id: name_token_id, text: name.text().into() };
|
||||
let type_or_const_params =
|
||||
params.map_or(0, |type_param_list| type_param_list.type_or_const_params().count());
|
||||
Ok(BasicAdtInfo { name: name_token, type_or_const_params })
|
||||
}
|
||||
|
||||
fn make_type_args(n: usize, bound: Vec<tt::TokenTree>) -> Vec<tt::TokenTree> {
|
||||
let mut result = Vec::<tt::TokenTree>::with_capacity(n * 2);
|
||||
result.push(
|
||||
tt::Leaf::Punct(tt::Punct {
|
||||
char: '<',
|
||||
spacing: tt::Spacing::Alone,
|
||||
id: tt::TokenId::unspecified(),
|
||||
let param_types = params
|
||||
.into_iter()
|
||||
.flat_map(|param_list| param_list.type_or_const_params())
|
||||
.map(|param| {
|
||||
if let ast::TypeOrConstParam::Const(param) = param {
|
||||
let ty = param
|
||||
.ty()
|
||||
.map(|ty| mbe::syntax_node_to_token_tree(ty.syntax()).0)
|
||||
.unwrap_or_default();
|
||||
Some(ty)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.into(),
|
||||
);
|
||||
for i in 0..n {
|
||||
if i > 0 {
|
||||
result.push(
|
||||
tt::Leaf::Punct(tt::Punct {
|
||||
char: ',',
|
||||
spacing: tt::Spacing::Alone,
|
||||
id: tt::TokenId::unspecified(),
|
||||
})
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
result.push(
|
||||
tt::Leaf::Ident(tt::Ident {
|
||||
id: tt::TokenId::unspecified(),
|
||||
text: format!("T{}", i).into(),
|
||||
})
|
||||
.into(),
|
||||
);
|
||||
result.extend(bound.iter().cloned());
|
||||
}
|
||||
result.push(
|
||||
tt::Leaf::Punct(tt::Punct {
|
||||
char: '>',
|
||||
spacing: tt::Spacing::Alone,
|
||||
id: tt::TokenId::unspecified(),
|
||||
})
|
||||
.into(),
|
||||
);
|
||||
result
|
||||
.collect();
|
||||
Ok(BasicAdtInfo { name: name_token, param_types })
|
||||
}
|
||||
|
||||
fn expand_simple_derive(tt: &tt::Subtree, trait_path: tt::Subtree) -> ExpandResult<tt::Subtree> {
|
||||
@ -143,14 +116,27 @@ fn expand_simple_derive(tt: &tt::Subtree, trait_path: tt::Subtree) -> ExpandResu
|
||||
Ok(info) => info,
|
||||
Err(e) => return ExpandResult::only_err(e),
|
||||
};
|
||||
let (params, args): (Vec<_>, Vec<_>) = info
|
||||
.param_types
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(idx, param_ty)| {
|
||||
let ident = tt::Leaf::Ident(tt::Ident {
|
||||
id: tt::TokenId::unspecified(),
|
||||
text: format!("T{idx}").into(),
|
||||
});
|
||||
let ident_ = ident.clone();
|
||||
if let Some(ty) = param_ty {
|
||||
(quote! { const #ident : #ty , }, quote! { #ident_ , })
|
||||
} else {
|
||||
let bound = trait_path.clone();
|
||||
(quote! { #ident : #bound , }, quote! { #ident_ , })
|
||||
}
|
||||
})
|
||||
.unzip();
|
||||
let name = info.name;
|
||||
let trait_path_clone = trait_path.token_trees.clone();
|
||||
let bound = (quote! { : ##trait_path_clone }).token_trees;
|
||||
let type_params = make_type_args(info.type_or_const_params, bound);
|
||||
let type_args = make_type_args(info.type_or_const_params, Vec::new());
|
||||
let trait_path = trait_path.token_trees;
|
||||
let expanded = quote! {
|
||||
impl ##type_params ##trait_path for #name ##type_args {}
|
||||
impl < ##params > #trait_path for #name < ##args > {}
|
||||
};
|
||||
ExpandResult::ok(expanded)
|
||||
}
|
||||
|
@ -259,7 +259,6 @@ macro_rules! __known_path {
|
||||
(core::future::Future) => {};
|
||||
(core::future::IntoFuture) => {};
|
||||
(core::ops::Try) => {};
|
||||
(core::ops::FromResidual) => {};
|
||||
($path:path) => {
|
||||
compile_error!("Please register your known path in the path module")
|
||||
};
|
||||
|
@ -279,8 +279,6 @@ macro_rules! known_names {
|
||||
RangeToInclusive,
|
||||
RangeTo,
|
||||
Range,
|
||||
Residual,
|
||||
FromResidual,
|
||||
Neg,
|
||||
Not,
|
||||
None,
|
||||
|
@ -190,9 +190,7 @@ fn map<U>(self, f: impl FnOnce(T) -> U) -> InferOk<U> {
|
||||
pub enum InferenceDiagnostic {
|
||||
NoSuchField { expr: ExprId },
|
||||
BreakOutsideOfLoop { expr: ExprId, is_break: bool },
|
||||
IncorrectTryTarget { expr: ExprId },
|
||||
MismatchedArgCount { call_expr: ExprId, expected: usize, found: usize },
|
||||
DoesNotImplement { expr: ExprId, trait_: TraitId, ty: Ty },
|
||||
}
|
||||
|
||||
/// A mismatch between an expected and an inferred type.
|
||||
@ -907,6 +905,17 @@ fn resolve_iterator_item(&self) -> Option<TypeAliasId> {
|
||||
self.db.trait_data(trait_).associated_type_by_name(&name![Item])
|
||||
}
|
||||
|
||||
fn resolve_ops_try_ok(&self) -> Option<TypeAliasId> {
|
||||
// FIXME resolve via lang_item once try v2 is stable
|
||||
let path = path![core::ops::Try];
|
||||
let trait_ = self.resolver.resolve_known_trait(self.db.upcast(), &path)?;
|
||||
let trait_data = self.db.trait_data(trait_);
|
||||
trait_data
|
||||
// FIXME remove once try v2 is stable
|
||||
.associated_type_by_name(&name![Ok])
|
||||
.or_else(|| trait_data.associated_type_by_name(&name![Output]))
|
||||
}
|
||||
|
||||
fn resolve_ops_neg_output(&self) -> Option<TypeAliasId> {
|
||||
let trait_ = self.resolve_lang_item(name![neg])?.as_trait()?;
|
||||
self.db.trait_data(trait_).associated_type_by_name(&name![Output])
|
||||
|
@ -19,24 +19,24 @@
|
||||
resolver::resolver_for_expr,
|
||||
ConstParamId, FieldId, ItemContainerId, Lookup,
|
||||
};
|
||||
use hir_expand::{name, name::Name};
|
||||
use hir_expand::name::Name;
|
||||
use stdx::always;
|
||||
use syntax::ast::RangeOp;
|
||||
|
||||
use crate::{
|
||||
autoderef::{self, Autoderef},
|
||||
consteval,
|
||||
infer::{coerce::CoerceMany, find_continuable, path, BreakableKind},
|
||||
infer::{coerce::CoerceMany, find_continuable, BreakableKind},
|
||||
lower::{
|
||||
const_or_path_to_chalk, generic_arg_to_chalk, lower_to_chalk_mutability, ParamLoweringMode,
|
||||
},
|
||||
mapping::{from_chalk, ToChalk},
|
||||
method_resolution::{self, lang_names_for_bin_op, VisibleFromModule},
|
||||
primitive::{self, UintTy},
|
||||
static_lifetime, to_assoc_type_id, to_chalk_trait_id,
|
||||
static_lifetime, to_chalk_trait_id,
|
||||
utils::{generics, Generics},
|
||||
AdtId, AliasEq, AliasTy, Binders, CallableDefId, FnPointer, FnSig, FnSubst, Interner,
|
||||
ProjectionTy, Rawness, Scalar, Substitution, TraitRef, Ty, TyBuilder, TyExt, TyKind,
|
||||
AdtId, Binders, CallableDefId, FnPointer, FnSig, FnSubst, Interner, Rawness, Scalar,
|
||||
Substitution, TraitRef, Ty, TyBuilder, TyExt, TyKind,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@ -564,29 +564,9 @@ fn infer_expr_inner(&mut self, tgt_expr: ExprId, expected: &Expectation) -> Ty {
|
||||
let inner_ty = self.infer_expr_inner(*expr, &Expectation::none());
|
||||
self.resolve_associated_type(inner_ty, self.resolve_future_future_output())
|
||||
}
|
||||
&Expr::Try { expr } => {
|
||||
let inner_ty = self.infer_expr_inner(expr, &Expectation::none());
|
||||
match self.resolve_try_impl_for(inner_ty.clone()) {
|
||||
Some((_, Some((output, residual)))) => {
|
||||
if let Some((_trait, false)) =
|
||||
self.implements_from_residual(self.return_ty.clone(), residual)
|
||||
{
|
||||
self.push_diagnostic(InferenceDiagnostic::IncorrectTryTarget {
|
||||
expr: tgt_expr,
|
||||
});
|
||||
}
|
||||
output
|
||||
}
|
||||
Some((trait_, None)) => {
|
||||
self.push_diagnostic(InferenceDiagnostic::DoesNotImplement {
|
||||
expr,
|
||||
trait_,
|
||||
ty: inner_ty,
|
||||
});
|
||||
self.err_ty()
|
||||
}
|
||||
None => self.err_ty(),
|
||||
}
|
||||
Expr::Try { expr } => {
|
||||
let inner_ty = self.infer_expr_inner(*expr, &Expectation::none());
|
||||
self.resolve_associated_type(inner_ty, self.resolve_ops_try_ok())
|
||||
}
|
||||
Expr::Cast { expr, type_ref } => {
|
||||
// FIXME: propagate the "castable to" expectation (and find a test case that shows this is necessary)
|
||||
@ -1550,67 +1530,4 @@ fn with_breakable_ctx<T>(
|
||||
let ctx = self.breakables.pop().expect("breakable stack broken");
|
||||
(ctx.may_break.then(|| ctx.coerce.complete()), res)
|
||||
}
|
||||
|
||||
/// Check whether `ty` implements `FromResidual<r>`
|
||||
fn implements_from_residual(&mut self, ty: Ty, r: Ty) -> Option<(hir_def::TraitId, bool)> {
|
||||
let from_residual_trait = self
|
||||
.resolver
|
||||
.resolve_known_trait(self.db.upcast(), &(super::path![core::ops::FromResidual]))?;
|
||||
let r = GenericArgData::Ty(r).intern(Interner);
|
||||
let b = TyBuilder::trait_ref(self.db, from_residual_trait);
|
||||
if b.remaining() != 2 {
|
||||
return Some((from_residual_trait, false));
|
||||
}
|
||||
let trait_ref = b.push(ty).push(r).build();
|
||||
Some((from_residual_trait, self.table.try_obligation(trait_ref.cast(Interner)).is_some()))
|
||||
}
|
||||
|
||||
fn resolve_try_impl_for(&mut self, ty: Ty) -> Option<(hir_def::TraitId, Option<(Ty, Ty)>)> {
|
||||
let path = path![core::ops::Try];
|
||||
let trait_ = self.resolver.resolve_known_trait(self.db.upcast(), &path)?;
|
||||
|
||||
let trait_ref = TyBuilder::trait_ref(self.db, trait_).push(ty).build();
|
||||
let substitution = trait_ref.substitution.clone();
|
||||
self.push_obligation(trait_ref.clone().cast(Interner));
|
||||
|
||||
let trait_data = self.db.trait_data(trait_);
|
||||
let output = trait_data.associated_type_by_name(&name![Output]);
|
||||
let residual = trait_data.associated_type_by_name(&name![Residual]);
|
||||
|
||||
let output_ty = match output {
|
||||
Some(output) => {
|
||||
let output_ty = self.table.new_type_var();
|
||||
let alias_eq = AliasEq {
|
||||
alias: AliasTy::Projection(ProjectionTy {
|
||||
associated_ty_id: to_assoc_type_id(output),
|
||||
substitution: substitution.clone(),
|
||||
}),
|
||||
ty: output_ty.clone(),
|
||||
};
|
||||
self.push_obligation(alias_eq.cast(Interner));
|
||||
output_ty
|
||||
}
|
||||
None => self.err_ty(),
|
||||
};
|
||||
let residual_ty = match residual {
|
||||
Some(residual) => {
|
||||
let residual_ty = self.table.new_type_var();
|
||||
let alias_eq = AliasEq {
|
||||
alias: AliasTy::Projection(ProjectionTy {
|
||||
associated_ty_id: to_assoc_type_id(residual),
|
||||
substitution,
|
||||
}),
|
||||
ty: residual_ty.clone(),
|
||||
};
|
||||
self.push_obligation(alias_eq.cast(Interner));
|
||||
residual_ty
|
||||
}
|
||||
None => self.err_ty(),
|
||||
};
|
||||
// FIXME: We are doing the work twice here I think?
|
||||
Some((
|
||||
trait_,
|
||||
self.table.try_obligation(trait_ref.cast(Interner)).map(|_| (output_ty, residual_ty)),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -1111,24 +1111,6 @@ pub fn resolve_indexing_op(
|
||||
}
|
||||
None
|
||||
}
|
||||
/// Returns the receiver type for the try branch trait call.
|
||||
pub fn resolve_branch_op(
|
||||
db: &dyn HirDatabase,
|
||||
env: Arc<TraitEnvironment>,
|
||||
ty: Canonical<Ty>,
|
||||
try_trait: TraitId,
|
||||
) -> Option<ReceiverAdjustments> {
|
||||
let mut table = InferenceTable::new(db, env.clone());
|
||||
let ty = table.instantiate_canonical(ty);
|
||||
let (deref_chain, adj) = autoderef_method_receiver(&mut table, ty);
|
||||
for (ty, adj) in deref_chain.into_iter().zip(adj) {
|
||||
let goal = generic_implements_goal(db, env.clone(), try_trait, &ty);
|
||||
if db.trait_solve(env.krate, goal.cast(Interner)).is_some() {
|
||||
return Some(adj);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
macro_rules! check_that {
|
||||
($cond:expr) => {
|
||||
|
@ -162,16 +162,98 @@ fn test() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn infer_try() {
|
||||
check_types(
|
||||
r#"
|
||||
//- /main.rs crate:main deps:core
|
||||
fn test() {
|
||||
let r: Result<i32, u64> = Result::Ok(1);
|
||||
let v = r?;
|
||||
v;
|
||||
} //^ i32
|
||||
|
||||
//- /core.rs crate:core
|
||||
pub mod ops {
|
||||
pub trait Try {
|
||||
type Ok;
|
||||
type Error;
|
||||
}
|
||||
}
|
||||
|
||||
pub mod result {
|
||||
pub enum Result<O, E> {
|
||||
Ok(O),
|
||||
Err(E)
|
||||
}
|
||||
|
||||
impl<O, E> crate::ops::Try for Result<O, E> {
|
||||
type Ok = O;
|
||||
type Error = E;
|
||||
}
|
||||
}
|
||||
|
||||
pub mod prelude {
|
||||
pub mod rust_2018 {
|
||||
pub use crate::{result::*, ops::*};
|
||||
}
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn infer_try_trait_v2() {
|
||||
check_types(
|
||||
r#"
|
||||
//- minicore: try
|
||||
fn test() -> core::ops::ControlFlow<u32, f32> {
|
||||
let r: core::ops::ControlFlow<u32, f32> = core::ops::ControlFlow::Continue(1.0);
|
||||
//- /main.rs crate:main deps:core
|
||||
fn test() {
|
||||
let r: Result<i32, u64> = Result::Ok(1);
|
||||
let v = r?;
|
||||
//^ f32
|
||||
r
|
||||
v;
|
||||
} //^ i32
|
||||
|
||||
//- /core.rs crate:core
|
||||
mod ops {
|
||||
mod try_trait {
|
||||
pub trait Try: FromResidual {
|
||||
type Output;
|
||||
type Residual;
|
||||
}
|
||||
pub trait FromResidual<R = <Self as Try>::Residual> {}
|
||||
}
|
||||
|
||||
pub use self::try_trait::FromResidual;
|
||||
pub use self::try_trait::Try;
|
||||
}
|
||||
|
||||
mod convert {
|
||||
pub trait From<T> {}
|
||||
impl<T> From<T> for T {}
|
||||
}
|
||||
|
||||
pub mod result {
|
||||
use crate::convert::From;
|
||||
use crate::ops::{Try, FromResidual};
|
||||
|
||||
pub enum Infallible {}
|
||||
pub enum Result<O, E> {
|
||||
Ok(O),
|
||||
Err(E)
|
||||
}
|
||||
|
||||
impl<O, E> Try for Result<O, E> {
|
||||
type Output = O;
|
||||
type Error = Result<Infallible, E>;
|
||||
}
|
||||
|
||||
impl<T, E, F: From<E>> FromResidual<Result<Infallible, E>> for Result<T, F> {}
|
||||
}
|
||||
|
||||
pub mod prelude {
|
||||
pub mod rust_2018 {
|
||||
pub use crate::result::*;
|
||||
}
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
@ -6,7 +6,7 @@
|
||||
use base_db::CrateId;
|
||||
use cfg::{CfgExpr, CfgOptions};
|
||||
use either::Either;
|
||||
use hir_def::{path::ModPath, TraitId};
|
||||
use hir_def::path::ModPath;
|
||||
use hir_expand::{name::Name, HirFileId, InFile};
|
||||
use syntax::{ast, AstPtr, SyntaxNodePtr, TextRange};
|
||||
|
||||
@ -33,7 +33,6 @@ fn from(d: $diag) -> AnyDiagnostic {
|
||||
BreakOutsideOfLoop,
|
||||
InactiveCode,
|
||||
IncorrectCase,
|
||||
IncorrectTryExpr,
|
||||
InvalidDeriveTarget,
|
||||
MacroError,
|
||||
MalformedDerive,
|
||||
@ -41,7 +40,6 @@ fn from(d: $diag) -> AnyDiagnostic {
|
||||
MissingFields,
|
||||
MissingMatchArms,
|
||||
MissingUnsafe,
|
||||
NotImplemented,
|
||||
NoSuchField,
|
||||
ReplaceFilterMapNextWithFindMap,
|
||||
TypeMismatch,
|
||||
@ -155,16 +153,6 @@ pub struct MismatchedArgCount {
|
||||
pub expected: usize,
|
||||
pub found: usize,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct IncorrectTryExpr {
|
||||
pub expr: InFile<AstPtr<ast::Expr>>,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct NotImplemented {
|
||||
pub expr: InFile<AstPtr<ast::Expr>>,
|
||||
pub trait_: TraitId,
|
||||
pub ty: Type,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MissingMatchArms {
|
||||
|
@ -81,12 +81,11 @@
|
||||
pub use crate::{
|
||||
attrs::{HasAttrs, Namespace},
|
||||
diagnostics::{
|
||||
AnyDiagnostic, BreakOutsideOfLoop, InactiveCode, IncorrectCase, IncorrectTryExpr,
|
||||
InvalidDeriveTarget, MacroError, MalformedDerive, MismatchedArgCount, MissingFields,
|
||||
MissingMatchArms, MissingUnsafe, NoSuchField, NotImplemented,
|
||||
ReplaceFilterMapNextWithFindMap, TypeMismatch, UnimplementedBuiltinMacro,
|
||||
UnresolvedExternCrate, UnresolvedImport, UnresolvedMacroCall, UnresolvedModule,
|
||||
UnresolvedProcMacro,
|
||||
AnyDiagnostic, BreakOutsideOfLoop, InactiveCode, IncorrectCase, InvalidDeriveTarget,
|
||||
MacroError, MalformedDerive, MismatchedArgCount, MissingFields, MissingMatchArms,
|
||||
MissingUnsafe, NoSuchField, ReplaceFilterMapNextWithFindMap, TypeMismatch,
|
||||
UnimplementedBuiltinMacro, UnresolvedExternCrate, UnresolvedImport, UnresolvedMacroCall,
|
||||
UnresolvedModule, UnresolvedProcMacro,
|
||||
},
|
||||
has_source::HasSource,
|
||||
semantics::{PathResolution, Semantics, SemanticsScope, TypeInfo, VisibleTraits},
|
||||
@ -1283,45 +1282,30 @@ pub fn diagnostics(self, db: &dyn HirDatabase, acc: &mut Vec<AnyDiagnostic>) {
|
||||
let infer = db.infer(self.into());
|
||||
let source_map = Lazy::new(|| db.body_with_source_map(self.into()).1);
|
||||
for d in &infer.diagnostics {
|
||||
match *d {
|
||||
match d {
|
||||
hir_ty::InferenceDiagnostic::NoSuchField { expr } => {
|
||||
let field = source_map.field_syntax(expr);
|
||||
let field = source_map.field_syntax(*expr);
|
||||
acc.push(NoSuchField { field }.into())
|
||||
}
|
||||
hir_ty::InferenceDiagnostic::BreakOutsideOfLoop { expr, is_break } => {
|
||||
&hir_ty::InferenceDiagnostic::BreakOutsideOfLoop { expr, is_break } => {
|
||||
let expr = source_map
|
||||
.expr_syntax(expr)
|
||||
.expect("break outside of loop in synthetic syntax");
|
||||
acc.push(BreakOutsideOfLoop { expr, is_break }.into())
|
||||
}
|
||||
hir_ty::InferenceDiagnostic::MismatchedArgCount { call_expr, expected, found } => {
|
||||
match source_map.expr_syntax(call_expr) {
|
||||
match source_map.expr_syntax(*call_expr) {
|
||||
Ok(source_ptr) => acc.push(
|
||||
MismatchedArgCount {
|
||||
call_expr: source_ptr,
|
||||
expected: expected,
|
||||
found: found,
|
||||
expected: *expected,
|
||||
found: *found,
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
Err(SyntheticSyntax) => (),
|
||||
}
|
||||
}
|
||||
hir_ty::InferenceDiagnostic::IncorrectTryTarget { expr } => {
|
||||
let expr = source_map.expr_syntax(expr).expect("try in synthetic syntax");
|
||||
acc.push(IncorrectTryExpr { expr }.into())
|
||||
}
|
||||
hir_ty::InferenceDiagnostic::DoesNotImplement { expr, trait_, ref ty } => {
|
||||
let expr = source_map.expr_syntax(expr).expect("try in synthetic syntax");
|
||||
acc.push(
|
||||
NotImplemented {
|
||||
expr,
|
||||
trait_,
|
||||
ty: Type::new(db, DefWithBodyId::from(self), ty.clone()),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (expr, mismatch) in infer.expr_type_mismatches() {
|
||||
|
@ -77,7 +77,7 @@ pub(crate) fn generate_constant(acc: &mut Assists, ctx: &AssistContext<'_>) -> O
|
||||
target_data_for_generate_constant(ctx, current_module, constant_module).unwrap_or_else(
|
||||
|| {
|
||||
let indent = IndentLevel::from_node(statement.syntax());
|
||||
(statement.syntax().text_range().start(), indent, None, format!("\n{}", indent))
|
||||
(statement.syntax().text_range().start(), indent, None, format!("\n{indent}"))
|
||||
},
|
||||
);
|
||||
|
||||
@ -90,7 +90,7 @@ pub(crate) fn generate_constant(acc: &mut Assists, ctx: &AssistContext<'_>) -> O
|
||||
if let Some(file_id) = file_id {
|
||||
builder.edit_file(file_id);
|
||||
}
|
||||
builder.insert(offset, format!("{}{}", text, post_string));
|
||||
builder.insert(offset, format!("{text}{post_string}"));
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -103,13 +103,13 @@ fn get_text_for_generate_constant(
|
||||
) -> Option<String> {
|
||||
let constant_token = not_exist_name_ref.pop()?;
|
||||
let vis = if not_exist_name_ref.len() == 0 && !outer_exists { "" } else { "\npub " };
|
||||
let mut text = format!("{}const {}: {} = $0;", vis, constant_token, type_name);
|
||||
let mut text = format!("{vis}const {constant_token}: {type_name} = $0;");
|
||||
while let Some(name_ref) = not_exist_name_ref.pop() {
|
||||
let vis = if not_exist_name_ref.len() == 0 && !outer_exists { "" } else { "\npub " };
|
||||
text = text.replace("\n", "\n ");
|
||||
text = format!("{}mod {} {{{}\n}}", vis, name_ref.to_string(), text);
|
||||
text = format!("{vis}mod {name_ref} {{{text}\n}}");
|
||||
}
|
||||
Some(text.replace("\n", &format!("\n{}", indent)))
|
||||
Some(text.replace("\n", &format!("\n{indent}")))
|
||||
}
|
||||
|
||||
fn target_data_for_generate_constant(
|
||||
@ -134,7 +134,7 @@ fn target_data_for_generate_constant(
|
||||
.find(|it| it.kind() == SyntaxKind::WHITESPACE && it.to_string().contains("\n"))
|
||||
.is_some();
|
||||
let post_string =
|
||||
if siblings_has_newline { format!("{}", indent) } else { format!("\n{}", indent) };
|
||||
if siblings_has_newline { format!("{indent}") } else { format!("\n{indent}") };
|
||||
Some((offset, indent + 1, Some(file_id), post_string))
|
||||
}
|
||||
_ => Some((TextSize::from(0), 0.into(), Some(file_id), "\n".into())),
|
||||
|
@ -55,12 +55,11 @@ pub(crate) fn generate_default_from_enum_variant(
|
||||
let buf = format!(
|
||||
r#"
|
||||
|
||||
impl Default for {0} {{
|
||||
impl Default for {enum_name} {{
|
||||
fn default() -> Self {{
|
||||
Self::{1}
|
||||
Self::{variant_name}
|
||||
}}
|
||||
}}"#,
|
||||
enum_name, variant_name
|
||||
);
|
||||
edit.insert(start_offset, buf);
|
||||
},
|
||||
|
@ -1,8 +1,7 @@
|
||||
use ide_db::famous_defs::FamousDefs;
|
||||
use itertools::Itertools;
|
||||
use stdx::format_to;
|
||||
use syntax::{
|
||||
ast::{self, HasGenericParams, HasName, HasTypeBounds, Impl},
|
||||
ast::{self, make, HasGenericParams, HasName, Impl},
|
||||
AstNode,
|
||||
};
|
||||
|
||||
@ -77,45 +76,47 @@ pub(crate) fn generate_default_from_new(acc: &mut Assists, ctx: &AssistContext<'
|
||||
)
|
||||
}
|
||||
|
||||
// FIXME: based on from utils::generate_impl_text_inner
|
||||
fn generate_trait_impl_text_from_impl(impl_: &ast::Impl, trait_text: &str, code: &str) -> String {
|
||||
let generic_params = impl_.generic_param_list();
|
||||
let impl_ty = impl_.self_ty().unwrap();
|
||||
let generic_params = impl_.generic_param_list().map(|generic_params| {
|
||||
let lifetime_params =
|
||||
generic_params.lifetime_params().map(ast::GenericParam::LifetimeParam);
|
||||
let ty_or_const_params = generic_params.type_or_const_params().filter_map(|param| {
|
||||
// remove defaults since they can't be specified in impls
|
||||
match param {
|
||||
ast::TypeOrConstParam::Type(param) => {
|
||||
let param = param.clone_for_update();
|
||||
param.remove_default();
|
||||
Some(ast::GenericParam::TypeParam(param))
|
||||
}
|
||||
ast::TypeOrConstParam::Const(param) => {
|
||||
let param = param.clone_for_update();
|
||||
param.remove_default();
|
||||
Some(ast::GenericParam::ConstParam(param))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
make::generic_param_list(itertools::chain(lifetime_params, ty_or_const_params))
|
||||
});
|
||||
|
||||
let mut buf = String::with_capacity(code.len());
|
||||
buf.push_str("\n\n");
|
||||
|
||||
// `impl{generic_params} {trait_text} for {impl_.self_ty()}`
|
||||
buf.push_str("impl");
|
||||
|
||||
if let Some(generic_params) = &generic_params {
|
||||
let lifetimes = generic_params.lifetime_params().map(|lt| format!("{}", lt.syntax()));
|
||||
let toc_params = generic_params.type_or_const_params().map(|toc_param| match toc_param {
|
||||
ast::TypeOrConstParam::Type(type_param) => {
|
||||
let mut buf = String::new();
|
||||
if let Some(it) = type_param.name() {
|
||||
format_to!(buf, "{}", it.syntax());
|
||||
}
|
||||
if let Some(it) = type_param.colon_token() {
|
||||
format_to!(buf, "{} ", it);
|
||||
}
|
||||
if let Some(it) = type_param.type_bound_list() {
|
||||
format_to!(buf, "{}", it.syntax());
|
||||
}
|
||||
buf
|
||||
}
|
||||
ast::TypeOrConstParam::Const(const_param) => const_param.syntax().to_string(),
|
||||
});
|
||||
let generics = lifetimes.chain(toc_params).format(", ");
|
||||
format_to!(buf, "<{}>", generics);
|
||||
format_to!(buf, "{generic_params}")
|
||||
}
|
||||
|
||||
buf.push(' ');
|
||||
buf.push_str(trait_text);
|
||||
buf.push_str(" for ");
|
||||
buf.push_str(&impl_.self_ty().unwrap().syntax().text().to_string());
|
||||
format_to!(buf, " {trait_text} for {impl_ty}");
|
||||
|
||||
match impl_.where_clause() {
|
||||
Some(where_clause) => {
|
||||
format_to!(buf, "\n{}\n{{\n{}\n}}", where_clause, code);
|
||||
format_to!(buf, "\n{where_clause}\n{{\n{code}\n}}");
|
||||
}
|
||||
None => {
|
||||
format_to!(buf, " {{\n{}\n}}", code);
|
||||
format_to!(buf, " {{\n{code}\n}}");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,14 +51,14 @@ pub(crate) fn generate_delegate_methods(acc: &mut Assists, ctx: &AssistContext<'
|
||||
Some(field) => {
|
||||
let field_name = field.name()?;
|
||||
let field_ty = field.ty()?;
|
||||
(format!("{}", field_name), field_ty, field.syntax().text_range())
|
||||
(field_name.to_string(), field_ty, field.syntax().text_range())
|
||||
}
|
||||
None => {
|
||||
let field = ctx.find_node_at_offset::<ast::TupleField>()?;
|
||||
let field_list = ctx.find_node_at_offset::<ast::TupleFieldList>()?;
|
||||
let field_list_index = field_list.fields().position(|it| it == field)?;
|
||||
let field_ty = field.ty()?;
|
||||
(format!("{}", field_list_index), field_ty, field.syntax().text_range())
|
||||
(field_list_index.to_string(), field_ty, field.syntax().text_range())
|
||||
}
|
||||
};
|
||||
|
||||
@ -77,7 +77,7 @@ pub(crate) fn generate_delegate_methods(acc: &mut Assists, ctx: &AssistContext<'
|
||||
for method in methods {
|
||||
let adt = ast::Adt::Struct(strukt.clone());
|
||||
let name = method.name(ctx.db()).to_string();
|
||||
let impl_def = find_struct_impl(ctx, &adt, &name).flatten();
|
||||
let impl_def = find_struct_impl(ctx, &adt, &[name]).flatten();
|
||||
acc.add_group(
|
||||
&GroupLabel("Generate delegate methods…".to_owned()),
|
||||
AssistId("generate_delegate_methods", AssistKind::Generate),
|
||||
@ -151,7 +151,7 @@ pub(crate) fn generate_delegate_methods(acc: &mut Assists, ctx: &AssistContext<'
|
||||
Some(cap) => {
|
||||
let offset = strukt.syntax().text_range().end();
|
||||
let snippet = render_snippet(cap, impl_def.syntax(), cursor);
|
||||
let snippet = format!("\n\n{}", snippet);
|
||||
let snippet = format!("\n\n{snippet}");
|
||||
builder.insert_snippet(cap, offset, snippet);
|
||||
}
|
||||
None => {
|
||||
|
@ -66,7 +66,7 @@ fn generate_record_deref(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<(
|
||||
let target = field.syntax().text_range();
|
||||
acc.add(
|
||||
AssistId("generate_deref", AssistKind::Generate),
|
||||
format!("Generate `{:?}` impl using `{}`", deref_type_to_generate, field_name),
|
||||
format!("Generate `{deref_type_to_generate:?}` impl using `{field_name}`"),
|
||||
target,
|
||||
|edit| {
|
||||
generate_edit(
|
||||
@ -106,7 +106,7 @@ fn generate_tuple_deref(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()
|
||||
let target = field.syntax().text_range();
|
||||
acc.add(
|
||||
AssistId("generate_deref", AssistKind::Generate),
|
||||
format!("Generate `{:?}` impl using `{}`", deref_type_to_generate, field.syntax()),
|
||||
format!("Generate `{deref_type_to_generate:?}` impl using `{field}`"),
|
||||
target,
|
||||
|edit| {
|
||||
generate_edit(
|
||||
@ -132,18 +132,16 @@ fn generate_edit(
|
||||
let start_offset = strukt.syntax().text_range().end();
|
||||
let impl_code = match deref_type {
|
||||
DerefType::Deref => format!(
|
||||
r#" type Target = {0};
|
||||
r#" type Target = {field_type_syntax};
|
||||
|
||||
fn deref(&self) -> &Self::Target {{
|
||||
&self.{1}
|
||||
&self.{field_name}
|
||||
}}"#,
|
||||
field_type_syntax, field_name
|
||||
),
|
||||
DerefType::DerefMut => format!(
|
||||
r#" fn deref_mut(&mut self) -> &mut Self::Target {{
|
||||
&mut self.{}
|
||||
&mut self.{field_name}
|
||||
}}"#,
|
||||
field_name
|
||||
),
|
||||
};
|
||||
let strukt_adt = ast::Adt::Struct(strukt);
|
||||
|
@ -139,40 +139,44 @@ fn make_example_for_fn(ast_func: &ast::Fn, ctx: &AssistContext<'_>) -> Option<St
|
||||
|
||||
let mut example = String::new();
|
||||
|
||||
let use_path = build_path(ast_func, ctx)?;
|
||||
let is_unsafe = ast_func.unsafe_token().is_some();
|
||||
let param_list = ast_func.param_list()?;
|
||||
let ref_mut_params = ref_mut_params(¶m_list);
|
||||
let self_name = self_name(ast_func);
|
||||
|
||||
format_to!(example, "use {};\n\n", build_path(ast_func, ctx)?);
|
||||
format_to!(example, "use {use_path};\n\n");
|
||||
if let Some(self_name) = &self_name {
|
||||
if let Some(mtbl) = is_ref_mut_self(ast_func) {
|
||||
let mtbl = if mtbl == true { " mut" } else { "" };
|
||||
format_to!(example, "let{} {} = ;\n", mtbl, self_name);
|
||||
if let Some(mut_) = is_ref_mut_self(ast_func) {
|
||||
let mut_ = if mut_ == true { "mut " } else { "" };
|
||||
format_to!(example, "let {mut_}{self_name} = ;\n");
|
||||
}
|
||||
}
|
||||
for param_name in &ref_mut_params {
|
||||
format_to!(example, "let mut {} = ;\n", param_name);
|
||||
format_to!(example, "let mut {param_name} = ;\n");
|
||||
}
|
||||
// Call the function, check result
|
||||
let function_call = function_call(ast_func, ¶m_list, self_name.as_deref(), is_unsafe)?;
|
||||
if returns_a_value(ast_func, ctx) {
|
||||
if count_parameters(¶m_list) < 3 {
|
||||
format_to!(example, "assert_eq!({}, );\n", function_call);
|
||||
format_to!(example, "assert_eq!({function_call}, );\n");
|
||||
} else {
|
||||
format_to!(example, "let result = {};\n", function_call);
|
||||
format_to!(example, "let result = {function_call};\n");
|
||||
example.push_str("assert_eq!(result, );\n");
|
||||
}
|
||||
} else {
|
||||
format_to!(example, "{};\n", function_call);
|
||||
format_to!(example, "{function_call};\n");
|
||||
}
|
||||
// Check the mutated values
|
||||
if is_ref_mut_self(ast_func) == Some(true) {
|
||||
format_to!(example, "assert_eq!({}, );", self_name?);
|
||||
if let Some(self_name) = &self_name {
|
||||
if is_ref_mut_self(ast_func) == Some(true) {
|
||||
format_to!(example, "assert_eq!({self_name}, );");
|
||||
}
|
||||
}
|
||||
for param_name in &ref_mut_params {
|
||||
format_to!(example, "assert_eq!({}, );", param_name);
|
||||
format_to!(example, "assert_eq!({param_name}, );");
|
||||
}
|
||||
|
||||
Some(example)
|
||||
}
|
||||
|
||||
@ -189,7 +193,8 @@ fn introduction_builder(ast_func: &ast::Fn, ctx: &AssistContext<'_>) -> Option<S
|
||||
let intro_for_new = || {
|
||||
let is_new = name == "new";
|
||||
if is_new && ret_ty == self_ty {
|
||||
Some(format!("Creates a new [`{}`].", linkable_self_ty?))
|
||||
let self_ty = linkable_self_ty?;
|
||||
Some(format!("Creates a new [`{self_ty}`]."))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -214,7 +219,9 @@ fn introduction_builder(ast_func: &ast::Fn, ctx: &AssistContext<'_>) -> Option<S
|
||||
} else {
|
||||
""
|
||||
};
|
||||
Some(format!("Returns{reference} the {what} of this [`{}`].", linkable_self_ty?))
|
||||
|
||||
let self_ty = linkable_self_ty?;
|
||||
Some(format!("Returns{reference} the {what} of this [`{self_ty}`]."))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
@ -228,7 +235,9 @@ fn introduction_builder(ast_func: &ast::Fn, ctx: &AssistContext<'_>) -> Option<S
|
||||
if what == "len" {
|
||||
what = "length".into()
|
||||
};
|
||||
Some(format!("Sets the {what} of this [`{}`].", linkable_self_ty?))
|
||||
|
||||
let self_ty = linkable_self_ty?;
|
||||
Some(format!("Sets the {what} of this [`{self_ty}`]."))
|
||||
};
|
||||
|
||||
if let Some(intro) = intro_for_new() {
|
||||
@ -404,7 +413,7 @@ fn arguments_from_params(param_list: &ast::ParamList) -> String {
|
||||
// instance `TuplePat`) could be managed later.
|
||||
Some(ast::Pat::IdentPat(ident_pat)) => match ident_pat.name() {
|
||||
Some(name) => match is_a_ref_mut_param(¶m) {
|
||||
true => format!("&mut {}", name),
|
||||
true => format!("&mut {name}"),
|
||||
false => name.to_string(),
|
||||
},
|
||||
None => "_".to_string(),
|
||||
@ -424,14 +433,15 @@ fn function_call(
|
||||
let name = ast_func.name()?;
|
||||
let arguments = arguments_from_params(param_list);
|
||||
let function_call = if param_list.self_param().is_some() {
|
||||
format!("{}.{}({})", self_name?, name, arguments)
|
||||
let self_ = self_name?;
|
||||
format!("{self_}.{name}({arguments})")
|
||||
} else if let Some(implementation) = self_partial_type(ast_func) {
|
||||
format!("{}::{}({})", implementation, name, arguments)
|
||||
format!("{implementation}::{name}({arguments})")
|
||||
} else {
|
||||
format!("{}({})", name, arguments)
|
||||
format!("{name}({arguments})")
|
||||
};
|
||||
match is_unsafe {
|
||||
true => Some(format!("unsafe {{ {} }}", function_call)),
|
||||
true => Some(format!("unsafe {{ {function_call} }}")),
|
||||
false => Some(function_call),
|
||||
}
|
||||
}
|
||||
@ -469,8 +479,8 @@ fn build_path(ast_func: &ast::Fn, ctx: &AssistContext<'_>) -> Option<String> {
|
||||
.unwrap_or_else(|| "*".into());
|
||||
let module_def: ModuleDef = ctx.sema.to_def(ast_func)?.module(ctx.db()).into();
|
||||
match module_def.canonical_path(ctx.db()) {
|
||||
Some(path) => Some(format!("{}::{}::{}", crate_name, path, leaf)),
|
||||
None => Some(format!("{}::{}", crate_name, leaf)),
|
||||
Some(path) => Some(format!("{crate_name}::{path}::{leaf}")),
|
||||
None => Some(format!("{crate_name}::{leaf}")),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ pub(crate) fn generate_enum_is_method(acc: &mut Assists, ctx: &AssistContext<'_>
|
||||
let fn_name = format!("is_{}", &to_lower_snake_case(&variant_name.text()));
|
||||
|
||||
// Return early if we've found an existing new fn
|
||||
let impl_def = find_struct_impl(ctx, &parent_enum, &fn_name)?;
|
||||
let impl_def = find_struct_impl(ctx, &parent_enum, &[fn_name.clone()])?;
|
||||
|
||||
let target = variant.syntax().text_range();
|
||||
acc.add_group(
|
||||
@ -61,21 +61,15 @@ pub(crate) fn generate_enum_is_method(acc: &mut Assists, ctx: &AssistContext<'_>
|
||||
"Generate an `is_` method for this enum variant",
|
||||
target,
|
||||
|builder| {
|
||||
let vis = parent_enum.visibility().map_or(String::new(), |v| format!("{} ", v));
|
||||
let vis = parent_enum.visibility().map_or(String::new(), |v| format!("{v} "));
|
||||
let method = format!(
|
||||
" /// Returns `true` if the {} is [`{variant}`].
|
||||
" /// Returns `true` if the {enum_lowercase_name} is [`{variant_name}`].
|
||||
///
|
||||
/// [`{variant}`]: {}::{variant}
|
||||
/// [`{variant_name}`]: {enum_name}::{variant_name}
|
||||
#[must_use]
|
||||
{}fn {}(&self) -> bool {{
|
||||
matches!(self, Self::{variant}{})
|
||||
{vis}fn {fn_name}(&self) -> bool {{
|
||||
matches!(self, Self::{variant_name}{pattern_suffix})
|
||||
}}",
|
||||
enum_lowercase_name,
|
||||
enum_name,
|
||||
vis,
|
||||
fn_name,
|
||||
pattern_suffix,
|
||||
variant = variant_name
|
||||
);
|
||||
|
||||
add_method_to_adt(builder, &parent_enum, impl_def, &method);
|
||||
|
@ -116,6 +116,14 @@ fn generate_enum_projection_method(
|
||||
assist_description: &str,
|
||||
props: ProjectionProps,
|
||||
) -> Option<()> {
|
||||
let ProjectionProps {
|
||||
fn_name_prefix,
|
||||
self_param,
|
||||
return_prefix,
|
||||
return_suffix,
|
||||
happy_case,
|
||||
sad_case,
|
||||
} = props;
|
||||
let variant = ctx.find_node_at_offset::<ast::Variant>()?;
|
||||
let variant_name = variant.name()?;
|
||||
let parent_enum = ast::Adt::Enum(variant.parent_enum());
|
||||
@ -125,7 +133,7 @@ fn generate_enum_projection_method(
|
||||
let (field,) = record.fields().collect_tuple()?;
|
||||
let name = field.name()?.to_string();
|
||||
let ty = field.ty()?;
|
||||
let pattern_suffix = format!(" {{ {} }}", name);
|
||||
let pattern_suffix = format!(" {{ {name} }}");
|
||||
(pattern_suffix, ty, name)
|
||||
}
|
||||
ast::StructKind::Tuple(tuple) => {
|
||||
@ -136,11 +144,10 @@ fn generate_enum_projection_method(
|
||||
ast::StructKind::Unit => return None,
|
||||
};
|
||||
|
||||
let fn_name =
|
||||
format!("{}_{}", props.fn_name_prefix, &to_lower_snake_case(&variant_name.text()));
|
||||
let fn_name = format!("{}_{}", fn_name_prefix, &to_lower_snake_case(&variant_name.text()));
|
||||
|
||||
// Return early if we've found an existing new fn
|
||||
let impl_def = find_struct_impl(ctx, &parent_enum, &fn_name)?;
|
||||
let impl_def = find_struct_impl(ctx, &parent_enum, &[fn_name.clone()])?;
|
||||
|
||||
let target = variant.syntax().text_range();
|
||||
acc.add_group(
|
||||
@ -149,27 +156,15 @@ fn generate_enum_projection_method(
|
||||
assist_description,
|
||||
target,
|
||||
|builder| {
|
||||
let vis = parent_enum.visibility().map_or(String::new(), |v| format!("{} ", v));
|
||||
let vis = parent_enum.visibility().map_or(String::new(), |v| format!("{v} "));
|
||||
let method = format!(
|
||||
" {0}fn {1}({2}) -> {3}{4}{5} {{
|
||||
if let Self::{6}{7} = self {{
|
||||
{8}({9})
|
||||
" {vis}fn {fn_name}({self_param}) -> {return_prefix}{field_type}{return_suffix} {{
|
||||
if let Self::{variant_name}{pattern_suffix} = self {{
|
||||
{happy_case}({bound_name})
|
||||
}} else {{
|
||||
{10}
|
||||
{sad_case}
|
||||
}}
|
||||
}}",
|
||||
vis,
|
||||
fn_name,
|
||||
props.self_param,
|
||||
props.return_prefix,
|
||||
field_type.syntax(),
|
||||
props.return_suffix,
|
||||
variant_name,
|
||||
pattern_suffix,
|
||||
props.happy_case,
|
||||
bound_name,
|
||||
props.sad_case,
|
||||
);
|
||||
}}");
|
||||
|
||||
add_method_to_adt(builder, &parent_enum, impl_def, &method);
|
||||
},
|
||||
|
@ -56,23 +56,18 @@ pub(crate) fn generate_from_impl_for_enum(
|
||||
target,
|
||||
|edit| {
|
||||
let start_offset = variant.parent_enum().syntax().text_range().end();
|
||||
let from_trait = format!("From<{}>", field_type.syntax());
|
||||
let from_trait = format!("From<{field_type}>");
|
||||
let impl_code = if let Some(name) = field_name {
|
||||
format!(
|
||||
r#" fn from({0}: {1}) -> Self {{
|
||||
Self::{2} {{ {0} }}
|
||||
}}"#,
|
||||
name.text(),
|
||||
field_type.syntax(),
|
||||
variant_name,
|
||||
r#" fn from({name}: {field_type}) -> Self {{
|
||||
Self::{variant_name} {{ {name} }}
|
||||
}}"#
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
r#" fn from(v: {}) -> Self {{
|
||||
Self::{}(v)
|
||||
}}"#,
|
||||
field_type.syntax(),
|
||||
variant_name,
|
||||
r#" fn from(v: {field_type}) -> Self {{
|
||||
Self::{variant_name}(v)
|
||||
}}"#
|
||||
)
|
||||
};
|
||||
let from_impl = generate_trait_impl_text(&enum_, &from_trait, &impl_code);
|
||||
|
@ -179,7 +179,7 @@ fn add_func_to_accumulator(
|
||||
let function_template = function_builder.render(adt_name.is_some());
|
||||
let mut func = function_template.to_string(ctx.config.snippet_cap);
|
||||
if let Some(name) = adt_name {
|
||||
func = format!("\n{}impl {} {{\n{}\n{}}}", indent, name, func, indent);
|
||||
func = format!("\n{indent}impl {name} {{\n{func}\n{indent}}}");
|
||||
}
|
||||
builder.edit_file(file);
|
||||
match ctx.config.snippet_cap {
|
||||
@ -198,7 +198,7 @@ fn get_adt_source(
|
||||
let file = ctx.sema.parse(range.file_id);
|
||||
let adt_source =
|
||||
ctx.sema.find_node_at_offset_with_macros(file.syntax(), range.range.start())?;
|
||||
find_struct_impl(ctx, &adt_source, fn_name).map(|impl_| (impl_, range.file_id))
|
||||
find_struct_impl(ctx, &adt_source, &[fn_name.to_string()]).map(|impl_| (impl_, range.file_id))
|
||||
}
|
||||
|
||||
struct FunctionTemplate {
|
||||
@ -212,23 +212,26 @@ struct FunctionTemplate {
|
||||
|
||||
impl FunctionTemplate {
|
||||
fn to_string(&self, cap: Option<SnippetCap>) -> String {
|
||||
let Self { leading_ws, fn_def, ret_type, should_focus_return_type, trailing_ws, tail_expr } =
|
||||
self;
|
||||
|
||||
let f = match cap {
|
||||
Some(cap) => {
|
||||
let cursor = if self.should_focus_return_type {
|
||||
let cursor = if *should_focus_return_type {
|
||||
// Focus the return type if there is one
|
||||
match self.ret_type {
|
||||
Some(ref ret_type) => ret_type.syntax(),
|
||||
None => self.tail_expr.syntax(),
|
||||
match ret_type {
|
||||
Some(ret_type) => ret_type.syntax(),
|
||||
None => tail_expr.syntax(),
|
||||
}
|
||||
} else {
|
||||
self.tail_expr.syntax()
|
||||
tail_expr.syntax()
|
||||
};
|
||||
render_snippet(cap, self.fn_def.syntax(), Cursor::Replace(cursor))
|
||||
render_snippet(cap, fn_def.syntax(), Cursor::Replace(cursor))
|
||||
}
|
||||
None => self.fn_def.to_string(),
|
||||
None => fn_def.to_string(),
|
||||
};
|
||||
|
||||
format!("{}{}{}", self.leading_ws, f, self.trailing_ws)
|
||||
format!("{leading_ws}{f}{trailing_ws}")
|
||||
}
|
||||
}
|
||||
|
||||
@ -330,9 +333,9 @@ fn render(self, is_method: bool) -> FunctionTemplate {
|
||||
let mut indent = IndentLevel::from_node(&it);
|
||||
if is_method {
|
||||
indent = indent + 1;
|
||||
leading_ws = format!("{}", indent);
|
||||
leading_ws = format!("{indent}");
|
||||
} else {
|
||||
leading_ws = format!("\n\n{}", indent);
|
||||
leading_ws = format!("\n\n{indent}");
|
||||
}
|
||||
|
||||
fn_def = fn_def.indent(indent);
|
||||
@ -340,9 +343,10 @@ fn render(self, is_method: bool) -> FunctionTemplate {
|
||||
}
|
||||
GeneratedFunctionTarget::InEmptyItemList(it) => {
|
||||
let indent = IndentLevel::from_node(&it);
|
||||
leading_ws = format!("\n{}", indent + 1);
|
||||
fn_def = fn_def.indent(indent + 1);
|
||||
trailing_ws = format!("\n{}", indent);
|
||||
let leading_indent = indent + 1;
|
||||
leading_ws = format!("\n{leading_indent}");
|
||||
fn_def = fn_def.indent(leading_indent);
|
||||
trailing_ws = format!("\n{indent}");
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,9 @@
|
||||
use ide_db::famous_defs::FamousDefs;
|
||||
use stdx::{format_to, to_lower_snake_case};
|
||||
use syntax::ast::{self, AstNode, HasName, HasVisibility};
|
||||
use syntax::{
|
||||
ast::{self, AstNode, HasName, HasVisibility},
|
||||
TextRange,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
utils::{convert_reference_type, find_impl_block_end, find_struct_impl, generate_impl_text},
|
||||
@ -72,92 +75,259 @@ pub(crate) fn generate_getter_mut(acc: &mut Assists, ctx: &AssistContext<'_>) ->
|
||||
generate_getter_impl(acc, ctx, true)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct RecordFieldInfo {
|
||||
field_name: syntax::ast::Name,
|
||||
field_ty: syntax::ast::Type,
|
||||
fn_name: String,
|
||||
target: TextRange,
|
||||
}
|
||||
|
||||
struct GetterInfo {
|
||||
impl_def: Option<ast::Impl>,
|
||||
strukt: ast::Struct,
|
||||
mutable: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn generate_getter_impl(
|
||||
acc: &mut Assists,
|
||||
ctx: &AssistContext<'_>,
|
||||
mutable: bool,
|
||||
) -> Option<()> {
|
||||
let strukt = ctx.find_node_at_offset::<ast::Struct>()?;
|
||||
let field = ctx.find_node_at_offset::<ast::RecordField>()?;
|
||||
// This if condition denotes two modes this assist can work in:
|
||||
// - First is acting upon selection of record fields
|
||||
// - Next is acting upon a single record field
|
||||
//
|
||||
// This is the only part where implementation diverges a bit,
|
||||
// subsequent code is generic for both of these modes
|
||||
|
||||
let field_name = field.name()?;
|
||||
let field_ty = field.ty()?;
|
||||
let (strukt, info_of_record_fields, fn_names) = if !ctx.has_empty_selection() {
|
||||
// Selection Mode
|
||||
let node = ctx.covering_element();
|
||||
|
||||
// Return early if we've found an existing fn
|
||||
let mut fn_name = to_lower_snake_case(&field_name.to_string());
|
||||
if mutable {
|
||||
format_to!(fn_name, "_mut");
|
||||
let node = match node {
|
||||
syntax::NodeOrToken::Node(n) => n,
|
||||
syntax::NodeOrToken::Token(t) => t.parent()?,
|
||||
};
|
||||
|
||||
let parent_struct = node.ancestors().find_map(ast::Struct::cast)?;
|
||||
|
||||
let (info_of_record_fields, field_names) =
|
||||
extract_and_parse_record_fields(&parent_struct, ctx.selection_trimmed(), mutable)?;
|
||||
|
||||
(parent_struct, info_of_record_fields, field_names)
|
||||
} else {
|
||||
// Single Record Field mode
|
||||
let strukt = ctx.find_node_at_offset::<ast::Struct>()?;
|
||||
let field = ctx.find_node_at_offset::<ast::RecordField>()?;
|
||||
|
||||
let record_field_info = parse_record_field(field, mutable)?;
|
||||
|
||||
let fn_name = record_field_info.fn_name.clone();
|
||||
|
||||
(strukt, vec![record_field_info], vec![fn_name])
|
||||
};
|
||||
|
||||
// No record fields to do work on :(
|
||||
if info_of_record_fields.len() == 0 {
|
||||
return None;
|
||||
}
|
||||
let impl_def = find_struct_impl(ctx, &ast::Adt::Struct(strukt.clone()), fn_name.as_str())?;
|
||||
|
||||
let impl_def = find_struct_impl(ctx, &ast::Adt::Struct(strukt.clone()), &fn_names)?;
|
||||
|
||||
let (id, label) = if mutable {
|
||||
("generate_getter_mut", "Generate a mut getter method")
|
||||
} else {
|
||||
("generate_getter", "Generate a getter method")
|
||||
};
|
||||
let target = field.syntax().text_range();
|
||||
|
||||
// Computing collective text range of all record fields in selected region
|
||||
let target: TextRange = info_of_record_fields
|
||||
.iter()
|
||||
.map(|record_field_info| record_field_info.target)
|
||||
.reduce(|acc, target| acc.cover(target))?;
|
||||
|
||||
let getter_info = GetterInfo { impl_def, strukt, mutable };
|
||||
|
||||
acc.add_group(
|
||||
&GroupLabel("Generate getter/setter".to_owned()),
|
||||
AssistId(id, AssistKind::Generate),
|
||||
label,
|
||||
target,
|
||||
|builder| {
|
||||
let record_fields_count = info_of_record_fields.len();
|
||||
|
||||
let mut buf = String::with_capacity(512);
|
||||
|
||||
if impl_def.is_some() {
|
||||
buf.push('\n');
|
||||
// Check if an impl exists
|
||||
if let Some(impl_def) = &getter_info.impl_def {
|
||||
// Check if impl is empty
|
||||
if let Some(assoc_item_list) = impl_def.assoc_item_list() {
|
||||
if assoc_item_list.assoc_items().next().is_some() {
|
||||
// If not empty then only insert a new line
|
||||
buf.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let vis = strukt.visibility().map_or(String::new(), |v| format!("{} ", v));
|
||||
let (ty, body) = if mutable {
|
||||
(format!("&mut {}", field_ty), format!("&mut self.{}", field_name))
|
||||
} else {
|
||||
(|| {
|
||||
let krate = ctx.sema.scope(field_ty.syntax())?.krate();
|
||||
let famous_defs = &FamousDefs(&ctx.sema, krate);
|
||||
ctx.sema
|
||||
.resolve_type(&field_ty)
|
||||
.and_then(|ty| convert_reference_type(ty, ctx.db(), famous_defs))
|
||||
.map(|conversion| {
|
||||
cov_mark::hit!(convert_reference_type);
|
||||
(
|
||||
conversion.convert_type(ctx.db()),
|
||||
conversion.getter(field_name.to_string()),
|
||||
)
|
||||
})
|
||||
})()
|
||||
.unwrap_or_else(|| (format!("&{}", field_ty), format!("&self.{}", field_name)))
|
||||
};
|
||||
for (i, record_field_info) in info_of_record_fields.iter().enumerate() {
|
||||
// this buf inserts a newline at the end of a getter
|
||||
// automatically, if one wants to add one more newline
|
||||
// for separating it from other assoc items, that needs
|
||||
// to be handled spearately
|
||||
let mut getter_buf =
|
||||
generate_getter_from_info(ctx, &getter_info, &record_field_info);
|
||||
|
||||
format_to!(
|
||||
buf,
|
||||
" {}fn {}(&{}self) -> {} {{
|
||||
{}
|
||||
}}",
|
||||
vis,
|
||||
fn_name,
|
||||
mutable.then(|| "mut ").unwrap_or_default(),
|
||||
ty,
|
||||
body,
|
||||
);
|
||||
// Insert `$0` only for last getter we generate
|
||||
if i == record_fields_count - 1 {
|
||||
getter_buf = getter_buf.replacen("fn ", "fn $0", 1);
|
||||
}
|
||||
|
||||
let start_offset = impl_def
|
||||
.and_then(|impl_def| find_impl_block_end(impl_def, &mut buf))
|
||||
// For first element we do not merge with '\n', as
|
||||
// that can be inserted by impl_def check defined
|
||||
// above, for other cases which are:
|
||||
//
|
||||
// - impl exists but it empty, here we would ideally
|
||||
// not want to keep newline between impl <struct> {
|
||||
// and fn <fn-name>() { line
|
||||
//
|
||||
// - next if impl itself does not exist, in this
|
||||
// case we ourselves generate a new impl and that
|
||||
// again ends up with the same reasoning as above
|
||||
// for not keeping newline
|
||||
if i == 0 {
|
||||
buf = buf + &getter_buf;
|
||||
} else {
|
||||
buf = buf + "\n" + &getter_buf;
|
||||
}
|
||||
|
||||
// We don't insert a new line at the end of
|
||||
// last getter as it will end up in the end
|
||||
// of an impl where we would not like to keep
|
||||
// getter and end of impl ( i.e. `}` ) with an
|
||||
// extra line for no reason
|
||||
if i < record_fields_count - 1 {
|
||||
buf = buf + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
let start_offset = getter_info
|
||||
.impl_def
|
||||
.as_ref()
|
||||
.and_then(|impl_def| find_impl_block_end(impl_def.to_owned(), &mut buf))
|
||||
.unwrap_or_else(|| {
|
||||
buf = generate_impl_text(&ast::Adt::Struct(strukt.clone()), &buf);
|
||||
strukt.syntax().text_range().end()
|
||||
buf = generate_impl_text(&ast::Adt::Struct(getter_info.strukt.clone()), &buf);
|
||||
getter_info.strukt.syntax().text_range().end()
|
||||
});
|
||||
|
||||
match ctx.config.snippet_cap {
|
||||
Some(cap) => {
|
||||
builder.insert_snippet(cap, start_offset, buf.replacen("fn ", "fn $0", 1))
|
||||
}
|
||||
Some(cap) => builder.insert_snippet(cap, start_offset, buf),
|
||||
None => builder.insert(start_offset, buf),
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn generate_getter_from_info(
|
||||
ctx: &AssistContext<'_>,
|
||||
info: &GetterInfo,
|
||||
record_field_info: &RecordFieldInfo,
|
||||
) -> String {
|
||||
let mut buf = String::with_capacity(512);
|
||||
|
||||
let vis = info.strukt.visibility().map_or(String::new(), |v| format!("{} ", v));
|
||||
let (ty, body) = if info.mutable {
|
||||
(
|
||||
format!("&mut {}", record_field_info.field_ty),
|
||||
format!("&mut self.{}", record_field_info.field_name),
|
||||
)
|
||||
} else {
|
||||
(|| {
|
||||
let krate = ctx.sema.scope(record_field_info.field_ty.syntax())?.krate();
|
||||
let famous_defs = &FamousDefs(&ctx.sema, krate);
|
||||
ctx.sema
|
||||
.resolve_type(&record_field_info.field_ty)
|
||||
.and_then(|ty| convert_reference_type(ty, ctx.db(), famous_defs))
|
||||
.map(|conversion| {
|
||||
cov_mark::hit!(convert_reference_type);
|
||||
(
|
||||
conversion.convert_type(ctx.db()),
|
||||
conversion.getter(record_field_info.field_name.to_string()),
|
||||
)
|
||||
})
|
||||
})()
|
||||
.unwrap_or_else(|| {
|
||||
(
|
||||
format!("&{}", record_field_info.field_ty),
|
||||
format!("&self.{}", record_field_info.field_name),
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
format_to!(
|
||||
buf,
|
||||
" {}fn {}(&{}self) -> {} {{
|
||||
{}
|
||||
}}",
|
||||
vis,
|
||||
record_field_info.fn_name,
|
||||
info.mutable.then(|| "mut ").unwrap_or_default(),
|
||||
ty,
|
||||
body,
|
||||
);
|
||||
|
||||
buf
|
||||
}
|
||||
|
||||
fn extract_and_parse_record_fields(
|
||||
node: &ast::Struct,
|
||||
selection_range: TextRange,
|
||||
mutable: bool,
|
||||
) -> Option<(Vec<RecordFieldInfo>, Vec<String>)> {
|
||||
let mut field_names: Vec<String> = vec![];
|
||||
let field_list = node.field_list()?;
|
||||
|
||||
match field_list {
|
||||
ast::FieldList::RecordFieldList(ele) => {
|
||||
let info_of_record_fields_in_selection = ele
|
||||
.fields()
|
||||
.filter_map(|record_field| {
|
||||
if selection_range.contains_range(record_field.syntax().text_range()) {
|
||||
let record_field_info = parse_record_field(record_field, mutable)?;
|
||||
field_names.push(record_field_info.fn_name.clone());
|
||||
return Some(record_field_info);
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
.collect::<Vec<RecordFieldInfo>>();
|
||||
|
||||
if info_of_record_fields_in_selection.len() == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((info_of_record_fields_in_selection, field_names))
|
||||
}
|
||||
ast::FieldList::TupleFieldList(_) => {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_record_field(record_field: ast::RecordField, mutable: bool) -> Option<RecordFieldInfo> {
|
||||
let field_name = record_field.name()?;
|
||||
let field_ty = record_field.ty()?;
|
||||
|
||||
let mut fn_name = to_lower_snake_case(&field_name.to_string());
|
||||
if mutable {
|
||||
format_to!(fn_name, "_mut");
|
||||
}
|
||||
|
||||
let target = record_field.syntax().text_range();
|
||||
|
||||
Some(RecordFieldInfo { field_name, field_ty, fn_name, target })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::tests::{check_assist, check_assist_not_applicable};
|
||||
@ -489,4 +659,53 @@ fn $0data(&self) -> Result<&bool, &i32> {
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_multiple_getters_from_selection() {
|
||||
check_assist(
|
||||
generate_getter,
|
||||
r#"
|
||||
struct Context {
|
||||
$0data: Data,
|
||||
count: usize,$0
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
struct Context {
|
||||
data: Data,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
fn data(&self) -> &Data {
|
||||
&self.data
|
||||
}
|
||||
|
||||
fn $0count(&self) -> &usize {
|
||||
&self.count
|
||||
}
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_multiple_getters_from_selection_one_already_exists() {
|
||||
// As impl for one of the fields already exist, skip it
|
||||
check_assist_not_applicable(
|
||||
generate_getter,
|
||||
r#"
|
||||
struct Context {
|
||||
$0data: Data,
|
||||
count: usize,$0
|
||||
}
|
||||
|
||||
impl Context {
|
||||
fn data(&self) -> &Data {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ pub(crate) fn generate_impl(acc: &mut Assists, ctx: &AssistContext<'_>) -> Optio
|
||||
|
||||
acc.add(
|
||||
AssistId("generate_impl", AssistKind::Generate),
|
||||
format!("Generate impl for `{}`", name),
|
||||
format!("Generate impl for `{name}`"),
|
||||
target,
|
||||
|edit| {
|
||||
let start_offset = nominal.syntax().text_range().end();
|
||||
|
@ -39,7 +39,8 @@ pub(crate) fn generate_new(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option
|
||||
};
|
||||
|
||||
// Return early if we've found an existing new fn
|
||||
let impl_def = find_struct_impl(ctx, &ast::Adt::Struct(strukt.clone()), "new")?;
|
||||
let impl_def =
|
||||
find_struct_impl(ctx, &ast::Adt::Struct(strukt.clone()), &[String::from("new")])?;
|
||||
|
||||
let current_module = ctx.sema.scope(strukt.syntax())?.module();
|
||||
|
||||
@ -51,11 +52,13 @@ pub(crate) fn generate_new(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option
|
||||
buf.push('\n');
|
||||
}
|
||||
|
||||
let vis = strukt.visibility().map_or(String::new(), |v| format!("{} ", v));
|
||||
let vis = strukt.visibility().map_or(String::new(), |v| format!("{v} "));
|
||||
|
||||
let trivial_constructors = field_list
|
||||
.fields()
|
||||
.map(|f| {
|
||||
let name = f.name()?;
|
||||
|
||||
let ty = ctx.sema.resolve_type(&f.ty()?)?;
|
||||
|
||||
let item_in_ns = hir::ItemInNs::from(hir::ModuleDef::from(ty.as_adt()?));
|
||||
@ -72,7 +75,7 @@ pub(crate) fn generate_new(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option
|
||||
&ty,
|
||||
)?;
|
||||
|
||||
Some(format!("{}: {}", f.name()?.syntax(), expr))
|
||||
Some(format!("{name}: {expr}"))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@ -81,7 +84,10 @@ pub(crate) fn generate_new(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option
|
||||
.enumerate()
|
||||
.filter_map(|(i, f)| {
|
||||
if trivial_constructors[i].is_none() {
|
||||
Some(format!("{}: {}", f.name()?.syntax(), f.ty()?.syntax()))
|
||||
let name = f.name()?;
|
||||
let ty = f.ty()?;
|
||||
|
||||
Some(format!("{name}: {ty}"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -101,7 +107,7 @@ pub(crate) fn generate_new(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option
|
||||
})
|
||||
.format(", ");
|
||||
|
||||
format_to!(buf, " {}fn new({}) -> Self {{ Self {{ {} }} }}", vis, params, fields);
|
||||
format_to!(buf, " {vis}fn new({params}) -> Self {{ Self {{ {fields} }} }}");
|
||||
|
||||
let start_offset = impl_def
|
||||
.and_then(|impl_def| find_impl_block_start(impl_def, &mut buf))
|
||||
|
@ -36,11 +36,8 @@ pub(crate) fn generate_setter(acc: &mut Assists, ctx: &AssistContext<'_>) -> Opt
|
||||
|
||||
// Return early if we've found an existing fn
|
||||
let fn_name = to_lower_snake_case(&field_name.to_string());
|
||||
let impl_def = find_struct_impl(
|
||||
ctx,
|
||||
&ast::Adt::Struct(strukt.clone()),
|
||||
format!("set_{}", fn_name).as_str(),
|
||||
)?;
|
||||
let impl_def =
|
||||
find_struct_impl(ctx, &ast::Adt::Struct(strukt.clone()), &[format!("set_{fn_name}")])?;
|
||||
|
||||
let target = field.syntax().text_range();
|
||||
acc.add_group(
|
||||
@ -55,18 +52,12 @@ pub(crate) fn generate_setter(acc: &mut Assists, ctx: &AssistContext<'_>) -> Opt
|
||||
buf.push('\n');
|
||||
}
|
||||
|
||||
let vis = strukt.visibility().map_or(String::new(), |v| format!("{} ", v));
|
||||
let vis = strukt.visibility().map_or(String::new(), |v| format!("{v} "));
|
||||
format_to!(
|
||||
buf,
|
||||
" {}fn set_{}(&mut self, {}: {}) {{
|
||||
self.{} = {};
|
||||
}}",
|
||||
vis,
|
||||
fn_name,
|
||||
fn_name,
|
||||
field_ty,
|
||||
fn_name,
|
||||
fn_name,
|
||||
" {vis}fn set_{fn_name}(&mut self, {fn_name}: {field_ty}) {{
|
||||
self.{fn_name} = {fn_name};
|
||||
}}"
|
||||
);
|
||||
|
||||
let start_offset = impl_def
|
||||
|
@ -331,10 +331,14 @@ fn calc_depth(pat: &ast::Pat, depth: usize) -> usize {
|
||||
// FIXME: change the new fn checking to a more semantic approach when that's more
|
||||
// viable (e.g. we process proc macros, etc)
|
||||
// FIXME: this partially overlaps with `find_impl_block_*`
|
||||
|
||||
/// `find_struct_impl` looks for impl of a struct, but this also has additional feature
|
||||
/// where it takes a list of function names and check if they exist inside impl_, if
|
||||
/// even one match is found, it returns None
|
||||
pub(crate) fn find_struct_impl(
|
||||
ctx: &AssistContext<'_>,
|
||||
adt: &ast::Adt,
|
||||
name: &str,
|
||||
names: &[String],
|
||||
) -> Option<Option<ast::Impl>> {
|
||||
let db = ctx.db();
|
||||
let module = adt.syntax().parent()?;
|
||||
@ -362,7 +366,7 @@ pub(crate) fn find_struct_impl(
|
||||
});
|
||||
|
||||
if let Some(ref impl_blk) = block {
|
||||
if has_fn(impl_blk, name) {
|
||||
if has_any_fn(impl_blk, names) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@ -370,12 +374,12 @@ pub(crate) fn find_struct_impl(
|
||||
Some(block)
|
||||
}
|
||||
|
||||
fn has_fn(imp: &ast::Impl, rhs_name: &str) -> bool {
|
||||
fn has_any_fn(imp: &ast::Impl, names: &[String]) -> bool {
|
||||
if let Some(il) = imp.assoc_item_list() {
|
||||
for item in il.assoc_items() {
|
||||
if let ast::AssocItem::Fn(f) = item {
|
||||
if let Some(name) = f.name() {
|
||||
if name.text().eq_ignore_ascii_case(rhs_name) {
|
||||
if names.iter().any(|n| n.eq_ignore_ascii_case(&name.text())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1,37 +0,0 @@
|
||||
use hir::InFile;
|
||||
|
||||
use crate::{Diagnostic, DiagnosticsContext};
|
||||
|
||||
// Diagnostic: incorrect-try-target
|
||||
//
|
||||
// This diagnostic is triggered if a question mark operator was used in a context where it is not applicable.
|
||||
pub(crate) fn incorrect_try_expr(
|
||||
ctx: &DiagnosticsContext<'_>,
|
||||
d: &hir::IncorrectTryExpr,
|
||||
) -> Diagnostic {
|
||||
Diagnostic::new(
|
||||
"incorrect-try-target",
|
||||
format!("the return type of the containing function does not implement `FromResidual`"),
|
||||
ctx.sema
|
||||
.diagnostics_display_range(InFile::new(d.expr.file_id, d.expr.value.clone().into()))
|
||||
.range,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::tests::check_diagnostics;
|
||||
|
||||
#[test]
|
||||
fn try_ops_diag() {
|
||||
check_diagnostics(
|
||||
r#"
|
||||
//- minicore: try
|
||||
fn test() {
|
||||
core::ops::ControlFlow::<u32, f32>::Continue(1.0)?;
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: the return type of the containing function does not implement `FromResidual`
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
use hir::{db::DefDatabase, HirDisplay};
|
||||
|
||||
use crate::{Diagnostic, DiagnosticsContext};
|
||||
|
||||
// Diagnostic: not-implemented
|
||||
//
|
||||
// This diagnostic is triggered if a type doesn't implement a necessary trait.
|
||||
pub(crate) fn not_implemented(ctx: &DiagnosticsContext<'_>, d: &hir::NotImplemented) -> Diagnostic {
|
||||
Diagnostic::new(
|
||||
"not-implemented",
|
||||
format!(
|
||||
"the trait `{}` is not implemented for `{}`",
|
||||
ctx.sema.db.trait_data(d.trait_).name,
|
||||
d.ty.display(ctx.sema.db)
|
||||
),
|
||||
ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())).range,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::tests::check_diagnostics;
|
||||
|
||||
#[test]
|
||||
fn missing_try_impl() {
|
||||
check_diagnostics(
|
||||
r#"
|
||||
//- minicore: try
|
||||
fn main() {
|
||||
()?;
|
||||
} //^^ error: the trait `Try` is not implemented for `()`
|
||||
"#,
|
||||
)
|
||||
}
|
||||
}
|
@ -29,7 +29,6 @@ mod handlers {
|
||||
pub(crate) mod break_outside_of_loop;
|
||||
pub(crate) mod inactive_code;
|
||||
pub(crate) mod incorrect_case;
|
||||
pub(crate) mod incorrect_try_expr;
|
||||
pub(crate) mod invalid_derive_target;
|
||||
pub(crate) mod macro_error;
|
||||
pub(crate) mod malformed_derive;
|
||||
@ -37,7 +36,6 @@ mod handlers {
|
||||
pub(crate) mod missing_fields;
|
||||
pub(crate) mod missing_match_arms;
|
||||
pub(crate) mod missing_unsafe;
|
||||
pub(crate) mod not_implemented;
|
||||
pub(crate) mod no_such_field;
|
||||
pub(crate) mod replace_filter_map_next_with_find_map;
|
||||
pub(crate) mod type_mismatch;
|
||||
@ -227,14 +225,12 @@ pub fn diagnostics(
|
||||
let d = match diag {
|
||||
AnyDiagnostic::BreakOutsideOfLoop(d) => handlers::break_outside_of_loop::break_outside_of_loop(&ctx, &d),
|
||||
AnyDiagnostic::IncorrectCase(d) => handlers::incorrect_case::incorrect_case(&ctx, &d),
|
||||
AnyDiagnostic::IncorrectTryExpr(d) => handlers::incorrect_try_expr::incorrect_try_expr(&ctx, &d),
|
||||
AnyDiagnostic::MacroError(d) => handlers::macro_error::macro_error(&ctx, &d),
|
||||
AnyDiagnostic::MalformedDerive(d) => handlers::malformed_derive::malformed_derive(&ctx, &d),
|
||||
AnyDiagnostic::MismatchedArgCount(d) => handlers::mismatched_arg_count::mismatched_arg_count(&ctx, &d),
|
||||
AnyDiagnostic::MissingFields(d) => handlers::missing_fields::missing_fields(&ctx, &d),
|
||||
AnyDiagnostic::MissingMatchArms(d) => handlers::missing_match_arms::missing_match_arms(&ctx, &d),
|
||||
AnyDiagnostic::MissingUnsafe(d) => handlers::missing_unsafe::missing_unsafe(&ctx, &d),
|
||||
AnyDiagnostic::NotImplemented(d) => handlers::not_implemented::not_implemented(&ctx, &d),
|
||||
AnyDiagnostic::NoSuchField(d) => handlers::no_such_field::no_such_field(&ctx, &d),
|
||||
AnyDiagnostic::ReplaceFilterMapNextWithFindMap(d) => handlers::replace_filter_map_next_with_find_map::replace_filter_map_next_with_find_map(&ctx, &d),
|
||||
AnyDiagnostic::TypeMismatch(d) => handlers::type_mismatch::type_mismatch(&ctx, &d),
|
||||
|
@ -4913,22 +4913,6 @@ fn foo() -> NotResult<(), Short> {
|
||||
```
|
||||
"#]],
|
||||
);
|
||||
check_hover_range(
|
||||
r#"
|
||||
//- minicore: try
|
||||
use core::ops::ControlFlow;
|
||||
fn foo() -> ControlFlow<()> {
|
||||
$0ControlFlow::Break(())?$0;
|
||||
ControlFlow::Continue(())
|
||||
}
|
||||
"#,
|
||||
expect![[r#"
|
||||
```text
|
||||
Try Target Type: ControlFlow<(), {unknown}>
|
||||
Propagated as: ControlFlow<(), ()>
|
||||
```
|
||||
"#]],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -4944,9 +4928,9 @@ fn foo() -> Option<()> {
|
||||
}
|
||||
"#,
|
||||
expect![[r#"
|
||||
```rust
|
||||
i32
|
||||
```"#]],
|
||||
```rust
|
||||
<Option<i32> as Try>::Output
|
||||
```"#]],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -482,8 +482,18 @@ pub fn parent_module(&self, position: FilePosition) -> Cancellable<Vec<Navigatio
|
||||
}
|
||||
|
||||
/// Returns crates this file belongs too.
|
||||
pub fn crate_for(&self, file_id: FileId) -> Cancellable<Vec<CrateId>> {
|
||||
self.with_db(|db| parent_module::crate_for(db, file_id))
|
||||
pub fn crates_for(&self, file_id: FileId) -> Cancellable<Vec<CrateId>> {
|
||||
self.with_db(|db| parent_module::crates_for(db, file_id))
|
||||
}
|
||||
|
||||
/// Returns crates this file belongs too.
|
||||
pub fn transitive_rev_deps(&self, crate_id: CrateId) -> Cancellable<Vec<CrateId>> {
|
||||
self.with_db(|db| db.crate_graph().transitive_rev_deps(crate_id).collect())
|
||||
}
|
||||
|
||||
/// Returns crates this file *might* belong too.
|
||||
pub fn relevant_crates_for(&self, file_id: FileId) -> Cancellable<Vec<CrateId>> {
|
||||
self.with_db(|db| db.relevant_crates(file_id).iter().copied().collect())
|
||||
}
|
||||
|
||||
/// Returns the edition of the given crate.
|
||||
|
@ -1,6 +1,6 @@
|
||||
use hir::Semantics;
|
||||
use hir::{db::DefDatabase, Semantics};
|
||||
use ide_db::{
|
||||
base_db::{CrateId, FileId, FilePosition},
|
||||
base_db::{CrateId, FileId, FileLoader, FilePosition},
|
||||
RootDatabase,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
@ -55,9 +55,13 @@ pub(crate) fn parent_module(db: &RootDatabase, position: FilePosition) -> Vec<Na
|
||||
}
|
||||
|
||||
/// Returns `Vec` for the same reason as `parent_module`
|
||||
pub(crate) fn crate_for(db: &RootDatabase, file_id: FileId) -> Vec<CrateId> {
|
||||
let sema = Semantics::new(db);
|
||||
sema.to_module_defs(file_id).map(|module| module.krate().into()).unique().collect()
|
||||
pub(crate) fn crates_for(db: &RootDatabase, file_id: FileId) -> Vec<CrateId> {
|
||||
db.relevant_crates(file_id)
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&crate_id| db.crate_def_map(crate_id).modules_for_file(file_id).next().is_some())
|
||||
.sorted()
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -147,7 +151,7 @@ fn test_resolve_crate_root() {
|
||||
mod foo;
|
||||
"#,
|
||||
);
|
||||
assert_eq!(analysis.crate_for(file_id).unwrap().len(), 1);
|
||||
assert_eq!(analysis.crates_for(file_id).unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -162,6 +166,6 @@ fn test_resolve_multi_parent_crate() {
|
||||
mod baz;
|
||||
"#,
|
||||
);
|
||||
assert_eq!(analysis.crate_for(file_id).unwrap().len(), 2);
|
||||
assert_eq!(analysis.crates_for(file_id).unwrap().len(), 2);
|
||||
}
|
||||
}
|
||||
|
@ -210,9 +210,7 @@ fn get_definition(sema: &Semantics<'_, RootDatabase>, token: SyntaxToken) -> Opt
|
||||
let def = IdentClass::classify_token(sema, &token).map(IdentClass::definitions_no_ops);
|
||||
if let Some(&[x]) = def.as_deref() {
|
||||
return Some(x);
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ pub(crate) fn status(db: &RootDatabase, file_id: Option<FileId>) -> String {
|
||||
|
||||
if let Some(file_id) = file_id {
|
||||
format_to!(buf, "\nFile info:\n");
|
||||
let crates = crate::parent_module::crate_for(db, file_id);
|
||||
let crates = crate::parent_module::crates_for(db, file_id);
|
||||
if crates.is_empty() {
|
||||
format_to!(buf, "Does not belong to any crate");
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ semver = "1.0.14"
|
||||
serde = { version = "1.0.137", features = ["derive"] }
|
||||
serde_json = "1.0.86"
|
||||
anyhow = "1.0.62"
|
||||
expect-test = "1.4.0"
|
||||
la-arena = { version = "0.3.0", path = "../../lib/la-arena" }
|
||||
|
||||
cfg = { path = "../cfg", version = "0.0.0" }
|
||||
@ -26,3 +25,6 @@ toolchain = { path = "../toolchain", version = "0.0.0" }
|
||||
paths = { path = "../paths", version = "0.0.0" }
|
||||
stdx = { path = "../stdx", version = "0.0.0" }
|
||||
profile = { path = "../profile", version = "0.0.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
expect-test = "1.4.0"
|
||||
|
@ -6,7 +6,12 @@
|
||||
//! This module implements this second part. We use "build script" terminology
|
||||
//! here, but it covers procedural macros as well.
|
||||
|
||||
use std::{cell::RefCell, io, path::PathBuf, process::Command};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
io, mem,
|
||||
path::{self, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use cargo_metadata::{camino::Utf8Path, Message};
|
||||
use la_arena::ArenaMap;
|
||||
@ -15,11 +20,14 @@
|
||||
use semver::Version;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{cfg_flag::CfgFlag, CargoConfig, CargoFeatures, CargoWorkspace, Package};
|
||||
use crate::{
|
||||
cfg_flag::CfgFlag, CargoConfig, CargoFeatures, CargoWorkspace, InvocationLocation,
|
||||
InvocationStrategy, Package,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct WorkspaceBuildScripts {
|
||||
outputs: ArenaMap<Package, Option<BuildScriptOutput>>,
|
||||
outputs: ArenaMap<Package, BuildScriptOutput>,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
@ -38,76 +46,57 @@ pub(crate) struct BuildScriptOutput {
|
||||
pub(crate) proc_macro_dylib_path: Option<AbsPathBuf>,
|
||||
}
|
||||
|
||||
impl BuildScriptOutput {
|
||||
fn is_unchanged(&self) -> bool {
|
||||
self.cfgs.is_empty()
|
||||
&& self.envs.is_empty()
|
||||
&& self.out_dir.is_none()
|
||||
&& self.proc_macro_dylib_path.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkspaceBuildScripts {
|
||||
fn build_command(config: &CargoConfig) -> Command {
|
||||
if let Some([program, args @ ..]) = config.run_build_script_command.as_deref() {
|
||||
let mut cmd = Command::new(program);
|
||||
cmd.args(args);
|
||||
cmd.envs(&config.extra_env);
|
||||
return cmd;
|
||||
}
|
||||
fn build_command(config: &CargoConfig) -> io::Result<Command> {
|
||||
let mut cmd = match config.run_build_script_command.as_deref() {
|
||||
Some([program, args @ ..]) => {
|
||||
let mut cmd = Command::new(program);
|
||||
cmd.args(args);
|
||||
cmd
|
||||
}
|
||||
_ => {
|
||||
let mut cmd = Command::new(toolchain::cargo());
|
||||
|
||||
cmd.args(&["check", "--quiet", "--workspace", "--message-format=json"]);
|
||||
|
||||
// --all-targets includes tests, benches and examples in addition to the
|
||||
// default lib and bins. This is an independent concept from the --targets
|
||||
// flag below.
|
||||
cmd.arg("--all-targets");
|
||||
|
||||
if let Some(target) = &config.target {
|
||||
cmd.args(&["--target", target]);
|
||||
}
|
||||
|
||||
match &config.features {
|
||||
CargoFeatures::All => {
|
||||
cmd.arg("--all-features");
|
||||
}
|
||||
CargoFeatures::Selected { features, no_default_features } => {
|
||||
if *no_default_features {
|
||||
cmd.arg("--no-default-features");
|
||||
}
|
||||
if !features.is_empty() {
|
||||
cmd.arg("--features");
|
||||
cmd.arg(features.join(" "));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cmd
|
||||
}
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(toolchain::cargo());
|
||||
cmd.envs(&config.extra_env);
|
||||
cmd.args(&["check", "--quiet", "--workspace", "--message-format=json"]);
|
||||
|
||||
// --all-targets includes tests, benches and examples in addition to the
|
||||
// default lib and bins. This is an independent concept from the --targets
|
||||
// flag below.
|
||||
cmd.arg("--all-targets");
|
||||
|
||||
if let Some(target) = &config.target {
|
||||
cmd.args(&["--target", target]);
|
||||
}
|
||||
|
||||
match &config.features {
|
||||
CargoFeatures::All => {
|
||||
cmd.arg("--all-features");
|
||||
}
|
||||
CargoFeatures::Selected { features, no_default_features } => {
|
||||
if *no_default_features {
|
||||
cmd.arg("--no-default-features");
|
||||
}
|
||||
if !features.is_empty() {
|
||||
cmd.arg("--features");
|
||||
cmd.arg(features.join(" "));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cmd
|
||||
}
|
||||
|
||||
pub(crate) fn run(
|
||||
config: &CargoConfig,
|
||||
workspace: &CargoWorkspace,
|
||||
progress: &dyn Fn(String),
|
||||
toolchain: &Option<Version>,
|
||||
) -> io::Result<WorkspaceBuildScripts> {
|
||||
const RUST_1_62: Version = Version::new(1, 62, 0);
|
||||
|
||||
match Self::run_(Self::build_command(config), config, workspace, progress) {
|
||||
Ok(WorkspaceBuildScripts { error: Some(error), .. })
|
||||
if toolchain.as_ref().map_or(false, |it| *it >= RUST_1_62) =>
|
||||
{
|
||||
// building build scripts failed, attempt to build with --keep-going so
|
||||
// that we potentially get more build data
|
||||
let mut cmd = Self::build_command(config);
|
||||
cmd.args(&["-Z", "unstable-options", "--keep-going"]).env("RUSTC_BOOTSTRAP", "1");
|
||||
let mut res = Self::run_(cmd, config, workspace, progress)?;
|
||||
res.error = Some(error);
|
||||
Ok(res)
|
||||
}
|
||||
res => res,
|
||||
}
|
||||
}
|
||||
|
||||
fn run_(
|
||||
mut cmd: Command,
|
||||
config: &CargoConfig,
|
||||
workspace: &CargoWorkspace,
|
||||
progress: &dyn Fn(String),
|
||||
) -> io::Result<WorkspaceBuildScripts> {
|
||||
if config.wrap_rustc_in_build_scripts {
|
||||
// Setup RUSTC_WRAPPER to point to `rust-analyzer` binary itself. We use
|
||||
// that to compile only proc macros and build scripts during the initial
|
||||
@ -117,8 +106,126 @@ fn run_(
|
||||
cmd.env("RA_RUSTC_WRAPPER", "1");
|
||||
}
|
||||
|
||||
cmd.current_dir(workspace.workspace_root());
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Runs the build scripts for the given workspace
|
||||
pub(crate) fn run_for_workspace(
|
||||
config: &CargoConfig,
|
||||
workspace: &CargoWorkspace,
|
||||
progress: &dyn Fn(String),
|
||||
toolchain: &Option<Version>,
|
||||
) -> io::Result<WorkspaceBuildScripts> {
|
||||
const RUST_1_62: Version = Version::new(1, 62, 0);
|
||||
|
||||
let current_dir = match &config.invocation_location {
|
||||
InvocationLocation::Root(root) if config.run_build_script_command.is_some() => {
|
||||
root.as_path()
|
||||
}
|
||||
_ => &workspace.workspace_root(),
|
||||
}
|
||||
.as_ref();
|
||||
|
||||
match Self::run_per_ws(Self::build_command(config)?, workspace, current_dir, progress) {
|
||||
Ok(WorkspaceBuildScripts { error: Some(error), .. })
|
||||
if toolchain.as_ref().map_or(false, |it| *it >= RUST_1_62) =>
|
||||
{
|
||||
// building build scripts failed, attempt to build with --keep-going so
|
||||
// that we potentially get more build data
|
||||
let mut cmd = Self::build_command(config)?;
|
||||
cmd.args(&["-Z", "unstable-options", "--keep-going"]).env("RUSTC_BOOTSTRAP", "1");
|
||||
let mut res = Self::run_per_ws(cmd, workspace, current_dir, progress)?;
|
||||
res.error = Some(error);
|
||||
Ok(res)
|
||||
}
|
||||
res => res,
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the build scripts by invoking the configured command *once*.
|
||||
/// This populates the outputs for all passed in workspaces.
|
||||
pub(crate) fn run_once(
|
||||
config: &CargoConfig,
|
||||
workspaces: &[&CargoWorkspace],
|
||||
progress: &dyn Fn(String),
|
||||
) -> io::Result<Vec<WorkspaceBuildScripts>> {
|
||||
assert_eq!(config.invocation_strategy, InvocationStrategy::Once);
|
||||
|
||||
let current_dir = match &config.invocation_location {
|
||||
InvocationLocation::Root(root) => root,
|
||||
InvocationLocation::Workspace => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Cannot run build scripts from workspace with invocation strategy `once`",
|
||||
))
|
||||
}
|
||||
};
|
||||
let cmd = Self::build_command(config)?;
|
||||
// NB: Cargo.toml could have been modified between `cargo metadata` and
|
||||
// `cargo check`. We shouldn't assume that package ids we see here are
|
||||
// exactly those from `config`.
|
||||
let mut by_id = FxHashMap::default();
|
||||
// some workspaces might depend on the same crates, so we need to duplicate the outputs
|
||||
// to those collisions
|
||||
let mut collisions = Vec::new();
|
||||
let mut res: Vec<_> = workspaces
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, workspace)| {
|
||||
let mut res = WorkspaceBuildScripts::default();
|
||||
for package in workspace.packages() {
|
||||
res.outputs.insert(package, BuildScriptOutput::default());
|
||||
if by_id.contains_key(&workspace[package].id) {
|
||||
collisions.push((&workspace[package].id, idx, package));
|
||||
} else {
|
||||
by_id.insert(workspace[package].id.clone(), (package, idx));
|
||||
}
|
||||
}
|
||||
res
|
||||
})
|
||||
.collect();
|
||||
|
||||
let errors = Self::run_command(
|
||||
cmd,
|
||||
current_dir.as_path().as_ref(),
|
||||
|package, cb| {
|
||||
if let Some(&(package, workspace)) = by_id.get(package) {
|
||||
cb(&workspaces[workspace][package].name, &mut res[workspace].outputs[package]);
|
||||
}
|
||||
},
|
||||
progress,
|
||||
)?;
|
||||
res.iter_mut().for_each(|it| it.error = errors.clone());
|
||||
collisions.into_iter().for_each(|(id, workspace, package)| {
|
||||
if let Some(&(p, w)) = by_id.get(id) {
|
||||
res[workspace].outputs[package] = res[w].outputs[p].clone();
|
||||
}
|
||||
});
|
||||
|
||||
if tracing::enabled!(tracing::Level::INFO) {
|
||||
for (idx, workspace) in workspaces.iter().enumerate() {
|
||||
for package in workspace.packages() {
|
||||
let package_build_data = &mut res[idx].outputs[package];
|
||||
if !package_build_data.is_unchanged() {
|
||||
tracing::info!(
|
||||
"{}: {:?}",
|
||||
workspace[package].manifest.parent().display(),
|
||||
package_build_data,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn run_per_ws(
|
||||
cmd: Command,
|
||||
workspace: &CargoWorkspace,
|
||||
current_dir: &path::Path,
|
||||
progress: &dyn Fn(String),
|
||||
) -> io::Result<WorkspaceBuildScripts> {
|
||||
let mut res = WorkspaceBuildScripts::default();
|
||||
let outputs = &mut res.outputs;
|
||||
// NB: Cargo.toml could have been modified between `cargo metadata` and
|
||||
@ -126,10 +233,46 @@ fn run_(
|
||||
// exactly those from `config`.
|
||||
let mut by_id: FxHashMap<String, Package> = FxHashMap::default();
|
||||
for package in workspace.packages() {
|
||||
outputs.insert(package, None);
|
||||
outputs.insert(package, BuildScriptOutput::default());
|
||||
by_id.insert(workspace[package].id.clone(), package);
|
||||
}
|
||||
|
||||
res.error = Self::run_command(
|
||||
cmd,
|
||||
current_dir,
|
||||
|package, cb| {
|
||||
if let Some(&package) = by_id.get(package) {
|
||||
cb(&workspace[package].name, &mut outputs[package]);
|
||||
}
|
||||
},
|
||||
progress,
|
||||
)?;
|
||||
|
||||
if tracing::enabled!(tracing::Level::INFO) {
|
||||
for package in workspace.packages() {
|
||||
let package_build_data = &mut outputs[package];
|
||||
if !package_build_data.is_unchanged() {
|
||||
tracing::info!(
|
||||
"{}: {:?}",
|
||||
workspace[package].manifest.parent().display(),
|
||||
package_build_data,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn run_command(
|
||||
mut cmd: Command,
|
||||
current_dir: &path::Path,
|
||||
// ideally this would be something like:
|
||||
// with_output_for: impl FnMut(&str, dyn FnOnce(&mut BuildScriptOutput)),
|
||||
// but owned trait objects aren't a thing
|
||||
mut with_output_for: impl FnMut(&str, &mut dyn FnMut(&str, &mut BuildScriptOutput)),
|
||||
progress: &dyn Fn(String),
|
||||
) -> io::Result<Option<String>> {
|
||||
let errors = RefCell::new(String::new());
|
||||
let push_err = |err: &str| {
|
||||
let mut e = errors.borrow_mut();
|
||||
@ -137,7 +280,8 @@ fn run_(
|
||||
e.push('\n');
|
||||
};
|
||||
|
||||
tracing::info!("Running build scripts: {:?}", cmd);
|
||||
tracing::info!("Running build scripts in {}: {:?}", current_dir.display(), cmd);
|
||||
cmd.current_dir(current_dir);
|
||||
let output = stdx::process::spawn_with_streaming_output(
|
||||
cmd,
|
||||
&mut |line| {
|
||||
@ -149,61 +293,58 @@ fn run_(
|
||||
.unwrap_or_else(|_| Message::TextLine(line.to_string()));
|
||||
|
||||
match message {
|
||||
Message::BuildScriptExecuted(message) => {
|
||||
let package = match by_id.get(&message.package_id.repr) {
|
||||
Some(&it) => it,
|
||||
None => return,
|
||||
};
|
||||
progress(format!("running build-script: {}", workspace[package].name));
|
||||
|
||||
let cfgs = {
|
||||
let mut acc = Vec::new();
|
||||
for cfg in message.cfgs {
|
||||
match cfg.parse::<CfgFlag>() {
|
||||
Ok(it) => acc.push(it),
|
||||
Err(err) => {
|
||||
push_err(&format!(
|
||||
"invalid cfg from cargo-metadata: {}",
|
||||
err
|
||||
));
|
||||
return;
|
||||
}
|
||||
};
|
||||
Message::BuildScriptExecuted(mut message) => {
|
||||
with_output_for(&message.package_id.repr, &mut |name, data| {
|
||||
progress(format!("running build-script: {}", name));
|
||||
let cfgs = {
|
||||
let mut acc = Vec::new();
|
||||
for cfg in &message.cfgs {
|
||||
match cfg.parse::<CfgFlag>() {
|
||||
Ok(it) => acc.push(it),
|
||||
Err(err) => {
|
||||
push_err(&format!(
|
||||
"invalid cfg from cargo-metadata: {}",
|
||||
err
|
||||
));
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
acc
|
||||
};
|
||||
if !message.env.is_empty() {
|
||||
data.envs = mem::take(&mut message.env);
|
||||
}
|
||||
acc
|
||||
};
|
||||
// cargo_metadata crate returns default (empty) path for
|
||||
// older cargos, which is not absolute, so work around that.
|
||||
let out_dir = message.out_dir.into_os_string();
|
||||
if !out_dir.is_empty() {
|
||||
let data = outputs[package].get_or_insert_with(Default::default);
|
||||
data.out_dir = Some(AbsPathBuf::assert(PathBuf::from(out_dir)));
|
||||
data.cfgs = cfgs;
|
||||
}
|
||||
if !message.env.is_empty() {
|
||||
outputs[package].get_or_insert_with(Default::default).envs =
|
||||
message.env;
|
||||
}
|
||||
// cargo_metadata crate returns default (empty) path for
|
||||
// older cargos, which is not absolute, so work around that.
|
||||
let out_dir = mem::take(&mut message.out_dir).into_os_string();
|
||||
if !out_dir.is_empty() {
|
||||
let out_dir = AbsPathBuf::assert(PathBuf::from(out_dir));
|
||||
// inject_cargo_env(package, package_build_data);
|
||||
// NOTE: cargo and rustc seem to hide non-UTF-8 strings from env! and option_env!()
|
||||
if let Some(out_dir) =
|
||||
out_dir.as_os_str().to_str().map(|s| s.to_owned())
|
||||
{
|
||||
data.envs.push(("OUT_DIR".to_string(), out_dir));
|
||||
}
|
||||
data.out_dir = Some(out_dir);
|
||||
data.cfgs = cfgs;
|
||||
}
|
||||
});
|
||||
}
|
||||
Message::CompilerArtifact(message) => {
|
||||
let package = match by_id.get(&message.package_id.repr) {
|
||||
Some(it) => *it,
|
||||
None => return,
|
||||
};
|
||||
|
||||
progress(format!("building proc-macros: {}", message.target.name));
|
||||
|
||||
if message.target.kind.iter().any(|k| k == "proc-macro") {
|
||||
// Skip rmeta file
|
||||
if let Some(filename) =
|
||||
message.filenames.iter().find(|name| is_dylib(name))
|
||||
{
|
||||
let filename = AbsPathBuf::assert(PathBuf::from(&filename));
|
||||
outputs[package]
|
||||
.get_or_insert_with(Default::default)
|
||||
.proc_macro_dylib_path = Some(filename);
|
||||
with_output_for(&message.package_id.repr, &mut |name, data| {
|
||||
progress(format!("building proc-macros: {}", name));
|
||||
if message.target.kind.iter().any(|k| k == "proc-macro") {
|
||||
// Skip rmeta file
|
||||
if let Some(filename) =
|
||||
message.filenames.iter().find(|name| is_dylib(name))
|
||||
{
|
||||
let filename = AbsPathBuf::assert(PathBuf::from(&filename));
|
||||
data.proc_macro_dylib_path = Some(filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Message::CompilerMessage(message) => {
|
||||
progress(message.target.name);
|
||||
@ -222,32 +363,13 @@ fn run_(
|
||||
},
|
||||
)?;
|
||||
|
||||
for package in workspace.packages() {
|
||||
if let Some(package_build_data) = &mut outputs[package] {
|
||||
tracing::info!(
|
||||
"{}: {:?}",
|
||||
workspace[package].manifest.parent().display(),
|
||||
package_build_data,
|
||||
);
|
||||
// inject_cargo_env(package, package_build_data);
|
||||
if let Some(out_dir) = &package_build_data.out_dir {
|
||||
// NOTE: cargo and rustc seem to hide non-UTF-8 strings from env! and option_env!()
|
||||
if let Some(out_dir) = out_dir.as_os_str().to_str().map(|s| s.to_owned()) {
|
||||
package_build_data.envs.push(("OUT_DIR".to_string(), out_dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut errors = errors.into_inner();
|
||||
if !output.status.success() {
|
||||
if errors.is_empty() {
|
||||
errors = "cargo check failed".to_string();
|
||||
}
|
||||
res.error = Some(errors);
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
let errors = if !output.status.success() {
|
||||
let errors = errors.into_inner();
|
||||
Some(if errors.is_empty() { "cargo check failed".to_string() } else { errors })
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(errors)
|
||||
}
|
||||
|
||||
pub fn error(&self) -> Option<&str> {
|
||||
@ -255,11 +377,11 @@ pub fn error(&self) -> Option<&str> {
|
||||
}
|
||||
|
||||
pub(crate) fn get_output(&self, idx: Package) -> Option<&BuildScriptOutput> {
|
||||
self.outputs.get(idx)?.as_ref()
|
||||
self.outputs.get(idx)
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: File a better way to know if it is a dylib.
|
||||
// FIXME: Find a better way to know if it is a dylib.
|
||||
fn is_dylib(path: &Utf8Path) -> bool {
|
||||
match path.extension().map(|e| e.to_string().to_lowercase()) {
|
||||
None => false,
|
||||
|
@ -14,8 +14,8 @@
|
||||
use serde::Deserialize;
|
||||
use serde_json::from_value;
|
||||
|
||||
use crate::CfgOverrides;
|
||||
use crate::{utf8_stdout, ManifestPath};
|
||||
use crate::{utf8_stdout, InvocationLocation, ManifestPath};
|
||||
use crate::{CfgOverrides, InvocationStrategy};
|
||||
|
||||
/// [`CargoWorkspace`] represents the logical structure of, well, a Cargo
|
||||
/// workspace. It pretty closely mirrors `cargo metadata` output.
|
||||
@ -106,6 +106,8 @@ pub struct CargoConfig {
|
||||
pub run_build_script_command: Option<Vec<String>>,
|
||||
/// Extra env vars to set when invoking the cargo command
|
||||
pub extra_env: FxHashMap<String, String>,
|
||||
pub invocation_strategy: InvocationStrategy,
|
||||
pub invocation_location: InvocationLocation,
|
||||
}
|
||||
|
||||
impl CargoConfig {
|
||||
@ -283,8 +285,6 @@ pub fn fetch_metadata(
|
||||
}
|
||||
CargoFeatures::Selected { features, no_default_features } => {
|
||||
if *no_default_features {
|
||||
// FIXME: `NoDefaultFeatures` is mutual exclusive with `SomeFeatures`
|
||||
// https://github.com/oli-obk/cargo_metadata/issues/79
|
||||
meta.features(CargoOpt::NoDefaultFeatures);
|
||||
}
|
||||
if !features.is_empty() {
|
||||
@ -329,18 +329,21 @@ pub fn new(mut meta: cargo_metadata::Metadata) -> CargoWorkspace {
|
||||
let ws_members = &meta.workspace_members;
|
||||
|
||||
meta.packages.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
for meta_pkg in &meta.packages {
|
||||
for meta_pkg in meta.packages {
|
||||
let cargo_metadata::Package {
|
||||
id,
|
||||
edition,
|
||||
name,
|
||||
manifest_path,
|
||||
version,
|
||||
metadata,
|
||||
id,
|
||||
source,
|
||||
targets: meta_targets,
|
||||
features,
|
||||
manifest_path,
|
||||
repository,
|
||||
edition,
|
||||
metadata,
|
||||
..
|
||||
} = meta_pkg;
|
||||
let meta = from_value::<PackageMetadata>(metadata.clone()).unwrap_or_default();
|
||||
let meta = from_value::<PackageMetadata>(metadata).unwrap_or_default();
|
||||
let edition = match edition {
|
||||
cargo_metadata::Edition::E2015 => Edition::Edition2015,
|
||||
cargo_metadata::Edition::E2018 => Edition::Edition2018,
|
||||
@ -352,35 +355,36 @@ pub fn new(mut meta: cargo_metadata::Metadata) -> CargoWorkspace {
|
||||
};
|
||||
// We treat packages without source as "local" packages. That includes all members of
|
||||
// the current workspace, as well as any path dependency outside the workspace.
|
||||
let is_local = meta_pkg.source.is_none();
|
||||
let is_member = ws_members.contains(id);
|
||||
let is_local = source.is_none();
|
||||
let is_member = ws_members.contains(&id);
|
||||
|
||||
let pkg = packages.alloc(PackageData {
|
||||
id: id.repr.clone(),
|
||||
name: name.clone(),
|
||||
version: version.clone(),
|
||||
manifest: AbsPathBuf::assert(PathBuf::from(&manifest_path)).try_into().unwrap(),
|
||||
name,
|
||||
version,
|
||||
manifest: AbsPathBuf::assert(manifest_path.into()).try_into().unwrap(),
|
||||
targets: Vec::new(),
|
||||
is_local,
|
||||
is_member,
|
||||
edition,
|
||||
repository: repository.clone(),
|
||||
repository,
|
||||
dependencies: Vec::new(),
|
||||
features: meta_pkg.features.clone().into_iter().collect(),
|
||||
features: features.into_iter().collect(),
|
||||
active_features: Vec::new(),
|
||||
metadata: meta.rust_analyzer.unwrap_or_default(),
|
||||
});
|
||||
let pkg_data = &mut packages[pkg];
|
||||
pkg_by_id.insert(id, pkg);
|
||||
for meta_tgt in &meta_pkg.targets {
|
||||
let is_proc_macro = meta_tgt.kind.as_slice() == ["proc-macro"];
|
||||
for meta_tgt in meta_targets {
|
||||
let cargo_metadata::Target { name, kind, required_features, src_path, .. } =
|
||||
meta_tgt;
|
||||
let tgt = targets.alloc(TargetData {
|
||||
package: pkg,
|
||||
name: meta_tgt.name.clone(),
|
||||
root: AbsPathBuf::assert(PathBuf::from(&meta_tgt.src_path)),
|
||||
kind: TargetKind::new(meta_tgt.kind.as_slice()),
|
||||
is_proc_macro,
|
||||
required_features: meta_tgt.required_features.clone(),
|
||||
name,
|
||||
root: AbsPathBuf::assert(src_path.into()),
|
||||
kind: TargetKind::new(&kind),
|
||||
is_proc_macro: &*kind == ["proc-macro"],
|
||||
required_features,
|
||||
});
|
||||
pkg_data.targets.push(tgt);
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ pub fn from_manifest_file(path: AbsPathBuf) -> Result<ProjectManifest> {
|
||||
if path.file_name().unwrap_or_default() == "Cargo.toml" {
|
||||
return Ok(ProjectManifest::CargoToml(path));
|
||||
}
|
||||
bail!("project root must point to Cargo.toml or rust-project.json: {}", path.display())
|
||||
bail!("project root must point to Cargo.toml or rust-project.json: {}", path.display());
|
||||
}
|
||||
|
||||
pub fn discover_single(path: &AbsPath) -> Result<ProjectManifest> {
|
||||
@ -78,7 +78,7 @@ pub fn discover_single(path: &AbsPath) -> Result<ProjectManifest> {
|
||||
};
|
||||
|
||||
if !candidates.is_empty() {
|
||||
bail!("more than one project")
|
||||
bail!("more than one project");
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
@ -157,3 +157,17 @@ fn utf8_stdout(mut cmd: Command) -> Result<String> {
|
||||
let stdout = String::from_utf8(output.stdout)?;
|
||||
Ok(stdout.trim().to_string())
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum InvocationStrategy {
|
||||
Once,
|
||||
#[default]
|
||||
PerWorkspace,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum InvocationLocation {
|
||||
Root(AbsPathBuf),
|
||||
#[default]
|
||||
Workspace,
|
||||
}
|
||||
|
@ -64,14 +64,15 @@ pub fn proc_macro(&self) -> Option<SysrootCrate> {
|
||||
self.by_name("proc_macro")
|
||||
}
|
||||
|
||||
pub fn crates<'a>(&'a self) -> impl Iterator<Item = SysrootCrate> + ExactSizeIterator + 'a {
|
||||
pub fn crates(&self) -> impl Iterator<Item = SysrootCrate> + ExactSizeIterator + '_ {
|
||||
self.crates.iter().map(|(id, _data)| id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sysroot {
|
||||
/// Attempts to discover the toolchain's sysroot from the given `dir`.
|
||||
pub fn discover(dir: &AbsPath, extra_env: &FxHashMap<String, String>) -> Result<Sysroot> {
|
||||
tracing::debug!("Discovering sysroot for {}", dir.display());
|
||||
tracing::debug!("discovering sysroot for {}", dir.display());
|
||||
let sysroot_dir = discover_sysroot_dir(dir, extra_env)?;
|
||||
let sysroot_src_dir =
|
||||
discover_sysroot_src_dir_or_add_component(&sysroot_dir, dir, extra_env)?;
|
||||
@ -83,11 +84,10 @@ pub fn discover_rustc(
|
||||
cargo_toml: &ManifestPath,
|
||||
extra_env: &FxHashMap<String, String>,
|
||||
) -> Option<ManifestPath> {
|
||||
tracing::debug!("Discovering rustc source for {}", cargo_toml.display());
|
||||
tracing::debug!("discovering rustc source for {}", cargo_toml.display());
|
||||
let current_dir = cargo_toml.parent();
|
||||
discover_sysroot_dir(current_dir, extra_env)
|
||||
.ok()
|
||||
.and_then(|sysroot_dir| get_rustc_src(&sysroot_dir))
|
||||
let sysroot_dir = discover_sysroot_dir(current_dir, extra_env).ok()?;
|
||||
get_rustc_src(&sysroot_dir)
|
||||
}
|
||||
|
||||
pub fn with_sysroot_dir(sysroot_dir: AbsPathBuf) -> Result<Sysroot> {
|
||||
@ -189,6 +189,7 @@ fn discover_sysroot_src_dir(sysroot_path: &AbsPathBuf) -> Option<AbsPathBuf> {
|
||||
|
||||
get_rust_src(sysroot_path)
|
||||
}
|
||||
|
||||
fn discover_sysroot_src_dir_or_add_component(
|
||||
sysroot_path: &AbsPathBuf,
|
||||
current_dir: &AbsPath,
|
||||
@ -199,6 +200,7 @@ fn discover_sysroot_src_dir_or_add_component(
|
||||
let mut rustup = Command::new(toolchain::rustup());
|
||||
rustup.envs(extra_env);
|
||||
rustup.current_dir(current_dir).args(&["component", "add", "rust-src"]);
|
||||
tracing::info!("adding rust-src component by {:?}", rustup);
|
||||
utf8_stdout(rustup).ok()?;
|
||||
get_rust_src(sysroot_path)
|
||||
})
|
||||
@ -217,7 +219,7 @@ fn discover_sysroot_src_dir_or_add_component(
|
||||
fn get_rustc_src(sysroot_path: &AbsPath) -> Option<ManifestPath> {
|
||||
let rustc_src = sysroot_path.join("lib/rustlib/rustc-src/rust/compiler/rustc/Cargo.toml");
|
||||
let rustc_src = ManifestPath::try_from(rustc_src).ok()?;
|
||||
tracing::debug!("Checking for rustc source code: {}", rustc_src.display());
|
||||
tracing::debug!("checking for rustc source code: {}", rustc_src.display());
|
||||
if fs::metadata(&rustc_src).is_ok() {
|
||||
Some(rustc_src)
|
||||
} else {
|
||||
@ -227,7 +229,7 @@ fn get_rustc_src(sysroot_path: &AbsPath) -> Option<ManifestPath> {
|
||||
|
||||
fn get_rust_src(sysroot_path: &AbsPath) -> Option<AbsPathBuf> {
|
||||
let rust_src = sysroot_path.join("lib/rustlib/src/rust/library");
|
||||
tracing::debug!("Checking sysroot: {}", rust_src.display());
|
||||
tracing::debug!("checking sysroot library: {}", rust_src.display());
|
||||
if fs::metadata(&rust_src).is_ok() {
|
||||
Some(rust_src)
|
||||
} else {
|
||||
|
@ -2,7 +2,7 @@
|
||||
//! metadata` or `rust-project.json`) into representation stored in the salsa
|
||||
//! database -- `CrateGraph`.
|
||||
|
||||
use std::{collections::VecDeque, fmt, fs, process::Command};
|
||||
use std::{collections::VecDeque, fmt, fs, process::Command, sync::Arc};
|
||||
|
||||
use anyhow::{format_err, Context, Result};
|
||||
use base_db::{
|
||||
@ -21,8 +21,8 @@
|
||||
cfg_flag::CfgFlag,
|
||||
rustc_cfg,
|
||||
sysroot::SysrootCrate,
|
||||
utf8_stdout, CargoConfig, CargoWorkspace, ManifestPath, Package, ProjectJson, ProjectManifest,
|
||||
Sysroot, TargetKind, WorkspaceBuildScripts,
|
||||
utf8_stdout, CargoConfig, CargoWorkspace, InvocationStrategy, ManifestPath, Package,
|
||||
ProjectJson, ProjectManifest, Sysroot, TargetKind, WorkspaceBuildScripts,
|
||||
};
|
||||
|
||||
/// A set of cfg-overrides per crate.
|
||||
@ -209,6 +209,9 @@ pub fn load(
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
if let Some(sysroot) = &sysroot {
|
||||
tracing::info!(src_root = %sysroot.src_root().display(), root = %sysroot.root().display(), "Using sysroot");
|
||||
}
|
||||
|
||||
let rustc_dir = match &config.rustc_source {
|
||||
Some(RustcSource::Path(path)) => ManifestPath::try_from(path.clone()).ok(),
|
||||
@ -217,6 +220,9 @@ pub fn load(
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
if let Some(rustc_dir) = &rustc_dir {
|
||||
tracing::info!(rustc_dir = %rustc_dir.display(), "Using rustc source");
|
||||
}
|
||||
|
||||
let rustc = match rustc_dir {
|
||||
Some(rustc_dir) => Some({
|
||||
@ -277,6 +283,9 @@ pub fn load_inline(
|
||||
}
|
||||
(None, None) => None,
|
||||
};
|
||||
if let Some(sysroot) = &sysroot {
|
||||
tracing::info!(src_root = %sysroot.src_root().display(), root = %sysroot.root().display(), "Using sysroot");
|
||||
}
|
||||
|
||||
let rustc_cfg = rustc_cfg::get(None, target, extra_env);
|
||||
Ok(ProjectWorkspace::Json { project: project_json, sysroot, rustc_cfg })
|
||||
@ -294,6 +303,7 @@ pub fn load_detached_files(detached_files: Vec<AbsPathBuf>) -> Result<ProjectWor
|
||||
Ok(ProjectWorkspace::DetachedFiles { files: detached_files, sysroot, rustc_cfg })
|
||||
}
|
||||
|
||||
/// Runs the build scripts for this [`ProjectWorkspace`].
|
||||
pub fn run_build_scripts(
|
||||
&self,
|
||||
config: &CargoConfig,
|
||||
@ -301,9 +311,13 @@ pub fn run_build_scripts(
|
||||
) -> Result<WorkspaceBuildScripts> {
|
||||
match self {
|
||||
ProjectWorkspace::Cargo { cargo, toolchain, .. } => {
|
||||
WorkspaceBuildScripts::run(config, cargo, progress, toolchain).with_context(|| {
|
||||
format!("Failed to run build scripts for {}", &cargo.workspace_root().display())
|
||||
})
|
||||
WorkspaceBuildScripts::run_for_workspace(config, cargo, progress, toolchain)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to run build scripts for {}",
|
||||
&cargo.workspace_root().display()
|
||||
)
|
||||
})
|
||||
}
|
||||
ProjectWorkspace::Json { .. } | ProjectWorkspace::DetachedFiles { .. } => {
|
||||
Ok(WorkspaceBuildScripts::default())
|
||||
@ -311,6 +325,49 @@ pub fn run_build_scripts(
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the build scripts for the given [`ProjectWorkspace`]s. Depending on the invocation
|
||||
/// strategy this may run a single build process for all project workspaces.
|
||||
pub fn run_all_build_scripts(
|
||||
workspaces: &[ProjectWorkspace],
|
||||
config: &CargoConfig,
|
||||
progress: &dyn Fn(String),
|
||||
) -> Vec<Result<WorkspaceBuildScripts>> {
|
||||
if matches!(config.invocation_strategy, InvocationStrategy::PerWorkspace)
|
||||
|| config.run_build_script_command.is_none()
|
||||
{
|
||||
return workspaces.iter().map(|it| it.run_build_scripts(config, progress)).collect();
|
||||
}
|
||||
|
||||
let cargo_ws: Vec<_> = workspaces
|
||||
.iter()
|
||||
.filter_map(|it| match it {
|
||||
ProjectWorkspace::Cargo { cargo, .. } => Some(cargo),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let ref mut outputs = match WorkspaceBuildScripts::run_once(config, &cargo_ws, progress) {
|
||||
Ok(it) => Ok(it.into_iter()),
|
||||
// io::Error is not Clone?
|
||||
Err(e) => Err(Arc::new(e)),
|
||||
};
|
||||
|
||||
workspaces
|
||||
.iter()
|
||||
.map(|it| match it {
|
||||
ProjectWorkspace::Cargo { cargo, .. } => match outputs {
|
||||
Ok(outputs) => Ok(outputs.next().unwrap()),
|
||||
Err(e) => Err(e.clone()).with_context(|| {
|
||||
format!(
|
||||
"Failed to run build scripts for {}",
|
||||
&cargo.workspace_root().display()
|
||||
)
|
||||
}),
|
||||
},
|
||||
_ => Ok(WorkspaceBuildScripts::default()),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn set_build_scripts(&mut self, bs: WorkspaceBuildScripts) {
|
||||
match self {
|
||||
ProjectWorkspace::Cargo { build_scripts, .. } => *build_scripts = bs,
|
||||
|
@ -118,7 +118,7 @@ pub(crate) fn for_file(
|
||||
global_state_snapshot: &GlobalStateSnapshot,
|
||||
file_id: FileId,
|
||||
) -> Result<Option<CargoTargetSpec>> {
|
||||
let crate_id = match &*global_state_snapshot.analysis.crate_for(file_id)? {
|
||||
let crate_id = match &*global_state_snapshot.analysis.crates_for(file_id)? {
|
||||
&[crate_id, ..] => crate_id,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
@ -8,8 +8,8 @@
|
||||
use crate::line_index::{LineEndings, LineIndex, OffsetEncoding};
|
||||
use hir::Name;
|
||||
use ide::{
|
||||
LineCol, MonikerDescriptorKind, MonikerResult, StaticIndex, StaticIndexedFile, TextRange,
|
||||
TokenId,
|
||||
LineCol, MonikerDescriptorKind, StaticIndex, StaticIndexedFile, TextRange, TokenId,
|
||||
TokenStaticData,
|
||||
};
|
||||
use ide_db::LineIndexDatabase;
|
||||
use project_model::{CargoConfig, ProjectManifest, ProjectWorkspace};
|
||||
@ -75,7 +75,7 @@ pub fn run(self) -> Result<()> {
|
||||
let mut symbols_emitted: HashSet<TokenId> = HashSet::default();
|
||||
let mut tokens_to_symbol: HashMap<TokenId, String> = HashMap::new();
|
||||
|
||||
for file in si.files {
|
||||
for StaticIndexedFile { file_id, tokens, .. } in si.files {
|
||||
let mut local_count = 0;
|
||||
let mut new_local_symbol = || {
|
||||
let new_symbol = scip::types::Symbol::new_local(local_count);
|
||||
@ -84,7 +84,6 @@ pub fn run(self) -> Result<()> {
|
||||
new_symbol
|
||||
};
|
||||
|
||||
let StaticIndexedFile { file_id, tokens, .. } = file;
|
||||
let relative_path = match get_relative_filepath(&vfs, &rootpath, file_id) {
|
||||
Some(relative_path) => relative_path,
|
||||
None => continue,
|
||||
@ -107,28 +106,20 @@ pub fn run(self) -> Result<()> {
|
||||
|
||||
let mut occurrence = scip_types::Occurrence::default();
|
||||
occurrence.range = text_range_to_scip_range(&line_index, range);
|
||||
occurrence.symbol = match tokens_to_symbol.get(&id) {
|
||||
Some(symbol) => symbol.clone(),
|
||||
None => {
|
||||
let symbol = match &token.moniker {
|
||||
Some(moniker) => moniker_to_symbol(&moniker),
|
||||
None => new_local_symbol(),
|
||||
};
|
||||
|
||||
let symbol = scip::symbol::format_symbol(symbol);
|
||||
tokens_to_symbol.insert(id, symbol.clone());
|
||||
symbol
|
||||
}
|
||||
};
|
||||
occurrence.symbol = tokens_to_symbol
|
||||
.entry(id)
|
||||
.or_insert_with(|| {
|
||||
let symbol = token_to_symbol(&token).unwrap_or_else(&mut new_local_symbol);
|
||||
scip::symbol::format_symbol(symbol)
|
||||
})
|
||||
.clone();
|
||||
|
||||
if let Some(def) = token.definition {
|
||||
if def.range == range {
|
||||
occurrence.symbol_roles |= scip_types::SymbolRole::Definition as i32;
|
||||
}
|
||||
|
||||
if !symbols_emitted.contains(&id) {
|
||||
symbols_emitted.insert(id);
|
||||
|
||||
if symbols_emitted.insert(id) {
|
||||
let mut symbol_info = scip_types::SymbolInformation::default();
|
||||
symbol_info.symbol = occurrence.symbol.clone();
|
||||
if let Some(hover) = &token.hover {
|
||||
@ -207,9 +198,11 @@ fn new_descriptor(name: Name, suffix: scip_types::descriptor::Suffix) -> scip_ty
|
||||
///
|
||||
/// Only returns a Symbol when it's a non-local symbol.
|
||||
/// So if the visibility isn't outside of a document, then it will return None
|
||||
fn moniker_to_symbol(moniker: &MonikerResult) -> scip_types::Symbol {
|
||||
fn token_to_symbol(token: &TokenStaticData) -> Option<scip_types::Symbol> {
|
||||
use scip_types::descriptor::Suffix::*;
|
||||
|
||||
let moniker = token.moniker.as_ref()?;
|
||||
|
||||
let package_name = moniker.package_information.name.clone();
|
||||
let version = moniker.package_information.version.clone();
|
||||
let descriptors = moniker
|
||||
@ -233,7 +226,7 @@ fn moniker_to_symbol(moniker: &MonikerResult) -> scip_types::Symbol {
|
||||
})
|
||||
.collect();
|
||||
|
||||
scip_types::Symbol {
|
||||
Some(scip_types::Symbol {
|
||||
scheme: "rust-analyzer".into(),
|
||||
package: Some(scip_types::Package {
|
||||
manager: "cargo".to_string(),
|
||||
@ -244,19 +237,15 @@ fn moniker_to_symbol(moniker: &MonikerResult) -> scip_types::Symbol {
|
||||
.into(),
|
||||
descriptors,
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use hir::Semantics;
|
||||
use ide::{AnalysisHost, FilePosition};
|
||||
use ide_db::defs::IdentClass;
|
||||
use ide_db::{base_db::fixture::ChangeFixture, helpers::pick_best_token};
|
||||
use ide::{AnalysisHost, FilePosition, StaticIndex, TextSize};
|
||||
use ide_db::base_db::fixture::ChangeFixture;
|
||||
use scip::symbol::format_symbol;
|
||||
use syntax::SyntaxKind::*;
|
||||
use syntax::{AstNode, T};
|
||||
|
||||
fn position(ra_fixture: &str) -> (AnalysisHost, FilePosition) {
|
||||
let mut host = AnalysisHost::default();
|
||||
@ -273,53 +262,33 @@ fn position(ra_fixture: &str) -> (AnalysisHost, FilePosition) {
|
||||
fn check_symbol(ra_fixture: &str, expected: &str) {
|
||||
let (host, position) = position(ra_fixture);
|
||||
|
||||
let analysis = host.analysis();
|
||||
let si = StaticIndex::compute(&analysis);
|
||||
|
||||
let FilePosition { file_id, offset } = position;
|
||||
|
||||
let db = host.raw_database();
|
||||
let sema = &Semantics::new(db);
|
||||
let file = sema.parse(file_id).syntax().clone();
|
||||
let original_token = pick_best_token(file.token_at_offset(offset), |kind| match kind {
|
||||
IDENT
|
||||
| INT_NUMBER
|
||||
| LIFETIME_IDENT
|
||||
| T![self]
|
||||
| T![super]
|
||||
| T![crate]
|
||||
| T![Self]
|
||||
| COMMENT => 2,
|
||||
kind if kind.is_trivia() => 0,
|
||||
_ => 1,
|
||||
})
|
||||
.expect("OK OK");
|
||||
|
||||
let navs = sema
|
||||
.descend_into_macros(original_token.clone())
|
||||
.into_iter()
|
||||
.filter_map(|token| {
|
||||
IdentClass::classify_token(sema, &token).map(IdentClass::definitions).map(|it| {
|
||||
it.into_iter().flat_map(|def| {
|
||||
let module = def.module(db).unwrap();
|
||||
let current_crate = module.krate();
|
||||
|
||||
match MonikerResult::from_def(sema.db, def, current_crate) {
|
||||
Some(moniker_result) => Some(moniker_to_symbol(&moniker_result)),
|
||||
None => None,
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
let mut found_symbol = None;
|
||||
for file in &si.files {
|
||||
if file.file_id != file_id {
|
||||
continue;
|
||||
}
|
||||
for &(range, id) in &file.tokens {
|
||||
if range.contains(offset - TextSize::from(1)) {
|
||||
let token = si.tokens.get(id).unwrap();
|
||||
found_symbol = token_to_symbol(token);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if expected == "" {
|
||||
assert_eq!(0, navs.len(), "must have no symbols {:?}", navs);
|
||||
assert!(found_symbol.is_none(), "must have no symbols {:?}", found_symbol);
|
||||
return;
|
||||
}
|
||||
|
||||
assert_eq!(1, navs.len(), "must have one symbol {:?}", navs);
|
||||
|
||||
let res = navs.get(0).unwrap();
|
||||
let formatted = format_symbol(res.clone());
|
||||
assert!(found_symbol.is_some(), "must have one symbol {:?}", found_symbol);
|
||||
let res = found_symbol.unwrap();
|
||||
let formatted = format_symbol(res);
|
||||
assert_eq!(formatted, expected);
|
||||
}
|
||||
|
||||
|
@ -69,6 +69,19 @@ struct ConfigData {
|
||||
cargo_autoreload: bool = "true",
|
||||
/// Run build scripts (`build.rs`) for more precise code analysis.
|
||||
cargo_buildScripts_enable: bool = "true",
|
||||
/// Specifies the working directory for running build scripts.
|
||||
/// - "workspace": run build scripts for a workspace in the workspace's root directory.
|
||||
/// This is incompatible with `#rust-analyzer.cargo.buildScripts.invocationStrategy#` set to `once`.
|
||||
/// - "root": run build scripts in the project's root directory.
|
||||
/// This config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`
|
||||
/// is set.
|
||||
cargo_buildScripts_invocationLocation: InvocationLocation = "\"workspace\"",
|
||||
/// Specifies the invocation strategy to use when running the build scripts command.
|
||||
/// If `per_workspace` is set, the command will be executed for each workspace.
|
||||
/// If `once` is set, the command will be executed once.
|
||||
/// This config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`
|
||||
/// is set.
|
||||
cargo_buildScripts_invocationStrategy: InvocationStrategy = "\"per_workspace\"",
|
||||
/// Override the command rust-analyzer uses to run build scripts and
|
||||
/// build procedural macros. The command is required to output json
|
||||
/// and should therefore include `--message-format=json` or a similar
|
||||
@ -122,6 +135,20 @@ struct ConfigData {
|
||||
///
|
||||
/// Set to `"all"` to pass `--all-features` to Cargo.
|
||||
checkOnSave_features: Option<CargoFeaturesDef> = "null",
|
||||
/// Specifies the working directory for running checks.
|
||||
/// - "workspace": run checks for workspaces in the corresponding workspaces' root directories.
|
||||
// FIXME: Ideally we would support this in some way
|
||||
/// This falls back to "root" if `#rust-analyzer.cargo.checkOnSave.invocationStrategy#` is set to `once`.
|
||||
/// - "root": run checks in the project's root directory.
|
||||
/// This config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`
|
||||
/// is set.
|
||||
checkOnSave_invocationLocation: InvocationLocation = "\"workspace\"",
|
||||
/// Specifies the invocation strategy to use when running the checkOnSave command.
|
||||
/// If `per_workspace` is set, the command will be executed for each workspace.
|
||||
/// If `once` is set, the command will be executed once.
|
||||
/// This config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`
|
||||
/// is set.
|
||||
checkOnSave_invocationStrategy: InvocationStrategy = "\"per_workspace\"",
|
||||
/// Whether to pass `--no-default-features` to Cargo. Defaults to
|
||||
/// `#rust-analyzer.cargo.noDefaultFeatures#`.
|
||||
checkOnSave_noDefaultFeatures: Option<bool> = "null",
|
||||
@ -1056,6 +1083,16 @@ pub fn cargo(&self) -> CargoConfig {
|
||||
rustc_source,
|
||||
unset_test_crates: UnsetTestCrates::Only(self.data.cargo_unsetTest.clone()),
|
||||
wrap_rustc_in_build_scripts: self.data.cargo_buildScripts_useRustcWrapper,
|
||||
invocation_strategy: match self.data.cargo_buildScripts_invocationStrategy {
|
||||
InvocationStrategy::Once => project_model::InvocationStrategy::Once,
|
||||
InvocationStrategy::PerWorkspace => project_model::InvocationStrategy::PerWorkspace,
|
||||
},
|
||||
invocation_location: match self.data.cargo_buildScripts_invocationLocation {
|
||||
InvocationLocation::Root => {
|
||||
project_model::InvocationLocation::Root(self.root_path.clone())
|
||||
}
|
||||
InvocationLocation::Workspace => project_model::InvocationLocation::Workspace,
|
||||
},
|
||||
run_build_script_command: self.data.cargo_buildScripts_overrideCommand.clone(),
|
||||
extra_env: self.data.cargo_extraEnv.clone(),
|
||||
}
|
||||
@ -1087,6 +1124,18 @@ pub fn flycheck(&self) -> Option<FlycheckConfig> {
|
||||
command,
|
||||
args,
|
||||
extra_env: self.check_on_save_extra_env(),
|
||||
invocation_strategy: match self.data.checkOnSave_invocationStrategy {
|
||||
InvocationStrategy::Once => flycheck::InvocationStrategy::Once,
|
||||
InvocationStrategy::PerWorkspace => {
|
||||
flycheck::InvocationStrategy::PerWorkspace
|
||||
}
|
||||
},
|
||||
invocation_location: match self.data.checkOnSave_invocationLocation {
|
||||
InvocationLocation::Root => {
|
||||
flycheck::InvocationLocation::Root(self.root_path.clone())
|
||||
}
|
||||
InvocationLocation::Workspace => flycheck::InvocationLocation::Workspace,
|
||||
},
|
||||
}
|
||||
}
|
||||
Some(_) | None => FlycheckConfig::CargoCommand {
|
||||
@ -1587,6 +1636,20 @@ enum CargoFeaturesDef {
|
||||
Selected(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum InvocationStrategy {
|
||||
Once,
|
||||
PerWorkspace,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum InvocationLocation {
|
||||
Root,
|
||||
Workspace,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
enum LifetimeElisionDef {
|
||||
@ -2001,6 +2064,22 @@ macro_rules! set {
|
||||
"Render annotations above the whole item, including documentation comments and attributes."
|
||||
],
|
||||
},
|
||||
"InvocationStrategy" => set! {
|
||||
"type": "string",
|
||||
"enum": ["per_workspace", "once"],
|
||||
"enumDescriptions": [
|
||||
"The command will be executed for each workspace.",
|
||||
"The command will be executed once."
|
||||
],
|
||||
},
|
||||
"InvocationLocation" => set! {
|
||||
"type": "string",
|
||||
"enum": ["workspace", "root"],
|
||||
"enumDescriptions": [
|
||||
"The command will be executed in the corresponding workspace root.",
|
||||
"The command will be executed in the project root."
|
||||
],
|
||||
},
|
||||
_ => panic!("missing entry for {}: {}", ty, default),
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ pub(crate) fn on_sync_mut<R>(
|
||||
let _pctx = stdx::panic_context::enter(panic_context);
|
||||
f(self.global_state, params)
|
||||
};
|
||||
if let Ok(response) = result_to_response::<R>(req.id.clone(), result) {
|
||||
if let Ok(response) = result_to_response::<R>(req.id, result) {
|
||||
self.global_state.respond(response);
|
||||
}
|
||||
|
||||
@ -80,7 +80,7 @@ pub(crate) fn on_sync<R>(
|
||||
f(global_state_snapshot, params)
|
||||
});
|
||||
|
||||
if let Ok(response) = thread_result_to_response::<R>(req.id.clone(), result) {
|
||||
if let Ok(response) = thread_result_to_response::<R>(req.id, result) {
|
||||
self.global_state.respond(response);
|
||||
}
|
||||
|
||||
|
@ -64,7 +64,7 @@ pub(crate) struct GlobalState {
|
||||
pub(crate) source_root_config: SourceRootConfig,
|
||||
pub(crate) proc_macro_clients: Vec<Result<ProcMacroServer, String>>,
|
||||
|
||||
pub(crate) flycheck: Vec<FlycheckHandle>,
|
||||
pub(crate) flycheck: Arc<[FlycheckHandle]>,
|
||||
pub(crate) flycheck_sender: Sender<flycheck::Message>,
|
||||
pub(crate) flycheck_receiver: Receiver<flycheck::Message>,
|
||||
|
||||
@ -117,6 +117,7 @@ pub(crate) struct GlobalStateSnapshot {
|
||||
vfs: Arc<RwLock<(vfs::Vfs, NoHashHashMap<FileId, LineEndings>)>>,
|
||||
pub(crate) workspaces: Arc<Vec<ProjectWorkspace>>,
|
||||
pub(crate) proc_macros_loaded: bool,
|
||||
pub(crate) flycheck: Arc<[FlycheckHandle]>,
|
||||
}
|
||||
|
||||
impl std::panic::UnwindSafe for GlobalStateSnapshot {}
|
||||
@ -155,7 +156,7 @@ pub(crate) fn new(sender: Sender<lsp_server::Message>, config: Config) -> Global
|
||||
source_root_config: SourceRootConfig::default(),
|
||||
proc_macro_clients: vec![],
|
||||
|
||||
flycheck: Vec::new(),
|
||||
flycheck: Arc::new([]),
|
||||
flycheck_sender,
|
||||
flycheck_receiver,
|
||||
|
||||
@ -295,6 +296,7 @@ pub(crate) fn snapshot(&self) -> GlobalStateSnapshot {
|
||||
mem_docs: self.mem_docs.clone(),
|
||||
semantic_tokens_cache: Arc::clone(&self.semantic_tokens_cache),
|
||||
proc_macros_loaded: !self.fetch_build_data_queue.last_op_result().0.is_empty(),
|
||||
flycheck: self.flycheck.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -398,6 +400,10 @@ pub(crate) fn anchored_path(&self, path: &AnchoredPathBuf) -> Url {
|
||||
url_from_abs_path(path)
|
||||
}
|
||||
|
||||
pub(crate) fn file_id_to_file_path(&self, file_id: FileId) -> vfs::VfsPath {
|
||||
self.vfs.read().0.file_path(file_id)
|
||||
}
|
||||
|
||||
pub(crate) fn cargo_target_for_crate_root(
|
||||
&self,
|
||||
crate_id: CrateId,
|
||||
|
@ -658,7 +658,7 @@ pub(crate) fn handle_parent_module(
|
||||
|
||||
// check if invoked at the crate root
|
||||
let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?;
|
||||
let crate_id = match snap.analysis.crate_for(file_id)?.first() {
|
||||
let crate_id = match snap.analysis.crates_for(file_id)?.first() {
|
||||
Some(&crate_id) => crate_id,
|
||||
None => return Ok(None),
|
||||
};
|
||||
@ -1782,7 +1782,15 @@ fn run_rustfmt(
|
||||
) -> Result<Option<Vec<lsp_types::TextEdit>>> {
|
||||
let file_id = from_proto::file_id(snap, &text_document.uri)?;
|
||||
let file = snap.analysis.file_text(file_id)?;
|
||||
let crate_ids = snap.analysis.crate_for(file_id)?;
|
||||
|
||||
// find the edition of the package the file belongs to
|
||||
// (if it belongs to multiple we'll just pick the first one and pray)
|
||||
let edition = snap
|
||||
.analysis
|
||||
.relevant_crates_for(file_id)?
|
||||
.into_iter()
|
||||
.find_map(|crate_id| snap.cargo_target_for_crate_root(crate_id))
|
||||
.map(|(ws, target)| ws[ws[target].package].edition);
|
||||
|
||||
let line_index = snap.file_line_index(file_id)?;
|
||||
|
||||
@ -1808,9 +1816,7 @@ fn run_rustfmt(
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(&crate_id) = crate_ids.first() {
|
||||
// Assume all crates are in the same edition
|
||||
let edition = snap.analysis.crate_edition(crate_id)?;
|
||||
if let Some(edition) = edition {
|
||||
cmd.arg("--edition");
|
||||
cmd.arg(edition.to_string());
|
||||
}
|
||||
|
@ -87,6 +87,7 @@ pub(crate) fn report_progress(
|
||||
state: Progress,
|
||||
message: Option<String>,
|
||||
fraction: Option<f64>,
|
||||
cancel_token: Option<String>,
|
||||
) {
|
||||
if !self.config.work_done_progress() {
|
||||
return;
|
||||
@ -95,7 +96,10 @@ pub(crate) fn report_progress(
|
||||
assert!((0.0..=1.0).contains(&f));
|
||||
(f * 100.0) as u32
|
||||
});
|
||||
let token = lsp_types::ProgressToken::String(format!("rustAnalyzer/{}", title));
|
||||
let cancellable = Some(cancel_token.is_some());
|
||||
let token = lsp_types::ProgressToken::String(
|
||||
cancel_token.unwrap_or_else(|| format!("rustAnalyzer/{}", title)),
|
||||
);
|
||||
let work_done_progress = match state {
|
||||
Progress::Begin => {
|
||||
self.send_request::<lsp_types::request::WorkDoneProgressCreate>(
|
||||
@ -105,14 +109,14 @@ pub(crate) fn report_progress(
|
||||
|
||||
lsp_types::WorkDoneProgress::Begin(lsp_types::WorkDoneProgressBegin {
|
||||
title: title.into(),
|
||||
cancellable: None,
|
||||
cancellable,
|
||||
message,
|
||||
percentage,
|
||||
})
|
||||
}
|
||||
Progress::Report => {
|
||||
lsp_types::WorkDoneProgress::Report(lsp_types::WorkDoneProgressReport {
|
||||
cancellable: None,
|
||||
cancellable,
|
||||
message,
|
||||
percentage,
|
||||
})
|
||||
|
@ -10,7 +10,7 @@
|
||||
use always_assert::always;
|
||||
use crossbeam_channel::{select, Receiver};
|
||||
use flycheck::FlycheckHandle;
|
||||
use ide_db::base_db::{SourceDatabase, SourceDatabaseExt, VfsPath};
|
||||
use ide_db::base_db::{SourceDatabaseExt, VfsPath};
|
||||
use itertools::Itertools;
|
||||
use lsp_server::{Connection, Notification, Request};
|
||||
use lsp_types::notification::Notification as _;
|
||||
@ -191,7 +191,7 @@ fn handle_event(&mut self, event: Event) -> Result<()> {
|
||||
// NOTE: don't count blocking select! call as a loop-turn time
|
||||
let _p = profile::span("GlobalState::handle_event");
|
||||
|
||||
tracing::debug!("handle_event({:?})", event);
|
||||
tracing::debug!("{:?} handle_event({:?})", loop_start, event);
|
||||
let task_queue_len = self.task_pool.handle.len();
|
||||
if task_queue_len > 0 {
|
||||
tracing::info!("task queue len: {}", task_queue_len);
|
||||
@ -257,7 +257,7 @@ fn handle_event(&mut self, event: Event) -> Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
self.report_progress("Indexing", state, message, Some(fraction));
|
||||
self.report_progress("Indexing", state, message, Some(fraction), None);
|
||||
}
|
||||
}
|
||||
Event::Vfs(message) => {
|
||||
@ -465,7 +465,7 @@ fn handle_task(&mut self, prime_caches_progress: &mut Vec<PrimeCachesProgress>,
|
||||
}
|
||||
};
|
||||
|
||||
self.report_progress("Fetching", state, msg, None);
|
||||
self.report_progress("Fetching", state, msg, None, None);
|
||||
}
|
||||
Task::FetchBuildData(progress) => {
|
||||
let (state, msg) = match progress {
|
||||
@ -481,7 +481,7 @@ fn handle_task(&mut self, prime_caches_progress: &mut Vec<PrimeCachesProgress>,
|
||||
};
|
||||
|
||||
if let Some(state) = state {
|
||||
self.report_progress("Loading", state, msg, None);
|
||||
self.report_progress("Loading", state, msg, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -518,6 +518,7 @@ fn handle_vfs_msg(&mut self, message: vfs::loader::Message) {
|
||||
state,
|
||||
Some(format!("{}/{}", n_done, n_total)),
|
||||
Some(Progress::fraction(n_done, n_total)),
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -542,7 +543,10 @@ fn handle_flycheck_msg(&mut self, message: flycheck::Message) {
|
||||
diag.fix,
|
||||
),
|
||||
Err(err) => {
|
||||
tracing::error!("File with cargo diagnostic not found in VFS: {}", err);
|
||||
tracing::error!(
|
||||
"flycheck {id}: File with cargo diagnostic not found in VFS: {}",
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -584,7 +588,13 @@ fn handle_flycheck_msg(&mut self, message: flycheck::Message) {
|
||||
} else {
|
||||
format!("cargo check (#{})", id + 1)
|
||||
};
|
||||
self.report_progress(&title, state, message, None);
|
||||
self.report_progress(
|
||||
&title,
|
||||
state,
|
||||
message,
|
||||
None,
|
||||
Some(format!("rust-analyzer/checkOnSave/{}", id)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -698,7 +708,16 @@ fn on_notification(&mut self, not: Notification) -> Result<()> {
|
||||
this.cancel(id);
|
||||
Ok(())
|
||||
})?
|
||||
.on::<lsp_types::notification::WorkDoneProgressCancel>(|_this, _params| {
|
||||
.on::<lsp_types::notification::WorkDoneProgressCancel>(|this, params| {
|
||||
if let lsp_types::NumberOrString::String(s) = ¶ms.token {
|
||||
if let Some(id) = s.strip_prefix("rust-analyzer/checkOnSave/") {
|
||||
if let Ok(id) = u32::from_str_radix(id, 10) {
|
||||
if let Some(flycheck) = this.flycheck.get(id as usize) {
|
||||
flycheck.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Just ignore this. It is OK to continue sending progress
|
||||
// notifications for this token, as the client can't know when
|
||||
// we accepted notification.
|
||||
@ -711,7 +730,7 @@ fn on_notification(&mut self, not: Notification) -> Result<()> {
|
||||
.insert(path.clone(), DocumentData::new(params.text_document.version))
|
||||
.is_err();
|
||||
if already_exists {
|
||||
tracing::error!("duplicate DidOpenTextDocument: {}", path)
|
||||
tracing::error!("duplicate DidOpenTextDocument: {}", path);
|
||||
}
|
||||
this.vfs
|
||||
.write()
|
||||
@ -758,69 +777,7 @@ fn on_notification(&mut self, not: Notification) -> Result<()> {
|
||||
Ok(())
|
||||
})?
|
||||
.on::<lsp_types::notification::DidSaveTextDocument>(|this, params| {
|
||||
let mut updated = false;
|
||||
if let Ok(vfs_path) = from_proto::vfs_path(¶ms.text_document.uri) {
|
||||
let (vfs, _) = &*this.vfs.read();
|
||||
|
||||
// Trigger flychecks for all workspaces that depend on the saved file
|
||||
if let Some(file_id) = vfs.file_id(&vfs_path) {
|
||||
let analysis = this.analysis_host.analysis();
|
||||
// Crates containing or depending on the saved file
|
||||
let crate_ids: Vec<_> = analysis
|
||||
.crate_for(file_id)?
|
||||
.into_iter()
|
||||
.flat_map(|id| {
|
||||
this.analysis_host
|
||||
.raw_database()
|
||||
.crate_graph()
|
||||
.transitive_rev_deps(id)
|
||||
})
|
||||
.sorted()
|
||||
.unique()
|
||||
.collect();
|
||||
|
||||
let crate_root_paths: Vec<_> = crate_ids
|
||||
.iter()
|
||||
.filter_map(|&crate_id| {
|
||||
analysis
|
||||
.crate_root(crate_id)
|
||||
.map(|file_id| {
|
||||
vfs.file_path(file_id).as_path().map(ToOwned::to_owned)
|
||||
})
|
||||
.transpose()
|
||||
})
|
||||
.collect::<ide::Cancellable<_>>()?;
|
||||
let crate_root_paths: Vec<_> =
|
||||
crate_root_paths.iter().map(Deref::deref).collect();
|
||||
|
||||
// Find all workspaces that have at least one target containing the saved file
|
||||
let workspace_ids =
|
||||
this.workspaces.iter().enumerate().filter(|(_, ws)| match ws {
|
||||
project_model::ProjectWorkspace::Cargo { cargo, .. } => {
|
||||
cargo.packages().any(|pkg| {
|
||||
cargo[pkg].targets.iter().any(|&it| {
|
||||
crate_root_paths.contains(&cargo[it].root.as_path())
|
||||
})
|
||||
})
|
||||
}
|
||||
project_model::ProjectWorkspace::Json { project, .. } => project
|
||||
.crates()
|
||||
.any(|(c, _)| crate_ids.iter().any(|&crate_id| crate_id == c)),
|
||||
project_model::ProjectWorkspace::DetachedFiles { .. } => false,
|
||||
});
|
||||
|
||||
// Find and trigger corresponding flychecks
|
||||
for flycheck in &this.flycheck {
|
||||
for (id, _) in workspace_ids.clone() {
|
||||
if id == flycheck.id() {
|
||||
updated = true;
|
||||
flycheck.restart();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-fetch workspaces if a workspace related file has changed
|
||||
if let Some(abs_path) = vfs_path.as_path() {
|
||||
if reload::should_refresh_for_change(&abs_path, ChangeKind::Modify) {
|
||||
@ -828,13 +785,90 @@ fn on_notification(&mut self, not: Notification) -> Result<()> {
|
||||
.request_op(format!("DidSaveTextDocument {}", abs_path.display()));
|
||||
}
|
||||
}
|
||||
|
||||
let file_id = this.vfs.read().0.file_id(&vfs_path);
|
||||
if let Some(file_id) = file_id {
|
||||
let world = this.snapshot();
|
||||
let mut updated = false;
|
||||
let task = move || -> std::result::Result<(), ide::Cancelled> {
|
||||
// Trigger flychecks for all workspaces that depend on the saved file
|
||||
// Crates containing or depending on the saved file
|
||||
let crate_ids: Vec<_> = world
|
||||
.analysis
|
||||
.crates_for(file_id)?
|
||||
.into_iter()
|
||||
.flat_map(|id| world.analysis.transitive_rev_deps(id))
|
||||
.flatten()
|
||||
.sorted()
|
||||
.unique()
|
||||
.collect();
|
||||
|
||||
let crate_root_paths: Vec<_> = crate_ids
|
||||
.iter()
|
||||
.filter_map(|&crate_id| {
|
||||
world
|
||||
.analysis
|
||||
.crate_root(crate_id)
|
||||
.map(|file_id| {
|
||||
world
|
||||
.file_id_to_file_path(file_id)
|
||||
.as_path()
|
||||
.map(ToOwned::to_owned)
|
||||
})
|
||||
.transpose()
|
||||
})
|
||||
.collect::<ide::Cancellable<_>>()?;
|
||||
let crate_root_paths: Vec<_> =
|
||||
crate_root_paths.iter().map(Deref::deref).collect();
|
||||
|
||||
// Find all workspaces that have at least one target containing the saved file
|
||||
let workspace_ids =
|
||||
world.workspaces.iter().enumerate().filter(|(_, ws)| match ws {
|
||||
project_model::ProjectWorkspace::Cargo { cargo, .. } => {
|
||||
cargo.packages().any(|pkg| {
|
||||
cargo[pkg].targets.iter().any(|&it| {
|
||||
crate_root_paths.contains(&cargo[it].root.as_path())
|
||||
})
|
||||
})
|
||||
}
|
||||
project_model::ProjectWorkspace::Json { project, .. } => {
|
||||
project.crates().any(|(c, _)| {
|
||||
crate_ids.iter().any(|&crate_id| crate_id == c)
|
||||
})
|
||||
}
|
||||
project_model::ProjectWorkspace::DetachedFiles { .. } => false,
|
||||
});
|
||||
|
||||
// Find and trigger corresponding flychecks
|
||||
for flycheck in world.flycheck.iter() {
|
||||
for (id, _) in workspace_ids.clone() {
|
||||
if id == flycheck.id() {
|
||||
updated = true;
|
||||
flycheck.restart();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// No specific flycheck was triggered, so let's trigger all of them.
|
||||
if !updated {
|
||||
for flycheck in world.flycheck.iter() {
|
||||
flycheck.restart();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
this.task_pool.handle.spawn_with_sender(move |_| {
|
||||
if let Err(e) = std::panic::catch_unwind(task) {
|
||||
tracing::error!("DidSaveTextDocument flycheck task panicked: {e:?}")
|
||||
}
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// No specific flycheck was triggered, so let's trigger all of them.
|
||||
if !updated {
|
||||
for flycheck in &this.flycheck {
|
||||
flycheck.restart();
|
||||
}
|
||||
for flycheck in this.flycheck.iter() {
|
||||
flycheck.restart();
|
||||
}
|
||||
Ok(())
|
||||
})?
|
||||
|
@ -175,10 +175,8 @@ pub(crate) fn fetch_build_data(&mut self, cause: Cause) {
|
||||
sender.send(Task::FetchBuildData(BuildDataProgress::Report(msg))).unwrap()
|
||||
}
|
||||
};
|
||||
let mut res = Vec::new();
|
||||
for ws in workspaces.iter() {
|
||||
res.push(ws.run_build_scripts(&config, &progress));
|
||||
}
|
||||
let res = ProjectWorkspace::run_all_build_scripts(&workspaces, &config, &progress);
|
||||
|
||||
sender.send(Task::FetchBuildData(BuildDataProgress::End((workspaces, res)))).unwrap();
|
||||
});
|
||||
}
|
||||
@ -468,39 +466,54 @@ fn reload_flycheck(&mut self) {
|
||||
let config = match self.config.flycheck() {
|
||||
Some(it) => it,
|
||||
None => {
|
||||
self.flycheck = Vec::new();
|
||||
self.flycheck = Arc::new([]);
|
||||
self.diagnostics.clear_check_all();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let sender = self.flycheck_sender.clone();
|
||||
self.flycheck = self
|
||||
.workspaces
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(id, w)| match w {
|
||||
ProjectWorkspace::Cargo { cargo, .. } => Some((id, cargo.workspace_root())),
|
||||
ProjectWorkspace::Json { project, .. } => {
|
||||
// Enable flychecks for json projects if a custom flycheck command was supplied
|
||||
// in the workspace configuration.
|
||||
match config {
|
||||
FlycheckConfig::CustomCommand { .. } => Some((id, project.path())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
ProjectWorkspace::DetachedFiles { .. } => None,
|
||||
})
|
||||
.map(|(id, root)| {
|
||||
let sender = sender.clone();
|
||||
FlycheckHandle::spawn(
|
||||
id,
|
||||
Box::new(move |msg| sender.send(msg).unwrap()),
|
||||
config.clone(),
|
||||
root.to_path_buf(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let invocation_strategy = match config {
|
||||
FlycheckConfig::CargoCommand { .. } => flycheck::InvocationStrategy::PerWorkspace,
|
||||
FlycheckConfig::CustomCommand { invocation_strategy, .. } => invocation_strategy,
|
||||
};
|
||||
|
||||
self.flycheck = match invocation_strategy {
|
||||
flycheck::InvocationStrategy::Once => vec![FlycheckHandle::spawn(
|
||||
0,
|
||||
Box::new(move |msg| sender.send(msg).unwrap()),
|
||||
config.clone(),
|
||||
self.config.root_path().clone(),
|
||||
)],
|
||||
flycheck::InvocationStrategy::PerWorkspace => {
|
||||
self.workspaces
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(id, w)| match w {
|
||||
ProjectWorkspace::Cargo { cargo, .. } => Some((id, cargo.workspace_root())),
|
||||
ProjectWorkspace::Json { project, .. } => {
|
||||
// Enable flychecks for json projects if a custom flycheck command was supplied
|
||||
// in the workspace configuration.
|
||||
match config {
|
||||
FlycheckConfig::CustomCommand { .. } => Some((id, project.path())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
ProjectWorkspace::DetachedFiles { .. } => None,
|
||||
})
|
||||
.map(|(id, root)| {
|
||||
let sender = sender.clone();
|
||||
FlycheckHandle::spawn(
|
||||
id,
|
||||
Box::new(move |msg| sender.send(msg).unwrap()),
|
||||
config.clone(),
|
||||
root.to_path_buf(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
.into();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -645,7 +645,7 @@ pub fn add_field(&self, field: ast::RecordPatField) {
|
||||
}
|
||||
|
||||
fn get_or_insert_comma_after(syntax: &SyntaxNode) -> SyntaxToken {
|
||||
let comma = match syntax
|
||||
match syntax
|
||||
.siblings_with_tokens(Direction::Next)
|
||||
.filter_map(|it| it.into_token())
|
||||
.find(|it| it.kind() == T![,])
|
||||
@ -656,8 +656,7 @@ fn get_or_insert_comma_after(syntax: &SyntaxNode) -> SyntaxToken {
|
||||
ted::insert(Position::after(syntax), &comma);
|
||||
comma
|
||||
}
|
||||
};
|
||||
comma
|
||||
}
|
||||
}
|
||||
|
||||
impl ast::StmtList {
|
||||
|
@ -92,7 +92,7 @@ pub fn syntax_node(&self) -> SyntaxNode {
|
||||
SyntaxNode::new_root(self.green.clone())
|
||||
}
|
||||
pub fn errors(&self) -> &[SyntaxError] {
|
||||
&*self.errors
|
||||
&self.errors
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,6 @@
|
||||
//! generator: pin
|
||||
//! hash:
|
||||
//! index: sized
|
||||
//! infallible:
|
||||
//! iterator: option
|
||||
//! iterators: iterator, fn
|
||||
//! option:
|
||||
@ -37,7 +36,7 @@
|
||||
//! result:
|
||||
//! sized:
|
||||
//! slice:
|
||||
//! try: infallible
|
||||
//! try:
|
||||
//! unsize: sized
|
||||
|
||||
pub mod marker {
|
||||
@ -151,9 +150,6 @@ pub trait AsRef<T: ?Sized> {
|
||||
fn as_ref(&self) -> &T;
|
||||
}
|
||||
// endregion:as_ref
|
||||
// region:infallible
|
||||
pub enum Infallible {}
|
||||
// endregion:infallible
|
||||
}
|
||||
|
||||
pub mod ops {
|
||||
@ -330,7 +326,7 @@ pub enum ControlFlow<B, C = ()> {
|
||||
Continue(C),
|
||||
Break(B),
|
||||
}
|
||||
pub trait FromResidual<R = <Self as Try>::Residual> {
|
||||
pub trait FromResidual<R = Self::Residual> {
|
||||
#[lang = "from_residual"]
|
||||
fn from_residual(residual: R) -> Self;
|
||||
}
|
||||
@ -346,13 +342,13 @@ pub trait Try: FromResidual<Self::Residual> {
|
||||
|
||||
impl<B, C> Try for ControlFlow<B, C> {
|
||||
type Output = C;
|
||||
type Residual = ControlFlow<B, crate::convert::Infallible>;
|
||||
type Residual = ControlFlow<B, convert::Infallible>;
|
||||
fn from_output(output: Self::Output) -> Self {}
|
||||
fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {}
|
||||
}
|
||||
|
||||
impl<B, C> FromResidual for ControlFlow<B, C> {
|
||||
fn from_residual(residual: ControlFlow<B, crate::convert::Infallible>) -> Self {}
|
||||
fn from_residual(residual: ControlFlow<B, convert::Infallible>) -> Self {}
|
||||
}
|
||||
}
|
||||
pub use self::try_::{ControlFlow, FromResidual, Try};
|
||||
@ -473,33 +469,6 @@ pub const fn unwrap(self) -> T {
|
||||
}
|
||||
}
|
||||
}
|
||||
// region:try
|
||||
impl<T> crate::ops::Try for Option<T> {
|
||||
type Output = T;
|
||||
type Residual = Option<crate::convert::Infallible>;
|
||||
|
||||
#[inline]
|
||||
fn from_output(output: Self::Output) -> Self {
|
||||
Some(output)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn branch(self) -> crate::ops::ControlFlow<Self::Residual, Self::Output> {
|
||||
match self {
|
||||
Some(v) => crate::ops::ControlFlow::Continue(v),
|
||||
None => crate::ops::ControlFlow::Break(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> crate::ops::FromResidual for Option<T> {
|
||||
#[inline]
|
||||
fn from_residual(residual: Option<crate::convert::Infallible>) -> Self {
|
||||
match residual {
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion:try
|
||||
}
|
||||
// endregion:option
|
||||
|
||||
|
@ -24,6 +24,25 @@ Automatically refresh project info via `cargo metadata` on
|
||||
--
|
||||
Run build scripts (`build.rs`) for more precise code analysis.
|
||||
--
|
||||
[[rust-analyzer.cargo.buildScripts.invocationLocation]]rust-analyzer.cargo.buildScripts.invocationLocation (default: `"workspace"`)::
|
||||
+
|
||||
--
|
||||
Specifies the working directory for running build scripts.
|
||||
- "workspace": run build scripts for a workspace in the workspace's root directory.
|
||||
This is incompatible with `#rust-analyzer.cargo.buildScripts.invocationStrategy#` set to `once`.
|
||||
- "root": run build scripts in the project's root directory.
|
||||
This config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`
|
||||
is set.
|
||||
--
|
||||
[[rust-analyzer.cargo.buildScripts.invocationStrategy]]rust-analyzer.cargo.buildScripts.invocationStrategy (default: `"per_workspace"`)::
|
||||
+
|
||||
--
|
||||
Specifies the invocation strategy to use when running the build scripts command.
|
||||
If `per_workspace` is set, the command will be executed for each workspace.
|
||||
If `once` is set, the command will be executed once.
|
||||
This config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`
|
||||
is set.
|
||||
--
|
||||
[[rust-analyzer.cargo.buildScripts.overrideCommand]]rust-analyzer.cargo.buildScripts.overrideCommand (default: `null`)::
|
||||
+
|
||||
--
|
||||
@ -118,6 +137,25 @@ List of features to activate. Defaults to
|
||||
|
||||
Set to `"all"` to pass `--all-features` to Cargo.
|
||||
--
|
||||
[[rust-analyzer.checkOnSave.invocationLocation]]rust-analyzer.checkOnSave.invocationLocation (default: `"workspace"`)::
|
||||
+
|
||||
--
|
||||
Specifies the working directory for running checks.
|
||||
- "workspace": run checks for workspaces in the corresponding workspaces' root directories.
|
||||
This falls back to "root" if `#rust-analyzer.cargo.checkOnSave.invocationStrategy#` is set to `once`.
|
||||
- "root": run checks in the project's root directory.
|
||||
This config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`
|
||||
is set.
|
||||
--
|
||||
[[rust-analyzer.checkOnSave.invocationStrategy]]rust-analyzer.checkOnSave.invocationStrategy (default: `"per_workspace"`)::
|
||||
+
|
||||
--
|
||||
Specifies the invocation strategy to use when running the checkOnSave command.
|
||||
If `per_workspace` is set, the command will be executed for each workspace.
|
||||
If `once` is set, the command will be executed once.
|
||||
This config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`
|
||||
is set.
|
||||
--
|
||||
[[rust-analyzer.checkOnSave.noDefaultFeatures]]rust-analyzer.checkOnSave.noDefaultFeatures (default: `null`)::
|
||||
+
|
||||
--
|
||||
|
@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"d3": "^7.6.1",
|
||||
"d3-graphviz": "^4.1.1",
|
||||
"vscode-languageclient": "^8.0.0-next.14"
|
||||
"vscode-languageclient": "^8.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "~16.11.7",
|
||||
@ -3791,39 +3791,39 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-jsonrpc": {
|
||||
"version": "8.0.0-next.7",
|
||||
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.0-next.7.tgz",
|
||||
"integrity": "sha512-JX/F31LEsims0dAlOTKFE4E+AJMiJvdRSRViifFJSqSN7EzeYyWlfuDchF7g91oRNPZOIWfibTkDf3/UMsQGzQ==",
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2.tgz",
|
||||
"integrity": "sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ==",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-languageclient": {
|
||||
"version": "8.0.0-next.14",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.0-next.14.tgz",
|
||||
"integrity": "sha512-NqjkOuDTMu8uo+PhoMsV72VO9Gd3wBi/ZpOrkRUOrWKQo7yUdiIw183g8wjH8BImgbK9ZP51HM7TI0ZhCnI1Mw==",
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.2.tgz",
|
||||
"integrity": "sha512-lHlthJtphG9gibGb/y72CKqQUxwPsMXijJVpHEC2bvbFqxmkj9LwQ3aGU9dwjBLqsX1S4KjShYppLvg1UJDF/Q==",
|
||||
"dependencies": {
|
||||
"minimatch": "^3.0.4",
|
||||
"semver": "^7.3.5",
|
||||
"vscode-languageserver-protocol": "3.17.0-next.16"
|
||||
"vscode-languageserver-protocol": "3.17.2"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.66.0"
|
||||
"vscode": "^1.67.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-languageserver-protocol": {
|
||||
"version": "3.17.0-next.16",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.0-next.16.tgz",
|
||||
"integrity": "sha512-tx4DnXw9u3N7vw+bx6n2NKp6FoxoNwiP/biH83AS30I2AnTGyLd7afSeH6Oewn2E8jvB7K15bs12sMppkKOVeQ==",
|
||||
"version": "3.17.2",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2.tgz",
|
||||
"integrity": "sha512-8kYisQ3z/SQ2kyjlNeQxbkkTNmVFoQCqkmGrzLH6A9ecPlgTbp3wDTnUNqaUxYr4vlAcloxx8zwy7G5WdguYNg==",
|
||||
"dependencies": {
|
||||
"vscode-jsonrpc": "8.0.0-next.7",
|
||||
"vscode-languageserver-types": "3.17.0-next.9"
|
||||
"vscode-jsonrpc": "8.0.2",
|
||||
"vscode-languageserver-types": "3.17.2"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-languageserver-types": {
|
||||
"version": "3.17.0-next.9",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.0-next.9.tgz",
|
||||
"integrity": "sha512-9/PeDNPYduaoXRUzYpqmu4ZV9L01HGo0wH9FUt+sSHR7IXwA7xoXBfNUlv8gB9H0D2WwEmMomSy1NmhjKQyn3A=="
|
||||
"version": "3.17.2",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz",
|
||||
"integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA=="
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
@ -6634,33 +6634,33 @@
|
||||
}
|
||||
},
|
||||
"vscode-jsonrpc": {
|
||||
"version": "8.0.0-next.7",
|
||||
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.0-next.7.tgz",
|
||||
"integrity": "sha512-JX/F31LEsims0dAlOTKFE4E+AJMiJvdRSRViifFJSqSN7EzeYyWlfuDchF7g91oRNPZOIWfibTkDf3/UMsQGzQ=="
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2.tgz",
|
||||
"integrity": "sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ=="
|
||||
},
|
||||
"vscode-languageclient": {
|
||||
"version": "8.0.0-next.14",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.0-next.14.tgz",
|
||||
"integrity": "sha512-NqjkOuDTMu8uo+PhoMsV72VO9Gd3wBi/ZpOrkRUOrWKQo7yUdiIw183g8wjH8BImgbK9ZP51HM7TI0ZhCnI1Mw==",
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.2.tgz",
|
||||
"integrity": "sha512-lHlthJtphG9gibGb/y72CKqQUxwPsMXijJVpHEC2bvbFqxmkj9LwQ3aGU9dwjBLqsX1S4KjShYppLvg1UJDF/Q==",
|
||||
"requires": {
|
||||
"minimatch": "^3.0.4",
|
||||
"semver": "^7.3.5",
|
||||
"vscode-languageserver-protocol": "3.17.0-next.16"
|
||||
"vscode-languageserver-protocol": "3.17.2"
|
||||
}
|
||||
},
|
||||
"vscode-languageserver-protocol": {
|
||||
"version": "3.17.0-next.16",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.0-next.16.tgz",
|
||||
"integrity": "sha512-tx4DnXw9u3N7vw+bx6n2NKp6FoxoNwiP/biH83AS30I2AnTGyLd7afSeH6Oewn2E8jvB7K15bs12sMppkKOVeQ==",
|
||||
"version": "3.17.2",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2.tgz",
|
||||
"integrity": "sha512-8kYisQ3z/SQ2kyjlNeQxbkkTNmVFoQCqkmGrzLH6A9ecPlgTbp3wDTnUNqaUxYr4vlAcloxx8zwy7G5WdguYNg==",
|
||||
"requires": {
|
||||
"vscode-jsonrpc": "8.0.0-next.7",
|
||||
"vscode-languageserver-types": "3.17.0-next.9"
|
||||
"vscode-jsonrpc": "8.0.2",
|
||||
"vscode-languageserver-types": "3.17.2"
|
||||
}
|
||||
},
|
||||
"vscode-languageserver-types": {
|
||||
"version": "3.17.0-next.9",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.0-next.9.tgz",
|
||||
"integrity": "sha512-9/PeDNPYduaoXRUzYpqmu4ZV9L01HGo0wH9FUt+sSHR7IXwA7xoXBfNUlv8gB9H0D2WwEmMomSy1NmhjKQyn3A=="
|
||||
"version": "3.17.2",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz",
|
||||
"integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA=="
|
||||
},
|
||||
"which": {
|
||||
"version": "2.0.2",
|
||||
|
@ -37,7 +37,7 @@
|
||||
"dependencies": {
|
||||
"d3": "^7.6.1",
|
||||
"d3-graphviz": "^4.1.1",
|
||||
"vscode-languageclient": "^8.0.0-next.14"
|
||||
"vscode-languageclient": "^8.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "~16.11.7",
|
||||
@ -60,6 +60,7 @@
|
||||
"onCommand:rust-analyzer.analyzerStatus",
|
||||
"onCommand:rust-analyzer.memoryUsage",
|
||||
"onCommand:rust-analyzer.reloadWorkspace",
|
||||
"onCommand:rust-analyzer.startServer",
|
||||
"workspaceContains:*/Cargo.toml",
|
||||
"workspaceContains:*/rust-project.json"
|
||||
],
|
||||
@ -191,6 +192,16 @@
|
||||
"title": "Restart server",
|
||||
"category": "rust-analyzer"
|
||||
},
|
||||
{
|
||||
"command": "rust-analyzer.startServer",
|
||||
"title": "Start server",
|
||||
"category": "rust-analyzer"
|
||||
},
|
||||
{
|
||||
"command": "rust-analyzer.stopServer",
|
||||
"title": "Stop server",
|
||||
"category": "rust-analyzer"
|
||||
},
|
||||
{
|
||||
"command": "rust-analyzer.onEnter",
|
||||
"title": "Enhanced enter key",
|
||||
@ -421,6 +432,32 @@
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"rust-analyzer.cargo.buildScripts.invocationLocation": {
|
||||
"markdownDescription": "Specifies the working directory for running build scripts.\n- \"workspace\": run build scripts for a workspace in the workspace's root directory.\n This is incompatible with `#rust-analyzer.cargo.buildScripts.invocationStrategy#` set to `once`.\n- \"root\": run build scripts in the project's root directory.\nThis config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`\nis set.",
|
||||
"default": "workspace",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"workspace",
|
||||
"root"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"The command will be executed in the corresponding workspace root.",
|
||||
"The command will be executed in the project root."
|
||||
]
|
||||
},
|
||||
"rust-analyzer.cargo.buildScripts.invocationStrategy": {
|
||||
"markdownDescription": "Specifies the invocation strategy to use when running the build scripts command.\nIf `per_workspace` is set, the command will be executed for each workspace.\nIf `once` is set, the command will be executed once.\nThis config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`\nis set.",
|
||||
"default": "per_workspace",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"per_workspace",
|
||||
"once"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"The command will be executed for each workspace.",
|
||||
"The command will be executed once."
|
||||
]
|
||||
},
|
||||
"rust-analyzer.cargo.buildScripts.overrideCommand": {
|
||||
"markdownDescription": "Override the command rust-analyzer uses to run build scripts and\nbuild procedural macros. The command is required to output json\nand should therefore include `--message-format=json` or a similar\noption.\n\nBy default, a cargo invocation will be constructed for the configured\ntargets and features, with the following base command line:\n\n```bash\ncargo check --quiet --workspace --message-format=json --all-targets\n```\n.",
|
||||
"default": null,
|
||||
@ -546,6 +583,32 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"rust-analyzer.checkOnSave.invocationLocation": {
|
||||
"markdownDescription": "Specifies the working directory for running checks.\n- \"workspace\": run checks for workspaces in the corresponding workspaces' root directories.\n This falls back to \"root\" if `#rust-analyzer.cargo.checkOnSave.invocationStrategy#` is set to `once`.\n- \"root\": run checks in the project's root directory.\nThis config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`\nis set.",
|
||||
"default": "workspace",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"workspace",
|
||||
"root"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"The command will be executed in the corresponding workspace root.",
|
||||
"The command will be executed in the project root."
|
||||
]
|
||||
},
|
||||
"rust-analyzer.checkOnSave.invocationStrategy": {
|
||||
"markdownDescription": "Specifies the invocation strategy to use when running the checkOnSave command.\nIf `per_workspace` is set, the command will be executed for each workspace.\nIf `once` is set, the command will be executed once.\nThis config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`\nis set.",
|
||||
"default": "per_workspace",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"per_workspace",
|
||||
"once"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"The command will be executed for each workspace.",
|
||||
"The command will be executed once."
|
||||
]
|
||||
},
|
||||
"rust-analyzer.checkOnSave.noDefaultFeatures": {
|
||||
"markdownDescription": "Whether to pass `--no-default-features` to Cargo. Defaults to\n`#rust-analyzer.cargo.noDefaultFeatures#`.",
|
||||
"default": null,
|
||||
|
@ -35,8 +35,10 @@ export class AstInspector implements vscode.HoverProvider, vscode.DefinitionProv
|
||||
});
|
||||
|
||||
constructor(ctx: Ctx) {
|
||||
ctx.pushCleanup(vscode.languages.registerHoverProvider({ scheme: "rust-analyzer" }, this));
|
||||
ctx.pushCleanup(vscode.languages.registerDefinitionProvider({ language: "rust" }, this));
|
||||
ctx.pushExtCleanup(
|
||||
vscode.languages.registerHoverProvider({ scheme: "rust-analyzer" }, this)
|
||||
);
|
||||
ctx.pushExtCleanup(vscode.languages.registerDefinitionProvider({ language: "rust" }, this));
|
||||
vscode.workspace.onDidCloseTextDocument(
|
||||
this.onDidCloseTextDocument,
|
||||
this,
|
||||
@ -52,8 +54,6 @@ export class AstInspector implements vscode.HoverProvider, vscode.DefinitionProv
|
||||
this,
|
||||
ctx.subscriptions
|
||||
);
|
||||
|
||||
ctx.pushCleanup(this);
|
||||
}
|
||||
dispose() {
|
||||
this.setRustEditor(undefined);
|
||||
|
148
src/tools/rust-analyzer/editors/code/src/bootstrap.ts
Normal file
148
src/tools/rust-analyzer/editors/code/src/bootstrap.ts
Normal file
@ -0,0 +1,148 @@
|
||||
import * as vscode from "vscode";
|
||||
import * as os from "os";
|
||||
import { Config } from "./config";
|
||||
import { log, isValidExecutable } from "./util";
|
||||
import { PersistentState } from "./persistent_state";
|
||||
import { exec } from "child_process";
|
||||
|
||||
export async function bootstrap(
|
||||
context: vscode.ExtensionContext,
|
||||
config: Config,
|
||||
state: PersistentState
|
||||
): Promise<string> {
|
||||
const path = await getServer(context, config, state);
|
||||
if (!path) {
|
||||
throw new Error(
|
||||
"Rust Analyzer Language Server is not available. " +
|
||||
"Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
|
||||
);
|
||||
}
|
||||
|
||||
log.info("Using server binary at", path);
|
||||
|
||||
if (!isValidExecutable(path)) {
|
||||
if (config.serverPath) {
|
||||
throw new Error(`Failed to execute ${path} --version. \`config.server.path\` or \`config.serverPath\` has been set explicitly.\
|
||||
Consider removing this config or making a valid server binary available at that path.`);
|
||||
} else {
|
||||
throw new Error(`Failed to execute ${path} --version`);
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
async function patchelf(dest: vscode.Uri): Promise<void> {
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: "Patching rust-analyzer for NixOS",
|
||||
},
|
||||
async (progress, _) => {
|
||||
const expression = `
|
||||
{srcStr, pkgs ? import <nixpkgs> {}}:
|
||||
pkgs.stdenv.mkDerivation {
|
||||
name = "rust-analyzer";
|
||||
src = /. + srcStr;
|
||||
phases = [ "installPhase" "fixupPhase" ];
|
||||
installPhase = "cp $src $out";
|
||||
fixupPhase = ''
|
||||
chmod 755 $out
|
||||
patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $out
|
||||
'';
|
||||
}
|
||||
`;
|
||||
const origFile = vscode.Uri.file(dest.fsPath + "-orig");
|
||||
await vscode.workspace.fs.rename(dest, origFile, { overwrite: true });
|
||||
try {
|
||||
progress.report({ message: "Patching executable", increment: 20 });
|
||||
await new Promise((resolve, reject) => {
|
||||
const handle = exec(
|
||||
`nix-build -E - --argstr srcStr '${origFile.fsPath}' -o '${dest.fsPath}'`,
|
||||
(err, stdout, stderr) => {
|
||||
if (err != null) {
|
||||
reject(Error(stderr));
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
}
|
||||
);
|
||||
handle.stdin?.write(expression);
|
||||
handle.stdin?.end();
|
||||
});
|
||||
} finally {
|
||||
await vscode.workspace.fs.delete(origFile);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function getServer(
|
||||
context: vscode.ExtensionContext,
|
||||
config: Config,
|
||||
state: PersistentState
|
||||
): Promise<string | undefined> {
|
||||
const explicitPath = serverPath(config);
|
||||
if (explicitPath) {
|
||||
if (explicitPath.startsWith("~/")) {
|
||||
return os.homedir() + explicitPath.slice("~".length);
|
||||
}
|
||||
return explicitPath;
|
||||
}
|
||||
if (config.package.releaseTag === null) return "rust-analyzer";
|
||||
|
||||
const ext = process.platform === "win32" ? ".exe" : "";
|
||||
const bundled = vscode.Uri.joinPath(context.extensionUri, "server", `rust-analyzer${ext}`);
|
||||
const bundledExists = await vscode.workspace.fs.stat(bundled).then(
|
||||
() => true,
|
||||
() => false
|
||||
);
|
||||
if (bundledExists) {
|
||||
let server = bundled;
|
||||
if (await isNixOs()) {
|
||||
await vscode.workspace.fs.createDirectory(config.globalStorageUri).then();
|
||||
const dest = vscode.Uri.joinPath(config.globalStorageUri, `rust-analyzer${ext}`);
|
||||
let exists = await vscode.workspace.fs.stat(dest).then(
|
||||
() => true,
|
||||
() => false
|
||||
);
|
||||
if (exists && config.package.version !== state.serverVersion) {
|
||||
await vscode.workspace.fs.delete(dest);
|
||||
exists = false;
|
||||
}
|
||||
if (!exists) {
|
||||
await vscode.workspace.fs.copy(bundled, dest);
|
||||
await patchelf(dest);
|
||||
}
|
||||
server = dest;
|
||||
}
|
||||
await state.updateServerVersion(config.package.version);
|
||||
return server.fsPath;
|
||||
}
|
||||
|
||||
await state.updateServerVersion(undefined);
|
||||
await vscode.window.showErrorMessage(
|
||||
"Unfortunately we don't ship binaries for your platform yet. " +
|
||||
"You need to manually clone the rust-analyzer repository and " +
|
||||
"run `cargo xtask install --server` to build the language server from sources. " +
|
||||
"If you feel that your platform should be supported, please create an issue " +
|
||||
"about that [here](https://github.com/rust-lang/rust-analyzer/issues) and we " +
|
||||
"will consider it."
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
function serverPath(config: Config): string | null {
|
||||
return process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
|
||||
}
|
||||
|
||||
async function isNixOs(): Promise<boolean> {
|
||||
try {
|
||||
const contents = (
|
||||
await vscode.workspace.fs.readFile(vscode.Uri.file("/etc/os-release"))
|
||||
).toString();
|
||||
const idString = contents.split("\n").find((a) => a.startsWith("ID=")) || "ID=linux";
|
||||
return idString.indexOf("nixos") !== -1;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -4,9 +4,7 @@ import * as ra from "../src/lsp_ext";
|
||||
import * as Is from "vscode-languageclient/lib/common/utils/is";
|
||||
import { assert } from "./util";
|
||||
import { WorkspaceEdit } from "vscode";
|
||||
import { Workspace } from "./ctx";
|
||||
import { substituteVariablesInEnv } from "./config";
|
||||
import { outputChannel, traceOutputChannel } from "./main";
|
||||
import { substituteVSCodeVariables } from "./config";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
export interface Env {
|
||||
@ -65,40 +63,42 @@ function renderHoverActions(actions: ra.CommandLinkGroup[]): vscode.MarkdownStri
|
||||
}
|
||||
|
||||
export async function createClient(
|
||||
serverPath: string,
|
||||
workspace: Workspace,
|
||||
extraEnv: Env
|
||||
traceOutputChannel: vscode.OutputChannel,
|
||||
outputChannel: vscode.OutputChannel,
|
||||
initializationOptions: vscode.WorkspaceConfiguration,
|
||||
serverOptions: lc.ServerOptions
|
||||
): Promise<lc.LanguageClient> {
|
||||
// '.' Is the fallback if no folder is open
|
||||
// TODO?: Workspace folders support Uri's (eg: file://test.txt).
|
||||
// It might be a good idea to test if the uri points to a file.
|
||||
|
||||
const newEnv = substituteVariablesInEnv(Object.assign({}, process.env, extraEnv));
|
||||
const run: lc.Executable = {
|
||||
command: serverPath,
|
||||
options: { env: newEnv },
|
||||
};
|
||||
const serverOptions: lc.ServerOptions = {
|
||||
run,
|
||||
debug: run,
|
||||
};
|
||||
|
||||
let initializationOptions = vscode.workspace.getConfiguration("rust-analyzer");
|
||||
|
||||
if (workspace.kind === "Detached Files") {
|
||||
initializationOptions = {
|
||||
detachedFiles: workspace.files.map((file) => file.uri.fsPath),
|
||||
...initializationOptions,
|
||||
};
|
||||
}
|
||||
|
||||
const clientOptions: lc.LanguageClientOptions = {
|
||||
documentSelector: [{ scheme: "file", language: "rust" }],
|
||||
initializationOptions,
|
||||
diagnosticCollectionName: "rustc",
|
||||
traceOutputChannel: traceOutputChannel(),
|
||||
outputChannel: outputChannel(),
|
||||
traceOutputChannel,
|
||||
outputChannel,
|
||||
middleware: {
|
||||
workspace: {
|
||||
// HACK: This is a workaround, when the client has been disposed, VSCode
|
||||
// continues to emit events to the client and the default one for this event
|
||||
// attempt to restart the client for no reason
|
||||
async didChangeWatchedFile(event, next) {
|
||||
if (client.isRunning()) {
|
||||
await next(event);
|
||||
}
|
||||
},
|
||||
async configuration(
|
||||
params: lc.ConfigurationParams,
|
||||
token: vscode.CancellationToken,
|
||||
next: lc.ConfigurationRequest.HandlerSignature
|
||||
) {
|
||||
const resp = await next(params, token);
|
||||
if (resp && Array.isArray(resp)) {
|
||||
return resp.map((val) => {
|
||||
return substituteVSCodeVariables(val);
|
||||
});
|
||||
} else {
|
||||
return resp;
|
||||
}
|
||||
},
|
||||
},
|
||||
async provideHover(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
@ -255,6 +255,9 @@ export async function createClient(
|
||||
}
|
||||
|
||||
class ExperimentalFeatures implements lc.StaticFeature {
|
||||
getState(): lc.FeatureState {
|
||||
return { kind: "static" };
|
||||
}
|
||||
fillClientCapabilities(capabilities: lc.ClientCapabilities): void {
|
||||
const caps: any = capabilities.experimental ?? {};
|
||||
caps.snippetTextEdit = true;
|
||||
|
@ -21,16 +21,16 @@ export function analyzerStatus(ctx: Ctx): Cmd {
|
||||
readonly uri = vscode.Uri.parse("rust-analyzer-status://status");
|
||||
readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
|
||||
|
||||
provideTextDocumentContent(_uri: vscode.Uri): vscode.ProviderResult<string> {
|
||||
async provideTextDocumentContent(_uri: vscode.Uri): Promise<string> {
|
||||
if (!vscode.window.activeTextEditor) return "";
|
||||
const client = await ctx.getClient();
|
||||
|
||||
const params: ra.AnalyzerStatusParams = {};
|
||||
const doc = ctx.activeRustEditor?.document;
|
||||
if (doc != null) {
|
||||
params.textDocument =
|
||||
ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(doc);
|
||||
params.textDocument = client.code2ProtocolConverter.asTextDocumentIdentifier(doc);
|
||||
}
|
||||
return ctx.client.sendRequest(ra.analyzerStatus, params);
|
||||
return await client.sendRequest(ra.analyzerStatus, params);
|
||||
}
|
||||
|
||||
get onDidChange(): vscode.Event<vscode.Uri> {
|
||||
@ -38,7 +38,7 @@ export function analyzerStatus(ctx: Ctx): Cmd {
|
||||
}
|
||||
})();
|
||||
|
||||
ctx.pushCleanup(
|
||||
ctx.pushExtCleanup(
|
||||
vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-status", tdcp)
|
||||
);
|
||||
|
||||
@ -60,9 +60,14 @@ export function memoryUsage(ctx: Ctx): Cmd {
|
||||
provideTextDocumentContent(_uri: vscode.Uri): vscode.ProviderResult<string> {
|
||||
if (!vscode.window.activeTextEditor) return "";
|
||||
|
||||
return ctx.client.sendRequest(ra.memoryUsage).then((mem: any) => {
|
||||
return "Per-query memory usage:\n" + mem + "\n(note: database has been cleared)";
|
||||
});
|
||||
return ctx
|
||||
.getClient()
|
||||
.then((it) => it.sendRequest(ra.memoryUsage))
|
||||
.then((mem: any) => {
|
||||
return (
|
||||
"Per-query memory usage:\n" + mem + "\n(note: database has been cleared)"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
get onDidChange(): vscode.Event<vscode.Uri> {
|
||||
@ -70,7 +75,7 @@ export function memoryUsage(ctx: Ctx): Cmd {
|
||||
}
|
||||
})();
|
||||
|
||||
ctx.pushCleanup(
|
||||
ctx.pushExtCleanup(
|
||||
vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-memory", tdcp)
|
||||
);
|
||||
|
||||
@ -83,23 +88,19 @@ export function memoryUsage(ctx: Ctx): Cmd {
|
||||
|
||||
export function shuffleCrateGraph(ctx: Ctx): Cmd {
|
||||
return async () => {
|
||||
const client = ctx.client;
|
||||
if (!client) return;
|
||||
|
||||
await client.sendRequest(ra.shuffleCrateGraph);
|
||||
return ctx.getClient().then((it) => it.sendRequest(ra.shuffleCrateGraph));
|
||||
};
|
||||
}
|
||||
|
||||
export function matchingBrace(ctx: Ctx): Cmd {
|
||||
return async () => {
|
||||
const editor = ctx.activeRustEditor;
|
||||
const client = ctx.client;
|
||||
if (!editor || !client) return;
|
||||
if (!editor) return;
|
||||
|
||||
const client = await ctx.getClient();
|
||||
|
||||
const response = await client.sendRequest(ra.matchingBrace, {
|
||||
textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
|
||||
editor.document
|
||||
),
|
||||
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
|
||||
positions: editor.selections.map((s) =>
|
||||
client.code2ProtocolConverter.asPosition(s.active)
|
||||
),
|
||||
@ -116,14 +117,13 @@ export function matchingBrace(ctx: Ctx): Cmd {
|
||||
export function joinLines(ctx: Ctx): Cmd {
|
||||
return async () => {
|
||||
const editor = ctx.activeRustEditor;
|
||||
const client = ctx.client;
|
||||
if (!editor || !client) return;
|
||||
if (!editor) return;
|
||||
|
||||
const client = await ctx.getClient();
|
||||
|
||||
const items: lc.TextEdit[] = await client.sendRequest(ra.joinLines, {
|
||||
ranges: editor.selections.map((it) => client.code2ProtocolConverter.asRange(it)),
|
||||
textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
|
||||
editor.document
|
||||
),
|
||||
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
|
||||
});
|
||||
const textEdits = await client.protocol2CodeConverter.asTextEdits(items);
|
||||
await editor.edit((builder) => {
|
||||
@ -145,14 +145,12 @@ export function moveItemDown(ctx: Ctx): Cmd {
|
||||
export function moveItem(ctx: Ctx, direction: ra.Direction): Cmd {
|
||||
return async () => {
|
||||
const editor = ctx.activeRustEditor;
|
||||
const client = ctx.client;
|
||||
if (!editor || !client) return;
|
||||
if (!editor) return;
|
||||
const client = await ctx.getClient();
|
||||
|
||||
const lcEdits = await client.sendRequest(ra.moveItem, {
|
||||
range: client.code2ProtocolConverter.asRange(editor.selection),
|
||||
textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
|
||||
editor.document
|
||||
),
|
||||
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
|
||||
direction,
|
||||
});
|
||||
|
||||
@ -166,13 +164,13 @@ export function moveItem(ctx: Ctx, direction: ra.Direction): Cmd {
|
||||
export function onEnter(ctx: Ctx): Cmd {
|
||||
async function handleKeypress() {
|
||||
const editor = ctx.activeRustEditor;
|
||||
const client = ctx.client;
|
||||
|
||||
if (!editor || !client) return false;
|
||||
if (!editor) return false;
|
||||
|
||||
const client = await ctx.getClient();
|
||||
const lcEdits = await client
|
||||
.sendRequest(ra.onEnter, {
|
||||
textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
|
||||
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(
|
||||
editor.document
|
||||
),
|
||||
position: client.code2ProtocolConverter.asPosition(editor.selection.active),
|
||||
@ -198,14 +196,13 @@ export function onEnter(ctx: Ctx): Cmd {
|
||||
export function parentModule(ctx: Ctx): Cmd {
|
||||
return async () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
const client = ctx.client;
|
||||
if (!editor || !client) return;
|
||||
if (!editor) return;
|
||||
if (!(isRustDocument(editor.document) || isCargoTomlDocument(editor.document))) return;
|
||||
|
||||
const client = await ctx.getClient();
|
||||
|
||||
const locations = await client.sendRequest(ra.parentModule, {
|
||||
textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
|
||||
editor.document
|
||||
),
|
||||
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
|
||||
position: client.code2ProtocolConverter.asPosition(editor.selection.active),
|
||||
});
|
||||
if (!locations) return;
|
||||
@ -236,13 +233,11 @@ export function parentModule(ctx: Ctx): Cmd {
|
||||
export function openCargoToml(ctx: Ctx): Cmd {
|
||||
return async () => {
|
||||
const editor = ctx.activeRustEditor;
|
||||
const client = ctx.client;
|
||||
if (!editor || !client) return;
|
||||
if (!editor) return;
|
||||
|
||||
const client = await ctx.getClient();
|
||||
const response = await client.sendRequest(ra.openCargoToml, {
|
||||
textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
|
||||
editor.document
|
||||
),
|
||||
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
|
||||
});
|
||||
if (!response) return;
|
||||
|
||||
@ -259,12 +254,13 @@ export function openCargoToml(ctx: Ctx): Cmd {
|
||||
export function ssr(ctx: Ctx): Cmd {
|
||||
return async () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
const client = ctx.client;
|
||||
if (!editor || !client) return;
|
||||
if (!editor) return;
|
||||
|
||||
const client = await ctx.getClient();
|
||||
|
||||
const position = editor.selection.active;
|
||||
const selections = editor.selections;
|
||||
const textDocument = ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
|
||||
const textDocument = client.code2ProtocolConverter.asTextDocumentIdentifier(
|
||||
editor.document
|
||||
);
|
||||
|
||||
@ -314,6 +310,10 @@ export function ssr(ctx: Ctx): Cmd {
|
||||
|
||||
export function serverVersion(ctx: Ctx): Cmd {
|
||||
return async () => {
|
||||
if (!ctx.serverPath) {
|
||||
void vscode.window.showWarningMessage(`rust-analyzer server is not running`);
|
||||
return;
|
||||
}
|
||||
const { stdout } = spawnSync(ctx.serverPath, ["--version"], { encoding: "utf8" });
|
||||
const versionString = stdout.slice(`rust-analyzer `.length).trim();
|
||||
|
||||
@ -354,21 +354,22 @@ export function syntaxTree(ctx: Ctx): Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
provideTextDocumentContent(
|
||||
async provideTextDocumentContent(
|
||||
uri: vscode.Uri,
|
||||
ct: vscode.CancellationToken
|
||||
): vscode.ProviderResult<string> {
|
||||
): Promise<string> {
|
||||
const rustEditor = ctx.activeRustEditor;
|
||||
if (!rustEditor) return "";
|
||||
const client = await ctx.getClient();
|
||||
|
||||
// When the range based query is enabled we take the range of the selection
|
||||
const range =
|
||||
uri.query === "range=true" && !rustEditor.selection.isEmpty
|
||||
? ctx.client.code2ProtocolConverter.asRange(rustEditor.selection)
|
||||
? client.code2ProtocolConverter.asRange(rustEditor.selection)
|
||||
: null;
|
||||
|
||||
const params = { textDocument: { uri: rustEditor.document.uri.toString() }, range };
|
||||
return ctx.client.sendRequest(ra.syntaxTree, params, ct);
|
||||
return client.sendRequest(ra.syntaxTree, params, ct);
|
||||
}
|
||||
|
||||
get onDidChange(): vscode.Event<vscode.Uri> {
|
||||
@ -376,12 +377,11 @@ export function syntaxTree(ctx: Ctx): Cmd {
|
||||
}
|
||||
})();
|
||||
|
||||
void new AstInspector(ctx);
|
||||
|
||||
ctx.pushCleanup(
|
||||
ctx.pushExtCleanup(new AstInspector(ctx));
|
||||
ctx.pushExtCleanup(
|
||||
vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-syntax-tree", tdcp)
|
||||
);
|
||||
ctx.pushCleanup(
|
||||
ctx.pushExtCleanup(
|
||||
vscode.languages.setLanguageConfiguration("ra_syntax_tree", {
|
||||
brackets: [["[", ")"]],
|
||||
})
|
||||
@ -437,14 +437,14 @@ export function viewHir(ctx: Ctx): Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
provideTextDocumentContent(
|
||||
async provideTextDocumentContent(
|
||||
_uri: vscode.Uri,
|
||||
ct: vscode.CancellationToken
|
||||
): vscode.ProviderResult<string> {
|
||||
): Promise<string> {
|
||||
const rustEditor = ctx.activeRustEditor;
|
||||
const client = ctx.client;
|
||||
if (!rustEditor || !client) return "";
|
||||
if (!rustEditor) return "";
|
||||
|
||||
const client = await ctx.getClient();
|
||||
const params = {
|
||||
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(
|
||||
rustEditor.document
|
||||
@ -459,7 +459,7 @@ export function viewHir(ctx: Ctx): Cmd {
|
||||
}
|
||||
})();
|
||||
|
||||
ctx.pushCleanup(
|
||||
ctx.pushExtCleanup(
|
||||
vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-hir", tdcp)
|
||||
);
|
||||
|
||||
@ -503,13 +503,13 @@ export function viewFileText(ctx: Ctx): Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
provideTextDocumentContent(
|
||||
async provideTextDocumentContent(
|
||||
_uri: vscode.Uri,
|
||||
ct: vscode.CancellationToken
|
||||
): vscode.ProviderResult<string> {
|
||||
): Promise<string> {
|
||||
const rustEditor = ctx.activeRustEditor;
|
||||
const client = ctx.client;
|
||||
if (!rustEditor || !client) return "";
|
||||
if (!rustEditor) return "";
|
||||
const client = await ctx.getClient();
|
||||
|
||||
const params = client.code2ProtocolConverter.asTextDocumentIdentifier(
|
||||
rustEditor.document
|
||||
@ -522,7 +522,7 @@ export function viewFileText(ctx: Ctx): Cmd {
|
||||
}
|
||||
})();
|
||||
|
||||
ctx.pushCleanup(
|
||||
ctx.pushExtCleanup(
|
||||
vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-file-text", tdcp)
|
||||
);
|
||||
|
||||
@ -566,13 +566,13 @@ export function viewItemTree(ctx: Ctx): Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
provideTextDocumentContent(
|
||||
async provideTextDocumentContent(
|
||||
_uri: vscode.Uri,
|
||||
ct: vscode.CancellationToken
|
||||
): vscode.ProviderResult<string> {
|
||||
): Promise<string> {
|
||||
const rustEditor = ctx.activeRustEditor;
|
||||
const client = ctx.client;
|
||||
if (!rustEditor || !client) return "";
|
||||
if (!rustEditor) return "";
|
||||
const client = await ctx.getClient();
|
||||
|
||||
const params = {
|
||||
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(
|
||||
@ -587,7 +587,7 @@ export function viewItemTree(ctx: Ctx): Cmd {
|
||||
}
|
||||
})();
|
||||
|
||||
ctx.pushCleanup(
|
||||
ctx.pushExtCleanup(
|
||||
vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-item-tree", tdcp)
|
||||
);
|
||||
|
||||
@ -618,8 +618,8 @@ function crateGraph(ctx: Ctx, full: boolean): Cmd {
|
||||
const params = {
|
||||
full: full,
|
||||
};
|
||||
|
||||
const dot = await ctx.client.sendRequest(ra.viewCrateGraph, params);
|
||||
const client = await ctx.getClient();
|
||||
const dot = await client.sendRequest(ra.viewCrateGraph, params);
|
||||
const uri = panel.webview.asWebviewUri(nodeModulesPath);
|
||||
|
||||
const html = `
|
||||
@ -690,13 +690,13 @@ export function expandMacro(ctx: Ctx): Cmd {
|
||||
eventEmitter = new vscode.EventEmitter<vscode.Uri>();
|
||||
async provideTextDocumentContent(_uri: vscode.Uri): Promise<string> {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
const client = ctx.client;
|
||||
if (!editor || !client) return "";
|
||||
if (!editor) return "";
|
||||
const client = await ctx.getClient();
|
||||
|
||||
const position = editor.selection.active;
|
||||
|
||||
const expanded = await client.sendRequest(ra.expandMacro, {
|
||||
textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
|
||||
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(
|
||||
editor.document
|
||||
),
|
||||
position,
|
||||
@ -712,7 +712,7 @@ export function expandMacro(ctx: Ctx): Cmd {
|
||||
}
|
||||
})();
|
||||
|
||||
ctx.pushCleanup(
|
||||
ctx.pushExtCleanup(
|
||||
vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-expand-macro", tdcp)
|
||||
);
|
||||
|
||||
@ -724,11 +724,11 @@ export function expandMacro(ctx: Ctx): Cmd {
|
||||
}
|
||||
|
||||
export function reloadWorkspace(ctx: Ctx): Cmd {
|
||||
return async () => ctx.client.sendRequest(ra.reloadWorkspace);
|
||||
return async () => (await ctx.getClient()).sendRequest(ra.reloadWorkspace);
|
||||
}
|
||||
|
||||
async function showReferencesImpl(
|
||||
client: LanguageClient,
|
||||
client: LanguageClient | undefined,
|
||||
uri: string,
|
||||
position: lc.Position,
|
||||
locations: lc.Location[]
|
||||
@ -745,7 +745,7 @@ async function showReferencesImpl(
|
||||
|
||||
export function showReferences(ctx: Ctx): Cmd {
|
||||
return async (uri: string, position: lc.Position, locations: lc.Location[]) => {
|
||||
await showReferencesImpl(ctx.client, uri, position, locations);
|
||||
await showReferencesImpl(await ctx.getClient(), uri, position, locations);
|
||||
};
|
||||
}
|
||||
|
||||
@ -762,25 +762,23 @@ export function applyActionGroup(_ctx: Ctx): Cmd {
|
||||
|
||||
export function gotoLocation(ctx: Ctx): Cmd {
|
||||
return async (locationLink: lc.LocationLink) => {
|
||||
const client = ctx.client;
|
||||
if (client) {
|
||||
const uri = client.protocol2CodeConverter.asUri(locationLink.targetUri);
|
||||
let range = client.protocol2CodeConverter.asRange(locationLink.targetSelectionRange);
|
||||
// collapse the range to a cursor position
|
||||
range = range.with({ end: range.start });
|
||||
const client = await ctx.getClient();
|
||||
const uri = client.protocol2CodeConverter.asUri(locationLink.targetUri);
|
||||
let range = client.protocol2CodeConverter.asRange(locationLink.targetSelectionRange);
|
||||
// collapse the range to a cursor position
|
||||
range = range.with({ end: range.start });
|
||||
|
||||
await vscode.window.showTextDocument(uri, { selection: range });
|
||||
}
|
||||
await vscode.window.showTextDocument(uri, { selection: range });
|
||||
};
|
||||
}
|
||||
|
||||
export function openDocs(ctx: Ctx): Cmd {
|
||||
return async () => {
|
||||
const client = ctx.client;
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor || !client) {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
const client = await ctx.getClient();
|
||||
|
||||
const position = editor.selection.active;
|
||||
const textDocument = { uri: editor.document.uri.toString() };
|
||||
@ -795,20 +793,21 @@ export function openDocs(ctx: Ctx): Cmd {
|
||||
|
||||
export function cancelFlycheck(ctx: Ctx): Cmd {
|
||||
return async () => {
|
||||
await ctx.client.sendRequest(ra.cancelFlycheck);
|
||||
const client = await ctx.getClient();
|
||||
await client.sendRequest(ra.cancelFlycheck);
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveCodeAction(ctx: Ctx): Cmd {
|
||||
const client = ctx.client;
|
||||
return async (params: lc.CodeAction) => {
|
||||
const client = await ctx.getClient();
|
||||
params.command = undefined;
|
||||
const item = await client.sendRequest(lc.CodeActionResolveRequest.type, params);
|
||||
if (!item.edit) {
|
||||
const item = await client?.sendRequest(lc.CodeActionResolveRequest.type, params);
|
||||
if (!item?.edit) {
|
||||
return;
|
||||
}
|
||||
const itemEdit = item.edit;
|
||||
const edit = await client.protocol2CodeConverter.asWorkspaceEdit(itemEdit);
|
||||
const edit = await client?.protocol2CodeConverter.asWorkspaceEdit(itemEdit);
|
||||
// filter out all text edits and recreate the WorkspaceEdit without them so we can apply
|
||||
// snippet edits on our own
|
||||
const lcFileSystemEdit = {
|
||||
@ -847,11 +846,10 @@ export function run(ctx: Ctx): Cmd {
|
||||
}
|
||||
|
||||
export function peekTests(ctx: Ctx): Cmd {
|
||||
const client = ctx.client;
|
||||
|
||||
return async () => {
|
||||
const editor = ctx.activeRustEditor;
|
||||
if (!editor || !client) return;
|
||||
if (!editor) return;
|
||||
const client = await ctx.getClient();
|
||||
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
@ -937,10 +935,10 @@ export function newDebugConfig(ctx: Ctx): Cmd {
|
||||
};
|
||||
}
|
||||
|
||||
export function linkToCommand(ctx: Ctx): Cmd {
|
||||
export function linkToCommand(_: Ctx): Cmd {
|
||||
return async (commandId: string) => {
|
||||
const link = LINKED_COMMANDS.get(commandId);
|
||||
if (ctx.client && link) {
|
||||
if (link) {
|
||||
const { command, arguments: args = [] } = link;
|
||||
await vscode.commands.executeCommand(command, ...args);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import path = require("path");
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
import * as vscode from "vscode";
|
||||
import { Env } from "./client";
|
||||
import { log } from "./util";
|
||||
@ -10,23 +11,17 @@ export type RunnableEnvCfg =
|
||||
|
||||
export class Config {
|
||||
readonly extensionId = "rust-lang.rust-analyzer";
|
||||
configureLang: vscode.Disposable | undefined;
|
||||
|
||||
readonly rootSection = "rust-analyzer";
|
||||
private readonly requiresWorkspaceReloadOpts = [
|
||||
"serverPath",
|
||||
"server",
|
||||
// FIXME: This shouldn't be here, changing this setting should reload
|
||||
// `continueCommentsOnNewline` behavior without restart
|
||||
"typing",
|
||||
].map((opt) => `${this.rootSection}.${opt}`);
|
||||
private readonly requiresReloadOpts = [
|
||||
"cargo",
|
||||
"procMacro",
|
||||
"serverPath",
|
||||
"server",
|
||||
"files",
|
||||
"lens", // works as lens.*
|
||||
]
|
||||
.map((opt) => `${this.rootSection}.${opt}`)
|
||||
.concat(this.requiresWorkspaceReloadOpts);
|
||||
].map((opt) => `${this.rootSection}.${opt}`);
|
||||
|
||||
readonly package: {
|
||||
version: string;
|
||||
@ -44,6 +39,11 @@ export class Config {
|
||||
ctx.subscriptions
|
||||
);
|
||||
this.refreshLogging();
|
||||
this.configureLanguage();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.configureLang?.dispose();
|
||||
}
|
||||
|
||||
private refreshLogging() {
|
||||
@ -57,33 +57,86 @@ export class Config {
|
||||
private async onDidChangeConfiguration(event: vscode.ConfigurationChangeEvent) {
|
||||
this.refreshLogging();
|
||||
|
||||
this.configureLanguage();
|
||||
|
||||
const requiresReloadOpt = this.requiresReloadOpts.find((opt) =>
|
||||
event.affectsConfiguration(opt)
|
||||
);
|
||||
|
||||
if (!requiresReloadOpt) return;
|
||||
|
||||
const requiresWorkspaceReloadOpt = this.requiresWorkspaceReloadOpts.find((opt) =>
|
||||
event.affectsConfiguration(opt)
|
||||
);
|
||||
|
||||
if (!requiresWorkspaceReloadOpt && this.restartServerOnConfigChange) {
|
||||
if (this.restartServerOnConfigChange) {
|
||||
await vscode.commands.executeCommand("rust-analyzer.reload");
|
||||
return;
|
||||
}
|
||||
|
||||
const message = requiresWorkspaceReloadOpt
|
||||
? `Changing "${requiresWorkspaceReloadOpt}" requires a window reload`
|
||||
: `Changing "${requiresReloadOpt}" requires a reload`;
|
||||
const userResponse = await vscode.window.showInformationMessage(message, "Reload now");
|
||||
const message = `Changing "${requiresReloadOpt}" requires a server restart`;
|
||||
const userResponse = await vscode.window.showInformationMessage(message, "Restart now");
|
||||
|
||||
if (userResponse === "Reload now") {
|
||||
const command = requiresWorkspaceReloadOpt
|
||||
? "workbench.action.reloadWindow"
|
||||
: "rust-analyzer.reload";
|
||||
if (userResponse === "Reload now") {
|
||||
await vscode.commands.executeCommand(command);
|
||||
}
|
||||
if (userResponse) {
|
||||
const command = "rust-analyzer.reload";
|
||||
await vscode.commands.executeCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up additional language configuration that's impossible to do via a
|
||||
* separate language-configuration.json file. See [1] for more information.
|
||||
*
|
||||
* [1]: https://github.com/Microsoft/vscode/issues/11514#issuecomment-244707076
|
||||
*/
|
||||
private configureLanguage() {
|
||||
if (this.typingContinueCommentsOnNewline && !this.configureLang) {
|
||||
const indentAction = vscode.IndentAction.None;
|
||||
|
||||
this.configureLang = vscode.languages.setLanguageConfiguration("rust", {
|
||||
onEnterRules: [
|
||||
{
|
||||
// Doc single-line comment
|
||||
// e.g. ///|
|
||||
beforeText: /^\s*\/{3}.*$/,
|
||||
action: { indentAction, appendText: "/// " },
|
||||
},
|
||||
{
|
||||
// Parent doc single-line comment
|
||||
// e.g. //!|
|
||||
beforeText: /^\s*\/{2}\!.*$/,
|
||||
action: { indentAction, appendText: "//! " },
|
||||
},
|
||||
{
|
||||
// Begins an auto-closed multi-line comment (standard or parent doc)
|
||||
// e.g. /** | */ or /*! | */
|
||||
beforeText: /^\s*\/\*(\*|\!)(?!\/)([^\*]|\*(?!\/))*$/,
|
||||
afterText: /^\s*\*\/$/,
|
||||
action: {
|
||||
indentAction: vscode.IndentAction.IndentOutdent,
|
||||
appendText: " * ",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Begins a multi-line comment (standard or parent doc)
|
||||
// e.g. /** ...| or /*! ...|
|
||||
beforeText: /^\s*\/\*(\*|\!)(?!\/)([^\*]|\*(?!\/))*$/,
|
||||
action: { indentAction, appendText: " * " },
|
||||
},
|
||||
{
|
||||
// Continues a multi-line comment
|
||||
// e.g. * ...|
|
||||
beforeText: /^(\ \ )*\ \*(\ ([^\*]|\*(?!\/))*)?$/,
|
||||
action: { indentAction, appendText: "* " },
|
||||
},
|
||||
{
|
||||
// Dedents after closing a multi-line comment
|
||||
// e.g. */|
|
||||
beforeText: /^(\ \ )*\ \*\/\s*$/,
|
||||
action: { indentAction, removeText: 1 },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (!this.typingContinueCommentsOnNewline && this.configureLang) {
|
||||
this.configureLang.dispose();
|
||||
this.configureLang = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,6 +240,37 @@ export class Config {
|
||||
}
|
||||
}
|
||||
|
||||
const VarRegex = new RegExp(/\$\{(.+?)\}/g);
|
||||
|
||||
export function substituteVSCodeVariableInString(val: string): string {
|
||||
return val.replace(VarRegex, (substring: string, varName) => {
|
||||
if (typeof varName === "string") {
|
||||
return computeVscodeVar(varName) || substring;
|
||||
} else {
|
||||
return substring;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function substituteVSCodeVariables(resp: any): any {
|
||||
if (typeof resp === "string") {
|
||||
return substituteVSCodeVariableInString(resp);
|
||||
} else if (resp && Array.isArray(resp)) {
|
||||
return resp.map((val) => {
|
||||
return substituteVSCodeVariables(val);
|
||||
});
|
||||
} else if (resp && typeof resp === "object") {
|
||||
const res: { [key: string]: any } = {};
|
||||
for (const key in resp) {
|
||||
const val = resp[key];
|
||||
res[key] = substituteVSCodeVariables(val);
|
||||
}
|
||||
return res;
|
||||
} else if (typeof resp === "function") {
|
||||
return null;
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
export function substituteVariablesInEnv(env: Env): Env {
|
||||
const missingDeps = new Set<string>();
|
||||
// vscode uses `env:ENV_NAME` for env vars resolution, and it's easier
|
||||
@ -233,7 +317,7 @@ export function substituteVariablesInEnv(env: Env): Env {
|
||||
}
|
||||
} else {
|
||||
envWithDeps[dep] = {
|
||||
value: computeVscodeVar(dep),
|
||||
value: computeVscodeVar(dep) || "${" + dep + "}",
|
||||
deps: [],
|
||||
};
|
||||
}
|
||||
@ -264,37 +348,34 @@ export function substituteVariablesInEnv(env: Env): Env {
|
||||
return resolvedEnv;
|
||||
}
|
||||
|
||||
function computeVscodeVar(varName: string): string {
|
||||
function computeVscodeVar(varName: string): string | null {
|
||||
const workspaceFolder = () => {
|
||||
const folders = vscode.workspace.workspaceFolders ?? [];
|
||||
if (folders.length === 1) {
|
||||
// TODO: support for remote workspaces?
|
||||
return folders[0].uri.fsPath;
|
||||
} else if (folders.length > 1) {
|
||||
// could use currently opened document to detect the correct
|
||||
// workspace. However, that would be determined by the document
|
||||
// user has opened on Editor startup. Could lead to
|
||||
// unpredictable workspace selection in practice.
|
||||
// It's better to pick the first one
|
||||
return folders[0].uri.fsPath;
|
||||
} else {
|
||||
// no workspace opened
|
||||
return "";
|
||||
}
|
||||
};
|
||||
// https://code.visualstudio.com/docs/editor/variables-reference
|
||||
const supportedVariables: { [k: string]: () => string } = {
|
||||
workspaceFolder: () => {
|
||||
const folders = vscode.workspace.workspaceFolders ?? [];
|
||||
if (folders.length === 1) {
|
||||
// TODO: support for remote workspaces?
|
||||
return folders[0].uri.fsPath;
|
||||
} else if (folders.length > 1) {
|
||||
// could use currently opened document to detect the correct
|
||||
// workspace. However, that would be determined by the document
|
||||
// user has opened on Editor startup. Could lead to
|
||||
// unpredictable workspace selection in practice.
|
||||
// It's better to pick the first one
|
||||
return folders[0].uri.fsPath;
|
||||
} else {
|
||||
// no workspace opened
|
||||
return "";
|
||||
}
|
||||
},
|
||||
workspaceFolder,
|
||||
|
||||
workspaceFolderBasename: () => {
|
||||
const workspaceFolder = computeVscodeVar("workspaceFolder");
|
||||
if (workspaceFolder) {
|
||||
return path.basename(workspaceFolder);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
return path.basename(workspaceFolder());
|
||||
},
|
||||
|
||||
cwd: () => process.cwd(),
|
||||
userHome: () => os.homedir(),
|
||||
|
||||
// see
|
||||
// https://github.com/microsoft/vscode/blob/08ac1bb67ca2459496b272d8f4a908757f24f56f/src/vs/workbench/api/common/extHostVariableResolverService.ts#L81
|
||||
@ -308,7 +389,7 @@ function computeVscodeVar(varName: string): string {
|
||||
if (varName in supportedVariables) {
|
||||
return supportedVariables[varName]();
|
||||
} else {
|
||||
// can't resolve, keep the expression as is
|
||||
return "${" + varName + "}";
|
||||
// return "${" + varName + "}";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -2,10 +2,12 @@ import * as vscode from "vscode";
|
||||
import * as lc from "vscode-languageclient/node";
|
||||
import * as ra from "./lsp_ext";
|
||||
|
||||
import { Config } from "./config";
|
||||
import { Config, substituteVariablesInEnv, substituteVSCodeVariables } from "./config";
|
||||
import { createClient } from "./client";
|
||||
import { isRustEditor, RustEditor } from "./util";
|
||||
import { isRustEditor, log, RustEditor } from "./util";
|
||||
import { ServerStatusParams } from "./lsp_ext";
|
||||
import { PersistentState } from "./persistent_state";
|
||||
import { bootstrap } from "./bootstrap";
|
||||
|
||||
export type Workspace =
|
||||
| {
|
||||
@ -16,36 +18,154 @@ export type Workspace =
|
||||
files: vscode.TextDocument[];
|
||||
};
|
||||
|
||||
export type CommandFactory = {
|
||||
enabled: (ctx: Ctx) => Cmd;
|
||||
disabled?: (ctx: Ctx) => Cmd;
|
||||
};
|
||||
|
||||
export class Ctx {
|
||||
private constructor(
|
||||
readonly config: Config,
|
||||
private readonly extCtx: vscode.ExtensionContext,
|
||||
readonly client: lc.LanguageClient,
|
||||
readonly serverPath: string,
|
||||
readonly statusBar: vscode.StatusBarItem
|
||||
) {}
|
||||
readonly statusBar: vscode.StatusBarItem;
|
||||
readonly config: Config;
|
||||
|
||||
static async create(
|
||||
config: Config,
|
||||
extCtx: vscode.ExtensionContext,
|
||||
serverPath: string,
|
||||
workspace: Workspace
|
||||
): Promise<Ctx> {
|
||||
const client = await createClient(serverPath, workspace, config.serverExtraEnv);
|
||||
private client: lc.LanguageClient | undefined;
|
||||
private _serverPath: string | undefined;
|
||||
private traceOutputChannel: vscode.OutputChannel | undefined;
|
||||
private outputChannel: vscode.OutputChannel | undefined;
|
||||
private clientSubscriptions: Disposable[];
|
||||
private state: PersistentState;
|
||||
private commandFactories: Record<string, CommandFactory>;
|
||||
private commandDisposables: Disposable[];
|
||||
|
||||
const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
|
||||
extCtx.subscriptions.push(statusBar);
|
||||
statusBar.text = "rust-analyzer";
|
||||
statusBar.tooltip = "ready";
|
||||
statusBar.command = "rust-analyzer.analyzerStatus";
|
||||
statusBar.show();
|
||||
workspace: Workspace;
|
||||
|
||||
const res = new Ctx(config, extCtx, client, serverPath, statusBar);
|
||||
constructor(
|
||||
readonly extCtx: vscode.ExtensionContext,
|
||||
workspace: Workspace,
|
||||
commandFactories: Record<string, CommandFactory>
|
||||
) {
|
||||
extCtx.subscriptions.push(this);
|
||||
this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
|
||||
this.statusBar.text = "rust-analyzer";
|
||||
this.statusBar.tooltip = "ready";
|
||||
this.statusBar.command = "rust-analyzer.analyzerStatus";
|
||||
this.statusBar.show();
|
||||
this.workspace = workspace;
|
||||
this.clientSubscriptions = [];
|
||||
this.commandDisposables = [];
|
||||
this.commandFactories = commandFactories;
|
||||
|
||||
res.pushCleanup(client.start());
|
||||
await client.onReady();
|
||||
client.onNotification(ra.serverStatus, (params) => res.setServerStatus(params));
|
||||
return res;
|
||||
this.state = new PersistentState(extCtx.globalState);
|
||||
this.config = new Config(extCtx);
|
||||
|
||||
this.updateCommands();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.config.dispose();
|
||||
this.statusBar.dispose();
|
||||
void this.disposeClient();
|
||||
this.commandDisposables.forEach((disposable) => disposable.dispose());
|
||||
}
|
||||
|
||||
clientFetcher() {
|
||||
const self = this;
|
||||
return {
|
||||
get client(): lc.LanguageClient | undefined {
|
||||
return self.client;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getClient() {
|
||||
if (!this.traceOutputChannel) {
|
||||
this.traceOutputChannel = vscode.window.createOutputChannel(
|
||||
"Rust Analyzer Language Server Trace"
|
||||
);
|
||||
this.pushExtCleanup(this.traceOutputChannel);
|
||||
}
|
||||
if (!this.outputChannel) {
|
||||
this.outputChannel = vscode.window.createOutputChannel("Rust Analyzer Language Server");
|
||||
this.pushExtCleanup(this.outputChannel);
|
||||
}
|
||||
|
||||
if (!this.client) {
|
||||
this._serverPath = await bootstrap(this.extCtx, this.config, this.state).catch(
|
||||
(err) => {
|
||||
let message = "bootstrap error. ";
|
||||
|
||||
message +=
|
||||
'See the logs in "OUTPUT > Rust Analyzer Client" (should open automatically). ';
|
||||
message +=
|
||||
'To enable verbose logs use { "rust-analyzer.trace.extension": true }';
|
||||
|
||||
log.error("Bootstrap error", err);
|
||||
throw new Error(message);
|
||||
}
|
||||
);
|
||||
const newEnv = substituteVariablesInEnv(
|
||||
Object.assign({}, process.env, this.config.serverExtraEnv)
|
||||
);
|
||||
const run: lc.Executable = {
|
||||
command: this._serverPath,
|
||||
options: { env: newEnv },
|
||||
};
|
||||
const serverOptions = {
|
||||
run,
|
||||
debug: run,
|
||||
};
|
||||
|
||||
let rawInitializationOptions = vscode.workspace.getConfiguration("rust-analyzer");
|
||||
|
||||
if (this.workspace.kind === "Detached Files") {
|
||||
rawInitializationOptions = {
|
||||
detachedFiles: this.workspace.files.map((file) => file.uri.fsPath),
|
||||
...rawInitializationOptions,
|
||||
};
|
||||
}
|
||||
|
||||
const initializationOptions = substituteVSCodeVariables(rawInitializationOptions);
|
||||
|
||||
this.client = await createClient(
|
||||
this.traceOutputChannel,
|
||||
this.outputChannel,
|
||||
initializationOptions,
|
||||
serverOptions
|
||||
);
|
||||
this.pushClientCleanup(
|
||||
this.client.onNotification(ra.serverStatus, (params) =>
|
||||
this.setServerStatus(params)
|
||||
)
|
||||
);
|
||||
}
|
||||
return this.client;
|
||||
}
|
||||
|
||||
async activate() {
|
||||
log.info("Activating language client");
|
||||
const client = await this.getClient();
|
||||
await client.start();
|
||||
this.updateCommands();
|
||||
return client;
|
||||
}
|
||||
|
||||
async deactivate() {
|
||||
log.info("Deactivating language client");
|
||||
await this.client?.stop();
|
||||
this.updateCommands();
|
||||
}
|
||||
|
||||
async stop() {
|
||||
log.info("Stopping language client");
|
||||
await this.disposeClient();
|
||||
this.updateCommands();
|
||||
}
|
||||
|
||||
private async disposeClient() {
|
||||
this.clientSubscriptions?.forEach((disposable) => disposable.dispose());
|
||||
this.clientSubscriptions = [];
|
||||
await this.client?.dispose();
|
||||
this._serverPath = undefined;
|
||||
this.client = undefined;
|
||||
}
|
||||
|
||||
get activeRustEditor(): RustEditor | undefined {
|
||||
@ -53,29 +173,37 @@ export class Ctx {
|
||||
return editor && isRustEditor(editor) ? editor : undefined;
|
||||
}
|
||||
|
||||
get visibleRustEditors(): RustEditor[] {
|
||||
return vscode.window.visibleTextEditors.filter(isRustEditor);
|
||||
}
|
||||
|
||||
registerCommand(name: string, factory: (ctx: Ctx) => Cmd) {
|
||||
const fullName = `rust-analyzer.${name}`;
|
||||
const cmd = factory(this);
|
||||
const d = vscode.commands.registerCommand(fullName, cmd);
|
||||
this.pushCleanup(d);
|
||||
}
|
||||
|
||||
get extensionPath(): string {
|
||||
return this.extCtx.extensionPath;
|
||||
}
|
||||
|
||||
get globalState(): vscode.Memento {
|
||||
return this.extCtx.globalState;
|
||||
}
|
||||
|
||||
get subscriptions(): Disposable[] {
|
||||
return this.extCtx.subscriptions;
|
||||
}
|
||||
|
||||
get serverPath(): string | undefined {
|
||||
return this._serverPath;
|
||||
}
|
||||
|
||||
private updateCommands() {
|
||||
this.commandDisposables.forEach((disposable) => disposable.dispose());
|
||||
this.commandDisposables = [];
|
||||
const fetchFactory = (factory: CommandFactory, fullName: string) => {
|
||||
return this.client && this.client.isRunning()
|
||||
? factory.enabled
|
||||
: factory.disabled ||
|
||||
((_) => () =>
|
||||
vscode.window.showErrorMessage(
|
||||
`command ${fullName} failed: rust-analyzer server is not running`
|
||||
));
|
||||
};
|
||||
for (const [name, factory] of Object.entries(this.commandFactories)) {
|
||||
const fullName = `rust-analyzer.${name}`;
|
||||
const callback = fetchFactory(factory, fullName)(this);
|
||||
this.commandDisposables.push(vscode.commands.registerCommand(fullName, callback));
|
||||
}
|
||||
}
|
||||
|
||||
setServerStatus(status: ServerStatusParams) {
|
||||
let icon = "";
|
||||
const statusBar = this.statusBar;
|
||||
@ -111,9 +239,13 @@ export class Ctx {
|
||||
statusBar.text = `${icon}rust-analyzer`;
|
||||
}
|
||||
|
||||
pushCleanup(d: Disposable) {
|
||||
pushExtCleanup(d: Disposable) {
|
||||
this.extCtx.subscriptions.push(d);
|
||||
}
|
||||
|
||||
private pushClientCleanup(d: Disposable) {
|
||||
this.clientSubscriptions.push(d);
|
||||
}
|
||||
}
|
||||
|
||||
export interface Disposable {
|
||||
|
@ -1,53 +1,37 @@
|
||||
import * as vscode from "vscode";
|
||||
import * as lc from "vscode-languageclient/node";
|
||||
import * as os from "os";
|
||||
|
||||
import * as commands from "./commands";
|
||||
import { Ctx } from "./ctx";
|
||||
import { Config } from "./config";
|
||||
import { log, isValidExecutable, isRustDocument } from "./util";
|
||||
import { PersistentState } from "./persistent_state";
|
||||
import { CommandFactory, Ctx, Workspace } from "./ctx";
|
||||
import { isRustDocument } from "./util";
|
||||
import { activateTaskProvider } from "./tasks";
|
||||
import { setContextValue } from "./util";
|
||||
import { exec } from "child_process";
|
||||
|
||||
let ctx: Ctx | undefined;
|
||||
|
||||
const RUST_PROJECT_CONTEXT_NAME = "inRustProject";
|
||||
|
||||
let TRACE_OUTPUT_CHANNEL: vscode.OutputChannel | null = null;
|
||||
export function traceOutputChannel() {
|
||||
if (!TRACE_OUTPUT_CHANNEL) {
|
||||
TRACE_OUTPUT_CHANNEL = vscode.window.createOutputChannel(
|
||||
"Rust Analyzer Language Server Trace"
|
||||
);
|
||||
}
|
||||
return TRACE_OUTPUT_CHANNEL;
|
||||
}
|
||||
let OUTPUT_CHANNEL: vscode.OutputChannel | null = null;
|
||||
export function outputChannel() {
|
||||
if (!OUTPUT_CHANNEL) {
|
||||
OUTPUT_CHANNEL = vscode.window.createOutputChannel("Rust Analyzer Language Server");
|
||||
}
|
||||
return OUTPUT_CHANNEL;
|
||||
export interface RustAnalyzerExtensionApi {
|
||||
// FIXME: this should be non-optional
|
||||
readonly client?: lc.LanguageClient;
|
||||
}
|
||||
|
||||
export interface RustAnalyzerExtensionApi {
|
||||
client?: lc.LanguageClient;
|
||||
export async function deactivate() {
|
||||
await setContextValue(RUST_PROJECT_CONTEXT_NAME, undefined);
|
||||
}
|
||||
|
||||
export async function activate(
|
||||
context: vscode.ExtensionContext
|
||||
): Promise<RustAnalyzerExtensionApi> {
|
||||
// VS Code doesn't show a notification when an extension fails to activate
|
||||
// so we do it ourselves.
|
||||
return await tryActivate(context).catch((err) => {
|
||||
void vscode.window.showErrorMessage(`Cannot activate rust-analyzer: ${err.message}`);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
if (vscode.extensions.getExtension("rust-lang.rust")) {
|
||||
vscode.window
|
||||
.showWarningMessage(
|
||||
`You have both the rust-analyzer (rust-lang.rust-analyzer) and Rust (rust-lang.rust) ` +
|
||||
"plugins enabled. These are known to conflict and cause various functions of " +
|
||||
"both plugins to not work correctly. You should disable one of them.",
|
||||
"Got it"
|
||||
)
|
||||
.then(() => {}, console.error);
|
||||
}
|
||||
|
||||
async function tryActivate(context: vscode.ExtensionContext): Promise<RustAnalyzerExtensionApi> {
|
||||
// We only support local folders, not eg. Live Share (`vlsl:` scheme), so don't activate if
|
||||
// only those are in use.
|
||||
// (r-a still somewhat works with Live Share, because commands are tunneled to the host)
|
||||
@ -65,351 +49,118 @@ async function tryActivate(context: vscode.ExtensionContext): Promise<RustAnalyz
|
||||
return {};
|
||||
}
|
||||
|
||||
const config = new Config(context);
|
||||
const state = new PersistentState(context.globalState);
|
||||
const serverPath = await bootstrap(context, config, state).catch((err) => {
|
||||
let message = "bootstrap error. ";
|
||||
const workspace: Workspace =
|
||||
folders.length === 0
|
||||
? {
|
||||
kind: "Detached Files",
|
||||
files: rustDocuments,
|
||||
}
|
||||
: { kind: "Workspace Folder" };
|
||||
|
||||
message += 'See the logs in "OUTPUT > Rust Analyzer Client" (should open automatically). ';
|
||||
message += 'To enable verbose logs use { "rust-analyzer.trace.extension": true }';
|
||||
|
||||
log.error("Bootstrap error", err);
|
||||
throw new Error(message);
|
||||
const ctx = new Ctx(context, workspace, createCommands());
|
||||
// VS Code doesn't show a notification when an extension fails to activate
|
||||
// so we do it ourselves.
|
||||
const api = await activateServer(ctx).catch((err) => {
|
||||
void vscode.window.showErrorMessage(
|
||||
`Cannot activate rust-analyzer extension: ${err.message}`
|
||||
);
|
||||
throw err;
|
||||
});
|
||||
await setContextValue(RUST_PROJECT_CONTEXT_NAME, true);
|
||||
return api;
|
||||
}
|
||||
|
||||
if (folders.length === 0) {
|
||||
ctx = await Ctx.create(config, context, serverPath, {
|
||||
kind: "Detached Files",
|
||||
files: rustDocuments,
|
||||
});
|
||||
} else {
|
||||
// Note: we try to start the server before we activate type hints so that it
|
||||
// registers its `onDidChangeDocument` handler before us.
|
||||
//
|
||||
// This a horribly, horribly wrong way to deal with this problem.
|
||||
ctx = await Ctx.create(config, context, serverPath, { kind: "Workspace Folder" });
|
||||
ctx.pushCleanup(activateTaskProvider(ctx.config));
|
||||
}
|
||||
await initCommonContext(context, ctx);
|
||||
|
||||
warnAboutExtensionConflicts();
|
||||
|
||||
if (config.typingContinueCommentsOnNewline) {
|
||||
ctx.pushCleanup(configureLanguage());
|
||||
async function activateServer(ctx: Ctx): Promise<RustAnalyzerExtensionApi> {
|
||||
if (ctx.workspace.kind === "Workspace Folder") {
|
||||
ctx.pushExtCleanup(activateTaskProvider(ctx.config));
|
||||
}
|
||||
|
||||
vscode.workspace.onDidChangeConfiguration(
|
||||
(_) =>
|
||||
ctx?.client
|
||||
?.sendNotification("workspace/didChangeConfiguration", { settings: "" })
|
||||
.catch(log.error),
|
||||
async (_) => {
|
||||
await ctx
|
||||
.clientFetcher()
|
||||
.client?.sendNotification("workspace/didChangeConfiguration", { settings: "" });
|
||||
},
|
||||
null,
|
||||
ctx.subscriptions
|
||||
);
|
||||
|
||||
await ctx.activate();
|
||||
return ctx.clientFetcher();
|
||||
}
|
||||
|
||||
function createCommands(): Record<string, CommandFactory> {
|
||||
return {
|
||||
client: ctx.client,
|
||||
onEnter: {
|
||||
enabled: commands.onEnter,
|
||||
disabled: (_) => () => vscode.commands.executeCommand("default:type", { text: "\n" }),
|
||||
},
|
||||
reload: {
|
||||
enabled: (ctx) => async () => {
|
||||
void vscode.window.showInformationMessage("Reloading rust-analyzer...");
|
||||
// FIXME: We should re-use the client, that is ctx.deactivate() if none of the configs have changed
|
||||
await ctx.stop();
|
||||
await ctx.activate();
|
||||
},
|
||||
disabled: (ctx) => async () => {
|
||||
void vscode.window.showInformationMessage("Reloading rust-analyzer...");
|
||||
await ctx.activate();
|
||||
},
|
||||
},
|
||||
startServer: {
|
||||
enabled: (ctx) => async () => {
|
||||
await ctx.activate();
|
||||
},
|
||||
disabled: (ctx) => async () => {
|
||||
await ctx.activate();
|
||||
},
|
||||
},
|
||||
stopServer: {
|
||||
enabled: (ctx) => async () => {
|
||||
// FIXME: We should re-use the client, that is ctx.deactivate() if none of the configs have changed
|
||||
await ctx.stop();
|
||||
ctx.setServerStatus({
|
||||
health: "ok",
|
||||
quiescent: true,
|
||||
message: "server is not running",
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
analyzerStatus: { enabled: commands.analyzerStatus },
|
||||
memoryUsage: { enabled: commands.memoryUsage },
|
||||
shuffleCrateGraph: { enabled: commands.shuffleCrateGraph },
|
||||
reloadWorkspace: { enabled: commands.reloadWorkspace },
|
||||
matchingBrace: { enabled: commands.matchingBrace },
|
||||
joinLines: { enabled: commands.joinLines },
|
||||
parentModule: { enabled: commands.parentModule },
|
||||
syntaxTree: { enabled: commands.syntaxTree },
|
||||
viewHir: { enabled: commands.viewHir },
|
||||
viewFileText: { enabled: commands.viewFileText },
|
||||
viewItemTree: { enabled: commands.viewItemTree },
|
||||
viewCrateGraph: { enabled: commands.viewCrateGraph },
|
||||
viewFullCrateGraph: { enabled: commands.viewFullCrateGraph },
|
||||
expandMacro: { enabled: commands.expandMacro },
|
||||
run: { enabled: commands.run },
|
||||
copyRunCommandLine: { enabled: commands.copyRunCommandLine },
|
||||
debug: { enabled: commands.debug },
|
||||
newDebugConfig: { enabled: commands.newDebugConfig },
|
||||
openDocs: { enabled: commands.openDocs },
|
||||
openCargoToml: { enabled: commands.openCargoToml },
|
||||
peekTests: { enabled: commands.peekTests },
|
||||
moveItemUp: { enabled: commands.moveItemUp },
|
||||
moveItemDown: { enabled: commands.moveItemDown },
|
||||
cancelFlycheck: { enabled: commands.cancelFlycheck },
|
||||
ssr: { enabled: commands.ssr },
|
||||
serverVersion: { enabled: commands.serverVersion },
|
||||
// Internal commands which are invoked by the server.
|
||||
applyActionGroup: { enabled: commands.applyActionGroup },
|
||||
applySnippetWorkspaceEdit: { enabled: commands.applySnippetWorkspaceEditCommand },
|
||||
debugSingle: { enabled: commands.debugSingle },
|
||||
gotoLocation: { enabled: commands.gotoLocation },
|
||||
linkToCommand: { enabled: commands.linkToCommand },
|
||||
resolveCodeAction: { enabled: commands.resolveCodeAction },
|
||||
runSingle: { enabled: commands.runSingle },
|
||||
showReferences: { enabled: commands.showReferences },
|
||||
};
|
||||
}
|
||||
|
||||
async function initCommonContext(context: vscode.ExtensionContext, ctx: Ctx) {
|
||||
// Register a "dumb" onEnter command for the case where server fails to
|
||||
// start.
|
||||
//
|
||||
// FIXME: refactor command registration code such that commands are
|
||||
// **always** registered, even if the server does not start. Use API like
|
||||
// this perhaps?
|
||||
//
|
||||
// ```TypeScript
|
||||
// registerCommand(
|
||||
// factory: (Ctx) => ((Ctx) => any),
|
||||
// fallback: () => any = () => vscode.window.showErrorMessage(
|
||||
// "rust-analyzer is not available"
|
||||
// ),
|
||||
// )
|
||||
const defaultOnEnter = vscode.commands.registerCommand("rust-analyzer.onEnter", () =>
|
||||
vscode.commands.executeCommand("default:type", { text: "\n" })
|
||||
);
|
||||
context.subscriptions.push(defaultOnEnter);
|
||||
|
||||
await setContextValue(RUST_PROJECT_CONTEXT_NAME, true);
|
||||
|
||||
// Commands which invokes manually via command palette, shortcut, etc.
|
||||
|
||||
// Reloading is inspired by @DanTup maneuver: https://github.com/microsoft/vscode/issues/45774#issuecomment-373423895
|
||||
ctx.registerCommand("reload", (_) => async () => {
|
||||
void vscode.window.showInformationMessage("Reloading rust-analyzer...");
|
||||
await doDeactivate();
|
||||
while (context.subscriptions.length > 0) {
|
||||
try {
|
||||
context.subscriptions.pop()!.dispose();
|
||||
} catch (err) {
|
||||
log.error("Dispose error:", err);
|
||||
}
|
||||
}
|
||||
await activate(context).catch(log.error);
|
||||
});
|
||||
|
||||
ctx.registerCommand("analyzerStatus", commands.analyzerStatus);
|
||||
ctx.registerCommand("memoryUsage", commands.memoryUsage);
|
||||
ctx.registerCommand("shuffleCrateGraph", commands.shuffleCrateGraph);
|
||||
ctx.registerCommand("reloadWorkspace", commands.reloadWorkspace);
|
||||
ctx.registerCommand("matchingBrace", commands.matchingBrace);
|
||||
ctx.registerCommand("joinLines", commands.joinLines);
|
||||
ctx.registerCommand("parentModule", commands.parentModule);
|
||||
ctx.registerCommand("syntaxTree", commands.syntaxTree);
|
||||
ctx.registerCommand("viewHir", commands.viewHir);
|
||||
ctx.registerCommand("viewFileText", commands.viewFileText);
|
||||
ctx.registerCommand("viewItemTree", commands.viewItemTree);
|
||||
ctx.registerCommand("viewCrateGraph", commands.viewCrateGraph);
|
||||
ctx.registerCommand("viewFullCrateGraph", commands.viewFullCrateGraph);
|
||||
ctx.registerCommand("expandMacro", commands.expandMacro);
|
||||
ctx.registerCommand("run", commands.run);
|
||||
ctx.registerCommand("copyRunCommandLine", commands.copyRunCommandLine);
|
||||
ctx.registerCommand("debug", commands.debug);
|
||||
ctx.registerCommand("newDebugConfig", commands.newDebugConfig);
|
||||
ctx.registerCommand("openDocs", commands.openDocs);
|
||||
ctx.registerCommand("openCargoToml", commands.openCargoToml);
|
||||
ctx.registerCommand("peekTests", commands.peekTests);
|
||||
ctx.registerCommand("moveItemUp", commands.moveItemUp);
|
||||
ctx.registerCommand("moveItemDown", commands.moveItemDown);
|
||||
ctx.registerCommand("cancelFlycheck", commands.cancelFlycheck);
|
||||
|
||||
defaultOnEnter.dispose();
|
||||
ctx.registerCommand("onEnter", commands.onEnter);
|
||||
|
||||
ctx.registerCommand("ssr", commands.ssr);
|
||||
ctx.registerCommand("serverVersion", commands.serverVersion);
|
||||
|
||||
// Internal commands which are invoked by the server.
|
||||
ctx.registerCommand("runSingle", commands.runSingle);
|
||||
ctx.registerCommand("debugSingle", commands.debugSingle);
|
||||
ctx.registerCommand("showReferences", commands.showReferences);
|
||||
ctx.registerCommand("applySnippetWorkspaceEdit", commands.applySnippetWorkspaceEditCommand);
|
||||
ctx.registerCommand("resolveCodeAction", commands.resolveCodeAction);
|
||||
ctx.registerCommand("applyActionGroup", commands.applyActionGroup);
|
||||
ctx.registerCommand("gotoLocation", commands.gotoLocation);
|
||||
|
||||
ctx.registerCommand("linkToCommand", commands.linkToCommand);
|
||||
}
|
||||
|
||||
export async function deactivate() {
|
||||
TRACE_OUTPUT_CHANNEL?.dispose();
|
||||
TRACE_OUTPUT_CHANNEL = null;
|
||||
OUTPUT_CHANNEL?.dispose();
|
||||
OUTPUT_CHANNEL = null;
|
||||
await doDeactivate();
|
||||
}
|
||||
|
||||
async function doDeactivate() {
|
||||
await setContextValue(RUST_PROJECT_CONTEXT_NAME, undefined);
|
||||
await ctx?.client.stop();
|
||||
ctx = undefined;
|
||||
}
|
||||
|
||||
async function bootstrap(
|
||||
context: vscode.ExtensionContext,
|
||||
config: Config,
|
||||
state: PersistentState
|
||||
): Promise<string> {
|
||||
const path = await getServer(context, config, state);
|
||||
if (!path) {
|
||||
throw new Error(
|
||||
"Rust Analyzer Language Server is not available. " +
|
||||
"Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
|
||||
);
|
||||
}
|
||||
|
||||
log.info("Using server binary at", path);
|
||||
|
||||
if (!isValidExecutable(path)) {
|
||||
if (config.serverPath) {
|
||||
throw new Error(`Failed to execute ${path} --version. \`config.server.path\` or \`config.serverPath\` has been set explicitly.\
|
||||
Consider removing this config or making a valid server binary available at that path.`);
|
||||
} else {
|
||||
throw new Error(`Failed to execute ${path} --version`);
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
async function patchelf(dest: vscode.Uri): Promise<void> {
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: "Patching rust-analyzer for NixOS",
|
||||
},
|
||||
async (progress, _) => {
|
||||
const expression = `
|
||||
{srcStr, pkgs ? import <nixpkgs> {}}:
|
||||
pkgs.stdenv.mkDerivation {
|
||||
name = "rust-analyzer";
|
||||
src = /. + srcStr;
|
||||
phases = [ "installPhase" "fixupPhase" ];
|
||||
installPhase = "cp $src $out";
|
||||
fixupPhase = ''
|
||||
chmod 755 $out
|
||||
patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $out
|
||||
'';
|
||||
}
|
||||
`;
|
||||
const origFile = vscode.Uri.file(dest.fsPath + "-orig");
|
||||
await vscode.workspace.fs.rename(dest, origFile, { overwrite: true });
|
||||
try {
|
||||
progress.report({ message: "Patching executable", increment: 20 });
|
||||
await new Promise((resolve, reject) => {
|
||||
const handle = exec(
|
||||
`nix-build -E - --argstr srcStr '${origFile.fsPath}' -o '${dest.fsPath}'`,
|
||||
(err, stdout, stderr) => {
|
||||
if (err != null) {
|
||||
reject(Error(stderr));
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
}
|
||||
);
|
||||
handle.stdin?.write(expression);
|
||||
handle.stdin?.end();
|
||||
});
|
||||
} finally {
|
||||
await vscode.workspace.fs.delete(origFile);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function getServer(
|
||||
context: vscode.ExtensionContext,
|
||||
config: Config,
|
||||
state: PersistentState
|
||||
): Promise<string | undefined> {
|
||||
const explicitPath = serverPath(config);
|
||||
if (explicitPath) {
|
||||
if (explicitPath.startsWith("~/")) {
|
||||
return os.homedir() + explicitPath.slice("~".length);
|
||||
}
|
||||
return explicitPath;
|
||||
}
|
||||
if (config.package.releaseTag === null) return "rust-analyzer";
|
||||
|
||||
const ext = process.platform === "win32" ? ".exe" : "";
|
||||
const bundled = vscode.Uri.joinPath(context.extensionUri, "server", `rust-analyzer${ext}`);
|
||||
const bundledExists = await vscode.workspace.fs.stat(bundled).then(
|
||||
() => true,
|
||||
() => false
|
||||
);
|
||||
if (bundledExists) {
|
||||
let server = bundled;
|
||||
if (await isNixOs()) {
|
||||
await vscode.workspace.fs.createDirectory(config.globalStorageUri).then();
|
||||
const dest = vscode.Uri.joinPath(config.globalStorageUri, `rust-analyzer${ext}`);
|
||||
let exists = await vscode.workspace.fs.stat(dest).then(
|
||||
() => true,
|
||||
() => false
|
||||
);
|
||||
if (exists && config.package.version !== state.serverVersion) {
|
||||
await vscode.workspace.fs.delete(dest);
|
||||
exists = false;
|
||||
}
|
||||
if (!exists) {
|
||||
await vscode.workspace.fs.copy(bundled, dest);
|
||||
await patchelf(dest);
|
||||
}
|
||||
server = dest;
|
||||
}
|
||||
await state.updateServerVersion(config.package.version);
|
||||
return server.fsPath;
|
||||
}
|
||||
|
||||
await state.updateServerVersion(undefined);
|
||||
await vscode.window.showErrorMessage(
|
||||
"Unfortunately we don't ship binaries for your platform yet. " +
|
||||
"You need to manually clone the rust-analyzer repository and " +
|
||||
"run `cargo xtask install --server` to build the language server from sources. " +
|
||||
"If you feel that your platform should be supported, please create an issue " +
|
||||
"about that [here](https://github.com/rust-lang/rust-analyzer/issues) and we " +
|
||||
"will consider it."
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function serverPath(config: Config): string | null {
|
||||
return process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
|
||||
}
|
||||
|
||||
async function isNixOs(): Promise<boolean> {
|
||||
try {
|
||||
const contents = (
|
||||
await vscode.workspace.fs.readFile(vscode.Uri.file("/etc/os-release"))
|
||||
).toString();
|
||||
const idString = contents.split("\n").find((a) => a.startsWith("ID=")) || "ID=linux";
|
||||
return idString.indexOf("nixos") !== -1;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function warnAboutExtensionConflicts() {
|
||||
if (vscode.extensions.getExtension("rust-lang.rust")) {
|
||||
vscode.window
|
||||
.showWarningMessage(
|
||||
`You have both the rust-analyzer (rust-lang.rust-analyzer) and Rust (rust-lang.rust) ` +
|
||||
"plugins enabled. These are known to conflict and cause various functions of " +
|
||||
"both plugins to not work correctly. You should disable one of them.",
|
||||
"Got it"
|
||||
)
|
||||
.then(() => {}, console.error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up additional language configuration that's impossible to do via a
|
||||
* separate language-configuration.json file. See [1] for more information.
|
||||
*
|
||||
* [1]: https://github.com/Microsoft/vscode/issues/11514#issuecomment-244707076
|
||||
*/
|
||||
function configureLanguage(): vscode.Disposable {
|
||||
const indentAction = vscode.IndentAction.None;
|
||||
return vscode.languages.setLanguageConfiguration("rust", {
|
||||
onEnterRules: [
|
||||
{
|
||||
// Doc single-line comment
|
||||
// e.g. ///|
|
||||
beforeText: /^\s*\/{3}.*$/,
|
||||
action: { indentAction, appendText: "/// " },
|
||||
},
|
||||
{
|
||||
// Parent doc single-line comment
|
||||
// e.g. //!|
|
||||
beforeText: /^\s*\/{2}\!.*$/,
|
||||
action: { indentAction, appendText: "//! " },
|
||||
},
|
||||
{
|
||||
// Begins an auto-closed multi-line comment (standard or parent doc)
|
||||
// e.g. /** | */ or /*! | */
|
||||
beforeText: /^\s*\/\*(\*|\!)(?!\/)([^\*]|\*(?!\/))*$/,
|
||||
afterText: /^\s*\*\/$/,
|
||||
action: { indentAction: vscode.IndentAction.IndentOutdent, appendText: " * " },
|
||||
},
|
||||
{
|
||||
// Begins a multi-line comment (standard or parent doc)
|
||||
// e.g. /** ...| or /*! ...|
|
||||
beforeText: /^\s*\/\*(\*|\!)(?!\/)([^\*]|\*(?!\/))*$/,
|
||||
action: { indentAction, appendText: " * " },
|
||||
},
|
||||
{
|
||||
// Continues a multi-line comment
|
||||
// e.g. * ...|
|
||||
beforeText: /^(\ \ )*\ \*(\ ([^\*]|\*(?!\/))*)?$/,
|
||||
action: { indentAction, appendText: "* " },
|
||||
},
|
||||
{
|
||||
// Dedents after closing a multi-line comment
|
||||
// e.g. */|
|
||||
beforeText: /^(\ \ )*\ \*\/\s*$/,
|
||||
action: { indentAction, removeText: 1 },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
@ -18,9 +18,9 @@ export async function selectRunnable(
|
||||
showButtons: boolean = true
|
||||
): Promise<RunnableQuickPick | undefined> {
|
||||
const editor = ctx.activeRustEditor;
|
||||
const client = ctx.client;
|
||||
if (!editor || !client) return;
|
||||
if (!editor) return;
|
||||
|
||||
const client = await ctx.getClient();
|
||||
const textDocument: lc.TextDocumentIdentifier = {
|
||||
uri: editor.document.uri.toString(),
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user