From 012fa9eab87b7b53cfa28faf1dfc890eff4f62d1 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Tue, 26 Jan 2021 22:51:16 +0100 Subject: [PATCH] feature(lsp): completion for unqualified paths and structs --- crates/mun_codegen/src/ir/body.rs | 8 +- crates/mun_hir/Cargo.toml | 3 +- crates/mun_hir/src/code_model.rs | 9 +- crates/mun_hir/src/code_model/function.rs | 6 + crates/mun_hir/src/code_model/src.rs | 8 +- crates/mun_hir/src/code_model/struct.rs | 35 ++- crates/mun_hir/src/item_scope.rs | 7 +- crates/mun_hir/src/item_tree/lower.rs | 5 +- crates/mun_hir/src/lib.rs | 5 +- crates/mun_hir/src/resolve.rs | 45 +++- crates/mun_hir/src/semantics.rs | 227 ++++++++++++++++++ crates/mun_hir/src/semantics/source_to_def.rs | 158 ++++++++++++ crates/mun_hir/src/source_analyzer.rs | 168 +++++++++++++ crates/mun_hir/src/ty.rs | 5 + crates/mun_language_server/Cargo.toml | 2 + crates/mun_language_server/src/analysis.rs | 25 +- .../mun_language_server/src/capabilities.rs | 10 +- .../mun_language_server/src/change_fixture.rs | 103 ++++++++ crates/mun_language_server/src/completion.rs | 78 ++++++ .../src/completion/context.rs | 138 +++++++++++ .../mun_language_server/src/completion/dot.rs | 93 +++++++ .../src/completion/item.rs | 114 +++++++++ .../src/completion/render.rs | 107 +++++++++ .../src/completion/render/function.rs | 37 +++ ...letion__dot__tests__incomplete_struct.snap | 5 + ...completion__dot__tests__nested_struct.snap | 5 + ...completion__dot__tests__struct_fields.snap | 6 + ..._completion__dot__tests__tuple_struct.snap | 6 + ..._unqualified_path__tests__local_scope.snap | 7 + .../src/completion/test_utils.rs | 64 +++++ .../src/completion/unqualified_path.rs | 40 +++ crates/mun_language_server/src/db.rs | 8 +- crates/mun_language_server/src/from_lsp.rs | 12 + crates/mun_language_server/src/handlers.rs | 54 ++++- crates/mun_language_server/src/lib.rs | 18 ++ crates/mun_language_server/src/state.rs | 2 +- .../mun_language_server/src/state/protocol.rs | 1 + crates/mun_language_server/src/symbol_kind.rs | 3 + crates/mun_language_server/src/to_lsp.rs | 48 ++++ crates/mun_syntax/Cargo.toml | 2 + crates/mun_syntax/src/lib.rs | 16 ++ crates/mun_syntax/src/parsing/grammar.rs | 11 +- .../src/parsing/grammar/expressions.rs | 9 +- crates/mun_syntax/src/tests/parser.rs | 10 + ...ax__tests__parser__missing_field_expr.snap | 28 +++ crates/mun_syntax/src/utils.rs | 27 +++ 46 files changed, 1718 insertions(+), 60 deletions(-) create mode 100644 crates/mun_hir/src/semantics.rs create mode 100644 crates/mun_hir/src/semantics/source_to_def.rs create mode 100644 crates/mun_hir/src/source_analyzer.rs create mode 100644 crates/mun_language_server/src/change_fixture.rs create mode 100644 crates/mun_language_server/src/completion.rs create mode 100644 crates/mun_language_server/src/completion/context.rs create mode 100644 crates/mun_language_server/src/completion/dot.rs create mode 100644 crates/mun_language_server/src/completion/item.rs create mode 100644 crates/mun_language_server/src/completion/render.rs create mode 100644 crates/mun_language_server/src/completion/render/function.rs create mode 100644 crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__dot__tests__incomplete_struct.snap create mode 100644 crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__dot__tests__nested_struct.snap create mode 100644 crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__dot__tests__struct_fields.snap create mode 100644 crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__dot__tests__tuple_struct.snap create mode 100644 crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__unqualified_path__tests__local_scope.snap create mode 100644 crates/mun_language_server/src/completion/test_utils.rs create mode 100644 crates/mun_language_server/src/completion/unqualified_path.rs create mode 100644 crates/mun_syntax/src/tests/snapshots/mun_syntax__tests__parser__missing_field_expr.snap create mode 100644 crates/mun_syntax/src/utils.rs diff --git a/crates/mun_codegen/src/ir/body.rs b/crates/mun_codegen/src/ir/body.rs index b10ccd1a8..1660108e8 100644 --- a/crates/mun_codegen/src/ir/body.rs +++ b/crates/mun_codegen/src/ir/body.rs @@ -1326,9 +1326,7 @@ impl<'db, 'ink, 't> BodyIrGenerator<'db, 'ink, 't> { let field_idx = hir_struct .field(self.db, name) .expect("expected a struct field") - .id() - .into_raw() - .into(); + .index(self.db); let field_ir_name = &format!("{}.{}", hir_struct_name, name); if self.is_place_expr(receiver_expr) { @@ -1383,9 +1381,7 @@ impl<'db, 'ink, 't> BodyIrGenerator<'db, 'ink, 't> { let field_idx = hir_struct .field(self.db, name) .expect("expected a struct field") - .id() - .into_raw() - .into(); + .index(self.db); let receiver_ptr = self.gen_place_expr(receiver_expr); let receiver_ptr = self diff --git a/crates/mun_hir/Cargo.toml b/crates/mun_hir/Cargo.toml index ff87da21c..724009a0b 100644 --- a/crates/mun_hir/Cargo.toml +++ b/crates/mun_hir/Cargo.toml @@ -24,7 +24,8 @@ ena = "0.14" drop_bomb = "0.1.4" either = "1.5.3" itertools = "0.10.0" -smallvec = "1.4.2" +smallvec = "1.6.1" +arrayvec = "0.5.2" [dev-dependencies] insta = "0.16" diff --git a/crates/mun_hir/src/code_model.rs b/crates/mun_hir/src/code_model.rs index 75a3b8623..e81c0d341 100644 --- a/crates/mun_hir/src/code_model.rs +++ b/crates/mun_hir/src/code_model.rs @@ -12,13 +12,14 @@ pub use self::{ function::Function, module::{Module, ModuleDef}, package::Package, - r#struct::{LocalStructFieldId, Struct, StructField, StructKind, StructMemoryKind}, + r#struct::{Field, LocalFieldId, Struct, StructKind, StructMemoryKind}, + src::HasSource, type_alias::TypeAlias, }; pub use self::{ function::FunctionData, - r#struct::{StructData, StructFieldData}, + r#struct::{FieldData, StructData}, type_alias::TypeAliasData, }; @@ -57,13 +58,13 @@ impl DefWithStruct { } } - pub fn fields(self, db: &dyn HirDatabase) -> Vec { + pub fn fields(self, db: &dyn HirDatabase) -> Vec { match self { DefWithStruct::Struct(s) => s.fields(db), } } - pub fn field(self, db: &dyn HirDatabase, name: &Name) -> Option { + pub fn field(self, db: &dyn HirDatabase, name: &Name) -> Option { match self { DefWithStruct::Struct(s) => s.field(db, name), } diff --git a/crates/mun_hir/src/code_model/function.rs b/crates/mun_hir/src/code_model/function.rs index fceecb549..a78cd535f 100644 --- a/crates/mun_hir/src/code_model/function.rs +++ b/crates/mun_hir/src/code_model/function.rs @@ -138,6 +138,12 @@ impl Function { db.type_for_def(self.into(), Namespace::Values).0 } + pub fn ret_type(self, db: &dyn HirDatabase) -> Ty { + let resolver = self.id.resolver(db.upcast()); + let data = self.data(db.upcast()); + Ty::from_hir(db, &resolver, &data.type_ref_map, data.ret_type).ty + } + pub fn infer(self, db: &dyn HirDatabase) -> Arc { db.infer(self.id.into()) } diff --git a/crates/mun_hir/src/code_model/src.rs b/crates/mun_hir/src/code_model/src.rs index f2db58b47..2dbdf8853 100644 --- a/crates/mun_hir/src/code_model/src.rs +++ b/crates/mun_hir/src/code_model/src.rs @@ -1,12 +1,16 @@ -use crate::code_model::{Function, Struct, StructField, TypeAlias}; +use crate::code_model::{Field, Function, Struct, TypeAlias}; use crate::ids::{AssocItemLoc, Lookup}; use crate::in_file::InFile; use crate::item_tree::{ItemTreeId, ItemTreeNode}; use crate::{DefDatabase, ItemLoc}; use mun_syntax::ast; +/// A trait implemented for items that can be related back to their source. The +/// [`HasSource::source`] method returns the source location of its instance. pub trait HasSource { type Ast; + + /// Returns the source location of this instance. fn source(&self, db: &dyn DefDatabase) -> InFile; } @@ -56,7 +60,7 @@ impl HasSource for Struct { } } -impl HasSource for StructField { +impl HasSource for Field { type Ast = ast::RecordFieldDef; fn source(&self, db: &dyn DefDatabase) -> InFile { diff --git a/crates/mun_hir/src/code_model/struct.rs b/crates/mun_hir/src/code_model/struct.rs index 8b824bce0..7504e0b04 100644 --- a/crates/mun_hir/src/code_model/struct.rs +++ b/crates/mun_hir/src/code_model/struct.rs @@ -31,13 +31,15 @@ impl From for Struct { } } +/// A field of a [`Struct`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct StructField { +pub struct Field { pub(crate) parent: Struct, - pub(crate) id: LocalStructFieldId, + pub(crate) id: LocalFieldId, } -impl StructField { +impl Field { + /// Returns the type of the field pub fn ty(self, db: &dyn HirDatabase) -> Ty { let data = self.parent.data(db.upcast()); let type_ref_id = data.fields[self.id].type_ref; @@ -45,11 +47,18 @@ impl StructField { lower[type_ref_id].clone() } + /// Returns the name of the field pub fn name(self, db: &dyn HirDatabase) -> Name { self.parent.data(db.upcast()).fields[self.id].name.clone() } - pub fn id(self) -> LocalStructFieldId { + /// Returns the index of this field in the parent + pub fn index(self, _db: &dyn HirDatabase) -> u32 { + self.id.into_raw().into() + } + + /// Returns the ID of the field with relation to the parent struct + pub(crate) fn id(self) -> LocalFieldId { self.id } } @@ -86,20 +95,20 @@ impl Struct { .collect() } - pub fn fields(self, db: &dyn HirDatabase) -> Vec { + pub fn fields(self, db: &dyn HirDatabase) -> Vec { self.data(db.upcast()) .fields .iter() - .map(|(id, _)| StructField { parent: self, id }) + .map(|(id, _)| Field { parent: self, id }) .collect() } - pub fn field(self, db: &dyn HirDatabase, name: &Name) -> Option { + pub fn field(self, db: &dyn HirDatabase, name: &Name) -> Option { self.data(db.upcast()) .fields .iter() .find(|(_, data)| data.name == *name) - .map(|(id, _)| StructField { parent: self, id }) + .map(|(id, _)| Field { parent: self, id }) } pub fn ty(self, db: &dyn HirDatabase) -> Ty { @@ -131,7 +140,7 @@ impl Struct { /// ) /// ``` #[derive(Debug, Clone, PartialEq, Eq)] -pub struct StructFieldData { +pub struct FieldData { pub name: Name, pub type_ref: LocalTypeRefId, } @@ -155,13 +164,13 @@ impl fmt::Display for StructKind { } /// An identifier for a struct's or tuple's field -pub type LocalStructFieldId = Idx; +pub type LocalFieldId = Idx; #[derive(Debug, PartialEq, Eq)] pub struct StructData { pub name: Name, pub visibility: RawVisibility, - pub fields: Arena, + pub fields: Arena, pub kind: StructKind, pub memory_kind: StructMemoryKind, type_ref_map: TypeRefMap, @@ -185,7 +194,7 @@ impl StructData { ast::StructKind::Record(r) => { let fields = r .fields() - .map(|fd| StructFieldData { + .map(|fd| FieldData { name: fd.name().map(|n| n.as_name()).unwrap_or_else(Name::missing), type_ref: type_ref_builder.alloc_from_node_opt(fd.ascribed_type().as_ref()), }) @@ -196,7 +205,7 @@ impl StructData { let fields = t .fields() .enumerate() - .map(|(index, fd)| StructFieldData { + .map(|(index, fd)| FieldData { name: Name::new_tuple_field(index), type_ref: type_ref_builder.alloc_from_node_opt(fd.type_ref().as_ref()), }) diff --git a/crates/mun_hir/src/item_scope.rs b/crates/mun_hir/src/item_scope.rs index a25bde428..3ec1a9485 100644 --- a/crates/mun_hir/src/item_scope.rs +++ b/crates/mun_hir/src/item_scope.rs @@ -1,6 +1,7 @@ -use crate::module_tree::LocalModuleId; -use crate::primitive_type::PrimitiveType; -use crate::{ids::ItemDefinitionId, visibility::Visibility, Name, PerNs}; +use crate::{ + ids::ItemDefinitionId, module_tree::LocalModuleId, primitive_type::PrimitiveType, + visibility::Visibility, Name, PerNs, +}; use once_cell::sync::Lazy; use rustc_hash::{FxHashMap, FxHashSet}; use std::collections::hash_map::Entry; diff --git a/crates/mun_hir/src/item_tree/lower.rs b/crates/mun_hir/src/item_tree/lower.rs index b1198883c..c5f353ee1 100644 --- a/crates/mun_hir/src/item_tree/lower.rs +++ b/crates/mun_hir/src/item_tree/lower.rs @@ -104,10 +104,7 @@ impl Context { ast::ModuleItemKind::StructDef(ast) => self.lower_struct(&ast).map(Into::into), ast::ModuleItemKind::TypeAliasDef(ast) => self.lower_type_alias(&ast).map(Into::into), ast::ModuleItemKind::Use(ast) => Some(ModItems( - self.lower_use(&ast) - .into_iter() - .map(Into::into) - .collect::>(), + self.lower_use(&ast).into_iter().map(Into::into).collect(), )), } } diff --git a/crates/mun_hir/src/lib.rs b/crates/mun_hir/src/lib.rs index af57a820a..622ba7621 100644 --- a/crates/mun_hir/src/lib.rs +++ b/crates/mun_hir/src/lib.rs @@ -37,7 +37,8 @@ pub use crate::{ use crate::{name::AsName, source_id::AstIdMap}; pub use self::code_model::{ - Function, FunctionData, Module, ModuleDef, Package, Struct, StructMemoryKind, TypeAlias, + Field, Function, FunctionData, HasSource, Module, ModuleDef, Package, Struct, StructMemoryKind, + TypeAlias, }; #[macro_use] @@ -71,6 +72,8 @@ mod item_scope; mod mock; mod package_defs; mod package_set; +pub mod semantics; +mod source_analyzer; #[cfg(test)] mod tests; mod visibility; diff --git a/crates/mun_hir/src/resolve.rs b/crates/mun_hir/src/resolve.rs index a4796da9a..90cb3bcf6 100644 --- a/crates/mun_hir/src/resolve.rs +++ b/crates/mun_hir/src/resolve.rs @@ -1,12 +1,13 @@ use crate::ids::{ DefWithBodyId, FunctionId, ItemDefinitionId, Lookup, ModuleId, StructId, TypeAliasId, }; +use crate::item_scope::BUILTIN_SCOPE; use crate::module_tree::LocalModuleId; use crate::package_defs::PackageDefs; use crate::primitive_type::PrimitiveType; use crate::visibility::RawVisibility; use crate::{ - expr::scope::LocalScopeId, expr::PatId, DefDatabase, ExprId, ExprScopes, Path, PerNs, + expr::scope::LocalScopeId, expr::PatId, DefDatabase, ExprId, ExprScopes, Name, Path, PerNs, Visibility, }; use std::sync::Arc; @@ -58,6 +59,12 @@ pub enum TypeNs { PrimitiveType(PrimitiveType), } +/// An item definition visible from a certain scope. +pub enum ScopeDef { + PerNs(PerNs<(ItemDefinitionId, Visibility)>), + Local(PatId), +} + impl Resolver { /// Adds another scope to the resolver from which it can resolve names pub(crate) fn push_scope(mut self, scope: Scope) -> Resolver { @@ -273,6 +280,42 @@ impl Resolver { local_id, }) } + + /// If the resolver holds a scope from a body, returns that body. + pub fn body_owner(&self) -> Option { + self.scopes.iter().rev().find_map(|scope| match scope { + Scope::ExprScope(it) => Some(it.owner), + _ => None, + }) + } + + /// Calls the `visitor` for each entry in scope. + pub fn visit_all_names(&self, db: &dyn DefDatabase, visitor: &mut dyn FnMut(Name, ScopeDef)) { + for scope in self.scopes.iter().rev() { + scope.visit_names(db, visitor) + } + } +} + +impl Scope { + /// Calls the `visitor` for each entry in scope. + fn visit_names(&self, _db: &dyn DefDatabase, visitor: &mut dyn FnMut(Name, ScopeDef)) { + match self { + Scope::ModuleScope(m) => { + m.package_defs[m.module_id] + .entries() + .for_each(|(name, def)| visitor(name.clone(), ScopeDef::PerNs(def))); + BUILTIN_SCOPE.iter().for_each(|(name, &def)| { + visitor(name.clone(), ScopeDef::PerNs(def)); + }) + } + Scope::ExprScope(scope) => scope + .expr_scopes + .entries(scope.scope_id) + .iter() + .for_each(|entry| visitor(entry.name().clone(), ScopeDef::Local(entry.pat()))), + } + } } /// Returns a resolver applicable to the specified expression diff --git a/crates/mun_hir/src/semantics.rs b/crates/mun_hir/src/semantics.rs new file mode 100644 index 000000000..4988ea924 --- /dev/null +++ b/crates/mun_hir/src/semantics.rs @@ -0,0 +1,227 @@ +//! `Semantics` provides the means to get semantic information from syntax trees that are not +//! necessarily part of the compilation process. This is useful when you want to extract information +//! from a modified source file in the context of the current state. +//! +//! Our compilation databases (e.g. `HirDatabase`) provides a lot of steps to go from a syntax tree +//! (as provided by the [`mun_syntax::ast`] module) to more abstract representations of the source +//! through the process of `lowering`. However, for IDE purposes we often want to cut through all +//! this and go from source locations straight to lowered data structures and back. This is what +//! [`Semantics`] enables. + +mod source_to_def; + +use crate::{ + ids::{DefWithBodyId, ItemDefinitionId}, + resolve, + resolve::HasResolver, + semantics::source_to_def::{SourceToDefCache, SourceToDefContainer, SourceToDefContext}, + source_analyzer::SourceAnalyzer, + FileId, HirDatabase, InFile, ModuleDef, Name, PatId, PerNs, Resolver, Ty, Visibility, +}; +use arrayvec::ArrayVec; +use mun_syntax::{ast, AstNode, SyntaxNode, TextSize}; +use rustc_hash::FxHashMap; +use std::cell::RefCell; + +/// The primary API to get semantic information, like types, from syntax trees. Exposes the database +/// it was created with through the `db` field. +pub struct Semantics<'db> { + pub db: &'db dyn HirDatabase, + + /// Cache of root syntax nodes to their `FileId` + source_file_to_file: RefCell>, + + /// A cache to map source locations to definitions + source_to_definition_cache: RefCell, +} + +impl<'db> Semantics<'db> { + /// Constructs a new `Semantics` instance with the given database. + pub fn new(db: &'db dyn HirDatabase) -> Self { + Self { + db, + source_file_to_file: Default::default(), + source_to_definition_cache: Default::default(), + } + } + + /// Returns the Concrete Syntax Tree for the file with the given `file_id`. + pub fn parse(&self, file_id: FileId) -> ast::SourceFile { + let tree = self.db.parse(file_id).tree(); + let mut cache = self.source_file_to_file.borrow_mut(); + cache.insert(tree.syntax().clone(), file_id); + tree + } + + /// Computes the `SemanticScope` at the given position in a CST. + pub fn scope_at_offset(&self, node: &SyntaxNode, offset: TextSize) -> SemanticsScope<'db> { + let analyzer = self.analyze_with_offset(node, offset); + SemanticsScope { + db: self.db, + file_id: analyzer.file_id, + resolver: analyzer.resolver, + } + } + + /// Returns the type of the given expression + pub fn type_of_expr(&self, expr: &ast::Expr) -> Option { + self.analyze(expr.syntax()).type_of_expr(self.db, expr) + } + + /// Returns the source analyzer for the given node. + fn analyze(&self, node: &SyntaxNode) -> SourceAnalyzer { + self.build_analyzer(node, None) + } + + /// Constructs a `SourceAnalyzer` for the token at the given offset. + fn analyze_with_offset(&self, node: &SyntaxNode, offset: TextSize) -> SourceAnalyzer { + self.build_analyzer(node, Some(offset)) + } + + /// Internal function that constructs a `SourceAnalyzer` from the given `node` and optional + /// `offset` in the file. + fn build_analyzer(&self, node: &SyntaxNode, offset: Option) -> SourceAnalyzer { + let node = self.find_file(node.clone()); + let node = node.as_ref(); + + // Find the "lowest" container that contains the code + let container = match self.with_source_to_def_context(|ctx| ctx.find_container(node)) { + Some(it) => it, + None => return SourceAnalyzer::new_for_resolver(Resolver::default(), node), + }; + + // Construct an analyzer for the given container + let resolver = match container { + SourceToDefContainer::DefWithBodyId(def) => { + return SourceAnalyzer::new_for_body(self.db, def, node, offset) + } + SourceToDefContainer::ModuleId(id) => id.resolver(self.db.upcast()), + }; + + SourceAnalyzer::new_for_resolver(resolver, node) + } + + /// Runs a function with a `SourceToDefContext` which can be used to cache definition queries. + fn with_source_to_def_context T, T>(&self, f: F) -> T { + let mut cache = self.source_to_definition_cache.borrow_mut(); + let mut context = SourceToDefContext { + db: self.db, + cache: &mut *cache, + }; + f(&mut context) + } + + /// Returns the file that is associated with the given root CST node or `None` if no such + /// association exists. + fn lookup_file(&self, root_node: &SyntaxNode) -> Option { + let cache = self.source_file_to_file.borrow(); + cache.get(root_node).copied() + } + + /// Decorates the specified node with the file that it is associated with. + fn find_file(&self, node: SyntaxNode) -> InFile { + let root_node = find_root(&node); + let file_id = self.lookup_file(&root_node).unwrap_or_else(|| { + panic!( + "\n\nFailed to lookup {:?} in this Semantics.\n\ + Make sure to use only query nodes, derived from this instance of Semantics.\n\ + root node: {:?}\n\ + known nodes: {}\n\n", + node, + root_node, + self.source_file_to_file + .borrow() + .keys() + .map(|it| format!("{:?}", it)) + .collect::>() + .join(", ") + ) + }); + InFile::new(file_id, node) + } +} + +/// Returns the root node of the specified node. +fn find_root(node: &SyntaxNode) -> SyntaxNode { + node.ancestors().last().unwrap() +} + +/// Represents the notion of a scope (set of possible names) at a particular position in source. +pub struct SemanticsScope<'a> { + pub db: &'a dyn HirDatabase, + file_id: FileId, + resolver: Resolver, +} + +/// Represents an element in a scope +pub enum ScopeDef { + ModuleDef(ModuleDef), + Local(Local), + Unknown, +} + +impl ScopeDef { + /// Returns all the `ScopeDef`s from a `PerNs`. Never returns duplicates. + pub fn all_items(def: PerNs<(ItemDefinitionId, Visibility)>) -> ArrayVec<[Self; 2]> { + let mut items = ArrayVec::new(); + match (def.take_types(), def.take_values()) { + (Some(ty), None) => items.push(ScopeDef::ModuleDef(ty.0.into())), + (None, Some(val)) => items.push(ScopeDef::ModuleDef(val.0.into())), + (Some(ty), Some(val)) => { + // Some things are returned as both a value and a type, such as a unit struct. + items.push(ScopeDef::ModuleDef(ty.0.into())); + if ty != val { + items.push(ScopeDef::ModuleDef(val.0.into())) + } + } + (None, None) => {} + }; + + if items.is_empty() { + items.push(ScopeDef::Unknown) + } + + items + } +} + +/// A local variable in a body +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Local { + pub(crate) parent: DefWithBodyId, + pub(crate) pat_id: PatId, +} + +impl Local { + /// Returns the type of this local + pub fn ty(self, db: &dyn HirDatabase) -> Ty { + let infer = db.infer(self.parent); + infer[self.pat_id].clone() + } +} + +impl<'a> SemanticsScope<'a> { + /// Call the `visit` function for every named item in the scope + pub fn visit_all_names(&self, visit: &mut dyn FnMut(Name, ScopeDef)) { + let resolver = &self.resolver; + + resolver.visit_all_names(self.db.upcast(), &mut |name, def| { + let def = match def { + resolve::ScopeDef::PerNs(it) => { + let items = ScopeDef::all_items(it); + for item in items { + visit(name.clone(), item); + } + return; + } + resolve::ScopeDef::Local(pat_id) => { + let parent = resolver + .body_owner() + .expect("found a local outside of a body"); + ScopeDef::Local(Local { parent, pat_id }) + } + }; + visit(name, def) + }) + } +} diff --git a/crates/mun_hir/src/semantics/source_to_def.rs b/crates/mun_hir/src/semantics/source_to_def.rs new file mode 100644 index 000000000..27703c181 --- /dev/null +++ b/crates/mun_hir/src/semantics/source_to_def.rs @@ -0,0 +1,158 @@ +use crate::{ + code_model::src::HasSource, + ids::{DefWithBodyId, FunctionId, ItemDefinitionId, Lookup, StructId, TypeAliasId}, + item_scope::ItemScope, + DefDatabase, FileId, HirDatabase, InFile, ModuleId, +}; +use mun_syntax::{ast, match_ast, AstNode, SyntaxNode}; +use rustc_hash::FxHashMap; + +pub(super) type SourceToDefCache = FxHashMap; + +/// An object that can be used to efficiently find definitions of source objects. It is used to +/// find HIR elements for corresponding AST elements. +pub(super) struct SourceToDefContext<'a, 'db> { + pub(super) db: &'db dyn HirDatabase, + pub(super) cache: &'a mut SourceToDefCache, +} + +impl SourceToDefContext<'_, '_> { + /// Find the container for the given syntax tree node. + pub(super) fn find_container( + &mut self, + src: InFile<&SyntaxNode>, + ) -> Option { + for container in std::iter::successors(Some(src.cloned()), move |node| { + node.value.parent().map(|parent| node.with_value(parent)) + }) + .skip(1) + { + let res: SourceToDefContainer = match_ast! { + match (container.value) { + ast::FunctionDef(it) => { + let def = self.fn_to_def(container.with_value(it))?; + DefWithBodyId::from(def).into() + }, + _ => continue, + } + }; + return Some(res); + } + + let def = self.file_to_def(src.file_id)?; + Some(def.into()) + } + + /// Find the `FunctionId` associated with the specified syntax tree node. + fn fn_to_def(&mut self, src: InFile) -> Option { + let container = self.find_container(src.as_ref().map(|it| it.syntax()))?; + let db = self.db; + let def_map = &*self + .cache + .entry(container) + .or_insert_with(|| container.source_to_def_map(db)); + def_map.functions.get(&src).copied() + } + + /// Finds the `ModuleId` associated with the specified `file` + fn file_to_def(&self, file_id: FileId) -> Option { + let source_root_id = self.db.file_source_root(file_id); + let packages = self.db.packages(); + let result = packages + .iter() + .filter(|package_id| packages[*package_id].source_root == source_root_id) + .find_map(|package_id| { + let module_tree = self.db.module_tree(package_id); + let module_id = module_tree.module_for_file(file_id)?; + Some(ModuleId { + package: package_id, + local_id: module_id, + }) + }); + result + } +} + +/// A container that holds other items. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub(crate) enum SourceToDefContainer { + DefWithBodyId(DefWithBodyId), + ModuleId(ModuleId), +} + +impl From for SourceToDefContainer { + fn from(id: DefWithBodyId) -> Self { + SourceToDefContainer::DefWithBodyId(id) + } +} + +impl From for SourceToDefContainer { + fn from(id: ModuleId) -> Self { + SourceToDefContainer::ModuleId(id) + } +} + +impl SourceToDefContainer { + fn source_to_def_map(self, db: &dyn HirDatabase) -> SourceToDefMap { + match self { + SourceToDefContainer::DefWithBodyId(id) => id.source_to_def_map(db), + SourceToDefContainer::ModuleId(id) => id.source_to_def_map(db), + } + } +} + +/// A trait to construct a `SourceToDefMap` from a definition like a module. +trait SourceToDef { + /// Returns all definitions in `self`. + fn source_to_def_map(&self, db: &dyn HirDatabase) -> SourceToDefMap; +} + +impl SourceToDef for DefWithBodyId { + fn source_to_def_map(&self, _db: &dyn HirDatabase) -> SourceToDefMap { + // TODO: bodies dont yet contain items themselves + SourceToDefMap::default() + } +} + +impl SourceToDef for ModuleId { + fn source_to_def_map(&self, db: &dyn HirDatabase) -> SourceToDefMap { + let package_defs = db.package_defs(self.package); + let module_scope = &package_defs[self.local_id]; + module_scope.source_to_def_map(db) + } +} + +impl SourceToDef for ItemScope { + fn source_to_def_map(&self, db: &dyn HirDatabase) -> SourceToDefMap { + let mut result = SourceToDefMap::default(); + self.declarations() + .for_each(|item| add_module_def(db.upcast(), &mut result, item)); + return result; + + fn add_module_def(db: &dyn DefDatabase, map: &mut SourceToDefMap, item: ItemDefinitionId) { + match item { + ItemDefinitionId::FunctionId(id) => { + let src = id.lookup(db).source(db); + map.functions.insert(src, id); + } + ItemDefinitionId::StructId(id) => { + let src = id.lookup(db).source(db); + map.structs.insert(src, id); + } + ItemDefinitionId::TypeAliasId(id) => { + let src = id.lookup(db).source(db); + map.type_aliases.insert(src, id); + } + _ => {} + } + } + } +} + +/// Holds conversion from source location to definitions in the HIR. +#[derive(Default)] +pub(crate) struct SourceToDefMap { + functions: FxHashMap, FunctionId>, + structs: FxHashMap, StructId>, + type_aliases: FxHashMap, TypeAliasId>, +} diff --git a/crates/mun_hir/src/source_analyzer.rs b/crates/mun_hir/src/source_analyzer.rs new file mode 100644 index 000000000..1ce3a51ae --- /dev/null +++ b/crates/mun_hir/src/source_analyzer.rs @@ -0,0 +1,168 @@ +use crate::{ + expr::scope::LocalScopeId, expr::BodySourceMap, ids::DefWithBodyId, resolver_for_scope, Body, + ExprId, ExprScopes, FileId, HirDatabase, InFile, InferenceResult, Resolver, Ty, +}; +use mun_syntax::{ast, AstNode, SyntaxNode, TextRange, TextSize}; +use std::sync::Arc; + +/// A `SourceAnalyzer` is a wrapper which exposes the HIR API in terms of the original source file. +/// It's useful to query things from the syntax. +pub(crate) struct SourceAnalyzer { + /// The file for which this analyzer was constructed + pub(crate) file_id: FileId, + + /// The resolver used to resolve names + pub(crate) resolver: Resolver, + + /// Optional body to res + body: Option>, + body_source_map: Option>, + infer: Option>, + scopes: Option>, +} + +impl SourceAnalyzer { + /// Constructs a new `SourceAnalyzer` for the given `def` and with an optional offset in the + /// source file. + pub(crate) fn new_for_body( + db: &dyn HirDatabase, + def: DefWithBodyId, + node: InFile<&SyntaxNode>, + offset: Option, + ) -> Self { + let (body, source_map) = db.body_with_source_map(def); + let scopes = db.expr_scopes(def); + let scope = match offset { + None => scope_for(&scopes, &source_map, node), + Some(offset) => scope_for_offset(db, &scopes, &source_map, node.with_value(offset)), + }; + let resolver = resolver_for_scope(db.upcast(), def, scope); + SourceAnalyzer { + resolver, + body: Some(body), + body_source_map: Some(source_map), + infer: Some(db.infer(def)), + scopes: Some(scopes), + file_id: node.file_id, + } + } + + /// Constructs a new `SourceAnalyzer` from the specified `resolver`. + pub(crate) fn new_for_resolver( + resolver: Resolver, + node: InFile<&SyntaxNode>, + ) -> SourceAnalyzer { + SourceAnalyzer { + resolver, + body: None, + body_source_map: None, + infer: None, + scopes: None, + file_id: node.file_id, + } + } + + /// Returns the type of the specified expression + pub(crate) fn type_of_expr(&self, db: &dyn HirDatabase, expr: &ast::Expr) -> Option { + let expr_id = self.expr_id(db, expr)?; + Some(self.infer.as_ref()?[expr_id].clone()) + } + + /// Returns the expression id of the given expression or None if it could not be found. + fn expr_id(&self, _db: &dyn HirDatabase, expr: &ast::Expr) -> Option { + let sm = self.body_source_map.as_ref()?; + sm.node_expr(expr) + } +} + +/// Returns the id of the scope that is active at the location of `node`. +fn scope_for( + scopes: &ExprScopes, + source_map: &BodySourceMap, + node: InFile<&SyntaxNode>, +) -> Option { + node.value + .ancestors() + .filter_map(ast::Expr::cast) + .filter_map(|it| source_map.node_expr(&it)) + .find_map(|it| scopes.scope_for(it)) +} + +/// Computes the id of the scope that is closest to the given `offset`. +fn scope_for_offset( + db: &dyn HirDatabase, + scopes: &ExprScopes, + source_map: &BodySourceMap, + offset: InFile, +) -> Option { + // Get all scopes and their ranges + let scopes_and_ranges = scopes.scope_by_expr().iter().filter_map(|(id, scope)| { + let source = source_map.expr_syntax(*id)?; + // FIXME: correctly handle macro expansion + if source.file_id != offset.file_id { + return None; + } + let root = source.file_syntax(db.upcast()); + let node = source + .value + .either(|ptr| ptr.syntax_node_ptr(), |ptr| ptr.syntax_node_ptr()); + Some((node.to_node(&root).text_range(), scope)) + }); + + let smallest_scope_containing_offset = scopes_and_ranges.min_by_key(|(expr_range, _scope)| { + ( + !(expr_range.start() <= offset.value && offset.value <= expr_range.end()), + expr_range.len(), + ) + }); + + smallest_scope_containing_offset.map(|(expr_range, scope)| { + adjust(db, scopes, source_map, expr_range, offset).unwrap_or(*scope) + }) +} + +/// During completion the cursor may be outside of any expression. Given the range of the containing +/// scope, finds the scope that is most likely the scope that the user is requesting. +fn adjust( + db: &dyn HirDatabase, + scopes: &ExprScopes, + source_map: &BodySourceMap, + expr_range: TextRange, + offset: InFile, +) -> Option { + let child_scopes = scopes + .scope_by_expr() + .iter() + .filter_map(|(id, scope)| { + let source = source_map.expr_syntax(*id)?; + if source.file_id != offset.file_id { + return None; + } + let root = source.file_syntax(db.upcast()); + let node = source + .value + .either(|ptr| ptr.syntax_node_ptr(), |ptr| ptr.syntax_node_ptr()); + Some((node.to_node(&root).text_range(), scope)) + }) + .filter(|&(range, _)| { + // The start of the scope is before the offset + range.start() <= offset.value + // The range is contained inside the expression scope + && expr_range.contains_range(range) + // The range is not the expression scope itself + && range != expr_range + }); + + child_scopes + .into_iter() + .max_by(|&(r1, _), &(r2, _)| { + if r1.contains_range(r2) { + std::cmp::Ordering::Greater + } else if r2.contains_range(r1) { + std::cmp::Ordering::Less + } else { + r1.start().cmp(&r2.start()) + } + }) + .map(|(_ptr, scope)| *scope) +} diff --git a/crates/mun_hir/src/ty.rs b/crates/mun_hir/src/ty.rs index aa2d17ba4..f553e615e 100644 --- a/crates/mun_hir/src/ty.rs +++ b/crates/mun_hir/src/ty.rs @@ -184,6 +184,11 @@ impl Ty { pub fn is_known(&self) -> bool { *self == Ty::Unknown } + + /// Returns true if this instance is of an unknown type. + pub fn is_unknown(&self) -> bool { + matches!(self, Ty::Unknown) + } } /// A list of substitutions for generic parameters. diff --git a/crates/mun_language_server/Cargo.toml b/crates/mun_language_server/Cargo.toml index e79adebb4..a07587e53 100644 --- a/crates/mun_language_server/Cargo.toml +++ b/crates/mun_language_server/Cargo.toml @@ -35,8 +35,10 @@ mun_diagnostics = { version = "=0.1.0", path = "../mun_diagnostics" } crossbeam-channel = "0.5.0" parking_lot="0.11.1" paths = {path="../mun_paths", package="mun_paths"} +ra_ap_text_edit="0.0.35" [dev-dependencies] tempdir = "0.3.7" mun_test = { path = "../mun_test"} insta = "0.16" +itertools = "0.10.0" diff --git a/crates/mun_language_server/src/analysis.rs b/crates/mun_language_server/src/analysis.rs index b51c0e827..6f2f3aa01 100644 --- a/crates/mun_language_server/src/analysis.rs +++ b/crates/mun_language_server/src/analysis.rs @@ -1,8 +1,9 @@ use crate::{ - cancelation::Canceled, change::AnalysisChange, db::AnalysisDatabase, diagnostics, - diagnostics::Diagnostic, file_structure, + cancelation::Canceled, change::AnalysisChange, completion, db::AnalysisDatabase, diagnostics, + diagnostics::Diagnostic, file_structure, FilePosition, }; use hir::{line_index::LineIndex, AstDatabase, SourceDatabase}; +use mun_syntax::SourceFile; use salsa::{ParallelDatabase, Snapshot}; use std::sync::Arc; @@ -11,17 +12,12 @@ pub type Cancelable = Result; /// The `Analysis` struct is the basis of all language server operations. It maintains the current /// state of the source. +#[derive(Default)] pub struct Analysis { db: AnalysisDatabase, } impl Analysis { - pub fn new() -> Self { - Analysis { - db: AnalysisDatabase::new(), - } - } - /// Applies the given changes to the state. If there are outstanding `AnalysisSnapshot`s they /// will be canceled. pub fn apply_change(&mut self, change: AnalysisChange) { @@ -53,6 +49,11 @@ pub struct AnalysisSnapshot { } impl AnalysisSnapshot { + /// Returns the syntax tree of the file. + pub fn parse(&self, file_id: hir::FileId) -> Cancelable { + self.with_db(|db| db.parse(file_id).tree()) + } + /// Computes the set of diagnostics for the given file. pub fn diagnostics(&self, file_id: hir::FileId) -> Cancelable> { self.with_db(|db| diagnostics::diagnostics(db, file_id)) @@ -80,6 +81,14 @@ impl AnalysisSnapshot { self.with_db(|db| file_structure::file_structure(&db.parse(file_id).tree())) } + /// Computes completions at the given position + pub fn completions( + &self, + position: FilePosition, + ) -> Cancelable>> { + self.with_db(|db| completion::completions(db, position).map(Into::into)) + } + /// Performs an operation on that may be Canceled. fn with_db T + std::panic::UnwindSafe, T>( &self, diff --git a/crates/mun_language_server/src/capabilities.rs b/crates/mun_language_server/src/capabilities.rs index 2dcc86119..1bae50ab2 100644 --- a/crates/mun_language_server/src/capabilities.rs +++ b/crates/mun_language_server/src/capabilities.rs @@ -1,5 +1,6 @@ use lsp_types::{ - ClientCapabilities, OneOf, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, + ClientCapabilities, CompletionOptions, OneOf, ServerCapabilities, TextDocumentSyncCapability, + TextDocumentSyncKind, WorkDoneProgressOptions, }; /// Returns the capabilities of this LSP server implementation given the capabilities of the client. @@ -9,6 +10,13 @@ pub fn server_capabilities(_client_caps: &ClientCapabilities) -> ServerCapabilit TextDocumentSyncKind::Incremental, )), document_symbol_provider: Some(OneOf::Left(true)), + completion_provider: Some(CompletionOptions { + resolve_provider: None, + trigger_characters: Some(vec![String::from(":"), String::from(".")]), + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: None, + }, + }), ..Default::default() } } diff --git a/crates/mun_language_server/src/change_fixture.rs b/crates/mun_language_server/src/change_fixture.rs new file mode 100644 index 000000000..82e382338 --- /dev/null +++ b/crates/mun_language_server/src/change_fixture.rs @@ -0,0 +1,103 @@ +use crate::change::AnalysisChange; +use hir::fixture::Fixture; +use mun_syntax::{TextRange, TextSize}; +use std::sync::Arc; + +pub const CURSOR_MARKER: &str = "$0"; + +/// A `ChangeFixture` is an extended [`Fixture`] that can be used to construct an entire +/// [`AnalysisDatabase`] with. It can also optionally contain a cursor indicated by `$0`. +pub struct ChangeFixture { + pub file_position: Option<(hir::FileId, RangeOrOffset)>, + pub files: Vec, + pub change: AnalysisChange, +} + +impl ChangeFixture { + pub fn parse(fixture: &str) -> ChangeFixture { + let fixture = Fixture::parse(fixture); + + let mut change = AnalysisChange::default(); + let mut source_root = hir::SourceRoot::default(); + let mut package_set = hir::PackageSet::default(); + + let mut file_id = hir::FileId(0); + let mut file_position = None; + let mut files = Vec::new(); + + for entry in fixture { + let text = if entry.text.contains(CURSOR_MARKER) { + let (range_or_offset, text) = extract_range_or_offset(&entry.text); + assert!( + file_position.is_none(), + "cannot have multiple cursor markers" + ); + file_position = Some((file_id, range_or_offset)); + text.to_string() + } else { + entry.text.clone() + }; + + change.change_file(file_id, Some(Arc::from(text))); + source_root.insert_file(file_id, entry.relative_path); + files.push(file_id); + file_id.0 += 1; + } + + package_set.add_package(hir::SourceRootId(0)); + + change.set_roots(vec![source_root]); + change.set_packages(package_set); + + ChangeFixture { + file_position, + files, + change, + } + } +} + +/// Returns the offset of the first occurrence of `$0` marker and the copy of `text` without the +/// marker. +fn try_extract_offset(text: &str) -> Option<(TextSize, String)> { + let cursor_pos = text.find(CURSOR_MARKER)?; + let mut new_text = String::with_capacity(text.len() - CURSOR_MARKER.len()); + new_text.push_str(&text[..cursor_pos]); + new_text.push_str(&text[cursor_pos + CURSOR_MARKER.len()..]); + let cursor_pos = TextSize::from(cursor_pos as u32); + Some((cursor_pos, new_text)) +} + +/// Returns `TextRange` between the first two markers `$0...$0` and the copy +/// of `text` without both of these markers. +fn try_extract_range(text: &str) -> Option<(TextRange, String)> { + let (start, text) = try_extract_offset(text)?; + let (end, text) = try_extract_offset(&text)?; + Some((TextRange::new(start, end), text)) +} + +#[derive(Clone, Copy)] +pub enum RangeOrOffset { + Range(TextRange), + Offset(TextSize), +} + +impl From for TextRange { + fn from(selection: RangeOrOffset) -> Self { + match selection { + RangeOrOffset::Range(it) => it, + RangeOrOffset::Offset(it) => TextRange::empty(it), + } + } +} + +/// Extracts `TextRange` or `TextSize` depending on the amount of `$0` markers found in `text`. +pub fn extract_range_or_offset(text: &str) -> (RangeOrOffset, String) { + if let Some((range, text)) = try_extract_range(text) { + (RangeOrOffset::Range(range), text) + } else if let Some((offset, text)) = try_extract_offset(text) { + (RangeOrOffset::Offset(offset), text) + } else { + panic!("text should contain a cursor marker") + } +} diff --git a/crates/mun_language_server/src/completion.rs b/crates/mun_language_server/src/completion.rs new file mode 100644 index 000000000..09ec31f07 --- /dev/null +++ b/crates/mun_language_server/src/completion.rs @@ -0,0 +1,78 @@ +//! A module that provides completions based on the position of the cursor (indicated as `$0` in the +//! documentation). +//! The [`completions`] function is the main entry point for computing the completions. + +mod context; +mod dot; +mod item; +mod render; +mod unqualified_path; + +#[cfg(test)] +mod test_utils; + +use crate::{ + completion::render::{render_field, render_resolution, RenderContext}, + db::AnalysisDatabase, + FilePosition, +}; +use context::CompletionContext; +use hir::semantics::ScopeDef; +pub use item::{CompletionItem, CompletionItemKind, CompletionKind}; + +/// This is the main entry point for computing completions. This is a two step process. +/// +/// The first step is to determine the context of where the completion is requested. This +/// information is captured in the [`CompletionContext`]. The context captures things like which +/// type of syntax node is before the cursor or the current scope. +/// +/// Second is to compute a set of completions based on the previously computed context. We provide +/// several methods for computing completions based on different syntax contexts. For instance when +/// writing `foo.$0` you want to complete the fields of `foo` and don't want the local variables of +/// the active scope. +pub(crate) fn completions(db: &AnalysisDatabase, position: FilePosition) -> Option { + let context = CompletionContext::new(db, position)?; + + let mut result = Completions::default(); + unqualified_path::complete_unqualified_path(&mut result, &context); + dot::complete_dot(&mut result, &context); + Some(result) +} + +/// Represents an in-progress set of completions being built. Use the `add_..` functions to quickly +/// add completion items. +#[derive(Debug, Default)] +pub(crate) struct Completions { + buf: Vec, +} + +impl Into> for Completions { + fn into(self) -> Vec { + self.buf + } +} + +impl Completions { + /// Adds a raw `CompletionItem` + fn add(&mut self, item: CompletionItem) { + self.buf.push(item) + } + + /// Adds a completion item for a resolved name + fn add_resolution( + &mut self, + ctx: &CompletionContext, + local_name: String, + resolution: &ScopeDef, + ) { + if let Some(item) = render_resolution(RenderContext::new(ctx), local_name, resolution) { + self.add(item); + } + } + + /// Adds a completion item for a field + fn add_field(&mut self, ctx: &CompletionContext, field: hir::Field) { + let item = render_field(RenderContext::new(ctx), field); + self.add(item); + } +} diff --git a/crates/mun_language_server/src/completion/context.rs b/crates/mun_language_server/src/completion/context.rs new file mode 100644 index 000000000..b414eb4d0 --- /dev/null +++ b/crates/mun_language_server/src/completion/context.rs @@ -0,0 +1,138 @@ +use crate::db::AnalysisDatabase; +use crate::FilePosition; +use hir::semantics::{Semantics, SemanticsScope}; +use hir::AstDatabase; +use mun_syntax::{ast, utils::find_node_at_offset, AstNode, SyntaxNode, TextRange, TextSize}; +use ra_ap_text_edit::Indel; + +/// A `CompletionContext` is created to figure out where exactly the cursor is. +pub(super) struct CompletionContext<'a> { + pub sema: Semantics<'a>, + pub scope: SemanticsScope<'a>, + pub db: &'a AnalysisDatabase, + pub position: FilePosition, + + /// True if the context is currently at a trivial path. + pub is_trivial_path: bool, + + /// True if the context is currently on a parameter + pub is_param: bool, + + /// True if we're at an ast::PathType + pub is_path_type: bool, + + /// The receiver if this is a field or method access, i.e. writing something.$0 + pub dot_receiver: Option, +} + +impl<'a> CompletionContext<'a> { + /// Tries to construct a new `CompletionContext` with the given database and file position. + pub fn new(db: &'a AnalysisDatabase, position: FilePosition) -> Option { + let sema = Semantics::new(db); + + let original_file = sema.parse(position.file_id); + + // Insert a fake identifier to get a valid parse tree. This tree will be used to determine + // context. The actual original_file will be used for completion. + let file_with_fake_ident = { + let parse = db.parse(position.file_id); + let edit = Indel::insert(position.offset, String::from("intellijRulezz")); + parse.reparse(&edit).tree() + }; + + // Get the current token + let token = original_file + .syntax() + .token_at_offset(position.offset) + .left_biased()?; + + let scope = sema.scope_at_offset(&token.parent(), position.offset); + + let mut context = Self { + sema, + scope, + db, + position, + is_trivial_path: false, + is_param: false, + is_path_type: false, + dot_receiver: None, + }; + + context.fill( + &original_file.syntax().clone(), + file_with_fake_ident.syntax().clone(), + position.offset, + ); + + Some(context) + } + + /// Examine the AST and determine what the context is at the given offset. + fn fill( + &mut self, + original_file: &SyntaxNode, + file_with_fake_ident: SyntaxNode, + offset: TextSize, + ) { + // First, let's try to complete a reference to some declaration. + if let Some(name_ref) = find_node_at_offset::(&file_with_fake_ident, offset) { + if is_node::(name_ref.syntax()) { + self.is_param = true; + return; + } + + self.classify_name_ref(original_file, name_ref); + } + } + + /// Classifies an `ast::NameRef` + fn classify_name_ref(&mut self, original_file: &SyntaxNode, name_ref: ast::NameRef) { + let parent = match name_ref.syntax().parent() { + Some(it) => it, + None => return, + }; + + // Complete references to declarations + if let Some(segment) = ast::PathSegment::cast(parent.clone()) { + let path = segment.parent_path(); + + self.is_path_type = path + .syntax() + .parent() + .and_then(ast::PathType::cast) + .is_some(); + + if let Some(segment) = path.segment() { + if segment.has_colon_colon() { + return; + } + } + + self.is_trivial_path = true; + } + + // Complete field expressions + if let Some(field_expr) = ast::FieldExpr::cast(parent) { + // The receiver comes before the point of insertion of the fake + // ident, so it should have the same range in the non-modified file + self.dot_receiver = field_expr + .expr() + .map(|e| e.syntax().text_range()) + .and_then(|r| find_node_with_range(original_file, r)); + } + } +} + +/// Returns true if the given `node` or one if its parents is of the specified type. +fn is_node(node: &SyntaxNode) -> bool { + match node.ancestors().find_map(N::cast) { + None => false, + Some(n) => n.syntax().text_range() == node.text_range(), + } +} + +/// Returns a node that covers the specified range. +fn find_node_with_range(syntax: &SyntaxNode, range: TextRange) -> Option { + syntax.covering_element(range).ancestors().find_map(N::cast) +} diff --git a/crates/mun_language_server/src/completion/dot.rs b/crates/mun_language_server/src/completion/dot.rs new file mode 100644 index 000000000..68521efee --- /dev/null +++ b/crates/mun_language_server/src/completion/dot.rs @@ -0,0 +1,93 @@ +use super::{CompletionContext, Completions}; +use hir::Upcast; + +/// Complete dot accesses, i.e. fields. Adds `CompletionItems` to `result. +pub(super) fn complete_dot(result: &mut Completions, ctx: &CompletionContext) { + // Get the expression that we want to get the fields of + let dot_receiver = match &ctx.dot_receiver { + Some(expr) => expr, + _ => return, + }; + + // Figure out the type of the expression + let receiver_ty = match ctx.sema.type_of_expr(&dot_receiver) { + Some(ty) => ty, + _ => return, + }; + + // Get all the fields of the expression + if let Some(strukt) = receiver_ty.as_struct() { + for field in strukt.fields(ctx.db.upcast()) { + result.add_field(ctx, field) + } + }; +} + +#[cfg(test)] +mod tests { + use crate::completion::{test_utils::completion_string, CompletionKind}; + + #[test] + fn test_struct_fields() { + insta::assert_snapshot!(completion_string( + r#" + struct FooBar { + foo: i32, + bar: i32, + }; + + fn foo() { + let bar = FooBar { foo: 0, bar: 0 }; + bar.$0 + } + "#, + Some(CompletionKind::Reference) + )) + } + + #[test] + fn test_tuple_struct() { + insta::assert_snapshot!(completion_string( + r#" + struct FooBar(i32, i32) + + fn foo() { + let bar = FooBar(0,0); + bar.$0 + } + "#, + Some(CompletionKind::Reference) + )) + } + + #[test] + fn test_nested_struct() { + insta::assert_snapshot!(completion_string( + r#" + struct Foo { baz: i32 } + struct Bar(Foo) + + fn foo() { + let bar = Bar(Foo{baz:0}); + bar.0.$0 + } + "#, + Some(CompletionKind::Reference) + )) + } + + #[test] + fn test_incomplete_struct() { + insta::assert_snapshot!(completion_string( + r#" + struct Foo { bar: i32 } + + fn foo() { + let bar = Foo; + bar.$0 + } + "#, + Some(CompletionKind::Reference) + )) + } +} diff --git a/crates/mun_language_server/src/completion/item.rs b/crates/mun_language_server/src/completion/item.rs new file mode 100644 index 000000000..7739e8d71 --- /dev/null +++ b/crates/mun_language_server/src/completion/item.rs @@ -0,0 +1,114 @@ +use crate::SymbolKind; + +/// A `CompletionItem` describes a single completion variant in an editor. +#[derive(Clone, Debug)] +pub struct CompletionItem { + /// Used for tests to filter certain type of completions + #[allow(unused)] + pub completion_kind: CompletionKind, + + /// Label in the completion pop up which identifies completion. + pub label: String, + + /// The type of completion + pub kind: Option, + + /// Additional info to show in the UI pop up. + pub detail: Option, +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum CompletionKind { + /// Your usual "complete all valid identifiers". + Reference, + BuiltinType, +} + +/// Type of completion used to provide hints to the user. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(unused)] +pub enum CompletionItemKind { + SymbolKind(SymbolKind), + Attribute, + Binding, + BuiltinType, + Keyword, + Method, + Snippet, + UnresolvedReference, +} + +impl CompletionItemKind { + /// Returns a tag that describes the type of item that was completed. This is only used in + /// tests, to be able to distinguish between items with the same name. + #[cfg(test)] + pub(crate) fn tag(&self) -> &'static str { + match self { + CompletionItemKind::SymbolKind(kind) => match kind { + SymbolKind::Field => "fd", + SymbolKind::Function => "fn", + SymbolKind::Local => "lc", + SymbolKind::Module => "md", + SymbolKind::Struct => "st", + SymbolKind::TypeAlias => "ta", + }, + CompletionItemKind::Attribute => "at", + CompletionItemKind::Binding => "bn", + CompletionItemKind::BuiltinType => "bt", + CompletionItemKind::Keyword => "kw", + CompletionItemKind::Method => "me", + CompletionItemKind::Snippet => "sn", + CompletionItemKind::UnresolvedReference => "??", + } + } +} + +impl From for CompletionItemKind { + fn from(kind: SymbolKind) -> Self { + CompletionItemKind::SymbolKind(kind) + } +} + +impl CompletionItem { + /// Constructs a [`Builder`] to build a `CompletionItem` with + pub fn builder(kind: CompletionKind, label: impl Into) -> Builder { + Builder { + label: label.into(), + kind: None, + completion_kind: kind, + detail: None, + } + } +} + +/// A builder for a `CompletionItem`. Constructed by calling [`CompletionItem::builder`]. +pub struct Builder { + label: String, + completion_kind: CompletionKind, + kind: Option, + detail: Option, +} + +impl Builder { + /// Completes building the `CompletionItem` and returns it + pub fn finish(self) -> CompletionItem { + CompletionItem { + completion_kind: self.completion_kind, + label: self.label, + kind: self.kind, + detail: self.detail, + } + } + + /// Sets the type of the completion + pub fn kind(mut self, kind: impl Into) -> Builder { + self.kind = Some(kind.into()); + self + } + + /// Set the details of the completion item + pub fn detail(mut self, detail: impl Into) -> Builder { + self.detail = Some(detail.into()); + self + } +} diff --git a/crates/mun_language_server/src/completion/render.rs b/crates/mun_language_server/src/completion/render.rs new file mode 100644 index 000000000..6ce84ef92 --- /dev/null +++ b/crates/mun_language_server/src/completion/render.rs @@ -0,0 +1,107 @@ +mod function; + +use super::{CompletionContext, CompletionItem, CompletionItemKind}; +use crate::completion::CompletionKind; +use crate::{db::AnalysisDatabase, SymbolKind}; +use function::FunctionRender; +use hir::{semantics::ScopeDef, HirDisplay}; + +pub(super) fn render_field(ctx: RenderContext, field: hir::Field) -> CompletionItem { + Render::new(ctx).render_field(field) +} + +pub(super) fn render_fn( + ctx: RenderContext, + local_name: Option, + func: hir::Function, +) -> Option { + Some(FunctionRender::new(ctx, local_name, func)?.render()) +} + +pub(super) fn render_resolution( + ctx: RenderContext, + local_name: String, + resolution: &ScopeDef, +) -> Option { + Render::new(ctx).render_resolution(local_name, resolution) +} + +/// Interface for data and methods required for items rendering. +pub(super) struct RenderContext<'a> { + completion: &'a CompletionContext<'a>, +} + +impl<'a> RenderContext<'a> { + pub(super) fn new(completion: &'a CompletionContext<'a>) -> RenderContext<'a> { + RenderContext { completion } + } + + pub(super) fn db(&self) -> &'a AnalysisDatabase { + self.completion.db + } +} + +/// Generic renderer for completion items. +struct Render<'a> { + ctx: RenderContext<'a>, +} + +impl<'a> Render<'a> { + fn new(ctx: RenderContext<'a>) -> Render<'a> { + Render { ctx } + } + + /// Constructs a `CompletionItem` for a resolved name. + fn render_resolution( + self, + local_name: String, + resolution: &ScopeDef, + ) -> Option { + use hir::ModuleDef::*; + + let completion_kind = match resolution { + ScopeDef::ModuleDef(PrimitiveType(..)) => CompletionKind::BuiltinType, + _ => CompletionKind::Reference, + }; + + let kind = match resolution { + ScopeDef::ModuleDef(Module(_)) => CompletionItemKind::SymbolKind(SymbolKind::Module), + ScopeDef::ModuleDef(Function(func)) => { + return render_fn(self.ctx, Some(local_name), *func) + } + ScopeDef::ModuleDef(PrimitiveType(_)) => CompletionItemKind::BuiltinType, + ScopeDef::ModuleDef(Struct(_)) => CompletionItemKind::SymbolKind(SymbolKind::Struct), + ScopeDef::ModuleDef(TypeAlias(_)) => { + CompletionItemKind::SymbolKind(SymbolKind::TypeAlias) + } + ScopeDef::Local(_) => CompletionItemKind::SymbolKind(SymbolKind::Local), + ScopeDef::Unknown => { + let item = CompletionItem::builder(CompletionKind::Reference, local_name) + .kind(CompletionItemKind::UnresolvedReference) + .finish(); + return Some(item); + } + }; + + let mut item = CompletionItem::builder(completion_kind, local_name); + + // Add the type for locals + if let ScopeDef::Local(local) = resolution { + let ty = local.ty(self.ctx.db()); + if !ty.is_unknown() { + item = item.detail(ty.display(self.ctx.db()).to_string()); + } + }; + + Some(item.kind(kind).finish()) + } + + /// Constructs a `CompletionItem` for a field. + fn render_field(&mut self, field: hir::Field) -> CompletionItem { + let name = field.name(self.ctx.db()); + CompletionItem::builder(CompletionKind::Reference, name.to_string()) + .kind(SymbolKind::Field) + .detail(field.ty(self.ctx.db()).display(self.ctx.db()).to_string()) + .finish() + } +} diff --git a/crates/mun_language_server/src/completion/render/function.rs b/crates/mun_language_server/src/completion/render/function.rs new file mode 100644 index 000000000..e4911679e --- /dev/null +++ b/crates/mun_language_server/src/completion/render/function.rs @@ -0,0 +1,37 @@ +use super::{CompletionItem, CompletionKind, RenderContext}; +use crate::SymbolKind; +use hir::HirDisplay; + +/// Similar to [`Render<'a>`] but used to render a completion item for a function +pub(super) struct FunctionRender<'a> { + ctx: RenderContext<'a>, + name: String, + func: hir::Function, +} + +impl<'a> FunctionRender<'a> { + /// Constructs a new `FunctionRender` + pub fn new( + ctx: RenderContext<'a>, + local_name: Option, + func: hir::Function, + ) -> Option> { + let name = local_name.unwrap_or_else(|| func.name(ctx.db()).to_string()); + + Some(Self { ctx, name, func }) + } + + /// Constructs a [`CompletionItem`] for the wrapped function. + pub fn render(self) -> CompletionItem { + CompletionItem::builder(CompletionKind::Reference, self.name.clone()) + .kind(SymbolKind::Function) + .detail(self.detail()) + .finish() + } + + /// Returns the detail text to add to the completion. This currently returns `-> `. + fn detail(&self) -> String { + let ty = self.func.ret_type(self.ctx.db()); + format!("-> {}", ty.display(self.ctx.db())) + } +} diff --git a/crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__dot__tests__incomplete_struct.snap b/crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__dot__tests__incomplete_struct.snap new file mode 100644 index 000000000..f1ebbfc82 --- /dev/null +++ b/crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__dot__tests__incomplete_struct.snap @@ -0,0 +1,5 @@ +--- +source: crates/mun_language_server/src/completion/dot.rs +expression: "completion_string(r#\"\n struct Foo { bar: i32 }\n \n fn foo() {\n let bar = Foo;\n bar.$0\n }\n \"#,\n Some(CompletionKind::Reference))" +--- +fd bar i32 diff --git a/crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__dot__tests__nested_struct.snap b/crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__dot__tests__nested_struct.snap new file mode 100644 index 000000000..f34a7b08f --- /dev/null +++ b/crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__dot__tests__nested_struct.snap @@ -0,0 +1,5 @@ +--- +source: crates/mun_language_server/src/completion/dot.rs +expression: "completion_string(r#\"\n struct Foo { baz: i32 }\n struct Bar(Foo)\n \n fn foo() {\n let bar = Bar(Foo{baz:0});\n bar.0.$0\n }\n \"#,\n Some(CompletionKind::Reference))" +--- +fd baz i32 diff --git a/crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__dot__tests__struct_fields.snap b/crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__dot__tests__struct_fields.snap new file mode 100644 index 000000000..902967b3e --- /dev/null +++ b/crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__dot__tests__struct_fields.snap @@ -0,0 +1,6 @@ +--- +source: crates/mun_language_server/src/completion/dot.rs +expression: "completion_string(r#\"\n struct FooBar {\n foo: i32,\n bar: i32,\n };\n \n fn foo() {\n let bar = FooBar { foo: 0, bar: 0 };\n bar.$0\n }\n \"#,\n Some(CompletionKind::Reference))" +--- +fd foo i32 +fd bar i32 diff --git a/crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__dot__tests__tuple_struct.snap b/crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__dot__tests__tuple_struct.snap new file mode 100644 index 000000000..248b2dd3c --- /dev/null +++ b/crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__dot__tests__tuple_struct.snap @@ -0,0 +1,6 @@ +--- +source: crates/mun_language_server/src/completion/dot.rs +expression: "completion_string(r#\"\n struct FooBar(i32, i32)\n \n fn foo() {\n let bar = FooBar(0,0);\n bar.$0\n }\n \"#,\n Some(CompletionKind::Reference))" +--- +fd 0 i32 +fd 1 i32 diff --git a/crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__unqualified_path__tests__local_scope.snap b/crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__unqualified_path__tests__local_scope.snap new file mode 100644 index 000000000..02023b8ed --- /dev/null +++ b/crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__unqualified_path__tests__local_scope.snap @@ -0,0 +1,7 @@ +--- +source: crates/mun_language_server/src/completion/unqualified_path.rs +expression: "completion_string(r#\"\n fn foo() {\n let bar = 0;\n let foo_bar = 0;\n f$0\n }\n \"#,\n Some(CompletionKind::Reference))" +--- +lc foo_bar i32 +lc bar i32 +fn foo -> nothing diff --git a/crates/mun_language_server/src/completion/test_utils.rs b/crates/mun_language_server/src/completion/test_utils.rs new file mode 100644 index 000000000..649e49a54 --- /dev/null +++ b/crates/mun_language_server/src/completion/test_utils.rs @@ -0,0 +1,64 @@ +use crate::{ + change_fixture::{ChangeFixture, RangeOrOffset}, + completion::{CompletionItem, CompletionKind}, + db::AnalysisDatabase, + FilePosition, +}; +use itertools::Itertools; + +/// Creates an analysis database from a multi-file fixture and a position marked with `$0`. +pub(crate) fn position(fixture: &str) -> (AnalysisDatabase, FilePosition) { + let change_fixture = ChangeFixture::parse(fixture); + let mut database = AnalysisDatabase::default(); + database.apply_change(change_fixture.change); + let (file_id, range_or_offset) = change_fixture + .file_position + .expect("expected a marker ($0)"); + let offset = match range_or_offset { + RangeOrOffset::Range(_) => panic!(), + RangeOrOffset::Offset(it) => it, + }; + (database, FilePosition { file_id, offset }) +} + +/// Creates a list of completions for the specified code. The code must contain a cursor in the text +/// indicated by `$0` +pub(crate) fn completion_list( + code: &str, + filter_kind: Option, +) -> Vec { + let (db, position) = position(code); + let completions = super::completions(&db, position).unwrap(); + if let Some(filter_kind) = filter_kind { + completions + .buf + .into_iter() + .filter(|item| item.completion_kind == filter_kind) + .collect() + } else { + completions.into() + } +} + +/// Constructs a string representation of all the completions for the specified code. The code must +/// contain a cursor in the text indicated by `$0`. +pub(crate) fn completion_string(code: &str, filter_kind: Option) -> String { + let completions = completion_list(code, filter_kind); + let label_width = completions + .iter() + .map(|it| it.label.chars().count()) + .max() + .unwrap_or_default() + .min(16); + completions + .into_iter() + .map(|item| { + let mut result = format!("{} {}", item.kind.unwrap().tag(), &item.label); + if let Some(detail) = item.detail { + result = format!("{:width$} {}", result, detail, width = label_width + 3); + } + result + }) + .intersperse(String::from("\n")) + .collect() +} diff --git a/crates/mun_language_server/src/completion/unqualified_path.rs b/crates/mun_language_server/src/completion/unqualified_path.rs new file mode 100644 index 000000000..16f2e1104 --- /dev/null +++ b/crates/mun_language_server/src/completion/unqualified_path.rs @@ -0,0 +1,40 @@ +use super::{CompletionContext, Completions}; + +/// Adds completions to `result` for unqualified path. Unqualified paths are simple names which do +/// not refer to anything outside of the current scope: local function names, variables, etc. E.g.: +/// ```mun +/// fn foo() { +/// let foo_bar = 3; +/// foo_$0 +/// } +/// ``` +pub(super) fn complete_unqualified_path(result: &mut Completions, ctx: &CompletionContext) { + // Only complete trivial paths (e.g. foo, not ::foo) + if !ctx.is_trivial_path { + return; + } + + // Iterate over all items in the current scope and add completions for them + ctx.scope.visit_all_names(&mut |name, def| { + result.add_resolution(ctx, name.to_string(), &def); + }); +} + +#[cfg(test)] +mod tests { + use crate::{completion::test_utils::completion_string, completion::CompletionKind}; + + #[test] + fn test_local_scope() { + insta::assert_snapshot!(completion_string( + r#" + fn foo() { + let bar = 0; + let foo_bar = 0; + f$0 + } + "#, + Some(CompletionKind::Reference) + )) + } +} diff --git a/crates/mun_language_server/src/db.rs b/crates/mun_language_server/src/db.rs index f111c463b..714ac7aa4 100644 --- a/crates/mun_language_server/src/db.rs +++ b/crates/mun_language_server/src/db.rs @@ -28,17 +28,17 @@ pub(crate) struct AnalysisDatabase { storage: salsa::Storage, } -impl AnalysisDatabase { - pub fn new() -> Self { +impl Default for AnalysisDatabase { + fn default() -> Self { let mut db = AnalysisDatabase { storage: Default::default(), }; - db.set_target(Target::host_target().expect("could not determine host target spec")); - db } +} +impl AnalysisDatabase { /// Triggers a simple write on the database which will cancell all outstanding snapshots. pub fn request_cancelation(&mut self) { self.salsa_runtime_mut().synthetic_write(Durability::LOW); diff --git a/crates/mun_language_server/src/from_lsp.rs b/crates/mun_language_server/src/from_lsp.rs index 27b1cf6bc..4230854d2 100644 --- a/crates/mun_language_server/src/from_lsp.rs +++ b/crates/mun_language_server/src/from_lsp.rs @@ -2,6 +2,7 @@ //! Server Protocol to our own datatypes. use crate::state::LanguageServerSnapshot; +use crate::FilePosition; use hir::line_index::LineIndex; use lsp_types::Url; use mun_syntax::{TextRange, TextSize}; @@ -48,3 +49,14 @@ pub(crate) fn text_range(line_index: &LineIndex, range: lsp_types::Range) -> Tex let end = offset(line_index, range.end); TextRange::new(start, end) } + +/// Converts the specified lsp `text_document_position` to a `TextPosition`. +pub(crate) fn file_position( + snapshot: &LanguageServerSnapshot, + text_document_position: lsp_types::TextDocumentPositionParams, +) -> anyhow::Result { + let file_id = file_id(snapshot, &text_document_position.text_document.uri)?; + let line_index = snapshot.analysis.file_line_index(file_id)?; + let offset = offset(&*line_index, text_document_position.position); + Ok(FilePosition { file_id, offset }) +} diff --git a/crates/mun_language_server/src/handlers.rs b/crates/mun_language_server/src/handlers.rs index 1af37799c..080fee4d4 100644 --- a/crates/mun_language_server/src/handlers.rs +++ b/crates/mun_language_server/src/handlers.rs @@ -1,5 +1,6 @@ -use crate::{from_lsp, state::LanguageServerSnapshot, to_lsp}; -use lsp_types::DocumentSymbol; +use crate::{from_lsp, state::LanguageServerSnapshot, to_lsp, FilePosition}; +use lsp_types::{CompletionContext, CompletionItem, DocumentSymbol}; +use mun_syntax::{AstNode, TextSize}; /// Computes the document symbols for a specific document. Converts the LSP types to internal /// formats and calls [`LanguageServerSnapshot::file_structure`] to fetch the symbols in the @@ -32,6 +33,55 @@ pub(crate) fn handle_document_symbol( Ok(Some(build_hierarchy_from_flat_list(parents).into())) } +/// Computes completion items that should be presented to the user when the cursor is at a specific +/// location. +pub(crate) fn handle_completion( + snapshot: LanguageServerSnapshot, + params: lsp_types::CompletionParams, +) -> anyhow::Result> { + let position = from_lsp::file_position(&snapshot, params.text_document_position)?; + + // If the completion was triggered after a single colon there is nothing to do. We only want + // completion after a *double* colon (::) or after a dot (.). + if is_position_at_single_colon(&snapshot, position, params.context)? { + return Ok(None); + } + + // Get all completions from the analysis database + let items = match snapshot.analysis.completions(position)? { + None => return Ok(None), + Some(items) => items, + }; + + // Convert all the items to the LSP protocol type + let items: Vec = items.into_iter().map(to_lsp::completion_item).collect(); + + return Ok(Some(items.into())); + + /// Helper function to check if the given position is preceded by a single colon. + fn is_position_at_single_colon( + snapshot: &LanguageServerSnapshot, + position: FilePosition, + context: Option, + ) -> anyhow::Result { + if let Some(ctx) = context { + if ctx.trigger_character.unwrap_or_default() == ":" { + let source_file = snapshot.analysis.parse(position.file_id)?; + let syntax = source_file.syntax(); + let text = syntax.text(); + if let Some(next_char) = text.char_at(position.offset) { + let diff = TextSize::of(next_char) + TextSize::of(':'); + let prev_char = position.offset - diff; + if text.char_at(prev_char) != Some(':') { + return Ok(true); + } + } + } + } + Ok(false) + } +} + /// Constructs a hierarchy of DocumentSymbols for a list of symbols that specify which index is the /// parent of a symbol. The parent index must always be smaller than the current index. fn build_hierarchy_from_flat_list( diff --git a/crates/mun_language_server/src/lib.rs b/crates/mun_language_server/src/lib.rs index 8973dbe1a..7cc9c3cea 100644 --- a/crates/mun_language_server/src/lib.rs +++ b/crates/mun_language_server/src/lib.rs @@ -4,6 +4,7 @@ use serde::{de::DeserializeOwned, Serialize}; pub use config::{Config, FilesWatcher}; pub use main_loop::main_loop; +use mun_syntax::{TextRange, TextSize}; use paths::AbsPathBuf; use project::ProjectManifest; pub(crate) use state::LanguageServerState; @@ -13,6 +14,9 @@ mod analysis; mod cancelation; mod capabilities; mod change; +#[cfg(test)] +mod change_fixture; +mod completion; mod config; mod db; mod diagnostics; @@ -25,6 +29,20 @@ mod state; mod symbol_kind; mod to_lsp; +/// Represents a position in a file +#[derive(Clone, Copy, Debug)] +pub struct FilePosition { + pub file_id: hir::FileId, + pub offset: TextSize, +} + +/// Represents a range of text in a file. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FileRange { + pub file_id: hir::FileId, + pub range: TextRange, +} + /// Deserializes a `T` from a json value. pub fn from_json( what: &'static str, diff --git a/crates/mun_language_server/src/state.rs b/crates/mun_language_server/src/state.rs index 08759483a..eef667d4b 100644 --- a/crates/mun_language_server/src/state.rs +++ b/crates/mun_language_server/src/state.rs @@ -106,7 +106,7 @@ impl LanguageServerState { let (task_sender, task_receiver) = unbounded(); // Construct the state that will hold all the analysis and apply the initial state - let mut analysis = Analysis::new(); + let mut analysis = Analysis::default(); let mut change = AnalysisChange::new(); change.set_packages(Default::default()); change.set_roots(Default::default()); diff --git a/crates/mun_language_server/src/state/protocol.rs b/crates/mun_language_server/src/state/protocol.rs index ca8cf8c2c..bc96926b8 100644 --- a/crates/mun_language_server/src/state/protocol.rs +++ b/crates/mun_language_server/src/state/protocol.rs @@ -93,6 +93,7 @@ impl LanguageServerState { Ok(()) })? .on::(handlers::handle_document_symbol)? + .on::(handlers::handle_completion)? .finish(); Ok(()) diff --git a/crates/mun_language_server/src/symbol_kind.rs b/crates/mun_language_server/src/symbol_kind.rs index 0f5130800..4285f5747 100644 --- a/crates/mun_language_server/src/symbol_kind.rs +++ b/crates/mun_language_server/src/symbol_kind.rs @@ -1,7 +1,10 @@ /// Defines a set of symbols that can live in a document. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum SymbolKind { + Field, Function, + Local, + Module, Struct, TypeAlias, } diff --git a/crates/mun_language_server/src/to_lsp.rs b/crates/mun_language_server/src/to_lsp.rs index db73a573c..ee06ef834 100644 --- a/crates/mun_language_server/src/to_lsp.rs +++ b/crates/mun_language_server/src/to_lsp.rs @@ -1,3 +1,4 @@ +use crate::completion::{CompletionItem, CompletionItemKind}; use crate::state::LanguageServerSnapshot; use crate::symbol_kind::SymbolKind; use lsp_types::Url; @@ -71,6 +72,9 @@ pub(crate) fn symbol_kind(symbol_kind: SymbolKind) -> lsp_types::SymbolKind { SymbolKind::Function => lsp_types::SymbolKind::Function, SymbolKind::Struct => lsp_types::SymbolKind::Struct, SymbolKind::TypeAlias => lsp_types::SymbolKind::TypeParameter, + SymbolKind::Field => lsp_types::SymbolKind::Field, + SymbolKind::Local => lsp_types::SymbolKind::Variable, + SymbolKind::Module => lsp_types::SymbolKind::Module, } } @@ -81,3 +85,47 @@ pub(crate) fn url(snapshot: &LanguageServerSnapshot, file_id: hir::FileId) -> an let url = url_from_path_with_drive_lowercasing(path)?; Ok(url) } + +/// Converts from our `CompletionItem` to an LSP `CompletionItem` +pub(crate) fn completion_item(completion_item: CompletionItem) -> lsp_types::CompletionItem { + lsp_types::CompletionItem { + label: completion_item.label, + kind: completion_item.kind.map(completion_item_kind), + detail: completion_item.detail, + 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, + data: None, + tags: None, + } +} + +pub(crate) fn completion_item_kind( + completion_item_kind: CompletionItemKind, +) -> lsp_types::CompletionItemKind { + match completion_item_kind { + CompletionItemKind::Binding => lsp_types::CompletionItemKind::Variable, + CompletionItemKind::BuiltinType => lsp_types::CompletionItemKind::Struct, + CompletionItemKind::Keyword => lsp_types::CompletionItemKind::Keyword, + CompletionItemKind::Method => lsp_types::CompletionItemKind::Method, + CompletionItemKind::Snippet => lsp_types::CompletionItemKind::Snippet, + CompletionItemKind::UnresolvedReference => lsp_types::CompletionItemKind::Reference, + CompletionItemKind::SymbolKind(symbol) => match symbol { + SymbolKind::Field => lsp_types::CompletionItemKind::Field, + SymbolKind::Function => lsp_types::CompletionItemKind::Function, + SymbolKind::Local => lsp_types::CompletionItemKind::Variable, + SymbolKind::Module => lsp_types::CompletionItemKind::Module, + SymbolKind::Struct => lsp_types::CompletionItemKind::Struct, + SymbolKind::TypeAlias => lsp_types::CompletionItemKind::Struct, + }, + CompletionItemKind::Attribute => lsp_types::CompletionItemKind::EnumMember, + } +} diff --git a/crates/mun_syntax/Cargo.toml b/crates/mun_syntax/Cargo.toml index f1da575ba..bf3419dd1 100644 --- a/crates/mun_syntax/Cargo.toml +++ b/crates/mun_syntax/Cargo.toml @@ -19,6 +19,8 @@ text-size = { version = "1.1.0", features = ["serde"] } smol_str = { version = "0.1.17", features = ["serde"] } unicode-xid = "0.1.0" drop_bomb = "0.1.4" +ra_ap_text_edit = "0.0.35" +itertools = "0.10.0" [dev-dependencies] insta = "0.16" diff --git a/crates/mun_syntax/src/lib.rs b/crates/mun_syntax/src/lib.rs index 13676484a..cca0bf51f 100644 --- a/crates/mun_syntax/src/lib.rs +++ b/crates/mun_syntax/src/lib.rs @@ -20,6 +20,7 @@ mod syntax_node; #[cfg(test)] mod tests; +pub mod utils; use std::{fmt::Write, marker::PhantomData, sync::Arc}; @@ -117,10 +118,25 @@ impl Parse { } buf } + + /// Parses the `SourceFile` again but with the given modification applied. + pub fn reparse(&self, indel: &Indel) -> Parse { + // TODO: Implement something smarter here. + self.full_reparse(indel) + } + + /// Performs a "reparse" of the `SourceFile` after applying the specified modification by + /// simply parsing the entire thing again. + fn full_reparse(&self, indel: &Indel) -> Parse { + let mut text = self.tree().syntax().text().to_string(); + indel.apply(&mut text); + SourceFile::parse(&text) + } } /// `SourceFile` represents a parse tree for a single Mun file. pub use crate::ast::SourceFile; +use ra_ap_text_edit::Indel; impl SourceFile { pub fn parse(text: &str) -> Parse { diff --git a/crates/mun_syntax/src/parsing/grammar.rs b/crates/mun_syntax/src/parsing/grammar.rs index a2deec2ff..d37148de6 100644 --- a/crates/mun_syntax/src/parsing/grammar.rs +++ b/crates/mun_syntax/src/parsing/grammar.rs @@ -67,13 +67,10 @@ fn name_ref(p: &mut Parser) { } fn name_ref_or_index(p: &mut Parser) { - if p.at(IDENT) || p.at(INT_NUMBER) { - let m = p.start(); - p.bump_any(); - m.complete(p, NAME_REF); - } else { - p.error_and_bump("expected an identifier"); - } + assert!(p.at(IDENT) || p.at(INT_NUMBER)); + let m = p.start(); + p.bump_any(); + m.complete(p, NAME_REF); } fn opt_visibility(p: &mut Parser) -> bool { diff --git a/crates/mun_syntax/src/parsing/grammar/expressions.rs b/crates/mun_syntax/src/parsing/grammar/expressions.rs index 9e309a113..a6709c2f5 100644 --- a/crates/mun_syntax/src/parsing/grammar/expressions.rs +++ b/crates/mun_syntax/src/parsing/grammar/expressions.rs @@ -248,7 +248,8 @@ fn arg_list(p: &mut Parser) { fn postfix_dot_expr(p: &mut Parser, lhs: CompletedMarker) -> CompletedMarker { assert!(p.at(T![.])); if p.nth(1) == IDENT && p.nth(2) == T!['('] { - unimplemented!("Method calls are not supported yet."); + // TODO: Implement method calls here + //unimplemented!("Method calls are not supported yet."); } field_expr(p, lhs) @@ -259,7 +260,11 @@ fn field_expr(p: &mut Parser, lhs: CompletedMarker) -> CompletedMarker { let m = lhs.precede(p); if p.at(T![.]) { p.bump(T![.]); - name_ref_or_index(p); + if p.at(IDENT) || p.at(INT_NUMBER) { + name_ref_or_index(p) + } else { + p.error("expected field name or number"); + } } else if p.at(INDEX) { p.bump(INDEX); } else { diff --git a/crates/mun_syntax/src/tests/parser.rs b/crates/mun_syntax/src/tests/parser.rs index ac30d2b3d..c2310363b 100644 --- a/crates/mun_syntax/src/tests/parser.rs +++ b/crates/mun_syntax/src/tests/parser.rs @@ -6,6 +6,16 @@ fn snapshot_test(text: &str) { insta::assert_snapshot!(insta::_macro_support::AutoName, file.debug_dump(), &text); } +#[test] +fn missing_field_expr() { + snapshot_test( + r#" + fn foo() { + var. + }"#, + ); +} + #[test] fn empty() { snapshot_test(r#""#); diff --git a/crates/mun_syntax/src/tests/snapshots/mun_syntax__tests__parser__missing_field_expr.snap b/crates/mun_syntax/src/tests/snapshots/mun_syntax__tests__parser__missing_field_expr.snap new file mode 100644 index 000000000..15e860b78 --- /dev/null +++ b/crates/mun_syntax/src/tests/snapshots/mun_syntax__tests__parser__missing_field_expr.snap @@ -0,0 +1,28 @@ +--- +source: crates/mun_syntax/src/tests/parser.rs +expression: "fn foo() {\n var.\n}" +--- +SOURCE_FILE@0..21 + FUNCTION_DEF@0..21 + FN_KW@0..2 "fn" + WHITESPACE@2..3 " " + NAME@3..6 + IDENT@3..6 "foo" + PARAM_LIST@6..8 + L_PAREN@6..7 "(" + R_PAREN@7..8 ")" + WHITESPACE@8..9 " " + BLOCK_EXPR@9..21 + L_CURLY@9..10 "{" + WHITESPACE@10..15 "\n " + FIELD_EXPR@15..19 + PATH_EXPR@15..18 + PATH@15..18 + PATH_SEGMENT@15..18 + NAME_REF@15..18 + IDENT@15..18 "var" + DOT@18..19 "." + WHITESPACE@19..20 "\n" + R_CURLY@20..21 "}" +error Offset(19): expected field name or number + diff --git a/crates/mun_syntax/src/utils.rs b/crates/mun_syntax/src/utils.rs new file mode 100644 index 000000000..c2069f0ae --- /dev/null +++ b/crates/mun_syntax/src/utils.rs @@ -0,0 +1,27 @@ +use crate::{AstNode, SyntaxNode, TextSize}; +use itertools::Itertools; + +/// Returns ancestors of the node at the offset, sorted by length. This should do the right thing at +/// an edge, e.g. when searching for expressions at `{ $0foo }` we will get the name reference +/// instead of the whole block, which we would get if we just did `find_token_at_offset(...). +/// flat_map(|t| t.parent().ancestors())`. +pub fn ancestors_at_offset( + node: &SyntaxNode, + offset: TextSize, +) -> impl Iterator { + node.token_at_offset(offset) + .map(|token| token.parent().ancestors()) + .kmerge_by(|node1, node2| node1.text_range().len() < node2.text_range().len()) +} + +/// Finds a node of specific Ast type at offset. Note that this is slightly imprecise: if the cursor +/// is strictly between two nodes of the desired type, as in +/// +/// ```mun +/// struct Foo {}|struct Bar; +/// ``` +/// +/// then the shorter node will be silently preferred. +pub fn find_node_at_offset(syntax: &SyntaxNode, offset: TextSize) -> Option { + ancestors_at_offset(syntax, offset).find_map(N::cast) +}