From c7c9f3998f3da44109da1eb8bbc2c3a065d86035 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Tue, 7 Jan 2025 22:56:52 +0100 Subject: [PATCH] feat: assoc method completion --- Cargo.lock | 1 + crates/mun_hir/src/lib.rs | 4 +- crates/mun_hir/src/semantics.rs | 20 +++ crates/mun_hir/src/source_analyzer.rs | 50 +++++- crates/mun_hir/src/ty/lower.rs | 2 +- crates/mun_language_server/Cargo.toml | 1 + crates/mun_language_server/src/completion.rs | 38 +++- .../src/completion/context.rs | 146 ---------------- .../src/completion/context/analysis.rs | 124 +++++++++++++ .../src/completion/context/mod.rs | 163 ++++++++++++++++++ .../mun_language_server/src/completion/dot.rs | 22 +-- .../src/completion/expr.rs | 77 +++++++++ .../src/completion/item.rs | 2 + .../src/completion/name_ref.rs | 21 +++ ...tion__expr__tests__associate_function.snap | 5 + ...completion__expr__tests__local_scope.snap} | 6 +- .../src/completion/unqualified_path.rs | 22 +++ .../mun_language_server/src/file_structure.rs | 26 ++- crates/mun_language_server/src/symbol_kind.rs | 2 + crates/mun_language_server/src/to_lsp.rs | 4 + .../tests/initialization.rs | 52 ------ .../integration_tests/document_symbols.rs | 90 ++++++++++ .../tests/integration_tests/initialization.rs | 20 +++ .../tests/integration_tests/main.rs | 5 + ...s__document_symbols__document_symbols.snap | 13 ++ .../tests/{ => integration_tests}/support.rs | 0 .../initialization__document_symbols.snap | 100 ----------- crates/mun_syntax/src/ast/extensions.rs | 6 + crates/mun_syntax/src/lib.rs | 4 +- 29 files changed, 695 insertions(+), 331 deletions(-) delete mode 100644 crates/mun_language_server/src/completion/context.rs create mode 100644 crates/mun_language_server/src/completion/context/analysis.rs create mode 100644 crates/mun_language_server/src/completion/context/mod.rs create mode 100644 crates/mun_language_server/src/completion/expr.rs create mode 100644 crates/mun_language_server/src/completion/name_ref.rs create mode 100644 crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__expr__tests__associate_function.snap rename crates/mun_language_server/src/completion/snapshots/{mun_language_server__completion__unqualified_path__tests__local_scope.snap => mun_language_server__completion__expr__tests__local_scope.snap} (51%) delete mode 100644 crates/mun_language_server/tests/initialization.rs create mode 100644 crates/mun_language_server/tests/integration_tests/document_symbols.rs create mode 100644 crates/mun_language_server/tests/integration_tests/initialization.rs create mode 100644 crates/mun_language_server/tests/integration_tests/main.rs create mode 100644 crates/mun_language_server/tests/integration_tests/snapshots/integration_tests__document_symbols__document_symbols.snap rename crates/mun_language_server/tests/{ => integration_tests}/support.rs (100%) delete mode 100644 crates/mun_language_server/tests/snapshots/initialization__document_symbols.snap diff --git a/Cargo.lock b/Cargo.lock index e4e35e0de..692d55bac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2565,6 +2565,7 @@ dependencies = [ "serde_derive", "serde_json", "tempdir", + "text_trees", "thiserror 1.0.69", "threadpool", ] diff --git a/crates/mun_hir/src/lib.rs b/crates/mun_hir/src/lib.rs index 9ef0cc6e5..54c8969ec 100644 --- a/crates/mun_hir/src/lib.rs +++ b/crates/mun_hir/src/lib.rs @@ -25,7 +25,7 @@ pub use crate::{ ArithOp, BinaryOp, Body, CmpOp, Expr, ExprId, ExprScopes, Literal, LogicOp, Ordering, Pat, PatId, RecordLitField, Statement, UnaryOp, }, - ids::ItemLoc, + ids::{AssocItemId, ItemLoc}, in_file::InFile, name::Name, name_resolution::PerNs, @@ -62,7 +62,7 @@ mod utils; mod has_module; mod item_scope; -mod method_resolution; +pub mod method_resolution; #[cfg(test)] mod mock; mod package_defs; diff --git a/crates/mun_hir/src/semantics.rs b/crates/mun_hir/src/semantics.rs index 84db72387..61026f9ed 100644 --- a/crates/mun_hir/src/semantics.rs +++ b/crates/mun_hir/src/semantics.rs @@ -27,6 +27,15 @@ use crate::{ HirDatabase, InFile, ModuleDef, Name, PatId, PerNs, Resolver, Ty, Visibility, }; +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum PathResolution { + /// An item + Def(ModuleDef), + /// A local binding (only value namespace) + Local(Local), + SelfType(Impl), +} + /// 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> { @@ -147,6 +156,11 @@ impl<'db> Semantics<'db> { }); InFile::new(file_id, node) } + + /// Resolves the specified `ast::Path` + pub fn resolve_path(&self, path: &ast::Path) -> Option { + self.analyze(path.syntax()).resolve_path(self.db, path) + } } /// Returns the root node of the specified node. @@ -201,6 +215,12 @@ pub struct Impl { pub(crate) id: ImplId, } +impl From for Impl { + fn from(id: ImplId) -> Self { + Impl { id } + } +} + impl Impl { pub fn self_ty(self, db: &dyn HirDatabase) -> Ty { db.type_for_impl_self(self.id) diff --git a/crates/mun_hir/src/source_analyzer.rs b/crates/mun_hir/src/source_analyzer.rs index 571abd763..25aff395b 100644 --- a/crates/mun_hir/src/source_analyzer.rs +++ b/crates/mun_hir/src/source_analyzer.rs @@ -6,8 +6,10 @@ use mun_syntax::{ast, AstNode, SyntaxNode, TextRange, TextSize}; use crate::{ expr::{scope::LocalScopeId, BodySourceMap}, ids::DefWithBodyId, - resolver_for_scope, Body, ExprId, ExprScopes, HirDatabase, InFile, InferenceResult, Resolver, - Ty, + resolver_for_scope, + semantics::PathResolution, + Body, ExprId, ExprScopes, HirDatabase, InFile, InferenceResult, Path, Resolver, Struct, Ty, + TypeAlias, TypeNs, }; /// A `SourceAnalyzer` is a wrapper which exposes the HIR API in terms of the @@ -79,6 +81,22 @@ impl SourceAnalyzer { let sm = self.body_source_map.as_ref()?; sm.node_expr(expr) } + + pub(crate) fn resolve_path( + &self, + db: &dyn HirDatabase, + path: &ast::Path, + ) -> Option { + let hir_path = Path::from_ast(path.clone())?; + + // Case where path is a qualifier of another path, e.g. foo::bar::Baz where we + // are trying to resolve foo::bar. + if path.parent_path().is_some() { + return resolve_hir_path_qualifier(db, &self.resolver, &hir_path); + } + + None + } } /// Returns the id of the scope that is active at the location of `node`. @@ -173,3 +191,31 @@ fn adjust( }) .map(|(_ptr, scope)| *scope) } + +/// Resolves a path where we know it is a qualifier of another path. +fn resolve_hir_path_qualifier( + db: &dyn HirDatabase, + resolver: &Resolver, + path: &Path, +) -> Option { + let (ty, _, remaining_idx) = resolver.resolve_path_as_type(db.upcast(), path)?; + let (ty, _unresolved) = match remaining_idx { + Some(remaining_idx) => { + if remaining_idx + 1 == path.segments.len() { + Some((ty, path.segments.last())) + } else { + None + } + } + None => Some((ty, None)), + }?; + + let res = match ty { + TypeNs::SelfType(it) => PathResolution::SelfType(it.into()), + TypeNs::StructId(it) => PathResolution::Def(Struct::from(it).into()), + TypeNs::TypeAliasId(it) => PathResolution::Def(TypeAlias::from(it).into()), + TypeNs::PrimitiveType(it) => PathResolution::Def(it.into()), + }; + + Some(res) +} diff --git a/crates/mun_hir/src/ty/lower.rs b/crates/mun_hir/src/ty/lower.rs index 9f19f236b..bb39812b0 100644 --- a/crates/mun_hir/src/ty/lower.rs +++ b/crates/mun_hir/src/ty/lower.rs @@ -340,7 +340,7 @@ pub mod diagnostics { }; #[derive(Debug, PartialEq, Eq, Clone)] - pub(crate) enum LowerDiagnostic { + pub enum LowerDiagnostic { UnresolvedType { id: LocalTypeRefId }, TypeIsPrivate { id: LocalTypeRefId }, } diff --git a/crates/mun_language_server/Cargo.toml b/crates/mun_language_server/Cargo.toml index fb35fe192..926dfc3fa 100644 --- a/crates/mun_language_server/Cargo.toml +++ b/crates/mun_language_server/Cargo.toml @@ -42,3 +42,4 @@ mun_test = { path = "../mun_test"} insta = { workspace = true } itertools = { workspace = true } tempdir = { workspace = true } +text_trees = { workspace = true } diff --git a/crates/mun_language_server/src/completion.rs b/crates/mun_language_server/src/completion.rs index 2c805dd6b..6e73b9cf6 100644 --- a/crates/mun_language_server/src/completion.rs +++ b/crates/mun_language_server/src/completion.rs @@ -4,20 +4,26 @@ //! completions. mod context; -mod dot; +// mod dot; mod item; mod render; -mod unqualified_path; +// mod unqualified_path; +mod dot; +mod expr; +mod name_ref; #[cfg(test)] mod test_utils; -use context::CompletionContext; +use context::{ + CompletionAnalysis, CompletionContext, DotAccess, NameRefContext, NameRefKind, + PathCompletionContext, PathExprContext, PathKind, Qualified, +}; pub use item::{CompletionItem, CompletionItemKind, CompletionKind}; use mun_hir::semantics::ScopeDef; use crate::{ - completion::render::{render_field, render_resolution, RenderContext}, + completion::render::{render_field, render_fn, render_resolution, RenderContext}, db::AnalysisDatabase, FilePosition, }; @@ -36,11 +42,18 @@ use crate::{ /// 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 (context, analysis) = CompletionContext::new(db, position)?; let mut result = Completions::default(); - unqualified_path::complete_unqualified_path(&mut result, &context); - dot::complete_dot(&mut result, &context); + + match analysis { + CompletionAnalysis::NameRef(name_ref_ctx) => { + name_ref::complete_name_ref(&mut result, &context, &name_ref_ctx); + } + } + + // unqualified_path::complete_unqualified_path(&mut result, &context); + // dot::complete_dot(&mut result, &context); Some(result) } @@ -80,4 +93,15 @@ impl Completions { let item = render_field(RenderContext::new(ctx), field); self.add(item); } + + fn add_function( + &mut self, + ctx: &CompletionContext<'_>, + func: mun_hir::Function, + local_name: Option, + ) { + if let Some(item) = render_fn(RenderContext::new(ctx), local_name, func) { + self.add(item); + } + } } diff --git a/crates/mun_language_server/src/completion/context.rs b/crates/mun_language_server/src/completion/context.rs deleted file mode 100644 index 9f66b2e3d..000000000 --- a/crates/mun_language_server/src/completion/context.rs +++ /dev/null @@ -1,146 +0,0 @@ -use mun_hir::{ - semantics::{Semantics, SemanticsScope}, - AstDatabase, -}; -use mun_syntax::{ast, utils::find_node_at_offset, AstNode, SyntaxNode, TextRange, TextSize}; -use ra_ap_text_edit::Indel; - -use crate::{db::AnalysisDatabase, FilePosition}; - -/// 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, - - // TODO: Add this when it is used - //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, - // TODO: add this when it is used - //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/context/analysis.rs b/crates/mun_language_server/src/completion/context/analysis.rs new file mode 100644 index 000000000..949a892f2 --- /dev/null +++ b/crates/mun_language_server/src/completion/context/analysis.rs @@ -0,0 +1,124 @@ +use mun_hir::semantics::Semantics; +use mun_syntax::{ast, match_ast, utils::find_node_at_offset, AstNode, SyntaxNode, TextSize}; + +use super::{ + find_node_in_file, find_opt_node_in_file, CompletionAnalysis, DotAccess, NameRefContext, + NameRefKind, PathCompletionContext, PathExprContext, PathKind, Qualified, +}; + +/// The result of the analysis of a completion request. This contains +/// information about the context of the completion request which helps identify +/// the surrounding code and the position of the cursor. +pub(super) struct AnalysisResult { + pub(super) analysis: CompletionAnalysis, +} + +pub fn analyze( + sema: &Semantics<'_>, + original_file: SyntaxNode, + speculative_file: SyntaxNode, + offset: TextSize, +) -> Option { + if let Some(name_ref) = find_node_at_offset::(&speculative_file, offset) { + let parent = name_ref.syntax().parent()?; + let name_ref_ctx = classify_name_ref(sema, &original_file, name_ref, parent)?; + return Some(AnalysisResult { + analysis: CompletionAnalysis::NameRef(name_ref_ctx), + }); + } + + None +} + +fn classify_name_ref( + sema: &Semantics<'_>, + original_file: &SyntaxNode, + name_ref: ast::NameRef, + parent: SyntaxNode, +) -> Option { + let name_ref = find_node_at_offset(original_file, name_ref.syntax().text_range().start()); + + let segment = match_ast! { + match parent { + ast::PathSegment(segment) => segment, + ast::FieldExpr(field) => { + let receiver = find_opt_node_in_file(original_file, field.expr()); + let kind = NameRefKind::DotAccess(DotAccess { + receiver_ty: receiver.as_ref().and_then(|it| sema.type_of_expr(it)), + receiver + }); + return Some(NameRefContext { + name_ref, + kind, + }); + }, + _ => return None, + } + }; + + let path = segment.parent_path(); + + let mut path_ctx = PathCompletionContext { + qualified: Qualified::No, + use_tree_parent: false, + kind: PathKind::SourceFile, + }; + + let make_path_kind_expr = |_expr: ast::Expr| PathKind::Expr(PathExprContext {}); + + // Infer the type of path + let parent = path.syntax().parent()?; + let kind = match_ast! { + match parent { + ast::PathExpr(it) => { + make_path_kind_expr(it.into()) + }, + ast::UseTree(_) => PathKind::Use, + _ => return None, + } + }; + + path_ctx.kind = kind; + + // If the path has a qualifier, we need to determine if it is a use tree or a + // path + if let Some((qualifier, use_tree_parent)) = path_or_use_tree_qualifier(&path) { + path_ctx.use_tree_parent = use_tree_parent; + if !use_tree_parent && segment.has_colon_colon() { + path_ctx.qualified = Qualified::Absolute; + } else { + let qualifier = qualifier + .segment() + .and_then(|it| find_node_in_file(original_file, &it)) + .map(|it| it.parent_path()); + if let Some(qualifier) = qualifier { + let res = sema.resolve_path(&qualifier); + path_ctx.qualified = Qualified::With { + path: qualifier, + resolution: res, + } + } + } + } else if let Some(segment) = path.segment() { + if segment.has_colon_colon() { + path_ctx.qualified = Qualified::Absolute; + } + } + + Some(NameRefContext { + name_ref, + kind: NameRefKind::Path(path_ctx), + }) +} + +fn path_or_use_tree_qualifier(path: &ast::Path) -> Option<(ast::Path, bool)> { + if let Some(qual) = path.qualifier() { + return Some((qual, false)); + } + let use_tree_list = path.syntax().ancestors().find_map(ast::UseTreeList::cast)?; + let use_tree = use_tree_list + .syntax() + .parent() + .and_then(ast::UseTree::cast)?; + Some((use_tree.path()?, true)) +} diff --git a/crates/mun_language_server/src/completion/context/mod.rs b/crates/mun_language_server/src/completion/context/mod.rs new file mode 100644 index 000000000..2bef8d4ca --- /dev/null +++ b/crates/mun_language_server/src/completion/context/mod.rs @@ -0,0 +1,163 @@ +#![allow(dead_code)] + +mod analysis; + +use mun_hir::{ + semantics::{PathResolution, Semantics, SemanticsScope}, + AstDatabase, Ty, +}; +use mun_syntax::{ast, AstNode, SyntaxNode}; +use ra_ap_text_edit::Indel; + +use crate::{ + completion::context::analysis::{analyze, AnalysisResult}, + db::AnalysisDatabase, + FilePosition, +}; + +/// 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, + // TODO: Add this when it is used + //pub position: FilePosition, +} + +/// Information about the identifier that we are currently completing. +#[derive(Debug)] +pub(super) enum CompletionAnalysis { + NameRef(NameRefContext), +} + +/// The identifier to complete is a name reference. +#[derive(Debug)] +pub(super) struct NameRefContext { + /// `NameRef` syntax in the original file + pub(super) name_ref: Option, + pub(super) kind: NameRefKind, +} + +/// The kind of the `NameRef` we are completing. +#[derive(Debug)] +pub(super) enum NameRefKind { + Path(PathCompletionContext), + DotAccess(DotAccess), +} + +/// Information about the field or method access we are completing. +#[derive(Debug)] +pub(crate) struct DotAccess { + pub(crate) receiver: Option, + pub(crate) receiver_ty: Option, +} + +/// The state of the path we are currently completing. +#[derive(Debug)] +pub(super) struct PathCompletionContext { + /// The type of path we are completing. + pub(super) kind: PathKind, + + /// The qualifier of the current path. + pub(super) qualified: Qualified, + + /// Whether the qualifier comes from a use tree parent or not + pub(super) use_tree_parent: bool, +} + +#[derive(Debug)] +pub(super) enum Qualified { + /// No path qualifier, this is a bare path, e.g. `foo` + No, + + /// The path has a qualifier, e.g. `foo` in `foo::bar` + With { + /// The path that is being completed + path: ast::Path, + + /// The resolution of the path that is being completed + resolution: Option, + }, + + /// The path has an absolute qualifier, e.g. `::foo` + Absolute, +} + +/// The kind of path we are completing right now. +#[derive(Debug)] +pub(super) enum PathKind { + Expr(PathExprContext), + Use, + SourceFile, +} + +#[derive(Debug)] +pub(super) struct PathExprContext {} + +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<(Self, CompletionAnalysis)> { + 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 original_token = original_file + .syntax() + .token_at_offset(position.offset) + .left_biased()?; + + // Analyze the context of the completion request + let AnalysisResult { analysis } = analyze( + &sema, + original_file.syntax().clone(), + file_with_fake_ident.syntax().clone(), + position.offset, + )?; + + let scope = sema.scope_at_offset(&original_token.parent()?, position.offset); + + let context = Self { + sema, + scope, + db, + // TODO: add this when it is used + //position, + }; + + Some((context, analysis)) + } +} + +/// Attempts to find `node` inside `syntax` via `node`'s text range. +/// If the fake identifier has been inserted after this node or inside of this +/// node use the `_compensated` version instead. +fn find_opt_node_in_file(syntax: &SyntaxNode, node: Option) -> Option { + find_node_in_file(syntax, &node?) +} + +/// Attempts to find `node` inside `syntax` via `node`'s text range. +/// If the fake identifier has been inserted after this node or inside of this +/// node use the `_compensated` version instead. +fn find_node_in_file(syntax: &SyntaxNode, node: &N) -> Option { + let syntax_range = syntax.text_range(); + let range = node.syntax().text_range(); + let intersection = range.intersect(syntax_range)?; + syntax + .covering_element(intersection) + .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 index 26017b9c3..3a583328c 100644 --- a/crates/mun_language_server/src/completion/dot.rs +++ b/crates/mun_language_server/src/completion/dot.rs @@ -1,18 +1,18 @@ use mun_db::Upcast; -use super::{CompletionContext, Completions}; +use super::{CompletionContext, Completions, DotAccess}; /// 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, +pub(super) fn complete_dot( + result: &mut Completions, + ctx: &CompletionContext<'_>, + dot_access: &DotAccess, +) { + let receiver_ty = match dot_access { + DotAccess { + receiver_ty: Some(receiver_ty), + .. + } => receiver_ty, _ => return, }; diff --git a/crates/mun_language_server/src/completion/expr.rs b/crates/mun_language_server/src/completion/expr.rs new file mode 100644 index 000000000..c6f70426b --- /dev/null +++ b/crates/mun_language_server/src/completion/expr.rs @@ -0,0 +1,77 @@ +use mun_hir::{ + method_resolution::{AssociationMode, MethodResolutionCtx}, + semantics::PathResolution, + AssocItemId, ModuleDef, +}; + +use super::{CompletionContext, Completions, PathCompletionContext, PathExprContext, Qualified}; + +pub(super) fn complete_expr_path( + result: &mut Completions, + ctx: &CompletionContext<'_>, + PathCompletionContext { qualified, .. }: &PathCompletionContext, + _expr_ctx: &PathExprContext, +) { + match qualified { + Qualified::With { + resolution: Some(PathResolution::Def(ModuleDef::Struct(s))), + .. + } => { + let ty = s.ty(ctx.db); + MethodResolutionCtx::new(ctx.db, ty.clone()) + .with_association(AssociationMode::WithoutSelf) + .collect(|item, _visible| { + match item { + AssocItemId::FunctionId(f) => result.add_function(ctx, f.into(), None), + }; + None::<()> + }); + } + Qualified::No => { + // 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, 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) + )); + } + + #[test] + fn test_associate_function() { + insta::assert_snapshot!(completion_string( + r#" + struct Foo; + + impl Foo { + fn new() -> Self { + Self + } + } + + fn foo() { + let bar = Foo::$0; + } + "#, + Some(CompletionKind::Reference) + )); + } +} diff --git a/crates/mun_language_server/src/completion/item.rs b/crates/mun_language_server/src/completion/item.rs index 1e887011a..f5f0d5979 100644 --- a/crates/mun_language_server/src/completion/item.rs +++ b/crates/mun_language_server/src/completion/item.rs @@ -54,6 +54,8 @@ impl CompletionItemKind { SymbolKind::SelfType => "sy", SymbolKind::Struct => "st", SymbolKind::TypeAlias => "ta", + SymbolKind::Impl => "im", + SymbolKind::Method => "mt", }, CompletionItemKind::Attribute => "at", CompletionItemKind::Binding => "bn", diff --git a/crates/mun_language_server/src/completion/name_ref.rs b/crates/mun_language_server/src/completion/name_ref.rs new file mode 100644 index 000000000..d29182ff0 --- /dev/null +++ b/crates/mun_language_server/src/completion/name_ref.rs @@ -0,0 +1,21 @@ +use super::{dot, expr, CompletionContext, Completions, NameRefContext, NameRefKind, PathKind}; + +/// Generate completions for a name reference. +#[allow(clippy::single_match)] +pub(super) fn complete_name_ref( + completions: &mut Completions, + ctx: &CompletionContext<'_>, + NameRefContext { kind, .. }: &NameRefContext, +) { + match kind { + NameRefKind::Path(path_ctx) => match &path_ctx.kind { + PathKind::Expr(expr_ctx) => { + expr::complete_expr_path(completions, ctx, path_ctx, expr_ctx); + } + _ => {} + }, + NameRefKind::DotAccess(dot_access) => { + dot::complete_dot(completions, ctx, dot_access); + } + } +} diff --git a/crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__expr__tests__associate_function.snap b/crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__expr__tests__associate_function.snap new file mode 100644 index 000000000..852c9688d --- /dev/null +++ b/crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__expr__tests__associate_function.snap @@ -0,0 +1,5 @@ +--- +source: crates/mun_language_server/src/completion/expr.rs +expression: "completion_string(r#\"\n struct Foo;\n\n impl Foo {\n fn new() -> Self {\n Self\n }\n }\n\n fn foo() {\n let bar = Foo::$0;\n }\n \"#,\nSome(CompletionKind::Reference))" +--- +fn new -> Foo 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__expr__tests__local_scope.snap similarity index 51% rename from crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__unqualified_path__tests__local_scope.snap rename to crates/mun_language_server/src/completion/snapshots/mun_language_server__completion__expr__tests__local_scope.snap index 5bb35330f..49761be8a 100644 --- 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__expr__tests__local_scope.snap @@ -1,8 +1,6 @@ --- -source: crates/mun_language_server/src/completion/unqualified_path.rs -assertion_line: 29 -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))" - +source: crates/mun_language_server/src/completion/expr.rs +expression: "completion_string(r#\"\n fn foo() {\n let bar = 0;\n let foo_bar = 0;\n f$0\n }\n \"#,\nSome(CompletionKind::Reference))" --- lc foo_bar i32 lc bar i32 diff --git a/crates/mun_language_server/src/completion/unqualified_path.rs b/crates/mun_language_server/src/completion/unqualified_path.rs index 544a531ff..52c6a8112 100644 --- a/crates/mun_language_server/src/completion/unqualified_path.rs +++ b/crates/mun_language_server/src/completion/unqualified_path.rs @@ -10,6 +10,8 @@ use super::{CompletionContext, Completions}; /// } /// ``` pub(super) fn complete_unqualified_path(result: &mut Completions, ctx: &CompletionContext<'_>) { + dbg!(ctx.is_trivial_path, ctx.is_param, ctx.is_path_type); + // Only complete trivial paths (e.g. foo, not ::foo) if !ctx.is_trivial_path { return; @@ -38,4 +40,24 @@ mod tests { Some(CompletionKind::Reference) )); } + + #[test] + fn test_associate_function() { + insta::assert_snapshot!(completion_string( + r#" + struct Foo; + + impl Foo { + fn new() -> Self { + Self + } + } + + fn foo() { + let bar = Foo::$0; + } + "#, + Some(CompletionKind::Reference) + )); + } } diff --git a/crates/mun_language_server/src/file_structure.rs b/crates/mun_language_server/src/file_structure.rs index 09be108d0..4fd8bc3d6 100644 --- a/crates/mun_language_server/src/file_structure.rs +++ b/crates/mun_language_server/src/file_structure.rs @@ -1,5 +1,5 @@ use mun_syntax::{ - ast::{self, NameOwner}, + ast::{self, NameOwner, TypeAscriptionOwner}, match_ast, AstNode, SourceFile, SyntaxNode, TextRange, WalkEvent, }; @@ -117,18 +117,36 @@ fn try_convert_to_structure_node(node: &SyntaxNode) -> Option { match node { ast::FunctionDef(it) => { let mut detail = String::from("fn"); - if let Some(param_list) = it.param_list() { + let has_self_param = if let Some(param_list) = it.param_list() { collapse_whitespaces(param_list.syntax(), &mut detail); - } + param_list.self_param().is_some() + } else { + false + }; if let Some(ret_type) = it.ret_type() { detail.push(' '); collapse_whitespaces(ret_type.syntax(), &mut detail); } - decl_with_detail(&it, Some(detail), SymbolKind::Function) + decl_with_detail(&it, Some(detail), if has_self_param { SymbolKind::Method } else { SymbolKind::Function }) }, ast::StructDef(it) => decl(it, SymbolKind::Struct), ast::TypeAliasDef(it) => decl_with_type_ref(&it, it.type_ref(), SymbolKind::TypeAlias), + ast::RecordFieldDef(it) => decl_with_type_ref(&it, it.ascribed_type(), SymbolKind::Field), + ast::Impl(it) => { + let target_type = it.type_ref()?; + let label = format!("impl {}", target_type.syntax().text()); + + let node = StructureNode { + parent: None, + label, + navigation_range: target_type.syntax().text_range(), + node_range: it.syntax().text_range(), + kind: SymbolKind::Impl, + detail: None, + }; + Some(node) + }, _ => None } } diff --git a/crates/mun_language_server/src/symbol_kind.rs b/crates/mun_language_server/src/symbol_kind.rs index 1206d7939..fde848d62 100644 --- a/crates/mun_language_server/src/symbol_kind.rs +++ b/crates/mun_language_server/src/symbol_kind.rs @@ -3,8 +3,10 @@ pub enum SymbolKind { Field, Function, + Method, Local, Module, + Impl, SelfParam, SelfType, Struct, diff --git a/crates/mun_language_server/src/to_lsp.rs b/crates/mun_language_server/src/to_lsp.rs index 576fe3be5..f40205f60 100644 --- a/crates/mun_language_server/src/to_lsp.rs +++ b/crates/mun_language_server/src/to_lsp.rs @@ -79,6 +79,8 @@ pub(crate) fn symbol_kind(symbol_kind: SymbolKind) -> lsp_types::SymbolKind { SymbolKind::Field => lsp_types::SymbolKind::FIELD, SymbolKind::Local | SymbolKind::SelfParam => lsp_types::SymbolKind::VARIABLE, SymbolKind::Module => lsp_types::SymbolKind::MODULE, + SymbolKind::Method => lsp_types::SymbolKind::METHOD, + SymbolKind::Impl => lsp_types::SymbolKind::OBJECT, } } @@ -118,6 +120,8 @@ pub(crate) fn completion_item_kind( SymbolKind::SelfParam => lsp_types::CompletionItemKind::VALUE, SymbolKind::SelfType => lsp_types::CompletionItemKind::TYPE_PARAMETER, SymbolKind::Struct | SymbolKind::TypeAlias => lsp_types::CompletionItemKind::STRUCT, + SymbolKind::Method => lsp_types::CompletionItemKind::METHOD, + SymbolKind::Impl => lsp_types::CompletionItemKind::TEXT, }, CompletionItemKind::Attribute => lsp_types::CompletionItemKind::ENUM_MEMBER, } diff --git a/crates/mun_language_server/tests/initialization.rs b/crates/mun_language_server/tests/initialization.rs deleted file mode 100644 index a1484966a..000000000 --- a/crates/mun_language_server/tests/initialization.rs +++ /dev/null @@ -1,52 +0,0 @@ -mod support; - -use lsp_types::{PartialResultParams, WorkDoneProgressParams}; -use support::Project; - -#[test] -fn test_server() { - let _server = Project::with_fixture( - r#" -//- /mun.toml -[package] -name = "foo" -version = "0.0.0" - -//- /src/mod.mun -fn add(a: i32, b: i32) -> i32 { - a + b -} -"#, - ) - .server() - .wait_until_workspace_is_loaded(); -} - -#[test] -fn test_document_symbols() { - let server = Project::with_fixture( - r#" - //- /mun.toml - [package] - name = "foo" - version = "0.0.0" - - //- /src/mod.mun - fn main() -> i32 {} - struct Foo {} - type Bar = Foo; - "#, - ) - .server() - .wait_until_workspace_is_loaded(); - - let symbols = server.send_request::( - lsp_types::DocumentSymbolParams { - text_document: server.doc_id("src/mod.mun"), - work_done_progress_params: WorkDoneProgressParams::default(), - partial_result_params: PartialResultParams::default(), - }, - ); - - insta::assert_debug_snapshot!(symbols); -} diff --git a/crates/mun_language_server/tests/integration_tests/document_symbols.rs b/crates/mun_language_server/tests/integration_tests/document_symbols.rs new file mode 100644 index 000000000..d219f64e1 --- /dev/null +++ b/crates/mun_language_server/tests/integration_tests/document_symbols.rs @@ -0,0 +1,90 @@ +use itertools::Itertools; +use lsp_types::{DocumentSymbolResponse, PartialResultParams, WorkDoneProgressParams}; +use text_trees::FormatCharacters; + +use crate::Project; + +#[test] +fn test_document_symbols() { + let server = Project::with_fixture( + r#" + //- /mun.toml + [package] + name = "foo" + version = "0.0.0" + + //- /src/mod.mun + struct Foo { + a: i32, + } + type Bar = Foo; + + impl Foo { + fn new() -> Self {} + + fn modify(self) -> Self { + self + } + } + + impl Foo { + fn modify2(self) -> Self { + self + } + } + + fn main() -> i32 {} + "#, + ) + .server() + .wait_until_workspace_is_loaded(); + + let symbols = server.send_request::( + lsp_types::DocumentSymbolParams { + text_document: server.doc_id("src/mod.mun"), + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: PartialResultParams::default(), + }, + ); + + insta::assert_snapshot!(format_document_symbols_response(symbols)); +} + +fn format_document_symbols_response(response: Option) -> String { + let Some(response) = response else { + return "received empty response".to_string(); + }; + + let nodes = match response { + DocumentSymbolResponse::Flat(_symbols) => { + unimplemented!("Flat document symbols are not supported") + } + DocumentSymbolResponse::Nested(symbols) => symbols + .iter() + .map(format_document_symbol) + .collect::>(), + }; + + let formatting = text_trees::TreeFormatting::dir_tree(FormatCharacters::ascii()); + format!( + "{}", + nodes + .into_iter() + .map(|node| node.to_string_with_format(&formatting).unwrap()) + .format("") + ) +} + +fn format_document_symbol(symbol: &lsp_types::DocumentSymbol) -> text_trees::StringTreeNode { + text_trees::StringTreeNode::with_child_nodes( + format!( + "{}{}", + symbol.name, + symbol + .detail + .as_ref() + .map_or_else(String::new, |s| format!(" ({})", s)) + ), + symbol.children.iter().flatten().map(format_document_symbol), + ) +} diff --git a/crates/mun_language_server/tests/integration_tests/initialization.rs b/crates/mun_language_server/tests/integration_tests/initialization.rs new file mode 100644 index 000000000..11bacd308 --- /dev/null +++ b/crates/mun_language_server/tests/integration_tests/initialization.rs @@ -0,0 +1,20 @@ +use crate::Project; + +#[test] +fn test_server() { + let _server = Project::with_fixture( + r#" +//- /mun.toml +[package] +name = "foo" +version = "0.0.0" + +//- /src/mod.mun +fn add(a: i32, b: i32) -> i32 { + a + b +} +"#, + ) + .server() + .wait_until_workspace_is_loaded(); +} diff --git a/crates/mun_language_server/tests/integration_tests/main.rs b/crates/mun_language_server/tests/integration_tests/main.rs new file mode 100644 index 000000000..a336c791e --- /dev/null +++ b/crates/mun_language_server/tests/integration_tests/main.rs @@ -0,0 +1,5 @@ +mod document_symbols; +mod initialization; +mod support; + +pub use support::Project; diff --git a/crates/mun_language_server/tests/integration_tests/snapshots/integration_tests__document_symbols__document_symbols.snap b/crates/mun_language_server/tests/integration_tests/snapshots/integration_tests__document_symbols__document_symbols.snap new file mode 100644 index 000000000..80d7698e4 --- /dev/null +++ b/crates/mun_language_server/tests/integration_tests/snapshots/integration_tests__document_symbols__document_symbols.snap @@ -0,0 +1,13 @@ +--- +source: crates/mun_language_server/tests/integration_tests/document_symbols.rs +expression: format_document_symbols_response(symbols) +--- +Foo +'-- a (i32) +Bar (Foo) +impl Foo ++-- new (fn() -> Self) +'-- modify (fn(self) -> Self) +impl Foo +'-- modify2 (fn(self) -> Self) +main (fn() -> i32) diff --git a/crates/mun_language_server/tests/support.rs b/crates/mun_language_server/tests/integration_tests/support.rs similarity index 100% rename from crates/mun_language_server/tests/support.rs rename to crates/mun_language_server/tests/integration_tests/support.rs diff --git a/crates/mun_language_server/tests/snapshots/initialization__document_symbols.snap b/crates/mun_language_server/tests/snapshots/initialization__document_symbols.snap deleted file mode 100644 index f0d556e99..000000000 --- a/crates/mun_language_server/tests/snapshots/initialization__document_symbols.snap +++ /dev/null @@ -1,100 +0,0 @@ ---- -source: crates/mun_language_server/tests/initialization.rs -assertion_line: 50 -expression: symbols - ---- -Some( - Nested( - [ - DocumentSymbol { - name: "main", - detail: Some( - "fn() -> i32", - ), - kind: Function, - tags: None, - deprecated: None, - range: Range { - start: Position { - line: 0, - character: 0, - }, - end: Position { - line: 0, - character: 19, - }, - }, - selection_range: Range { - start: Position { - line: 0, - character: 3, - }, - end: Position { - line: 0, - character: 7, - }, - }, - children: None, - }, - DocumentSymbol { - name: "Foo", - detail: None, - kind: Struct, - tags: None, - deprecated: None, - range: Range { - start: Position { - line: 1, - character: 0, - }, - end: Position { - line: 1, - character: 13, - }, - }, - selection_range: Range { - start: Position { - line: 1, - character: 7, - }, - end: Position { - line: 1, - character: 10, - }, - }, - children: None, - }, - DocumentSymbol { - name: "Bar", - detail: Some( - "Foo", - ), - kind: TypeParameter, - tags: None, - deprecated: None, - range: Range { - start: Position { - line: 2, - character: 0, - }, - end: Position { - line: 2, - character: 15, - }, - }, - selection_range: Range { - start: Position { - line: 2, - character: 5, - }, - end: Position { - line: 2, - character: 8, - }, - }, - children: None, - }, - ], - ), -) diff --git a/crates/mun_syntax/src/ast/extensions.rs b/crates/mun_syntax/src/ast/extensions.rs index 0871100a3..412f01b0c 100644 --- a/crates/mun_syntax/src/ast/extensions.rs +++ b/crates/mun_syntax/src/ast/extensions.rs @@ -78,6 +78,12 @@ fn text_of_first_token(node: &SyntaxNode) -> TokenText<'_> { } } +impl ast::Path { + pub fn parent_path(&self) -> Option { + self.syntax().parent().and_then(ast::Path::cast) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum PathSegmentKind { Name(ast::NameRef), diff --git a/crates/mun_syntax/src/lib.rs b/crates/mun_syntax/src/lib.rs index efad6a38b..d30d69833 100644 --- a/crates/mun_syntax/src/lib.rs +++ b/crates/mun_syntax/src/lib.rs @@ -176,10 +176,10 @@ macro_rules! match_ast { (match $node:ident { $($tt:tt)* }) => { match_ast!(match ($node) { $($tt)* }) }; (match ($node:expr) { - $( ast::$ast:ident($it:ident) => $res:expr, )* + $( $( $path:ident )::+ ($it:pat) => $res:expr, )* _ => $catch_all:expr $(,)? }) => {{ - $( if let Some($it) = ast::$ast::cast($node.clone()) { $res } else )* + $( if let Some($it) = $($path::)+cast($node.clone()) { $res } else )* { $catch_all } }}; }