From 5a3d24192d440c5bfe3749d4bcd8ebbc9cf4902b Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Thu, 15 Aug 2024 12:15:02 -0300 Subject: [PATCH] feat: LSP signature help (#5725) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description ## Problem Resolves #1585 ## Summary While working on autocompletion I was always checking what Rust Analyzer did, or how it worked, and I noticed that in Rust Analyzer when it autocompletes a function a little popup with the parameter names and types would show up, which is really useful! But it turned out that's a separate feature: signature help. This PR implements that. I think it makes autocompletion much better. Before: ![lsp-signature-help-before](https://github.com/user-attachments/assets/2480e9bf-ae17-47a6-be0a-bcc58f2b2b92) After: ![lsp-signature-help-after](https://github.com/user-attachments/assets/e50573d0-17cb-4c3a-b50b-a4fed462acdf) Note: in the latest version the popup text starts with "fn ", but I didn't want to re-record all the gifs 😅 . You basically don't have to remember what were the parameter types. It also works with methods: ![lsp-signature-help-method](https://github.com/user-attachments/assets/72fe65ee-2c68-4463-aa75-fd304fcbe50c) And if you have an `fn`: ![lsp-signature-help-fn](https://github.com/user-attachments/assets/d29001e1-5de4-47f1-aede-da01bc1a3c53) And here it's working in an aztec project (I always check that it works outside of small toy programs 😊): ![lsp-signature-help-aztec](https://github.com/user-attachments/assets/3aa1d395-09bc-4578-b56d-c0e90630c4da) ## Additional Context I promise this will be the last big LSP PR I send 🤞 . Now that we have most of the LSP features registered, next PRs should be much smaller, just adding on top of what there is. These PRs are also relatively big because they traverse the AST and that code is kind of boilerplatey. For that reason here I decided to put the boilerplatey code in a `traversal.rs` file, so that it's clear there's no extra logic there other than traversing the AST. I might send a follow-up PR doing the same for autocompletion. One more thing: you can trigger this functionality as a VSCode command (Trigger Parameter Hints). But then when offering autocompletion you can also tell VSCode to execute a command, and in this PR we tell it to execute exactly that command (Rust Analyzer does the same, though they have a custom command for it, not sure why). UPDATE: I managed to implement the visitor pattern to reduce the boilerplate. [Here's a PR](https://github.com/noir-lang/noir/pull/5727) with that. I think it will greatly simplify things, and make it easier to keep the code updated as we add more nodes. It takes inspiration in [how it's done in the Crystal compiler](https://github.com/crystal-lang/crystal/blob/master/src/compiler/crystal/syntax/visitor.cr), which in turn is inspired by how it was done in the Java editor support for Eclipse 😄 ## Documentation\* Check one: - [x] No documentation needed. - [ ] Documentation included in this PR. - [ ] **[For Experimental Features]** Documentation to be submitted in a separate PR. # PR Checklist\* - [x] I have tested the changes locally. - [x] I have formatted the changes with [Prettier](https://prettier.io/) and/or `cargo fmt` on default settings. --------- Co-authored-by: jfecher --- tooling/lsp/src/lib.rs | 5 +- .../requests/completion/completion_items.rs | 24 +- tooling/lsp/src/requests/completion/tests.rs | 79 +++-- tooling/lsp/src/requests/mod.rs | 13 +- tooling/lsp/src/requests/signature_help.rs | 291 +++++++++++++++++ .../lsp/src/requests/signature_help/tests.rs | 196 +++++++++++ .../src/requests/signature_help/traversal.rs | 308 ++++++++++++++++++ tooling/lsp/src/types.rs | 6 +- 8 files changed, 885 insertions(+), 37 deletions(-) create mode 100644 tooling/lsp/src/requests/signature_help.rs create mode 100644 tooling/lsp/src/requests/signature_help/tests.rs create mode 100644 tooling/lsp/src/requests/signature_help/traversal.rs diff --git a/tooling/lsp/src/lib.rs b/tooling/lsp/src/lib.rs index ca34d7686fd..796137a8e9f 100644 --- a/tooling/lsp/src/lib.rs +++ b/tooling/lsp/src/lib.rs @@ -23,7 +23,7 @@ use fxhash::FxHashSet; use lsp_types::{ request::{ Completion, DocumentSymbolRequest, HoverRequest, InlayHintRequest, PrepareRenameRequest, - References, Rename, + References, Rename, SignatureHelpRequest, }, CodeLens, }; @@ -55,7 +55,7 @@ use requests::{ on_goto_declaration_request, on_goto_definition_request, on_goto_type_definition_request, on_hover_request, on_initialize, on_inlay_hint_request, on_prepare_rename_request, on_profile_run_request, on_references_request, on_rename_request, on_shutdown, - on_test_run_request, on_tests_request, LspInitializationOptions, + on_signature_help_request, on_test_run_request, on_tests_request, LspInitializationOptions, }; use serde_json::Value as JsonValue; use thiserror::Error; @@ -143,6 +143,7 @@ impl NargoLspService { .request::(on_hover_request) .request::(on_inlay_hint_request) .request::(on_completion_request) + .request::(on_signature_help_request) .notification::(on_initialized) .notification::(on_did_change_configuration) .notification::(on_did_open_text_document) diff --git a/tooling/lsp/src/requests/completion/completion_items.rs b/tooling/lsp/src/requests/completion/completion_items.rs index db08dc9054a..a5848bf2a72 100644 --- a/tooling/lsp/src/requests/completion/completion_items.rs +++ b/tooling/lsp/src/requests/completion/completion_items.rs @@ -1,4 +1,6 @@ -use lsp_types::{CompletionItem, CompletionItemKind, CompletionItemLabelDetails, InsertTextFormat}; +use lsp_types::{ + Command, CompletionItem, CompletionItemKind, CompletionItemLabelDetails, InsertTextFormat, +}; use noirc_frontend::{ hir_def::{function::FuncMeta, stmt::HirPattern}, macros_api::{ModuleDefId, StructId}, @@ -164,6 +166,13 @@ impl<'a> NodeFinder<'a> { completion_item }; + let completion_item = match function_completion_kind { + FunctionCompletionKind::Name => completion_item, + FunctionCompletionKind::NameAndParameters => { + completion_item_with_trigger_parameter_hints_command(completion_item) + } + }; + Some(completion_item) } @@ -342,3 +351,16 @@ pub(super) fn completion_item_with_sort_text( ) -> CompletionItem { CompletionItem { sort_text: Some(sort_text), ..completion_item } } + +pub(super) fn completion_item_with_trigger_parameter_hints_command( + completion_item: CompletionItem, +) -> CompletionItem { + CompletionItem { + command: Some(Command { + title: "Trigger parameter hints".to_string(), + command: "editor.action.triggerParameterHints".to_string(), + arguments: None, + }), + ..completion_item + } +} diff --git a/tooling/lsp/src/requests/completion/tests.rs b/tooling/lsp/src/requests/completion/tests.rs index 1c89cc09413..42632a2f445 100644 --- a/tooling/lsp/src/requests/completion/tests.rs +++ b/tooling/lsp/src/requests/completion/tests.rs @@ -5,8 +5,9 @@ mod completion_tests { requests::{ completion::{ completion_items::{ - completion_item_with_sort_text, crate_completion_item, module_completion_item, - simple_completion_item, snippet_completion_item, + completion_item_with_sort_text, + completion_item_with_trigger_parameter_hints_command, crate_completion_item, + module_completion_item, simple_completion_item, snippet_completion_item, }, sort_text::self_mismatch_sort_text, }, @@ -90,6 +91,20 @@ mod completion_tests { assert_eq!(items, expected); } + pub(super) fn function_completion_item( + label: impl Into, + kind: CompletionItemKind, + insert_text: impl Into, + description: impl Into, + ) -> CompletionItem { + completion_item_with_trigger_parameter_hints_command(snippet_completion_item( + label, + kind, + insert_text, + Some(description.into()), + )) + } + #[test] async fn test_use_first_segment() { let src = r#" @@ -408,11 +423,11 @@ mod completion_tests { "#; assert_completion( src, - vec![snippet_completion_item( + vec![function_completion_item( "hello()", CompletionItemKind::FUNCTION, "hello()", - Some("fn()".to_string()), + "fn()", )], ) .await; @@ -429,11 +444,11 @@ mod completion_tests { "#; assert_completion( src, - vec![snippet_completion_item( + vec![function_completion_item( "hello(…)", CompletionItemKind::FUNCTION, "hello(${1:x}, ${2:y})", - Some("fn(i32, Field)".to_string()), + "fn(i32, Field)".to_string(), )], ) .await; @@ -455,11 +470,11 @@ mod completion_tests { "assert(${1:predicate})", Some("fn(T)".to_string()), ), - snippet_completion_item( + function_completion_item( "assert_constant(…)", CompletionItemKind::FUNCTION, "assert_constant(${1:x})", - Some("fn(T)".to_string()), + "fn(T)", ), snippet_completion_item( "assert_eq(…)", @@ -1063,17 +1078,17 @@ mod completion_tests { assert_completion( src, vec![ - snippet_completion_item( + function_completion_item( "foobar(…)", CompletionItemKind::FUNCTION, "foobar(${1:x})", - Some("fn(self, i32)".to_string()), + "fn(self, i32)".to_string(), ), - snippet_completion_item( + function_completion_item( "foobar2(…)", CompletionItemKind::FUNCTION, "foobar2(${1:x})", - Some("fn(&mut self, i32)".to_string()), + "fn(&mut self, i32)".to_string(), ), ], ) @@ -1102,11 +1117,11 @@ mod completion_tests { "#; assert_completion( src, - vec![snippet_completion_item( + vec![function_completion_item( "foobar(…)", CompletionItemKind::FUNCTION, "foobar(${1:x})", - Some("fn(self, i32)".to_string()), + "fn(self, i32)", )], ) .await; @@ -1131,11 +1146,11 @@ mod completion_tests { "#; assert_completion( src, - vec![snippet_completion_item( + vec![function_completion_item( "foobar(…)", CompletionItemKind::FUNCTION, "foobar(${1:x})", - Some("fn(self, i32)".to_string()), + "fn(self, i32)".to_string(), )], ) .await; @@ -1161,28 +1176,28 @@ mod completion_tests { src, vec![ completion_item_with_sort_text( - snippet_completion_item( + function_completion_item( "foobar(…)", CompletionItemKind::FUNCTION, "foobar(${1:self}, ${2:x})", - Some("fn(self, i32)".to_string()), + "fn(self, i32)", ), self_mismatch_sort_text(), ), completion_item_with_sort_text( - snippet_completion_item( + function_completion_item( "foobar2(…)", CompletionItemKind::FUNCTION, "foobar2(${1:self}, ${2:x})", - Some("fn(&mut self, i32)".to_string()), + "fn(&mut self, i32)", ), self_mismatch_sort_text(), ), - snippet_completion_item( + function_completion_item( "foobar3(…)", CompletionItemKind::FUNCTION, "foobar3(${1:y})", - Some("fn(i32)".to_string()), + "fn(i32)", ), ], ) @@ -1207,11 +1222,11 @@ mod completion_tests { "#; assert_completion( src, - vec![snippet_completion_item( + vec![function_completion_item( "foobar(…)", CompletionItemKind::FUNCTION, "foobar(${1:x})", - Some("fn(self, i32)".to_string()), + "fn(self, i32)".to_string(), )], ) .await; @@ -1239,28 +1254,28 @@ mod completion_tests { src, vec![ completion_item_with_sort_text( - snippet_completion_item( + function_completion_item( "foobar(…)", CompletionItemKind::FUNCTION, "foobar(${1:self}, ${2:x})", - Some("fn(self, i32)".to_string()), + "fn(self, i32)", ), self_mismatch_sort_text(), ), completion_item_with_sort_text( - snippet_completion_item( + function_completion_item( "foobar2(…)", CompletionItemKind::FUNCTION, "foobar2(${1:self}, ${2:x})", - Some("fn(&mut self, i32)".to_string()), + "fn(&mut self, i32)", ), self_mismatch_sort_text(), ), - snippet_completion_item( + function_completion_item( "foobar3(…)", CompletionItemKind::FUNCTION, "foobar3(${1:y})", - Some("fn(i32)".to_string()), + "fn(i32)", ), ], ) @@ -1317,11 +1332,11 @@ mod completion_tests { "#; assert_completion( src, - vec![snippet_completion_item( + vec![function_completion_item( "foo()", CompletionItemKind::FUNCTION, "foo()", - Some("fn(self) -> Foo".to_string()), + "fn(self) -> Foo".to_string(), )], ) .await; diff --git a/tooling/lsp/src/requests/mod.rs b/tooling/lsp/src/requests/mod.rs index b6c26110e59..e88c7f11465 100644 --- a/tooling/lsp/src/requests/mod.rs +++ b/tooling/lsp/src/requests/mod.rs @@ -46,6 +46,7 @@ mod inlay_hint; mod profile_run; mod references; mod rename; +mod signature_help; mod test_run; mod tests; @@ -56,7 +57,8 @@ pub(crate) use { goto_definition::on_goto_type_definition_request, hover::on_hover_request, inlay_hint::on_inlay_hint_request, profile_run::on_profile_run_request, references::on_references_request, rename::on_prepare_rename_request, - rename::on_rename_request, test_run::on_test_run_request, tests::on_tests_request, + rename::on_rename_request, signature_help::on_signature_help_request, + test_run::on_test_run_request, tests::on_tests_request, }; /// LSP client will send initialization request after the server has started. @@ -241,6 +243,15 @@ pub(crate) fn on_initialize( }, completion_item: None, })), + signature_help_provider: Some(lsp_types::OneOf::Right( + lsp_types::SignatureHelpOptions { + trigger_characters: Some(vec!["(".to_string(), ",".to_string()]), + retrigger_characters: None, + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: None, + }, + }, + )), }, server_info: None, }) diff --git a/tooling/lsp/src/requests/signature_help.rs b/tooling/lsp/src/requests/signature_help.rs new file mode 100644 index 00000000000..c2c69185547 --- /dev/null +++ b/tooling/lsp/src/requests/signature_help.rs @@ -0,0 +1,291 @@ +use std::future::{self, Future}; + +use async_lsp::ResponseError; +use fm::{FileId, PathString}; +use lsp_types::{ + ParameterInformation, ParameterLabel, SignatureHelp, SignatureHelpParams, SignatureInformation, +}; +use noirc_errors::{Location, Span}; +use noirc_frontend::{ + ast::{CallExpression, Expression, FunctionReturnType, MethodCallExpression}, + hir_def::{function::FuncMeta, stmt::HirPattern}, + macros_api::NodeInterner, + node_interner::ReferenceId, + ParsedModule, Type, +}; + +use crate::{utils, LspState}; + +use super::process_request; + +mod tests; +mod traversal; + +pub(crate) fn on_signature_help_request( + state: &mut LspState, + params: SignatureHelpParams, +) -> impl Future, ResponseError>> { + let uri = params.text_document_position_params.clone().text_document.uri; + + let result = process_request(state, params.text_document_position_params.clone(), |args| { + let path = PathString::from_path(uri.to_file_path().unwrap()); + args.files.get_file_id(&path).and_then(|file_id| { + utils::position_to_byte_index( + args.files, + file_id, + ¶ms.text_document_position_params.position, + ) + .and_then(|byte_index| { + let file = args.files.get_file(file_id).unwrap(); + let source = file.source(); + let (parsed_module, _errors) = noirc_frontend::parse_program(source); + + let mut finder = SignatureFinder::new(file_id, byte_index, args.interner); + finder.find(&parsed_module) + }) + }) + }); + future::ready(result) +} + +struct SignatureFinder<'a> { + file: FileId, + byte_index: usize, + interner: &'a NodeInterner, + signature_help: Option, +} + +impl<'a> SignatureFinder<'a> { + fn new(file: FileId, byte_index: usize, interner: &'a NodeInterner) -> Self { + Self { file, byte_index, interner, signature_help: None } + } + + fn find(&mut self, parsed_module: &ParsedModule) -> Option { + self.find_in_parsed_module(parsed_module); + + self.signature_help.clone() + } + + fn find_in_call_expression(&mut self, call_expression: &CallExpression, span: Span) { + self.find_in_expression(&call_expression.func); + self.find_in_expressions(&call_expression.arguments); + + let arguments_span = Span::from(call_expression.func.span.end() + 1..span.end() - 1); + let span = call_expression.func.span; + let name_span = Span::from(span.end() - 1..span.end()); + let has_self = false; + + self.try_compute_signature_help( + &call_expression.arguments, + arguments_span, + name_span, + has_self, + ); + } + + fn find_in_method_call_expression( + &mut self, + method_call_expression: &MethodCallExpression, + span: Span, + ) { + self.find_in_expression(&method_call_expression.object); + self.find_in_expressions(&method_call_expression.arguments); + + let arguments_span = + Span::from(method_call_expression.method_name.span().end() + 1..span.end() - 1); + let name_span = method_call_expression.method_name.span(); + let has_self = true; + + self.try_compute_signature_help( + &method_call_expression.arguments, + arguments_span, + name_span, + has_self, + ); + } + + fn try_compute_signature_help( + &mut self, + arguments: &[Expression], + arguments_span: Span, + name_span: Span, + has_self: bool, + ) { + if self.signature_help.is_some() { + return; + } + + if !self.includes_span(arguments_span) { + return; + } + + let mut active_parameter = None; + for (index, arg) in arguments.iter().enumerate() { + if self.includes_span(arg.span) || arg.span.start() as usize >= self.byte_index { + active_parameter = Some(index as u32); + break; + } + } + + if active_parameter.is_none() { + active_parameter = Some(arguments.len() as u32); + } + + let location = Location::new(name_span, self.file); + + // Check if the call references a named function + if let Some(ReferenceId::Function(func_id)) = self.interner.find_referenced(location) { + let name = self.interner.function_name(&func_id); + let func_meta = self.interner.function_meta(&func_id); + + let signature_information = + self.func_meta_signature_information(func_meta, name, active_parameter, has_self); + self.set_signature_help(signature_information); + return; + } + + // Otherwise, the call must be a reference to an fn type + if let Some(mut typ) = self.interner.type_at_location(location) { + if let Type::Forall(_, forall_typ) = typ { + typ = *forall_typ; + } + if let Type::Function(args, return_type, _, unconstrained) = typ { + let signature_information = self.function_type_signature_information( + &args, + &return_type, + unconstrained, + active_parameter, + ); + self.set_signature_help(signature_information); + } + } + } + + fn func_meta_signature_information( + &self, + func_meta: &FuncMeta, + name: &str, + active_parameter: Option, + has_self: bool, + ) -> SignatureInformation { + let mut label = String::new(); + let mut parameters = Vec::new(); + + label.push_str("fn "); + label.push_str(name); + label.push('('); + for (index, (pattern, typ, _)) in func_meta.parameters.0.iter().enumerate() { + if index > 0 { + label.push_str(", "); + } + + if has_self && index == 0 { + if let Type::MutableReference(..) = typ { + label.push_str("&mut self"); + } else { + label.push_str("self"); + } + } else { + let parameter_start = label.chars().count(); + + self.hir_pattern_to_argument(pattern, &mut label); + label.push_str(": "); + label.push_str(&typ.to_string()); + + let parameter_end = label.chars().count(); + + parameters.push(ParameterInformation { + label: ParameterLabel::LabelOffsets([ + parameter_start as u32, + parameter_end as u32, + ]), + documentation: None, + }); + } + } + label.push(')'); + + match &func_meta.return_type { + FunctionReturnType::Default(_) => (), + FunctionReturnType::Ty(typ) => { + label.push_str(" -> "); + label.push_str(&typ.to_string()); + } + } + + SignatureInformation { + label, + documentation: None, + parameters: Some(parameters), + active_parameter, + } + } + + fn function_type_signature_information( + &self, + args: &[Type], + return_type: &Type, + unconstrained: bool, + active_parameter: Option, + ) -> SignatureInformation { + let mut label = String::new(); + let mut parameters = Vec::new(); + + if unconstrained { + label.push_str("unconstrained "); + } + label.push_str("fn("); + for (index, typ) in args.iter().enumerate() { + if index > 0 { + label.push_str(", "); + } + + let parameter_start = label.chars().count(); + label.push_str(&typ.to_string()); + let parameter_end = label.chars().count(); + + parameters.push(ParameterInformation { + label: ParameterLabel::LabelOffsets([parameter_start as u32, parameter_end as u32]), + documentation: None, + }); + } + label.push(')'); + + if let Type::Unit = return_type { + // Nothing + } else { + label.push_str(" -> "); + label.push_str(&return_type.to_string()); + } + + SignatureInformation { + label, + documentation: None, + parameters: Some(parameters), + active_parameter, + } + } + + fn hir_pattern_to_argument(&self, pattern: &HirPattern, text: &mut String) { + match pattern { + HirPattern::Identifier(hir_ident) => { + text.push_str(self.interner.definition_name(hir_ident.id)); + } + HirPattern::Mutable(pattern, _) => self.hir_pattern_to_argument(pattern, text), + HirPattern::Tuple(_, _) | HirPattern::Struct(_, _, _) => text.push('_'), + } + } + + fn set_signature_help(&mut self, signature_information: SignatureInformation) { + let signature_help = SignatureHelp { + active_parameter: signature_information.active_parameter, + signatures: vec![signature_information], + active_signature: Some(0), + }; + self.signature_help = Some(signature_help); + } + + fn includes_span(&self, span: Span) -> bool { + span.start() as usize <= self.byte_index && self.byte_index <= span.end() as usize + } +} diff --git a/tooling/lsp/src/requests/signature_help/tests.rs b/tooling/lsp/src/requests/signature_help/tests.rs new file mode 100644 index 00000000000..c48ee159084 --- /dev/null +++ b/tooling/lsp/src/requests/signature_help/tests.rs @@ -0,0 +1,196 @@ +#[cfg(test)] +mod signature_help_tests { + use crate::{ + notifications::on_did_open_text_document, requests::on_signature_help_request, test_utils, + }; + + use lsp_types::{ + DidOpenTextDocumentParams, ParameterLabel, Position, SignatureHelp, SignatureHelpParams, + TextDocumentIdentifier, TextDocumentItem, TextDocumentPositionParams, + WorkDoneProgressParams, + }; + use tokio::test; + + async fn get_signature_help(src: &str) -> SignatureHelp { + let (mut state, noir_text_document) = test_utils::init_lsp_server("document_symbol").await; + + let (line, column) = src + .lines() + .enumerate() + .find_map(|(line_index, line)| { + line.find(">|<").map(|char_index| (line_index, char_index)) + }) + .expect("Expected to find one >|< in the source code"); + + let src = src.replace(">|<", ""); + + on_did_open_text_document( + &mut state, + DidOpenTextDocumentParams { + text_document: TextDocumentItem { + uri: noir_text_document.clone(), + language_id: "noir".to_string(), + version: 0, + text: src.to_string(), + }, + }, + ); + + on_signature_help_request( + &mut state, + SignatureHelpParams { + context: None, + text_document_position_params: TextDocumentPositionParams { + text_document: TextDocumentIdentifier { uri: noir_text_document }, + position: Position { line: line as u32, character: column as u32 }, + }, + work_done_progress_params: WorkDoneProgressParams { work_done_token: None }, + }, + ) + .await + .expect("Could not execute on_signature_help_request") + .unwrap() + } + + fn check_label(signature_label: &str, parameter_label: &ParameterLabel, expected_string: &str) { + let ParameterLabel::LabelOffsets(offsets) = parameter_label else { + panic!("Expected label to be LabelOffsets, got {:?}", parameter_label); + }; + + assert_eq!(&signature_label[offsets[0] as usize..offsets[1] as usize], expected_string); + } + + #[test] + async fn test_signature_help_for_call_at_first_argument() { + let src = r#" + fn foo(x: i32, y: Field) -> u32 { 0 } + fn wrapper(x: u32) {} + + fn bar() { + wrapper(foo(>|<1, 2)); + } + "#; + + let signature_help = get_signature_help(src).await; + assert_eq!(signature_help.signatures.len(), 1); + + let signature = &signature_help.signatures[0]; + assert_eq!(signature.label, "fn foo(x: i32, y: Field) -> u32"); + + let params = signature.parameters.as_ref().unwrap(); + assert_eq!(params.len(), 2); + + check_label(&signature.label, ¶ms[0].label, "x: i32"); + check_label(&signature.label, ¶ms[1].label, "y: Field"); + + assert_eq!(signature.active_parameter, Some(0)); + } + + #[test] + async fn test_signature_help_for_call_between_arguments() { + let src = r#" + fn foo(x: i32, y: Field) -> u32 { 0 } + + fn bar() { + foo(1,>|< 2); + } + "#; + + let signature_help = get_signature_help(src).await; + assert_eq!(signature_help.signatures.len(), 1); + + let signature = &signature_help.signatures[0]; + assert_eq!(signature.active_parameter, Some(1)); + } + + #[test] + async fn test_signature_help_for_call_at_second_argument() { + let src = r#" + fn foo(x: i32, y: Field) -> u32 { 0 } + + fn bar() { + foo(1, >|<2); + } + "#; + + let signature_help = get_signature_help(src).await; + assert_eq!(signature_help.signatures.len(), 1); + + let signature = &signature_help.signatures[0]; + assert_eq!(signature.active_parameter, Some(1)); + } + + #[test] + async fn test_signature_help_for_call_past_last_argument() { + let src = r#" + fn foo(x: i32, y: Field) -> u32 { 0 } + + fn bar() { + foo(1, 2, >|<); + } + "#; + + let signature_help = get_signature_help(src).await; + assert_eq!(signature_help.signatures.len(), 1); + + let signature = &signature_help.signatures[0]; + assert_eq!(signature.active_parameter, Some(2)); + } + + #[test] + async fn test_signature_help_for_method_call() { + let src = r#" + struct Foo {} + + impl Foo { + fn foo(self, x: i32, y: Field) -> u32 { 0 } + } + + fn wrapper(x: u32) {} + + fn bar(f: Foo) { + wrapper(f.foo(>|<1, 2)); + } + "#; + + let signature_help = get_signature_help(src).await; + assert_eq!(signature_help.signatures.len(), 1); + + let signature = &signature_help.signatures[0]; + assert_eq!(signature.label, "fn foo(self, x: i32, y: Field) -> u32"); + + let params = signature.parameters.as_ref().unwrap(); + assert_eq!(params.len(), 2); + + check_label(&signature.label, ¶ms[0].label, "x: i32"); + check_label(&signature.label, ¶ms[1].label, "y: Field"); + + assert_eq!(signature.active_parameter, Some(0)); + } + + #[test] + async fn test_signature_help_for_fn_call() { + let src = r#" + fn foo(x: i32, y: Field) -> u32 { 0 } + + fn bar() { + let f = foo; + f(>|<1, 2); + } + "#; + + let signature_help = get_signature_help(src).await; + assert_eq!(signature_help.signatures.len(), 1); + + let signature = &signature_help.signatures[0]; + assert_eq!(signature.label, "fn(i32, Field) -> u32"); + + let params = signature.parameters.as_ref().unwrap(); + assert_eq!(params.len(), 2); + + check_label(&signature.label, ¶ms[0].label, "i32"); + check_label(&signature.label, ¶ms[1].label, "Field"); + + assert_eq!(signature.active_parameter, Some(0)); + } +} diff --git a/tooling/lsp/src/requests/signature_help/traversal.rs b/tooling/lsp/src/requests/signature_help/traversal.rs new file mode 100644 index 00000000000..ecb3bf46487 --- /dev/null +++ b/tooling/lsp/src/requests/signature_help/traversal.rs @@ -0,0 +1,308 @@ +/// This file includes the signature help logic that's just about +/// traversing the AST without any additional logic. +use super::SignatureFinder; + +use noirc_frontend::{ + ast::{ + ArrayLiteral, BlockExpression, CastExpression, ConstrainStatement, ConstructorExpression, + Expression, ForLoopStatement, ForRange, IfExpression, IndexExpression, InfixExpression, + LValue, Lambda, LetStatement, Literal, MemberAccessExpression, NoirFunction, NoirTrait, + NoirTraitImpl, Statement, TraitImplItem, TraitItem, TypeImpl, + }, + parser::{Item, ItemKind}, + ParsedModule, +}; + +impl<'a> SignatureFinder<'a> { + pub(super) fn find_in_parsed_module(&mut self, parsed_module: &ParsedModule) { + for item in &parsed_module.items { + self.find_in_item(item); + } + } + + pub(super) fn find_in_item(&mut self, item: &Item) { + if !self.includes_span(item.span) { + return; + } + + match &item.kind { + ItemKind::Submodules(parsed_sub_module) => { + self.find_in_parsed_module(&parsed_sub_module.contents); + } + ItemKind::Function(noir_function) => self.find_in_noir_function(noir_function), + ItemKind::TraitImpl(noir_trait_impl) => self.find_in_noir_trait_impl(noir_trait_impl), + ItemKind::Impl(type_impl) => self.find_in_type_impl(type_impl), + ItemKind::Global(let_statement) => self.find_in_let_statement(let_statement), + ItemKind::Trait(noir_trait) => self.find_in_noir_trait(noir_trait), + ItemKind::Import(..) + | ItemKind::TypeAlias(_) + | ItemKind::Struct(_) + | ItemKind::ModuleDecl(_) => (), + } + } + + pub(super) fn find_in_noir_function(&mut self, noir_function: &NoirFunction) { + self.find_in_block_expression(&noir_function.def.body); + } + + pub(super) fn find_in_noir_trait_impl(&mut self, noir_trait_impl: &NoirTraitImpl) { + for item in &noir_trait_impl.items { + self.find_in_trait_impl_item(item); + } + } + + pub(super) fn find_in_trait_impl_item(&mut self, item: &TraitImplItem) { + match item { + TraitImplItem::Function(noir_function) => self.find_in_noir_function(noir_function), + TraitImplItem::Constant(_, _, _) => (), + TraitImplItem::Type { .. } => (), + } + } + + pub(super) fn find_in_type_impl(&mut self, type_impl: &TypeImpl) { + for (method, span) in &type_impl.methods { + if self.includes_span(*span) { + self.find_in_noir_function(method); + } + } + } + + pub(super) fn find_in_noir_trait(&mut self, noir_trait: &NoirTrait) { + for item in &noir_trait.items { + self.find_in_trait_item(item); + } + } + + pub(super) fn find_in_trait_item(&mut self, trait_item: &TraitItem) { + match trait_item { + TraitItem::Function { body, .. } => { + if let Some(body) = body { + self.find_in_block_expression(body); + }; + } + TraitItem::Constant { default_value, .. } => { + if let Some(default_value) = default_value { + self.find_in_expression(default_value); + } + } + TraitItem::Type { .. } => (), + } + } + + pub(super) fn find_in_block_expression(&mut self, block_expression: &BlockExpression) { + for statement in &block_expression.statements { + if self.includes_span(statement.span) { + self.find_in_statement(statement); + } + } + } + + pub(super) fn find_in_statement(&mut self, statement: &Statement) { + if !self.includes_span(statement.span) { + return; + } + + match &statement.kind { + noirc_frontend::ast::StatementKind::Let(let_statement) => { + self.find_in_let_statement(let_statement); + } + noirc_frontend::ast::StatementKind::Constrain(constrain_statement) => { + self.find_in_constrain_statement(constrain_statement); + } + noirc_frontend::ast::StatementKind::Expression(expression) => { + self.find_in_expression(expression); + } + noirc_frontend::ast::StatementKind::Assign(assign_statement) => { + self.find_in_assign_statement(assign_statement); + } + noirc_frontend::ast::StatementKind::For(for_loop_statement) => { + self.find_in_for_loop_statement(for_loop_statement); + } + noirc_frontend::ast::StatementKind::Comptime(statement) => { + self.find_in_statement(statement); + } + noirc_frontend::ast::StatementKind::Semi(expression) => { + self.find_in_expression(expression); + } + noirc_frontend::ast::StatementKind::Break + | noirc_frontend::ast::StatementKind::Continue + | noirc_frontend::ast::StatementKind::Error => (), + } + } + + pub(super) fn find_in_let_statement(&mut self, let_statement: &LetStatement) { + self.find_in_expression(&let_statement.expression); + } + + pub(super) fn find_in_constrain_statement(&mut self, constrain_statement: &ConstrainStatement) { + self.find_in_expression(&constrain_statement.0); + + if let Some(exp) = &constrain_statement.1 { + self.find_in_expression(exp); + } + } + + pub(super) fn find_in_assign_statement( + &mut self, + assign_statement: &noirc_frontend::ast::AssignStatement, + ) { + self.find_in_lvalue(&assign_statement.lvalue); + self.find_in_expression(&assign_statement.expression); + } + + pub(super) fn find_in_for_loop_statement(&mut self, for_loop_statement: &ForLoopStatement) { + self.find_in_for_range(&for_loop_statement.range); + self.find_in_expression(&for_loop_statement.block); + } + + pub(super) fn find_in_lvalue(&mut self, lvalue: &LValue) { + match lvalue { + LValue::Ident(_) => (), + LValue::MemberAccess { object, field_name: _, span: _ } => self.find_in_lvalue(object), + LValue::Index { array, index, span: _ } => { + self.find_in_lvalue(array); + self.find_in_expression(index); + } + LValue::Dereference(lvalue, _) => self.find_in_lvalue(lvalue), + } + } + + pub(super) fn find_in_for_range(&mut self, for_range: &ForRange) { + match for_range { + ForRange::Range(start, end) => { + self.find_in_expression(start); + self.find_in_expression(end); + } + ForRange::Array(expression) => self.find_in_expression(expression), + } + } + + pub(super) fn find_in_expressions(&mut self, expressions: &[Expression]) { + for expression in expressions { + self.find_in_expression(expression); + } + } + + pub(super) fn find_in_expression(&mut self, expression: &Expression) { + match &expression.kind { + noirc_frontend::ast::ExpressionKind::Literal(literal) => self.find_in_literal(literal), + noirc_frontend::ast::ExpressionKind::Block(block_expression) => { + self.find_in_block_expression(block_expression); + } + noirc_frontend::ast::ExpressionKind::Prefix(prefix_expression) => { + self.find_in_expression(&prefix_expression.rhs); + } + noirc_frontend::ast::ExpressionKind::Index(index_expression) => { + self.find_in_index_expression(index_expression); + } + noirc_frontend::ast::ExpressionKind::Call(call_expression) => { + self.find_in_call_expression(call_expression, expression.span); + } + noirc_frontend::ast::ExpressionKind::MethodCall(method_call_expression) => { + self.find_in_method_call_expression(method_call_expression, expression.span); + } + noirc_frontend::ast::ExpressionKind::Constructor(constructor_expression) => { + self.find_in_constructor_expression(constructor_expression); + } + noirc_frontend::ast::ExpressionKind::MemberAccess(member_access_expression) => { + self.find_in_member_access_expression(member_access_expression); + } + noirc_frontend::ast::ExpressionKind::Cast(cast_expression) => { + self.find_in_cast_expression(cast_expression); + } + noirc_frontend::ast::ExpressionKind::Infix(infix_expression) => { + self.find_in_infix_expression(infix_expression); + } + noirc_frontend::ast::ExpressionKind::If(if_expression) => { + self.find_in_if_expression(if_expression); + } + noirc_frontend::ast::ExpressionKind::Tuple(expressions) => { + self.find_in_expressions(expressions); + } + noirc_frontend::ast::ExpressionKind::Lambda(lambda) => self.find_in_lambda(lambda), + noirc_frontend::ast::ExpressionKind::Parenthesized(expression) => { + self.find_in_expression(expression); + } + noirc_frontend::ast::ExpressionKind::Unquote(expression) => { + self.find_in_expression(expression); + } + noirc_frontend::ast::ExpressionKind::Comptime(block_expression, _) => { + self.find_in_block_expression(block_expression); + } + noirc_frontend::ast::ExpressionKind::Unsafe(block_expression, _) => { + self.find_in_block_expression(block_expression); + } + noirc_frontend::ast::ExpressionKind::Variable(_) + | noirc_frontend::ast::ExpressionKind::AsTraitPath(_) + | noirc_frontend::ast::ExpressionKind::Quote(_) + | noirc_frontend::ast::ExpressionKind::Resolved(_) + | noirc_frontend::ast::ExpressionKind::Error => (), + } + } + + pub(super) fn find_in_literal(&mut self, literal: &Literal) { + match literal { + Literal::Array(array_literal) => self.find_in_array_literal(array_literal), + Literal::Slice(array_literal) => self.find_in_array_literal(array_literal), + Literal::Bool(_) + | Literal::Integer(_, _) + | Literal::Str(_) + | Literal::RawStr(_, _) + | Literal::FmtStr(_) + | Literal::Unit => (), + } + } + + pub(super) fn find_in_array_literal(&mut self, array_literal: &ArrayLiteral) { + match array_literal { + ArrayLiteral::Standard(expressions) => self.find_in_expressions(expressions), + ArrayLiteral::Repeated { repeated_element, length } => { + self.find_in_expression(repeated_element); + self.find_in_expression(length); + } + } + } + + pub(super) fn find_in_index_expression(&mut self, index_expression: &IndexExpression) { + self.find_in_expression(&index_expression.collection); + self.find_in_expression(&index_expression.index); + } + + pub(super) fn find_in_constructor_expression( + &mut self, + constructor_expression: &ConstructorExpression, + ) { + for (_field_name, expression) in &constructor_expression.fields { + self.find_in_expression(expression); + } + } + + pub(super) fn find_in_member_access_expression( + &mut self, + member_access_expression: &MemberAccessExpression, + ) { + self.find_in_expression(&member_access_expression.lhs); + } + + pub(super) fn find_in_cast_expression(&mut self, cast_expression: &CastExpression) { + self.find_in_expression(&cast_expression.lhs); + } + + pub(super) fn find_in_infix_expression(&mut self, infix_expression: &InfixExpression) { + self.find_in_expression(&infix_expression.lhs); + self.find_in_expression(&infix_expression.rhs); + } + + pub(super) fn find_in_if_expression(&mut self, if_expression: &IfExpression) { + self.find_in_expression(&if_expression.condition); + self.find_in_expression(&if_expression.consequence); + + if let Some(alternative) = &if_expression.alternative { + self.find_in_expression(alternative); + } + } + + pub(super) fn find_in_lambda(&mut self, lambda: &Lambda) { + self.find_in_expression(&lambda.body); + } +} diff --git a/tooling/lsp/src/types.rs b/tooling/lsp/src/types.rs index 5afda0d292a..3ac1f35e18e 100644 --- a/tooling/lsp/src/types.rs +++ b/tooling/lsp/src/types.rs @@ -1,7 +1,7 @@ use fm::FileId; use lsp_types::{ CompletionOptions, DeclarationCapability, DefinitionOptions, DocumentSymbolOptions, - HoverOptions, InlayHintOptions, OneOf, ReferencesOptions, RenameOptions, + HoverOptions, InlayHintOptions, OneOf, ReferencesOptions, RenameOptions, SignatureHelpOptions, TypeDefinitionProviderCapability, }; use noirc_driver::DebugFile; @@ -161,6 +161,10 @@ pub(crate) struct ServerCapabilities { /// The server provides completion support. #[serde(skip_serializing_if = "Option::is_none")] pub(crate) completion_provider: Option>, + + /// The server provides signature help support. + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) signature_help_provider: Option>, } #[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)]