Add doc-alias based completion

This commit is contained in:
hecatia-elegua 2023-03-29 14:08:25 +02:00
parent 7a98e24777
commit 0863389dd1
18 changed files with 330 additions and 36 deletions

View file

@ -7,6 +7,7 @@ use cfg::{CfgExpr, CfgOptions};
use either::Either;
use hir_expand::{
attrs::{collect_attrs, Attr, AttrId, RawAttrs},
name::{AsName, Name},
HirFileId, InFile,
};
use itertools::Itertools;
@ -238,6 +239,17 @@ impl Attrs {
})
}
pub fn doc_exprs(&self) -> Vec<DocExpr> {
self.by_key("doc").tt_values().map(DocExpr::parse).collect()
}
pub fn doc_aliases(&self) -> Vec<SmolStr> {
self.doc_exprs()
.into_iter()
.flat_map(|doc_expr| doc_expr.aliases())
.collect()
}
pub fn is_proc_macro(&self) -> bool {
self.by_key("proc_macro").exists()
}
@ -251,6 +263,106 @@ 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 },
}
#[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 {
pub fn parse<S>(tt: &tt::Subtree<S>) -> DocExpr {
next_doc_expr(&mut tt.token_trees.iter()).unwrap_or(DocExpr::Invalid)
}
pub fn aliases(self) -> Vec<SmolStr> {
match self {
DocExpr::Atom(DocAtom::KeyValue { key, value }) if key == "alias" => {
vec![value]
}
DocExpr::Alias(aliases) => aliases,
_ => vec![],
}
}
}
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 {
// FIXME: this should use `Trace` to avoid duplication in `source_map` below

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

@ -53,6 +53,8 @@ pub mod import_map;
mod test_db;
#[cfg(test)]
mod macro_expansion_tests;
#[cfg(test)]
mod attr_tests;
mod pretty;
use std::{

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

@ -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<syntax::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()
}
pub fn doc_aliases(&self, scope_def: ScopeDef) -> Vec<syntax::SmolStr> {
if let Some(attrs) = scope_def.attrs(self.db) {
attrs.doc_aliases()
} else {
vec![]
}
}
}
// CompletionContext construction

View file

@ -353,6 +353,7 @@ impl CompletionItem {
relevance: CompletionRelevance::default(),
ref_match: None,
imports_to_add: Default::default(),
doc_aliases: None,
}
}
@ -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,
@ -424,6 +426,8 @@ impl Builder {
}
} else if let Some(trait_name) = self.trait_name {
label = SmolStr::from(format!("{label} (as {trait_name})"));
} else if let Some(doc_aliases) = self.doc_aliases {
label = SmolStr::from(format!("{label} (alias {doc_aliases})"));
}
let text_edit = match self.text_edit {
@ -459,6 +463,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
"#]],
);
}