mod analysis;
#[cfg(test)]
mod tests;
use std::{iter, ops::ControlFlow};
use hir::{
HasAttrs, Local, ModuleSource, Name, PathResolution, ScopeDef, Semantics, SemanticsScope,
Symbol, Type, TypeInfo,
};
use ide_db::{
base_db::SourceDatabase, famous_defs::FamousDefs, helpers::is_editable_crate, FilePosition,
FxHashMap, FxHashSet, RootDatabase,
};
use syntax::{
ast::{self, AttrKind, NameOrNameRef},
match_ast, AstNode, Edition, SmolStr,
SyntaxKind::{self, *},
SyntaxToken, TextRange, TextSize, T,
};
use crate::{
context::analysis::{expand_and_analyze, AnalysisResult},
CompletionConfig,
};
const COMPLETION_MARKER: &str = "raCompletionMarker";
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum PatternRefutability {
Refutable,
Irrefutable,
}
#[derive(Debug)]
pub(crate) enum Visible {
Yes,
Editable,
No,
}
#[derive(Debug, Default)]
pub(crate) struct QualifierCtx {
pub(crate) async_tok: Option<SyntaxToken>,
pub(crate) unsafe_tok: Option<SyntaxToken>,
pub(crate) safe_tok: Option<SyntaxToken>,
pub(crate) vis_node: Option<ast::Visibility>,
}
impl QualifierCtx {
pub(crate) fn none(&self) -> bool {
self.async_tok.is_none()
&& self.unsafe_tok.is_none()
&& self.safe_tok.is_none()
&& self.vis_node.is_none()
}
}
#[derive(Debug)]
pub(crate) struct PathCompletionCtx {
pub(crate) has_call_parens: bool,
pub(crate) has_macro_bang: bool,
pub(crate) qualified: Qualified,
pub(crate) parent: Option<ast::Path>,
#[allow(dead_code)]
pub(crate) path: ast::Path,
pub(crate) original_path: Option<ast::Path>,
pub(crate) kind: PathKind,
pub(crate) has_type_args: bool,
pub(crate) use_tree_parent: bool,
}
impl PathCompletionCtx {
pub(crate) fn is_trivial_path(&self) -> bool {
matches!(
self,
PathCompletionCtx {
has_call_parens: false,
has_macro_bang: false,
qualified: Qualified::No,
parent: None,
has_type_args: false,
..
}
)
}
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum PathKind {
Expr {
expr_ctx: PathExprCtx,
},
Type {
location: TypeLocation,
},
Attr {
attr_ctx: AttrCtx,
},
Derive {
existing_derives: ExistingDerives,
},
Item {
kind: ItemListKind,
},
Pat {
pat_ctx: PatternContext,
},
Vis {
has_in_token: bool,
},
Use,
}
pub(crate) type ExistingDerives = FxHashSet<hir::Macro>;
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct AttrCtx {
pub(crate) kind: AttrKind,
pub(crate) annotated_item_kind: Option<SyntaxKind>,
pub(crate) derive_helpers: Vec<(Symbol, Symbol)>,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct PathExprCtx {
pub(crate) in_block_expr: bool,
pub(crate) in_breakable: BreakableKind,
pub(crate) after_if_expr: bool,
pub(crate) in_condition: bool,
pub(crate) incomplete_let: bool,
pub(crate) ref_expr_parent: Option<ast::RefExpr>,
pub(crate) is_func_update: Option<ast::RecordExpr>,
pub(crate) self_param: Option<hir::SelfParam>,
pub(crate) innermost_ret_ty: Option<hir::Type>,
pub(crate) impl_: Option<ast::Impl>,
pub(crate) in_match_guard: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum TypeLocation {
TupleField,
TypeAscription(TypeAscriptionTarget),
GenericArg {
args: Option<ast::GenericArgList>,
of_trait: Option<hir::Trait>,
corresponding_param: Option<ast::GenericParam>,
},
AssocTypeEq,
AssocConstEq,
TypeBound,
ImplTarget,
ImplTrait,
Other,
}
impl TypeLocation {
pub(crate) fn complete_lifetimes(&self) -> bool {
matches!(
self,
TypeLocation::GenericArg {
corresponding_param: Some(ast::GenericParam::LifetimeParam(_)),
..
}
)
}
pub(crate) fn complete_consts(&self) -> bool {
matches!(
self,
TypeLocation::GenericArg {
corresponding_param: Some(ast::GenericParam::ConstParam(_)),
..
} | TypeLocation::AssocConstEq
)
}
pub(crate) fn complete_types(&self) -> bool {
match self {
TypeLocation::GenericArg { corresponding_param: Some(param), .. } => {
matches!(param, ast::GenericParam::TypeParam(_))
}
TypeLocation::AssocConstEq => false,
TypeLocation::AssocTypeEq => true,
TypeLocation::ImplTrait => false,
_ => true,
}
}
pub(crate) fn complete_self_type(&self) -> bool {
self.complete_types() && !matches!(self, TypeLocation::ImplTarget | TypeLocation::ImplTrait)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum TypeAscriptionTarget {
Let(Option<ast::Pat>),
FnParam(Option<ast::Pat>),
RetType(Option<ast::Expr>),
Const(Option<ast::Expr>),
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum ItemListKind {
SourceFile,
Module,
Impl,
TraitImpl(Option<ast::Impl>),
Trait,
ExternBlock { is_unsafe: bool },
}
#[derive(Debug)]
pub(crate) enum Qualified {
No,
With {
path: ast::Path,
resolution: Option<PathResolution>,
super_chain_len: Option<usize>,
},
TypeAnchor {
ty: Option<hir::Type>,
trait_: Option<hir::Trait>,
},
Absolute,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PatternContext {
pub(crate) refutability: PatternRefutability,
pub(crate) param_ctx: Option<ParamContext>,
pub(crate) has_type_ascription: bool,
pub(crate) should_suggest_name: bool,
pub(crate) parent_pat: Option<ast::Pat>,
pub(crate) ref_token: Option<SyntaxToken>,
pub(crate) mut_token: Option<SyntaxToken>,
pub(crate) record_pat: Option<ast::RecordPat>,
pub(crate) impl_: Option<ast::Impl>,
pub(crate) missing_variants: Vec<hir::Variant>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ParamContext {
pub(crate) param_list: ast::ParamList,
pub(crate) param: ast::Param,
pub(crate) kind: ParamKind,
}
#[derive(Debug)]
pub(crate) struct LifetimeContext {
pub(crate) kind: LifetimeKind,
}
#[derive(Debug)]
pub(crate) enum LifetimeKind {
LifetimeParam,
Lifetime { in_lifetime_param_bound: bool, def: Option<hir::GenericDef> },
LabelRef,
LabelDef,
}
#[derive(Debug)]
pub(crate) struct NameContext {
#[allow(dead_code)]
pub(crate) name: Option<ast::Name>,
pub(crate) kind: NameKind,
}
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) enum NameKind {
Const,
ConstParam,
Enum,
Function,
IdentPat(PatternContext),
MacroDef,
MacroRules,
Module(ast::Module),
RecordField,
Rename,
SelfParam,
Static,
Struct,
Trait,
TypeAlias,
TypeParam,
Union,
Variant,
}
#[derive(Debug)]
pub(crate) struct NameRefContext {
pub(crate) nameref: Option<ast::NameRef>,
pub(crate) kind: NameRefKind,
}
#[derive(Debug)]
pub(crate) enum NameRefKind {
Path(PathCompletionCtx),
DotAccess(DotAccess),
Keyword(ast::Item),
RecordExpr {
dot_prefix: bool,
expr: ast::RecordExpr,
},
Pattern(PatternContext),
ExternCrate,
}
#[derive(Debug)]
pub(crate) enum CompletionAnalysis {
Name(NameContext),
NameRef(NameRefContext),
Lifetime(LifetimeContext),
String {
original: ast::String,
expanded: Option<ast::String>,
},
UnexpandedAttrTT {
colon_prefix: bool,
fake_attribute_under_caret: Option<ast::Attr>,
extern_crate: Option<ast::ExternCrate>,
},
}
#[derive(Debug)]
pub(crate) struct DotAccess {
pub(crate) receiver: Option<ast::Expr>,
pub(crate) receiver_ty: Option<TypeInfo>,
pub(crate) kind: DotAccessKind,
pub(crate) ctx: DotAccessExprCtx,
}
#[derive(Debug)]
pub(crate) enum DotAccessKind {
Field {
receiver_is_ambiguous_float_literal: bool,
},
Method {
has_parens: bool,
},
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct DotAccessExprCtx {
pub(crate) in_block_expr: bool,
pub(crate) in_breakable: BreakableKind,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum BreakableKind {
None,
Loop,
For,
While,
Block,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum ParamKind {
Function(ast::Fn),
Closure(ast::ClosureExpr),
}
#[derive(Debug)]
pub(crate) struct CompletionContext<'a> {
pub(crate) sema: Semantics<'a, RootDatabase>,
pub(crate) scope: SemanticsScope<'a>,
pub(crate) db: &'a RootDatabase,
pub(crate) config: &'a CompletionConfig,
pub(crate) position: FilePosition,
pub(crate) original_token: SyntaxToken,
pub(crate) token: SyntaxToken,
pub(crate) krate: hir::Crate,
pub(crate) module: hir::Module,
is_nightly: bool,
pub(crate) edition: Edition,
pub(crate) expected_name: Option<NameOrNameRef>,
pub(crate) expected_type: Option<Type>,
pub(crate) qualifier_ctx: QualifierCtx,
pub(crate) locals: FxHashMap<Name, Local>,
pub(crate) depth_from_crate_root: usize,
pub(crate) complete_semicolon: CompleteSemicolon,
}
#[derive(Debug)]
pub(crate) enum CompleteSemicolon {
DoNotComplete,
CompleteSemi,
CompleteComma,
}
impl CompletionContext<'_> {
pub(crate) fn source_range(&self) -> TextRange {
let kind = self.original_token.kind();
match kind {
CHAR => {
cov_mark::hit!(completes_if_lifetime_without_idents);
TextRange::at(self.original_token.text_range().start(), TextSize::from(1))
}
LIFETIME_IDENT | UNDERSCORE | INT_NUMBER => self.original_token.text_range(),
_ if kind.is_any_identifier() => self.original_token.text_range(),
_ => TextRange::empty(self.position.offset),
}
}
pub(crate) fn famous_defs(&self) -> FamousDefs<'_, '_> {
FamousDefs(&self.sema, self.krate)
}
pub(crate) fn def_is_visible(&self, item: &ScopeDef) -> Visible {
match item {
ScopeDef::ModuleDef(def) => match def {
hir::ModuleDef::Module(it) => self.is_visible(it),
hir::ModuleDef::Function(it) => self.is_visible(it),
hir::ModuleDef::Adt(it) => self.is_visible(it),
hir::ModuleDef::Variant(it) => self.is_visible(it),
hir::ModuleDef::Const(it) => self.is_visible(it),
hir::ModuleDef::Static(it) => self.is_visible(it),
hir::ModuleDef::Trait(it) => self.is_visible(it),
hir::ModuleDef::TraitAlias(it) => self.is_visible(it),
hir::ModuleDef::TypeAlias(it) => self.is_visible(it),
hir::ModuleDef::Macro(it) => self.is_visible(it),
hir::ModuleDef::BuiltinType(_) => Visible::Yes,
},
ScopeDef::GenericParam(_)
| ScopeDef::ImplSelfType(_)
| ScopeDef::AdtSelfType(_)
| ScopeDef::Local(_)
| ScopeDef::Label(_)
| ScopeDef::Unknown => Visible::Yes,
}
}
pub(crate) fn is_visible<I>(&self, item: &I) -> Visible
where
I: hir::HasVisibility + hir::HasAttrs + hir::HasCrate + Copy,
{
let vis = item.visibility(self.db);
let attrs = item.attrs(self.db);
self.is_visible_impl(&vis, &attrs, item.krate(self.db))
}
pub(crate) fn doc_aliases<I>(&self, item: &I) -> Vec<SmolStr>
where
I: hir::HasAttrs + Copy,
{
let attrs = item.attrs(self.db);
attrs.doc_aliases().map(|it| it.as_str().into()).collect()
}
pub(crate) fn is_item_hidden(&self, item: &hir::ItemInNs) -> bool {
let attrs = item.attrs(self.db);
let krate = item.krate(self.db);
match (attrs, krate) {
(Some(attrs), Some(krate)) => self.is_doc_hidden(&attrs, krate),
_ => false,
}
}
pub(crate) fn check_stability(&self, attrs: Option<&hir::Attrs>) -> bool {
let Some(attrs) = attrs else {
return true;
};
!attrs.is_unstable() || self.is_nightly
}
pub(crate) fn is_ops_trait(&self, trait_: hir::Trait) -> bool {
match trait_.attrs(self.db).lang() {
Some(lang) => OP_TRAIT_LANG_NAMES.contains(&lang.as_str()),
None => false,
}
}
pub(crate) fn is_doc_notable_trait(&self, trait_: hir::Trait) -> bool {
trait_.attrs(self.db).has_doc_notable_trait()
}
pub(crate) fn traits_in_scope(&self) -> hir::VisibleTraits {
let mut traits_in_scope = self.scope.visible_traits();
if let Some(drop) = self.famous_defs().core_ops_Drop() {
traits_in_scope.0.remove(&drop.into());
}
traits_in_scope
}
pub(crate) fn iterate_path_candidates(
&self,
ty: &hir::Type,
mut cb: impl FnMut(hir::AssocItem),
) {
let mut seen = FxHashSet::default();
ty.iterate_path_candidates(
self.db,
&self.scope,
&self.traits_in_scope(),
Some(self.module),
None,
|item| {
if seen.insert(item) {
cb(item)
}
None::<()>
},
);
}
pub(crate) fn process_all_names(&self, f: &mut dyn FnMut(Name, ScopeDef, Vec<SmolStr>)) {
let _p = tracing::info_span!("CompletionContext::process_all_names").entered();
self.scope.process_all_names(&mut |name, def| {
if self.is_scope_def_hidden(def) {
return;
}
let doc_aliases = self.doc_aliases_in_scope(def);
f(name, def, doc_aliases);
});
}
pub(crate) fn process_all_names_raw(&self, f: &mut dyn FnMut(Name, ScopeDef)) {
let _p = tracing::info_span!("CompletionContext::process_all_names_raw").entered();
self.scope.process_all_names(f);
}
fn is_scope_def_hidden(&self, scope_def: ScopeDef) -> bool {
if let (Some(attrs), Some(krate)) = (scope_def.attrs(self.db), scope_def.krate(self.db)) {
return self.is_doc_hidden(&attrs, krate);
}
false
}
fn is_visible_impl(
&self,
vis: &hir::Visibility,
attrs: &hir::Attrs,
defining_crate: hir::Crate,
) -> Visible {
if !vis.is_visible_from(self.db, self.module.into()) {
if !self.config.enable_private_editable {
return Visible::No;
}
return if is_editable_crate(defining_crate, self.db) {
Visible::Editable
} else {
Visible::No
};
}
if self.is_doc_hidden(attrs, defining_crate) {
Visible::No
} else {
Visible::Yes
}
}
fn is_doc_hidden(&self, attrs: &hir::Attrs, defining_crate: hir::Crate) -> bool {
self.krate != defining_crate && attrs.has_doc_hidden()
}
pub(crate) fn doc_aliases_in_scope(&self, scope_def: ScopeDef) -> Vec<SmolStr> {
if let Some(attrs) = scope_def.attrs(self.db) {
attrs.doc_aliases().map(|it| it.as_str().into()).collect()
} else {
vec![]
}
}
}
impl<'a> CompletionContext<'a> {
pub(crate) fn new(
db: &'a RootDatabase,
position @ FilePosition { file_id, offset }: FilePosition,
config: &'a CompletionConfig,
) -> Option<(CompletionContext<'a>, CompletionAnalysis)> {
let _p = tracing::info_span!("CompletionContext::new").entered();
let sema = Semantics::new(db);
let file_id = sema.attach_first_edition(file_id)?;
let original_file = sema.parse(file_id);
let file_with_fake_ident = {
let parse = db.parse(file_id);
parse.reparse(TextRange::empty(offset), COMPLETION_MARKER, file_id.edition()).tree()
};
let original_token = original_file.syntax().token_at_offset(offset).left_biased()?;
if original_token.kind() == T![:] {
let prev_token = original_token.prev_token()?;
if prev_token.kind() != T![:] {
return None;
}
if prev_token
.prev_token()
.map(|t| t.kind() == T![:] || t.kind() == T![::])
.unwrap_or(false)
{
return None;
}
}
let AnalysisResult {
analysis,
expected: (expected_type, expected_name),
qualifier_ctx,
token,
offset,
} = expand_and_analyze(
&sema,
original_file.syntax().clone(),
file_with_fake_ident.syntax().clone(),
offset,
&original_token,
)?;
let scope = sema.scope_at_offset(&token.parent()?, offset)?;
let krate = scope.krate();
let module = scope.module();
let edition = krate.edition(db);
let toolchain = db.toolchain_channel(krate.into());
let is_nightly = matches!(toolchain, Some(base_db::ReleaseChannel::Nightly) | None);
let mut locals = FxHashMap::default();
scope.process_all_names(&mut |name, scope| {
if let ScopeDef::Local(local) = scope {
locals.insert(name, local);
}
});
let depth_from_crate_root = iter::successors(Some(module), |m| m.parent(db))
.filter(|m| !matches!(m.definition_source(db).value, ModuleSource::BlockExpr(_)))
.count()
.saturating_sub(1);
let complete_semicolon = if config.add_semicolon_to_unit {
let inside_closure_ret = token.parent_ancestors().try_for_each(|ancestor| {
match_ast! {
match ancestor {
ast::BlockExpr(_) => ControlFlow::Break(false),
ast::ClosureExpr(_) => ControlFlow::Break(true),
_ => ControlFlow::Continue(())
}
}
});
if inside_closure_ret == ControlFlow::Break(true) {
CompleteSemicolon::DoNotComplete
} else {
let next_non_trivia_token =
std::iter::successors(token.next_token(), |it| it.next_token())
.find(|it| !it.kind().is_trivia());
let in_match_arm = token.parent_ancestors().try_for_each(|ancestor| {
if ast::MatchArm::can_cast(ancestor.kind()) {
ControlFlow::Break(true)
} else if matches!(
ancestor.kind(),
SyntaxKind::EXPR_STMT | SyntaxKind::BLOCK_EXPR
) {
ControlFlow::Break(false)
} else {
ControlFlow::Continue(())
}
});
let in_match_arm = match in_match_arm {
ControlFlow::Continue(()) => false,
ControlFlow::Break(it) => it,
};
let complete_token = if in_match_arm { T![,] } else { T![;] };
if next_non_trivia_token.map(|it| it.kind()) == Some(complete_token) {
CompleteSemicolon::DoNotComplete
} else if in_match_arm {
CompleteSemicolon::CompleteComma
} else {
CompleteSemicolon::CompleteSemi
}
}
} else {
CompleteSemicolon::DoNotComplete
};
let ctx = CompletionContext {
sema,
scope,
db,
config,
position,
original_token,
token,
krate,
module,
is_nightly,
edition,
expected_name,
expected_type,
qualifier_ctx,
locals,
depth_from_crate_root,
complete_semicolon,
};
Some((ctx, analysis))
}
}
const OP_TRAIT_LANG_NAMES: &[&str] = &[
"add_assign",
"add",
"bitand_assign",
"bitand",
"bitor_assign",
"bitor",
"bitxor_assign",
"bitxor",
"deref_mut",
"deref",
"div_assign",
"div",
"eq",
"fn_mut",
"fn_once",
"fn",
"index_mut",
"index",
"mul_assign",
"mul",
"neg",
"not",
"partial_ord",
"rem_assign",
"rem",
"shl_assign",
"shl",
"shr_assign",
"shr",
"sub",
];