Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/ide-completion/src/patterns.rs')
-rw-r--r--crates/ide-completion/src/patterns.rs521
1 files changed, 521 insertions, 0 deletions
diff --git a/crates/ide-completion/src/patterns.rs b/crates/ide-completion/src/patterns.rs
new file mode 100644
index 0000000000..6fdec78385
--- /dev/null
+++ b/crates/ide-completion/src/patterns.rs
@@ -0,0 +1,521 @@
+//! Patterns telling us certain facts about current syntax element, they are used in completion context
+//!
+//! Most logic in this module first expands the token below the cursor to a maximum node that acts similar to the token itself.
+//! This means we for example expand a NameRef token to its outermost Path node, as semantically these act in the same location
+//! and the completions usually query for path specific things on the Path context instead. This simplifies some location handling.
+
+use hir::Semantics;
+use ide_db::RootDatabase;
+use syntax::{
+ algo::non_trivia_sibling,
+ ast::{self, HasArgList, HasLoopBody, HasName},
+ match_ast, AstNode, Direction, SyntaxElement,
+ SyntaxKind::*,
+ SyntaxNode, SyntaxToken, TextRange, TextSize,
+};
+
+#[cfg(test)]
+use crate::tests::check_pattern_is_applicable;
+
+/// Immediate previous node to what we are completing.
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub(crate) enum ImmediatePrevSibling {
+ IfExpr,
+ TraitDefName,
+ ImplDefType,
+ Visibility,
+ Attribute,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub(crate) enum TypeAnnotation {
+ Let(Option<ast::Pat>),
+ FnParam(Option<ast::Pat>),
+ RetType(Option<ast::Expr>),
+ Const(Option<ast::Expr>),
+}
+
+/// Direct parent "thing" of what we are currently completing.
+///
+/// This may contain nodes of the fake file as well as the original, comments on the variants specify
+/// from which file the nodes are.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub(crate) enum ImmediateLocation {
+ Impl,
+ Trait,
+ TupleField,
+ RefExpr,
+ IdentPat,
+ StmtList,
+ ItemList,
+ TypeBound,
+ /// Original file ast node
+ TypeAnnotation(TypeAnnotation),
+ /// Original file ast node
+ MethodCall {
+ receiver: Option<ast::Expr>,
+ has_parens: bool,
+ },
+ /// Original file ast node
+ FieldAccess {
+ receiver: Option<ast::Expr>,
+ receiver_is_ambiguous_float_literal: bool,
+ },
+ // Only set from a type arg
+ /// Original file ast node
+ GenericArgList(ast::GenericArgList),
+ /// The record expr of the field name we are completing
+ ///
+ /// Original file ast node
+ RecordExpr(ast::RecordExpr),
+ /// The record expr of the functional update syntax we are completing
+ ///
+ /// Original file ast node
+ RecordExprUpdate(ast::RecordExpr),
+ /// The record pat of the field name we are completing
+ ///
+ /// Original file ast node
+ // FIXME: This should be moved to pattern_ctx
+ RecordPat(ast::RecordPat),
+}
+
+pub(crate) fn determine_prev_sibling(name_like: &ast::NameLike) -> Option<ImmediatePrevSibling> {
+ let node = match name_like {
+ ast::NameLike::NameRef(name_ref) => maximize_name_ref(name_ref),
+ ast::NameLike::Name(n) => n.syntax().clone(),
+ ast::NameLike::Lifetime(lt) => lt.syntax().clone(),
+ };
+ let node = match node.parent().and_then(ast::MacroCall::cast) {
+ // When a path is being typed after the name of a trait/type of an impl it is being
+ // parsed as a macro, so when the trait/impl has a block following it an we are between the
+ // name and block the macro will attach the block to itself so maximizing fails to take
+ // that into account
+ // FIXME path expr and statement have a similar problem with attrs
+ Some(call)
+ if call.excl_token().is_none()
+ && call.token_tree().map_or(false, |t| t.l_curly_token().is_some())
+ && call.semicolon_token().is_none() =>
+ {
+ call.syntax().clone()
+ }
+ _ => node,
+ };
+ let prev_sibling = non_trivia_sibling(node.into(), Direction::Prev)?.into_node()?;
+ if prev_sibling.kind() == ERROR {
+ let prev_sibling = prev_sibling.first_child()?;
+ let res = match_ast! {
+ match prev_sibling {
+ // vis followed by random ident will always error the parser
+ ast::Visibility(_) => ImmediatePrevSibling::Visibility,
+ _ => return None,
+ }
+ };
+ return Some(res);
+ }
+ let res = match_ast! {
+ match prev_sibling {
+ ast::ExprStmt(it) => {
+ let node = it.expr().filter(|_| it.semicolon_token().is_none())?.syntax().clone();
+ match_ast! {
+ match node {
+ ast::IfExpr(_) => ImmediatePrevSibling::IfExpr,
+ _ => return None,
+ }
+ }
+ },
+ ast::Trait(it) => if it.assoc_item_list().is_none() {
+ ImmediatePrevSibling::TraitDefName
+ } else {
+ return None
+ },
+ ast::Impl(it) => if it.assoc_item_list().is_none()
+ && (it.for_token().is_none() || it.self_ty().is_some()) {
+ ImmediatePrevSibling::ImplDefType
+ } else {
+ return None
+ },
+ ast::Attr(_) => ImmediatePrevSibling::Attribute,
+ _ => return None,
+ }
+ };
+ Some(res)
+}
+
+pub(crate) fn determine_location(
+ sema: &Semantics<RootDatabase>,
+ original_file: &SyntaxNode,
+ offset: TextSize,
+ name_like: &ast::NameLike,
+) -> Option<ImmediateLocation> {
+ let node = match name_like {
+ ast::NameLike::NameRef(name_ref) => {
+ if ast::RecordExprField::for_field_name(name_ref).is_some() {
+ return sema
+ .find_node_at_offset_with_macros(original_file, offset)
+ .map(ImmediateLocation::RecordExpr);
+ }
+ if ast::RecordPatField::for_field_name_ref(name_ref).is_some() {
+ return sema
+ .find_node_at_offset_with_macros(original_file, offset)
+ .map(ImmediateLocation::RecordPat);
+ }
+ maximize_name_ref(name_ref)
+ }
+ ast::NameLike::Name(name) => {
+ if ast::RecordPatField::for_field_name(name).is_some() {
+ return sema
+ .find_node_at_offset_with_macros(original_file, offset)
+ .map(ImmediateLocation::RecordPat);
+ }
+ name.syntax().clone()
+ }
+ ast::NameLike::Lifetime(lt) => lt.syntax().clone(),
+ };
+
+ match_ast! {
+ match node {
+ ast::TypeBoundList(_it) => return Some(ImmediateLocation::TypeBound),
+ _ => (),
+ }
+ };
+
+ let parent = match node.parent() {
+ Some(parent) => match ast::MacroCall::cast(parent.clone()) {
+ // When a path is being typed in an (Assoc)ItemList the parser will always emit a macro_call.
+ // This is usually fine as the node expansion code above already accounts for that with
+ // the ancestors call, but there is one exception to this which is that when an attribute
+ // precedes it the code above will not walk the Path to the parent MacroCall as their ranges differ.
+ // FIXME path expr and statement have a similar problem
+ Some(call)
+ if call.excl_token().is_none()
+ && call.token_tree().is_none()
+ && call.semicolon_token().is_none() =>
+ {
+ call.syntax().parent()?
+ }
+ _ => parent,
+ },
+ // SourceFile
+ None => {
+ return match node.kind() {
+ MACRO_ITEMS | SOURCE_FILE => Some(ImmediateLocation::ItemList),
+ _ => None,
+ }
+ }
+ };
+
+ let res = match_ast! {
+ match parent {
+ ast::IdentPat(_) => ImmediateLocation::IdentPat,
+ ast::StmtList(_) => ImmediateLocation::StmtList,
+ ast::SourceFile(_) => ImmediateLocation::ItemList,
+ ast::ItemList(_) => ImmediateLocation::ItemList,
+ ast::RefExpr(_) => ImmediateLocation::RefExpr,
+ ast::RecordExprFieldList(_) => sema
+ .find_node_at_offset_with_macros(original_file, offset)
+ .map(ImmediateLocation::RecordExprUpdate)?,
+ ast::TupleField(_) => ImmediateLocation::TupleField,
+ ast::TupleFieldList(_) => ImmediateLocation::TupleField,
+ ast::TypeBound(_) => ImmediateLocation::TypeBound,
+ ast::TypeBoundList(_) => ImmediateLocation::TypeBound,
+ ast::AssocItemList(it) => match it.syntax().parent().map(|it| it.kind()) {
+ Some(IMPL) => ImmediateLocation::Impl,
+ Some(TRAIT) => ImmediateLocation::Trait,
+ _ => return None,
+ },
+ ast::GenericArgList(_) => sema
+ .find_node_at_offset_with_macros(original_file, offset)
+ .map(ImmediateLocation::GenericArgList)?,
+ ast::FieldExpr(it) => {
+ let receiver = find_in_original_file(it.expr(), original_file);
+ let receiver_is_ambiguous_float_literal = if let Some(ast::Expr::Literal(l)) = &receiver {
+ match l.kind() {
+ ast::LiteralKind::FloatNumber { .. } => l.token().text().ends_with('.'),
+ _ => false,
+ }
+ } else {
+ false
+ };
+ ImmediateLocation::FieldAccess {
+ receiver,
+ receiver_is_ambiguous_float_literal,
+ }
+ },
+ ast::MethodCallExpr(it) => ImmediateLocation::MethodCall {
+ receiver: find_in_original_file(it.receiver(), original_file),
+ has_parens: it.arg_list().map_or(false, |it| it.l_paren_token().is_some())
+ },
+ ast::Const(it) => {
+ if !it.ty().map_or(false, |x| x.syntax().text_range().contains(offset)) {
+ return None;
+ }
+ let name = find_in_original_file(it.name(), original_file)?;
+ let original = ast::Const::cast(name.syntax().parent()?)?;
+ ImmediateLocation::TypeAnnotation(TypeAnnotation::Const(original.body()))
+ },
+ ast::RetType(it) => {
+ if it.thin_arrow_token().is_none() {
+ return None;
+ }
+ if !it.ty().map_or(false, |x| x.syntax().text_range().contains(offset)) {
+ return None;
+ }
+ let parent = match ast::Fn::cast(parent.parent()?) {
+ Some(x) => x.param_list(),
+ None => ast::ClosureExpr::cast(parent.parent()?)?.param_list(),
+ };
+ let parent = find_in_original_file(parent, original_file)?.syntax().parent()?;
+ ImmediateLocation::TypeAnnotation(TypeAnnotation::RetType(match_ast! {
+ match parent {
+ ast::ClosureExpr(it) => {
+ it.body()
+ },
+ ast::Fn(it) => {
+ it.body().map(ast::Expr::BlockExpr)
+ },
+ _ => return None,
+ }
+ }))
+ },
+ ast::Param(it) => {
+ if it.colon_token().is_none() {
+ return None;
+ }
+ if !it.ty().map_or(false, |x| x.syntax().text_range().contains(offset)) {
+ return None;
+ }
+ ImmediateLocation::TypeAnnotation(TypeAnnotation::FnParam(find_in_original_file(it.pat(), original_file)))
+ },
+ ast::LetStmt(it) => {
+ if it.colon_token().is_none() {
+ return None;
+ }
+ if !it.ty().map_or(false, |x| x.syntax().text_range().contains(offset)) {
+ return None;
+ }
+ ImmediateLocation::TypeAnnotation(TypeAnnotation::Let(find_in_original_file(it.pat(), original_file)))
+ },
+ _ => return None,
+ }
+ };
+ fn find_in_original_file<N: AstNode>(x: Option<N>, original_file: &SyntaxNode) -> Option<N> {
+ x.map(|e| e.syntax().text_range()).and_then(|r| find_node_with_range(original_file, r))
+ }
+ Some(res)
+}
+
+/// Maximize a nameref to its enclosing path if its the last segment of said path.
+/// That is, when completing a [`NameRef`] we actually handle it as the path it is part of when determining
+/// its location.
+fn maximize_name_ref(name_ref: &ast::NameRef) -> SyntaxNode {
+ if let Some(segment) = name_ref.syntax().parent().and_then(ast::PathSegment::cast) {
+ let p = segment.parent_path();
+ if p.parent_path().is_none() {
+ // Get rid of PathExpr, PathType, etc...
+ let path = p
+ .syntax()
+ .ancestors()
+ .take_while(|it| it.text_range() == p.syntax().text_range())
+ .last();
+ if let Some(it) = path {
+ return it;
+ }
+ }
+ }
+ name_ref.syntax().clone()
+}
+
+fn find_node_with_range<N: AstNode>(syntax: &SyntaxNode, range: TextRange) -> Option<N> {
+ let range = syntax.text_range().intersect(range)?;
+ syntax.covering_element(range).ancestors().find_map(N::cast)
+}
+
+pub(crate) fn previous_token(element: SyntaxElement) -> Option<SyntaxToken> {
+ element.into_token().and_then(previous_non_trivia_token)
+}
+
+pub(crate) fn is_in_token_of_for_loop(element: SyntaxElement) -> bool {
+ // oh my ...
+ (|| {
+ let syntax_token = element.into_token()?;
+ let range = syntax_token.text_range();
+ let for_expr = syntax_token.ancestors().find_map(ast::ForExpr::cast)?;
+
+ // check if the current token is the `in` token of a for loop
+ if let Some(token) = for_expr.in_token() {
+ return Some(syntax_token == token);
+ }
+ let pat = for_expr.pat()?;
+ if range.end() < pat.syntax().text_range().end() {
+ // if we are inside or before the pattern we can't be at the `in` token position
+ return None;
+ }
+ let next_sibl = next_non_trivia_sibling(pat.syntax().clone().into())?;
+ Some(match next_sibl {
+ // the loop body is some node, if our token is at the start we are at the `in` position,
+ // otherwise we could be in a recovered expression, we don't wanna ruin completions there
+ syntax::NodeOrToken::Node(n) => n.text_range().start() == range.start(),
+ // the loop body consists of a single token, if we are this we are certainly at the `in` token position
+ syntax::NodeOrToken::Token(t) => t == syntax_token,
+ })
+ })()
+ .unwrap_or(false)
+}
+
+#[test]
+fn test_for_is_prev2() {
+ check_pattern_is_applicable(r"fn __() { for i i$0 }", is_in_token_of_for_loop);
+}
+
+pub(crate) fn is_in_loop_body(node: &SyntaxNode) -> bool {
+ node.ancestors()
+ .take_while(|it| it.kind() != FN && it.kind() != CLOSURE_EXPR)
+ .find_map(|it| {
+ let loop_body = match_ast! {
+ match it {
+ ast::ForExpr(it) => it.loop_body(),
+ ast::WhileExpr(it) => it.loop_body(),
+ ast::LoopExpr(it) => it.loop_body(),
+ _ => None,
+ }
+ };
+ loop_body.filter(|it| it.syntax().text_range().contains_range(node.text_range()))
+ })
+ .is_some()
+}
+
+fn previous_non_trivia_token(token: SyntaxToken) -> Option<SyntaxToken> {
+ let mut token = token.prev_token();
+ while let Some(inner) = token {
+ if !inner.kind().is_trivia() {
+ return Some(inner);
+ } else {
+ token = inner.prev_token();
+ }
+ }
+ None
+}
+
+fn next_non_trivia_sibling(ele: SyntaxElement) -> Option<SyntaxElement> {
+ let mut e = ele.next_sibling_or_token();
+ while let Some(inner) = e {
+ if !inner.kind().is_trivia() {
+ return Some(inner);
+ } else {
+ e = inner.next_sibling_or_token();
+ }
+ }
+ None
+}
+
+#[cfg(test)]
+mod tests {
+ use syntax::algo::find_node_at_offset;
+
+ use crate::tests::position;
+
+ use super::*;
+
+ fn check_location(code: &str, loc: impl Into<Option<ImmediateLocation>>) {
+ let (db, pos) = position(code);
+
+ let sema = Semantics::new(&db);
+ let original_file = sema.parse(pos.file_id);
+
+ let name_like = find_node_at_offset(original_file.syntax(), pos.offset).unwrap();
+ assert_eq!(
+ determine_location(&sema, original_file.syntax(), pos.offset, &name_like),
+ loc.into()
+ );
+ }
+
+ fn check_prev_sibling(code: &str, sibling: impl Into<Option<ImmediatePrevSibling>>) {
+ check_pattern_is_applicable(code, |e| {
+ let name = &e.parent().and_then(ast::NameLike::cast).expect("Expected a namelike");
+ assert_eq!(determine_prev_sibling(name), sibling.into());
+ true
+ });
+ }
+
+ #[test]
+ fn test_trait_loc() {
+ check_location(r"trait A { f$0 }", ImmediateLocation::Trait);
+ check_location(r"trait A { #[attr] f$0 }", ImmediateLocation::Trait);
+ check_location(r"trait A { f$0 fn f() {} }", ImmediateLocation::Trait);
+ check_location(r"trait A { fn f() {} f$0 }", ImmediateLocation::Trait);
+ check_location(r"trait A$0 {}", None);
+ check_location(r"trait A { fn f$0 }", None);
+ }
+
+ #[test]
+ fn test_impl_loc() {
+ check_location(r"impl A { f$0 }", ImmediateLocation::Impl);
+ check_location(r"impl A { #[attr] f$0 }", ImmediateLocation::Impl);
+ check_location(r"impl A { f$0 fn f() {} }", ImmediateLocation::Impl);
+ check_location(r"impl A { fn f() {} f$0 }", ImmediateLocation::Impl);
+ check_location(r"impl A$0 {}", None);
+ check_location(r"impl A { fn f$0 }", None);
+ }
+
+ #[test]
+ fn test_block_expr_loc() {
+ check_location(r"fn my_fn() { let a = 2; f$0 }", ImmediateLocation::StmtList);
+ check_location(r"fn my_fn() { f$0 f }", ImmediateLocation::StmtList);
+ }
+
+ #[test]
+ fn test_ident_pat_loc() {
+ check_location(r"fn my_fn(m$0) {}", ImmediateLocation::IdentPat);
+ check_location(r"fn my_fn() { let m$0 }", ImmediateLocation::IdentPat);
+ check_location(r"fn my_fn(&m$0) {}", ImmediateLocation::IdentPat);
+ check_location(r"fn my_fn() { let &m$0 }", ImmediateLocation::IdentPat);
+ }
+
+ #[test]
+ fn test_ref_expr_loc() {
+ check_location(r"fn my_fn() { let x = &m$0 foo; }", ImmediateLocation::RefExpr);
+ }
+
+ #[test]
+ fn test_item_list_loc() {
+ check_location(r"i$0", ImmediateLocation::ItemList);
+ check_location(r"#[attr] i$0", ImmediateLocation::ItemList);
+ check_location(r"fn f() {} i$0", ImmediateLocation::ItemList);
+ check_location(r"mod foo { f$0 }", ImmediateLocation::ItemList);
+ check_location(r"mod foo { #[attr] f$0 }", ImmediateLocation::ItemList);
+ check_location(r"mod foo { fn f() {} f$0 }", ImmediateLocation::ItemList);
+ check_location(r"mod foo$0 {}", None);
+ }
+
+ #[test]
+ fn test_impl_prev_sibling() {
+ check_prev_sibling(r"impl A w$0 ", ImmediatePrevSibling::ImplDefType);
+ check_prev_sibling(r"impl A w$0 {}", ImmediatePrevSibling::ImplDefType);
+ check_prev_sibling(r"impl A for A w$0 ", ImmediatePrevSibling::ImplDefType);
+ check_prev_sibling(r"impl A for A w$0 {}", ImmediatePrevSibling::ImplDefType);
+ check_prev_sibling(r"impl A for w$0 {}", None);
+ check_prev_sibling(r"impl A for w$0", None);
+ }
+
+ #[test]
+ fn test_trait_prev_sibling() {
+ check_prev_sibling(r"trait A w$0 ", ImmediatePrevSibling::TraitDefName);
+ check_prev_sibling(r"trait A w$0 {}", ImmediatePrevSibling::TraitDefName);
+ }
+
+ #[test]
+ fn test_if_expr_prev_sibling() {
+ check_prev_sibling(r"fn foo() { if true {} w$0", ImmediatePrevSibling::IfExpr);
+ check_prev_sibling(r"fn foo() { if true {}; w$0", None);
+ }
+
+ #[test]
+ fn test_vis_prev_sibling() {
+ check_prev_sibling(r"pub w$0", ImmediatePrevSibling::Visibility);
+ }
+
+ #[test]
+ fn test_attr_prev_sibling() {
+ check_prev_sibling(r"#[attr] w$0", ImmediatePrevSibling::Attribute);
+ }
+}