feat: Enable completions within derive helper attributes

This commit is contained in:
Lukas Wirth 2024-06-02 09:28:34 +02:00
parent 8ab8bb0603
commit f122a6f2f1
2 changed files with 209 additions and 98 deletions

View File

@ -380,6 +380,27 @@ pub fn is_derive_annotated(&self, adt: &ast::Adt) -> bool {
self.with_ctx(|ctx| ctx.has_derives(adt)) self.with_ctx(|ctx| ctx.has_derives(adt))
} }
pub fn derive_helper(&self, attr: &ast::Attr) -> Option<Vec<(Macro, MacroFileId)>> {
let adt = attr.syntax().ancestors().find_map(ast::Item::cast).and_then(|it| match it {
ast::Item::Struct(it) => Some(ast::Adt::Struct(it)),
ast::Item::Enum(it) => Some(ast::Adt::Enum(it)),
ast::Item::Union(it) => Some(ast::Adt::Union(it)),
_ => None,
})?;
let attr_name = attr.path().and_then(|it| it.as_single_name_ref())?.as_name();
let sa = self.analyze_no_infer(adt.syntax())?;
let id = self.db.ast_id_map(sa.file_id).ast_id(&adt);
let res: Vec<_> = sa
.resolver
.def_map()
.derive_helpers_in_scope(InFile::new(sa.file_id, id))?
.iter()
.filter(|&(name, _, _)| *name == attr_name)
.map(|&(_, macro_, call)| (macro_.into(), call.as_macro_file()))
.collect();
res.is_empty().not().then_some(res)
}
pub fn is_attr_macro_call(&self, item: &ast::Item) -> bool { pub fn is_attr_macro_call(&self, item: &ast::Item) -> bool {
let file_id = self.find_file(item.syntax()).file_id; let file_id = self.find_file(item.syntax()).file_id;
let src = InFile::new(file_id, item.clone()); let src = InFile::new(file_id, item.clone());
@ -409,6 +430,20 @@ pub fn speculative_expand(
) )
} }
pub fn speculative_expand_raw(
&self,
macro_file: MacroFileId,
speculative_args: &SyntaxNode,
token_to_map: SyntaxToken,
) -> Option<(SyntaxNode, SyntaxToken)> {
hir_expand::db::expand_speculative(
self.db.upcast(),
macro_file.macro_call_id,
speculative_args,
token_to_map,
)
}
/// Expand the macro call with a different item as the input, mapping the `token_to_map` down into the /// Expand the macro call with a different item as the input, mapping the `token_to_map` down into the
/// expansion. `token_to_map` should be a token from the `speculative args` node. /// expansion. `token_to_map` should be a token from the `speculative args` node.
pub fn speculative_expand_attr_macro( pub fn speculative_expand_attr_macro(
@ -826,17 +861,19 @@ fn descend_into_macros_impl(
// Then check for token trees, that means we are either in a function-like macro or // Then check for token trees, that means we are either in a function-like macro or
// secondary attribute inputs // secondary attribute inputs
let tt = token.parent_ancestors().map_while(ast::TokenTree::cast).last()?; let tt = token
let parent = tt.syntax().parent()?; .parent_ancestors()
.map_while(Either::<ast::TokenTree, ast::Meta>::cast)
.last()?;
match tt {
Either::Left(tt) => {
if tt.left_delimiter_token().map_or(false, |it| it == token) { if tt.left_delimiter_token().map_or(false, |it| it == token) {
return None; return None;
} }
if tt.right_delimiter_token().map_or(false, |it| it == token) { if tt.right_delimiter_token().map_or(false, |it| it == token) {
return None; return None;
} }
let macro_call = tt.syntax().parent().and_then(ast::MacroCall::cast)?;
if let Some(macro_call) = ast::MacroCall::cast(parent.clone()) {
let mcall: hir_expand::files::InFileWrapper<HirFileId, ast::MacroCall> = let mcall: hir_expand::files::InFileWrapper<HirFileId, ast::MacroCall> =
InFile::new(file_id, macro_call); InFile::new(file_id, macro_call);
let file_id = match mcache.get(&mcall) { let file_id = match mcache.get(&mcall) {
@ -858,12 +895,13 @@ fn descend_into_macros_impl(
// also descend into eager expansions // also descend into eager expansions
process_expansion_for_token(&mut stack, arg.as_macro_file()) process_expansion_for_token(&mut stack, arg.as_macro_file())
})) }))
} else if let Some(meta) = ast::Meta::cast(parent) { }
Either::Right(meta) => {
// attribute we failed expansion for earlier, this might be a derive invocation // attribute we failed expansion for earlier, this might be a derive invocation
// or derive helper attribute // or derive helper attribute
let attr = meta.parent_attr()?; let attr = meta.parent_attr()?;
let adt = if let Some(adt) = attr.syntax().parent().and_then(ast::Adt::cast) let adt = match attr.syntax().parent().and_then(ast::Adt::cast) {
{ Some(adt) => {
// this might be a derive on an ADT // this might be a derive on an ADT
let derive_call = self.with_ctx(|ctx| { let derive_call = self.with_ctx(|ctx| {
// so try downmapping the token into the pseudo derive expansion // so try downmapping the token into the pseudo derive expansion
@ -882,31 +920,39 @@ fn descend_into_macros_impl(
let text_range = attr.syntax().text_range(); let text_range = attr.syntax().text_range();
// remove any other token in this macro input, all their mappings are the // remove any other token in this macro input, all their mappings are the
// same as this // same as this
tokens.retain(|t| !text_range.contains_range(t.text_range())); tokens.retain(|t| {
return process_expansion_for_token(&mut stack, file_id); !text_range.contains_range(t.text_range())
});
return process_expansion_for_token(
&mut stack, file_id,
);
} }
None => Some(adt), None => Some(adt),
} }
} else { }
None => {
// Otherwise this could be a derive helper on a variant or field // Otherwise this could be a derive helper on a variant or field
attr.syntax().ancestors().find_map(ast::Item::cast).and_then(|it| { attr.syntax().ancestors().find_map(ast::Item::cast).and_then(
match it { |it| match it {
ast::Item::Struct(it) => Some(ast::Adt::Struct(it)), ast::Item::Struct(it) => Some(ast::Adt::Struct(it)),
ast::Item::Enum(it) => Some(ast::Adt::Enum(it)), ast::Item::Enum(it) => Some(ast::Adt::Enum(it)),
ast::Item::Union(it) => Some(ast::Adt::Union(it)), ast::Item::Union(it) => Some(ast::Adt::Union(it)),
_ => None, _ => None,
},
)
} }
})
}?; }?;
if !self.with_ctx(|ctx| ctx.has_derives(InFile::new(file_id, &adt))) { if !self.with_ctx(|ctx| ctx.has_derives(InFile::new(file_id, &adt))) {
return None; return None;
} }
// Not an attribute, nor a derive, so it's either a builtin or a derive helper
// Try to resolve to a derive helper and downmap
let attr_name = let attr_name =
attr.path().and_then(|it| it.as_single_name_ref())?.as_name(); attr.path().and_then(|it| it.as_single_name_ref())?.as_name();
// Not an attribute, nor a derive, so it's either a builtin or a derive helper
// Try to resolve to a derive helper and downmap
let id = self.db.ast_id_map(file_id).ast_id(&adt); let id = self.db.ast_id_map(file_id).ast_id(&adt);
let helpers = def_map.derive_helpers_in_scope(InFile::new(file_id, id))?; let helpers =
def_map.derive_helpers_in_scope(InFile::new(file_id, id))?;
let mut res = None; let mut res = None;
for (.., derive) in for (.., derive) in
helpers.iter().filter(|(helper, ..)| *helper == attr_name) helpers.iter().filter(|(helper, ..)| *helper == attr_name)
@ -917,8 +963,7 @@ fn descend_into_macros_impl(
)); ));
} }
res res
} else { }
None
} }
})() })()
.is_none(); .is_none();

View File

@ -3,8 +3,9 @@
use hir::{Semantics, Type, TypeInfo, Variant}; use hir::{Semantics, Type, TypeInfo, Variant};
use ide_db::{active_parameter::ActiveParameter, RootDatabase}; use ide_db::{active_parameter::ActiveParameter, RootDatabase};
use itertools::Either;
use syntax::{ use syntax::{
algo::{find_node_at_offset, non_trivia_sibling}, algo::{ancestors_at_offset, find_node_at_offset, non_trivia_sibling},
ast::{self, AttrKind, HasArgList, HasGenericParams, HasLoopBody, HasName, NameOrNameRef}, ast::{self, AttrKind, HasArgList, HasGenericParams, HasLoopBody, HasName, NameOrNameRef},
match_ast, AstNode, AstToken, Direction, NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode, match_ast, AstNode, AstToken, Direction, NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode,
SyntaxToken, TextRange, TextSize, T, SyntaxToken, TextRange, TextSize, T,
@ -119,20 +120,45 @@ fn expand(
} }
// No attributes have been expanded, so look for macro_call! token trees or derive token trees // No attributes have been expanded, so look for macro_call! token trees or derive token trees
let orig_tt = match find_node_at_offset::<ast::TokenTree>(&original_file, offset) { let orig_tt = match ancestors_at_offset(&original_file, offset)
.map_while(Either::<ast::TokenTree, ast::Meta>::cast)
.last()
{
Some(it) => it, Some(it) => it,
None => break 'expansion, None => break 'expansion,
}; };
let spec_tt = match find_node_at_offset::<ast::TokenTree>(&speculative_file, offset) { let spec_tt = match ancestors_at_offset(&speculative_file, offset)
.map_while(Either::<ast::TokenTree, ast::Meta>::cast)
.last()
{
Some(it) => it, Some(it) => it,
None => break 'expansion, None => break 'expansion,
}; };
// Expand pseudo-derive expansion let (tts, attrs) = match (orig_tt, spec_tt) {
if let (Some(orig_attr), Some(spec_attr)) = ( (Either::Left(orig_tt), Either::Left(spec_tt)) => {
orig_tt.syntax().parent().and_then(ast::Meta::cast).and_then(|it| it.parent_attr()), let attrs = orig_tt
spec_tt.syntax().parent().and_then(ast::Meta::cast).and_then(|it| it.parent_attr()), .syntax()
) { .parent()
.and_then(ast::Meta::cast)
.and_then(|it| it.parent_attr())
.zip(
spec_tt
.syntax()
.parent()
.and_then(ast::Meta::cast)
.and_then(|it| it.parent_attr()),
);
(Some((orig_tt, spec_tt)), attrs)
}
(Either::Right(orig_path), Either::Right(spec_path)) => {
(None, orig_path.parent_attr().zip(spec_path.parent_attr()))
}
_ => break 'expansion,
};
// Expand pseudo-derive expansion aka `derive(Debug$0)`
if let Some((orig_attr, spec_attr)) = attrs {
if let (Some(actual_expansion), Some((fake_expansion, fake_mapped_token))) = ( if let (Some(actual_expansion), Some((fake_expansion, fake_mapped_token))) = (
sema.expand_derive_as_pseudo_attr_macro(&orig_attr), sema.expand_derive_as_pseudo_attr_macro(&orig_attr),
sema.speculative_expand_derive_as_pseudo_attr_macro( sema.speculative_expand_derive_as_pseudo_attr_macro(
@ -147,15 +173,54 @@ fn expand(
fake_mapped_token.text_range().start(), fake_mapped_token.text_range().start(),
orig_attr, orig_attr,
)); ));
break 'expansion;
}
if let Some(spec_adt) =
spec_attr.syntax().ancestors().find_map(ast::Item::cast).and_then(|it| match it {
ast::Item::Struct(it) => Some(ast::Adt::Struct(it)),
ast::Item::Enum(it) => Some(ast::Adt::Enum(it)),
ast::Item::Union(it) => Some(ast::Adt::Union(it)),
_ => None,
})
{
// might be the path of derive helper or a token tree inside of one
if let Some(helpers) = sema.derive_helper(&orig_attr) {
for (_mac, file) in helpers {
if let Some((fake_expansion, fake_mapped_token)) = sema
.speculative_expand_raw(
file,
spec_adt.syntax(),
fake_ident_token.clone(),
)
{
// we are inside a derive helper token tree, treat this as being inside
// the derive expansion
let actual_expansion = sema.parse_or_expand(file.into());
let new_offset = fake_mapped_token.text_range().start();
if new_offset + relative_offset > actual_expansion.text_range().end() {
// offset outside of bounds from the original expansion,
// stop here to prevent problems from happening
break 'expansion;
}
original_file = actual_expansion;
speculative_file = fake_expansion;
fake_ident_token = fake_mapped_token;
offset = new_offset;
continue 'expansion;
}
}
}
} }
// at this point we won't have any more successful expansions, so stop // at this point we won't have any more successful expansions, so stop
break 'expansion; break 'expansion;
} }
// Expand fn-like macro calls // Expand fn-like macro calls
let Some((orig_tt, spec_tt)) = tts else { break 'expansion };
if let (Some(actual_macro_call), Some(macro_call_with_fake_ident)) = ( if let (Some(actual_macro_call), Some(macro_call_with_fake_ident)) = (
orig_tt.syntax().ancestors().find_map(ast::MacroCall::cast), orig_tt.syntax().parent().and_then(ast::MacroCall::cast),
spec_tt.syntax().ancestors().find_map(ast::MacroCall::cast), spec_tt.syntax().parent().and_then(ast::MacroCall::cast),
) { ) {
let mac_call_path0 = actual_macro_call.path().as_ref().map(|s| s.syntax().text()); let mac_call_path0 = actual_macro_call.path().as_ref().map(|s| s.syntax().text());
let mac_call_path1 = let mac_call_path1 =
@ -201,6 +266,7 @@ fn expand(
// none of our states have changed so stop the loop // none of our states have changed so stop the loop
break 'expansion; break 'expansion;
} }
ExpansionResult { original_file, speculative_file, offset, fake_ident_token, derive_ctx } ExpansionResult { original_file, speculative_file, offset, fake_ident_token, derive_ctx }
} }