diff --git a/compiler/noirc_frontend/src/hir/def_map/module_data.rs b/compiler/noirc_frontend/src/hir/def_map/module_data.rs index 488ccc476d7..22875ffe18a 100644 --- a/compiler/noirc_frontend/src/hir/def_map/module_data.rs +++ b/compiler/noirc_frontend/src/hir/def_map/module_data.rs @@ -42,6 +42,10 @@ impl ModuleData { &self.scope } + pub fn definitions(&self) -> &ItemScope { + &self.definitions + } + fn declare( &mut self, name: Ident, diff --git a/compiler/noirc_frontend/src/parser/errors.rs b/compiler/noirc_frontend/src/parser/errors.rs index 390afbefcda..36d3ce5898c 100644 --- a/compiler/noirc_frontend/src/parser/errors.rs +++ b/compiler/noirc_frontend/src/parser/errors.rs @@ -16,6 +16,8 @@ pub enum ParserErrorReason { ExpectedFieldName(Token), #[error("expected a pattern but found a type - {0}")] ExpectedPatternButFoundType(Token), + #[error("expected an identifier after ::")] + ExpectedIdentifierAfterColons, #[error("Expected a ; separating these two statements")] MissingSeparatingSemi, #[error("constrain keyword is deprecated")] diff --git a/compiler/noirc_frontend/src/parser/parser/path.rs b/compiler/noirc_frontend/src/parser/parser/path.rs index d0f7d66b270..ae3a1bc0b93 100644 --- a/compiler/noirc_frontend/src/parser/parser/path.rs +++ b/compiler/noirc_frontend/src/parser/parser/path.rs @@ -19,7 +19,19 @@ pub fn path_no_turbofish() -> impl NoirParser { } fn path_inner<'a>(segment: impl NoirParser + 'a) -> impl NoirParser + 'a { - let segments = segment.separated_by(just(Token::DoubleColon)).at_least(1); + let segments = segment + .separated_by(just(Token::DoubleColon)) + .at_least(1) + .then(just(Token::DoubleColon).then_ignore(none_of(Token::LeftBrace).rewind()).or_not()) + .validate(|(path_segments, trailing_colons), span, emit_error| { + if trailing_colons.is_some() { + emit_error(ParserError::with_reason( + ParserErrorReason::ExpectedIdentifierAfterColons, + span, + )); + } + path_segments + }); let make_path = |kind| move |segments, span| Path { segments, kind, span }; let prefix = |key| keyword(key).ignore_then(just(Token::DoubleColon)); @@ -69,7 +81,7 @@ mod test { use super::*; use crate::parser::{ parse_type, - parser::test_helpers::{parse_all_failing, parse_with}, + parser::test_helpers::{parse_all_failing, parse_recover, parse_with}, }; #[test] @@ -111,4 +123,18 @@ mod test { vec!["crate", "crate::std::crate", "foo::bar::crate", "foo::dep"], ); } + + #[test] + fn parse_path_with_trailing_colons() { + let src = "foo::bar::"; + + let (path, errors) = parse_recover(path_no_turbofish(), src); + let path = path.unwrap(); + assert_eq!(path.segments.len(), 2); + assert_eq!(path.segments[0].ident.0.contents, "foo"); + assert_eq!(path.segments[1].ident.0.contents, "bar"); + + assert_eq!(errors.len(), 1); + assert_eq!(errors[0].message, "expected an identifier after ::"); + } } diff --git a/tooling/lsp/src/lib.rs b/tooling/lsp/src/lib.rs index 88aab65c6fa..ca34d7686fd 100644 --- a/tooling/lsp/src/lib.rs +++ b/tooling/lsp/src/lib.rs @@ -22,8 +22,8 @@ use fm::{codespan_files as files, FileManager}; use fxhash::FxHashSet; use lsp_types::{ request::{ - DocumentSymbolRequest, HoverRequest, InlayHintRequest, PrepareRenameRequest, References, - Rename, + Completion, DocumentSymbolRequest, HoverRequest, InlayHintRequest, PrepareRenameRequest, + References, Rename, }, CodeLens, }; @@ -36,7 +36,10 @@ use nargo_toml::{find_file_manifest, resolve_workspace_from_toml, PackageSelecti use noirc_driver::{file_manager_with_stdlib, prepare_crate, NOIR_ARTIFACT_VERSION_STRING}; use noirc_frontend::{ graph::{CrateId, CrateName}, - hir::{def_map::parse_file, Context, FunctionNameMatch, ParsedFiles}, + hir::{ + def_map::{parse_file, CrateDefMap}, + Context, FunctionNameMatch, ParsedFiles, + }, node_interner::NodeInterner, parser::ParserError, ParsedModule, @@ -48,11 +51,11 @@ use notifications::{ on_did_open_text_document, on_did_save_text_document, on_exit, on_initialized, }; use requests::{ - on_code_lens_request, on_document_symbol_request, on_formatting, 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_code_lens_request, on_completion_request, on_document_symbol_request, on_formatting, + 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, }; use serde_json::Value as JsonValue; use thiserror::Error; @@ -62,6 +65,7 @@ mod notifications; mod requests; mod solver; mod types; +mod utils; #[cfg(test)] mod test_utils; @@ -86,6 +90,7 @@ pub struct LspState { cached_lenses: HashMap>, cached_definitions: HashMap, cached_parsed_files: HashMap))>, + cached_def_maps: HashMap>, options: LspInitializationOptions, } @@ -103,6 +108,7 @@ impl LspState { cached_definitions: HashMap::new(), open_documents_count: 0, cached_parsed_files: HashMap::new(), + cached_def_maps: HashMap::new(), options: Default::default(), } } @@ -136,6 +142,7 @@ impl NargoLspService { .request::(on_rename_request) .request::(on_hover_request) .request::(on_inlay_hint_request) + .request::(on_completion_request) .notification::(on_initialized) .notification::(on_did_change_configuration) .notification::(on_did_open_text_document) diff --git a/tooling/lsp/src/notifications/mod.rs b/tooling/lsp/src/notifications/mod.rs index 56aef90cfde..3a60de15c4a 100644 --- a/tooling/lsp/src/notifications/mod.rs +++ b/tooling/lsp/src/notifications/mod.rs @@ -30,7 +30,7 @@ pub(super) fn on_did_change_configuration( ControlFlow::Continue(()) } -pub(super) fn on_did_open_text_document( +pub(crate) fn on_did_open_text_document( state: &mut LspState, params: DidOpenTextDocumentParams, ) -> ControlFlow> { @@ -153,8 +153,8 @@ pub(crate) fn process_workspace_for_noir_document( Some(&file_path), ); state.cached_lenses.insert(document_uri.to_string(), collected_lenses); - - state.cached_definitions.insert(package_root_dir, context.def_interner); + state.cached_definitions.insert(package_root_dir.clone(), context.def_interner); + state.cached_def_maps.insert(package_root_dir.clone(), context.def_maps); let fm = &context.file_manager; let files = fm.as_file_map(); diff --git a/tooling/lsp/src/requests/completion.rs b/tooling/lsp/src/requests/completion.rs new file mode 100644 index 00000000000..0c1f7e724dc --- /dev/null +++ b/tooling/lsp/src/requests/completion.rs @@ -0,0 +1,716 @@ +use std::{ + collections::BTreeMap, + future::{self, Future}, +}; + +use async_lsp::ResponseError; +use fm::{FileId, PathString}; +use lsp_types::{ + CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionParams, + CompletionResponse, +}; +use noirc_errors::Span; +use noirc_frontend::{ + ast::{Ident, Path, PathKind, PathSegment, UseTree, UseTreeKind}, + graph::{CrateId, Dependency}, + hir::{ + def_map::{CrateDefMap, LocalModuleId, ModuleId}, + resolution::path_resolver::{PathResolver, StandardPathResolver}, + }, + macros_api::{ModuleDefId, NodeInterner, StructId}, + node_interner::{FuncId, GlobalId, TraitId, TypeAliasId}, + parser::{Item, ItemKind}, + ParsedModule, Type, +}; + +use crate::{utils, LspState}; + +use super::process_request; + +pub(crate) fn on_completion_request( + state: &mut LspState, + params: CompletionParams, +) -> impl Future, ResponseError>> { + let uri = params.text_document_position.clone().text_document.uri; + + let result = process_request(state, params.text_document_position.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.position, + ) + .and_then(|byte_index| { + let file = args.files.get_file(file_id).unwrap(); + let source = file.source(); + let byte = source.as_bytes().get(byte_index - 1).copied(); + let (parsed_module, _errors) = noirc_frontend::parse_program(source); + + let mut finder = NodeFinder::new( + file_id, + byte_index, + byte, + args.crate_id, + args.def_maps, + args.dependencies, + args.interner, + ); + finder.find(&parsed_module) + }) + }) + }); + future::ready(result) +} + +struct NodeFinder<'a> { + byte_index: usize, + byte: Option, + root_module_id: ModuleId, + module_id: ModuleId, + def_maps: &'a BTreeMap, + dependencies: &'a Vec, + interner: &'a NodeInterner, +} + +impl<'a> NodeFinder<'a> { + fn new( + file: FileId, + byte_index: usize, + byte: Option, + krate: CrateId, + def_maps: &'a BTreeMap, + dependencies: &'a Vec, + interner: &'a NodeInterner, + ) -> Self { + // Find the module the current file belongs to + let def_map = &def_maps[&krate]; + let root_module_id = ModuleId { krate, local_id: def_map.root() }; + let local_id = if let Some((module_index, _)) = + def_map.modules().iter().find(|(_, module_data)| module_data.location.file == file) + { + LocalModuleId(module_index) + } else { + def_map.root() + }; + let module_id = ModuleId { krate, local_id }; + Self { byte_index, byte, root_module_id, module_id, def_maps, dependencies, interner } + } + + fn find(&mut self, parsed_module: &ParsedModule) -> Option { + self.find_in_parsed_module(parsed_module) + } + + fn find_in_parsed_module( + &mut self, + parsed_module: &ParsedModule, + ) -> Option { + for item in &parsed_module.items { + if let Some(response) = self.find_in_item(item) { + return Some(response); + } + } + + None + } + + fn find_in_item(&mut self, item: &Item) -> Option { + if !self.includes_span(item.span) { + return None; + } + + match &item.kind { + ItemKind::Import(use_tree) => { + let mut prefixes = Vec::new(); + if let Some(completion) = self.find_in_use_tree(use_tree, &mut prefixes) { + return Some(completion); + } + } + ItemKind::Submodules(parsed_sub_module) => { + // Switch `self.module_id` to the submodule + let previous_module_id = self.module_id; + + let def_map = &self.def_maps[&self.module_id.krate]; + let module_data = def_map.modules().get(self.module_id.local_id.0)?; + if let Some(child_module) = module_data.children.get(&parsed_sub_module.name) { + self.module_id = + ModuleId { krate: self.module_id.krate, local_id: *child_module }; + } + + let completion = self.find_in_parsed_module(&parsed_sub_module.contents); + + // Restore the old module before continuing + self.module_id = previous_module_id; + + if let Some(completion) = completion { + return Some(completion); + } + } + _ => (), + } + + None + } + + fn find_in_use_tree( + &self, + use_tree: &UseTree, + prefixes: &mut Vec, + ) -> Option { + match &use_tree.kind { + UseTreeKind::Path(ident, alias) => { + prefixes.push(use_tree.prefix.clone()); + let response = self.find_in_use_tree_path(prefixes, ident, alias); + prefixes.pop(); + response + } + UseTreeKind::List(use_trees) => { + prefixes.push(use_tree.prefix.clone()); + for use_tree in use_trees { + if let Some(completion) = self.find_in_use_tree(use_tree, prefixes) { + return Some(completion); + } + } + prefixes.pop(); + None + } + } + } + + fn find_in_use_tree_path( + &self, + prefixes: &Vec, + ident: &Ident, + alias: &Option, + ) -> Option { + if let Some(_alias) = alias { + // Won't handle completion if there's an alias (for now) + return None; + } + + let after_colons = self.byte == Some(b':'); + let at_ident_end = self.byte_index == ident.span().end() as usize; + let at_ident_colons_end = + after_colons && self.byte_index - 2 == ident.span().end() as usize; + + if !(at_ident_end || at_ident_colons_end) { + return None; + } + + let path_kind = prefixes[0].kind; + + let mut segments: Vec = Vec::new(); + for prefix in prefixes { + for segment in &prefix.segments { + segments.push(segment.ident.clone()); + } + } + + if after_colons { + // We are right after "::" + segments.push(ident.clone()); + + self.resolve_module(segments).and_then(|module_id| { + let prefix = String::new(); + let at_root = false; + self.complete_in_module(module_id, prefix, path_kind, at_root) + }) + } else { + // We are right after the last segment + let prefix = ident.to_string(); + if segments.is_empty() { + let at_root = true; + self.complete_in_module(self.module_id, prefix, path_kind, at_root) + } else { + let at_root = false; + self.resolve_module(segments).and_then(|module_id| { + self.complete_in_module(module_id, prefix, path_kind, at_root) + }) + } + } + } + + fn complete_in_module( + &self, + module_id: ModuleId, + prefix: String, + path_kind: PathKind, + at_root: bool, + ) -> Option { + let def_map = &self.def_maps[&module_id.krate]; + let mut module_data = def_map.modules().get(module_id.local_id.0)?; + + if at_root { + match path_kind { + PathKind::Crate => { + module_data = def_map.modules().get(def_map.root().0)?; + } + PathKind::Super => { + module_data = def_map.modules().get(module_data.parent?.0)?; + } + PathKind::Dep => (), + PathKind::Plain => (), + } + } + + let mut completion_items = Vec::new(); + + for ident in module_data.definitions().names() { + let name = &ident.0.contents; + + if name_matches(name, &prefix) { + let per_ns = module_data.find_name(ident); + if let Some((module_def_id, _, _)) = per_ns.types { + completion_items + .push(self.module_def_id_completion_item(module_def_id, name.clone())); + } + + if let Some((module_def_id, _, _)) = per_ns.values { + completion_items + .push(self.module_def_id_completion_item(module_def_id, name.clone())); + } + } + } + + if at_root && path_kind == PathKind::Plain { + for dependency in self.dependencies { + let dependency_name = dependency.as_name(); + if name_matches(&dependency_name, &prefix) { + completion_items.push(crate_completion_item(dependency_name)); + } + } + + if name_matches("crate::", &prefix) { + completion_items.push(simple_completion_item( + "crate::", + CompletionItemKind::KEYWORD, + None, + )); + } + + if module_data.parent.is_some() && name_matches("super::", &prefix) { + completion_items.push(simple_completion_item( + "super::", + CompletionItemKind::KEYWORD, + None, + )); + } + } + + Some(CompletionResponse::Array(completion_items)) + } + + fn module_def_id_completion_item( + &self, + module_def_id: ModuleDefId, + name: String, + ) -> CompletionItem { + match module_def_id { + ModuleDefId::ModuleId(_) => module_completion_item(name), + ModuleDefId::FunctionId(func_id) => self.function_completion_item(func_id), + ModuleDefId::TypeId(struct_id) => self.struct_completion_item(struct_id), + ModuleDefId::TypeAliasId(type_alias_id) => { + self.type_alias_completion_item(type_alias_id) + } + ModuleDefId::TraitId(trait_id) => self.trait_completion_item(trait_id), + ModuleDefId::GlobalId(global_id) => self.global_completion_item(global_id), + } + } + + fn function_completion_item(&self, func_id: FuncId) -> CompletionItem { + let name = self.interner.function_name(&func_id).to_string(); + let mut typ = &self.interner.function_meta(&func_id).typ; + if let Type::Forall(_, typ_) = typ { + typ = typ_; + } + let description = typ.to_string(); + + simple_completion_item(name, CompletionItemKind::FUNCTION, Some(description)) + } + + fn struct_completion_item(&self, struct_id: StructId) -> CompletionItem { + let struct_type = self.interner.get_struct(struct_id); + let struct_type = struct_type.borrow(); + let name = struct_type.name.to_string(); + + simple_completion_item(name.clone(), CompletionItemKind::STRUCT, Some(name)) + } + + fn type_alias_completion_item(&self, type_alias_id: TypeAliasId) -> CompletionItem { + let type_alias = self.interner.get_type_alias(type_alias_id); + let type_alias = type_alias.borrow(); + let name = type_alias.name.to_string(); + + simple_completion_item(name.clone(), CompletionItemKind::STRUCT, Some(name)) + } + + fn trait_completion_item(&self, trait_id: TraitId) -> CompletionItem { + let trait_ = self.interner.get_trait(trait_id); + let name = trait_.name.to_string(); + + simple_completion_item(name.clone(), CompletionItemKind::INTERFACE, Some(name)) + } + + fn global_completion_item(&self, global_id: GlobalId) -> CompletionItem { + let global_definition = self.interner.get_global_definition(global_id); + let name = global_definition.name.clone(); + + let global = self.interner.get_global(global_id); + let typ = self.interner.definition_type(global.definition_id); + let description = typ.to_string(); + + simple_completion_item(name, CompletionItemKind::CONSTANT, Some(description)) + } + + fn resolve_module(&self, segments: Vec) -> Option { + if let Some(ModuleDefId::ModuleId(module_id)) = self.resolve_path(segments) { + Some(module_id) + } else { + None + } + } + + fn resolve_path(&self, segments: Vec) -> Option { + let path_segments = segments.into_iter().map(PathSegment::from).collect(); + let path = Path { segments: path_segments, kind: PathKind::Plain, span: Span::default() }; + + let path_resolver = StandardPathResolver::new(self.root_module_id); + match path_resolver.resolve(self.def_maps, path, &mut None) { + Ok(path_resolution) => Some(path_resolution.module_def_id), + Err(_) => None, + } + } + + fn includes_span(&self, span: Span) -> bool { + span.start() as usize <= self.byte_index && self.byte_index <= span.end() as usize + } +} + +fn name_matches(name: &str, prefix: &str) -> bool { + name.starts_with(prefix) +} + +fn module_completion_item(name: impl Into) -> CompletionItem { + simple_completion_item(name, CompletionItemKind::MODULE, None) +} + +fn crate_completion_item(name: impl Into) -> CompletionItem { + simple_completion_item(name, CompletionItemKind::MODULE, None) +} + +fn simple_completion_item( + label: impl Into, + kind: CompletionItemKind, + description: Option, +) -> CompletionItem { + CompletionItem { + label: label.into(), + label_details: Some(CompletionItemLabelDetails { detail: None, description }), + kind: Some(kind), + detail: None, + documentation: None, + deprecated: None, + preselect: None, + sort_text: None, + filter_text: None, + insert_text: None, + insert_text_format: None, + insert_text_mode: None, + text_edit: None, + additional_text_edits: None, + command: None, + commit_characters: None, + data: None, + tags: None, + } +} + +#[cfg(test)] +mod completion_tests { + use crate::{notifications::on_did_open_text_document, test_utils}; + + use super::*; + use lsp_types::{ + DidOpenTextDocumentParams, PartialResultParams, Position, TextDocumentIdentifier, + TextDocumentItem, TextDocumentPositionParams, WorkDoneProgressParams, + }; + use tokio::test; + + async fn assert_completion(src: &str, expected: Vec) { + let (mut state, noir_text_document) = test_utils::init_lsp_server("document_symbol").await; + + let (line, column) = src + .lines() + .enumerate() + .filter_map(|(line_index, line)| { + line.find(">|<").map(|char_index| (line_index, char_index)) + }) + .next() + .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(), + }, + }, + ); + + // Get inlay hints. These should now be relative to the changed text, + // not the saved file's text. + let response = on_completion_request( + &mut state, + CompletionParams { + text_document_position: 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 }, + partial_result_params: PartialResultParams { partial_result_token: None }, + context: None, + }, + ) + .await + .expect("Could not execute on_completion_request") + .unwrap(); + + let CompletionResponse::Array(items) = response else { + panic!("Expected response to be CompletionResponse::Array"); + }; + + let mut items = items.clone(); + items.sort_by_key(|item| item.label.clone()); + + let mut expected = expected.clone(); + expected.sort_by_key(|item| item.label.clone()); + + if items != expected { + println!( + "Items: {:?}", + items.iter().map(|item| item.label.clone()).collect::>() + ); + println!( + "Expected: {:?}", + expected.iter().map(|item| item.label.clone()).collect::>() + ); + } + + assert_eq!(items, expected); + } + + #[test] + async fn test_use_first_segment() { + let src = r#" + mod foo {} + mod foobar {} + use f>|< + "#; + + assert_completion( + src, + vec![module_completion_item("foo"), module_completion_item("foobar")], + ) + .await; + } + + #[test] + async fn test_use_second_segment() { + let src = r#" + mod foo { + mod bar {} + mod baz {} + } + use foo::>|< + "#; + + assert_completion(src, vec![module_completion_item("bar"), module_completion_item("baz")]) + .await; + } + + #[test] + async fn test_use_second_segment_after_typing() { + let src = r#" + mod foo { + mod bar {} + mod brave {} + } + use foo::ba>|< + "#; + + assert_completion(src, vec![module_completion_item("bar")]).await; + } + + #[test] + async fn test_use_struct() { + let src = r#" + mod foo { + struct Foo {} + } + use foo::>|< + "#; + + assert_completion( + src, + vec![simple_completion_item( + "Foo", + CompletionItemKind::STRUCT, + Some("Foo".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_use_function() { + let src = r#" + mod foo { + fn bar(x: i32) -> u64 { 0 } + } + use foo::>|< + "#; + + assert_completion( + src, + vec![simple_completion_item( + "bar", + CompletionItemKind::FUNCTION, + Some("fn(i32) -> u64".to_string()), + )], + ) + .await; + } + + #[test] + async fn test_use_after_crate_and_letter() { + // Prove that "std" shows up + let src = r#" + use s>|< + "#; + assert_completion(src, vec![crate_completion_item("std")]).await; + + // "std" doesn't show up anymore because of the "crate::" prefix + let src = r#" + mod something {} + use crate::s>|< + "#; + assert_completion(src, vec![module_completion_item("something")]).await; + } + + #[test] + async fn test_use_suggests_hardcoded_crate() { + let src = r#" + use c>|< + "#; + + assert_completion( + src, + vec![simple_completion_item("crate::", CompletionItemKind::KEYWORD, None)], + ) + .await; + } + + #[test] + async fn test_use_in_tree_after_letter() { + let src = r#" + mod foo { + mod bar {} + } + use foo::{b>|<} + "#; + + assert_completion(src, vec![module_completion_item("bar")]).await; + } + + #[test] + async fn test_use_in_tree_after_colons() { + let src = r#" + mod foo { + mod bar { + mod baz {} + } + } + use foo::{bar::>|<} + "#; + + assert_completion(src, vec![module_completion_item("baz")]).await; + } + + #[test] + async fn test_use_in_tree_after_colons_after_another_segment() { + let src = r#" + mod foo { + mod bar {} + mod qux {} + } + use foo::{bar, q>|<} + "#; + + assert_completion(src, vec![module_completion_item("qux")]).await; + } + + #[test] + async fn test_use_in_nested_module() { + let src = r#" + mod foo { + mod something {} + + use s>|< + } + "#; + + assert_completion( + src, + vec![ + module_completion_item("something"), + crate_completion_item("std"), + simple_completion_item("super::", CompletionItemKind::KEYWORD, None), + ], + ) + .await; + } + + #[test] + async fn test_use_after_super() { + let src = r#" + mod foo {} + + mod bar { + mod something {} + + use super::f>|< + } + "#; + + assert_completion(src, vec![module_completion_item("foo")]).await; + } + + #[test] + async fn test_use_after_crate_and_letter_nested_in_module() { + let src = r#" + mod something { + mod something_else {} + use crate::s>|< + } + + "#; + assert_completion(src, vec![module_completion_item("something")]).await; + } + #[test] + + async fn test_use_after_crate_segment_and_letter_nested_in_module() { + let src = r#" + mod something { + mod something_else {} + use crate::something::s>|< + } + + "#; + assert_completion(src, vec![module_completion_item("something_else")]).await; + } +} diff --git a/tooling/lsp/src/requests/hover.rs b/tooling/lsp/src/requests/hover.rs index 73ea504b496..b6fdc6f7842 100644 --- a/tooling/lsp/src/requests/hover.rs +++ b/tooling/lsp/src/requests/hover.rs @@ -321,9 +321,9 @@ fn format_parent_module_from_module_id( ) -> bool { let crate_id = module.krate; let crate_name = match crate_id { - CrateId::Root(_) => Some(args.root_crate_name.clone()), + CrateId::Root(_) => Some(args.crate_name.clone()), CrateId::Crate(_) => args - .root_crate_dependencies + .dependencies .iter() .find(|dep| dep.crate_id == crate_id) .map(|dep| format!("{}", dep.name)), diff --git a/tooling/lsp/src/requests/inlay_hint.rs b/tooling/lsp/src/requests/inlay_hint.rs index bc7567b3237..8c3d8a05652 100644 --- a/tooling/lsp/src/requests/inlay_hint.rs +++ b/tooling/lsp/src/requests/inlay_hint.rs @@ -1,4 +1,3 @@ -use fm::codespan_files::Files; use std::future::{self, Future}; use async_lsp::ResponseError; @@ -21,7 +20,7 @@ use noirc_frontend::{ ParsedModule, Type, TypeBinding, TypeVariable, TypeVariableKind, }; -use crate::LspState; +use crate::{utils, LspState}; use super::{process_request, to_lsp_location, InlayHintsOptions}; @@ -43,7 +42,7 @@ pub(crate) fn on_inlay_hint_request( let source = file.source(); let (parsed_moduled, _errors) = noirc_frontend::parse_program(source); - let span = range_to_byte_span(args.files, file_id, ¶ms.range) + let span = utils::range_to_byte_span(args.files, file_id, ¶ms.range) .map(|range| Span::from(range.start as u32..range.end as u32)); let mut collector = @@ -691,63 +690,6 @@ fn get_expression_name(expression: &Expression) -> Option { } } -// These functions are copied from the codespan_lsp crate, except that they never panic -// (the library will sometimes panic, so functions returning Result are not always accurate) - -fn range_to_byte_span( - files: &FileMap, - file_id: FileId, - range: &lsp_types::Range, -) -> Option> { - Some( - position_to_byte_index(files, file_id, &range.start)? - ..position_to_byte_index(files, file_id, &range.end)?, - ) -} - -fn position_to_byte_index( - files: &FileMap, - file_id: FileId, - position: &lsp_types::Position, -) -> Option { - let Ok(source) = files.source(file_id) else { - return None; - }; - - let Ok(line_span) = files.line_range(file_id, position.line as usize) else { - return None; - }; - let line_str = source.get(line_span.clone())?; - - let byte_offset = character_to_line_offset(line_str, position.character)?; - - Some(line_span.start + byte_offset) -} - -fn character_to_line_offset(line: &str, character: u32) -> Option { - let line_len = line.len(); - let mut character_offset = 0; - - let mut chars = line.chars(); - while let Some(ch) = chars.next() { - if character_offset == character { - let chars_off = chars.as_str().len(); - let ch_off = ch.len_utf8(); - - return Some(line_len - chars_off - ch_off); - } - - character_offset += ch.len_utf16() as u32; - } - - // Handle positions after the last character on the line - if character_offset == character { - Some(line_len) - } else { - None - } -} - #[cfg(test)] mod inlay_hints_tests { use crate::{ diff --git a/tooling/lsp/src/requests/mod.rs b/tooling/lsp/src/requests/mod.rs index aef5d782c46..e138f839600 100644 --- a/tooling/lsp/src/requests/mod.rs +++ b/tooling/lsp/src/requests/mod.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::path::PathBuf; use std::{collections::HashMap, future::Future}; @@ -15,6 +16,8 @@ use lsp_types::{ }; use nargo_fmt::Config; use noirc_driver::file_manager_with_stdlib; +use noirc_frontend::graph::CrateId; +use noirc_frontend::hir::def_map::CrateDefMap; use noirc_frontend::{graph::Dependency, macros_api::NodeInterner}; use serde::{Deserialize, Serialize}; @@ -34,6 +37,7 @@ use crate::{ // and params passed in. mod code_lens_request; +mod completion; mod document_symbol; mod goto_declaration; mod goto_definition; @@ -47,12 +51,12 @@ mod tests; pub(crate) use { code_lens_request::collect_lenses_for_package, code_lens_request::on_code_lens_request, - document_symbol::on_document_symbol_request, goto_declaration::on_goto_declaration_request, - goto_definition::on_goto_definition_request, 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, + completion::on_completion_request, document_symbol::on_document_symbol_request, + goto_declaration::on_goto_declaration_request, goto_definition::on_goto_definition_request, + 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, }; /// LSP client will send initialization request after the server has started. @@ -228,6 +232,15 @@ pub(crate) fn on_initialize( label: Some("Noir".to_string()), }, )), + completion_provider: Some(lsp_types::OneOf::Right(lsp_types::CompletionOptions { + resolve_provider: None, + trigger_characters: Some(vec![":".to_string()]), + all_commit_characters: None, + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: None, + }, + completion_item: None, + })), }, server_info: None, }) @@ -375,8 +388,10 @@ pub(crate) struct ProcessRequestCallbackArgs<'a> { files: &'a FileMap, interner: &'a NodeInterner, interners: &'a HashMap, - root_crate_name: String, - root_crate_dependencies: &'a Vec, + crate_id: CrateId, + crate_name: String, + dependencies: &'a Vec, + def_maps: &'a BTreeMap, } pub(crate) fn process_request( @@ -411,12 +426,15 @@ where crate::prepare_package(&workspace_file_manager, &parsed_files, package); let interner; + let def_maps; if let Some(def_interner) = state.cached_definitions.get(&package_root_path) { interner = def_interner; + def_maps = state.cached_def_maps.get(&package_root_path).unwrap(); } else { // We ignore the warnings and errors produced by compilation while resolving the definition let _ = noirc_driver::check_crate(&mut context, crate_id, &Default::default()); interner = &context.def_interner; + def_maps = &context.def_maps; } let files = context.file_manager.as_file_map(); @@ -432,8 +450,10 @@ where files, interner, interners: &state.cached_definitions, - root_crate_name: package.name.to_string(), - root_crate_dependencies: &context.crate_graph[context.root_crate_id()].dependencies, + crate_id, + crate_name: package.name.to_string(), + dependencies: &context.crate_graph[context.root_crate_id()].dependencies, + def_maps, })) } pub(crate) fn find_all_references_in_workspace( diff --git a/tooling/lsp/src/types.rs b/tooling/lsp/src/types.rs index fa3234cf3bb..5afda0d292a 100644 --- a/tooling/lsp/src/types.rs +++ b/tooling/lsp/src/types.rs @@ -1,7 +1,8 @@ use fm::FileId; use lsp_types::{ - DeclarationCapability, DefinitionOptions, DocumentSymbolOptions, HoverOptions, - InlayHintOptions, OneOf, ReferencesOptions, RenameOptions, TypeDefinitionProviderCapability, + CompletionOptions, DeclarationCapability, DefinitionOptions, DocumentSymbolOptions, + HoverOptions, InlayHintOptions, OneOf, ReferencesOptions, RenameOptions, + TypeDefinitionProviderCapability, }; use noirc_driver::DebugFile; use noirc_errors::{debug_info::OpCodesCount, Location}; @@ -156,6 +157,10 @@ pub(crate) struct ServerCapabilities { /// The server provides document symbol support. #[serde(skip_serializing_if = "Option::is_none")] pub(crate) document_symbol_provider: Option>, + + /// The server provides completion support. + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) completion_provider: Option>, } #[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)] diff --git a/tooling/lsp/src/utils.rs b/tooling/lsp/src/utils.rs new file mode 100644 index 00000000000..96db1f7bfa2 --- /dev/null +++ b/tooling/lsp/src/utils.rs @@ -0,0 +1,59 @@ +// These functions are copied from the codespan_lsp crate, except that they never panic +// (the library will sometimes panic, so functions returning Result are not always accurate) + +use fm::codespan_files::Files; +use fm::{FileId, FileMap}; + +pub(crate) fn range_to_byte_span( + files: &FileMap, + file_id: FileId, + range: &lsp_types::Range, +) -> Option> { + Some( + position_to_byte_index(files, file_id, &range.start)? + ..position_to_byte_index(files, file_id, &range.end)?, + ) +} + +pub(crate) fn position_to_byte_index( + files: &FileMap, + file_id: FileId, + position: &lsp_types::Position, +) -> Option { + let Ok(source) = files.source(file_id) else { + return None; + }; + + let Ok(line_span) = files.line_range(file_id, position.line as usize) else { + return None; + }; + let line_str = source.get(line_span.clone())?; + + let byte_offset = character_to_line_offset(line_str, position.character)?; + + Some(line_span.start + byte_offset) +} + +pub(crate) fn character_to_line_offset(line: &str, character: u32) -> Option { + let line_len = line.len(); + let mut character_offset = 0; + + let mut chars = line.chars(); + while let Some(ch) = chars.next() { + if character_offset == character { + let chars_off = chars.as_str().len(); + let ch_off = ch.len_utf8(); + + return Some(line_len - chars_off - ch_off); + } + + character_offset += ch.len_utf16() as u32; + } + + // Handle positions after the last character on the line + if character_offset == character { + Some(line_len) + } else { + None + } +}