diff --git a/Cargo.lock b/Cargo.lock index 871c6d56cda..9083cd1be9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1345,6 +1345,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.9.0" @@ -4509,6 +4515,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "prettydiff" version = "0.5.1" @@ -5882,6 +5898,7 @@ dependencies = [ "notify", "notify-debouncer-mini", "parking_lot 0.12.1", + "pretty_assertions", "proc-macro2", "quote", "rayon", @@ -7290,6 +7307,12 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "yansi-term" version = "0.1.2" diff --git a/sway-ast/src/item/item_use.rs b/sway-ast/src/item/item_use.rs index c780631dc6a..f826988b51e 100644 --- a/sway-ast/src/item/item_use.rs +++ b/sway-ast/src/item/item_use.rs @@ -46,3 +46,16 @@ pub enum UseTree { spans: Box<[Span]>, }, } + +impl Spanned for UseTree { + fn span(&self) -> Span { + match self { + UseTree::Group { imports } => imports.span(), + UseTree::Name { name } => name.span(), + UseTree::Rename { name, alias, .. } => Span::join(name.span(), alias.span()), + UseTree::Glob { star_token } => star_token.span(), + UseTree::Path { prefix, suffix, .. } => Span::join(prefix.span(), suffix.span()), + UseTree::Error { spans } => Span::join_all(spans.to_vec().clone()), + } + } +} diff --git a/sway-core/src/language/call_path.rs b/sway-core/src/language/call_path.rs index cea47e86bf1..28afc9d09f5 100644 --- a/sway-core/src/language/call_path.rs +++ b/sway-core/src/language/call_path.rs @@ -176,7 +176,7 @@ pub struct CallPath { pub suffix: T, // If `is_absolute` is true, then this call path is an absolute path from // the project root namespace. If not, then it is relative to the current namespace. - pub(crate) is_absolute: bool, + pub is_absolute: bool, } impl std::convert::From for CallPath { @@ -258,6 +258,19 @@ impl CallPath { } } + /// Removes the first prefix. Does nothing if prefixes are empty. + pub fn lshift(&self) -> CallPath { + if self.prefixes.is_empty() { + self.clone() + } else { + CallPath { + prefixes: self.prefixes[1..self.prefixes.len()].to_vec(), + suffix: self.suffix.clone(), + is_absolute: self.is_absolute, + } + } + } + pub fn as_vec_string(&self) -> Vec { self.prefixes .iter() @@ -266,7 +279,7 @@ impl CallPath { .collect::>() } - /// Convert a given `CallPath` to an symbol to a full `CallPath` from the root of the project + /// Convert a given [CallPath] to an symbol to a full [CallPath] from the root of the project /// in which the symbol is declared. For example, given a path `pkga::SOME_CONST` where `pkga` /// is an _internal_ library of a package named `my_project`, the corresponding call path is /// `my_project::pkga::SOME_CONST`. @@ -353,4 +366,21 @@ impl CallPath { } } } + + /// Convert a given [CallPath] into a call path suitable for a `use` statement. + /// + /// For example, given a path `pkga::SOME_CONST` where `pkga` is an _internal_ library of a package named + /// `my_project`, the corresponding call path is `pkga::SOME_CONST`. + /// + /// Paths to _external_ libraries such `std::lib1::lib2::my_obj` are left unchanged. + pub fn to_import_path(&self, namespace: &Namespace) -> CallPath { + let converted = self.to_fullpath(namespace); + + if let Some(first) = converted.prefixes.first() { + if namespace.root().name == Some(first.clone()) { + return converted.lshift(); + } + } + converted + } } diff --git a/sway-core/src/language/lexed/mod.rs b/sway-core/src/language/lexed/mod.rs index 2e6a30a85fc..4ca08a948a8 100644 --- a/sway-core/src/language/lexed/mod.rs +++ b/sway-core/src/language/lexed/mod.rs @@ -4,6 +4,8 @@ use crate::language::ModName; pub use program::LexedProgram; use sway_ast::Module; +use super::{HasModule, HasSubmodules}; + /// A module and its submodules in the form of a tree. #[derive(Debug, Clone)] pub struct LexedModule { @@ -20,3 +22,15 @@ pub struct LexedModule { pub struct LexedSubmodule { pub module: LexedModule, } + +impl HasModule for LexedSubmodule { + fn module(&self) -> &LexedModule { + &self.module + } +} + +impl HasSubmodules for LexedModule { + fn submodules(&self) -> &[(ModName, LexedSubmodule)] { + &self.submodules + } +} diff --git a/sway-core/src/language/module.rs b/sway-core/src/language/module.rs index f19832b2367..2649ddd416e 100644 --- a/sway-core/src/language/module.rs +++ b/sway-core/src/language/module.rs @@ -5,3 +5,72 @@ use sway_types::Ident; /// If an alias was given to the `mod`, this will be the alias. If not, this is the submodule's /// library name. pub type ModName = Ident; + +pub trait HasModule +where + T: HasSubmodules, + Self: Sized, +{ + /// Returns the module of this submodule. + fn module(&self) -> &T; +} + +pub trait HasSubmodules +where + E: HasModule, + Self: Sized, +{ + /// Returns the submodules of this module. + fn submodules(&self) -> &[(ModName, E)]; + + /// An iterator yielding all submodules recursively, depth-first. + fn submodules_recursive(&self) -> SubmodulesRecursive { + SubmodulesRecursive { + _module_type: std::marker::PhantomData, + submods: self.submodules().iter(), + current: None, + } + } +} + +type NamedSubmodule = (ModName, E); +type SubmoduleItem<'module, T, E> = ( + &'module NamedSubmodule, + Box>, +); + +/// Iterator type for iterating over submodules. +/// +/// Used rather than `impl Iterator` to enable recursive submodule iteration. +pub struct SubmodulesRecursive<'module, T, E> { + _module_type: std::marker::PhantomData, + submods: std::slice::Iter<'module, NamedSubmodule>, + current: Option>, +} + +impl<'module, T, E> Iterator for SubmodulesRecursive<'module, T, E> +where + T: HasSubmodules + 'module, + E: HasModule, +{ + type Item = &'module (ModName, E); + fn next(&mut self) -> Option { + loop { + self.current = match self.current.take() { + None => match self.submods.next() { + None => return None, + Some(submod) => { + Some((submod, Box::new(submod.1.module().submodules_recursive()))) + } + }, + Some((submod, mut submods)) => match submods.next() { + Some(next) => { + self.current = Some((submod, submods)); + return Some(next); + } + None => return Some(submod), + }, + } + } + } +} diff --git a/sway-core/src/language/parsed/include_statement.rs b/sway-core/src/language/parsed/include_statement.rs index 1c48a3085c3..cad0fc4a48c 100644 --- a/sway-core/src/language/parsed/include_statement.rs +++ b/sway-core/src/language/parsed/include_statement.rs @@ -1,8 +1,11 @@ -use sway_types::span::Span; +use sway_types::{span::Span, Ident}; + +use crate::language::Visibility; #[derive(Clone, Debug)] pub struct IncludeStatement { // this span may be used for errors in the future, although it is not right now. - pub(crate) _span: Span, - pub(crate) _mod_name_span: Span, + pub span: Span, + pub mod_name: Ident, + pub visibility: Visibility, } diff --git a/sway-core/src/language/parsed/mod.rs b/sway-core/src/language/parsed/mod.rs index 7c3860f36f5..d4f93d20a26 100644 --- a/sway-core/src/language/parsed/mod.rs +++ b/sway-core/src/language/parsed/mod.rs @@ -11,7 +11,7 @@ mod use_statement; pub use code_block::*; pub use declaration::*; pub use expression::*; -pub(crate) use include_statement::IncludeStatement; +pub use include_statement::IncludeStatement; pub use module::{ParseModule, ParseSubmodule}; pub use program::{ParseProgram, TreeType}; pub use return_statement::*; diff --git a/sway-core/src/language/parsed/module.rs b/sway-core/src/language/parsed/module.rs index 661d4217404..41476852379 100644 --- a/sway-core/src/language/parsed/module.rs +++ b/sway-core/src/language/parsed/module.rs @@ -1,5 +1,5 @@ use crate::{ - language::{ModName, Visibility}, + language::{HasModule, HasSubmodules, ModName, Visibility}, transform, }; @@ -33,3 +33,15 @@ pub struct ParseSubmodule { pub mod_name_span: Span, pub visibility: Visibility, } + +impl HasModule for ParseSubmodule { + fn module(&self) -> &ParseModule { + &self.module + } +} + +impl HasSubmodules for ParseModule { + fn submodules(&self) -> &[(ModName, ParseSubmodule)] { + &self.submodules + } +} diff --git a/sway-core/src/language/parsed/use_statement.rs b/sway-core/src/language/parsed/use_statement.rs index c5e0c1db475..9734d4f8bfc 100644 --- a/sway-core/src/language/parsed/use_statement.rs +++ b/sway-core/src/language/parsed/use_statement.rs @@ -12,6 +12,7 @@ pub enum ImportType { #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct UseStatement { pub call_path: Vec, + pub span: Span, pub import_type: ImportType, // If `is_absolute` is true, then this use statement is an absolute path from // the project root namespace. If not, then it is relative to the current namespace. diff --git a/sway-core/src/language/ty/declaration/function.rs b/sway-core/src/language/ty/declaration/function.rs index 87d98dfab52..6445395ca9b 100644 --- a/sway-core/src/language/ty/declaration/function.rs +++ b/sway-core/src/language/ty/declaration/function.rs @@ -7,7 +7,7 @@ use std::{ use sha2::{Digest, Sha256}; use sway_error::handler::{ErrorEmitted, Handler}; -use crate::semantic_analysis::type_check_context::MonomorphizeHelper; +use crate::{language::CallPath, semantic_analysis::type_check_context::MonomorphizeHelper}; use crate::{ decl_engine::*, @@ -31,6 +31,7 @@ pub struct TyFunctionDecl { pub parameters: Vec, pub implementing_type: Option, pub span: Span, + pub call_path: CallPath, pub attributes: transform::AttributesMap, pub type_parameters: Vec, pub return_type: TypeArgument, @@ -91,6 +92,7 @@ impl HashWithEngines for TyFunctionDecl { purity, // these fields are not hashed because they aren't relevant/a // reliable source of obj v. obj distinction + call_path: _, span: _, attributes: _, implementing_type: _, @@ -234,6 +236,7 @@ impl TyFunctionDecl { }, implementing_type: None, span, + call_path: CallPath::from(Ident::dummy()), attributes: Default::default(), is_contract_call: false, parameters: Default::default(), diff --git a/sway-core/src/language/ty/declaration/trait.rs b/sway-core/src/language/ty/declaration/trait.rs index f033ad6fe9c..dc9d667fd76 100644 --- a/sway-core/src/language/ty/declaration/trait.rs +++ b/sway-core/src/language/ty/declaration/trait.rs @@ -9,7 +9,7 @@ use crate::{ DeclRefTraitType, ReplaceFunctionImplementingType, }, engine_threading::*, - language::{parsed, Visibility}, + language::{parsed, CallPath, Visibility}, semantic_analysis::{ type_check_context::MonomorphizeHelper, TypeCheckAnalysis, TypeCheckAnalysisContext, TypeCheckFinalization, TypeCheckFinalizationContext, @@ -30,6 +30,7 @@ pub struct TyTraitDecl { pub supertraits: Vec, pub visibility: Visibility, pub attributes: transform::AttributesMap, + pub call_path: CallPath, pub span: Span, } @@ -85,6 +86,7 @@ impl HashWithEngines for TyTraitDecl { // reliable source of obj v. obj distinction attributes: _, span: _, + call_path: _, } = self; name.hash(state); type_parameters.hash(state, engines); diff --git a/sway-core/src/language/ty/declaration/type_alias.rs b/sway-core/src/language/ty/declaration/type_alias.rs index e94df224be5..ee4b4e69006 100644 --- a/sway-core/src/language/ty/declaration/type_alias.rs +++ b/sway-core/src/language/ty/declaration/type_alias.rs @@ -2,11 +2,17 @@ use std::hash::{Hash, Hasher}; use sway_types::{Ident, Named, Span, Spanned}; -use crate::{engine_threading::*, language::Visibility, transform, type_system::*}; +use crate::{ + engine_threading::*, + language::{CallPath, Visibility}, + transform, + type_system::*, +}; #[derive(Clone, Debug)] pub struct TyTypeAliasDecl { pub name: Ident, + pub call_path: CallPath, pub attributes: transform::AttributesMap, pub ty: TypeArgument, pub visibility: Visibility, @@ -36,6 +42,7 @@ impl HashWithEngines for TyTypeAliasDecl { visibility, // these fields are not hashed because they aren't relevant/a // reliable source of obj v. obj distinction + call_path: _, span: _, attributes: _, } = self; diff --git a/sway-core/src/language/ty/module.rs b/sway-core/src/language/ty/module.rs index ea895e2484c..6abcefe1c95 100644 --- a/sway-core/src/language/ty/module.rs +++ b/sway-core/src/language/ty/module.rs @@ -3,8 +3,8 @@ use sway_types::Span; use crate::{ decl_engine::{DeclEngine, DeclRef, DeclRefFunction}, - language::ty::*, language::ModName, + language::{ty::*, HasModule, HasSubmodules}, semantic_analysis::namespace, transform::{self, AllowDeprecatedState}, Engines, @@ -110,3 +110,15 @@ impl<'module> Iterator for SubmodulesRecursive<'module> { } } } + +impl HasModule for TySubmodule { + fn module(&self) -> &TyModule { + &self.module + } +} + +impl HasSubmodules for TyModule { + fn submodules(&self) -> &[(ModName, TySubmodule)] { + &self.submodules + } +} diff --git a/sway-core/src/language/ty/side_effect/include_statement.rs b/sway-core/src/language/ty/side_effect/include_statement.rs new file mode 100644 index 00000000000..5d35f02b722 --- /dev/null +++ b/sway-core/src/language/ty/side_effect/include_statement.rs @@ -0,0 +1,16 @@ +use crate::language::Visibility; + +use sway_types::{ident::Ident, Span, Spanned}; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct TyIncludeStatement { + pub span: Span, + pub visibility: Visibility, + pub mod_name: Ident, +} + +impl Spanned for TyIncludeStatement { + fn span(&self) -> Span { + self.span.clone() + } +} diff --git a/sway-core/src/language/ty/side_effect/mod.rs b/sway-core/src/language/ty/side_effect/mod.rs index a3616223506..e70b040de37 100644 --- a/sway-core/src/language/ty/side_effect/mod.rs +++ b/sway-core/src/language/ty/side_effect/mod.rs @@ -1,6 +1,8 @@ +mod include_statement; #[allow(clippy::module_inception)] mod side_effect; mod use_statement; +pub use include_statement::*; pub use side_effect::*; pub use use_statement::*; diff --git a/sway-core/src/language/ty/side_effect/side_effect.rs b/sway-core/src/language/ty/side_effect/side_effect.rs index e92d752fc39..41f2a5b945b 100644 --- a/sway-core/src/language/ty/side_effect/side_effect.rs +++ b/sway-core/src/language/ty/side_effect/side_effect.rs @@ -1,4 +1,4 @@ -use super::TyUseStatement; +use super::{TyIncludeStatement, TyUseStatement}; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct TySideEffect { @@ -7,6 +7,6 @@ pub struct TySideEffect { #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum TySideEffectVariant { - IncludeStatement, + IncludeStatement(TyIncludeStatement), UseStatement(TyUseStatement), } diff --git a/sway-core/src/language/ty/side_effect/use_statement.rs b/sway-core/src/language/ty/side_effect/use_statement.rs index a47b43b630b..5d9c0cf1ca1 100644 --- a/sway-core/src/language/ty/side_effect/use_statement.rs +++ b/sway-core/src/language/ty/side_effect/use_statement.rs @@ -1,12 +1,19 @@ use crate::language::parsed; -use sway_types::ident::Ident; +use sway_types::{ident::Ident, Span, Spanned}; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct TyUseStatement { pub call_path: Vec, + pub span: Span, pub import_type: parsed::ImportType, // If `is_absolute` is true, then this use statement is an absolute path from // the project root namespace. If not, then it is relative to the current namespace. pub is_absolute: bool, pub alias: Option, } + +impl Spanned for TyUseStatement { + fn span(&self) -> Span { + self.span.clone() + } +} diff --git a/sway-core/src/semantic_analysis/ast_node/declaration/declaration.rs b/sway-core/src/semantic_analysis/ast_node/declaration/declaration.rs index bd796d21e29..24c8c68cbb1 100644 --- a/sway-core/src/semantic_analysis/ast_node/declaration/declaration.rs +++ b/sway-core/src/semantic_analysis/ast_node/declaration/declaration.rs @@ -6,6 +6,7 @@ use crate::{ language::{ parsed, ty::{self, TyDecl}, + CallPath, }, namespace::{IsExtendingExistingImpl, IsImplSelf}, semantic_analysis::{ @@ -355,6 +356,7 @@ impl TyDecl { // create the type alias decl using the resolved type above let decl = ty::TyTypeAliasDecl { name: name.clone(), + call_path: CallPath::from(name.clone()).to_fullpath(ctx.namespace), attributes: decl.attributes, ty: TypeArgument { initial_type_id: ty.initial_type_id, diff --git a/sway-core/src/semantic_analysis/ast_node/declaration/function.rs b/sway-core/src/semantic_analysis/ast_node/declaration/function.rs index 8022b357b6e..6eec2c3e018 100644 --- a/sway-core/src/semantic_analysis/ast_node/declaration/function.rs +++ b/sway-core/src/semantic_analysis/ast_node/declaration/function.rs @@ -11,7 +11,7 @@ use crate::{ language::{ parsed::*, ty::{self, TyCodeBlock}, - Visibility, + CallPath, Visibility, }, semantic_analysis::{type_check_context::EnforceTypeArguments, *}, type_system::*, @@ -127,12 +127,15 @@ impl ty::TyFunctionDecl { (visibility, matches!(ctx.abi_mode(), AbiMode::ImplAbiFn(..))) }; + let call_path = CallPath::from(name.clone()).to_fullpath(ctx.namespace); + let function_decl = ty::TyFunctionDecl { name, body: TyCodeBlock::default(), parameters: new_parameters, implementing_type: None, span, + call_path, attributes, return_type, type_parameters: new_type_parameters, @@ -301,11 +304,12 @@ fn test_function_selector_behavior() { let handler = Handler::default(); let decl = ty::TyFunctionDecl { purity: Default::default(), - name: Ident::new_no_span("foo".into()), + name: Ident::dummy(), implementing_type: None, body: ty::TyCodeBlock { contents: vec![] }, parameters: vec![], span: Span::dummy(), + call_path: CallPath::from(Ident::dummy()), attributes: Default::default(), return_type: TypeId::from(0).into(), type_parameters: vec![], @@ -328,7 +332,7 @@ fn test_function_selector_behavior() { body: ty::TyCodeBlock { contents: vec![] }, parameters: vec![ ty::TyFunctionParameter { - name: Ident::new_no_span("foo".into()), + name: Ident::dummy(), is_reference: false, is_mutable: false, mutability_span: Span::dummy(), @@ -359,6 +363,7 @@ fn test_function_selector_behavior() { }, ], span: Span::dummy(), + call_path: CallPath::from(Ident::dummy()), attributes: Default::default(), return_type: TypeId::from(0).into(), type_parameters: vec![], diff --git a/sway-core/src/semantic_analysis/ast_node/declaration/trait.rs b/sway-core/src/semantic_analysis/ast_node/declaration/trait.rs index 026127a7113..4b146664934 100644 --- a/sway-core/src/semantic_analysis/ast_node/declaration/trait.rs +++ b/sway-core/src/semantic_analysis/ast_node/declaration/trait.rs @@ -206,7 +206,7 @@ impl TyTraitDecl { } let typed_trait_decl = ty::TyTraitDecl { - name, + name: name.clone(), type_parameters: new_type_parameters, self_type: self_type_param, interface_surface: new_interface_surface, @@ -214,6 +214,7 @@ impl TyTraitDecl { supertraits, visibility, attributes, + call_path: CallPath::from(name).to_fullpath(ctx.namespace), span, }; Ok(typed_trait_decl) diff --git a/sway-core/src/semantic_analysis/ast_node/declaration/trait_fn.rs b/sway-core/src/semantic_analysis/ast_node/declaration/trait_fn.rs index a1ff1d36ffd..a4ea2157d7d 100644 --- a/sway-core/src/semantic_analysis/ast_node/declaration/trait_fn.rs +++ b/sway-core/src/semantic_analysis/ast_node/declaration/trait_fn.rs @@ -2,7 +2,7 @@ use sway_types::{Span, Spanned}; use crate::{ decl_engine::DeclId, - language::{parsed, ty, Visibility}, + language::{parsed, ty, CallPath, Visibility}, semantic_analysis::type_check_context::EnforceTypeArguments, }; use sway_error::handler::{ErrorEmitted, Handler}; @@ -98,6 +98,7 @@ impl ty::TyTraitFn { AbiMode::NonAbi => None, }, span: self.name.span(), + call_path: CallPath::from(self.name.clone()), attributes: self.attributes.clone(), return_type: self.return_type.clone(), visibility: Visibility::Public, diff --git a/sway-core/src/semantic_analysis/ast_node/mod.rs b/sway-core/src/semantic_analysis/ast_node/mod.rs index aec5da2b558..bfe5ed4db3b 100644 --- a/sway-core/src/semantic_analysis/ast_node/mod.rs +++ b/sway-core/src/semantic_analysis/ast_node/mod.rs @@ -121,14 +121,21 @@ impl ty::TyAstNode { side_effect: ty::TySideEffectVariant::UseStatement(ty::TyUseStatement { alias: a.alias, call_path: a.call_path, + span: a.span, is_absolute: a.is_absolute, import_type: a.import_type, }), }) } - AstNodeContent::IncludeStatement(_) => { + AstNodeContent::IncludeStatement(i) => { ty::TyAstNodeContent::SideEffect(ty::TySideEffect { - side_effect: ty::TySideEffectVariant::IncludeStatement, + side_effect: ty::TySideEffectVariant::IncludeStatement( + ty::TyIncludeStatement { + mod_name: i.mod_name, + span: i.span, + visibility: i.visibility, + }, + ), }) } AstNodeContent::Declaration(decl) => { diff --git a/sway-core/src/transform/to_parsed_lang/convert_parse_tree.rs b/sway-core/src/transform/to_parsed_lang/convert_parse_tree.rs index 91db9a9d53d..8fdd686193b 100644 --- a/sway-core/src/transform/to_parsed_lang/convert_parse_tree.rs +++ b/sway-core/src/transform/to_parsed_lang/convert_parse_tree.rs @@ -249,11 +249,14 @@ fn item_use_to_use_statements( } let mut ret = Vec::new(); let mut prefix = Vec::new(); + let item_span = item_use.span(); + use_tree_to_use_statements( item_use.tree, item_use.root_import.is_some(), &mut prefix, &mut ret, + item_span, ); debug_assert!(prefix.is_empty()); Ok(ret) @@ -264,11 +267,12 @@ fn use_tree_to_use_statements( is_absolute: bool, path: &mut Vec, ret: &mut Vec, + item_span: Span, ) { match use_tree { UseTree::Group { imports } => { for use_tree in imports.into_inner() { - use_tree_to_use_statements(use_tree, is_absolute, path, ret); + use_tree_to_use_statements(use_tree, is_absolute, path, ret, item_span.clone()); } } UseTree::Name { name } => { @@ -279,6 +283,7 @@ fn use_tree_to_use_statements( }; ret.push(UseStatement { call_path: path.clone(), + span: item_span, import_type, is_absolute, alias: None, @@ -292,6 +297,7 @@ fn use_tree_to_use_statements( }; ret.push(UseStatement { call_path: path.clone(), + span: item_span, import_type, is_absolute, alias: Some(alias), @@ -300,6 +306,7 @@ fn use_tree_to_use_statements( UseTree::Glob { .. } => { ret.push(UseStatement { call_path: path.clone(), + span: item_span, import_type: ImportType::Star, is_absolute, alias: None, @@ -307,7 +314,7 @@ fn use_tree_to_use_statements( } UseTree::Path { prefix, suffix, .. } => { path.push(prefix); - use_tree_to_use_statements(*suffix, is_absolute, path, ret); + use_tree_to_use_statements(*suffix, is_absolute, path, ret, item_span); path.pop().unwrap(); } UseTree::Error { .. } => { @@ -3570,8 +3577,9 @@ fn statement_let_to_ast_nodes( fn submodule_to_include_statement(dependency: &Submodule) -> IncludeStatement { IncludeStatement { - _span: dependency.span(), - _mod_name_span: dependency.name.span(), + span: dependency.span(), + mod_name: dependency.name.clone(), + visibility: pub_token_opt_to_visibility(dependency.visibility.clone()), } } diff --git a/sway-lsp/Cargo.toml b/sway-lsp/Cargo.toml index c30b1d52abd..61bf5a0ed1d 100644 --- a/sway-lsp/Cargo.toml +++ b/sway-lsp/Cargo.toml @@ -35,7 +35,15 @@ swayfmt = { version = "0.46.1", path = "../swayfmt" } syn = { version = "1.0.73", features = ["full"] } tempfile = "3" thiserror = "1.0.30" -tokio = { version = "1.3", features = ["io-std", "io-util", "macros", "net", "rt-multi-thread", "sync", "time"] } +tokio = { version = "1.3", features = [ + "io-std", + "io-util", + "macros", + "net", + "rt-multi-thread", + "sync", + "time", +] } toml_edit = "0.19" tower-lsp = { version = "0.19", features = ["proposed"] } tracing = "0.1" @@ -45,10 +53,14 @@ urlencoding = "2.1.2" assert-json-diff = "2.0" criterion = "0.5" dirs = "4.0" -futures = { version = "0.3", default-features = false, features = ["std", "async-await"] } +futures = { version = "0.3", default-features = false, features = [ + "std", + "async-await", +] } +pretty_assertions = "1.4.0" sway-lsp-test-utils = { path = "tests/utils" } tower = { version = "0.4.12", default-features = false, features = ["util"] } [[bench]] name = "bench_main" -harness = false \ No newline at end of file +harness = false diff --git a/sway-lsp/benches/lsp_benchmarks/requests.rs b/sway-lsp/benches/lsp_benchmarks/requests.rs index 67ad616871a..fe87df01026 100644 --- a/sway-lsp/benches/lsp_benchmarks/requests.rs +++ b/sway-lsp/benches/lsp_benchmarks/requests.rs @@ -75,7 +75,9 @@ fn benchmarks(c: &mut Criterion) { c.bench_function("code_action", |b| { let range = Range::new(Position::new(4, 10), Position::new(4, 10)); - b.iter(|| capabilities::code_actions::code_actions(session.clone(), &range, &uri, &uri)) + b.iter(|| { + capabilities::code_actions::code_actions(session.clone(), &range, &uri, &uri, &vec![]) + }) }); c.bench_function("code_lens", |b| { diff --git a/sway-lsp/src/capabilities/code_actions/abi_decl/abi_impl.rs b/sway-lsp/src/capabilities/code_actions/abi_decl/abi_impl.rs index d9357cb2027..66c5aaad1bc 100644 --- a/sway-lsp/src/capabilities/code_actions/abi_decl/abi_impl.rs +++ b/sway-lsp/src/capabilities/code_actions/abi_decl/abi_impl.rs @@ -22,7 +22,7 @@ impl<'a> GenerateImplCodeAction<'a, TyAbiDecl> for AbiImplCodeAction<'a> { } impl<'a> CodeAction<'a, TyAbiDecl> for AbiImplCodeAction<'a> { - fn new(ctx: CodeActionContext<'a>, decl: &'a TyAbiDecl) -> Self { + fn new(ctx: &CodeActionContext<'a>, decl: &'a TyAbiDecl) -> Self { Self { engines: ctx.engines, decl, diff --git a/sway-lsp/src/capabilities/code_actions/abi_decl/mod.rs b/sway-lsp/src/capabilities/code_actions/abi_decl/mod.rs index 260457bec7c..1ab1f76198e 100644 --- a/sway-lsp/src/capabilities/code_actions/abi_decl/mod.rs +++ b/sway-lsp/src/capabilities/code_actions/abi_decl/mod.rs @@ -7,7 +7,7 @@ use sway_core::{decl_engine::id::DeclId, language::ty::TyAbiDecl}; pub(crate) fn code_actions( decl_id: &DeclId, - ctx: CodeActionContext, + ctx: &CodeActionContext, ) -> Option> { let decl = ctx.engines.de().get_abi(decl_id); Some(vec![AbiImplCodeAction::new(ctx, &decl).code_action()]) diff --git a/sway-lsp/src/capabilities/code_actions/common/basic_doc_comment.rs b/sway-lsp/src/capabilities/code_actions/common/basic_doc_comment.rs index b85305cfe88..ddcc7be2974 100644 --- a/sway-lsp/src/capabilities/code_actions/common/basic_doc_comment.rs +++ b/sway-lsp/src/capabilities/code_actions/common/basic_doc_comment.rs @@ -12,7 +12,7 @@ pub struct BasicDocCommentCodeAction<'a, T: Spanned> { impl<'a, T: Spanned> GenerateDocCodeAction<'a, T> for BasicDocCommentCodeAction<'a, T> {} impl<'a, T: Spanned> CodeAction<'a, T> for BasicDocCommentCodeAction<'a, T> { - fn new(ctx: CodeActionContext<'a>, decl: &'a T) -> Self { + fn new(ctx: &CodeActionContext<'a>, decl: &'a T) -> Self { Self { decl, uri: ctx.uri } } diff --git a/sway-lsp/src/capabilities/code_actions/common/fn_doc_comment.rs b/sway-lsp/src/capabilities/code_actions/common/fn_doc_comment.rs index 6a7e503d507..20dd4650964 100644 --- a/sway-lsp/src/capabilities/code_actions/common/fn_doc_comment.rs +++ b/sway-lsp/src/capabilities/code_actions/common/fn_doc_comment.rs @@ -19,7 +19,7 @@ impl<'a, T: Spanned + Named + FunctionSignature> GenerateDocCodeAction<'a, T> impl<'a, T: Spanned + Named + FunctionSignature> CodeAction<'a, T> for FnDocCommentCodeAction<'a, T> { - fn new(ctx: CodeActionContext<'a>, decl: &'a T) -> Self { + fn new(ctx: &CodeActionContext<'a>, decl: &'a T) -> Self { Self { engines: ctx.engines, decl, diff --git a/sway-lsp/src/capabilities/code_actions/constant_decl/mod.rs b/sway-lsp/src/capabilities/code_actions/constant_decl/mod.rs index ed8e59a4c25..6b1f54a2cdb 100644 --- a/sway-lsp/src/capabilities/code_actions/constant_decl/mod.rs +++ b/sway-lsp/src/capabilities/code_actions/constant_decl/mod.rs @@ -6,7 +6,7 @@ use super::common::basic_doc_comment::BasicDocCommentCodeAction; pub(crate) fn code_actions( decl: &ty::TyConstantDecl, - ctx: CodeActionContext, + ctx: &CodeActionContext, ) -> Option> { Some(vec![BasicDocCommentCodeAction::new(ctx, decl).code_action()]) } diff --git a/sway-lsp/src/capabilities/code_actions/diagnostic/auto_import.rs b/sway-lsp/src/capabilities/code_actions/diagnostic/auto_import.rs new file mode 100644 index 00000000000..2ca67491a4e --- /dev/null +++ b/sway-lsp/src/capabilities/code_actions/diagnostic/auto_import.rs @@ -0,0 +1,503 @@ +use super::CODE_ACTION_IMPORT_TITLE; +use crate::{ + capabilities::{code_actions::CodeActionContext, diagnostic::DiagnosticData}, + core::token::{get_range_from_span, AstToken, SymbolKind, TypedAstToken}, +}; +use lsp_types::{ + CodeAction as LspCodeAction, CodeActionKind, CodeActionOrCommand, Position, Range, TextEdit, + WorkspaceEdit, +}; +use serde_json::Value; +use std::{ + cmp::Ordering, + collections::{BTreeSet, HashMap}, + iter, +}; +use sway_core::language::{ + parsed::ImportType, + ty::{ + TyConstantDecl, TyDecl, TyFunctionDecl, TyIncludeStatement, TyTypeAliasDecl, TyUseStatement, + }, + CallPath, +}; +use sway_types::{Ident, Spanned}; + +/// Returns a list of [CodeActionOrCommand] suggestions for inserting a missing import. +pub(crate) fn import_code_action( + ctx: &CodeActionContext, + diagnostics: &mut impl Iterator, +) -> Option> { + // Find a diagnostic that has the attached metadata indicating we should try to suggest an auto-import. + let symbol_name = diagnostics.find_map(|diag| diag.unknown_symbol_name)?; + + // Check if there are any matching call paths to import using the name from the diagnostic data. + let call_paths = get_call_paths_for_name(ctx, &symbol_name)?; + + // Collect the tokens we need to determine where to insert the import statement. + let mut use_statements = Vec::::new(); + let mut include_statements = Vec::::new(); + let mut program_type_keyword = None; + + ctx.tokens + .tokens_for_file(ctx.temp_uri) + .for_each(|(_, token)| { + if let Some(TypedAstToken::TypedUseStatement(use_stmt)) = token.typed { + use_statements.push(use_stmt); + } else if let Some(TypedAstToken::TypedIncludeStatement(include_stmt)) = token.typed { + include_statements.push(include_stmt); + } else if token.kind == SymbolKind::ProgramTypeKeyword { + if let AstToken::Keyword(ident) = token.parsed { + program_type_keyword = Some(ident); + } + } + }); + + // Create a list of code actions, one for each potential call path. + let actions = call_paths + .map(|call_path| { + let text_edit = get_text_edit( + &call_path, + &use_statements, + &include_statements, + &program_type_keyword, + ); + let changes = HashMap::from([(ctx.uri.clone(), vec![text_edit])]); + + CodeActionOrCommand::CodeAction(LspCodeAction { + title: format!("{} `{}`", CODE_ACTION_IMPORT_TITLE, call_path), + kind: Some(CodeActionKind::QUICKFIX), + edit: Some(WorkspaceEdit { + changes: Some(changes), + ..Default::default() + }), + data: Some(Value::String(ctx.uri.to_string())), + ..Default::default() + }) + }) + .collect::>(); + + if !actions.is_empty() { + return Some(actions); + } + + None +} + +/// Returns an [Iterator] of [CallPath]s that match the given symbol name. The [CallPath]s are sorted +/// alphabetically. +fn get_call_paths_for_name<'s>( + ctx: &'s CodeActionContext, + symbol_name: &'s String, +) -> Option> { + let namespace = ctx.namespace.to_owned()?; + let mut call_paths = ctx + .tokens + .tokens_for_name(symbol_name) + .filter_map(move |(_, token)| { + // If the typed token is a declaration, then we can import it. + match token.typed.as_ref() { + Some(TypedAstToken::TypedDeclaration(ty_decl)) => { + return match ty_decl { + TyDecl::StructDecl(decl) => { + let struct_decl = ctx.engines.de().get_struct(&decl.decl_id); + let call_path = struct_decl.call_path.to_import_path(&namespace); + Some(call_path) + } + TyDecl::EnumDecl(decl) => { + let enum_decl = ctx.engines.de().get_enum(&decl.decl_id); + let call_path = enum_decl.call_path.to_import_path(&namespace); + Some(call_path) + } + TyDecl::TraitDecl(decl) => { + let trait_decl = ctx.engines.de().get_trait(&decl.decl_id); + let call_path = trait_decl.call_path.to_import_path(&namespace); + Some(call_path) + } + _ => None, + }; + } + Some(TypedAstToken::TypedFunctionDeclaration(TyFunctionDecl { + call_path, .. + })) + | Some(TypedAstToken::TypedConstantDeclaration(TyConstantDecl { + call_path, .. + })) + | Some(TypedAstToken::TypedTypeAliasDeclaration(TyTypeAliasDecl { + call_path, + .. + })) => { + let call_path = call_path.to_import_path(&namespace); + Some(call_path) + } + _ => None, + } + }) + .collect::>(); + call_paths.sort(); + Some(call_paths.into_iter()) +} + +/// Returns a [TextEdit] to insert an import statement for the given [CallPath] in the appropriate location in the file. +/// +/// To determine where to insert the import statement in the file, we try these options and do +/// one of the following, based on the contents of the file. +/// +/// 1. Add the import to an existing import that has the same prefix. +/// 2. Insert the import on a new line relative to existing use statements. +/// 3. Insert the import on a new line after existing mod statements. +/// 4. Insert the import on a new line after the program type statement (e.g. `contract;`) +/// 5. If all else fails, insert it at the beginning of the file. +fn get_text_edit( + call_path: &CallPath, + use_statements: &[TyUseStatement], + include_statements: &[TyIncludeStatement], + program_type_keyword: &Option, +) -> TextEdit { + get_text_edit_for_group(call_path, use_statements) + .or_else(|| get_text_edit_in_use_block(call_path, use_statements)) + .unwrap_or(get_text_edit_fallback( + call_path, + include_statements, + program_type_keyword, + )) +} + +/// Returns a [TextEdit] that inserts the call path into the existing statement if there is an +/// existing [TyUseStatement] with the same prefix as the given [CallPath]. Otherwise, returns [None]. +fn get_text_edit_for_group( + call_path: &CallPath, + use_statements: &[TyUseStatement], +) -> Option { + let group_statements = use_statements.iter().filter(|use_stmt| { + call_path + .prefixes + .iter() + .zip(use_stmt.call_path.iter()) + .all(|(prefix, stmt_prefix)| prefix.as_str() == stmt_prefix.as_str()) + }); + + let mut group_statement_span = None; + let mut suffixes = group_statements + .filter_map(|stmt| { + // Set the group statement span if it hasn't been set yet. If it has been set, filter out + // any statements that aren't part of the same import group. + if group_statement_span.is_none() { + group_statement_span = Some(stmt.span()); + } else if group_statement_span != Some(stmt.span()) { + return None; + } + + let name = match &stmt.import_type { + ImportType::Star => "*".to_string(), + ImportType::SelfImport(_) => "self".to_string(), + ImportType::Item(ident) => ident.to_string(), + }; + match &stmt.alias { + Some(alias) => Some(format!("{} as {}", name, alias)), + None => Some(name), + } + }) + .chain(iter::once(call_path.suffix.to_string())) + .collect::>() + .into_iter() + .collect::>(); + + // If there were no imports with the same prefix, return None. Otherwise, build the text edit response. + group_statement_span.map(|span| { + suffixes.sort(); + let suffix_string = suffixes.join(", "); + let prefix_string = call_path + .prefixes + .iter() + .map(|ident| ident.as_str()) + .collect::>() + .join("::"); + + TextEdit { + range: get_range_from_span(&span.clone()), + new_text: format!("use {}::{{{}}};", prefix_string, suffix_string), + } + }) +} + +/// If there are existing [TyUseStatement]s, returns a [TextEdit] to insert the new import statement on the +/// line above or below an existing statement, ordered alphabetically. +fn get_text_edit_in_use_block( + call_path: &CallPath, + use_statements: &[TyUseStatement], +) -> Option { + let after_statement = use_statements.iter().reduce(|acc, curr| { + if call_path.span().as_str().cmp(curr.span().as_str()) == Ordering::Greater + && curr.span().as_str().cmp(acc.span().as_str()) == Ordering::Greater + { + return curr; + } + acc + })?; + + let after_range = get_range_from_span(&after_statement.span()); + let range_line = if call_path + .span() + .as_str() + .cmp(after_statement.span().as_str()) + == Ordering::Greater + { + after_range.end.line + 1 + } else { + after_range.start.line + }; + + Some(TextEdit { + range: Range::new(Position::new(range_line, 0), Position::new(range_line, 0)), + new_text: format!("use {};\n", call_path), + }) +} + +/// Returns a [TextEdit] to insert an import statement either after the last mod statement, after the program +/// type statement, or at the beginning of the file. +fn get_text_edit_fallback( + call_path: &CallPath, + include_statements: &[TyIncludeStatement], + program_type_keyword: &Option, +) -> TextEdit { + let range_line = include_statements + .iter() + .map(|stmt| stmt.span()) + .reduce(|acc, span| { + if span > acc { + return span; + } + acc + }) + .map(|span| get_range_from_span(&span).end.line + 1) + .unwrap_or( + program_type_keyword + .clone() + .map(|keyword| get_range_from_span(&keyword.span()).end.line + 1) + .unwrap_or(1), + ); + TextEdit { + range: Range::new(Position::new(range_line, 0), Position::new(range_line, 0)), + new_text: format!("\nuse {};\n", call_path), + } +} + +#[cfg(test)] +mod tests { + use sway_core::language::Visibility; + use sway_types::Span; + + use super::*; + + fn assert_text_edit(text_edit: TextEdit, expected_range: Range, expected_text: String) { + assert_eq!(text_edit.range, expected_range); + assert_eq!(text_edit.new_text, expected_text); + } + + fn get_mock_call_path(prefixes: Vec<&str>, suffix: &str) -> CallPath { + CallPath { + prefixes: get_mock_prefixes(prefixes), + suffix: Ident::new_no_span(suffix.to_string()), + is_absolute: false, + } + } + + fn get_mock_prefixes(prefixes: Vec<&str>) -> Vec { + prefixes + .into_iter() + .map(|p| Ident::new(Span::from_string(p.into()))) + .collect() + } + + fn get_prefixes_from_src(src: &str, prefixes: Vec<&str>) -> Vec { + prefixes + .into_iter() + .filter_map(|p| get_ident_from_src(src, p)) + .collect() + } + + fn get_span_from_src(src: &str, text: &str) -> Option { + let start = src.find(text)?; + let end = start + text.len(); + Span::new(src.into(), start, end, None) + } + + fn get_ident_from_src(src: &str, name: &str) -> Option { + let span = get_span_from_src(src, name)?; + Some(Ident::new(span)) + } + + fn get_use_stmt_from_src( + src: &str, + prefixes: Vec<&str>, + import_type: ImportType, + text: &str, + ) -> TyUseStatement { + TyUseStatement { + call_path: get_prefixes_from_src(src, prefixes), + span: get_span_from_src(src, text).unwrap(), + import_type, + is_absolute: false, + alias: None, + } + } + + fn get_incl_stmt_from_src(src: &str, mod_name: &str, text: &str) -> TyIncludeStatement { + TyIncludeStatement { + span: get_span_from_src(src, text).unwrap(), + mod_name: get_ident_from_src(src, mod_name).unwrap(), + visibility: Visibility::Private, + } + } + + #[test] + fn get_text_edit_existing_import() { + let src = r#"contract; + +use a:b:C; +use b:c:*; +"#; + let new_call_path = get_mock_call_path(vec!["a", "b"], "D"); + let use_statements = vec![ + get_use_stmt_from_src( + src, + Vec::from(["a", "b"]), + ImportType::Item(get_ident_from_src(src, "C").unwrap()), + "use a:b:C;", + ), + get_use_stmt_from_src(src, Vec::from(["b", "c"]), ImportType::Star, "use b:c:*;"), + ]; + + let include_statements = vec![]; + let program_type_keyword = get_ident_from_src(src, "contract"); + + let expected_range = Range::new(Position::new(2, 0), Position::new(2, 10)); + let expected_text = "use a::b::{C, D};".into(); + + let text_edit = get_text_edit( + &new_call_path, + &use_statements, + &include_statements, + &program_type_keyword, + ); + assert_text_edit(text_edit, expected_range, expected_text); + } + + #[test] + fn get_text_edit_new_import() { + let src = r#"predicate; + +use b:c:*; +"#; + let new_call_path = get_mock_call_path(vec!["a", "b"], "C"); + let use_statements = vec![get_use_stmt_from_src( + src, + Vec::from(["b", "c"]), + ImportType::Star, + "use b:c:*;", + )]; + + let include_statements = vec![]; + let program_type_keyword = get_ident_from_src(src, "predicate"); + + let expected_range = Range::new(Position::new(2, 0), Position::new(2, 0)); + let expected_text = "use a::b::C;\n".into(); + + let text_edit = get_text_edit( + &new_call_path, + &use_statements, + &include_statements, + &program_type_keyword, + ); + assert_text_edit(text_edit, expected_range, expected_text); + } + + #[test] + fn get_text_edit_existing_group_import() { + let src = r#"contract; + +use b:c:{D, F}; +"#; + let new_call_path = get_mock_call_path(vec!["b", "c"], "E"); + let use_statements = vec![ + get_use_stmt_from_src( + src, + Vec::from(["b", "c"]), + ImportType::Item(get_ident_from_src(src, "D").unwrap()), + "use b:c:{D, F};", + ), + get_use_stmt_from_src( + src, + Vec::from(["b", "c"]), + ImportType::Item(get_ident_from_src(src, "F").unwrap()), + "use b:c:{D, F};", + ), + ]; + + let include_statements = vec![]; + let program_type_keyword = get_ident_from_src(src, "contract"); + + let expected_range = Range::new(Position::new(2, 0), Position::new(2, 15)); + let expected_text = "use b::c::{D, E, F};".into(); + + let text_edit = get_text_edit( + &new_call_path, + &use_statements, + &include_statements, + &program_type_keyword, + ); + assert_text_edit(text_edit, expected_range, expected_text); + } + + #[test] + fn get_text_edit_after_mod() { + let src = r#"library; + +mod my_module; +pub mod zz_module; +"#; + let new_call_path = get_mock_call_path(vec!["b", "c"], "D"); + let use_statements = vec![]; + + let include_statements = vec![ + get_incl_stmt_from_src(src, "my_module", "mod my_module;"), + get_incl_stmt_from_src(src, "zz_module", "pub mod zz_module"), + ]; + let program_type_keyword = get_ident_from_src(src, "library"); + + let expected_range = Range::new(Position::new(4, 0), Position::new(4, 0)); + let expected_text = "\nuse b::c::D;\n".into(); + + let text_edit = get_text_edit( + &new_call_path, + &use_statements, + &include_statements, + &program_type_keyword, + ); + assert_text_edit(text_edit, expected_range, expected_text); + } + + #[test] + fn get_text_edit_after_program() { + let src = r#"script; + +const HI: u8 = 0; +"#; + let new_call_path = get_mock_call_path(vec!["b", "c"], "D"); + let use_statements = vec![]; + + let include_statements = vec![]; + let program_type_keyword = get_ident_from_src(src, "script"); + + let expected_range = Range::new(Position::new(1, 0), Position::new(1, 0)); + let expected_text = "\nuse b::c::D;\n".into(); + + let text_edit = get_text_edit( + &new_call_path, + &use_statements, + &include_statements, + &program_type_keyword, + ); + assert_text_edit(text_edit, expected_range, expected_text); + } +} diff --git a/sway-lsp/src/capabilities/code_actions/diagnostic/mod.rs b/sway-lsp/src/capabilities/code_actions/diagnostic/mod.rs new file mode 100644 index 00000000000..04f32575c3d --- /dev/null +++ b/sway-lsp/src/capabilities/code_actions/diagnostic/mod.rs @@ -0,0 +1,26 @@ +mod auto_import; + +use crate::capabilities::{code_actions::CodeActionContext, diagnostic::DiagnosticData}; +use lsp_types::CodeActionOrCommand; + +use self::auto_import::import_code_action; + +use super::CODE_ACTION_IMPORT_TITLE; + +/// Returns a list of [CodeActionOrCommand] based on the relavent compiler diagnostics. +pub(crate) fn code_actions(ctx: &CodeActionContext) -> Option> { + // Find diagnostics that have attached metadata. + let diagnostics_with_data = ctx.diagnostics.iter().filter_map(|diag| { + if let Some(data) = diag.clone().data { + return serde_json::from_value::(data).ok(); + } + None + }); + + import_code_action(ctx, &mut diagnostics_with_data.clone()) + .into_iter() + .reduce(|mut combined, mut curr| { + combined.append(&mut curr); + combined + }) +} diff --git a/sway-lsp/src/capabilities/code_actions/enum_decl/enum_impl.rs b/sway-lsp/src/capabilities/code_actions/enum_decl/enum_impl.rs index e241f1cbb27..32220432295 100644 --- a/sway-lsp/src/capabilities/code_actions/enum_decl/enum_impl.rs +++ b/sway-lsp/src/capabilities/code_actions/enum_decl/enum_impl.rs @@ -17,7 +17,7 @@ impl<'a> GenerateImplCodeAction<'a, TyEnumDecl> for EnumImplCodeAction<'a> { } impl<'a> CodeAction<'a, TyEnumDecl> for EnumImplCodeAction<'a> { - fn new(ctx: CodeActionContext<'a>, decl: &'a TyEnumDecl) -> Self { + fn new(ctx: &CodeActionContext<'a>, decl: &'a TyEnumDecl) -> Self { Self { decl, uri: ctx.uri } } diff --git a/sway-lsp/src/capabilities/code_actions/enum_decl/mod.rs b/sway-lsp/src/capabilities/code_actions/enum_decl/mod.rs index 043ae951092..665ef870558 100644 --- a/sway-lsp/src/capabilities/code_actions/enum_decl/mod.rs +++ b/sway-lsp/src/capabilities/code_actions/enum_decl/mod.rs @@ -9,11 +9,11 @@ use super::common::basic_doc_comment::BasicDocCommentCodeAction; pub(crate) fn code_actions( decl_id: &DeclId, - ctx: CodeActionContext, + ctx: &CodeActionContext, ) -> Option> { let decl = ctx.engines.de().get_enum(decl_id); Some(vec![ - EnumImplCodeAction::new(ctx.clone(), &decl).code_action(), + EnumImplCodeAction::new(ctx, &decl).code_action(), BasicDocCommentCodeAction::new(ctx, &decl).code_action(), ]) } diff --git a/sway-lsp/src/capabilities/code_actions/enum_variant/mod.rs b/sway-lsp/src/capabilities/code_actions/enum_variant/mod.rs index bffd7ce4589..6ac8a57f1bd 100644 --- a/sway-lsp/src/capabilities/code_actions/enum_variant/mod.rs +++ b/sway-lsp/src/capabilities/code_actions/enum_variant/mod.rs @@ -6,7 +6,7 @@ use super::common::basic_doc_comment::BasicDocCommentCodeAction; pub(crate) fn code_actions( decl: &ty::TyEnumVariant, - ctx: CodeActionContext, + ctx: &CodeActionContext, ) -> Option> { Some(vec![BasicDocCommentCodeAction::new(ctx, decl).code_action()]) } diff --git a/sway-lsp/src/capabilities/code_actions/function_decl/mod.rs b/sway-lsp/src/capabilities/code_actions/function_decl/mod.rs index 7cd6c0559ee..35f670cfd90 100644 --- a/sway-lsp/src/capabilities/code_actions/function_decl/mod.rs +++ b/sway-lsp/src/capabilities/code_actions/function_decl/mod.rs @@ -6,7 +6,7 @@ use super::common::fn_doc_comment::FnDocCommentCodeAction; pub(crate) fn code_actions( decl: &ty::TyFunctionDecl, - ctx: CodeActionContext, + ctx: &CodeActionContext, ) -> Option> { Some(vec![FnDocCommentCodeAction::new(ctx, decl).code_action()]) } diff --git a/sway-lsp/src/capabilities/code_actions/mod.rs b/sway-lsp/src/capabilities/code_actions/mod.rs index 48abda9da65..ec8c2047e2c 100644 --- a/sway-lsp/src/capabilities/code_actions/mod.rs +++ b/sway-lsp/src/capabilities/code_actions/mod.rs @@ -1,6 +1,7 @@ pub mod abi_decl; pub mod common; pub mod constant_decl; +pub mod diagnostic; pub mod enum_decl; pub mod enum_variant; pub mod function_decl; @@ -17,16 +18,17 @@ use crate::core::{ pub use crate::error::DocumentError; use lsp_types::{ CodeAction as LspCodeAction, CodeActionDisabled, CodeActionKind, CodeActionOrCommand, - CodeActionResponse, Position, Range, TextEdit, Url, WorkspaceEdit, + CodeActionResponse, Diagnostic, Position, Range, TextEdit, Url, WorkspaceEdit, }; use serde_json::Value; use std::{collections::HashMap, sync::Arc}; -use sway_core::{language::ty, Engines}; +use sway_core::{language::ty, Engines, Namespace}; use sway_types::Spanned; pub(crate) const CODE_ACTION_IMPL_TITLE: &str = "Generate impl for"; pub(crate) const CODE_ACTION_NEW_TITLE: &str = "Generate `new`"; pub(crate) const CODE_ACTION_DOC_TITLE: &str = "Generate a documentation template"; +pub(crate) const CODE_ACTION_IMPORT_TITLE: &str = "Import"; #[derive(Clone)] pub(crate) struct CodeActionContext<'a> { @@ -34,6 +36,9 @@ pub(crate) struct CodeActionContext<'a> { tokens: &'a TokenMap, token: &'a Token, uri: &'a Url, + temp_uri: &'a Url, + diagnostics: &'a Vec, + namespace: &'a Option, } pub fn code_actions( @@ -41,6 +46,7 @@ pub fn code_actions( range: &Range, uri: &Url, temp_uri: &Url, + diagnostics: &Vec, ) -> Option { let engines = session.engines.read(); let (_, token) = session @@ -52,34 +58,49 @@ pub fn code_actions( tokens: session.token_map(), token: &token, uri, + temp_uri, + diagnostics, + namespace: &session.namespace(), }; - match token.typed.as_ref()? { - TypedAstToken::TypedDeclaration(decl) => match decl { - ty::TyDecl::AbiDecl(ty::AbiDecl { decl_id, .. }) => { - abi_decl::code_actions(decl_id, ctx) + let actions_by_type = token + .typed + .as_ref() + .and_then(|typed_token| match typed_token { + TypedAstToken::TypedDeclaration(decl) => match decl { + ty::TyDecl::AbiDecl(ty::AbiDecl { decl_id, .. }) => { + abi_decl::code_actions(decl_id, &ctx) + } + ty::TyDecl::StructDecl(ty::StructDecl { decl_id, .. }) => { + struct_decl::code_actions(decl_id, &ctx) + } + ty::TyDecl::EnumDecl(ty::EnumDecl { decl_id, .. }) => { + enum_decl::code_actions(decl_id, &ctx) + } + _ => None, + }, + TypedAstToken::TypedFunctionDeclaration(decl) => { + function_decl::code_actions(decl, &ctx) } - ty::TyDecl::StructDecl(ty::StructDecl { decl_id, .. }) => { - struct_decl::code_actions(decl_id, ctx) - } - ty::TyDecl::EnumDecl(ty::EnumDecl { decl_id, .. }) => { - enum_decl::code_actions(decl_id, ctx) + TypedAstToken::TypedStorageField(decl) => storage_field::code_actions(decl, &ctx), + TypedAstToken::TypedConstantDeclaration(decl) => { + constant_decl::code_actions(decl, &ctx) } + TypedAstToken::TypedEnumVariant(decl) => enum_variant::code_actions(decl, &ctx), + TypedAstToken::TypedStructField(decl) => struct_field::code_actions(decl, &ctx), + TypedAstToken::TypedTraitFn(decl) => trait_fn::code_actions(decl, &ctx), _ => None, - }, - TypedAstToken::TypedFunctionDeclaration(decl) => function_decl::code_actions(decl, ctx), - TypedAstToken::TypedStorageField(decl) => storage_field::code_actions(decl, ctx), - TypedAstToken::TypedConstantDeclaration(decl) => constant_decl::code_actions(decl, ctx), - TypedAstToken::TypedEnumVariant(decl) => enum_variant::code_actions(decl, ctx), - TypedAstToken::TypedStructField(decl) => struct_field::code_actions(decl, ctx), - TypedAstToken::TypedTraitFn(decl) => trait_fn::code_actions(decl, ctx), - _ => None, - } + }) + .unwrap_or_default(); + + let actions_by_diagnostic = diagnostic::code_actions(&ctx).unwrap_or_default(); + + Some([actions_by_type, actions_by_diagnostic].concat()) } pub(crate) trait CodeAction<'a, T: Spanned> { /// Creates a new [CodeAction] with the given [Engines], delcaration type, and [Url]. - fn new(ctx: CodeActionContext<'a>, decl: &'a T) -> Self; + fn new(ctx: &CodeActionContext<'a>, decl: &'a T) -> Self; /// Returns a [String] of text to insert into the document. fn new_text(&self) -> String; diff --git a/sway-lsp/src/capabilities/code_actions/storage_field/mod.rs b/sway-lsp/src/capabilities/code_actions/storage_field/mod.rs index b335d076eeb..04921d2c4e6 100644 --- a/sway-lsp/src/capabilities/code_actions/storage_field/mod.rs +++ b/sway-lsp/src/capabilities/code_actions/storage_field/mod.rs @@ -6,7 +6,7 @@ use super::common::basic_doc_comment::BasicDocCommentCodeAction; pub(crate) fn code_actions( decl: &ty::TyStorageField, - ctx: CodeActionContext, + ctx: &CodeActionContext, ) -> Option> { Some(vec![BasicDocCommentCodeAction::new(ctx, decl).code_action()]) } diff --git a/sway-lsp/src/capabilities/code_actions/struct_decl/mod.rs b/sway-lsp/src/capabilities/code_actions/struct_decl/mod.rs index c7c0e3cdb42..4c6ffffa6dd 100644 --- a/sway-lsp/src/capabilities/code_actions/struct_decl/mod.rs +++ b/sway-lsp/src/capabilities/code_actions/struct_decl/mod.rs @@ -10,12 +10,12 @@ use super::common::basic_doc_comment::BasicDocCommentCodeAction; pub(crate) fn code_actions( decl_id: &DeclId, - ctx: CodeActionContext, + ctx: &CodeActionContext, ) -> Option> { let decl = ctx.engines.de().get_struct(decl_id); Some(vec![ - StructImplCodeAction::new(ctx.clone(), &decl).code_action(), - StructNewCodeAction::new(ctx.clone(), &decl).code_action(), + StructImplCodeAction::new(ctx, &decl).code_action(), + StructNewCodeAction::new(ctx, &decl).code_action(), BasicDocCommentCodeAction::new(ctx, &decl).code_action(), ]) } diff --git a/sway-lsp/src/capabilities/code_actions/struct_decl/struct_impl.rs b/sway-lsp/src/capabilities/code_actions/struct_decl/struct_impl.rs index fc509675db7..b532c09b0b5 100644 --- a/sway-lsp/src/capabilities/code_actions/struct_decl/struct_impl.rs +++ b/sway-lsp/src/capabilities/code_actions/struct_decl/struct_impl.rs @@ -17,7 +17,7 @@ impl<'a> GenerateImplCodeAction<'a, TyStructDecl> for StructImplCodeAction<'a> { } impl<'a> CodeAction<'a, TyStructDecl> for StructImplCodeAction<'a> { - fn new(ctx: CodeActionContext<'a>, decl: &'a TyStructDecl) -> Self { + fn new(ctx: &CodeActionContext<'a>, decl: &'a TyStructDecl) -> Self { Self { decl, uri: ctx.uri } } diff --git a/sway-lsp/src/capabilities/code_actions/struct_decl/struct_new.rs b/sway-lsp/src/capabilities/code_actions/struct_decl/struct_new.rs index cb84b823931..08eaffabd94 100644 --- a/sway-lsp/src/capabilities/code_actions/struct_decl/struct_new.rs +++ b/sway-lsp/src/capabilities/code_actions/struct_decl/struct_new.rs @@ -22,7 +22,7 @@ impl<'a> GenerateImplCodeAction<'a, TyStructDecl> for StructNewCodeAction<'a> { } impl<'a> CodeAction<'a, TyStructDecl> for StructNewCodeAction<'a> { - fn new(ctx: CodeActionContext<'a>, decl: &'a TyStructDecl) -> Self { + fn new(ctx: &CodeActionContext<'a>, decl: &'a TyStructDecl) -> Self { // Before the other functions are called, we need to determine if the new function // should be generated in a new impl block, an existing impl block, or not at all. // Find the first impl block for this struct if it exists. diff --git a/sway-lsp/src/capabilities/code_actions/struct_field/mod.rs b/sway-lsp/src/capabilities/code_actions/struct_field/mod.rs index 2ec875de0b0..f1023a49eff 100644 --- a/sway-lsp/src/capabilities/code_actions/struct_field/mod.rs +++ b/sway-lsp/src/capabilities/code_actions/struct_field/mod.rs @@ -6,7 +6,7 @@ use super::common::basic_doc_comment::BasicDocCommentCodeAction; pub(crate) fn code_actions( decl: &ty::TyStructField, - ctx: CodeActionContext, + ctx: &CodeActionContext, ) -> Option> { Some(vec![BasicDocCommentCodeAction::new(ctx, decl).code_action()]) } diff --git a/sway-lsp/src/capabilities/code_actions/trait_fn/mod.rs b/sway-lsp/src/capabilities/code_actions/trait_fn/mod.rs index 3e5f7dd105d..68ead347d6d 100644 --- a/sway-lsp/src/capabilities/code_actions/trait_fn/mod.rs +++ b/sway-lsp/src/capabilities/code_actions/trait_fn/mod.rs @@ -6,7 +6,7 @@ use super::common::fn_doc_comment::FnDocCommentCodeAction; pub(crate) fn code_actions( decl: &ty::TyTraitFn, - ctx: CodeActionContext, + ctx: &CodeActionContext, ) -> Option> { Some(vec![FnDocCommentCodeAction::new(ctx, decl).code_action()]) } diff --git a/sway-lsp/src/capabilities/diagnostic.rs b/sway-lsp/src/capabilities/diagnostic.rs index 69ff60624b1..dd6f4d1bd40 100644 --- a/sway-lsp/src/capabilities/diagnostic.rs +++ b/sway-lsp/src/capabilities/diagnostic.rs @@ -2,11 +2,12 @@ use std::collections::HashMap; use std::path::PathBuf; use lsp_types::{Diagnostic, DiagnosticSeverity, DiagnosticTag, Position, Range}; +use serde::{Deserialize, Serialize}; use sway_error::warning::CompileWarning; use sway_error::{error::CompileError, warning::Warning}; use sway_types::{LineCol, SourceEngine, Spanned}; -pub type DiagnosticMap = HashMap; +pub(crate) type DiagnosticMap = HashMap; #[derive(Debug, Default, Clone)] pub struct Diagnostics { @@ -15,10 +16,13 @@ pub struct Diagnostics { } fn get_error_diagnostic(error: &CompileError) -> Diagnostic { + let data = serde_json::to_value(DiagnosticData::try_from(error.clone()).ok()).ok(); + Diagnostic { range: get_range(error.span().line_col()), severity: Some(DiagnosticSeverity::ERROR), message: format!("{error}"), + data, ..Default::default() } } @@ -90,3 +94,33 @@ fn get_warning_diagnostic_tags(warning: &Warning) -> Option> _ => None, } } + +/// Extra data to be sent with a diagnostic and provided in CodeAction context. +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct DiagnosticData { + pub unknown_symbol_name: Option, +} + +impl TryFrom for DiagnosticData { + type Error = anyhow::Error; + + fn try_from(_value: CompileWarning) -> Result { + anyhow::bail!("Not implemented"); + } +} + +impl TryFrom for DiagnosticData { + type Error = anyhow::Error; + + fn try_from(value: CompileError) -> Result { + match value { + CompileError::SymbolNotFound { name, .. } => Ok(DiagnosticData { + unknown_symbol_name: Some(name.to_string()), + }), + CompileError::UnknownVariable { var_name, .. } => Ok(DiagnosticData { + unknown_symbol_name: Some(var_name.to_string()), + }), + _ => anyhow::bail!("Not implemented"), + } + } +} diff --git a/sway-lsp/src/capabilities/document_symbol.rs b/sway-lsp/src/capabilities/document_symbol.rs index f142b929cc5..018742b8d34 100644 --- a/sway-lsp/src/capabilities/document_symbol.rs +++ b/sway-lsp/src/capabilities/document_symbol.rs @@ -37,10 +37,11 @@ pub(crate) fn symbol_kind(symbol_kind: &SymbolKind) -> lsp_types::SymbolKind { | SymbolKind::ByteLiteral | SymbolKind::Variable | SymbolKind::TypeAlias - | SymbolKind::TraiType + | SymbolKind::TraitType | SymbolKind::Keyword | SymbolKind::SelfKeyword | SymbolKind::SelfTypeKeyword + | SymbolKind::ProgramTypeKeyword | SymbolKind::Unknown => lsp_types::SymbolKind::VARIABLE, } } diff --git a/sway-lsp/src/capabilities/hover/mod.rs b/sway-lsp/src/capabilities/hover/mod.rs index a7fa0e61f13..4ec5cb75206 100644 --- a/sway-lsp/src/capabilities/hover/mod.rs +++ b/sway-lsp/src/capabilities/hover/mod.rs @@ -33,7 +33,10 @@ pub fn hover_data( // check if our token is a keyword if matches!( token.kind, - SymbolKind::BoolLiteral | SymbolKind::Keyword | SymbolKind::SelfKeyword + SymbolKind::BoolLiteral + | SymbolKind::Keyword + | SymbolKind::SelfKeyword + | SymbolKind::ProgramTypeKeyword ) { let name = &ident.name; let documentation = keyword_docs.get(name).unwrap(); diff --git a/sway-lsp/src/capabilities/rename.rs b/sway-lsp/src/capabilities/rename.rs index 504ab7db404..3f6f19e8d45 100644 --- a/sway-lsp/src/capabilities/rename.rs +++ b/sway-lsp/src/capabilities/rename.rs @@ -117,7 +117,15 @@ pub fn prepare_rename( // Make sure we don't allow renaming of tokens that // are keywords or intrinsics. - if matches!(token.kind, SymbolKind::Keyword | SymbolKind::Intrinsic) { + if matches!( + token.kind, + SymbolKind::Keyword + | SymbolKind::SelfKeyword + | SymbolKind::SelfTypeKeyword + | SymbolKind::ProgramTypeKeyword + | SymbolKind::BoolLiteral + | SymbolKind::Intrinsic + ) { return Err(LanguageServerError::RenameError( RenameError::SymbolKindNotAllowed, )); diff --git a/sway-lsp/src/capabilities/semantic_tokens.rs b/sway-lsp/src/capabilities/semantic_tokens.rs index 9e990f00747..61e3ce74724 100644 --- a/sway-lsp/src/capabilities/semantic_tokens.rs +++ b/sway-lsp/src/capabilities/semantic_tokens.rs @@ -156,8 +156,8 @@ fn semantic_token_type(kind: &SymbolKind) -> SemanticTokenType { SymbolKind::ByteLiteral | SymbolKind::NumericLiteral => SemanticTokenType::NUMBER, SymbolKind::BoolLiteral => SemanticTokenType::new("boolean"), SymbolKind::TypeAlias => SemanticTokenType::new("typeAlias"), - SymbolKind::TraiType => SemanticTokenType::new("traitType"), - SymbolKind::Keyword => SemanticTokenType::new("keyword"), + SymbolKind::TraitType => SemanticTokenType::new("traitType"), + SymbolKind::Keyword | SymbolKind::ProgramTypeKeyword => SemanticTokenType::new("keyword"), SymbolKind::Unknown => SemanticTokenType::new("generic"), SymbolKind::BuiltinType => SemanticTokenType::new("builtinType"), SymbolKind::DeriveHelper => SemanticTokenType::new("deriveHelper"), diff --git a/sway-lsp/src/core/session.rs b/sway-lsp/src/core/session.rs index f66313d685c..6eebbcd5923 100644 --- a/sway-lsp/src/core/session.rs +++ b/sway-lsp/src/core/session.rs @@ -38,7 +38,8 @@ use sway_core::{ language::{ lexed::LexedProgram, parsed::{AstNode, ParseProgram}, - ty::{self, TyProgram}, + ty::{self}, + HasSubmodules, }, BuildTarget, Engines, Namespace, Programs, }; @@ -443,7 +444,7 @@ pub fn compile( pub struct TraversalResult { pub diagnostics: (Vec, Vec), - pub programs: Option<(LexedProgram, ParseProgram, TyProgram)>, + pub programs: Option<(LexedProgram, ParseProgram, ty::TyProgram)>, pub token_map: TokenMap, } @@ -545,8 +546,7 @@ fn parse_ast_to_tokens( let root_nodes = parse_program.root.tree.root_nodes.iter(); let sub_nodes = parse_program .root - .submodules - .iter() + .submodules_recursive() .flat_map(|(_, submodule)| &submodule.module.tree.root_nodes); root_nodes @@ -564,8 +564,7 @@ fn parse_ast_to_typed_tokens( let root_nodes = typed_program.root.all_nodes.iter(); let sub_nodes = typed_program .root - .submodules - .iter() + .submodules_recursive() .flat_map(|(_, submodule)| submodule.module.all_nodes.iter()); root_nodes.chain(sub_nodes).for_each(|n| f(n, ctx)); diff --git a/sway-lsp/src/core/token.rs b/sway-lsp/src/core/token.rs index c1a7f05953b..96c76a6fd9b 100644 --- a/sway-lsp/src/core/token.rs +++ b/sway-lsp/src/core/token.rs @@ -6,9 +6,9 @@ use sway_core::{ parsed::{ AbiCastExpression, AmbiguousPathExpression, Declaration, DelineatedPathExpression, EnumVariant, Expression, FunctionApplicationExpression, FunctionParameter, - MethodApplicationExpression, Scrutinee, StorageField, StructExpression, - StructExpressionField, StructField, StructScrutineeField, Supertrait, TraitFn, - UseStatement, + IncludeStatement, MethodApplicationExpression, Scrutinee, StorageField, + StructExpression, StructExpressionField, StructField, StructScrutineeField, Supertrait, + TraitFn, UseStatement, }, ty, }, @@ -35,7 +35,8 @@ pub enum AstToken { FunctionApplicationExpression(FunctionApplicationExpression), FunctionParameter(FunctionParameter), Ident(Ident), - IncludeStatement, + ModuleName, + IncludeStatement(IncludeStatement), Intrinsic(Intrinsic), Keyword(Ident), LibrarySpan(Span), @@ -77,7 +78,8 @@ pub enum TypedAstToken { TypedArgument(TypeArgument), TypedParameter(TypeParameter), TypedTraitConstraint(TraitConstraint), - TypedIncludeStatement, + TypedModuleName, + TypedIncludeStatement(ty::TyIncludeStatement), TypedUseStatement(ty::TyUseStatement), Ident(Ident), } @@ -109,6 +111,8 @@ pub enum SymbolKind { Module, /// Emitted for numeric literals. NumericLiteral, + /// Emitted for keywords. + ProgramTypeKeyword, /// Emitted for the self function parameter and self path-specifier. SelfKeyword, /// Emitted for the Self type parameter. @@ -120,7 +124,7 @@ pub enum SymbolKind { /// Emitted for traits. Trait, /// Emitted for associated types. - TraiType, + TraitType, /// Emitted for type aliases. TypeAlias, /// Emitted for type parameters. diff --git a/sway-lsp/src/core/token_map.rs b/sway-lsp/src/core/token_map.rs index 215f3b12132..b2a8c1eb968 100644 --- a/sway-lsp/src/core/token_map.rs +++ b/sway-lsp/src/core/token_map.rs @@ -45,6 +45,20 @@ impl TokenMap { }) } + /// Return an Iterator of tokens matching the given name. + pub fn tokens_for_name<'s>( + &'s self, + name: &'s String, + ) -> impl 's + Iterator { + self.iter().flat_map(|(ident, token)| { + if ident.name == *name { + Some((ident.clone(), token.clone())) + } else { + None + } + }) + } + /// Given a cursor [Position], return the [TokenIdent] of a token in the /// Iterator if one exists at that position. pub fn idents_at_position(&self, cursor_position: Position, tokens: I) -> Vec diff --git a/sway-lsp/src/handlers/request.rs b/sway-lsp/src/handlers/request.rs index e9d0bcf690a..3d8e5fdfc31 100644 --- a/sway-lsp/src/handlers/request.rs +++ b/sway-lsp/src/handlers/request.rs @@ -228,6 +228,7 @@ pub fn handle_code_action( ¶ms.range, ¶ms.text_document.uri, &temp_uri, + ¶ms.context.diagnostics, )), Err(err) => { tracing::error!("{}", err.to_string()); diff --git a/sway-lsp/src/traverse/dependency.rs b/sway-lsp/src/traverse/dependency.rs index 8d7db4bf671..cde3f2942c6 100644 --- a/sway-lsp/src/traverse/dependency.rs +++ b/sway-lsp/src/traverse/dependency.rs @@ -43,6 +43,7 @@ pub fn collect_typed_declaration(node: &ty::TyAstNode, ctx: &ParseContext) { | ty::TyDecl::ConstantDecl(ty::ConstantDecl { name, .. }) => name.clone(), _ => return, }; + let token_ident = ctx.ident(&ident); if let Some(mut token) = ctx.tokens.try_get_mut(&token_ident).try_unwrap() { token.typed = Some(typed_token); diff --git a/sway-lsp/src/traverse/lexed_tree.rs b/sway-lsp/src/traverse/lexed_tree.rs index 0334c7c154c..b4ec5328c12 100644 --- a/sway-lsp/src/traverse/lexed_tree.rs +++ b/sway-lsp/src/traverse/lexed_tree.rs @@ -11,7 +11,7 @@ use sway_ast::{ MatchBranchKind, ModuleKind, Pattern, PatternStructField, Statement, StatementLet, StorageField, TraitType, Ty, TypeField, UseTree, }; -use sway_core::language::lexed::LexedProgram; +use sway_core::language::{lexed::LexedProgram, HasSubmodules}; use sway_types::{Ident, Span, Spanned}; pub fn parse(lexed_program: &LexedProgram, ctx: &ParseContext) { @@ -25,8 +25,7 @@ pub fn parse(lexed_program: &LexedProgram, ctx: &ParseContext) { lexed_program .root - .submodules - .par_iter() + .submodules_recursive() .for_each(|(_, dep)| { insert_module_kind(ctx, &dep.module.tree.kind); dep.module @@ -40,20 +39,29 @@ pub fn parse(lexed_program: &LexedProgram, ctx: &ParseContext) { fn insert_module_kind(ctx: &ParseContext, kind: &ModuleKind) { match kind { ModuleKind::Script { script_token } => { - insert_keyword(ctx, script_token.span()); + insert_program_type_keyword(ctx, script_token.span()); } ModuleKind::Contract { contract_token } => { - insert_keyword(ctx, contract_token.span()); + insert_program_type_keyword(ctx, contract_token.span()); } ModuleKind::Predicate { predicate_token } => { - insert_keyword(ctx, predicate_token.span()); + insert_program_type_keyword(ctx, predicate_token.span()); } ModuleKind::Library { library_token, .. } => { - insert_keyword(ctx, library_token.span()); + insert_program_type_keyword(ctx, library_token.span()); } } } +fn insert_program_type_keyword(ctx: &ParseContext, span: Span) { + let ident = Ident::new(span); + let token = Token::from_parsed( + AstToken::Keyword(ident.clone()), + SymbolKind::ProgramTypeKeyword, + ); + ctx.tokens.insert(ctx.ident(&ident), token); +} + fn insert_keyword(ctx: &ParseContext, span: Span) { let ident = Ident::new(span); let token = Token::from_parsed(AstToken::Keyword(ident.clone()), SymbolKind::Keyword); diff --git a/sway-lsp/src/traverse/parsed_tree.rs b/sway-lsp/src/traverse/parsed_tree.rs index fb10b545493..f41f26a1f29 100644 --- a/sway-lsp/src/traverse/parsed_tree.rs +++ b/sway-lsp/src/traverse/parsed_tree.rs @@ -17,17 +17,17 @@ use sway_core::{ ArrayIndexExpression, AstNode, AstNodeContent, ConstantDeclaration, Declaration, DelineatedPathExpression, EnumDeclaration, EnumVariant, Expression, ExpressionKind, FunctionApplicationExpression, FunctionDeclaration, FunctionParameter, IfExpression, - ImplItem, ImplSelf, ImplTrait, ImportType, IntrinsicFunctionExpression, - LazyOperatorExpression, MatchExpression, MethodApplicationExpression, MethodName, - ParseModule, ParseProgram, ParseSubmodule, QualifiedPathRootTypes, - ReassignmentExpression, ReassignmentTarget, Scrutinee, StorageAccessExpression, - StorageDeclaration, StorageField, StructDeclaration, StructExpression, - StructExpressionField, StructField, StructScrutineeField, SubfieldExpression, - Supertrait, TraitDeclaration, TraitFn, TraitItem, TraitTypeDeclaration, - TupleIndexExpression, TypeAliasDeclaration, UseStatement, VariableDeclaration, - WhileLoopExpression, + ImplItem, ImplSelf, ImplTrait, ImportType, IncludeStatement, + IntrinsicFunctionExpression, LazyOperatorExpression, MatchExpression, + MethodApplicationExpression, MethodName, ParseModule, ParseProgram, ParseSubmodule, + QualifiedPathRootTypes, ReassignmentExpression, ReassignmentTarget, Scrutinee, + StorageAccessExpression, StorageDeclaration, StorageField, StructDeclaration, + StructExpression, StructExpressionField, StructField, StructScrutineeField, + SubfieldExpression, Supertrait, TraitDeclaration, TraitFn, TraitItem, + TraitTypeDeclaration, TupleIndexExpression, TypeAliasDeclaration, UseStatement, + VariableDeclaration, WhileLoopExpression, }, - CallPathTree, Literal, + CallPathTree, HasSubmodules, Literal, }, transform::{AttributeKind, AttributesMap}, type_system::{TypeArgument, TypeParameter}, @@ -70,11 +70,11 @@ impl<'a> ParsedTree<'a> { mod_name_span, .. }, - ) in &parse_module.submodules + ) in parse_module.submodules_recursive() { self.ctx.tokens.insert( self.ctx.ident(&Ident::new(mod_name_span.clone())), - Token::from_parsed(AstToken::IncludeStatement, SymbolKind::Module), + Token::from_parsed(AstToken::ModuleName, SymbolKind::Module), ); self.collect_parse_module(module); } @@ -107,8 +107,7 @@ impl Parse for AstNode { expression.parse(ctx); } AstNodeContent::UseStatement(use_statement) => use_statement.parse(ctx), - // include statements are handled throught [`collect_module_spans`] - AstNodeContent::IncludeStatement(_) => {} + AstNodeContent::IncludeStatement(include_statement) => include_statement.parse(ctx), AstNodeContent::Error(_, _) => {} } } @@ -165,6 +164,18 @@ impl Parse for UseStatement { } } +impl Parse for IncludeStatement { + fn parse(&self, ctx: &ParseContext) { + ctx.tokens.insert( + ctx.ident(&self.mod_name), + Token::from_parsed( + AstToken::IncludeStatement(self.clone()), + SymbolKind::Unknown, + ), + ); + } +} + impl Parse for Expression { fn parse(&self, ctx: &ParseContext) { match &self.kind { @@ -902,7 +913,7 @@ impl Parse for TraitTypeDeclaration { ctx.ident(&self.name), Token::from_parsed( AstToken::Declaration(Declaration::TraitTypeDeclaration(self.clone())), - SymbolKind::TraiType, + SymbolKind::TraitType, ), ); if let Some(ty) = &self.ty_opt { diff --git a/sway-lsp/src/traverse/typed_tree.rs b/sway-lsp/src/traverse/typed_tree.rs index 8008c870144..e24c4bb8d98 100644 --- a/sway-lsp/src/traverse/typed_tree.rs +++ b/sway-lsp/src/traverse/typed_tree.rs @@ -16,7 +16,7 @@ use sway_core::{ type_system::TypeArgument, TraitConstraint, TypeId, TypeInfo, }; -use sway_types::{Ident, Span, Spanned}; +use sway_types::{Ident, Named, Span, Spanned}; use sway_utils::iter_prefixes; pub struct TypedTree<'a> { @@ -44,7 +44,7 @@ impl<'a> TypedTree<'a> { module, mod_name_span, }, - ) in &typed_module.submodules + ) in typed_module.submodules_recursive() { if let Some(mut token) = self .ctx @@ -52,7 +52,7 @@ impl<'a> TypedTree<'a> { .try_get_mut(&self.ctx.ident(&Ident::new(mod_name_span.clone()))) .try_unwrap() { - token.typed = Some(TypedAstToken::TypedIncludeStatement); + token.typed = Some(TypedAstToken::TypedModuleName); token.type_def = Some(TypeDefinition::Ident(Ident::new(module.span.clone()))); } self.collect_module(module); @@ -100,6 +100,7 @@ impl Parse for ty::TySideEffect { UseStatement( use_statement @ ty::TyUseStatement { call_path, + span: _, import_type, alias, is_absolute: _, @@ -178,7 +179,26 @@ impl Parse for ty::TySideEffect { ImportType::Star => {} } } - IncludeStatement => {} + IncludeStatement( + include_statement @ ty::TyIncludeStatement { + span: _, + mod_name, + visibility: _, + }, + ) => { + if let Some(mut token) = ctx.tokens.try_get_mut(&ctx.ident(mod_name)).try_unwrap() { + token.typed = Some(TypedAstToken::TypedIncludeStatement( + include_statement.clone(), + )); + if let Some(span) = ctx + .namespace + .submodule(&[mod_name.clone()]) + .and_then(|tgt_submod| tgt_submod.span.clone()) + { + token.type_def = Some(TypeDefinition::Ident(Ident::new(span))); + } + } + } } } } @@ -270,8 +290,9 @@ impl Parse for ty::TyExpression { ref const_decl, span, call_path, + .. } => { - collect_const_decl(ctx, const_decl, span); + collect_const_decl(ctx, const_decl, Some(&Ident::new(span.clone()))); if let Some(call_path) = call_path { collect_call_path_prefixes(ctx, &call_path.prefixes); } @@ -594,7 +615,7 @@ impl Parse for ty::TyVariableDecl { impl Parse for ty::ConstantDecl { fn parse(&self, ctx: &ParseContext) { let const_decl = ctx.engines.de().get_constant(&self.decl_id); - collect_const_decl(ctx, &const_decl, &self.decl_span); + collect_const_decl(ctx, &const_decl, None); } } @@ -677,7 +698,7 @@ impl Parse for ty::TraitDecl { } ty::TyTraitInterfaceItem::Constant(decl_ref) => { let constant = ctx.engines.de().get_constant(decl_ref); - collect_const_decl(ctx, &constant, &decl_ref.span()); + collect_const_decl(ctx, &constant, None); } ty::TyTraitInterfaceItem::Type(decl_ref) => { let trait_type = ctx.engines.de().get_type(decl_ref); @@ -785,7 +806,7 @@ impl Parse for ty::ImplTrait { } ty::TyTraitItem::Constant(const_ref) => { let constant = ctx.engines.de().get_constant(const_ref); - collect_const_decl(ctx, &constant, &const_ref.span()); + collect_const_decl(ctx, &constant, None); } ty::TyTraitItem::Type(type_ref) => { let trait_type = ctx.engines.de().get_type(type_ref); @@ -834,7 +855,7 @@ impl Parse for ty::AbiDecl { } ty::TyTraitInterfaceItem::Constant(const_ref) => { let constant = ctx.engines.de().get_constant(const_ref); - collect_const_decl(ctx, &constant, &const_ref.span()); + collect_const_decl(ctx, &constant, None); } ty::TyTraitInterfaceItem::Type(type_ref) => { let trait_type = ctx.engines.de().get_type(type_ref); @@ -1197,12 +1218,10 @@ fn collect_call_path_prefixes(ctx: &ParseContext, prefixes: &[Ident]) { } } -fn collect_const_decl(ctx: &ParseContext, const_decl: &ty::TyConstantDecl, span: &Span) { - if let Some(mut token) = ctx - .tokens - .try_get_mut(&ctx.ident(&Ident::new(span.clone()))) - .try_unwrap() - { +fn collect_const_decl(ctx: &ParseContext, const_decl: &ty::TyConstantDecl, ident: Option<&Ident>) { + let key = ctx.ident(ident.unwrap_or(const_decl.name())); + + if let Some(mut token) = ctx.tokens.try_get_mut(&key).try_unwrap() { token.typed = Some(TypedAstToken::TypedConstantDeclaration(const_decl.clone())); token.type_def = Some(TypeDefinition::Ident(const_decl.call_path.suffix.clone())); } diff --git a/sway-lsp/tests/fixtures/auto_import/.gitignore b/sway-lsp/tests/fixtures/auto_import/.gitignore new file mode 100644 index 00000000000..77d3844f58c --- /dev/null +++ b/sway-lsp/tests/fixtures/auto_import/.gitignore @@ -0,0 +1,2 @@ +out +target diff --git a/sway-lsp/tests/fixtures/auto_import/Forc.lock b/sway-lsp/tests/fixtures/auto_import/Forc.lock new file mode 100644 index 00000000000..64d2d65076b --- /dev/null +++ b/sway-lsp/tests/fixtures/auto_import/Forc.lock @@ -0,0 +1,13 @@ +[[package]] +name = "auto_import" +source = "member" +dependencies = ["std"] + +[[package]] +name = "core" +source = "path+from-root-C29FCA7313EFC5E1" + +[[package]] +name = "std" +source = "path+from-root-C29FCA7313EFC5E1" +dependencies = ["core"] diff --git a/sway-lsp/tests/fixtures/auto_import/Forc.toml b/sway-lsp/tests/fixtures/auto_import/Forc.toml new file mode 100644 index 00000000000..e4a46a399a6 --- /dev/null +++ b/sway-lsp/tests/fixtures/auto_import/Forc.toml @@ -0,0 +1,9 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "auto_import" + +[dependencies] +std = { path = "../../../../sway-lib-std" } +core = { path = "../../../../sway-lib-core" } diff --git a/sway-lsp/tests/fixtures/auto_import/src/deep_mod.sw b/sway-lsp/tests/fixtures/auto_import/src/deep_mod.sw new file mode 100644 index 00000000000..ea7106afe89 --- /dev/null +++ b/sway-lsp/tests/fixtures/auto_import/src/deep_mod.sw @@ -0,0 +1,3 @@ +library; + +pub mod deeper_mod; diff --git a/sway-lsp/tests/fixtures/auto_import/src/deep_mod/deeper_mod.sw b/sway-lsp/tests/fixtures/auto_import/src/deep_mod/deeper_mod.sw new file mode 100644 index 00000000000..2f80f41f1f1 --- /dev/null +++ b/sway-lsp/tests/fixtures/auto_import/src/deep_mod/deeper_mod.sw @@ -0,0 +1,18 @@ +library; + +pub fn deep_fun(){} + +pub enum DeepEnum { + Variant: (), + Number: u32, +} + +pub struct DeepStruct { + field: T, +} + +pub type A = DeepStruct; + +pub trait DeepTrait { + fn deep_method(self); +} \ No newline at end of file diff --git a/sway-lsp/tests/fixtures/auto_import/src/main.sw b/sway-lsp/tests/fixtures/auto_import/src/main.sw new file mode 100644 index 00000000000..a46fe3a4beb --- /dev/null +++ b/sway-lsp/tests/fixtures/auto_import/src/main.sw @@ -0,0 +1,39 @@ +contract; + +mod test_mod; +mod deep_mod; + +use test_mod::test_fun; + +pub fn fun() { + let _ = EvmAddress { + value: b256::min(), + }; + + test_fun(); + deep_fun(); + A::fun(); + + let a: DeepEnum = DeepEnum::Variant; + let _ = DeepStruct:: { field: 0 }; + + let _ = TEST_CONST; + let _ = ZERO_B256; + + let _ = overflow(); + let _: Result = msg_sender(); +} + +struct LocalStruct { + field: u64, +} + +impl DeepTrait for LocalStruct { + fn deep_method(self) {} +} + +impl TryFrom for LocalStruct { + fn try_from(u: u32) -> Option { + None + } +} diff --git a/sway-lsp/tests/fixtures/auto_import/src/test_mod.sw b/sway-lsp/tests/fixtures/auto_import/src/test_mod.sw new file mode 100644 index 00000000000..249ab122e12 --- /dev/null +++ b/sway-lsp/tests/fixtures/auto_import/src/test_mod.sw @@ -0,0 +1,11 @@ +library; + +pub fn test_fun(){} + +pub struct A {} + +impl A { + pub fn fun() {} +} + +pub const TEST_CONST: u64 = 111; \ No newline at end of file diff --git a/sway-lsp/tests/integration/code_actions.rs b/sway-lsp/tests/integration/code_actions.rs index 855f429c39a..28d7574555f 100644 --- a/sway-lsp/tests/integration/code_actions.rs +++ b/sway-lsp/tests/integration/code_actions.rs @@ -3,18 +3,23 @@ //! and assert the expected responses. use lsp_types::*; +use pretty_assertions::assert_eq; +use serde_json::json; use std::collections::HashMap; -use sway_lsp::{handlers::request, server_state::ServerState}; +use sway_lsp::{ + capabilities::diagnostic::DiagnosticData, handlers::request, server_state::ServerState, +}; fn create_code_action( uri: Url, title: String, changes: HashMap>, disabled: Option, -) -> CodeAction { - CodeAction { + kind: Option, +) -> CodeActionOrCommand { + CodeActionOrCommand::CodeAction(CodeAction { title, - kind: Some(CodeActionKind::REFACTOR), + kind, diagnostics: None, edit: Some(WorkspaceEdit { changes: Some(changes), @@ -25,15 +30,19 @@ fn create_code_action( is_preferred: None, disabled, data: Some(serde_json::to_value(uri).unwrap()), - } + }) } -fn create_code_action_params(uri: Url, range: Range) -> CodeActionParams { +fn create_code_action_params( + uri: Url, + range: Range, + diagnostics: Option>, +) -> CodeActionParams { CodeActionParams { text_document: TextDocumentIdentifier { uri }, range, context: CodeActionContext { - diagnostics: vec![], + diagnostics: diagnostics.unwrap_or_default(), only: None, trigger_kind: Some(CodeActionTriggerKind::AUTOMATIC), }, @@ -42,6 +51,30 @@ fn create_code_action_params(uri: Url, range: Range) -> CodeActionParams { } } +fn create_diagnostic_from_data(range: Range, data: DiagnosticData) -> Option> { + Some(vec![Diagnostic { + range, + data: Some(json!(data)), + ..Default::default() + }]) +} + +fn create_changes_map(uri: &Url, range: Range, new_text: &str) -> HashMap> { + HashMap::from([( + uri.clone(), + vec![TextEdit { + range, + new_text: new_text.to_string(), + }], + )]) +} + +fn send_request(server: &ServerState, params: &CodeActionParams) -> Vec { + request::handle_code_action(server, params.clone()) + .unwrap() + .unwrap_or_else(|| panic!("Empty response from server for request: {:?}", params)) +} + pub(crate) fn code_action_abi_request(server: &ServerState, uri: &Url) { let params = create_code_action_params( uri.clone(), @@ -55,32 +88,33 @@ pub(crate) fn code_action_abi_request(server: &ServerState, uri: &Url) { character: 9, }, }, + None, ); - let res = request::handle_code_action(server, params); - let mut changes = HashMap::new(); - changes.insert( - uri.clone(), - vec![TextEdit { - range: Range { - start: Position { - line: 31, - character: 0, - }, - end: Position { - line: 31, - character: 0, - }, - }, - new_text: "\nimpl FooABI for Contract {\n fn main() -> u64 {}\n}\n".to_string(), - }], + + let changes = create_changes_map( + uri, + Range { + start: Position { + line: 31, + character: 0, + }, + end: Position { + line: 31, + character: 0, + }, + }, + "\nimpl FooABI for Contract {\n fn main() -> u64 {}\n}\n", ); - let expected = vec![CodeActionOrCommand::CodeAction(create_code_action( + let expected = vec![create_code_action( uri.clone(), "Generate impl for `FooABI`".to_string(), changes, None, - ))]; - assert_eq!(res.unwrap().unwrap(), expected); + Some(CodeActionKind::REFACTOR), + )]; + + let actual = send_request(server, ¶ms); + assert_eq!(expected, actual); } pub(crate) fn code_action_function_request(server: &ServerState, uri: &Url) { @@ -96,29 +130,30 @@ pub(crate) fn code_action_function_request(server: &ServerState, uri: &Url) { character: 4, }, }, + None, ); - let res = request::handle_code_action(server, params); - let mut changes = HashMap::new(); - changes.insert(uri.clone(), vec![TextEdit { - range: Range { - start: Position { - line: 18, - character: 0, - }, - end: Position { - line: 18, - character: 0, + + let changes = create_changes_map(uri, Range { + start: Position { + line: 18, + character: 0, + }, + end: Position { + line: 18, + character: 0, + }, }, - }, - new_text: "/// Add a brief description.\n/// \n/// ### Additional Information\n/// \n/// Provide information beyond the core purpose or functionality.\n/// \n/// ### Reverts\n/// \n/// * List any cases where the function will revert\n/// \n/// ### Number of Storage Accesses\n/// \n/// * Reads: `0`\n/// * Writes: `0`\n/// * Clears: `0`\n/// \n/// ### Examples\n/// \n/// ```sway\n/// let x = test();\n/// ```\n".to_string(), - }]); - let expected = vec![CodeActionOrCommand::CodeAction(create_code_action( + "/// Add a brief description.\n/// \n/// ### Additional Information\n/// \n/// Provide information beyond the core purpose or functionality.\n/// \n/// ### Reverts\n/// \n/// * List any cases where the function will revert\n/// \n/// ### Number of Storage Accesses\n/// \n/// * Reads: `0`\n/// * Writes: `0`\n/// * Clears: `0`\n/// \n/// ### Examples\n/// \n/// ```sway\n/// let x = test();\n/// ```\n"); + let expected = vec![create_code_action( uri.clone(), "Generate a documentation template".to_string(), changes, None, - ))]; - assert_eq!(res.unwrap().unwrap(), expected); + Some(CodeActionKind::REFACTOR), + )]; + + let actual = send_request(server, ¶ms); + assert_eq!(expected, actual); } pub(crate) fn code_action_trait_fn_request(server: &ServerState, uri: &Url) { @@ -134,12 +169,10 @@ pub(crate) fn code_action_trait_fn_request(server: &ServerState, uri: &Url) { character: 10, }, }, + None, ); - let res = request::handle_code_action(server, params); - let mut changes = HashMap::new(); - changes.insert(uri.clone(), vec![TextEdit { - range: Range { + let changes = create_changes_map(uri, Range { start: Position { line: 10, character: 0, @@ -149,15 +182,17 @@ pub(crate) fn code_action_trait_fn_request(server: &ServerState, uri: &Url) { character: 0, }, }, - new_text: " /// Add a brief description.\n /// \n /// ### Additional Information\n /// \n /// Provide information beyond the core purpose or functionality.\n /// \n /// ### Returns\n /// \n /// * [Empty] - Add description here\n /// \n /// ### Reverts\n /// \n /// * List any cases where the function will revert\n /// \n /// ### Number of Storage Accesses\n /// \n /// * Reads: `0`\n /// * Writes: `0`\n /// * Clears: `0`\n /// \n /// ### Examples\n /// \n /// ```sway\n /// let x = test_function();\n /// ```\n".to_string(), - }]); - let expected = vec![CodeActionOrCommand::CodeAction(create_code_action( + " /// Add a brief description.\n /// \n /// ### Additional Information\n /// \n /// Provide information beyond the core purpose or functionality.\n /// \n /// ### Returns\n /// \n /// * [Empty] - Add description here\n /// \n /// ### Reverts\n /// \n /// * List any cases where the function will revert\n /// \n /// ### Number of Storage Accesses\n /// \n /// * Reads: `0`\n /// * Writes: `0`\n /// * Clears: `0`\n /// \n /// ### Examples\n /// \n /// ```sway\n /// let x = test_function();\n /// ```\n"); + let expected = vec![create_code_action( uri.clone(), "Generate a documentation template".to_string(), changes, None, - ))]; - assert_eq!(res.unwrap().unwrap(), expected); + Some(CodeActionKind::REFACTOR), + )]; + + let actual = send_request(server, ¶ms); + assert_eq!(expected, actual); } pub(crate) fn code_action_struct_request(server: &ServerState, uri: &Url) { @@ -173,37 +208,33 @@ pub(crate) fn code_action_struct_request(server: &ServerState, uri: &Url) { character: 11, }, }, + None, ); - let res = request::handle_code_action(server, params); let mut expected: Vec<_> = Vec::new(); - let mut changes = HashMap::new(); - changes.insert( - uri.clone(), - vec![TextEdit { - range: Range { - start: Position { - line: 25, - character: 0, - }, - end: Position { - line: 25, - character: 0, - }, - }, - new_text: "\nimpl Data {\n \n}\n".to_string(), - }], + + let changes = create_changes_map( + uri, + Range { + start: Position { + line: 25, + character: 0, + }, + end: Position { + line: 25, + character: 0, + }, + }, + "\nimpl Data {\n \n}\n", ); - expected.push(CodeActionOrCommand::CodeAction(create_code_action( + expected.push(create_code_action( uri.clone(), "Generate impl for `Data`".to_string(), changes, None, - ))); - let mut changes = HashMap::new(); - changes.insert( - uri.clone(), - vec![TextEdit { - range: Range { + Some(CodeActionKind::REFACTOR), + )); + + let changes = create_changes_map(uri, Range { start: Position { line: 25, character: 0, @@ -213,20 +244,17 @@ pub(crate) fn code_action_struct_request(server: &ServerState, uri: &Url) { character: 0, }, }, - new_text: "\nimpl Data {\n fn new(value: NumberOrString, address: u64) -> Self { Self { value, address } }\n}\n".to_string(), - }], - ); - expected.push(CodeActionOrCommand::CodeAction(create_code_action( + "\nimpl Data {\n fn new(value: NumberOrString, address: u64) -> Self { Self { value, address } }\n}\n"); + expected.push(create_code_action( uri.clone(), "Generate `new`".to_string(), changes, None, - ))); - let mut changes = HashMap::new(); - changes.insert( - uri.clone(), - vec![TextEdit { - range: Range { + Some(CodeActionKind::REFACTOR), + )); + let changes = create_changes_map( + uri, + Range { start: Position { line: 19, character: 0, @@ -236,16 +264,17 @@ pub(crate) fn code_action_struct_request(server: &ServerState, uri: &Url) { character: 0, }, }, - new_text: "/// Add a brief description.\n/// \n/// ### Additional Information\n/// \n/// Provide information beyond the core purpose or functionality.\n".to_string(), - }], - ); - expected.push(CodeActionOrCommand::CodeAction(create_code_action( + "/// Add a brief description.\n/// \n/// ### Additional Information\n/// \n/// Provide information beyond the core purpose or functionality.\n"); + expected.push(create_code_action( uri.clone(), "Generate a documentation template".to_string(), changes, None, - ))); - assert_eq!(res.unwrap().unwrap(), expected); + Some(CodeActionKind::REFACTOR), + )); + + let actual = send_request(server, ¶ms); + assert_eq!(expected, actual); } pub(crate) fn code_action_struct_type_params_request(server: &ServerState, uri: &Url) { @@ -261,64 +290,58 @@ pub(crate) fn code_action_struct_type_params_request(server: &ServerState, uri: character: 9, }, }, + None, ); - let res = request::handle_code_action(server, params); let mut expected: Vec<_> = Vec::new(); - let mut changes = HashMap::new(); - changes.insert( - uri.clone(), - vec![TextEdit { - range: Range { - start: Position { - line: 7, - character: 0, - }, - end: Position { - line: 7, - character: 0, - }, - }, - new_text: "\nimpl Data {\n \n}\n".to_string(), - }], + let changes = create_changes_map( + uri, + Range { + start: Position { + line: 7, + character: 0, + }, + end: Position { + line: 7, + character: 0, + }, + }, + "\nimpl Data {\n \n}\n", ); - expected.push(CodeActionOrCommand::CodeAction(create_code_action( + expected.push(create_code_action( uri.clone(), "Generate impl for `Data`".to_string(), changes, None, - ))); + Some(CodeActionKind::REFACTOR), + )); - let mut changes = HashMap::new(); - changes.insert( - uri.clone(), - vec![TextEdit { - range: Range { - start: Position { - line: 9, - character: 0, - }, - end: Position { - line: 9, - character: 0, - }, - }, - new_text: " fn new(value: T) -> Self { Self { value } }\n".to_string(), - }], + let changes = create_changes_map( + uri, + Range { + start: Position { + line: 9, + character: 0, + }, + end: Position { + line: 9, + character: 0, + }, + }, + " fn new(value: T) -> Self { Self { value } }\n", ); - expected.push(CodeActionOrCommand::CodeAction(create_code_action( + expected.push(create_code_action( uri.clone(), "Generate `new`".to_string(), changes, Some(CodeActionDisabled { reason: "Struct Data already has a `new` function".to_string(), }), - ))); + Some(CodeActionKind::REFACTOR), + )); - let mut changes = HashMap::new(); - changes.insert( - uri.clone(), - vec![TextEdit { - range: Range { + let changes = create_changes_map( + uri, + Range { start: Position { line: 4, character: 0, @@ -328,16 +351,18 @@ pub(crate) fn code_action_struct_type_params_request(server: &ServerState, uri: character: 0, }, }, - new_text: "/// Add a brief description.\n/// \n/// ### Additional Information\n/// \n/// Provide information beyond the core purpose or functionality.\n".to_string(), - }], -); - expected.push(CodeActionOrCommand::CodeAction(create_code_action( + "/// Add a brief description.\n/// \n/// ### Additional Information\n/// \n/// Provide information beyond the core purpose or functionality.\n"); + + expected.push(create_code_action( uri.clone(), "Generate a documentation template".to_string(), changes, None, - ))); - assert_eq!(res.unwrap().unwrap(), expected); + Some(CodeActionKind::REFACTOR), + )); + + let actual = send_request(server, ¶ms); + assert_eq!(expected, actual); } pub(crate) fn code_action_struct_existing_impl_request(server: &ServerState, uri: &Url) { @@ -353,62 +378,55 @@ pub(crate) fn code_action_struct_existing_impl_request(server: &ServerState, uri character: 7, }, }, + None, ); - let res = request::handle_code_action(server, params); let mut expected: Vec<_> = Vec::new(); - let mut changes = HashMap::new(); - changes.insert( - uri.clone(), - vec![TextEdit { - range: Range { - start: Position { - line: 6, - character: 0, - }, - end: Position { - line: 6, - character: 0, - }, - }, - new_text: "\nimpl A {\n \n}\n".to_string(), - }], + let changes = create_changes_map( + uri, + Range { + start: Position { + line: 6, + character: 0, + }, + end: Position { + line: 6, + character: 0, + }, + }, + "\nimpl A {\n \n}\n", ); - expected.push(CodeActionOrCommand::CodeAction(create_code_action( + expected.push(create_code_action( uri.clone(), "Generate impl for `A`".to_string(), changes, None, - ))); + Some(CodeActionKind::REFACTOR), + )); - let mut changes = HashMap::new(); - changes.insert( - uri.clone(), - vec![TextEdit { - range: Range { - start: Position { - line: 8, - character: 0, - }, - end: Position { - line: 8, - character: 0, - }, - }, - new_text: " fn new(a: u64, b: u64) -> Self { Self { a, b } }\n".to_string(), - }], + let changes = create_changes_map( + uri, + Range { + start: Position { + line: 8, + character: 0, + }, + end: Position { + line: 8, + character: 0, + }, + }, + " fn new(a: u64, b: u64) -> Self { Self { a, b } }\n", ); - expected.push(CodeActionOrCommand::CodeAction(create_code_action( + expected.push(create_code_action( uri.clone(), "Generate `new`".to_string(), changes, None, - ))); - - let mut changes = HashMap::new(); - changes.insert( - uri.clone(), - vec![TextEdit { - range: Range { + Some(CodeActionKind::REFACTOR), + )); + let changes = create_changes_map( + uri, + Range { start: Position { line: 2, character: 0, @@ -418,16 +436,480 @@ pub(crate) fn code_action_struct_existing_impl_request(server: &ServerState, uri character: 0, }, }, - new_text: "/// Add a brief description.\n/// \n/// ### Additional Information\n/// \n/// Provide information beyond the core purpose or functionality.\n".to_string(), - }], - ); - expected.push(CodeActionOrCommand::CodeAction(create_code_action( + "/// Add a brief description.\n/// \n/// ### Additional Information\n/// \n/// Provide information beyond the core purpose or functionality.\n"); + expected.push(create_code_action( uri.clone(), "Generate a documentation template".to_string(), changes, None, - ))); + Some(CodeActionKind::REFACTOR), + )); + + let actual = send_request(server, ¶ms); + assert_eq!(expected, actual); +} + +pub(crate) fn code_action_auto_import_struct_request(server: &ServerState, uri: &Url) { + // EvmAddress: external library + let range = Range { + start: Position { + line: 8, + character: 12, + }, + end: Position { + line: 8, + character: 22, + }, + }; + + let params = create_code_action_params( + uri.clone(), + range, + create_diagnostic_from_data( + range, + DiagnosticData { + unknown_symbol_name: Some("EvmAddress".to_string()), + }, + ), + ); + let changes = create_changes_map( + uri, + Range { + start: Position { + line: 5, + character: 0, + }, + end: Position { + line: 5, + character: 0, + }, + }, + "use std::vm::evm::evm_address::EvmAddress;\n", + ); + let expected = vec![create_code_action( + uri.clone(), + "Import `std::vm::evm::evm_address::EvmAddress`".to_string(), + changes, + None, + Some(CodeActionKind::QUICKFIX), + )]; + + let actual = send_request(server, ¶ms); + assert_eq!(expected, actual); + + // DeepStruct: local library + let range = Range { + start: Position { + line: 17, + character: 12, + }, + end: Position { + line: 17, + character: 22, + }, + }; + + let params = create_code_action_params( + uri.clone(), + range, + create_diagnostic_from_data( + range, + DiagnosticData { + unknown_symbol_name: Some("DeepStruct".to_string()), + }, + ), + ); + let changes = create_changes_map( + uri, + Range { + start: Position { + line: 5, + character: 0, + }, + end: Position { + line: 5, + character: 0, + }, + }, + "use deep_mod::deeper_mod::DeepStruct;\n", + ); + let expected = vec![create_code_action( + uri.clone(), + "Import `deep_mod::deeper_mod::DeepStruct`".to_string(), + changes, + None, + Some(CodeActionKind::QUICKFIX), + )]; + + let actual = send_request(server, ¶ms); + assert_eq!(expected, actual); +} + +pub(crate) fn code_action_auto_import_enum_request(server: &ServerState, uri: &Url) { + // TODO: Add a test for an enum variant when https://github.com/FuelLabs/sway/issues/5188 is fixed. + + // AuthError: external library + let range = Range { + start: Position { + line: 23, + character: 28, + }, + end: Position { + line: 23, + character: 37, + }, + }; + + let params = create_code_action_params( + uri.clone(), + range, + create_diagnostic_from_data( + range, + DiagnosticData { + unknown_symbol_name: Some("AuthError".to_string()), + }, + ), + ); + let changes = create_changes_map( + uri, + Range { + start: Position { + line: 5, + character: 0, + }, + end: Position { + line: 5, + character: 0, + }, + }, + "use std::auth::AuthError;\n", + ); + let expected = vec![create_code_action( + uri.clone(), + "Import `std::auth::AuthError`".to_string(), + changes, + None, + Some(CodeActionKind::QUICKFIX), + )]; + + let actual = send_request(server, ¶ms); + assert_eq!(expected, actual); + + // DeepEnum: local library + let range = Range { + start: Position { + line: 16, + character: 11, + }, + end: Position { + line: 16, + character: 19, + }, + }; + + let params = create_code_action_params( + uri.clone(), + range, + create_diagnostic_from_data( + range, + DiagnosticData { + unknown_symbol_name: Some("DeepEnum".to_string()), + }, + ), + ); + let changes = create_changes_map( + uri, + Range { + start: Position { + line: 5, + character: 0, + }, + end: Position { + line: 5, + character: 0, + }, + }, + "use deep_mod::deeper_mod::DeepEnum;\n", + ); + let expected = vec![create_code_action( + uri.clone(), + "Import `deep_mod::deeper_mod::DeepEnum`".to_string(), + changes, + None, + Some(CodeActionKind::QUICKFIX), + )]; + + let actual = send_request(server, ¶ms); + assert_eq!(expected, actual); +} + +pub(crate) fn code_action_auto_import_function_request(server: &ServerState, uri: &Url) { + // TODO: external library, test with `overflow`` + // Tracking issue: https://github.com/FuelLabs/sway/issues/5191 + + // deep_fun: local library + let range = Range { + start: Position { + line: 13, + character: 4, + }, + end: Position { + line: 13, + character: 12, + }, + }; + + let params = create_code_action_params( + uri.clone(), + range, + create_diagnostic_from_data( + range, + DiagnosticData { + unknown_symbol_name: Some("deep_fun".to_string()), + }, + ), + ); + let changes = create_changes_map( + uri, + Range { + start: Position { + line: 5, + character: 0, + }, + end: Position { + line: 5, + character: 0, + }, + }, + "use deep_mod::deeper_mod::deep_fun;\n", + ); + let expected = vec![create_code_action( + uri.clone(), + "Import `deep_mod::deeper_mod::deep_fun`".to_string(), + changes, + None, + Some(CodeActionKind::QUICKFIX), + )]; + + let actual = send_request(server, ¶ms); + assert_eq!(expected, actual); +} + +pub(crate) fn code_action_auto_import_constant_request(server: &ServerState, uri: &Url) { + // TODO: external library, test with ZERO_B256 + // Tracking issue: https://github.com/FuelLabs/sway/issues/5192 + + // TEST_CONST: import a constant from a local library + let range = Range { + start: Position { + line: 19, + character: 12, + }, + end: Position { + line: 19, + character: 22, + }, + }; + + let params = create_code_action_params( + uri.clone(), + range, + create_diagnostic_from_data( + range, + DiagnosticData { + unknown_symbol_name: Some("TEST_CONST".to_string()), + }, + ), + ); + let changes = create_changes_map( + uri, + Range { + start: Position { + line: 5, + character: 0, + }, + end: Position { + line: 5, + character: 23, + }, + }, + "use test_mod::{TEST_CONST, test_fun};", + ); + let expected = vec![create_code_action( + uri.clone(), + "Import `test_mod::TEST_CONST`".to_string(), + changes, + None, + Some(CodeActionKind::QUICKFIX), + )]; + + let actual = send_request(server, ¶ms); + assert_eq!(expected, actual); +} + +pub(crate) fn code_action_auto_import_trait_request(server: &ServerState, uri: &Url) { + // TryFrom: external library + let range = Range { + start: Position { + line: 34, + character: 5, + }, + end: Position { + line: 34, + character: 12, + }, + }; + + let params = create_code_action_params( + uri.clone(), + range, + create_diagnostic_from_data( + range, + DiagnosticData { + unknown_symbol_name: Some("TryFrom".to_string()), + }, + ), + ); + let changes = create_changes_map( + uri, + Range { + start: Position { + line: 5, + character: 0, + }, + end: Position { + line: 5, + character: 0, + }, + }, + "use std::convert::TryFrom;\n", + ); + let expected = vec![create_code_action( + uri.clone(), + "Import `std::convert::TryFrom`".to_string(), + changes, + None, + Some(CodeActionKind::QUICKFIX), + )]; + + let actual = send_request(server, ¶ms); + assert_eq!(expected, actual); + + // DeepTrait: local library + let range = Range { + start: Position { + line: 30, + character: 5, + }, + end: Position { + line: 30, + character: 14, + }, + }; + + let params = create_code_action_params( + uri.clone(), + range, + create_diagnostic_from_data( + range, + DiagnosticData { + unknown_symbol_name: Some("DeepTrait".to_string()), + }, + ), + ); + let changes = create_changes_map( + uri, + Range { + start: Position { + line: 5, + character: 0, + }, + end: Position { + line: 5, + character: 0, + }, + }, + "use deep_mod::deeper_mod::DeepTrait;\n", + ); + let expected = vec![create_code_action( + uri.clone(), + "Import `deep_mod::deeper_mod::DeepTrait`".to_string(), + changes, + None, + Some(CodeActionKind::QUICKFIX), + )]; + + let actual = send_request(server, ¶ms); + assert_eq!(expected, actual); +} + +pub(crate) fn code_action_auto_import_alias_request(server: &ServerState, uri: &Url) { + // TODO: find an example in an external library + // A: local library with multiple possible imports + let range = Range { + start: Position { + line: 14, + character: 4, + }, + end: Position { + line: 14, + character: 5, + }, + }; + + let params = create_code_action_params( + uri.clone(), + range, + create_diagnostic_from_data( + range, + DiagnosticData { + unknown_symbol_name: Some("A".to_string()), + }, + ), + ); + let changes = create_changes_map( + uri, + Range { + start: Position { + line: 5, + character: 0, + }, + end: Position { + line: 5, + character: 0, + }, + }, + "use deep_mod::deeper_mod::A;\n", + ); + let mut expected = vec![create_code_action( + uri.clone(), + "Import `deep_mod::deeper_mod::A`".to_string(), + changes, + None, + Some(CodeActionKind::QUICKFIX), + )]; + let changes = create_changes_map( + uri, + Range { + start: Position { + line: 5, + character: 0, + }, + end: Position { + line: 5, + character: 23, + }, + }, + "use test_mod::{A, test_fun};", + ); + expected.push(create_code_action( + uri.clone(), + "Import `test_mod::A`".to_string(), + changes, + None, + Some(CodeActionKind::QUICKFIX), + )); - let result = res.unwrap().unwrap(); - assert_eq!(result, expected); + let actual = send_request(server, ¶ms); + assert_eq!(expected, actual); } diff --git a/sway-lsp/tests/lib.rs b/sway-lsp/tests/lib.rs index eb94a2ae8e2..de50f86e179 100644 --- a/sway-lsp/tests/lib.rs +++ b/sway-lsp/tests/lib.rs @@ -1746,6 +1746,36 @@ lsp_capability_test!( code_actions::code_action_struct_existing_impl_request, self_impl_reassignment_dir().join("src/main.sw") ); +lsp_capability_test!( + code_action_auto_import_struct, + code_actions::code_action_auto_import_struct_request, + test_fixtures_dir().join("auto_import/src/main.sw") +); +lsp_capability_test!( + code_action_auto_import_enum, + code_actions::code_action_auto_import_enum_request, + test_fixtures_dir().join("auto_import/src/main.sw") +); +lsp_capability_test!( + code_action_auto_import_function, + code_actions::code_action_auto_import_function_request, + test_fixtures_dir().join("auto_import/src/main.sw") +); +lsp_capability_test!( + code_action_auto_import_constant, + code_actions::code_action_auto_import_constant_request, + test_fixtures_dir().join("auto_import/src/main.sw") +); +lsp_capability_test!( + code_action_auto_import_trait, + code_actions::code_action_auto_import_trait_request, + test_fixtures_dir().join("auto_import/src/main.sw") +); +lsp_capability_test!( + code_action_auto_import_alias, + code_actions::code_action_auto_import_alias_request, + test_fixtures_dir().join("auto_import/src/main.sw") +); lsp_capability_test!( code_lens, lsp::code_lens_request, diff --git a/sway-types/src/ident.rs b/sway-types/src/ident.rs index 8ad4fdec646..0bc63ca5f2e 100644 --- a/sway-types/src/ident.rs +++ b/sway-types/src/ident.rs @@ -75,6 +75,14 @@ impl BaseIdent { is_raw_ident: false, } } + + pub fn dummy() -> Ident { + Ident { + name_override_opt: Some("foo".into()), + span: Span::dummy(), + is_raw_ident: false, + } + } } /// An [Ident] is an _identifier_ with a corresponding `span` from which it was derived.