From f310d4cbb4ac792ecfe2bb680c869bb732e9e7af Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Tue, 27 Sep 2022 18:22:06 +0200 Subject: [PATCH 01/31] Don't auto-publish lib crates --- .github/workflows/publish.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a4497f49e3c..73e62ab32c6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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 From d7fb8d51d6269c2119254a886d883ac7e051938e Mon Sep 17 00:00:00 2001 From: DropDemBits Date: Mon, 10 Oct 2022 14:22:01 -0400 Subject: [PATCH 02/31] Migrate assists to format args captures, part 2 --- .../src/handlers/generate_constant.rs | 12 ++-- .../generate_default_from_enum_variant.rs | 5 +- .../src/handlers/generate_default_from_new.rs | 61 ++++++++++--------- .../src/handlers/generate_delegate_methods.rs | 12 ++-- .../src/handlers/generate_deref.rs | 12 ++-- .../generate_documentation_template.rs | 52 +++++++++------- .../src/handlers/generate_enum_is_method.rs | 16 ++--- .../generate_enum_projection_method.rs | 37 +++++------ .../handlers/generate_from_impl_for_enum.rs | 19 +++--- .../src/handlers/generate_function.rs | 32 +++++----- .../src/handlers/generate_getter.rs | 18 +++--- .../ide-assists/src/handlers/generate_impl.rs | 2 +- .../ide-assists/src/handlers/generate_new.rs | 13 ++-- .../src/handlers/generate_setter.rs | 16 ++--- 14 files changed, 150 insertions(+), 157 deletions(-) diff --git a/crates/ide-assists/src/handlers/generate_constant.rs b/crates/ide-assists/src/handlers/generate_constant.rs index eaa6de73eb3..ccdfcb0d9e4 100644 --- a/crates/ide-assists/src/handlers/generate_constant.rs +++ b/crates/ide-assists/src/handlers/generate_constant.rs @@ -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 { 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())), diff --git a/crates/ide-assists/src/handlers/generate_default_from_enum_variant.rs b/crates/ide-assists/src/handlers/generate_default_from_enum_variant.rs index 5e9995a9866..a6e3d49e0d1 100644 --- a/crates/ide-assists/src/handlers/generate_default_from_enum_variant.rs +++ b/crates/ide-assists/src/handlers/generate_default_from_enum_variant.rs @@ -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); }, diff --git a/crates/ide-assists/src/handlers/generate_default_from_new.rs b/crates/ide-assists/src/handlers/generate_default_from_new.rs index cbd33de19ed..49d9fd707ff 100644 --- a/crates/ide-assists/src/handlers/generate_default_from_new.rs +++ b/crates/ide-assists/src/handlers/generate_default_from_new.rs @@ -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}}"); } } diff --git a/crates/ide-assists/src/handlers/generate_delegate_methods.rs b/crates/ide-assists/src/handlers/generate_delegate_methods.rs index 85b193663a0..e5df3cbdaa9 100644 --- a/crates/ide-assists/src/handlers/generate_delegate_methods.rs +++ b/crates/ide-assists/src/handlers/generate_delegate_methods.rs @@ -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()) + (format!("{field_name}"), field_ty, field.syntax().text_range()) } None => { let field = ctx.find_node_at_offset::()?; let field_list = ctx.find_node_at_offset::()?; 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()) + (format!("{field_list_index}"), field_ty, field.syntax().text_range()) } }; @@ -78,10 +78,12 @@ pub(crate) fn generate_delegate_methods(acc: &mut Assists, ctx: &AssistContext<' 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 method_name = method.name(ctx.db()); + acc.add_group( &GroupLabel("Generate delegate methods…".to_owned()), AssistId("generate_delegate_methods", AssistKind::Generate), - format!("Generate delegate for `{}.{}()`", field_name, method.name(ctx.db())), + format!("Generate delegate for `{field_name}.{method_name}()`"), target, |builder| { // Create the function @@ -151,12 +153,12 @@ 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 => { let offset = strukt.syntax().text_range().end(); - let snippet = format!("\n\n{}", impl_def.syntax()); + let snippet = format!("\n\n{impl_def}"); builder.insert(offset, snippet); } } diff --git a/crates/ide-assists/src/handlers/generate_deref.rs b/crates/ide-assists/src/handlers/generate_deref.rs index 8f4405a8c86..55b7afb3d3b 100644 --- a/crates/ide-assists/src/handlers/generate_deref.rs +++ b/crates/ide-assists/src/handlers/generate_deref.rs @@ -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); diff --git a/crates/ide-assists/src/handlers/generate_documentation_template.rs b/crates/ide-assists/src/handlers/generate_documentation_template.rs index c91141f8eb5..b8415c72a2a 100644 --- a/crates/ide-assists/src/handlers/generate_documentation_template.rs +++ b/crates/ide-assists/src/handlers/generate_documentation_template.rs @@ -139,40 +139,44 @@ fn make_example_for_fn(ast_func: &ast::Fn, ctx: &AssistContext<'_>) -> Option) -> Option) -> Option None, }; @@ -228,7 +235,9 @@ fn introduction_builder(ast_func: &ast::Fn, ctx: &AssistContext<'_>) -> Option 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 { .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}")), } } diff --git a/crates/ide-assists/src/handlers/generate_enum_is_method.rs b/crates/ide-assists/src/handlers/generate_enum_is_method.rs index 52d27d8a7d6..dba98f7a04e 100644 --- a/crates/ide-assists/src/handlers/generate_enum_is_method.rs +++ b/crates/ide-assists/src/handlers/generate_enum_is_method.rs @@ -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); diff --git a/crates/ide-assists/src/handlers/generate_enum_projection_method.rs b/crates/ide-assists/src/handlers/generate_enum_projection_method.rs index b19aa0f652a..402ab1ee79d 100644 --- a/crates/ide-assists/src/handlers/generate_enum_projection_method.rs +++ b/crates/ide-assists/src/handlers/generate_enum_projection_method.rs @@ -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::()?; 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,8 +144,7 @@ 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)?; @@ -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); }, diff --git a/crates/ide-assists/src/handlers/generate_from_impl_for_enum.rs b/crates/ide-assists/src/handlers/generate_from_impl_for_enum.rs index 507ea012bab..7c81d2c6a6c 100644 --- a/crates/ide-assists/src/handlers/generate_from_impl_for_enum.rs +++ b/crates/ide-assists/src/handlers/generate_from_impl_for_enum.rs @@ -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); diff --git a/crates/ide-assists/src/handlers/generate_function.rs b/crates/ide-assists/src/handlers/generate_function.rs index 8b67982f915..e6cc8cacedd 100644 --- a/crates/ide-assists/src/handlers/generate_function.rs +++ b/crates/ide-assists/src/handlers/generate_function.rs @@ -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 { @@ -212,23 +212,26 @@ struct FunctionTemplate { impl FunctionTemplate { fn to_string(&self, cap: Option) -> 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}"); } }; diff --git a/crates/ide-assists/src/handlers/generate_getter.rs b/crates/ide-assists/src/handlers/generate_getter.rs index 76fcef0cad9..09eedb11fbc 100644 --- a/crates/ide-assists/src/handlers/generate_getter.rs +++ b/crates/ide-assists/src/handlers/generate_getter.rs @@ -108,9 +108,9 @@ pub(crate) fn generate_getter_impl( 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 (ty, body) = if mutable { - (format!("&mut {}", field_ty), format!("&mut self.{}", field_name)) + (format!("&mut {field_ty}"), format!("&mut self.{field_name}")) } else { (|| { let krate = ctx.sema.scope(field_ty.syntax())?.krate(); @@ -126,19 +126,15 @@ pub(crate) fn generate_getter_impl( ) }) })() - .unwrap_or_else(|| (format!("&{}", field_ty), format!("&self.{}", field_name))) + .unwrap_or_else(|| (format!("&{field_ty}"), format!("&self.{field_name}"))) }; + let mut_ = mutable.then(|| "mut ").unwrap_or_default(); format_to!( buf, - " {}fn {}(&{}self) -> {} {{ - {} - }}", - vis, - fn_name, - mutable.then(|| "mut ").unwrap_or_default(), - ty, - body, + " {vis}fn {fn_name}(&{mut_}self) -> {ty} {{ + {body} + }}" ); let start_offset = impl_def diff --git a/crates/ide-assists/src/handlers/generate_impl.rs b/crates/ide-assists/src/handlers/generate_impl.rs index 307cea3d0a4..9af26c04eb4 100644 --- a/crates/ide-assists/src/handlers/generate_impl.rs +++ b/crates/ide-assists/src/handlers/generate_impl.rs @@ -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(); diff --git a/crates/ide-assists/src/handlers/generate_new.rs b/crates/ide-assists/src/handlers/generate_new.rs index 9cda74d9e0d..c441a2dc48e 100644 --- a/crates/ide-assists/src/handlers/generate_new.rs +++ b/crates/ide-assists/src/handlers/generate_new.rs @@ -51,11 +51,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 +74,7 @@ pub(crate) fn generate_new(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option &ty, )?; - Some(format!("{}: {}", f.name()?.syntax(), expr)) + Some(format!("{name}: {expr}")) }) .collect::>(); @@ -81,7 +83,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 +106,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)) diff --git a/crates/ide-assists/src/handlers/generate_setter.rs b/crates/ide-assists/src/handlers/generate_setter.rs index 2a7ad6ce368..e139bcb1a1b 100644 --- a/crates/ide-assists/src/handlers/generate_setter.rs +++ b/crates/ide-assists/src/handlers/generate_setter.rs @@ -39,7 +39,7 @@ pub(crate) fn generate_setter(acc: &mut Assists, ctx: &AssistContext<'_>) -> Opt let impl_def = find_struct_impl( ctx, &ast::Adt::Struct(strukt.clone()), - format!("set_{}", fn_name).as_str(), + format!("set_{fn_name}").as_str(), )?; let target = field.syntax().text_range(); @@ -55,18 +55,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 From d5f467aa4a6b28ffa6acb7673bfea472fdcd64c8 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Sun, 16 Oct 2022 17:11:24 +0200 Subject: [PATCH 03/31] Substitute some VSCode variables in the VSCode client --- editors/code/src/client.ts | 26 ++++++++++-- editors/code/src/config.ts | 85 +++++++++++++++++++++++++------------- 2 files changed, 79 insertions(+), 32 deletions(-) diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts index 05d4d08f70b..45a7970b217 100644 --- a/editors/code/src/client.ts +++ b/editors/code/src/client.ts @@ -5,7 +5,7 @@ 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 { substituteVariablesInEnv, substituteVSCodeVariables } from "./config"; import { outputChannel, traceOutputChannel } from "./main"; import { randomUUID } from "crypto"; @@ -83,15 +83,17 @@ export async function createClient( debug: run, }; - let initializationOptions = vscode.workspace.getConfiguration("rust-analyzer"); + let rawInitializationOptions = vscode.workspace.getConfiguration("rust-analyzer"); if (workspace.kind === "Detached Files") { - initializationOptions = { + rawInitializationOptions = { detachedFiles: workspace.files.map((file) => file.uri.fsPath), - ...initializationOptions, + ...rawInitializationOptions, }; } + const initializationOptions = substituteVSCodeVariables(rawInitializationOptions); + const clientOptions: lc.LanguageClientOptions = { documentSelector: [{ scheme: "file", language: "rust" }], initializationOptions, @@ -99,6 +101,22 @@ export async function createClient( traceOutputChannel: traceOutputChannel(), outputChannel: outputChannel(), middleware: { + workspace: { + 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, diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index 15846a5e864..199db6e30fc 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -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"; @@ -187,6 +188,37 @@ export class Config { } } +const VarRegex = new RegExp(/\$\{(.+?)\}/g); + +export function substituteVSCodeVariableInString(val: string): string { + return val.replaceAll(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(); // vscode uses `env:ENV_NAME` for env vars resolution, and it's easier @@ -233,7 +265,7 @@ export function substituteVariablesInEnv(env: Env): Env { } } else { envWithDeps[dep] = { - value: computeVscodeVar(dep), + value: computeVscodeVar(dep) || "${" + dep + "}", deps: [], }; } @@ -264,37 +296,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 +337,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; } } From 6f435977df34e722b45d787d7eadac23cf5b040e Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 17 Oct 2022 14:20:14 +0200 Subject: [PATCH 04/31] Refactor language client handling --- editors/code/package-lock.json | 78 ++++---- editors/code/package.json | 4 +- editors/code/src/ast_inspector.ts | 8 +- editors/code/src/bootstrap.ts | 148 +++++++++++++++ editors/code/src/client.ts | 43 +---- editors/code/src/commands.ts | 183 +++++++++---------- editors/code/src/ctx.ts | 135 +++++++++++--- editors/code/src/main.ts | 294 ++++++------------------------ 8 files changed, 456 insertions(+), 437 deletions(-) create mode 100644 editors/code/src/bootstrap.ts diff --git a/editors/code/package-lock.json b/editors/code/package-lock.json index 3ff4b6897a1..192d8fabc31 100644 --- a/editors/code/package-lock.json +++ b/editors/code/package-lock.json @@ -11,11 +11,11 @@ "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", - "@types/vscode": "~1.66.0", + "@types/vscode": "~1.72.0", "@typescript-eslint/eslint-plugin": "^5.30.5", "@typescript-eslint/parser": "^5.30.5", "@vscode/test-electron": "^2.1.5", @@ -141,9 +141,9 @@ "dev": true }, "node_modules/@types/vscode": { - "version": "1.66.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.66.0.tgz", - "integrity": "sha512-ZfJck4M7nrGasfs4A4YbUoxis3Vu24cETw3DERsNYtDZmYSYtk6ljKexKFKhImO/ZmY6ZMsmegu2FPkXoUFImA==", + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.72.0.tgz", + "integrity": "sha512-WvHluhUo+lQvE3I4wUagRpnkHuysB4qSyOQUyIAS9n9PYMJjepzTUD8Jyks0YeXoPD0UGctjqp2u84/b3v6Ydw==", "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -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", @@ -4039,9 +4039,9 @@ "dev": true }, "@types/vscode": { - "version": "1.66.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.66.0.tgz", - "integrity": "sha512-ZfJck4M7nrGasfs4A4YbUoxis3Vu24cETw3DERsNYtDZmYSYtk6ljKexKFKhImO/ZmY6ZMsmegu2FPkXoUFImA==", + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.72.0.tgz", + "integrity": "sha512-WvHluhUo+lQvE3I4wUagRpnkHuysB4qSyOQUyIAS9n9PYMJjepzTUD8Jyks0YeXoPD0UGctjqp2u84/b3v6Ydw==", "dev": true }, "@typescript-eslint/eslint-plugin": { @@ -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", diff --git a/editors/code/package.json b/editors/code/package.json index f1dd3aa79ff..da4bac5ad84 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -37,11 +37,11 @@ "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", - "@types/vscode": "~1.66.0", + "@types/vscode": "~1.72.0", "@typescript-eslint/eslint-plugin": "^5.30.5", "@typescript-eslint/parser": "^5.30.5", "@vscode/test-electron": "^2.1.5", diff --git a/editors/code/src/ast_inspector.ts b/editors/code/src/ast_inspector.ts index e57fb20e2cf..2a2c9326b6a 100644 --- a/editors/code/src/ast_inspector.ts +++ b/editors/code/src/ast_inspector.ts @@ -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, @@ -53,7 +55,7 @@ export class AstInspector implements vscode.HoverProvider, vscode.DefinitionProv ctx.subscriptions ); - ctx.pushCleanup(this); + ctx.pushExtCleanup(this); } dispose() { this.setRustEditor(undefined); diff --git a/editors/code/src/bootstrap.ts b/editors/code/src/bootstrap.ts new file mode 100644 index 00000000000..374c3b8144c --- /dev/null +++ b/editors/code/src/bootstrap.ts @@ -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 { + 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 { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Patching rust-analyzer for NixOS", + }, + async (progress, _) => { + const expression = ` + {srcStr, pkgs ? import {}}: + 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 { + 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 { + 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; + } +} diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts index 45a7970b217..3408a2ee84e 100644 --- a/editors/code/src/client.ts +++ b/editors/code/src/client.ts @@ -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, substituteVSCodeVariables } from "./config"; -import { outputChannel, traceOutputChannel } from "./main"; +import { substituteVSCodeVariables } from "./config"; import { randomUUID } from "crypto"; export interface Env { @@ -65,41 +63,17 @@ 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 { - // '.' 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 rawInitializationOptions = vscode.workspace.getConfiguration("rust-analyzer"); - - if (workspace.kind === "Detached Files") { - rawInitializationOptions = { - detachedFiles: workspace.files.map((file) => file.uri.fsPath), - ...rawInitializationOptions, - }; - } - - const initializationOptions = substituteVSCodeVariables(rawInitializationOptions); - const clientOptions: lc.LanguageClientOptions = { documentSelector: [{ scheme: "file", language: "rust" }], initializationOptions, diagnosticCollectionName: "rustc", - traceOutputChannel: traceOutputChannel(), - outputChannel: outputChannel(), + traceOutputChannel, + outputChannel, middleware: { workspace: { async configuration( @@ -273,6 +247,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; diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts index b9ad525e361..cbdeb28c99f 100644 --- a/editors/code/src/commands.ts +++ b/editors/code/src/commands.ts @@ -21,16 +21,16 @@ export function analyzerStatus(ctx: Ctx): Cmd { readonly uri = vscode.Uri.parse("rust-analyzer-status://status"); readonly eventEmitter = new vscode.EventEmitter(); - provideTextDocumentContent(_uri: vscode.Uri): vscode.ProviderResult { + async provideTextDocumentContent(_uri: vscode.Uri): Promise { 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 { @@ -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 { 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 { @@ -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 ); @@ -354,21 +350,22 @@ export function syntaxTree(ctx: Ctx): Cmd { } } - provideTextDocumentContent( + async provideTextDocumentContent( uri: vscode.Uri, ct: vscode.CancellationToken - ): vscode.ProviderResult { + ): Promise { 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 { @@ -378,10 +375,10 @@ export function syntaxTree(ctx: Ctx): Cmd { void new AstInspector(ctx); - ctx.pushCleanup( + ctx.pushExtCleanup( vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-syntax-tree", tdcp) ); - ctx.pushCleanup( + ctx.pushExtCleanup( vscode.languages.setLanguageConfiguration("ra_syntax_tree", { brackets: [["[", ")"]], }) @@ -437,14 +434,14 @@ export function viewHir(ctx: Ctx): Cmd { } } - provideTextDocumentContent( + async provideTextDocumentContent( _uri: vscode.Uri, ct: vscode.CancellationToken - ): vscode.ProviderResult { + ): Promise { 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 +456,7 @@ export function viewHir(ctx: Ctx): Cmd { } })(); - ctx.pushCleanup( + ctx.pushExtCleanup( vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-hir", tdcp) ); @@ -503,13 +500,13 @@ export function viewFileText(ctx: Ctx): Cmd { } } - provideTextDocumentContent( + async provideTextDocumentContent( _uri: vscode.Uri, ct: vscode.CancellationToken - ): vscode.ProviderResult { + ): Promise { 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 +519,7 @@ export function viewFileText(ctx: Ctx): Cmd { } })(); - ctx.pushCleanup( + ctx.pushExtCleanup( vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-file-text", tdcp) ); @@ -566,13 +563,13 @@ export function viewItemTree(ctx: Ctx): Cmd { } } - provideTextDocumentContent( + async provideTextDocumentContent( _uri: vscode.Uri, ct: vscode.CancellationToken - ): vscode.ProviderResult { + ): Promise { 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 +584,7 @@ export function viewItemTree(ctx: Ctx): Cmd { } })(); - ctx.pushCleanup( + ctx.pushExtCleanup( vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-item-tree", tdcp) ); @@ -618,8 +615,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 +687,13 @@ export function expandMacro(ctx: Ctx): Cmd { eventEmitter = new vscode.EventEmitter(); async provideTextDocumentContent(_uri: vscode.Uri): Promise { 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 +709,7 @@ export function expandMacro(ctx: Ctx): Cmd { } })(); - ctx.pushCleanup( + ctx.pushExtCleanup( vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-expand-macro", tdcp) ); @@ -724,11 +721,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 +742,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 +759,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 +790,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 +843,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 +932,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); } diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index 26510011d43..d4f5ab3c88c 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts @@ -2,9 +2,9 @@ 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"; export type Workspace = @@ -17,35 +17,118 @@ export type Workspace = }; export class Ctx { - private constructor( - readonly config: Config, - private readonly extCtx: vscode.ExtensionContext, - readonly client: lc.LanguageClient, - readonly serverPath: string, - readonly statusBar: vscode.StatusBarItem - ) {} + private client: lc.LanguageClient | undefined; + readonly config: Config; + serverPath: string; + readonly statusBar: vscode.StatusBarItem; - static async create( + traceOutputChannel: vscode.OutputChannel | undefined; + outputChannel: vscode.OutputChannel | undefined; + + serverOptions: + | { + run: lc.Executable; + debug: lc.Executable; + } + | undefined; + workspace: Workspace; + + constructor( + readonly extCtx: vscode.ExtensionContext, config: Config, - extCtx: vscode.ExtensionContext, serverPath: string, workspace: Workspace - ): Promise { - const client = await createClient(serverPath, workspace, config.serverExtraEnv); + ) { + this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); + extCtx.subscriptions.push(this.statusBar); + extCtx.subscriptions.push({ + dispose() { + this.dispose(); + }, + }); + this.statusBar.text = "rust-analyzer"; + this.statusBar.tooltip = "ready"; + this.statusBar.command = "rust-analyzer.analyzerStatus"; + this.statusBar.show(); + this.serverPath = serverPath; + this.config = config; + this.workspace = workspace; + } - 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(); + clientFetcher() { + return { + get client(): lc.LanguageClient | undefined { + return this.client; + }, + }; + } - const res = new Ctx(config, extCtx, client, serverPath, statusBar); + async getClient() { + if (!this.traceOutputChannel) { + this.traceOutputChannel = vscode.window.createOutputChannel( + "Rust Analyzer Language Server Trace" + ); + } + if (!this.outputChannel) { + this.outputChannel = vscode.window.createOutputChannel("Rust Analyzer Language Server"); + } + if (!this.serverOptions) { + log.info("Creating server options client"); + const newEnv = substituteVariablesInEnv( + Object.assign({}, process.env, this.config.serverExtraEnv) + ); + const run: lc.Executable = { + command: this.serverPath, + options: { env: newEnv }, + }; + this.serverOptions = { + run, + debug: run, + }; + } else { + this.serverOptions.run.command = this.serverPath; + this.serverOptions.debug.command = this.serverPath; + } + if (!this.client) { + log.info("Creating language client"); + let rawInitializationOptions = vscode.workspace.getConfiguration("rust-analyzer"); - res.pushCleanup(client.start()); - await client.onReady(); - client.onNotification(ra.serverStatus, (params) => res.setServerStatus(params)); - return res; + 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, + this.serverOptions + ); + 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(); + return client; + } + + async deactivate() { + log.info("Deactivating language client"); + await this.client?.stop(); + } + + async disposeClient() { + log.info("Deactivating language client"); + await this.client?.dispose(); + this.client = undefined; } get activeRustEditor(): RustEditor | undefined { @@ -61,7 +144,7 @@ export class Ctx { const fullName = `rust-analyzer.${name}`; const cmd = factory(this); const d = vscode.commands.registerCommand(fullName, cmd); - this.pushCleanup(d); + this.pushExtCleanup(d); } get extensionPath(): string { @@ -111,7 +194,7 @@ export class Ctx { statusBar.text = `${icon}rust-analyzer`; } - pushCleanup(d: Disposable) { + pushExtCleanup(d: Disposable) { this.extCtx.subscriptions.push(d); } } diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index 41bde4195e0..4ff27e0b551 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -1,53 +1,36 @@ 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 { Ctx, Workspace } from "./ctx"; +import { log, isRustDocument } from "./util"; import { PersistentState } from "./persistent_state"; import { activateTaskProvider } from "./tasks"; import { setContextValue } from "./util"; -import { exec } from "child_process"; - -let ctx: Ctx | undefined; +import { bootstrap } from "./bootstrap"; +import { Config } from "./config"; 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 { - client?: lc.LanguageClient; + // FIXME: this should be non-optional + readonly client?: lc.LanguageClient; } export async function activate( context: vscode.ExtensionContext ): Promise { - // 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 { // 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,8 +48,17 @@ async function tryActivate(context: vscode.ExtensionContext): Promise { let message = "bootstrap error. "; @@ -77,42 +69,43 @@ async function tryActivate(context: vscode.ExtensionContext): Promise { + void vscode.window.showErrorMessage(`Cannot activate rust-analyzer: ${err.message}`); + throw err; + }); +} + +async function activateServer(ctx: Ctx): Promise { + if (ctx.workspace.kind === "Workspace Folder") { + ctx.pushExtCleanup(activateTaskProvider(ctx.config)); } - await initCommonContext(context, ctx); - warnAboutExtensionConflicts(); + await ctx.activate(); + await initCommonContext(ctx); - if (config.typingContinueCommentsOnNewline) { - ctx.pushCleanup(configureLanguage()); + if (ctx.config.typingContinueCommentsOnNewline) { + ctx.pushExtCleanup(configureLanguage()); } vscode.workspace.onDidChangeConfiguration( (_) => - ctx?.client - ?.sendNotification("workspace/didChangeConfiguration", { settings: "" }) + ctx + .getClient() + .then((it) => + it.sendNotification("workspace/didChangeConfiguration", { settings: "" }) + ) .catch(log.error), null, ctx.subscriptions ); - return { - client: ctx.client, - }; + return ctx.clientFetcher(); } -async function initCommonContext(context: vscode.ExtensionContext, ctx: Ctx) { +async function initCommonContext(ctx: Ctx) { // Register a "dumb" onEnter command for the case where server fails to // start. // @@ -130,24 +123,15 @@ async function initCommonContext(context: vscode.ExtensionContext, ctx: Ctx) { const defaultOnEnter = vscode.commands.registerCommand("rust-analyzer.onEnter", () => vscode.commands.executeCommand("default:type", { text: "\n" }) ); - context.subscriptions.push(defaultOnEnter); + ctx.pushExtCleanup(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); + await ctx.disposeClient(); + await ctx.activate(); }); ctx.registerCommand("analyzerStatus", commands.analyzerStatus); @@ -175,9 +159,6 @@ async function initCommonContext(context: vscode.ExtensionContext, ctx: Ctx) { 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); @@ -191,176 +172,9 @@ async function initCommonContext(context: vscode.ExtensionContext, ctx: Ctx) { 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 { - 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 { - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: "Patching rust-analyzer for NixOS", - }, - async (progress, _) => { - const expression = ` - {srcStr, pkgs ? import {}}: - 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 { - 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 { - 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); - } + defaultOnEnter.dispose(); + ctx.registerCommand("onEnter", commands.onEnter); } /** From 8aaafddee8ad709438a663084e6eec8f458bfd50 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 17 Oct 2022 14:53:46 +0200 Subject: [PATCH 05/31] Properly reload changed configs for server start --- editors/code/src/commands.ts | 4 +++ editors/code/src/config.ts | 4 +-- editors/code/src/ctx.ts | 58 +++++++++++++++++++----------------- editors/code/src/main.ts | 42 ++++++++++---------------- 4 files changed, 51 insertions(+), 57 deletions(-) diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts index cbdeb28c99f..cb088fc6c68 100644 --- a/editors/code/src/commands.ts +++ b/editors/code/src/commands.ts @@ -310,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(); diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index 199db6e30fc..10e243dc896 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -14,8 +14,6 @@ export class Config { 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", @@ -23,6 +21,8 @@ export class Config { private readonly requiresReloadOpts = [ "cargo", "procMacro", + "serverPath", + "server", "files", "lens", // works as lens.* ] diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index d4f5ab3c88c..f62ccd1a652 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts @@ -6,6 +6,8 @@ import { Config, substituteVariablesInEnv, substituteVSCodeVariables } from "./c import { createClient } from "./client"; import { isRustEditor, log, RustEditor } from "./util"; import { ServerStatusParams } from "./lsp_ext"; +import { PersistentState } from "./persistent_state"; +import { bootstrap } from "./bootstrap"; export type Workspace = | { @@ -17,28 +19,18 @@ export type Workspace = }; export class Ctx { - private client: lc.LanguageClient | undefined; - readonly config: Config; - serverPath: string; readonly statusBar: vscode.StatusBarItem; + readonly config: Config; + + private client: lc.LanguageClient | undefined; traceOutputChannel: vscode.OutputChannel | undefined; outputChannel: vscode.OutputChannel | undefined; - - serverOptions: - | { - run: lc.Executable; - debug: lc.Executable; - } - | undefined; workspace: Workspace; + state: PersistentState; + serverPath: string | undefined; - constructor( - readonly extCtx: vscode.ExtensionContext, - config: Config, - serverPath: string, - workspace: Workspace - ) { + constructor(readonly extCtx: vscode.ExtensionContext, workspace: Workspace) { this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); extCtx.subscriptions.push(this.statusBar); extCtx.subscriptions.push({ @@ -50,9 +42,10 @@ export class Ctx { this.statusBar.tooltip = "ready"; this.statusBar.command = "rust-analyzer.analyzerStatus"; this.statusBar.show(); - this.serverPath = serverPath; - this.config = config; this.workspace = workspace; + + this.state = new PersistentState(extCtx.globalState); + this.config = new Config(extCtx); } clientFetcher() { @@ -64,6 +57,7 @@ export class Ctx { } async getClient() { + // if server path changes -> dispose if (!this.traceOutputChannel) { this.traceOutputChannel = vscode.window.createOutputChannel( "Rust Analyzer Language Server Trace" @@ -72,8 +66,20 @@ export class Ctx { if (!this.outputChannel) { this.outputChannel = vscode.window.createOutputChannel("Rust Analyzer Language Server"); } - if (!this.serverOptions) { - log.info("Creating server options client"); + + if (!this.client) { + log.info("Creating language 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) ); @@ -81,16 +87,11 @@ export class Ctx { command: this.serverPath, options: { env: newEnv }, }; - this.serverOptions = { + const serverOptions = { run, debug: run, }; - } else { - this.serverOptions.run.command = this.serverPath; - this.serverOptions.debug.command = this.serverPath; - } - if (!this.client) { - log.info("Creating language client"); + let rawInitializationOptions = vscode.workspace.getConfiguration("rust-analyzer"); if (this.workspace.kind === "Detached Files") { @@ -106,7 +107,7 @@ export class Ctx { this.traceOutputChannel, this.outputChannel, initializationOptions, - this.serverOptions + serverOptions ); this.client.onNotification(ra.serverStatus, (params) => this.setServerStatus(params)); } @@ -128,6 +129,7 @@ export class Ctx { async disposeClient() { log.info("Deactivating language client"); await this.client?.dispose(); + this.serverPath = undefined; this.client = undefined; } diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index 4ff27e0b551..84f0a5810f9 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -3,12 +3,9 @@ import * as lc from "vscode-languageclient/node"; import * as commands from "./commands"; import { Ctx, Workspace } from "./ctx"; -import { log, isRustDocument } from "./util"; -import { PersistentState } from "./persistent_state"; +import { isRustDocument } from "./util"; import { activateTaskProvider } from "./tasks"; import { setContextValue } from "./util"; -import { bootstrap } from "./bootstrap"; -import { Config } from "./config"; const RUST_PROJECT_CONTEXT_NAME = "inRustProject"; @@ -56,24 +53,13 @@ export async function activate( } : { kind: "Workspace Folder" }; - const state = new PersistentState(context.globalState); - const config = new Config(context); - - const serverPath = await bootstrap(context, config, 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 ctx = new Ctx(context, config, serverPath, workspace); + const ctx = new Ctx(context, workspace); // VS Code doesn't show a notification when an extension fails to activate // so we do it ourselves. return await activateServer(ctx).catch((err) => { - void vscode.window.showErrorMessage(`Cannot activate rust-analyzer: ${err.message}`); + void vscode.window.showErrorMessage( + `Cannot activate rust-analyzer extension: ${err.message}` + ); throw err; }); } @@ -83,7 +69,6 @@ async function activateServer(ctx: Ctx): Promise { ctx.pushExtCleanup(activateTaskProvider(ctx.config)); } - await ctx.activate(); await initCommonContext(ctx); if (ctx.config.typingContinueCommentsOnNewline) { @@ -91,17 +76,19 @@ async function activateServer(ctx: Ctx): Promise { } vscode.workspace.onDidChangeConfiguration( - (_) => - ctx - .getClient() - .then((it) => - it.sendNotification("workspace/didChangeConfiguration", { settings: "" }) - ) - .catch(log.error), + async (_) => { + await ctx + .clientFetcher() + .client?.sendNotification("workspace/didChangeConfiguration", { settings: "" }); + }, null, ctx.subscriptions ); + await ctx.activate().catch((err) => { + void vscode.window.showErrorMessage(`Cannot activate rust-analyzer server: ${err.message}`); + }); + return ctx.clientFetcher(); } @@ -130,6 +117,7 @@ async function initCommonContext(ctx: Ctx) { // Commands which invokes manually via command palette, shortcut, etc. ctx.registerCommand("reload", (_) => 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.disposeClient(); await ctx.activate(); }); From d68616a140b35dd9bc4e2982e0993257ab0942e0 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 17 Oct 2022 15:05:20 +0200 Subject: [PATCH 06/31] Make more things private --- editors/code/src/ast_inspector.ts | 2 -- editors/code/src/commands.ts | 3 +-- editors/code/src/ctx.ts | 35 ++++++++++++++++++------------- editors/code/src/main.ts | 10 ++++++--- editors/code/src/run.ts | 4 ++-- 5 files changed, 31 insertions(+), 23 deletions(-) diff --git a/editors/code/src/ast_inspector.ts b/editors/code/src/ast_inspector.ts index 2a2c9326b6a..176040120f4 100644 --- a/editors/code/src/ast_inspector.ts +++ b/editors/code/src/ast_inspector.ts @@ -54,8 +54,6 @@ export class AstInspector implements vscode.HoverProvider, vscode.DefinitionProv this, ctx.subscriptions ); - - ctx.pushExtCleanup(this); } dispose() { this.setRustEditor(undefined); diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts index cb088fc6c68..12ceb4f2df8 100644 --- a/editors/code/src/commands.ts +++ b/editors/code/src/commands.ts @@ -377,8 +377,7 @@ export function syntaxTree(ctx: Ctx): Cmd { } })(); - void new AstInspector(ctx); - + ctx.pushExtCleanup(new AstInspector(ctx)); ctx.pushExtCleanup( vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-syntax-tree", tdcp) ); diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index f62ccd1a652..6b12d9ca1ec 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts @@ -23,12 +23,12 @@ export class Ctx { readonly config: Config; private client: lc.LanguageClient | undefined; + private _serverPath: string | undefined; + private traceOutputChannel: vscode.OutputChannel | undefined; + private outputChannel: vscode.OutputChannel | undefined; + private state: PersistentState; - traceOutputChannel: vscode.OutputChannel | undefined; - outputChannel: vscode.OutputChannel | undefined; workspace: Workspace; - state: PersistentState; - serverPath: string | undefined; constructor(readonly extCtx: vscode.ExtensionContext, workspace: Workspace) { this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); @@ -70,21 +70,24 @@ export class Ctx { if (!this.client) { log.info("Creating language client"); - this.serverPath = await bootstrap(this.extCtx, this.config, this.state).catch((err) => { - let message = "bootstrap error. "; + 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 }'; + 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); - }); + 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, + command: this._serverPath, options: { env: newEnv }, }; const serverOptions = { @@ -129,7 +132,7 @@ export class Ctx { async disposeClient() { log.info("Deactivating language client"); await this.client?.dispose(); - this.serverPath = undefined; + this._serverPath = undefined; this.client = undefined; } @@ -161,6 +164,10 @@ export class Ctx { return this.extCtx.subscriptions; } + get serverPath(): string | undefined { + return this._serverPath; + } + setServerStatus(status: ServerStatusParams) { let icon = ""; const statusBar = this.statusBar; diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index 84f0a5810f9..97ec41dce3a 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -14,6 +14,10 @@ export interface RustAnalyzerExtensionApi { readonly client?: lc.LanguageClient; } +export async function deactivate() { + await setContextValue(RUST_PROJECT_CONTEXT_NAME, undefined); +} + export async function activate( context: vscode.ExtensionContext ): Promise { @@ -56,12 +60,14 @@ export async function activate( const ctx = new Ctx(context, workspace); // VS Code doesn't show a notification when an extension fails to activate // so we do it ourselves. - return await activateServer(ctx).catch((err) => { + 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; } async function activateServer(ctx: Ctx): Promise { @@ -112,8 +118,6 @@ async function initCommonContext(ctx: Ctx) { ); ctx.pushExtCleanup(defaultOnEnter); - await setContextValue(RUST_PROJECT_CONTEXT_NAME, true); - // Commands which invokes manually via command palette, shortcut, etc. ctx.registerCommand("reload", (_) => async () => { void vscode.window.showInformationMessage("Reloading rust-analyzer..."); diff --git a/editors/code/src/run.ts b/editors/code/src/run.ts index 22e5eda6827..dadaa41b1d1 100644 --- a/editors/code/src/run.ts +++ b/editors/code/src/run.ts @@ -18,9 +18,9 @@ export async function selectRunnable( showButtons: boolean = true ): Promise { 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(), }; From 0421756b4256b8edf739b5889e754d7bc733ed38 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 17 Oct 2022 15:43:15 +0200 Subject: [PATCH 07/31] Implement stop and start server commands --- editors/code/package.json | 11 +++++++++++ editors/code/src/client.ts | 8 ++++++++ editors/code/src/ctx.ts | 2 -- editors/code/src/main.ts | 7 +++++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/editors/code/package.json b/editors/code/package.json index da4bac5ad84..10558e8c018 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -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", diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts index 3408a2ee84e..fb667619c86 100644 --- a/editors/code/src/client.ts +++ b/editors/code/src/client.ts @@ -76,6 +76,14 @@ export async function createClient( 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, diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index 6b12d9ca1ec..2080db7a2c1 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts @@ -68,8 +68,6 @@ export class Ctx { } if (!this.client) { - log.info("Creating language client"); - this._serverPath = await bootstrap(this.extCtx, this.config, this.state).catch( (err) => { let message = "bootstrap error. "; diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index 97ec41dce3a..f65620aebc6 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -126,6 +126,13 @@ async function initCommonContext(ctx: Ctx) { await ctx.activate(); }); + ctx.registerCommand("startServer", (_) => async () => { + await ctx.activate(); + }); + ctx.registerCommand("stopServer", (_) => async () => { + // FIXME: We should re-use the client, that is ctx.deactivate() if none of the configs have changed + await ctx.disposeClient(); + }); ctx.registerCommand("analyzerStatus", commands.analyzerStatus); ctx.registerCommand("memoryUsage", commands.memoryUsage); ctx.registerCommand("shuffleCrateGraph", commands.shuffleCrateGraph); From 7b5c943129ca285ec2c386b8608f1410ee258f07 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 17 Oct 2022 15:48:36 +0200 Subject: [PATCH 08/31] Downgrade vscode types dependency --- editors/code/package-lock.json | 14 +++++++------- editors/code/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/editors/code/package-lock.json b/editors/code/package-lock.json index 192d8fabc31..a72865d4fe4 100644 --- a/editors/code/package-lock.json +++ b/editors/code/package-lock.json @@ -15,7 +15,7 @@ }, "devDependencies": { "@types/node": "~16.11.7", - "@types/vscode": "~1.72.0", + "@types/vscode": "~1.66.0", "@typescript-eslint/eslint-plugin": "^5.30.5", "@typescript-eslint/parser": "^5.30.5", "@vscode/test-electron": "^2.1.5", @@ -141,9 +141,9 @@ "dev": true }, "node_modules/@types/vscode": { - "version": "1.72.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.72.0.tgz", - "integrity": "sha512-WvHluhUo+lQvE3I4wUagRpnkHuysB4qSyOQUyIAS9n9PYMJjepzTUD8Jyks0YeXoPD0UGctjqp2u84/b3v6Ydw==", + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.66.0.tgz", + "integrity": "sha512-ZfJck4M7nrGasfs4A4YbUoxis3Vu24cETw3DERsNYtDZmYSYtk6ljKexKFKhImO/ZmY6ZMsmegu2FPkXoUFImA==", "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -4039,9 +4039,9 @@ "dev": true }, "@types/vscode": { - "version": "1.72.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.72.0.tgz", - "integrity": "sha512-WvHluhUo+lQvE3I4wUagRpnkHuysB4qSyOQUyIAS9n9PYMJjepzTUD8Jyks0YeXoPD0UGctjqp2u84/b3v6Ydw==", + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.66.0.tgz", + "integrity": "sha512-ZfJck4M7nrGasfs4A4YbUoxis3Vu24cETw3DERsNYtDZmYSYtk6ljKexKFKhImO/ZmY6ZMsmegu2FPkXoUFImA==", "dev": true }, "@typescript-eslint/eslint-plugin": { diff --git a/editors/code/package.json b/editors/code/package.json index 10558e8c018..1afe2087c71 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -41,7 +41,7 @@ }, "devDependencies": { "@types/node": "~16.11.7", - "@types/vscode": "~1.72.0", + "@types/vscode": "~1.66.0", "@typescript-eslint/eslint-plugin": "^5.30.5", "@typescript-eslint/parser": "^5.30.5", "@vscode/test-electron": "^2.1.5", From d63c44e650f9a6ce2fc5f713b4688f3e9e2d9a8d Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 17 Oct 2022 16:01:39 +0200 Subject: [PATCH 09/31] Cleanup output channels --- editors/code/src/ctx.ts | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index 2080db7a2c1..e94d4365c37 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts @@ -62,9 +62,11 @@ export class Ctx { 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) { @@ -139,25 +141,10 @@ 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.pushExtCleanup(d); - } - get extensionPath(): string { return this.extCtx.extensionPath; } - get globalState(): vscode.Memento { - return this.extCtx.globalState; - } - get subscriptions(): Disposable[] { return this.extCtx.subscriptions; } @@ -201,6 +188,13 @@ export class Ctx { statusBar.text = `${icon}rust-analyzer`; } + registerCommand(name: string, factory: (ctx: Ctx) => Cmd) { + const fullName = `rust-analyzer.${name}`; + const cmd = factory(this); + const d = vscode.commands.registerCommand(fullName, cmd); + this.pushExtCleanup(d); + } + pushExtCleanup(d: Disposable) { this.extCtx.subscriptions.push(d); } From e41023ce46b36d57fd920a4520555d1067170e30 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 17 Oct 2022 16:32:47 +0200 Subject: [PATCH 10/31] Make flycheck workdone progress reports cancellable --- crates/rust-analyzer/src/lsp_utils.rs | 10 +++++++--- crates/rust-analyzer/src/main_loop.rs | 26 +++++++++++++++++++++----- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/crates/rust-analyzer/src/lsp_utils.rs b/crates/rust-analyzer/src/lsp_utils.rs index 5a37cbe2e33..b3cea64d417 100644 --- a/crates/rust-analyzer/src/lsp_utils.rs +++ b/crates/rust-analyzer/src/lsp_utils.rs @@ -87,6 +87,7 @@ pub(crate) fn report_progress( state: Progress, message: Option, fraction: Option, + cancel_token: Option, ) { 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::( @@ -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, }) diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index 15922dac651..3a379993ad6 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -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, } }; - 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, }; 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, ) } } @@ -584,7 +585,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 +705,16 @@ fn on_notification(&mut self, not: Notification) -> Result<()> { this.cancel(id); Ok(()) })? - .on::(|_this, _params| { + .on::(|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. From a762baca028e2a6f5efeb7d7804de08d035ba0c5 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 17 Oct 2022 17:53:50 +0200 Subject: [PATCH 11/31] fix: Fix formatting requests hanging when r-a is still starting The reason for that was that we were calculating the crate defmaps of the file we are saving by accident causing us to get stuck waiting on their expensive computation, while we only need the relevant crate id. --- crates/ide/src/lib.rs | 4 ++-- crates/ide/src/parent_module.rs | 12 +++++------- crates/ide/src/status.rs | 2 +- crates/rust-analyzer/src/cargo_target_spec.rs | 6 +----- crates/rust-analyzer/src/dispatch.rs | 4 ++-- crates/rust-analyzer/src/global_state.rs | 7 +++---- crates/rust-analyzer/src/handlers.rs | 4 ++-- crates/rust-analyzer/src/main_loop.rs | 2 +- 8 files changed, 17 insertions(+), 24 deletions(-) diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index 77fe0dbf556..09a5cb03ecd 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -482,8 +482,8 @@ pub fn parent_module(&self, position: FilePosition) -> Cancellable Cancellable> { - self.with_db(|db| parent_module::crate_for(db, file_id)) + pub fn crates_for(&self, file_id: FileId) -> Cancellable> { + self.with_db(|db| parent_module::crates_for(db, file_id)) } /// Returns the edition of the given crate. diff --git a/crates/ide/src/parent_module.rs b/crates/ide/src/parent_module.rs index 8f3cc86873f..9d425954e39 100644 --- a/crates/ide/src/parent_module.rs +++ b/crates/ide/src/parent_module.rs @@ -1,9 +1,8 @@ use hir::Semantics; use ide_db::{ - base_db::{CrateId, FileId, FilePosition}, + base_db::{CrateId, FileId, FileLoader, FilePosition}, RootDatabase, }; -use itertools::Itertools; use syntax::{ algo::find_node_at_offset, ast::{self, AstNode}, @@ -55,9 +54,8 @@ pub(crate) fn parent_module(db: &RootDatabase, position: FilePosition) -> Vec Vec { - 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 { + db.relevant_crates(file_id).iter().copied().collect() } #[cfg(test)] @@ -147,7 +145,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 +160,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); } } diff --git a/crates/ide/src/status.rs b/crates/ide/src/status.rs index f4d0387440d..20810c25b3e 100644 --- a/crates/ide/src/status.rs +++ b/crates/ide/src/status.rs @@ -45,7 +45,7 @@ pub(crate) fn status(db: &RootDatabase, file_id: Option) -> 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"); } diff --git a/crates/rust-analyzer/src/cargo_target_spec.rs b/crates/rust-analyzer/src/cargo_target_spec.rs index e1675a030c0..cbde7354761 100644 --- a/crates/rust-analyzer/src/cargo_target_spec.rs +++ b/crates/rust-analyzer/src/cargo_target_spec.rs @@ -118,11 +118,7 @@ pub(crate) fn for_file( global_state_snapshot: &GlobalStateSnapshot, file_id: FileId, ) -> Result> { - let crate_id = match &*global_state_snapshot.analysis.crate_for(file_id)? { - &[crate_id, ..] => crate_id, - _ => return Ok(None), - }; - let (cargo_ws, target) = match global_state_snapshot.cargo_target_for_crate_root(crate_id) { + let (cargo_ws, target) = match global_state_snapshot.cargo_target_for_file_id(file_id) { Some(it) => it, None => return Ok(None), }; diff --git a/crates/rust-analyzer/src/dispatch.rs b/crates/rust-analyzer/src/dispatch.rs index f16559148e6..57899b59914 100644 --- a/crates/rust-analyzer/src/dispatch.rs +++ b/crates/rust-analyzer/src/dispatch.rs @@ -52,7 +52,7 @@ pub(crate) fn on_sync_mut( let _pctx = stdx::panic_context::enter(panic_context); f(self.global_state, params) }; - if let Ok(response) = result_to_response::(req.id.clone(), result) { + if let Ok(response) = result_to_response::(req.id, result) { self.global_state.respond(response); } @@ -80,7 +80,7 @@ pub(crate) fn on_sync( f(global_state_snapshot, params) }); - if let Ok(response) = thread_result_to_response::(req.id.clone(), result) { + if let Ok(response) = thread_result_to_response::(req.id, result) { self.global_state.respond(response); } diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs index 000ff88e458..4cddb12083a 100644 --- a/crates/rust-analyzer/src/global_state.rs +++ b/crates/rust-analyzer/src/global_state.rs @@ -8,7 +8,7 @@ use crossbeam_channel::{unbounded, Receiver, Sender}; use flycheck::FlycheckHandle; use ide::{Analysis, AnalysisHost, Cancellable, Change, FileId}; -use ide_db::base_db::{CrateId, FileLoader, SourceDatabase}; +use ide_db::base_db::{FileLoader, SourceDatabase}; use lsp_types::{SemanticTokens, Url}; use parking_lot::{Mutex, RwLock}; use proc_macro_api::ProcMacroServer; @@ -398,11 +398,10 @@ pub(crate) fn anchored_path(&self, path: &AnchoredPathBuf) -> Url { url_from_abs_path(path) } - pub(crate) fn cargo_target_for_crate_root( + pub(crate) fn cargo_target_for_file_id( &self, - crate_id: CrateId, + file_id: FileId, ) -> Option<(&CargoWorkspace, Target)> { - let file_id = self.analysis.crate_root(crate_id).ok()?; let path = self.vfs.read().0.file_path(file_id); let path = path.as_path()?; self.workspaces.iter().find_map(|ws| match ws { diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs index 8c3ea77d061..701a009ea8b 100644 --- a/crates/rust-analyzer/src/handlers.rs +++ b/crates/rust-analyzer/src/handlers.rs @@ -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,7 @@ fn run_rustfmt( ) -> Result>> { 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)?; + let crate_ids = snap.analysis.crates_for(file_id)?; let line_index = snap.file_line_index(file_id)?; diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index 15922dac651..7cd7d002481 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -767,7 +767,7 @@ fn on_notification(&mut self, not: Notification) -> Result<()> { let analysis = this.analysis_host.analysis(); // Crates containing or depending on the saved file let crate_ids: Vec<_> = analysis - .crate_for(file_id)? + .crates_for(file_id)? .into_iter() .flat_map(|id| { this.analysis_host From 8047512dcab33780276771d398ddaee20ec8e933 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Tue, 18 Oct 2022 14:18:59 +0200 Subject: [PATCH 12/31] Revert "feat: Diagnose some incorrect usages of the question mark operator" --- crates/hir-expand/src/mod_path.rs | 1 - crates/hir-expand/src/name.rs | 2 - crates/hir-ty/src/infer.rs | 13 ++- crates/hir-ty/src/infer/expr.rs | 99 ++----------------- crates/hir-ty/src/method_resolution.rs | 18 ---- crates/hir-ty/src/tests/traits.rs | 92 ++++++++++++++++- crates/hir/src/diagnostics.rs | 14 +-- crates/hir/src/lib.rs | 38 +++---- .../src/handlers/incorrect_try_expr.rs | 37 ------- .../src/handlers/not_implemented.rs | 35 ------- crates/ide-diagnostics/src/lib.rs | 4 - crates/ide/src/hover/tests.rs | 22 +---- crates/test-utils/src/minicore.rs | 39 +------- 13 files changed, 125 insertions(+), 289 deletions(-) delete mode 100644 crates/ide-diagnostics/src/handlers/incorrect_try_expr.rs delete mode 100644 crates/ide-diagnostics/src/handlers/not_implemented.rs diff --git a/crates/hir-expand/src/mod_path.rs b/crates/hir-expand/src/mod_path.rs index 68413df420c..d7586d129b7 100644 --- a/crates/hir-expand/src/mod_path.rs +++ b/crates/hir-expand/src/mod_path.rs @@ -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") }; diff --git a/crates/hir-expand/src/name.rs b/crates/hir-expand/src/name.rs index 8a735b965ab..2679a1c3602 100644 --- a/crates/hir-expand/src/name.rs +++ b/crates/hir-expand/src/name.rs @@ -279,8 +279,6 @@ macro_rules! known_names { RangeToInclusive, RangeTo, Range, - Residual, - FromResidual, Neg, Not, None, diff --git a/crates/hir-ty/src/infer.rs b/crates/hir-ty/src/infer.rs index 4315ccab6c1..31e56dec625 100644 --- a/crates/hir-ty/src/infer.rs +++ b/crates/hir-ty/src/infer.rs @@ -190,9 +190,7 @@ fn map(self, f: impl FnOnce(T) -> U) -> InferOk { 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 { self.db.trait_data(trait_).associated_type_by_name(&name![Item]) } + fn resolve_ops_try_ok(&self) -> Option { + // 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 { let trait_ = self.resolve_lang_item(name![neg])?.as_trait()?; self.db.trait_data(trait_).associated_type_by_name(&name![Output]) diff --git a/crates/hir-ty/src/infer/expr.rs b/crates/hir-ty/src/infer/expr.rs index 59ab50d0717..f56108b26c4 100644 --- a/crates/hir-ty/src/infer/expr.rs +++ b/crates/hir-ty/src/infer/expr.rs @@ -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( let ctx = self.breakables.pop().expect("breakable stack broken"); (ctx.may_break.then(|| ctx.coerce.complete()), res) } - - /// Check whether `ty` implements `FromResidual` - 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)), - )) - } } diff --git a/crates/hir-ty/src/method_resolution.rs b/crates/hir-ty/src/method_resolution.rs index 224dc21e94e..5998680dcd3 100644 --- a/crates/hir-ty/src/method_resolution.rs +++ b/crates/hir-ty/src/method_resolution.rs @@ -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, - ty: Canonical, - try_trait: TraitId, -) -> Option { - 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) => { diff --git a/crates/hir-ty/src/tests/traits.rs b/crates/hir-ty/src/tests/traits.rs index b91172e3342..555b6972fb7 100644 --- a/crates/hir-ty/src/tests/traits.rs +++ b/crates/hir-ty/src/tests/traits.rs @@ -162,16 +162,98 @@ fn test() { ); } +#[test] +fn infer_try() { + check_types( + r#" +//- /main.rs crate:main deps:core +fn test() { + let r: Result = 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 { + Ok(O), + Err(E) + } + + impl crate::ops::Try for Result { + 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 { - let r: core::ops::ControlFlow = core::ops::ControlFlow::Continue(1.0); +//- /main.rs crate:main deps:core +fn test() { + let r: Result = 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::Residual> {} + } + + pub use self::try_trait::FromResidual; + pub use self::try_trait::Try; +} + +mod convert { + pub trait From {} + impl From for T {} +} + +pub mod result { + use crate::convert::From; + use crate::ops::{Try, FromResidual}; + + pub enum Infallible {} + pub enum Result { + Ok(O), + Err(E) + } + + impl Try for Result { + type Output = O; + type Error = Result; + } + + impl> FromResidual> for Result {} +} + +pub mod prelude { + pub mod rust_2018 { + pub use crate::result::*; + } } "#, ); diff --git a/crates/hir/src/diagnostics.rs b/crates/hir/src/diagnostics.rs index 6c8b3088adc..c5dc60f1ec5 100644 --- a/crates/hir/src/diagnostics.rs +++ b/crates/hir/src/diagnostics.rs @@ -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>, -} -#[derive(Debug)] -pub struct NotImplemented { - pub expr: InFile>, - pub trait_: TraitId, - pub ty: Type, -} #[derive(Debug)] pub struct MissingMatchArms { diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs index e6c5c6b5833..f5324208c9a 100644 --- a/crates/hir/src/lib.rs +++ b/crates/hir/src/lib.rs @@ -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) { 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() { diff --git a/crates/ide-diagnostics/src/handlers/incorrect_try_expr.rs b/crates/ide-diagnostics/src/handlers/incorrect_try_expr.rs deleted file mode 100644 index 085d8d32598..00000000000 --- a/crates/ide-diagnostics/src/handlers/incorrect_try_expr.rs +++ /dev/null @@ -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::::Continue(1.0)?; - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: the return type of the containing function does not implement `FromResidual` -} -"#, - ); - } -} diff --git a/crates/ide-diagnostics/src/handlers/not_implemented.rs b/crates/ide-diagnostics/src/handlers/not_implemented.rs deleted file mode 100644 index 3bf6a423229..00000000000 --- a/crates/ide-diagnostics/src/handlers/not_implemented.rs +++ /dev/null @@ -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 `()` -"#, - ) - } -} diff --git a/crates/ide-diagnostics/src/lib.rs b/crates/ide-diagnostics/src/lib.rs index 4577072149a..ae299f05841 100644 --- a/crates/ide-diagnostics/src/lib.rs +++ b/crates/ide-diagnostics/src/lib.rs @@ -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), diff --git a/crates/ide/src/hover/tests.rs b/crates/ide/src/hover/tests.rs index 5cab017a58d..eb997e6fef8 100644 --- a/crates/ide/src/hover/tests.rs +++ b/crates/ide/src/hover/tests.rs @@ -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 + as Try>::Output + ```"#]], ); } diff --git a/crates/test-utils/src/minicore.rs b/crates/test-utils/src/minicore.rs index 59b1c147d7f..69d2e62b256 100644 --- a/crates/test-utils/src/minicore.rs +++ b/crates/test-utils/src/minicore.rs @@ -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 { 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 { Continue(C), Break(B), } - pub trait FromResidual::Residual> { + pub trait FromResidual { #[lang = "from_residual"] fn from_residual(residual: R) -> Self; } @@ -346,13 +342,13 @@ pub trait Try: FromResidual { impl Try for ControlFlow { type Output = C; - type Residual = ControlFlow; + type Residual = ControlFlow; fn from_output(output: Self::Output) -> Self {} fn branch(self) -> ControlFlow {} } impl FromResidual for ControlFlow { - fn from_residual(residual: ControlFlow) -> Self {} + fn from_residual(residual: ControlFlow) -> Self {} } } pub use self::try_::{ControlFlow, FromResidual, Try}; @@ -473,33 +469,6 @@ pub const fn unwrap(self) -> T { } } } - // region:try - impl crate::ops::Try for Option { - type Output = T; - type Residual = Option; - - #[inline] - fn from_output(output: Self::Output) -> Self { - Some(output) - } - - #[inline] - fn branch(self) -> crate::ops::ControlFlow { - match self { - Some(v) => crate::ops::ControlFlow::Continue(v), - None => crate::ops::ControlFlow::Break(None), - } - } - } - impl crate::ops::FromResidual for Option { - #[inline] - fn from_residual(residual: Option) -> Self { - match residual { - None => None, - } - } - } - // endregion:try } // endregion:option From 9d3e616f82aecd4ba81ee632483f26169aca1aa3 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 19 Oct 2022 21:17:11 +0200 Subject: [PATCH 13/31] Simplify --- crates/project-model/Cargo.toml | 4 +- crates/project-model/src/cargo_workspace.rs | 46 +++++++++++---------- crates/project-model/src/lib.rs | 4 +- crates/project-model/src/sysroot.rs | 1 + crates/syntax/src/ast/edit_in_place.rs | 5 +-- crates/syntax/src/lib.rs | 2 +- 6 files changed, 33 insertions(+), 29 deletions(-) diff --git a/crates/project-model/Cargo.toml b/crates/project-model/Cargo.toml index 6fd7c3166f8..cf9868740cb 100644 --- a/crates/project-model/Cargo.toml +++ b/crates/project-model/Cargo.toml @@ -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" diff --git a/crates/project-model/src/cargo_workspace.rs b/crates/project-model/src/cargo_workspace.rs index 8e690f1125a..8d011f22e62 100644 --- a/crates/project-model/src/cargo_workspace.rs +++ b/crates/project-model/src/cargo_workspace.rs @@ -283,8 +283,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 +327,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::(metadata.clone()).unwrap_or_default(); + let meta = from_value::(metadata).unwrap_or_default(); let edition = match edition { cargo_metadata::Edition::E2015 => Edition::Edition2015, cargo_metadata::Edition::E2018 => Edition::Edition2018, @@ -352,35 +353,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); } diff --git a/crates/project-model/src/lib.rs b/crates/project-model/src/lib.rs index ce78ce85697..28e9379c1ae 100644 --- a/crates/project-model/src/lib.rs +++ b/crates/project-model/src/lib.rs @@ -67,7 +67,7 @@ pub fn from_manifest_file(path: AbsPathBuf) -> Result { 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 { @@ -78,7 +78,7 @@ pub fn discover_single(path: &AbsPath) -> Result { }; if !candidates.is_empty() { - bail!("more than one project") + bail!("more than one project"); } Ok(res) } diff --git a/crates/project-model/src/sysroot.rs b/crates/project-model/src/sysroot.rs index bc37e3d132a..d4f6940d7ed 100644 --- a/crates/project-model/src/sysroot.rs +++ b/crates/project-model/src/sysroot.rs @@ -189,6 +189,7 @@ fn discover_sysroot_src_dir(sysroot_path: &AbsPathBuf) -> Option { get_rust_src(sysroot_path) } + fn discover_sysroot_src_dir_or_add_component( sysroot_path: &AbsPathBuf, current_dir: &AbsPath, diff --git a/crates/syntax/src/ast/edit_in_place.rs b/crates/syntax/src/ast/edit_in_place.rs index 229e7419b73..660c057e99c 100644 --- a/crates/syntax/src/ast/edit_in_place.rs +++ b/crates/syntax/src/ast/edit_in_place.rs @@ -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 { diff --git a/crates/syntax/src/lib.rs b/crates/syntax/src/lib.rs index 4f5e273a520..84c66b27e69 100644 --- a/crates/syntax/src/lib.rs +++ b/crates/syntax/src/lib.rs @@ -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 } } From 653dafa7b176174aead7d8f975e4041f82d7d53c Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 19 Oct 2022 22:25:57 +0200 Subject: [PATCH 14/31] Add some sysroot logging --- crates/project-model/src/sysroot.rs | 17 +++++++++-------- crates/project-model/src/workspace.rs | 9 +++++++++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/crates/project-model/src/sysroot.rs b/crates/project-model/src/sysroot.rs index d4f6940d7ed..fa8d76f3f45 100644 --- a/crates/project-model/src/sysroot.rs +++ b/crates/project-model/src/sysroot.rs @@ -64,14 +64,15 @@ pub fn proc_macro(&self) -> Option { self.by_name("proc_macro") } - pub fn crates<'a>(&'a self) -> impl Iterator + ExactSizeIterator + 'a { + pub fn crates(&self) -> impl Iterator + 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) -> Result { - 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, ) -> Option { - 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 { @@ -200,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) }) @@ -218,7 +219,7 @@ fn discover_sysroot_src_dir_or_add_component( fn get_rustc_src(sysroot_path: &AbsPath) -> Option { 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 { @@ -228,7 +229,7 @@ fn get_rustc_src(sysroot_path: &AbsPath) -> Option { fn get_rust_src(sysroot_path: &AbsPath) -> Option { 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 { diff --git a/crates/project-model/src/workspace.rs b/crates/project-model/src/workspace.rs index 72ddf809288..9b0a2617036 100644 --- a/crates/project-model/src/workspace.rs +++ b/crates/project-model/src/workspace.rs @@ -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 }) From 7e2c41dbd68014acbf91c2657a975c5a4b486da9 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Sat, 27 Aug 2022 18:28:09 +0200 Subject: [PATCH 15/31] Implement invocation strategy config for build scripts --- crates/project-model/src/build_scripts.rs | 400 ++++++++++++-------- crates/project-model/src/cargo_workspace.rs | 3 +- crates/project-model/src/lib.rs | 8 + crates/project-model/src/workspace.rs | 60 ++- crates/rust-analyzer/src/config.rs | 32 ++ crates/rust-analyzer/src/reload.rs | 6 +- docs/user/generated_config.adoc | 11 + editors/code/package.json | 15 + 8 files changed, 373 insertions(+), 162 deletions(-) diff --git a/crates/project-model/src/build_scripts.rs b/crates/project-model/src/build_scripts.rs index d9f09c03495..6e6654e74e1 100644 --- a/crates/project-model/src/build_scripts.rs +++ b/crates/project-model/src/build_scripts.rs @@ -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,13 @@ use semver::Version; use serde::Deserialize; -use crate::{cfg_flag::CfgFlag, CargoConfig, CargoFeatures, CargoWorkspace, Package}; +use crate::{ + cfg_flag::CfgFlag, CargoConfig, CargoFeatures, CargoWorkspace, InvocationStrategy, Package, +}; #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct WorkspaceBuildScripts { - outputs: ArenaMap>, + outputs: ArenaMap, error: Option, } @@ -38,76 +45,55 @@ pub(crate) struct BuildScriptOutput { pub(crate) proc_macro_dylib_path: Option, } +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 { + 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()); - 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 + } + }; 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, - ) -> io::Result { - 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 { 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,7 +103,107 @@ 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, + ) -> io::Result { + const RUST_1_62: Version = Version::new(1, 62, 0); + + match Self::run_per_ws(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_per_ws(cmd, config, workspace, 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> { + assert_eq!(config.invocation_strategy, InvocationStrategy::OnceInRoot); + 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(); + 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()); + by_id.insert(workspace[package].id.clone(), (package, idx)); + } + res + }) + .collect(); + + let errors = Self::run_command( + cmd, + |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()); + + 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( + mut cmd: Command, + config: &CargoConfig, + workspace: &CargoWorkspace, + progress: &dyn Fn(String), + ) -> io::Result { + let workspace_root: &path::Path = &workspace.workspace_root().as_ref(); + + match config.invocation_strategy { + InvocationStrategy::OnceInRoot => (), + InvocationStrategy::PerWorkspaceWithManifestPath => { + cmd.arg("--manifest-path"); + cmd.arg(workspace_root.join("Cargo.toml")); + } + InvocationStrategy::PerWorkspace => { + cmd.current_dir(workspace_root); + } + } let mut res = WorkspaceBuildScripts::default(); let outputs = &mut res.outputs; @@ -126,10 +212,44 @@ fn run_( // exactly those from `config`. let mut by_id: FxHashMap = 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, + |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( + cmd: Command, + // 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> { let errors = RefCell::new(String::new()); let push_err = |err: &str| { let mut e = errors.borrow_mut(); @@ -149,61 +269,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::() { - 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::() { + 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 +339,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 +353,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, diff --git a/crates/project-model/src/cargo_workspace.rs b/crates/project-model/src/cargo_workspace.rs index 8e690f1125a..79b56815c02 100644 --- a/crates/project-model/src/cargo_workspace.rs +++ b/crates/project-model/src/cargo_workspace.rs @@ -14,8 +14,8 @@ use serde::Deserialize; use serde_json::from_value; -use crate::CfgOverrides; use crate::{utf8_stdout, 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,7 @@ pub struct CargoConfig { pub run_build_script_command: Option>, /// Extra env vars to set when invoking the cargo command pub extra_env: FxHashMap, + pub invocation_strategy: InvocationStrategy, } impl CargoConfig { diff --git a/crates/project-model/src/lib.rs b/crates/project-model/src/lib.rs index ce78ce85697..13a86901f71 100644 --- a/crates/project-model/src/lib.rs +++ b/crates/project-model/src/lib.rs @@ -157,3 +157,11 @@ fn utf8_stdout(mut cmd: Command) -> Result { let stdout = String::from_utf8(output.stdout)?; Ok(stdout.trim().to_string()) } + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub enum InvocationStrategy { + OnceInRoot, + PerWorkspaceWithManifestPath, + #[default] + PerWorkspace, +} diff --git a/crates/project-model/src/workspace.rs b/crates/project-model/src/workspace.rs index 72ddf809288..01f5157093a 100644 --- a/crates/project-model/src/workspace.rs +++ b/crates/project-model/src/workspace.rs @@ -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. @@ -294,6 +294,7 @@ pub fn load_detached_files(detached_files: Vec) -> Result Result { 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 +316,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> { + if let InvocationStrategy::PerWorkspaceWithManifestPath | InvocationStrategy::PerWorkspace = + config.invocation_strategy + { + 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, diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index 577a8640a4c..79f6ded4899 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -69,6 +69,14 @@ struct ConfigData { cargo_autoreload: bool = "true", /// Run build scripts (`build.rs`) for more precise code analysis. cargo_buildScripts_enable: bool = "true", + /// Specifies the invocation strategy to use when running the build scripts command. + /// If `per_workspace_with_manifest_path` is set, the command will be executed for each + /// workspace, `--manifest-path {workspace-dir}` will be passed to the invoked command and + /// the command will be executed from the project root. + /// If `per_workspace` is set, the command will be executed for each workspace and the + /// command will be executed from the corresponding workspace root. + /// If `once_in_root` is set, the command will be executed once in the project root. + 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 @@ -1056,6 +1064,13 @@ 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::OnceInRoot => project_model::InvocationStrategy::OnceInRoot, + InvocationStrategy::PerWorkspaceWithManifestPath => { + project_model::InvocationStrategy::PerWorkspaceWithManifestPath + } + InvocationStrategy::PerWorkspace => project_model::InvocationStrategy::PerWorkspace, + }, run_build_script_command: self.data.cargo_buildScripts_overrideCommand.clone(), extra_env: self.data.cargo_extraEnv.clone(), } @@ -1587,6 +1602,14 @@ enum CargoFeaturesDef { Selected(Vec), } +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +enum InvocationStrategy { + OnceInRoot, + PerWorkspaceWithManifestPath, + PerWorkspace, +} + #[derive(Deserialize, Debug, Clone)] #[serde(untagged)] enum LifetimeElisionDef { @@ -2001,6 +2024,15 @@ macro_rules! set { "Render annotations above the whole item, including documentation comments and attributes." ], }, + "InvocationStrategy" => set! { + "type": "string", + "enum": ["per_workspace", "per_workspace_with_manifest_path", "once_in_root"], + "enumDescriptions": [ + "The command will be executed for each workspace, `--manifest-path {workspace-dir}` will be passed to the invoked command and the command will be executed from the project root.", + "The command will be executed for each workspace and the command will be executed from the corresponding workspace root.", + "The command will be executed once in the project root." + ], + }, _ => panic!("missing entry for {}: {}", ty, default), } diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs index f8734893944..bd5741f6159 100644 --- a/crates/rust-analyzer/src/reload.rs +++ b/crates/rust-analyzer/src/reload.rs @@ -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(); }); } diff --git a/docs/user/generated_config.adoc b/docs/user/generated_config.adoc index acf0aaea859..a5307b63151 100644 --- a/docs/user/generated_config.adoc +++ b/docs/user/generated_config.adoc @@ -24,6 +24,17 @@ Automatically refresh project info via `cargo metadata` on -- Run build scripts (`build.rs`) for more precise code analysis. -- +[[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_with_manifest_path` is set, the command will be executed for each +workspace, `--manifest-path {workspace-dir}` will be passed to the invoked command and +the command will be executed from the project root. +If `per_workspace` is set, the command will be executed for each workspace and the +command will be executed from the corresponding workspace root. +If `once_in_root` is set, the command will be executed once in the project root. +-- [[rust-analyzer.cargo.buildScripts.overrideCommand]]rust-analyzer.cargo.buildScripts.overrideCommand (default: `null`):: + -- diff --git a/editors/code/package.json b/editors/code/package.json index f1dd3aa79ff..b1b565106a3 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -421,6 +421,21 @@ "default": true, "type": "boolean" }, + "rust-analyzer.cargo.buildScripts.invocationStrategy": { + "markdownDescription": "Specifies the invocation strategy to use when running the build scripts command.\nIf `per_workspace_with_manifest_path` is set, the command will be executed for each\nworkspace, `--manifest-path {workspace-dir}` will be passed to the invoked command and\nthe command will be executed from the project root.\nIf `per_workspace` is set, the command will be executed for each workspace and the\ncommand will be executed from the corresponding workspace root.\nIf `once_in_root` is set, the command will be executed once in the project root.", + "default": "per_workspace", + "type": "string", + "enum": [ + "per_workspace", + "per_workspace_with_manifest_path", + "once_in_root" + ], + "enumDescriptions": [ + "The command will be executed for each workspace, `--manifest-path {workspace-dir}` will be passed to the invoked command and the command will be executed from the project root.", + "The command will be executed for each workspace and the command will be executed from the corresponding workspace root.", + "The command will be executed once in the project root." + ] + }, "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, From 4a287d25258aac0873863584e6f3d075dfb69033 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 15 Sep 2022 13:28:09 +0200 Subject: [PATCH 16/31] Implement invocation strategy config for checkOnSave Note that due to how cargo works, none of the modes currently work for r-a --- crates/flycheck/src/lib.rs | 53 ++++++++++++++++++++---- crates/rust-analyzer/src/config.rs | 17 ++++++++ crates/rust-analyzer/src/reload.rs | 65 ++++++++++++++++++------------ docs/user/generated_config.adoc | 11 +++++ editors/code/package.json | 15 +++++++ 5 files changed, 127 insertions(+), 34 deletions(-) diff --git a/crates/flycheck/src/lib.rs b/crates/flycheck/src/lib.rs index e8c63d410aa..c3976e6b7a8 100644 --- a/crates/flycheck/src/lib.rs +++ b/crates/flycheck/src/lib.rs @@ -6,6 +6,7 @@ use std::{ fmt, io, + path::Path, process::{ChildStderr, ChildStdout, Command, Stdio}, time::Duration, }; @@ -21,6 +22,14 @@ DiagnosticSpanMacroExpansion, }; +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum InvocationStrategy { + OnceInRoot, + PerWorkspaceWithManifestPath, + #[default] + PerWorkspace, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub enum FlycheckConfig { CargoCommand { @@ -32,11 +41,13 @@ pub enum FlycheckConfig { features: Vec, extra_args: Vec, extra_env: FxHashMap, + invocation_strategy: InvocationStrategy, }, CustomCommand { command: String, args: Vec, extra_env: FxHashMap, + invocation_strategy: InvocationStrategy, }, } @@ -136,7 +147,9 @@ 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, config: FlycheckConfig, @@ -164,9 +177,11 @@ fn new( tracing::info!(%id, ?workspace_root, "Spawning flycheck"); FlycheckActor { id, sender, config, 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) -> Option { let check_chan = self.cargo_handle.as_ref().map(|cargo| &cargo.receiver); if let Ok(msg) = inbox.try_recv() { @@ -178,6 +193,7 @@ fn next_event(&self, inbox: &Receiver) -> Option { recv(check_chan.unwrap_or(&never())) -> msg => Some(Event::CheckEvent(msg.ok())), } } + fn run(mut self, inbox: Receiver) { 'event: while let Some(event) = self.next_event(&inbox) { match event { @@ -194,7 +210,20 @@ fn run(mut self, inbox: Receiver) { } } - let command = self.check_command(); + let mut command = self.check_command(); + let invocation_strategy = self.invocation_strategy(); + match invocation_strategy { + InvocationStrategy::OnceInRoot => (), + InvocationStrategy::PerWorkspaceWithManifestPath => { + command.arg("--manifest-path"); + command.arg(<_ as AsRef>::as_ref( + &self.workspace_root.join("Cargo.toml"), + )); + } + InvocationStrategy::PerWorkspace => { + command.current_dir(&self.workspace_root); + } + } tracing::debug!(?command, "will restart flycheck"); match CargoHandle::spawn(command) { Ok(cargo_handle) => { @@ -203,10 +232,10 @@ fn run(mut self, inbox: Receiver) { "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,11 +255,11 @@ fn run(mut self, inbox: Receiver) { 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) => { @@ -254,7 +283,14 @@ fn cancel_check_process(&mut self) { "did cancel flycheck" ); cargo_handle.cancel(); - self.progress(Progress::DidCancel); + self.report_progress(Progress::DidCancel); + } + } + + fn invocation_strategy(&self) -> InvocationStrategy { + match self.config { + FlycheckConfig::CargoCommand { invocation_strategy, .. } + | FlycheckConfig::CustomCommand { invocation_strategy, .. } => invocation_strategy, } } @@ -269,6 +305,7 @@ fn check_command(&self) -> Command { extra_args, features, extra_env, + invocation_strategy: _, } => { let mut cmd = Command::new(toolchain::cargo()); cmd.arg(command); @@ -297,7 +334,7 @@ fn check_command(&self) -> Command { cmd.envs(extra_env); cmd } - FlycheckConfig::CustomCommand { command, args, extra_env } => { + FlycheckConfig::CustomCommand { command, args, extra_env, invocation_strategy: _ } => { let mut cmd = Command::new(command); cmd.args(args); cmd.envs(extra_env); diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index 79f6ded4899..a61e38706e5 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -130,6 +130,14 @@ struct ConfigData { /// /// Set to `"all"` to pass `--all-features` to Cargo. checkOnSave_features: Option = "null", + /// Specifies the invocation strategy to use when running the checkOnSave command. + /// If `per_workspace_with_manifest_path` is set, the command will be executed for each + /// workspace, `--manifest-path {workspace-dir}` will be passed to the invoked command and + /// the command will be executed from the project root. + /// If `per_workspace` is set, the command will be executed for each workspace and the + /// command will be executed from the corresponding workspace root. + /// If `once_in_root` is set, the command will be executed once in the project root. + checkOnSave_invocationStrategy: InvocationStrategy = "\"per_workspace\"", /// Whether to pass `--no-default-features` to Cargo. Defaults to /// `#rust-analyzer.cargo.noDefaultFeatures#`. checkOnSave_noDefaultFeatures: Option = "null", @@ -1094,6 +1102,13 @@ pub fn flycheck(&self) -> Option { if !self.data.checkOnSave_enable { return None; } + let invocation_strategy = match self.data.cargo_buildScripts_invocationStrategy { + InvocationStrategy::OnceInRoot => flycheck::InvocationStrategy::OnceInRoot, + InvocationStrategy::PerWorkspaceWithManifestPath => { + flycheck::InvocationStrategy::PerWorkspaceWithManifestPath + } + InvocationStrategy::PerWorkspace => flycheck::InvocationStrategy::PerWorkspace, + }; let flycheck_config = match &self.data.checkOnSave_overrideCommand { Some(args) if !args.is_empty() => { let mut args = args.clone(); @@ -1102,6 +1117,7 @@ pub fn flycheck(&self) -> Option { command, args, extra_env: self.check_on_save_extra_env(), + invocation_strategy, } } Some(_) | None => FlycheckConfig::CargoCommand { @@ -1131,6 +1147,7 @@ pub fn flycheck(&self) -> Option { }, extra_args: self.data.checkOnSave_extraArgs.clone(), extra_env: self.check_on_save_extra_env(), + invocation_strategy, }, }; Some(flycheck_config) diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs index bd5741f6159..5382790f6e7 100644 --- a/crates/rust-analyzer/src/reload.rs +++ b/crates/rust-analyzer/src/reload.rs @@ -473,32 +473,45 @@ fn reload_flycheck(&mut self) { }; 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 (FlycheckConfig::CargoCommand { invocation_strategy, .. } + | FlycheckConfig::CustomCommand { invocation_strategy, .. }) = config; + + self.flycheck = match invocation_strategy { + flycheck::InvocationStrategy::OnceInRoot => vec![FlycheckHandle::spawn( + 0, + Box::new(move |msg| sender.send(msg).unwrap()), + config.clone(), + self.config.root_path().clone(), + )], + flycheck::InvocationStrategy::PerWorkspaceWithManifestPath + | 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() + } + }; } } diff --git a/docs/user/generated_config.adoc b/docs/user/generated_config.adoc index a5307b63151..3ced42ef72e 100644 --- a/docs/user/generated_config.adoc +++ b/docs/user/generated_config.adoc @@ -129,6 +129,17 @@ List of features to activate. Defaults to Set to `"all"` to pass `--all-features` to Cargo. -- +[[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_with_manifest_path` is set, the command will be executed for each +workspace, `--manifest-path {workspace-dir}` will be passed to the invoked command and +the command will be executed from the project root. +If `per_workspace` is set, the command will be executed for each workspace and the +command will be executed from the corresponding workspace root. +If `once_in_root` is set, the command will be executed once in the project root. +-- [[rust-analyzer.checkOnSave.noDefaultFeatures]]rust-analyzer.checkOnSave.noDefaultFeatures (default: `null`):: + -- diff --git a/editors/code/package.json b/editors/code/package.json index b1b565106a3..3af32685fda 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -561,6 +561,21 @@ } ] }, + "rust-analyzer.checkOnSave.invocationStrategy": { + "markdownDescription": "Specifies the invocation strategy to use when running the checkOnSave command.\nIf `per_workspace_with_manifest_path` is set, the command will be executed for each\nworkspace, `--manifest-path {workspace-dir}` will be passed to the invoked command and\nthe command will be executed from the project root.\nIf `per_workspace` is set, the command will be executed for each workspace and the\ncommand will be executed from the corresponding workspace root.\nIf `once_in_root` is set, the command will be executed once in the project root.", + "default": "per_workspace", + "type": "string", + "enum": [ + "per_workspace", + "per_workspace_with_manifest_path", + "once_in_root" + ], + "enumDescriptions": [ + "The command will be executed for each workspace, `--manifest-path {workspace-dir}` will be passed to the invoked command and the command will be executed from the project root.", + "The command will be executed for each workspace and the command will be executed from the corresponding workspace root.", + "The command will be executed once in the project root." + ] + }, "rust-analyzer.checkOnSave.noDefaultFeatures": { "markdownDescription": "Whether to pass `--no-default-features` to Cargo. Defaults to\n`#rust-analyzer.cargo.noDefaultFeatures#`.", "default": null, From 5174b65ed8ada51977b5621bc070fe5bf9137dc4 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 15 Sep 2022 13:35:46 +0200 Subject: [PATCH 17/31] Use correct invocation strategy config for checkOnSave --- crates/rust-analyzer/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index a61e38706e5..9f022d6add1 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -1102,7 +1102,7 @@ pub fn flycheck(&self) -> Option { if !self.data.checkOnSave_enable { return None; } - let invocation_strategy = match self.data.cargo_buildScripts_invocationStrategy { + let invocation_strategy = match self.data.checkOnSave_invocationStrategy { InvocationStrategy::OnceInRoot => flycheck::InvocationStrategy::OnceInRoot, InvocationStrategy::PerWorkspaceWithManifestPath => { flycheck::InvocationStrategy::PerWorkspaceWithManifestPath From 7db50294a3e458b7dd00ee646eee75a7ec933e3b Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 26 Sep 2022 15:58:55 +0200 Subject: [PATCH 18/31] {manifest-path} interpolation --- crates/flycheck/src/lib.rs | 69 +++++++++++------------ crates/project-model/src/build_scripts.rs | 66 +++++++++++++++------- crates/project-model/src/lib.rs | 3 +- crates/project-model/src/workspace.rs | 4 +- crates/rust-analyzer/src/config.rs | 38 ++++++------- crates/rust-analyzer/src/reload.rs | 3 +- docs/user/generated_config.adoc | 26 +++++---- editors/code/package.json | 12 ++-- 8 files changed, 118 insertions(+), 103 deletions(-) diff --git a/crates/flycheck/src/lib.rs b/crates/flycheck/src/lib.rs index c3976e6b7a8..cdb3c2969c8 100644 --- a/crates/flycheck/src/lib.rs +++ b/crates/flycheck/src/lib.rs @@ -6,7 +6,6 @@ use std::{ fmt, io, - path::Path, process::{ChildStderr, ChildStdout, Command, Stdio}, time::Duration, }; @@ -25,7 +24,6 @@ #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub enum InvocationStrategy { OnceInRoot, - PerWorkspaceWithManifestPath, #[default] PerWorkspace, } @@ -153,7 +151,9 @@ struct FlycheckActor { id: usize, sender: Box, 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 @@ -175,7 +175,7 @@ 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 report_progress(&self, progress: Progress) { @@ -210,20 +210,7 @@ fn run(mut self, inbox: Receiver) { } } - let mut command = self.check_command(); - let invocation_strategy = self.invocation_strategy(); - match invocation_strategy { - InvocationStrategy::OnceInRoot => (), - InvocationStrategy::PerWorkspaceWithManifestPath => { - command.arg("--manifest-path"); - command.arg(<_ as AsRef>::as_ref( - &self.workspace_root.join("Cargo.toml"), - )); - } - InvocationStrategy::PerWorkspace => { - command.current_dir(&self.workspace_root); - } - } + let command = self.check_command(); tracing::debug!(?command, "will restart flycheck"); match CargoHandle::spawn(command) { Ok(cargo_handle) => { @@ -265,7 +252,7 @@ fn run(mut self, inbox: Receiver) { CargoMessage::Diagnostic(msg) => { self.send(Message::AddDiagnostic { id: self.id, - workspace_root: self.workspace_root.clone(), + workspace_root: self.root.clone(), diagnostic: msg, }); } @@ -287,15 +274,8 @@ fn cancel_check_process(&mut self) { } } - fn invocation_strategy(&self) -> InvocationStrategy { - match self.config { - FlycheckConfig::CargoCommand { invocation_strategy, .. } - | FlycheckConfig::CustomCommand { invocation_strategy, .. } => invocation_strategy, - } - } - fn check_command(&self) -> Command { - let mut cmd = match &self.config { + let (mut cmd, args, invocation_strategy) = match &self.config { FlycheckConfig::CargoCommand { command, target_triple, @@ -305,13 +285,11 @@ fn check_command(&self) -> Command { extra_args, features, extra_env, - invocation_strategy: _, + invocation_strategy, } => { 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()]); @@ -330,18 +308,35 @@ fn check_command(&self) -> Command { cmd.arg(features.join(" ")); } } - cmd.args(extra_args); cmd.envs(extra_env); - cmd + (cmd, extra_args, invocation_strategy) } - FlycheckConfig::CustomCommand { command, args, extra_env, invocation_strategy: _ } => { + FlycheckConfig::CustomCommand { command, args, extra_env, invocation_strategy } => { let mut cmd = Command::new(command); - cmd.args(args); cmd.envs(extra_env); - cmd + (cmd, args, invocation_strategy) } }; - cmd.current_dir(&self.workspace_root); + if let InvocationStrategy::PerWorkspace = invocation_strategy { + let mut with_manifest_path = false; + for arg in args { + if let Some(_) = arg.find("$manifest_path") { + with_manifest_path = true; + cmd.arg(arg.replace( + "$manifest_path", + &self.root.join("Cargo.toml").display().to_string(), + )); + } else { + cmd.arg(arg); + } + } + + if !with_manifest_path { + cmd.current_dir(&self.root); + } + } else { + cmd.args(args); + } cmd } diff --git a/crates/project-model/src/build_scripts.rs b/crates/project-model/src/build_scripts.rs index 6e6654e74e1..12be67d9a0c 100644 --- a/crates/project-model/src/build_scripts.rs +++ b/crates/project-model/src/build_scripts.rs @@ -55,11 +55,37 @@ fn is_unchanged(&self) -> bool { } impl WorkspaceBuildScripts { - fn build_command(config: &CargoConfig) -> io::Result { + fn build_command( + config: &CargoConfig, + workspace_root: Option<&path::Path>, + ) -> io::Result { let mut cmd = match config.run_build_script_command.as_deref() { Some([program, args @ ..]) => { let mut cmd = Command::new(program); - cmd.args(args); + + // FIXME: strategy and workspace root are coupled, express that in code + if let (InvocationStrategy::PerWorkspace, Some(workspace_root)) = + (config.invocation_strategy, workspace_root) + { + let mut with_manifest_path = false; + for arg in args { + if let Some(_) = arg.find("$manifest_path") { + with_manifest_path = true; + cmd.arg(arg.replace( + "$manifest_path", + &workspace_root.join("Cargo.toml").display().to_string(), + )); + } else { + cmd.arg(arg); + } + } + + if !with_manifest_path { + cmd.current_dir(workspace_root); + } + } else { + cmd.args(args); + } cmd } _ => { @@ -90,9 +116,15 @@ fn build_command(config: &CargoConfig) -> io::Result { } } } + + if let Some(workspace_root) = workspace_root { + cmd.current_dir(workspace_root); + } + cmd } }; + cmd.envs(&config.extra_env); if config.wrap_rustc_in_build_scripts { // Setup RUSTC_WRAPPER to point to `rust-analyzer` binary itself. We use @@ -115,15 +147,21 @@ pub(crate) fn run_for_workspace( ) -> io::Result { const RUST_1_62: Version = Version::new(1, 62, 0); - match Self::run_per_ws(Self::build_command(config)?, config, workspace, progress) { + let workspace_root: &path::Path = &workspace.workspace_root().as_ref(); + + match Self::run_per_ws( + Self::build_command(config, Some(workspace_root))?, + 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)?; + let mut cmd = Self::build_command(config, Some(workspace_root))?; cmd.args(&["-Z", "unstable-options", "--keep-going"]).env("RUSTC_BOOTSTRAP", "1"); - let mut res = Self::run_per_ws(cmd, config, workspace, progress)?; + let mut res = Self::run_per_ws(cmd, workspace, progress)?; res.error = Some(error); Ok(res) } @@ -139,7 +177,7 @@ pub(crate) fn run_once( progress: &dyn Fn(String), ) -> io::Result> { assert_eq!(config.invocation_strategy, InvocationStrategy::OnceInRoot); - let cmd = Self::build_command(config)?; + let cmd = Self::build_command(config, None)?; // 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`. @@ -187,24 +225,10 @@ pub(crate) fn run_once( } fn run_per_ws( - mut cmd: Command, - config: &CargoConfig, + cmd: Command, workspace: &CargoWorkspace, progress: &dyn Fn(String), ) -> io::Result { - let workspace_root: &path::Path = &workspace.workspace_root().as_ref(); - - match config.invocation_strategy { - InvocationStrategy::OnceInRoot => (), - InvocationStrategy::PerWorkspaceWithManifestPath => { - cmd.arg("--manifest-path"); - cmd.arg(workspace_root.join("Cargo.toml")); - } - InvocationStrategy::PerWorkspace => { - cmd.current_dir(workspace_root); - } - } - let mut res = WorkspaceBuildScripts::default(); let outputs = &mut res.outputs; // NB: Cargo.toml could have been modified between `cargo metadata` and diff --git a/crates/project-model/src/lib.rs b/crates/project-model/src/lib.rs index 13a86901f71..956c872ccc6 100644 --- a/crates/project-model/src/lib.rs +++ b/crates/project-model/src/lib.rs @@ -158,10 +158,9 @@ fn utf8_stdout(mut cmd: Command) -> Result { Ok(stdout.trim().to_string()) } -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub enum InvocationStrategy { OnceInRoot, - PerWorkspaceWithManifestPath, #[default] PerWorkspace, } diff --git a/crates/project-model/src/workspace.rs b/crates/project-model/src/workspace.rs index 01f5157093a..561bad3a6ab 100644 --- a/crates/project-model/src/workspace.rs +++ b/crates/project-model/src/workspace.rs @@ -323,9 +323,7 @@ pub fn run_all_build_scripts( config: &CargoConfig, progress: &dyn Fn(String), ) -> Vec> { - if let InvocationStrategy::PerWorkspaceWithManifestPath | InvocationStrategy::PerWorkspace = - config.invocation_strategy - { + if let InvocationStrategy::PerWorkspace = config.invocation_strategy { return workspaces.iter().map(|it| it.run_build_scripts(config, progress)).collect(); } diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index 9f022d6add1..3bf28cb7b47 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -70,12 +70,15 @@ struct ConfigData { /// Run build scripts (`build.rs`) for more precise code analysis. cargo_buildScripts_enable: bool = "true", /// Specifies the invocation strategy to use when running the build scripts command. - /// If `per_workspace_with_manifest_path` is set, the command will be executed for each - /// workspace, `--manifest-path {workspace-dir}` will be passed to the invoked command and - /// the command will be executed from the project root. - /// If `per_workspace` is set, the command will be executed for each workspace and the - /// command will be executed from the corresponding workspace root. + /// If `per_workspace` is set, the command will be executed for each workspace and all + /// occurrences of `$manifest_path` in the command will be replaced by the corresponding + /// manifest path of the workspace that the command is being invoked for. If interpolation + /// for the manifest path happens at least once, the commands will be executed from the + /// project root, otherwise the commands will be executed from the corresponding workspace + /// root. /// If `once_in_root` is set, the command will be executed once in the project root. + /// 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 @@ -131,12 +134,15 @@ struct ConfigData { /// Set to `"all"` to pass `--all-features` to Cargo. checkOnSave_features: Option = "null", /// Specifies the invocation strategy to use when running the checkOnSave command. - /// If `per_workspace_with_manifest_path` is set, the command will be executed for each - /// workspace, `--manifest-path {workspace-dir}` will be passed to the invoked command and - /// the command will be executed from the project root. - /// If `per_workspace` is set, the command will be executed for each workspace and the - /// command will be executed from the corresponding workspace root. + /// If `per_workspace` is set, the command will be executed for each workspace and all + /// occurrences of `$manifest_path` in the command will be replaced by the corresponding + /// manifest path of the workspace that the command is being invoked for. If interpolation + /// for the manifest path happens at least once, the commands will be executed from the + /// project root, otherwise the commands will be executed from the corresponding workspace + /// root. /// If `once_in_root` is set, the command will be executed once in the project root. + /// 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#`. @@ -1074,9 +1080,6 @@ pub fn cargo(&self) -> CargoConfig { wrap_rustc_in_build_scripts: self.data.cargo_buildScripts_useRustcWrapper, invocation_strategy: match self.data.cargo_buildScripts_invocationStrategy { InvocationStrategy::OnceInRoot => project_model::InvocationStrategy::OnceInRoot, - InvocationStrategy::PerWorkspaceWithManifestPath => { - project_model::InvocationStrategy::PerWorkspaceWithManifestPath - } InvocationStrategy::PerWorkspace => project_model::InvocationStrategy::PerWorkspace, }, run_build_script_command: self.data.cargo_buildScripts_overrideCommand.clone(), @@ -1104,9 +1107,6 @@ pub fn flycheck(&self) -> Option { } let invocation_strategy = match self.data.checkOnSave_invocationStrategy { InvocationStrategy::OnceInRoot => flycheck::InvocationStrategy::OnceInRoot, - InvocationStrategy::PerWorkspaceWithManifestPath => { - flycheck::InvocationStrategy::PerWorkspaceWithManifestPath - } InvocationStrategy::PerWorkspace => flycheck::InvocationStrategy::PerWorkspace, }; let flycheck_config = match &self.data.checkOnSave_overrideCommand { @@ -1623,7 +1623,6 @@ enum CargoFeaturesDef { #[serde(rename_all = "snake_case")] enum InvocationStrategy { OnceInRoot, - PerWorkspaceWithManifestPath, PerWorkspace, } @@ -2043,10 +2042,9 @@ macro_rules! set { }, "InvocationStrategy" => set! { "type": "string", - "enum": ["per_workspace", "per_workspace_with_manifest_path", "once_in_root"], + "enum": ["per_workspace", "once_in_root"], "enumDescriptions": [ - "The command will be executed for each workspace, `--manifest-path {workspace-dir}` will be passed to the invoked command and the command will be executed from the project root.", - "The command will be executed for each workspace and the command will be executed from the corresponding workspace root.", + "The command will be executed for each workspace and `{manifest-path}` usages will be interpolated with the corresponding workspace manifests. If `{manifest-path}` is used, the commands will be executed in the project root, otherwise in the corresponding workspace roots.", "The command will be executed once in the project root." ], }, diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs index 5382790f6e7..af7a51a68ef 100644 --- a/crates/rust-analyzer/src/reload.rs +++ b/crates/rust-analyzer/src/reload.rs @@ -483,8 +483,7 @@ fn reload_flycheck(&mut self) { config.clone(), self.config.root_path().clone(), )], - flycheck::InvocationStrategy::PerWorkspaceWithManifestPath - | flycheck::InvocationStrategy::PerWorkspace => { + flycheck::InvocationStrategy::PerWorkspace => { self.workspaces .iter() .enumerate() diff --git a/docs/user/generated_config.adoc b/docs/user/generated_config.adoc index 3ced42ef72e..3948d8f7e72 100644 --- a/docs/user/generated_config.adoc +++ b/docs/user/generated_config.adoc @@ -28,12 +28,15 @@ Run build scripts (`build.rs`) for more precise code analysis. + -- Specifies the invocation strategy to use when running the build scripts command. -If `per_workspace_with_manifest_path` is set, the command will be executed for each -workspace, `--manifest-path {workspace-dir}` will be passed to the invoked command and -the command will be executed from the project root. -If `per_workspace` is set, the command will be executed for each workspace and the -command will be executed from the corresponding workspace root. +If `per_workspace` is set, the command will be executed for each workspace and all +occurrences of `$manifest_path` in the command will be replaced by the corresponding +manifest path of the workspace that the command is being invoked for. If interpolation +for the manifest path happens at least once, the commands will be executed from the +project root, otherwise the commands will be executed from the corresponding workspace +root. If `once_in_root` is set, the command will be executed once in the project root. +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`):: + @@ -133,12 +136,15 @@ Set to `"all"` to pass `--all-features` to Cargo. + -- Specifies the invocation strategy to use when running the checkOnSave command. -If `per_workspace_with_manifest_path` is set, the command will be executed for each -workspace, `--manifest-path {workspace-dir}` will be passed to the invoked command and -the command will be executed from the project root. -If `per_workspace` is set, the command will be executed for each workspace and the -command will be executed from the corresponding workspace root. +If `per_workspace` is set, the command will be executed for each workspace and all +occurrences of `$manifest_path` in the command will be replaced by the corresponding +manifest path of the workspace that the command is being invoked for. If interpolation +for the manifest path happens at least once, the commands will be executed from the +project root, otherwise the commands will be executed from the corresponding workspace +root. If `once_in_root` is set, the command will be executed once in the project root. +This config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#` +is set. -- [[rust-analyzer.checkOnSave.noDefaultFeatures]]rust-analyzer.checkOnSave.noDefaultFeatures (default: `null`):: + diff --git a/editors/code/package.json b/editors/code/package.json index 3af32685fda..8c40a3878f5 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -422,17 +422,15 @@ "type": "boolean" }, "rust-analyzer.cargo.buildScripts.invocationStrategy": { - "markdownDescription": "Specifies the invocation strategy to use when running the build scripts command.\nIf `per_workspace_with_manifest_path` is set, the command will be executed for each\nworkspace, `--manifest-path {workspace-dir}` will be passed to the invoked command and\nthe command will be executed from the project root.\nIf `per_workspace` is set, the command will be executed for each workspace and the\ncommand will be executed from the corresponding workspace root.\nIf `once_in_root` is set, the command will be executed once in the project root.", + "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 and all\noccurrences of `$manifest_path` in the command will be replaced by the corresponding\nmanifest path of the workspace that the command is being invoked for. If interpolation\nfor the manifest path happens at least once, the commands will be executed from the\nproject root, otherwise the commands will be executed from the corresponding workspace\nroot.\nIf `once_in_root` is set, the command will be executed once in the project root.\nThis config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`\nis set.", "default": "per_workspace", "type": "string", "enum": [ "per_workspace", - "per_workspace_with_manifest_path", "once_in_root" ], "enumDescriptions": [ - "The command will be executed for each workspace, `--manifest-path {workspace-dir}` will be passed to the invoked command and the command will be executed from the project root.", - "The command will be executed for each workspace and the command will be executed from the corresponding workspace root.", + "The command will be executed for each workspace and `{manifest-path}` usages will be interpolated with the corresponding workspace manifests. If `{manifest-path}` is used, the commands will be executed in the project root, otherwise in the corresponding workspace roots.", "The command will be executed once in the project root." ] }, @@ -562,17 +560,15 @@ ] }, "rust-analyzer.checkOnSave.invocationStrategy": { - "markdownDescription": "Specifies the invocation strategy to use when running the checkOnSave command.\nIf `per_workspace_with_manifest_path` is set, the command will be executed for each\nworkspace, `--manifest-path {workspace-dir}` will be passed to the invoked command and\nthe command will be executed from the project root.\nIf `per_workspace` is set, the command will be executed for each workspace and the\ncommand will be executed from the corresponding workspace root.\nIf `once_in_root` is set, the command will be executed once in the project root.", + "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 and all\noccurrences of `$manifest_path` in the command will be replaced by the corresponding\nmanifest path of the workspace that the command is being invoked for. If interpolation\nfor the manifest path happens at least once, the commands will be executed from the\nproject root, otherwise the commands will be executed from the corresponding workspace\nroot.\nIf `once_in_root` is set, the command will be executed once in the project root.\nThis config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`\nis set.", "default": "per_workspace", "type": "string", "enum": [ "per_workspace", - "per_workspace_with_manifest_path", "once_in_root" ], "enumDescriptions": [ - "The command will be executed for each workspace, `--manifest-path {workspace-dir}` will be passed to the invoked command and the command will be executed from the project root.", - "The command will be executed for each workspace and the command will be executed from the corresponding workspace root.", + "The command will be executed for each workspace and `{manifest-path}` usages will be interpolated with the corresponding workspace manifests. If `{manifest-path}` is used, the commands will be executed in the project root, otherwise in the corresponding workspace roots.", "The command will be executed once in the project root." ] }, From 46732369f4b8246e6dc764ef1aa01d03ab5f52b7 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 19 Oct 2022 23:34:36 +0200 Subject: [PATCH 19/31] Remove simplistic interpolation for manifest-path --- crates/flycheck/src/lib.rs | 26 ++++---------------- crates/project-model/src/build_scripts.rs | 27 ++------------------ crates/project-model/src/lib.rs | 2 +- crates/rust-analyzer/src/config.rs | 30 +++++++++-------------- crates/rust-analyzer/src/reload.rs | 2 +- docs/user/generated_config.adoc | 20 +++++---------- editors/code/package.json | 12 ++++----- 7 files changed, 32 insertions(+), 87 deletions(-) diff --git a/crates/flycheck/src/lib.rs b/crates/flycheck/src/lib.rs index cdb3c2969c8..0debf3270f6 100644 --- a/crates/flycheck/src/lib.rs +++ b/crates/flycheck/src/lib.rs @@ -23,7 +23,7 @@ #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub enum InvocationStrategy { - OnceInRoot, + Once, #[default] PerWorkspace, } @@ -317,26 +317,10 @@ fn check_command(&self) -> Command { (cmd, args, invocation_strategy) } }; - if let InvocationStrategy::PerWorkspace = invocation_strategy { - let mut with_manifest_path = false; - for arg in args { - if let Some(_) = arg.find("$manifest_path") { - with_manifest_path = true; - cmd.arg(arg.replace( - "$manifest_path", - &self.root.join("Cargo.toml").display().to_string(), - )); - } else { - cmd.arg(arg); - } - } - - if !with_manifest_path { - cmd.current_dir(&self.root); - } - } else { - cmd.args(args); - } + match invocation_strategy { + InvocationStrategy::PerWorkspace => cmd.current_dir(&self.root), + InvocationStrategy::Once => cmd.args(args), + }; cmd } diff --git a/crates/project-model/src/build_scripts.rs b/crates/project-model/src/build_scripts.rs index 12be67d9a0c..0bb9bd65dcc 100644 --- a/crates/project-model/src/build_scripts.rs +++ b/crates/project-model/src/build_scripts.rs @@ -62,30 +62,7 @@ fn build_command( let mut cmd = match config.run_build_script_command.as_deref() { Some([program, args @ ..]) => { let mut cmd = Command::new(program); - - // FIXME: strategy and workspace root are coupled, express that in code - if let (InvocationStrategy::PerWorkspace, Some(workspace_root)) = - (config.invocation_strategy, workspace_root) - { - let mut with_manifest_path = false; - for arg in args { - if let Some(_) = arg.find("$manifest_path") { - with_manifest_path = true; - cmd.arg(arg.replace( - "$manifest_path", - &workspace_root.join("Cargo.toml").display().to_string(), - )); - } else { - cmd.arg(arg); - } - } - - if !with_manifest_path { - cmd.current_dir(workspace_root); - } - } else { - cmd.args(args); - } + cmd.args(args); cmd } _ => { @@ -176,7 +153,7 @@ pub(crate) fn run_once( workspaces: &[&CargoWorkspace], progress: &dyn Fn(String), ) -> io::Result> { - assert_eq!(config.invocation_strategy, InvocationStrategy::OnceInRoot); + assert_eq!(config.invocation_strategy, InvocationStrategy::Once); let cmd = Self::build_command(config, None)?; // NB: Cargo.toml could have been modified between `cargo metadata` and // `cargo check`. We shouldn't assume that package ids we see here are diff --git a/crates/project-model/src/lib.rs b/crates/project-model/src/lib.rs index 956c872ccc6..2cc0a60bca0 100644 --- a/crates/project-model/src/lib.rs +++ b/crates/project-model/src/lib.rs @@ -160,7 +160,7 @@ fn utf8_stdout(mut cmd: Command) -> Result { #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub enum InvocationStrategy { - OnceInRoot, + Once, #[default] PerWorkspace, } diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index 3bf28cb7b47..3669fda926a 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -70,13 +70,9 @@ struct ConfigData { /// Run build scripts (`build.rs`) for more precise code analysis. cargo_buildScripts_enable: bool = "true", /// 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 and all - /// occurrences of `$manifest_path` in the command will be replaced by the corresponding - /// manifest path of the workspace that the command is being invoked for. If interpolation - /// for the manifest path happens at least once, the commands will be executed from the - /// project root, otherwise the commands will be executed from the corresponding workspace - /// root. - /// If `once_in_root` is set, the command will be executed once in the project root. + /// If `per_workspace` is set, the command will be executed for each workspace from the + /// corresponding workspace root. + /// If `once` is set, the command will be executed once in the project root. /// This config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#` /// is set. cargo_buildScripts_invocationStrategy: InvocationStrategy = "\"per_workspace\"", @@ -134,13 +130,9 @@ struct ConfigData { /// Set to `"all"` to pass `--all-features` to Cargo. checkOnSave_features: Option = "null", /// Specifies the invocation strategy to use when running the checkOnSave command. - /// If `per_workspace` is set, the command will be executed for each workspace and all - /// occurrences of `$manifest_path` in the command will be replaced by the corresponding - /// manifest path of the workspace that the command is being invoked for. If interpolation - /// for the manifest path happens at least once, the commands will be executed from the - /// project root, otherwise the commands will be executed from the corresponding workspace - /// root. - /// If `once_in_root` is set, the command will be executed once in the project root. + /// If `per_workspace` is set, the command will be executed for each workspace from the + /// corresponding workspace root. + /// If `once` is set, the command will be executed once in the project root. /// This config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#` /// is set. checkOnSave_invocationStrategy: InvocationStrategy = "\"per_workspace\"", @@ -1079,7 +1071,7 @@ pub fn cargo(&self) -> CargoConfig { 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::OnceInRoot => project_model::InvocationStrategy::OnceInRoot, + InvocationStrategy::Once => project_model::InvocationStrategy::Once, InvocationStrategy::PerWorkspace => project_model::InvocationStrategy::PerWorkspace, }, run_build_script_command: self.data.cargo_buildScripts_overrideCommand.clone(), @@ -1106,7 +1098,7 @@ pub fn flycheck(&self) -> Option { return None; } let invocation_strategy = match self.data.checkOnSave_invocationStrategy { - InvocationStrategy::OnceInRoot => flycheck::InvocationStrategy::OnceInRoot, + InvocationStrategy::Once => flycheck::InvocationStrategy::Once, InvocationStrategy::PerWorkspace => flycheck::InvocationStrategy::PerWorkspace, }; let flycheck_config = match &self.data.checkOnSave_overrideCommand { @@ -1622,7 +1614,7 @@ enum CargoFeaturesDef { #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "snake_case")] enum InvocationStrategy { - OnceInRoot, + Once, PerWorkspace, } @@ -2042,9 +2034,9 @@ macro_rules! set { }, "InvocationStrategy" => set! { "type": "string", - "enum": ["per_workspace", "once_in_root"], + "enum": ["per_workspace", "once"], "enumDescriptions": [ - "The command will be executed for each workspace and `{manifest-path}` usages will be interpolated with the corresponding workspace manifests. If `{manifest-path}` is used, the commands will be executed in the project root, otherwise in the corresponding workspace roots.", + "The command will be executed for each workspace from the corresponding workspace root.", "The command will be executed once in the project root." ], }, diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs index af7a51a68ef..f7db62baf2c 100644 --- a/crates/rust-analyzer/src/reload.rs +++ b/crates/rust-analyzer/src/reload.rs @@ -477,7 +477,7 @@ fn reload_flycheck(&mut self) { | FlycheckConfig::CustomCommand { invocation_strategy, .. }) = config; self.flycheck = match invocation_strategy { - flycheck::InvocationStrategy::OnceInRoot => vec![FlycheckHandle::spawn( + flycheck::InvocationStrategy::Once => vec![FlycheckHandle::spawn( 0, Box::new(move |msg| sender.send(msg).unwrap()), config.clone(), diff --git a/docs/user/generated_config.adoc b/docs/user/generated_config.adoc index 3948d8f7e72..e5d4395c345 100644 --- a/docs/user/generated_config.adoc +++ b/docs/user/generated_config.adoc @@ -28,13 +28,9 @@ Run build scripts (`build.rs`) for more precise code analysis. + -- 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 and all -occurrences of `$manifest_path` in the command will be replaced by the corresponding -manifest path of the workspace that the command is being invoked for. If interpolation -for the manifest path happens at least once, the commands will be executed from the -project root, otherwise the commands will be executed from the corresponding workspace -root. -If `once_in_root` is set, the command will be executed once in the project root. +If `per_workspace` is set, the command will be executed for each workspace from the +corresponding workspace root. +If `once` is set, the command will be executed once in the project root. This config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#` is set. -- @@ -136,13 +132,9 @@ Set to `"all"` to pass `--all-features` to Cargo. + -- Specifies the invocation strategy to use when running the checkOnSave command. -If `per_workspace` is set, the command will be executed for each workspace and all -occurrences of `$manifest_path` in the command will be replaced by the corresponding -manifest path of the workspace that the command is being invoked for. If interpolation -for the manifest path happens at least once, the commands will be executed from the -project root, otherwise the commands will be executed from the corresponding workspace -root. -If `once_in_root` is set, the command will be executed once in the project root. +If `per_workspace` is set, the command will be executed for each workspace from the +corresponding workspace root. +If `once` is set, the command will be executed once in the project root. This config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#` is set. -- diff --git a/editors/code/package.json b/editors/code/package.json index 8c40a3878f5..d685d634109 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -422,15 +422,15 @@ "type": "boolean" }, "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 and all\noccurrences of `$manifest_path` in the command will be replaced by the corresponding\nmanifest path of the workspace that the command is being invoked for. If interpolation\nfor the manifest path happens at least once, the commands will be executed from the\nproject root, otherwise the commands will be executed from the corresponding workspace\nroot.\nIf `once_in_root` is set, the command will be executed once in the project root.\nThis config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`\nis set.", + "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 from the\ncorresponding workspace root.\nIf `once` is set, the command will be executed once in the project root.\nThis config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`\nis set.", "default": "per_workspace", "type": "string", "enum": [ "per_workspace", - "once_in_root" + "once" ], "enumDescriptions": [ - "The command will be executed for each workspace and `{manifest-path}` usages will be interpolated with the corresponding workspace manifests. If `{manifest-path}` is used, the commands will be executed in the project root, otherwise in the corresponding workspace roots.", + "The command will be executed for each workspace from the corresponding workspace root.", "The command will be executed once in the project root." ] }, @@ -560,15 +560,15 @@ ] }, "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 and all\noccurrences of `$manifest_path` in the command will be replaced by the corresponding\nmanifest path of the workspace that the command is being invoked for. If interpolation\nfor the manifest path happens at least once, the commands will be executed from the\nproject root, otherwise the commands will be executed from the corresponding workspace\nroot.\nIf `once_in_root` is set, the command will be executed once in the project root.\nThis config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`\nis set.", + "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 from the\ncorresponding workspace root.\nIf `once` is set, the command will be executed once in the project root.\nThis config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`\nis set.", "default": "per_workspace", "type": "string", "enum": [ "per_workspace", - "once_in_root" + "once" ], "enumDescriptions": [ - "The command will be executed for each workspace and `{manifest-path}` usages will be interpolated with the corresponding workspace manifests. If `{manifest-path}` is used, the commands will be executed in the project root, otherwise in the corresponding workspace roots.", + "The command will be executed for each workspace from the corresponding workspace root.", "The command will be executed once in the project root." ] }, From 69b845674c44cc14793a9d3727ff687c0141cf72 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 20 Oct 2022 11:32:02 +0200 Subject: [PATCH 20/31] Don't catch the server activation error --- editors/code/src/main.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index f65620aebc6..c47680fbac5 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -91,10 +91,7 @@ async function activateServer(ctx: Ctx): Promise { ctx.subscriptions ); - await ctx.activate().catch((err) => { - void vscode.window.showErrorMessage(`Cannot activate rust-analyzer server: ${err.message}`); - }); - + await ctx.activate(); return ctx.clientFetcher(); } From 5bff6c55deaf4ae75dc463bb83a39ce2ce8cdc7f Mon Sep 17 00:00:00 2001 From: feniljain Date: Sat, 8 Oct 2022 00:54:57 +0530 Subject: [PATCH 21/31] feat: add multiple getters mode in `generate_getter` This commit adds two modes to generate_getter action. First, the plain old working on single fields. Second, working on a selected range of fields. --- .../src/handlers/generate_delegate_methods.rs | 12 +- .../src/handlers/generate_enum_is_method.rs | 2 +- .../generate_enum_projection_method.rs | 2 +- .../src/handlers/generate_function.rs | 2 +- .../src/handlers/generate_getter.rs | 317 +++++++++++++++--- .../ide-assists/src/handlers/generate_new.rs | 3 +- .../src/handlers/generate_setter.rs | 7 +- crates/ide-assists/src/utils.rs | 12 +- 8 files changed, 290 insertions(+), 67 deletions(-) diff --git a/crates/ide-assists/src/handlers/generate_delegate_methods.rs b/crates/ide-assists/src/handlers/generate_delegate_methods.rs index e5df3cbdaa9..ceae8075503 100644 --- a/crates/ide-assists/src/handlers/generate_delegate_methods.rs +++ b/crates/ide-assists/src/handlers/generate_delegate_methods.rs @@ -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::()?; let field_list = ctx.find_node_at_offset::()?; 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,13 +77,11 @@ 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 method_name = method.name(ctx.db()); - + 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), - format!("Generate delegate for `{field_name}.{method_name}()`"), + format!("Generate delegate for `{}.{}()`", field_name, method.name(ctx.db())), target, |builder| { // Create the function @@ -158,7 +156,7 @@ pub(crate) fn generate_delegate_methods(acc: &mut Assists, ctx: &AssistContext<' } None => { let offset = strukt.syntax().text_range().end(); - let snippet = format!("\n\n{impl_def}"); + let snippet = format!("\n\n{}", impl_def.syntax()); builder.insert(offset, snippet); } } diff --git a/crates/ide-assists/src/handlers/generate_enum_is_method.rs b/crates/ide-assists/src/handlers/generate_enum_is_method.rs index dba98f7a04e..63e91b835f1 100644 --- a/crates/ide-assists/src/handlers/generate_enum_is_method.rs +++ b/crates/ide-assists/src/handlers/generate_enum_is_method.rs @@ -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( diff --git a/crates/ide-assists/src/handlers/generate_enum_projection_method.rs b/crates/ide-assists/src/handlers/generate_enum_projection_method.rs index 402ab1ee79d..bdd3cf4f06c 100644 --- a/crates/ide-assists/src/handlers/generate_enum_projection_method.rs +++ b/crates/ide-assists/src/handlers/generate_enum_projection_method.rs @@ -147,7 +147,7 @@ fn generate_enum_projection_method( 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( diff --git a/crates/ide-assists/src/handlers/generate_function.rs b/crates/ide-assists/src/handlers/generate_function.rs index e6cc8cacedd..c229127e48f 100644 --- a/crates/ide-assists/src/handlers/generate_function.rs +++ b/crates/ide-assists/src/handlers/generate_function.rs @@ -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 { diff --git a/crates/ide-assists/src/handlers/generate_getter.rs b/crates/ide-assists/src/handlers/generate_getter.rs index 09eedb11fbc..5e719142834 100644 --- a/crates/ide-assists/src/handlers/generate_getter.rs +++ b/crates/ide-assists/src/handlers/generate_getter.rs @@ -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,88 +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, + 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::()?; - let field = ctx.find_node_at_offset::()?; + // 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::()?; + let field = ctx.find_node_at_offset::()?; + + 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); - let mut_ = mutable.then(|| "mut ").unwrap_or_default(); - format_to!( - buf, - " {vis}fn {fn_name}(&{mut_}self) -> {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 { + // and fn () { 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, Vec)> { + let mut field_names: Vec = 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::>(); + + 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 { + 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}; @@ -485,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 + } +} + "#, + ); + } } diff --git a/crates/ide-assists/src/handlers/generate_new.rs b/crates/ide-assists/src/handlers/generate_new.rs index c441a2dc48e..17fadea0eaf 100644 --- a/crates/ide-assists/src/handlers/generate_new.rs +++ b/crates/ide-assists/src/handlers/generate_new.rs @@ -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(); diff --git a/crates/ide-assists/src/handlers/generate_setter.rs b/crates/ide-assists/src/handlers/generate_setter.rs index e139bcb1a1b..62f72df1c9d 100644 --- a/crates/ide-assists/src/handlers/generate_setter.rs +++ b/crates/ide-assists/src/handlers/generate_setter.rs @@ -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( diff --git a/crates/ide-assists/src/utils.rs b/crates/ide-assists/src/utils.rs index 38396cd7d7b..db32e7182c4 100644 --- a/crates/ide-assists/src/utils.rs +++ b/crates/ide-assists/src/utils.rs @@ -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> { 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; } } From de195ff97cda417c61c8760bb332167b2c377078 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 20 Oct 2022 19:28:28 +0200 Subject: [PATCH 22/31] fix: Fix DidSaveDocument requests blocking the server on startup --- crates/ide/src/lib.rs | 10 ++ crates/ide/src/parent_module.rs | 10 +- crates/rust-analyzer/src/cargo_target_spec.rs | 6 +- crates/rust-analyzer/src/global_state.rs | 17 +- crates/rust-analyzer/src/handlers.rs | 14 +- crates/rust-analyzer/src/main_loop.rs | 153 ++++++++++-------- crates/rust-analyzer/src/reload.rs | 5 +- 7 files changed, 132 insertions(+), 83 deletions(-) diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index 09a5cb03ecd..416817ca0b4 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -486,6 +486,16 @@ pub fn crates_for(&self, file_id: FileId) -> Cancellable> { 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> { + 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> { + self.with_db(|db| db.relevant_crates(file_id).iter().copied().collect()) + } + /// Returns the edition of the given crate. pub fn crate_edition(&self, crate_id: CrateId) -> Cancellable { self.with_db(|db| db.crate_graph()[crate_id].edition) diff --git a/crates/ide/src/parent_module.rs b/crates/ide/src/parent_module.rs index 9d425954e39..506f9452cf1 100644 --- a/crates/ide/src/parent_module.rs +++ b/crates/ide/src/parent_module.rs @@ -1,8 +1,9 @@ -use hir::Semantics; +use hir::{db::DefDatabase, Semantics}; use ide_db::{ base_db::{CrateId, FileId, FileLoader, FilePosition}, RootDatabase, }; +use itertools::Itertools; use syntax::{ algo::find_node_at_offset, ast::{self, AstNode}, @@ -55,7 +56,12 @@ pub(crate) fn parent_module(db: &RootDatabase, position: FilePosition) -> Vec Vec { - db.relevant_crates(file_id).iter().copied().collect() + 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)] diff --git a/crates/rust-analyzer/src/cargo_target_spec.rs b/crates/rust-analyzer/src/cargo_target_spec.rs index cbde7354761..6ede194babc 100644 --- a/crates/rust-analyzer/src/cargo_target_spec.rs +++ b/crates/rust-analyzer/src/cargo_target_spec.rs @@ -118,7 +118,11 @@ pub(crate) fn for_file( global_state_snapshot: &GlobalStateSnapshot, file_id: FileId, ) -> Result> { - let (cargo_ws, target) = match global_state_snapshot.cargo_target_for_file_id(file_id) { + let crate_id = match &*global_state_snapshot.analysis.crates_for(file_id)? { + &[crate_id, ..] => crate_id, + _ => return Ok(None), + }; + let (cargo_ws, target) = match global_state_snapshot.cargo_target_for_crate_root(crate_id) { Some(it) => it, None => return Ok(None), }; diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs index 4cddb12083a..3fb06c31f7c 100644 --- a/crates/rust-analyzer/src/global_state.rs +++ b/crates/rust-analyzer/src/global_state.rs @@ -8,7 +8,7 @@ use crossbeam_channel::{unbounded, Receiver, Sender}; use flycheck::FlycheckHandle; use ide::{Analysis, AnalysisHost, Cancellable, Change, FileId}; -use ide_db::base_db::{FileLoader, SourceDatabase}; +use ide_db::base_db::{CrateId, FileLoader, SourceDatabase}; use lsp_types::{SemanticTokens, Url}; use parking_lot::{Mutex, RwLock}; use proc_macro_api::ProcMacroServer; @@ -64,7 +64,7 @@ pub(crate) struct GlobalState { pub(crate) source_root_config: SourceRootConfig, pub(crate) proc_macro_clients: Vec>, - pub(crate) flycheck: Vec, + pub(crate) flycheck: Arc<[FlycheckHandle]>, pub(crate) flycheck_sender: Sender, pub(crate) flycheck_receiver: Receiver, @@ -117,6 +117,7 @@ pub(crate) struct GlobalStateSnapshot { vfs: Arc)>>, pub(crate) workspaces: Arc>, 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, 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,10 +400,15 @@ pub(crate) fn anchored_path(&self, path: &AnchoredPathBuf) -> Url { url_from_abs_path(path) } - pub(crate) fn cargo_target_for_file_id( + 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, - file_id: FileId, + crate_id: CrateId, ) -> Option<(&CargoWorkspace, Target)> { + let file_id = self.analysis.crate_root(crate_id).ok()?; let path = self.vfs.read().0.file_path(file_id); let path = path.as_path()?; self.workspaces.iter().find_map(|ws| match ws { diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs index 701a009ea8b..34795a8eb40 100644 --- a/crates/rust-analyzer/src/handlers.rs +++ b/crates/rust-analyzer/src/handlers.rs @@ -1782,7 +1782,15 @@ fn run_rustfmt( ) -> Result>> { let file_id = from_proto::file_id(snap, &text_document.uri)?; let file = snap.analysis.file_text(file_id)?; - let crate_ids = snap.analysis.crates_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()); } diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index 89189cef149..319b86c58b5 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -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); @@ -727,7 +727,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() @@ -774,69 +774,7 @@ fn on_notification(&mut self, not: Notification) -> Result<()> { Ok(()) })? .on::(|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 - .crates_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::>()?; - 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) { @@ -844,13 +782,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::>()?; + 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(()) })? diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs index f7db62baf2c..cc7600a2fa9 100644 --- a/crates/rust-analyzer/src/reload.rs +++ b/crates/rust-analyzer/src/reload.rs @@ -466,7 +466,7 @@ 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; } @@ -510,7 +510,8 @@ fn reload_flycheck(&mut self) { }) .collect() } - }; + } + .into(); } } From e05df93b8ee0936c37df8e368817dc201e07fb75 Mon Sep 17 00:00:00 2001 From: Yotam Ofek Date: Thu, 20 Oct 2022 17:56:51 +0000 Subject: [PATCH 23/31] Workaround the python vscode extension's polyfill --- editors/code/src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index 10e243dc896..0aa998db625 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -191,7 +191,7 @@ export class Config { const VarRegex = new RegExp(/\$\{(.+?)\}/g); export function substituteVSCodeVariableInString(val: string): string { - return val.replaceAll(VarRegex, (substring: string, varName) => { + return val.replace(VarRegex, (substring: string, varName) => { if (typeof varName === "string") { return computeVscodeVar(varName) || substring; } else { From a8e0a20ce4396d3c77adf3b5deb48edfd27d6af2 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 20 Oct 2022 21:12:58 +0200 Subject: [PATCH 24/31] internal: Properly handle language configuration config changes --- editors/code/src/config.ts | 100 ++++++++++++++++++++++++++++--------- editors/code/src/ctx.ts | 8 ++- editors/code/src/main.ts | 60 ++-------------------- 3 files changed, 88 insertions(+), 80 deletions(-) diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index 10e243dc896..61125d7a83e 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -11,13 +11,9 @@ export type RunnableEnvCfg = export class Config { readonly extensionId = "rust-lang.rust-analyzer"; + configureLang: vscode.Disposable | undefined; readonly rootSection = "rust-analyzer"; - private readonly requiresWorkspaceReloadOpts = [ - // 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", @@ -25,9 +21,7 @@ export class Config { "server", "files", "lens", // works as lens.* - ] - .map((opt) => `${this.rootSection}.${opt}`) - .concat(this.requiresWorkspaceReloadOpts); + ].map((opt) => `${this.rootSection}.${opt}`); readonly package: { version: string; @@ -45,6 +39,11 @@ export class Config { ctx.subscriptions ); this.refreshLogging(); + this.configureLanguage(); + } + + dispose() { + this.configureLang?.dispose(); } private refreshLogging() { @@ -58,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; } } diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index e94d4365c37..75c6d4698c1 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts @@ -38,6 +38,7 @@ export class Ctx { this.dispose(); }, }); + extCtx.subscriptions.push(this); this.statusBar.text = "rust-analyzer"; this.statusBar.tooltip = "ready"; this.statusBar.command = "rust-analyzer.analyzerStatus"; @@ -48,10 +49,15 @@ export class Ctx { this.config = new Config(extCtx); } + dispose() { + this.config.dispose(); + } + clientFetcher() { + const self = this; return { get client(): lc.LanguageClient | undefined { - return this.client; + return self.client; }, }; } diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index c47680fbac5..fa7dc6fe304 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -77,10 +77,6 @@ async function activateServer(ctx: Ctx): Promise { await initCommonContext(ctx); - if (ctx.config.typingContinueCommentsOnNewline) { - ctx.pushExtCleanup(configureLanguage()); - } - vscode.workspace.onDidChangeConfiguration( async (_) => { await ctx @@ -129,6 +125,11 @@ async function initCommonContext(ctx: Ctx) { ctx.registerCommand("stopServer", (_) => async () => { // FIXME: We should re-use the client, that is ctx.deactivate() if none of the configs have changed await ctx.disposeClient(); + ctx.setServerStatus({ + health: "ok", + quiescent: true, + message: "server is not running", + }); }); ctx.registerCommand("analyzerStatus", commands.analyzerStatus); ctx.registerCommand("memoryUsage", commands.memoryUsage); @@ -172,54 +173,3 @@ async function initCommonContext(ctx: Ctx) { defaultOnEnter.dispose(); ctx.registerCommand("onEnter", commands.onEnter); } - -/** - * 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 }, - }, - ], - }); -} From 1cb46079e493af318a2dc6f97c93c2e0ac7ecd0e Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 21 Oct 2022 16:00:43 +0200 Subject: [PATCH 25/31] internal: Properly handle commands in the VSCode client when the server is stopped --- editors/code/src/ctx.ts | 80 +++++++++++++++----- editors/code/src/main.ts | 159 ++++++++++++++++++--------------------- 2 files changed, 136 insertions(+), 103 deletions(-) diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index 75c6d4698c1..044a9470aa9 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts @@ -18,6 +18,11 @@ export type Workspace = files: vscode.TextDocument[]; }; +export type CommandFactory = { + enabled: (ctx: Ctx) => Cmd; + disabled?: (ctx: Ctx) => Cmd; +}; + export class Ctx { readonly statusBar: vscode.StatusBarItem; readonly config: Config; @@ -26,31 +31,40 @@ export class Ctx { private _serverPath: string | undefined; private traceOutputChannel: vscode.OutputChannel | undefined; private outputChannel: vscode.OutputChannel | undefined; + private clientSubscriptions: Disposable[]; private state: PersistentState; + private commandFactories: Record; + private commandDisposables: Disposable[]; workspace: Workspace; - constructor(readonly extCtx: vscode.ExtensionContext, workspace: Workspace) { - this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); - extCtx.subscriptions.push(this.statusBar); - extCtx.subscriptions.push({ - dispose() { - this.dispose(); - }, - }); + constructor( + readonly extCtx: vscode.ExtensionContext, + workspace: Workspace, + commandFactories: Record + ) { 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; 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() { @@ -63,7 +77,6 @@ export class Ctx { } async getClient() { - // if server path changes -> dispose if (!this.traceOutputChannel) { this.traceOutputChannel = vscode.window.createOutputChannel( "Rust Analyzer Language Server Trace" @@ -118,7 +131,11 @@ export class Ctx { initializationOptions, serverOptions ); - this.client.onNotification(ra.serverStatus, (params) => this.setServerStatus(params)); + this.pushClientCleanup( + this.client.onNotification(ra.serverStatus, (params) => + this.setServerStatus(params) + ) + ); } return this.client; } @@ -127,16 +144,25 @@ export class Ctx { 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 disposeClient() { - log.info("Deactivating language client"); + 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; @@ -159,6 +185,25 @@ export class Ctx { 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; @@ -194,16 +239,13 @@ export class Ctx { statusBar.text = `${icon}rust-analyzer`; } - registerCommand(name: string, factory: (ctx: Ctx) => Cmd) { - const fullName = `rust-analyzer.${name}`; - const cmd = factory(this); - const d = vscode.commands.registerCommand(fullName, cmd); - this.pushExtCleanup(d); - } - pushExtCleanup(d: Disposable) { this.extCtx.subscriptions.push(d); } + + private pushClientCleanup(d: Disposable) { + this.clientSubscriptions.push(d); + } } export interface Disposable { diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index fa7dc6fe304..8c3a676ffb0 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -2,7 +2,7 @@ import * as vscode from "vscode"; import * as lc from "vscode-languageclient/node"; import * as commands from "./commands"; -import { Ctx, Workspace } from "./ctx"; +import { CommandFactory, Ctx, Workspace } from "./ctx"; import { isRustDocument } from "./util"; import { activateTaskProvider } from "./tasks"; import { setContextValue } from "./util"; @@ -57,7 +57,7 @@ export async function activate( } : { kind: "Workspace Folder" }; - const ctx = new Ctx(context, workspace); + 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) => { @@ -75,8 +75,6 @@ async function activateServer(ctx: Ctx): Promise { ctx.pushExtCleanup(activateTaskProvider(ctx.config)); } - await initCommonContext(ctx); - vscode.workspace.onDidChangeConfiguration( async (_) => { await ctx @@ -91,85 +89,78 @@ async function activateServer(ctx: Ctx): Promise { return ctx.clientFetcher(); } -async function initCommonContext(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" }) - ); - ctx.pushExtCleanup(defaultOnEnter); +function createCommands(): Record { + return { + 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", + }); + }, + }, - // Commands which invokes manually via command palette, shortcut, etc. - ctx.registerCommand("reload", (_) => 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.disposeClient(); - await ctx.activate(); - }); - - ctx.registerCommand("startServer", (_) => async () => { - await ctx.activate(); - }); - ctx.registerCommand("stopServer", (_) => async () => { - // FIXME: We should re-use the client, that is ctx.deactivate() if none of the configs have changed - await ctx.disposeClient(); - ctx.setServerStatus({ - health: "ok", - quiescent: true, - message: "server is not running", - }); - }); - 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); - - 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); - - defaultOnEnter.dispose(); - ctx.registerCommand("onEnter", commands.onEnter); + 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 }, + }; } From 7ee72256eba49faa53b6885460b5769279893885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20Cobos=20=C3=81lvarez?= Date: Sat, 22 Oct 2022 13:50:02 +0200 Subject: [PATCH 26/31] scip: minor clean-ups Avoids a couple redundant hash map lookups and so. --- crates/rust-analyzer/src/cli/scip.rs | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/crates/rust-analyzer/src/cli/scip.rs b/crates/rust-analyzer/src/cli/scip.rs index 2c29b3ee3a6..d70bc3f195f 100644 --- a/crates/rust-analyzer/src/cli/scip.rs +++ b/crates/rust-analyzer/src/cli/scip.rs @@ -75,7 +75,7 @@ pub fn run(self) -> Result<()> { let mut symbols_emitted: HashSet = HashSet::default(); let mut tokens_to_symbol: HashMap = 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,23 @@ 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 => { + occurrence.symbol = tokens_to_symbol + .entry(id) + .or_insert_with(|| { 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 - } - }; + 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 { From bd49d019063478f2f159165734600ee7c6e72ae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20Cobos=20=C3=81lvarez?= Date: Sat, 22 Oct 2022 14:04:47 +0200 Subject: [PATCH 27/31] ide: Remove unnecessary continue. --- crates/ide/src/static_index.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/ide/src/static_index.rs b/crates/ide/src/static_index.rs index 9e5eb909508..27ad1a948d1 100644 --- a/crates/ide/src/static_index.rs +++ b/crates/ide/src/static_index.rs @@ -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 } From ec6d72baa104fd5428af2d5b6c09ed5546a8af40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20Cobos=20=C3=81lvarez?= Date: Fri, 21 Oct 2022 22:04:43 +0200 Subject: [PATCH 28/31] scip: Rewrite tests to be closer to what we actually do. It's also less code. --- crates/rust-analyzer/src/cli/scip.rs | 85 ++++++++++------------------ 1 file changed, 30 insertions(+), 55 deletions(-) diff --git a/crates/rust-analyzer/src/cli/scip.rs b/crates/rust-analyzer/src/cli/scip.rs index d70bc3f195f..8b77ccde0ee 100644 --- a/crates/rust-analyzer/src/cli/scip.rs +++ b/crates/rust-analyzer/src/cli/scip.rs @@ -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}; @@ -109,10 +109,7 @@ pub fn run(self) -> Result<()> { occurrence.symbol = tokens_to_symbol .entry(id) .or_insert_with(|| { - let symbol = match &token.moniker { - Some(moniker) => moniker_to_symbol(&moniker), - None => new_local_symbol(), - }; + let symbol = token_to_symbol(&token).unwrap_or_else(&mut new_local_symbol); scip::symbol::format_symbol(symbol) }) .clone(); @@ -201,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 { 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 @@ -227,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(), @@ -238,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(); @@ -267,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::>(); + 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); } From 6459d7f817ddb91fa2bdc4de8ff61e7905177686 Mon Sep 17 00:00:00 2001 From: Ryo Yoshida Date: Sat, 22 Oct 2022 17:49:38 +0900 Subject: [PATCH 29/31] Support const generics for builtin derive macro --- .../builtin_derive_macro.rs | 36 +++++--- crates/hir-expand/src/builtin_derive_macro.rs | 86 ++++++++----------- 2 files changed, 62 insertions(+), 60 deletions(-) diff --git a/crates/hir-def/src/macro_expansion_tests/builtin_derive_macro.rs b/crates/hir-def/src/macro_expansion_tests/builtin_derive_macro.rs index 6819e9114a0..fafcde25ae7 100644 --- a/crates/hir-def/src/macro_expansion_tests/builtin_derive_macro.rs +++ b/crates/hir-def/src/macro_expansion_tests/builtin_derive_macro.rs @@ -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; "#, - expect![[r##" + expect![[r#" #[derive(Copy)] struct Foo; -impl core::marker::Copy for Foo {}"##]], +impl core::marker::Copy for Foo {}"#]], ); } @@ -70,11 +70,11 @@ fn test_copy_expand_with_lifetimes() { #[derive(Copy)] struct Foo; "#, - expect![[r##" + expect![[r#" #[derive(Copy)] struct Foo; -impl core::marker::Copy for Foo {}"##]], +impl core::marker::Copy for Foo {}"#]], ); } @@ -86,10 +86,26 @@ fn test_clone_expand() { #[derive(Clone)] struct Foo; "#, - expect![[r##" + expect![[r#" #[derive(Clone)] struct Foo; -impl core::clone::Clone for Foo {}"##]], +impl core::clone::Clone for Foo {}"#]], + ); +} + +#[test] +fn test_clone_expand_with_const_generics() { + check( + r#" +//- minicore: derive, clone +#[derive(Clone)] +struct Foo(u32); +"#, + expect![[r#" +#[derive(Clone)] +struct Foo(u32); + +impl core::clone::Clone for Foo {}"#]], ); } diff --git a/crates/hir-expand/src/builtin_derive_macro.rs b/crates/hir-expand/src/builtin_derive_macro.rs index 79989bc2e38..8966047c9b2 100644 --- a/crates/hir-expand/src/builtin_derive_macro.rs +++ b/crates/hir-expand/src/builtin_derive_macro.rs @@ -60,7 +60,8 @@ pub fn find_builtin_derive(ident: &name::Name) -> Option 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>, } fn parse_adt(tt: &tt::Subtree) -> Result { @@ -92,50 +93,22 @@ fn parse_adt(tt: &tt::Subtree) -> Result { 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) -> Vec { - let mut result = Vec::::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 { @@ -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) } From 0f8904ec9cc0d26fda0164bc4a61f8e8ceb4d4ee Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Sat, 22 Oct 2022 23:02:59 +0200 Subject: [PATCH 30/31] Implement invocation location config --- crates/flycheck/src/lib.rs | 48 ++++++++++++--- crates/project-model/src/build_scripts.rs | 41 +++++++------ crates/project-model/src/cargo_workspace.rs | 3 +- crates/project-model/src/lib.rs | 7 +++ crates/project-model/src/workspace.rs | 4 +- crates/rust-analyzer/src/config.rs | 68 ++++++++++++++++----- crates/rust-analyzer/src/reload.rs | 6 +- docs/user/generated_config.adoc | 30 +++++++-- editors/code/package.json | 38 ++++++++++-- 9 files changed, 188 insertions(+), 57 deletions(-) diff --git a/crates/flycheck/src/lib.rs b/crates/flycheck/src/lib.rs index 0debf3270f6..73c3a48b4c5 100644 --- a/crates/flycheck/src/lib.rs +++ b/crates/flycheck/src/lib.rs @@ -28,6 +28,13 @@ pub enum InvocationStrategy { PerWorkspace, } +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub enum InvocationLocation { + Root(AbsPathBuf), + #[default] + Workspace, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub enum FlycheckConfig { CargoCommand { @@ -39,13 +46,13 @@ pub enum FlycheckConfig { features: Vec, extra_args: Vec, extra_env: FxHashMap, - invocation_strategy: InvocationStrategy, }, CustomCommand { command: String, args: Vec, extra_env: FxHashMap, invocation_strategy: InvocationStrategy, + invocation_location: InvocationLocation, }, } @@ -275,7 +282,7 @@ fn cancel_check_process(&mut self) { } fn check_command(&self) -> Command { - let (mut cmd, args, invocation_strategy) = match &self.config { + let (mut cmd, args) = match &self.config { FlycheckConfig::CargoCommand { command, target_triple, @@ -285,7 +292,6 @@ fn check_command(&self) -> Command { extra_args, features, extra_env, - invocation_strategy, } => { let mut cmd = Command::new(toolchain::cargo()); cmd.arg(command); @@ -309,18 +315,40 @@ fn check_command(&self) -> Command { } } cmd.envs(extra_env); - (cmd, extra_args, invocation_strategy) + (cmd, extra_args) } - FlycheckConfig::CustomCommand { command, args, extra_env, invocation_strategy } => { + FlycheckConfig::CustomCommand { + command, + args, + extra_env, + invocation_strategy, + invocation_location, + } => { let mut cmd = Command::new(command); cmd.envs(extra_env); - (cmd, args, invocation_strategy) + + 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) } }; - match invocation_strategy { - InvocationStrategy::PerWorkspace => cmd.current_dir(&self.root), - InvocationStrategy::Once => cmd.args(args), - }; + + cmd.args(args); cmd } diff --git a/crates/project-model/src/build_scripts.rs b/crates/project-model/src/build_scripts.rs index 0bb9bd65dcc..b5f837d3c6b 100644 --- a/crates/project-model/src/build_scripts.rs +++ b/crates/project-model/src/build_scripts.rs @@ -21,7 +21,8 @@ use serde::Deserialize; use crate::{ - cfg_flag::CfgFlag, CargoConfig, CargoFeatures, CargoWorkspace, InvocationStrategy, Package, + cfg_flag::CfgFlag, CargoConfig, CargoFeatures, CargoWorkspace, InvocationLocation, + InvocationStrategy, Package, }; #[derive(Debug, Default, Clone, PartialEq, Eq)] @@ -55,10 +56,7 @@ fn is_unchanged(&self) -> bool { } impl WorkspaceBuildScripts { - fn build_command( - config: &CargoConfig, - workspace_root: Option<&path::Path>, - ) -> io::Result { + fn build_command(config: &CargoConfig, current_dir: &path::Path) -> io::Result { let mut cmd = match config.run_build_script_command.as_deref() { Some([program, args @ ..]) => { let mut cmd = Command::new(program); @@ -94,14 +92,11 @@ fn build_command( } } - if let Some(workspace_root) = workspace_root { - cmd.current_dir(workspace_root); - } - cmd } }; + cmd.current_dir(current_dir); cmd.envs(&config.extra_env); if config.wrap_rustc_in_build_scripts { // Setup RUSTC_WRAPPER to point to `rust-analyzer` binary itself. We use @@ -124,19 +119,21 @@ pub(crate) fn run_for_workspace( ) -> io::Result { const RUST_1_62: Version = Version::new(1, 62, 0); - let workspace_root: &path::Path = &workspace.workspace_root().as_ref(); + 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, Some(workspace_root))?, - workspace, - progress, - ) { + match Self::run_per_ws(Self::build_command(config, current_dir)?, 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, Some(workspace_root))?; + let mut cmd = Self::build_command(config, current_dir)?; cmd.args(&["-Z", "unstable-options", "--keep-going"]).env("RUSTC_BOOTSTRAP", "1"); let mut res = Self::run_per_ws(cmd, workspace, progress)?; res.error = Some(error); @@ -154,7 +151,17 @@ pub(crate) fn run_once( progress: &dyn Fn(String), ) -> io::Result> { assert_eq!(config.invocation_strategy, InvocationStrategy::Once); - let cmd = Self::build_command(config, None)?; + + 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, current_dir.as_path().as_ref())?; // 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`. diff --git a/crates/project-model/src/cargo_workspace.rs b/crates/project-model/src/cargo_workspace.rs index d8f6d62349e..b4c2ba43677 100644 --- a/crates/project-model/src/cargo_workspace.rs +++ b/crates/project-model/src/cargo_workspace.rs @@ -14,7 +14,7 @@ use serde::Deserialize; use serde_json::from_value; -use crate::{utf8_stdout, ManifestPath}; +use crate::{utf8_stdout, InvocationLocation, ManifestPath}; use crate::{CfgOverrides, InvocationStrategy}; /// [`CargoWorkspace`] represents the logical structure of, well, a Cargo @@ -107,6 +107,7 @@ pub struct CargoConfig { /// Extra env vars to set when invoking the cargo command pub extra_env: FxHashMap, pub invocation_strategy: InvocationStrategy, + pub invocation_location: InvocationLocation, } impl CargoConfig { diff --git a/crates/project-model/src/lib.rs b/crates/project-model/src/lib.rs index d116ff3c010..575581fa543 100644 --- a/crates/project-model/src/lib.rs +++ b/crates/project-model/src/lib.rs @@ -164,3 +164,10 @@ pub enum InvocationStrategy { #[default] PerWorkspace, } + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub enum InvocationLocation { + Root(AbsPathBuf), + #[default] + Workspace, +} diff --git a/crates/project-model/src/workspace.rs b/crates/project-model/src/workspace.rs index 01180fcf4c3..0ec5320997c 100644 --- a/crates/project-model/src/workspace.rs +++ b/crates/project-model/src/workspace.rs @@ -332,7 +332,9 @@ pub fn run_all_build_scripts( config: &CargoConfig, progress: &dyn Fn(String), ) -> Vec> { - if let InvocationStrategy::PerWorkspace = config.invocation_strategy { + if matches!(config.invocation_strategy, InvocationStrategy::PerWorkspace) + || config.run_build_script_command.is_some() + { return workspaces.iter().map(|it| it.run_build_scripts(config, progress)).collect(); } diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index 3669fda926a..85322f12a83 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -69,10 +69,16 @@ 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 from the - /// corresponding workspace root. - /// If `once` is set, the command will be executed once in the project root. + /// 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\"", @@ -129,10 +135,17 @@ struct ConfigData { /// /// Set to `"all"` to pass `--all-features` to Cargo. checkOnSave_features: Option = "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 from the - /// corresponding workspace root. - /// If `once` is set, the command will be executed once in the project root. + /// 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\"", @@ -1074,6 +1087,12 @@ pub fn cargo(&self) -> CargoConfig { 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(), } @@ -1097,10 +1116,6 @@ pub fn flycheck(&self) -> Option { if !self.data.checkOnSave_enable { return None; } - let invocation_strategy = match self.data.checkOnSave_invocationStrategy { - InvocationStrategy::Once => flycheck::InvocationStrategy::Once, - InvocationStrategy::PerWorkspace => flycheck::InvocationStrategy::PerWorkspace, - }; let flycheck_config = match &self.data.checkOnSave_overrideCommand { Some(args) if !args.is_empty() => { let mut args = args.clone(); @@ -1109,7 +1124,18 @@ pub fn flycheck(&self) -> Option { command, args, extra_env: self.check_on_save_extra_env(), - invocation_strategy, + 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 { @@ -1139,7 +1165,6 @@ pub fn flycheck(&self) -> Option { }, extra_args: self.data.checkOnSave_extraArgs.clone(), extra_env: self.check_on_save_extra_env(), - invocation_strategy, }, }; Some(flycheck_config) @@ -1618,6 +1643,13 @@ enum InvocationStrategy { PerWorkspace, } +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +enum InvocationLocation { + Root, + Workspace, +} + #[derive(Deserialize, Debug, Clone)] #[serde(untagged)] enum LifetimeElisionDef { @@ -2036,8 +2068,16 @@ macro_rules! set { "type": "string", "enum": ["per_workspace", "once"], "enumDescriptions": [ - "The command will be executed for each workspace from the corresponding workspace root.", - "The command will be executed once in the project root." + "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), diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs index cc7600a2fa9..e1f651786de 100644 --- a/crates/rust-analyzer/src/reload.rs +++ b/crates/rust-analyzer/src/reload.rs @@ -473,8 +473,10 @@ fn reload_flycheck(&mut self) { }; let sender = self.flycheck_sender.clone(); - let (FlycheckConfig::CargoCommand { invocation_strategy, .. } - | FlycheckConfig::CustomCommand { invocation_strategy, .. }) = config; + 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( diff --git a/docs/user/generated_config.adoc b/docs/user/generated_config.adoc index e5d4395c345..502833de72c 100644 --- a/docs/user/generated_config.adoc +++ b/docs/user/generated_config.adoc @@ -24,13 +24,22 @@ 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 from the -corresponding workspace root. -If `once` is set, the command will be executed once in the project root. +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. -- @@ -128,13 +137,22 @@ 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 from the -corresponding workspace root. -If `once` is set, the command will be executed once in the project root. +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. -- diff --git a/editors/code/package.json b/editors/code/package.json index 62ac1e60b00..6771cad28a7 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -432,8 +432,21 @@ "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 from the\ncorresponding workspace root.\nIf `once` is set, the command will be executed once in the project root.\nThis config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`\nis set.", + "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": [ @@ -441,8 +454,8 @@ "once" ], "enumDescriptions": [ - "The command will be executed for each workspace from the corresponding workspace root.", - "The command will be executed once in the project root." + "The command will be executed for each workspace.", + "The command will be executed once." ] }, "rust-analyzer.cargo.buildScripts.overrideCommand": { @@ -570,8 +583,21 @@ } ] }, + "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 from the\ncorresponding workspace root.\nIf `once` is set, the command will be executed once in the project root.\nThis config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`\nis set.", + "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": [ @@ -579,8 +605,8 @@ "once" ], "enumDescriptions": [ - "The command will be executed for each workspace from the corresponding workspace root.", - "The command will be executed once in the project root." + "The command will be executed for each workspace.", + "The command will be executed once." ] }, "rust-analyzer.checkOnSave.noDefaultFeatures": { From 859f5594acced1ebd9ca3b0f4705c94a326f84e9 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Sun, 23 Oct 2022 18:01:35 +0200 Subject: [PATCH 31/31] Handle multiple projects sharing dependency correctly in `once` strategy --- crates/project-model/src/build_scripts.rs | 34 +++++++++++++++++------ crates/project-model/src/workspace.rs | 2 +- crates/rust-analyzer/src/main_loop.rs | 5 +++- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/crates/project-model/src/build_scripts.rs b/crates/project-model/src/build_scripts.rs index b5f837d3c6b..a26a7c57acf 100644 --- a/crates/project-model/src/build_scripts.rs +++ b/crates/project-model/src/build_scripts.rs @@ -56,7 +56,7 @@ fn is_unchanged(&self) -> bool { } impl WorkspaceBuildScripts { - fn build_command(config: &CargoConfig, current_dir: &path::Path) -> io::Result { + fn build_command(config: &CargoConfig) -> io::Result { let mut cmd = match config.run_build_script_command.as_deref() { Some([program, args @ ..]) => { let mut cmd = Command::new(program); @@ -96,7 +96,6 @@ fn build_command(config: &CargoConfig, current_dir: &path::Path) -> io::Result= 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, current_dir)?; + 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, progress)?; + let mut res = Self::run_per_ws(cmd, workspace, current_dir, progress)?; res.error = Some(error); Ok(res) } @@ -161,11 +160,14 @@ pub(crate) fn run_once( )) } }; - let cmd = Self::build_command(config, current_dir.as_path().as_ref())?; + 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() @@ -173,7 +175,11 @@ pub(crate) fn run_once( let mut res = WorkspaceBuildScripts::default(); for package in workspace.packages() { res.outputs.insert(package, BuildScriptOutput::default()); - by_id.insert(workspace[package].id.clone(), (package, idx)); + 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 }) @@ -181,6 +187,7 @@ pub(crate) fn run_once( 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]); @@ -189,6 +196,11 @@ pub(crate) fn run_once( 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() { @@ -211,6 +223,7 @@ pub(crate) fn run_once( fn run_per_ws( cmd: Command, workspace: &CargoWorkspace, + current_dir: &path::Path, progress: &dyn Fn(String), ) -> io::Result { let mut res = WorkspaceBuildScripts::default(); @@ -226,6 +239,7 @@ fn run_per_ws( 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]); @@ -251,7 +265,8 @@ fn run_per_ws( } fn run_command( - cmd: 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 @@ -265,7 +280,8 @@ fn run_command( 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| { diff --git a/crates/project-model/src/workspace.rs b/crates/project-model/src/workspace.rs index 0ec5320997c..2780c62ed11 100644 --- a/crates/project-model/src/workspace.rs +++ b/crates/project-model/src/workspace.rs @@ -333,7 +333,7 @@ pub fn run_all_build_scripts( progress: &dyn Fn(String), ) -> Vec> { if matches!(config.invocation_strategy, InvocationStrategy::PerWorkspace) - || config.run_build_script_command.is_some() + || config.run_build_script_command.is_none() { return workspaces.iter().map(|it| it.run_build_scripts(config, progress)).collect(); } diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index 319b86c58b5..2c928a58040 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -543,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 + ); } }; }