diff --git a/sway-core/src/decl_engine/mod.rs b/sway-core/src/decl_engine/mod.rs index c101f781e27..77a9473fd53 100644 --- a/sway-core/src/decl_engine/mod.rs +++ b/sway-core/src/decl_engine/mod.rs @@ -10,7 +10,7 @@ #[allow(clippy::module_inception)] pub(crate) mod engine; -pub(crate) mod functional_decl_id; +pub mod functional_decl_id; pub mod id; pub(crate) mod interface_decl_id; pub(crate) mod mapping; diff --git a/sway-core/src/language/ty/ast_node.rs b/sway-core/src/language/ty/ast_node.rs index 0a51b7f0a1b..d4ad00df418 100644 --- a/sway-core/src/language/ty/ast_node.rs +++ b/sway-core/src/language/ty/ast_node.rs @@ -23,7 +23,7 @@ pub trait GetDeclIdent { #[derive(Clone, Debug)] pub struct TyAstNode { pub content: TyAstNodeContent, - pub(crate) span: Span, + pub span: Span, } impl EqWithEngines for TyAstNode {} diff --git a/sway-core/src/semantic_analysis/namespace/items.rs b/sway-core/src/semantic_analysis/namespace/items.rs index 5dbc0976206..818cca24788 100644 --- a/sway-core/src/semantic_analysis/namespace/items.rs +++ b/sway-core/src/semantic_analysis/namespace/items.rs @@ -156,7 +156,7 @@ impl Items { self.implemented_traits.insert_for_type(engines, type_id); } - pub(crate) fn get_methods_for_type( + pub fn get_methods_for_type( &self, engines: Engines<'_>, type_id: TypeId, diff --git a/sway-core/src/type_system/info.rs b/sway-core/src/type_system/info.rs index 26820b09571..007901df477 100644 --- a/sway-core/src/type_system/info.rs +++ b/sway-core/src/type_system/info.rs @@ -435,6 +435,21 @@ impl DisplayWithEngines for TypeInfo { } } +impl TypeInfo { + pub fn display_name(&self) -> String { + match self { + TypeInfo::UnknownGeneric { name, .. } => name.to_string(), + TypeInfo::Placeholder(type_param) => type_param.name_ident.to_string(), + TypeInfo::Enum(decl_ref) => decl_ref.name.to_string(), + TypeInfo::Struct(decl_ref) => decl_ref.name.to_string(), + TypeInfo::ContractCaller { abi_name, .. } => abi_name.to_string(), + TypeInfo::Custom { call_path, .. } => call_path.to_string(), + TypeInfo::Storage { .. } => "storage".into(), + _ => format!("{self:?}"), + } + } +} + impl UnconstrainedTypeParameters for TypeInfo { fn type_parameter_is_unconstrained( &self, diff --git a/sway-lsp/src/capabilities/completion.rs b/sway-lsp/src/capabilities/completion.rs index c7368190ad5..23353ff295b 100644 --- a/sway-lsp/src/capabilities/completion.rs +++ b/sway-lsp/src/capabilities/completion.rs @@ -1,18 +1,86 @@ -use crate::core::{ - token::{AstToken, SymbolKind, Token, TypedAstToken}, - token_map::TokenMap, +use sway_core::{ + language::ty::{TyAstNodeContent, TyDeclaration, TyFunctionDeclaration}, + namespace::Items, + Engines, TypeId, TypeInfo, }; -use tower_lsp::lsp_types::{CompletionItem, CompletionItemKind}; +use sway_types::Ident; +use tower_lsp::lsp_types::{ + CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionTextEdit, Position, + Range, TextEdit, +}; + +pub(crate) fn to_completion_items( + namespace: &Items, + engines: Engines<'_>, + ident_to_complete: &Ident, + fn_decl: &TyFunctionDeclaration, + position: Position, +) -> Vec { + type_id_of_raw_ident(engines, namespace, ident_to_complete, fn_decl) + .map(|type_id| completion_items_for_type_id(engines, namespace, type_id, position)) + .unwrap_or_default() +} -pub(crate) fn to_completion_items(token_map: &TokenMap) -> Vec { +/// Gathers the given [TypeId] struct's fields and methods and builds completion items. +fn completion_items_for_type_id( + engines: Engines<'_>, + namespace: &Items, + type_id: TypeId, + position: Position, +) -> Vec { let mut completion_items = vec![]; + let type_info = engines.te().get(type_id); - for item in token_map.iter() { - let ((ident, _), token) = item.pair(); - if is_initial_declaration(token) { + if let TypeInfo::Struct(decl_ref) = type_info.clone() { + let struct_decl = engines.de().get_struct(&decl_ref.id); + for field in struct_decl.fields { let item = CompletionItem { - label: ident.as_str().to_string(), - kind: completion_item_kind(&token.kind), + kind: Some(CompletionItemKind::FIELD), + label: field.name.as_str().to_string(), + label_details: Some(CompletionItemLabelDetails { + description: Some(field.type_argument.span.str()), + detail: None, + }), + ..Default::default() + }; + completion_items.push(item); + } + } + + for method in namespace.get_methods_for_type(engines, type_id) { + let fn_decl = engines.de().get_function(&method.id); + let params = fn_decl.clone().parameters; + + // Only show methods that take `self` as the first parameter. + if params.first().map(|p| p.is_self()).unwrap_or(false) { + let params_short = match params.is_empty() { + true => "()".to_string(), + false => "(…)".to_string(), + }; + let params_edit_str = params + .iter() + .filter_map(|p| { + if p.is_self() { + return None; + } + Some(p.name.as_str()) + }) + .collect::>() + .join(", "); + let item = CompletionItem { + kind: Some(CompletionItemKind::METHOD), + label: format!("{}{}", method.name.as_str(), params_short), + text_edit: Some(CompletionTextEdit::Edit(TextEdit { + range: Range { + start: position, + end: position, + }, + new_text: format!("{}({})", method.name.as_str(), params_edit_str), + })), + label_details: Some(CompletionItemLabelDetails { + description: Some(fn_signature_string(&fn_decl, &type_info)), + detail: None, + }), ..Default::default() }; completion_items.push(item); @@ -22,40 +90,101 @@ pub(crate) fn to_completion_items(token_map: &TokenMap) -> Vec { completion_items } -/// Given a `SymbolKind`, return the `lsp_types::CompletionItemKind` that corresponds to it. -pub fn completion_item_kind(symbol_kind: &SymbolKind) -> Option { - match symbol_kind { - SymbolKind::Field => Some(CompletionItemKind::FIELD), - SymbolKind::BuiltinType => Some(CompletionItemKind::TYPE_PARAMETER), - SymbolKind::ValueParam => Some(CompletionItemKind::VALUE), - SymbolKind::Function | SymbolKind::DeriveHelper => Some(CompletionItemKind::FUNCTION), - SymbolKind::Const => Some(CompletionItemKind::CONSTANT), - SymbolKind::Struct => Some(CompletionItemKind::STRUCT), - SymbolKind::Trait => Some(CompletionItemKind::INTERFACE), - SymbolKind::Module => Some(CompletionItemKind::MODULE), - SymbolKind::Enum => Some(CompletionItemKind::ENUM), - SymbolKind::Variant => Some(CompletionItemKind::ENUM_MEMBER), - SymbolKind::TypeParameter => Some(CompletionItemKind::TYPE_PARAMETER), - SymbolKind::BoolLiteral - | SymbolKind::ByteLiteral - | SymbolKind::StringLiteral - | SymbolKind::NumericLiteral => Some(CompletionItemKind::VALUE), - SymbolKind::Variable => Some(CompletionItemKind::VARIABLE), - SymbolKind::Keyword => Some(CompletionItemKind::KEYWORD), - SymbolKind::Unknown => None, +/// Returns the [String] of the shortened function signature to display in the completion item's label details. +fn fn_signature_string(fn_decl: &TyFunctionDeclaration, parent_type_info: &TypeInfo) -> String { + let params_str = fn_decl + .parameters + .iter() + .map(|p| replace_self_with_type_str(p.type_argument.clone().span.str(), parent_type_info)) + .collect::>() + .join(", "); + format!( + "fn({}) -> {}", + params_str, + replace_self_with_type_str(fn_decl.return_type.clone().span.str(), parent_type_info) + ) +} + +/// Given a [String] representing a type, replaces `Self` with the display name of the type. +fn replace_self_with_type_str(type_str: String, parent_type_info: &TypeInfo) -> String { + if type_str == "Self" { + return parent_type_info.display_name(); } + type_str } -fn is_initial_declaration(token_type: &Token) -> bool { - match &token_type.typed { - Some(typed_ast_token) => { - matches!( - typed_ast_token, - TypedAstToken::TypedDeclaration(_) | TypedAstToken::TypedFunctionDeclaration(_) - ) - } - None => { - matches!(token_type.parsed, AstToken::Declaration(_)) +/// Returns the [TypeId] of an ident that may include field accesses and may be incomplete. +/// For the first part of the ident, it looks for instantiation in the scope of the given +/// [TyFunctionDeclaration]. For example, given `a.b.c`, it will return the type ID of `c` +/// if it can resolve `a` in the given function. +fn type_id_of_raw_ident( + engines: Engines, + namespace: &Items, + ident: &Ident, + fn_decl: &TyFunctionDeclaration, +) -> Option { + let full_ident = ident.as_str(); + + // If this ident has no field accesses or chained methods, look for it in the local function scope. + if !full_ident.contains('.') { + return type_id_of_local_ident(full_ident, fn_decl); + } + + // Otherwise, start with the first part of the ident and follow the subsequent types. + let parts = full_ident.split('.').collect::>(); + let mut curr_type_id = type_id_of_local_ident(parts[0], fn_decl); + let mut i = 1; + + while (i < parts.len()) && curr_type_id.is_some() { + if parts[i].ends_with(')') { + let method_name = parts[i].split_at(parts[i].find('(').unwrap_or(0)).0; + curr_type_id = namespace + .get_methods_for_type(engines, curr_type_id?) + .iter() + .find_map(|decl_ref| { + if decl_ref.name.as_str() == method_name { + return Some(engines.de().get_function(&decl_ref.id).return_type.type_id); + } + None + }); + } else if let TypeInfo::Struct(decl_ref) = engines.te().get(curr_type_id.unwrap()) { + let struct_decl = engines.de().get_struct(&decl_ref.id); + curr_type_id = struct_decl + .fields + .iter() + .find(|field| field.name.to_string() == parts[i]) + .map(|field| field.type_argument.type_id); } + i += 1; } + curr_type_id +} + +/// Returns the [TypeId] of an ident by looking for its instantiation within the scope of the +/// given [TyFunctionDeclaration]. +fn type_id_of_local_ident(ident_name: &str, fn_decl: &TyFunctionDeclaration) -> Option { + fn_decl + .parameters + .iter() + .find_map(|param| { + // Check if this ident is a function parameter + if param.name.as_str() == ident_name { + return Some(param.type_argument.type_id); + } + None + }) + .or_else(|| { + // Check if there is a variable declaration for this ident + fn_decl.body.contents.iter().find_map(|node| { + if let TyAstNodeContent::Declaration(TyDeclaration::VariableDeclaration( + variable_decl, + )) = node.content.clone() + { + if variable_decl.name.as_str() == ident_name { + return Some(variable_decl.return_type); + } + } + None + }) + }) } diff --git a/sway-lsp/src/core/session.rs b/sway-lsp/src/core/session.rs index 49bac061097..69184cb71ac 100644 --- a/sway-lsp/src/core/session.rs +++ b/sway-lsp/src/core/session.rs @@ -6,7 +6,9 @@ use crate::{ runnable::{Runnable, RunnableMainFn, RunnableTestFn}, }, core::{ - document::TextDocument, sync::SyncWorkspace, token::get_range_from_span, + document::TextDocument, + sync::SyncWorkspace, + token::{get_range_from_span, TypedAstToken}, token_map::TokenMap, }, error::{DocumentError, LanguageServerError}, @@ -75,6 +77,7 @@ impl Session { pub fn init(&self, uri: &Url) -> Result { let manifest_dir = PathBuf::from(uri.path()); + // Create a new temp dir that clones the current workspace // and store manifest and temp paths self.sync.create_temp_dir_from_workspace(&manifest_dir)?; @@ -244,10 +247,34 @@ impl Session { }) } - pub fn completion_items(&self) -> Option> { - Some(capabilities::completion::to_completion_items( - self.token_map(), - )) + pub fn completion_items( + &self, + uri: &Url, + position: Position, + trigger_char: String, + ) -> Option> { + let shifted_position = Position { + line: position.line, + character: position.character - trigger_char.len() as u32 - 1, + }; + let (ident_to_complete, _) = self.token_map.token_at_position(uri, shifted_position)?; + let fn_tokens = self + .token_map + .tokens_at_position(uri, shifted_position, Some(true)); + let (_, fn_token) = fn_tokens.first()?; + let compiled_program = &*self.compiled_program.read(); + + if let Some(TypedAstToken::TypedFunctionDeclaration(fn_decl)) = fn_token.typed.clone() { + let program = compiled_program.typed.clone()?; + return Some(capabilities::completion::to_completion_items( + &program.root.namespace, + Engines::new(&self.type_engine.read(), &self.decl_engine.read()), + &ident_to_complete, + &fn_decl, + position, + )); + } + None } pub fn symbol_information(&self, url: &Url) -> Option> { @@ -399,7 +426,7 @@ impl Session { /// Create runnables if the `TyProgramKind` of the `TyProgram` is a script. fn create_runnables(&self, typed_program: &ty::TyProgram) { // Insert runnable test functions. - let decl_engine = &*self.decl_engine.read(); + let decl_engine = &self.decl_engine.read(); for (decl, _) in typed_program.test_fns(decl_engine) { // Get the span of the first attribute if it exists, otherwise use the span of the function name. let span = decl diff --git a/sway-lsp/src/core/token.rs b/sway-lsp/src/core/token.rs index c696b84d888..28d66645cac 100644 --- a/sway-lsp/src/core/token.rs +++ b/sway-lsp/src/core/token.rs @@ -24,33 +24,34 @@ use tower_lsp::lsp_types::{Position, Range}; /// useful to the language server. #[derive(Debug, Clone)] pub enum AstToken { + AbiCastExpression(AbiCastExpression), + AmbiguousPathExpression(AmbiguousPathExpression), + Attribute(Attribute), Declaration(Declaration), + DelineatedPathExpression(DelineatedPathExpression), + EnumVariant(EnumVariant), + ErrorRecovery(Span), Expression(Expression), - StructExpression(StructExpression), - StructExpressionField(StructExpressionField), - StructScrutineeField(StructScrutineeField), - FunctionParameter(FunctionParameter), FunctionApplicationExpression(FunctionApplicationExpression), + FunctionParameter(FunctionParameter), + Ident(Ident), + IncludeStatement, + Intrinsic(Intrinsic), + Keyword(Ident), + LibraryName(Ident), MethodApplicationExpression(MethodApplicationExpression), - AmbiguousPathExpression(AmbiguousPathExpression), - DelineatedPathExpression(DelineatedPathExpression), - AbiCastExpression(AbiCastExpression), + Scrutinee(Scrutinee), + StorageField(StorageField), + StructExpression(StructExpression), + StructExpressionField(StructExpressionField), StructField(StructField), - EnumVariant(EnumVariant), - TraitFn(TraitFn), + StructScrutineeField(StructScrutineeField), + Supertrait(Supertrait), TraitConstraint(TraitConstraint), - StorageField(StorageField), - Scrutinee(Scrutinee), - Keyword(Ident), - Intrinsic(Intrinsic), - Attribute(Attribute), - LibraryName(Ident), - IncludeStatement, - UseStatement(UseStatement), + TraitFn(TraitFn), TypeArgument(TypeArgument), TypeParameter(TypeParameter), - Supertrait(Supertrait), - Ident(Ident), + UseStatement(UseStatement), } /// The `TypedAstToken` holds the types produced by the [sway_core::language::ty::TyProgram]. diff --git a/sway-lsp/src/core/token_map.rs b/sway-lsp/src/core/token_map.rs index 0021e3735e3..ab7e4fb1883 100644 --- a/sway-lsp/src/core/token_map.rs +++ b/sway-lsp/src/core/token_map.rs @@ -58,30 +58,78 @@ impl TokenMap { /// Given a cursor [Position], return the [Ident] of a token in the /// Iterator if one exists at that position. - pub fn ident_at_position(&self, cursor_position: Position, tokens: I) -> Option + pub fn idents_at_position(&self, cursor_position: Position, tokens: I) -> Vec where I: Iterator, { - for (ident, _) in tokens { - let range = token::get_range_from_span(&ident.span()); - if cursor_position >= range.start && cursor_position <= range.end { - return Some(ident); - } - } - None + tokens + .filter_map(|(ident, _)| { + let range = token::get_range_from_span(&ident.span()); + if cursor_position >= range.start && cursor_position <= range.end { + return Some(ident); + } + None + }) + .collect() } - /// Check if the code editor's cursor is currently over one of our collected tokens. + /// Returns the first collected tokens that is at the cursor position. pub fn token_at_position(&self, uri: &Url, position: Position) -> Option<(Ident, Token)> { let tokens = self.tokens_for_file(uri); - self.ident_at_position(position, tokens).and_then(|ident| { - self.try_get(&token::to_ident_key(&ident)) - .try_unwrap() - .map(|item| { - let ((ident, _), token) = item.pair(); - (ident.clone(), token.clone()) - }) - }) + self.idents_at_position(position, tokens) + .first() + .and_then(|ident| { + self.try_get(&token::to_ident_key(ident)) + .try_unwrap() + .map(|item| { + let ((ident, _), token) = item.pair(); + (ident.clone(), token.clone()) + }) + }) + } + + /// Returns all collected tokens that are at the given [Position] in the file. + /// If `functions_only` is true, it only returns tokens of type [TypedAstToken::TypedFunctionDeclaration]. + /// + /// This is different from `idents_at_position` because this searches the spans of token bodies, not + /// just the spans of the token idents. For example, if we want to find out what function declaration + /// the cursor is inside of, we need to search the body of the function declaration, not just the ident + /// of the function declaration (the function name). + pub fn tokens_at_position( + &self, + uri: &Url, + position: Position, + functions_only: Option, + ) -> Vec<(Ident, Token)> { + let tokens = self.tokens_for_file(uri); + tokens + .filter_map(|(ident, token)| { + let span = match token.typed { + Some(TypedAstToken::TypedFunctionDeclaration(decl)) => decl.span(), + _ => ident.span(), + }; + let range = token::get_range_from_span(&span); + if position >= range.start && position <= range.end { + return self + .try_get(&token::to_ident_key(&ident)) + .try_unwrap() + .map(|item| { + let ((ident, _), token) = item.pair(); + (ident.clone(), token.clone()) + }); + } + None + }) + .filter_map(|(ident, token)| { + if functions_only == Some(true) { + if let Some(TypedAstToken::TypedFunctionDeclaration(_)) = token.typed { + return Some((ident, token)); + } + return None; + } + Some((ident, token)) + }) + .collect() } /// Uses the [TypeId] to find the associated [ty::TyDeclaration] in the TokenMap. diff --git a/sway-lsp/src/server.rs b/sway-lsp/src/server.rs index a0bf70598dc..9db8ad43149 100644 --- a/sway-lsp/src/server.rs +++ b/sway-lsp/src/server.rs @@ -103,8 +103,7 @@ pub fn capabilities() -> ServerCapabilities { ), document_symbol_provider: Some(OneOf::Left(true)), completion_provider: Some(CompletionOptions { - resolve_provider: Some(false), - trigger_characters: None, + trigger_characters: Some(vec![".".to_string()]), ..Default::default() }), document_formatting_provider: Some(OneOf::Left(true)), @@ -370,12 +369,16 @@ impl LanguageServer for Backend { &self, params: CompletionParams, ) -> jsonrpc::Result> { + let trigger_char = params + .context + .map(|ctx| ctx.trigger_character) + .unwrap_or_default() + .unwrap_or("".to_string()); + let position = params.text_document_position.position; match self.get_uri_and_session(¶ms.text_document_position.text_document.uri) { - Ok((_, session)) => { - // TODO - // here we would also need to provide a list of builtin methods not just the ones from the document - Ok(session.completion_items().map(CompletionResponse::Array)) - } + Ok((uri, session)) => Ok(session + .completion_items(&uri, position, trigger_char) + .map(CompletionResponse::Array)), Err(err) => { tracing::error!("{}", err.to_string()); Ok(None) diff --git a/sway-lsp/src/traverse/parsed_tree.rs b/sway-lsp/src/traverse/parsed_tree.rs index 1dff767af38..4de8f9d3950 100644 --- a/sway-lsp/src/traverse/parsed_tree.rs +++ b/sway-lsp/src/traverse/parsed_tree.rs @@ -172,8 +172,16 @@ impl Parse for UseStatement { impl Parse for Expression { fn parse(&self, ctx: &ParseContext) { match &self.kind { - ExpressionKind::Error(_part_spans) => { - // FIXME(Centril): Left for @JoshuaBatty to use. + ExpressionKind::Error(part_spans) => { + for span in part_spans.iter() { + ctx.tokens.insert( + to_ident_key(&Ident::new(span.clone())), + Token::from_parsed( + AstToken::ErrorRecovery(span.clone()), + SymbolKind::Unknown, + ), + ); + } } ExpressionKind::Literal(value) => { let symbol_kind = literal_to_symbol_kind(value); diff --git a/sway-lsp/tests/fixtures/completion/.gitignore b/sway-lsp/tests/fixtures/completion/.gitignore new file mode 100644 index 00000000000..77d3844f58c --- /dev/null +++ b/sway-lsp/tests/fixtures/completion/.gitignore @@ -0,0 +1,2 @@ +out +target diff --git a/sway-lsp/tests/fixtures/completion/Forc.toml b/sway-lsp/tests/fixtures/completion/Forc.toml new file mode 100644 index 00000000000..a34ea6ca84b --- /dev/null +++ b/sway-lsp/tests/fixtures/completion/Forc.toml @@ -0,0 +1,7 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "completion" + +[dependencies] diff --git a/sway-lsp/tests/fixtures/completion/src/main.sw b/sway-lsp/tests/fixtures/completion/src/main.sw new file mode 100644 index 00000000000..31d410c5dd7 --- /dev/null +++ b/sway-lsp/tests/fixtures/completion/src/main.sw @@ -0,0 +1,21 @@ +script; + +struct MyStruct { + a: bool, +} + +impl MyStruct { + fn new() -> Self { + Self { a: true } + } + + fn get(self, foo: Self) -> Self { + foo + } +} + +fn main() { + let foo = MyStruct::new(); + let bar = MyStruct::new(); + foo. +} diff --git a/sway-lsp/tests/integration/lsp.rs b/sway-lsp/tests/integration/lsp.rs index 0df75d5ccd4..0f41650bf9b 100644 --- a/sway-lsp/tests/integration/lsp.rs +++ b/sway-lsp/tests/integration/lsp.rs @@ -7,6 +7,7 @@ use assert_json_diff::assert_json_eq; use serde_json::json; use std::{borrow::Cow, path::Path}; use sway_lsp::server::{self, Backend}; +use sway_lsp_test_utils::extract_result_array; use tower::{Service, ServiceExt}; use tower_lsp::{ jsonrpc::{Id, Request, Response}, @@ -241,16 +242,7 @@ pub(crate) async fn code_lens_request(service: &mut LspService, uri: &U }); let code_lens = build_request_with_id("textDocument/codeLens", params, 1); let response = call_request(service, code_lens.clone()).await; - let actual_results = response - .unwrap() - .unwrap() - .into_parts() - .1 - .ok() - .unwrap() - .as_array() - .unwrap() - .clone(); + let actual_results = extract_result_array(response); let expected_results = vec![ json!({ "command": { @@ -322,6 +314,63 @@ pub(crate) async fn code_lens_request(service: &mut LspService, uri: &U code_lens } +pub(crate) async fn completion_request(service: &mut LspService, uri: &Url) -> Request { + let params = json!({ + "textDocument": { + "uri": uri + }, + "position": { + "line": 19, + "character": 8 + }, + "context": { + "triggerKind": 2, + "triggerCharacter": "." + } + }); + let completion = build_request_with_id("textDocument/completion", params, 1); + let response = call_request(service, completion.clone()).await; + let actual_results = extract_result_array(response); + let expected_results = vec![ + json!({ + "kind": 5, + "label": "a", + "labelDetails": { + "description": "bool" + } + }), + json!({ + "kind": 2, + "label": "get(…)", + "labelDetails": { + "description": "fn(self, MyStruct) -> MyStruct" + }, + "textEdit": { + "newText": "get(foo)", + "range": { + "end": { + "character": 8, + "line": 19 + }, + "start": { + "character": 8, + "line": 19 + } + } + } + }), + ]; + + assert_eq!(actual_results.len(), expected_results.len()); + for expected in expected_results.iter() { + assert!( + actual_results.contains(expected), + "Expected {actual_results:?} to contain {expected:?}" + ); + } + completion +} + pub(crate) async fn definition_check<'a>( service: &mut LspService, go_to: &'a GotoDefinition<'a>, diff --git a/sway-lsp/tests/lib.rs b/sway-lsp/tests/lib.rs index 6afcf21ccdf..589715aa355 100644 --- a/sway-lsp/tests/lib.rs +++ b/sway-lsp/tests/lib.rs @@ -1571,3 +1571,8 @@ lsp_capability_test!( lsp::code_lens_request, runnables_test_dir().join("src/main.sw") ); +lsp_capability_test!( + completion, + lsp::completion_request, + test_fixtures_dir().join("completion/src/main.sw") +); diff --git a/sway-lsp/tests/utils/src/lib.rs b/sway-lsp/tests/utils/src/lib.rs index d68494366b6..8d78470001f 100644 --- a/sway-lsp/tests/utils/src/lib.rs +++ b/sway-lsp/tests/utils/src/lib.rs @@ -9,7 +9,7 @@ use std::{ time::Duration, }; use tokio::task::JoinHandle; -use tower_lsp::{lsp_types::Url, ClientSocket}; +use tower_lsp::{jsonrpc::Response, lsp_types::Url, ClientSocket, ExitedError}; pub fn load_sway_example(src_path: PathBuf) -> (Url, String) { let mut file = fs::File::open(&src_path).unwrap(); @@ -122,3 +122,16 @@ pub async fn assert_server_requests( } }) } + +pub fn extract_result_array(response: Result, ExitedError>) -> Vec { + response + .unwrap() + .unwrap() + .into_parts() + .1 + .ok() + .unwrap() + .as_array() + .unwrap() + .clone() +}