Auto merge of #14433 - hecatia-elegua:alias-based-completion, r=Veykril

Add doc-alias based completion

Closes #14406.

I adapted the parsing code from the CfgExpr parsing code, maybe there's a better abstraction for both, or attribute parsing in general. It also includes `doc(hidden)`-parsing, which means it could replace the other function.
There are a few tests for parsing.

`process_all_names` changed the most, I added some docs there to explain what happens.

Many call sites just pass an empy vec to `add_path_resolution`'s `doc_aliases`, since either it doesn't make sense to pass anything (e.g. visibility completion) or I don't know where to get them from. Shouldn't really matter, as it will just not show aliases if the vec is empty and we can extend alias completion in these cases later.

I added two tests in `special.rs` for struct name completion (which was the main thing I wanted). I also tried function and field names, but these don't work yet. I want to add those in a follow-up PR.
This commit is contained in:
bors 2023-04-05 13:44:51 +00:00
commit 265f83031f
17 changed files with 334 additions and 41 deletions

View file

@ -1,5 +1,8 @@
//! A higher level attributes based on TokenTree, with also some shortcuts.
#[cfg(test)]
mod tests;
use std::{hash::Hash, ops, sync::Arc};
use base_db::CrateId;
@ -245,6 +248,14 @@ impl Attrs {
})
}
pub fn doc_exprs(&self) -> impl Iterator<Item = DocExpr> + '_ {
self.by_key("doc").tt_values().map(DocExpr::parse)
}
pub fn doc_aliases(&self) -> impl Iterator<Item = SmolStr> + '_ {
self.doc_exprs().flat_map(|doc_expr| doc_expr.aliases().to_vec())
}
pub fn is_proc_macro(&self) -> bool {
self.by_key("proc_macro").exists()
}
@ -258,6 +269,107 @@ impl Attrs {
}
}
use std::slice::Iter as SliceIter;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub enum DocAtom {
/// eg. `#[doc(hidden)]`
Flag(SmolStr),
/// eg. `#[doc(alias = "x")]`
///
/// Note that a key can have multiple values that are all considered "active" at the same time.
/// For example, `#[doc(alias = "x")]` and `#[doc(alias = "y")]`.
KeyValue { key: SmolStr, value: SmolStr },
}
// Adapted from `CfgExpr` parsing code
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
// #[cfg_attr(test, derive(derive_arbitrary::Arbitrary))]
pub enum DocExpr {
Invalid,
/// eg. `#[doc(hidden)]`, `#[doc(alias = "x")]`
Atom(DocAtom),
/// eg. `#[doc(alias("x", "y"))]`
Alias(Vec<SmolStr>),
}
impl From<DocAtom> for DocExpr {
fn from(atom: DocAtom) -> Self {
DocExpr::Atom(atom)
}
}
impl DocExpr {
fn parse<S>(tt: &tt::Subtree<S>) -> DocExpr {
next_doc_expr(&mut tt.token_trees.iter()).unwrap_or(DocExpr::Invalid)
}
pub fn aliases(&self) -> &[SmolStr] {
match self {
DocExpr::Atom(DocAtom::KeyValue { key, value }) if key == "alias" => {
std::slice::from_ref(value)
}
DocExpr::Alias(aliases) => aliases,
_ => &[],
}
}
}
fn next_doc_expr<S>(it: &mut SliceIter<'_, tt::TokenTree<S>>) -> Option<DocExpr> {
let name = match it.next() {
None => return None,
Some(tt::TokenTree::Leaf(tt::Leaf::Ident(ident))) => ident.text.clone(),
Some(_) => return Some(DocExpr::Invalid),
};
// Peek
let ret = match it.as_slice().first() {
Some(tt::TokenTree::Leaf(tt::Leaf::Punct(punct))) if punct.char == '=' => {
match it.as_slice().get(1) {
Some(tt::TokenTree::Leaf(tt::Leaf::Literal(literal))) => {
it.next();
it.next();
// FIXME: escape? raw string?
let value =
SmolStr::new(literal.text.trim_start_matches('"').trim_end_matches('"'));
DocAtom::KeyValue { key: name, value }.into()
}
_ => return Some(DocExpr::Invalid),
}
}
Some(tt::TokenTree::Subtree(subtree)) => {
it.next();
let subs = parse_comma_sep(subtree);
match name.as_str() {
"alias" => DocExpr::Alias(subs),
_ => DocExpr::Invalid,
}
}
_ => DocAtom::Flag(name).into(),
};
// Eat comma separator
if let Some(tt::TokenTree::Leaf(tt::Leaf::Punct(punct))) = it.as_slice().first() {
if punct.char == ',' {
it.next();
}
}
Some(ret)
}
fn parse_comma_sep<S>(subtree: &tt::Subtree<S>) -> Vec<SmolStr> {
subtree
.token_trees
.iter()
.filter_map(|tt| match tt {
tt::TokenTree::Leaf(tt::Leaf::Literal(lit)) => {
// FIXME: escape? raw string?
Some(SmolStr::new(lit.text.trim_start_matches('"').trim_end_matches('"')))
}
_ => None,
})
.collect()
}
impl AttrsWithOwner {
pub(crate) fn attrs_query(db: &dyn DefDatabase, def: AttrDefId) -> Self {
let _p = profile::span("attrs_query");

View file

@ -0,0 +1,40 @@
//! This module contains tests for doc-expression parsing.
//! Currently, it tests `#[doc(hidden)]` and `#[doc(alias)]`.
use mbe::syntax_node_to_token_tree;
use syntax::{ast, AstNode};
use crate::attr::{DocAtom, DocExpr};
fn assert_parse_result(input: &str, expected: DocExpr) {
let (tt, _) = {
let source_file = ast::SourceFile::parse(input).ok().unwrap();
let tt = source_file.syntax().descendants().find_map(ast::TokenTree::cast).unwrap();
syntax_node_to_token_tree(tt.syntax())
};
let cfg = DocExpr::parse(&tt);
assert_eq!(cfg, expected);
}
#[test]
fn test_doc_expr_parser() {
assert_parse_result("#![doc(hidden)]", DocAtom::Flag("hidden".into()).into());
assert_parse_result(
r#"#![doc(alias = "foo")]"#,
DocAtom::KeyValue { key: "alias".into(), value: "foo".into() }.into(),
);
assert_parse_result(r#"#![doc(alias("foo"))]"#, DocExpr::Alias(["foo".into()].into()));
assert_parse_result(
r#"#![doc(alias("foo", "bar", "baz"))]"#,
DocExpr::Alias(["foo".into(), "bar".into(), "baz".into()].into()),
);
assert_parse_result(
r#"
#[doc(alias("Bar", "Qux"))]
struct Foo;"#,
DocExpr::Alias(["Bar".into(), "Qux".into()].into()),
);
}

View file

@ -1644,6 +1644,7 @@ impl<'a> SemanticsScope<'a> {
VisibleTraits(resolver.traits_in_scope(self.db.upcast()))
}
/// Calls the passed closure `f` on all names in scope.
pub fn process_all_names(&self, f: &mut dyn FnMut(Name, ScopeDef)) {
let scope = self.resolver.names_in_scope(self.db.upcast());
for (name, entries) in scope {

View file

@ -165,9 +165,9 @@ impl Completions {
ctx: &CompletionContext<'_>,
path_ctx: &PathCompletionCtx,
) {
ctx.process_all_names(&mut |name, res| match res {
ctx.process_all_names(&mut |name, res, doc_aliases| match res {
ScopeDef::ModuleDef(hir::ModuleDef::Module(m)) if m.is_crate_root(ctx.db) => {
self.add_module(ctx, path_ctx, m, name);
self.add_module(ctx, path_ctx, m, name, doc_aliases);
}
_ => (),
});
@ -179,6 +179,7 @@ impl Completions {
path_ctx: &PathCompletionCtx,
local_name: hir::Name,
resolution: hir::ScopeDef,
doc_aliases: Vec<syntax::SmolStr>,
) {
let is_private_editable = match ctx.def_is_visible(&resolution) {
Visible::Yes => false,
@ -187,7 +188,9 @@ impl Completions {
};
self.add(
render_path_resolution(
RenderContext::new(ctx).private_editable(is_private_editable),
RenderContext::new(ctx)
.private_editable(is_private_editable)
.doc_aliases(doc_aliases),
path_ctx,
local_name,
resolution,
@ -236,12 +239,14 @@ impl Completions {
path_ctx: &PathCompletionCtx,
module: hir::Module,
local_name: hir::Name,
doc_aliases: Vec<syntax::SmolStr>,
) {
self.add_path_resolution(
ctx,
path_ctx,
local_name,
hir::ScopeDef::ModuleDef(module.into()),
doc_aliases,
);
}

View file

@ -93,7 +93,7 @@ pub(crate) fn complete_attribute_path(
acc.add_macro(ctx, path_ctx, m, name)
}
hir::ScopeDef::ModuleDef(hir::ModuleDef::Module(m)) => {
acc.add_module(ctx, path_ctx, m, name)
acc.add_module(ctx, path_ctx, m, name, vec![])
}
_ => (),
}
@ -104,12 +104,12 @@ pub(crate) fn complete_attribute_path(
Qualified::Absolute => acc.add_crate_roots(ctx, path_ctx),
// only show modules in a fresh UseTree
Qualified::No => {
ctx.process_all_names(&mut |name, def| match def {
ctx.process_all_names(&mut |name, def, doc_aliases| match def {
hir::ScopeDef::ModuleDef(hir::ModuleDef::Macro(m)) if m.is_attr(ctx.db) => {
acc.add_macro(ctx, path_ctx, m, name)
}
hir::ScopeDef::ModuleDef(hir::ModuleDef::Module(m)) => {
acc.add_module(ctx, path_ctx, m, name)
acc.add_module(ctx, path_ctx, m, name, doc_aliases)
}
_ => (),
});

View file

@ -34,7 +34,7 @@ pub(crate) fn complete_derive_path(
acc.add_macro(ctx, path_ctx, mac, name)
}
ScopeDef::ModuleDef(hir::ModuleDef::Module(m)) => {
acc.add_module(ctx, path_ctx, m, name)
acc.add_module(ctx, path_ctx, m, name, vec![])
}
_ => (),
}
@ -43,7 +43,7 @@ pub(crate) fn complete_derive_path(
Qualified::Absolute => acc.add_crate_roots(ctx, path_ctx),
// only show modules in a fresh UseTree
Qualified::No => {
ctx.process_all_names(&mut |name, def| {
ctx.process_all_names(&mut |name, def, doc_aliases| {
let mac = match def {
ScopeDef::ModuleDef(hir::ModuleDef::Macro(mac))
if !existing_derives.contains(&mac) && mac.is_derive(ctx.db) =>
@ -51,7 +51,7 @@ pub(crate) fn complete_derive_path(
mac
}
ScopeDef::ModuleDef(hir::ModuleDef::Module(m)) => {
return acc.add_module(ctx, path_ctx, m, name);
return acc.add_module(ctx, path_ctx, m, name, doc_aliases);
}
_ => return,
};

View file

@ -88,7 +88,7 @@ pub(crate) fn complete_expr_path(
let module_scope = module.scope(ctx.db, Some(ctx.module));
for (name, def) in module_scope {
if scope_def_applicable(def) {
acc.add_path_resolution(ctx, path_ctx, name, def);
acc.add_path_resolution(ctx, path_ctx, name, def, vec![]);
}
}
}
@ -212,7 +212,7 @@ pub(crate) fn complete_expr_path(
}
}
}
ctx.process_all_names(&mut |name, def| match def {
ctx.process_all_names(&mut |name, def, doc_aliases| match def {
ScopeDef::ModuleDef(hir::ModuleDef::Trait(t)) => {
let assocs = t.items_with_supertraits(ctx.db);
match &*assocs {
@ -220,12 +220,14 @@ pub(crate) fn complete_expr_path(
// there is no associated item path that can be constructed with them
[] => (),
// FIXME: Render the assoc item with the trait qualified
&[_item] => acc.add_path_resolution(ctx, path_ctx, name, def),
&[_item] => acc.add_path_resolution(ctx, path_ctx, name, def, doc_aliases),
// FIXME: Append `::` to the thing here, since a trait on its own won't work
[..] => acc.add_path_resolution(ctx, path_ctx, name, def),
[..] => acc.add_path_resolution(ctx, path_ctx, name, def, doc_aliases),
}
}
_ if scope_def_applicable(def) => acc.add_path_resolution(ctx, path_ctx, name, def),
_ if scope_def_applicable(def) => {
acc.add_path_resolution(ctx, path_ctx, name, def, doc_aliases)
}
_ => (),
});

View file

@ -45,7 +45,7 @@ pub(crate) fn complete_item_list(
acc.add_macro(ctx, path_ctx, m, name)
}
hir::ScopeDef::ModuleDef(hir::ModuleDef::Module(m)) => {
acc.add_module(ctx, path_ctx, m, name)
acc.add_module(ctx, path_ctx, m, name, vec![])
}
_ => (),
}
@ -55,12 +55,12 @@ pub(crate) fn complete_item_list(
}
Qualified::Absolute => acc.add_crate_roots(ctx, path_ctx),
Qualified::No if ctx.qualifier_ctx.none() => {
ctx.process_all_names(&mut |name, def| match def {
ctx.process_all_names(&mut |name, def, doc_aliases| match def {
hir::ScopeDef::ModuleDef(hir::ModuleDef::Macro(m)) if m.is_fn_like(ctx.db) => {
acc.add_macro(ctx, path_ctx, m, name)
}
hir::ScopeDef::ModuleDef(hir::ModuleDef::Module(m)) => {
acc.add_module(ctx, path_ctx, m, name)
acc.add_module(ctx, path_ctx, m, name, doc_aliases)
}
_ => (),
});

View file

@ -64,7 +64,7 @@ pub(crate) fn complete_pattern(
// FIXME: ideally, we should look at the type we are matching against and
// suggest variants + auto-imports
ctx.process_all_names(&mut |name, res| {
ctx.process_all_names(&mut |name, res, _| {
let add_simple_path = match res {
hir::ScopeDef::ModuleDef(def) => match def {
hir::ModuleDef::Adt(hir::Adt::Struct(strukt)) => {
@ -127,7 +127,7 @@ pub(crate) fn complete_pattern_path(
};
if add_resolution {
acc.add_path_resolution(ctx, path_ctx, name, def);
acc.add_path_resolution(ctx, path_ctx, name, def, vec![]);
}
}
}
@ -164,7 +164,7 @@ pub(crate) fn complete_pattern_path(
Qualified::Absolute => acc.add_crate_roots(ctx, path_ctx),
Qualified::No => {
// this will only be hit if there are brackets or braces, otherwise this will be parsed as an ident pattern
ctx.process_all_names(&mut |name, res| {
ctx.process_all_names(&mut |name, res, doc_aliases| {
// FIXME: we should check what kind of pattern we are in and filter accordingly
let add_completion = match res {
ScopeDef::ModuleDef(hir::ModuleDef::Macro(mac)) => mac.is_fn_like(ctx.db),
@ -175,7 +175,7 @@ pub(crate) fn complete_pattern_path(
_ => false,
};
if add_completion {
acc.add_path_resolution(ctx, path_ctx, name, res);
acc.add_path_resolution(ctx, path_ctx, name, res, doc_aliases);
}
});

View file

@ -85,7 +85,7 @@ pub(crate) fn complete_type_path(
let module_scope = module.scope(ctx.db, Some(ctx.module));
for (name, def) in module_scope {
if scope_def_applicable(def) {
acc.add_path_resolution(ctx, path_ctx, name, def);
acc.add_path_resolution(ctx, path_ctx, name, def, vec![]);
}
}
}
@ -141,7 +141,7 @@ pub(crate) fn complete_type_path(
match location {
TypeLocation::TypeBound => {
acc.add_nameref_keywords_with_colon(ctx);
ctx.process_all_names(&mut |name, res| {
ctx.process_all_names(&mut |name, res, doc_aliases| {
let add_resolution = match res {
ScopeDef::ModuleDef(hir::ModuleDef::Macro(mac)) => {
mac.is_fn_like(ctx.db)
@ -152,7 +152,7 @@ pub(crate) fn complete_type_path(
_ => false,
};
if add_resolution {
acc.add_path_resolution(ctx, path_ctx, name, res);
acc.add_path_resolution(ctx, path_ctx, name, res, doc_aliases);
}
});
return;
@ -215,9 +215,9 @@ pub(crate) fn complete_type_path(
};
acc.add_nameref_keywords_with_colon(ctx);
ctx.process_all_names(&mut |name, def| {
ctx.process_all_names(&mut |name, def, doc_aliases| {
if scope_def_applicable(def) {
acc.add_path_resolution(ctx, path_ctx, name, def);
acc.add_path_resolution(ctx, path_ctx, name, def, doc_aliases);
}
});
}

View file

@ -91,10 +91,10 @@ pub(crate) fn complete_use_path(
// only show modules and non-std enum in a fresh UseTree
Qualified::No => {
cov_mark::hit!(unqualified_path_selected_only);
ctx.process_all_names(&mut |name, res| {
ctx.process_all_names(&mut |name, res, doc_aliases| {
match res {
ScopeDef::ModuleDef(hir::ModuleDef::Module(module)) => {
acc.add_module(ctx, path_ctx, module, name);
acc.add_module(ctx, path_ctx, module, name, doc_aliases);
}
ScopeDef::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Enum(e))) => {
// exclude prelude enum

View file

@ -23,7 +23,7 @@ pub(crate) fn complete_vis_path(
if let Some(next) = next_towards_current {
if let Some(name) = next.name(ctx.db) {
cov_mark::hit!(visibility_qualified);
acc.add_module(ctx, path_ctx, next, name);
acc.add_module(ctx, path_ctx, next, name, vec![]);
}
}

View file

@ -17,7 +17,7 @@ use ide_db::{
};
use syntax::{
ast::{self, AttrKind, NameOrNameRef},
AstNode,
AstNode, SmolStr,
SyntaxKind::{self, *},
SyntaxToken, TextRange, TextSize, T,
};
@ -491,21 +491,22 @@ impl<'a> CompletionContext<'a> {
);
}
/// A version of [`SemanticsScope::process_all_names`] that filters out `#[doc(hidden)]` items.
pub(crate) fn process_all_names(&self, f: &mut dyn FnMut(Name, ScopeDef)) {
/// A version of [`SemanticsScope::process_all_names`] that filters out `#[doc(hidden)]` items and
/// passes all doc-aliases along, to funnel it into [`Completions::add_path_resolution`].
pub(crate) fn process_all_names(&self, f: &mut dyn FnMut(Name, ScopeDef, Vec<SmolStr>)) {
let _p = profile::span("CompletionContext::process_all_names");
self.scope.process_all_names(&mut |name, def| {
if self.is_scope_def_hidden(def) {
return;
}
f(name, def);
let doc_aliases = self.doc_aliases(def);
f(name, def, doc_aliases);
});
}
pub(crate) fn process_all_names_raw(&self, f: &mut dyn FnMut(Name, ScopeDef)) {
let _p = profile::span("CompletionContext::process_all_names_raw");
self.scope.process_all_names(&mut |name, def| f(name, def));
self.scope.process_all_names(f);
}
fn is_scope_def_hidden(&self, scope_def: ScopeDef) -> bool {
@ -545,6 +546,14 @@ impl<'a> CompletionContext<'a> {
// `doc(hidden)` items are only completed within the defining crate.
self.krate != defining_crate && attrs.has_doc_hidden()
}
fn doc_aliases(&self, scope_def: ScopeDef) -> Vec<SmolStr> {
if let Some(attrs) = scope_def.attrs(self.db) {
attrs.doc_aliases().collect()
} else {
vec![]
}
}
}
// CompletionContext construction

View file

@ -45,7 +45,7 @@ pub struct CompletionItem {
///
/// That is, in `foo.bar$0` lookup of `abracadabra` will be accepted (it
/// contains `bar` sub sequence), and `quux` will rejected.
pub lookup: Option<SmolStr>,
pub lookup: SmolStr,
/// Additional info to show in the UI pop up.
pub detail: Option<String>,
@ -353,12 +353,13 @@ impl CompletionItem {
relevance: CompletionRelevance::default(),
ref_match: None,
imports_to_add: Default::default(),
doc_aliases: None,
}
}
/// What string is used for filtering.
pub fn lookup(&self) -> &str {
self.lookup.as_deref().unwrap_or(&self.label)
self.lookup.as_str()
}
pub fn ref_match(&self) -> Option<(String, text_edit::Indel, CompletionRelevance)> {
@ -385,6 +386,7 @@ pub(crate) struct Builder {
source_range: TextRange,
imports_to_add: SmallVec<[LocatedImport; 1]>,
trait_name: Option<SmolStr>,
doc_aliases: Option<SmolStr>,
label: SmolStr,
insert_text: Option<String>,
is_snippet: bool,
@ -413,13 +415,16 @@ impl Builder {
let _p = profile::span("item::Builder::build");
let mut label = self.label;
let mut lookup = self.lookup;
let mut lookup = self.lookup.unwrap_or_else(|| label.clone());
let insert_text = self.insert_text.unwrap_or_else(|| label.to_string());
if let Some(doc_aliases) = self.doc_aliases {
label = SmolStr::from(format!("{label} (alias {doc_aliases})"));
lookup = SmolStr::from(format!("{lookup} {doc_aliases}"));
}
if let [import_edit] = &*self.imports_to_add {
// snippets can have multiple imports, but normal completions only have up to one
if let Some(original_path) = import_edit.original_path.as_ref() {
lookup = lookup.or_else(|| Some(label.clone()));
label = SmolStr::from(format!("{label} (use {original_path})"));
}
} else if let Some(trait_name) = self.trait_name {
@ -459,6 +464,10 @@ impl Builder {
self.trait_name = Some(trait_name);
self
}
pub(crate) fn doc_aliases(&mut self, doc_aliases: SmolStr) -> &mut Builder {
self.doc_aliases = Some(doc_aliases);
self
}
pub(crate) fn insert_text(&mut self, insert_text: impl Into<String>) -> &mut Builder {
self.insert_text = Some(insert_text.into());
self

View file

@ -97,7 +97,7 @@ pub use crate::{
/// Main entry point for completion. We run completion as a two-phase process.
///
/// First, we look at the position and collect a so-called `CompletionContext.
/// First, we look at the position and collect a so-called `CompletionContext`.
/// This is a somewhat messy process, because, during completion, syntax tree is
/// incomplete and can look really weird.
///

View file

@ -14,6 +14,7 @@ use hir::{AsAssocItem, HasAttrs, HirDisplay, ScopeDef};
use ide_db::{
helpers::item_name, imports::import_assets::LocatedImport, RootDatabase, SnippetCap, SymbolKind,
};
use itertools::Itertools;
use syntax::{AstNode, SmolStr, SyntaxKind, TextRange};
use crate::{
@ -32,11 +33,17 @@ pub(crate) struct RenderContext<'a> {
completion: &'a CompletionContext<'a>,
is_private_editable: bool,
import_to_add: Option<LocatedImport>,
doc_aliases: Vec<SmolStr>,
}
impl<'a> RenderContext<'a> {
pub(crate) fn new(completion: &'a CompletionContext<'a>) -> RenderContext<'a> {
RenderContext { completion, is_private_editable: false, import_to_add: None }
RenderContext {
completion,
is_private_editable: false,
import_to_add: None,
doc_aliases: vec![],
}
}
pub(crate) fn private_editable(mut self, private_editable: bool) -> Self {
@ -49,6 +56,11 @@ impl<'a> RenderContext<'a> {
self
}
pub(crate) fn doc_aliases(mut self, doc_aliases: Vec<SmolStr>) -> Self {
self.doc_aliases = doc_aliases;
self
}
fn snippet_cap(&self) -> Option<SnippetCap> {
self.completion.config.snippet_cap
}
@ -348,6 +360,12 @@ fn render_resolution_simple_(
if let Some(import_to_add) = ctx.import_to_add {
item.add_import(import_to_add);
}
let doc_aliases = ctx.doc_aliases;
if !doc_aliases.is_empty() {
let doc_aliases = doc_aliases.into_iter().join(", ").into();
item.doc_aliases(doc_aliases);
}
item
}

View file

@ -989,3 +989,100 @@ fn foo { crate::::$0 }
expect![""],
)
}
#[test]
fn completes_struct_via_doc_alias_in_fn_body() {
check(
r#"
#[doc(alias = "Bar")]
struct Foo;
fn here_we_go() {
$0
}
"#,
expect![[r#"
fn here_we_go() fn()
st Foo (alias Bar)
bt u32
kw const
kw crate::
kw enum
kw extern
kw false
kw fn
kw for
kw if
kw if let
kw impl
kw let
kw loop
kw match
kw mod
kw return
kw self::
kw static
kw struct
kw trait
kw true
kw type
kw union
kw unsafe
kw use
kw while
kw while let
sn macro_rules
sn pd
sn ppd
"#]],
);
}
#[test]
fn completes_struct_via_multiple_doc_aliases_in_fn_body() {
check(
r#"
#[doc(alias("Bar", "Qux"))]
#[doc(alias = "Baz")]
struct Foo;
fn here_we_go() {
B$0
}
"#,
expect![[r#"
fn here_we_go() fn()
st Foo (alias Bar, Qux, Baz)
bt u32
kw const
kw crate::
kw enum
kw extern
kw false
kw fn
kw for
kw if
kw if let
kw impl
kw let
kw loop
kw match
kw mod
kw return
kw self::
kw static
kw struct
kw trait
kw true
kw type
kw union
kw unsafe
kw use
kw while
kw while let
sn macro_rules
sn pd
sn ppd
"#]],
);
}