Skip to content

Completion in macros #3513

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 9, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/ra_hir/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ log = "0.4.8"
rustc-hash = "1.1.0"
either = "1.5.3"

itertools = "0.8.2"

ra_syntax = { path = "../ra_syntax" }
ra_db = { path = "../ra_db" }
ra_prof = { path = "../ra_prof" }
Expand Down
55 changes: 53 additions & 2 deletions crates/ra_hir/src/semantics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ use std::{cell::RefCell, fmt, iter::successors};

use hir_def::{
resolver::{self, HasResolver, Resolver},
TraitId,
AsMacroCall, TraitId,
};
use hir_expand::ExpansionInfo;
use ra_db::{FileId, FileRange};
use ra_prof::profile;
use ra_syntax::{
algo::skip_trivia_token, ast, AstNode, Direction, SyntaxNode, SyntaxToken, TextRange, TextUnit,
algo::{self, skip_trivia_token},
ast, AstNode, Direction, SyntaxNode, SyntaxToken, TextRange, TextUnit,
};
use rustc_hash::{FxHashMap, FxHashSet};

Expand Down Expand Up @@ -70,6 +71,37 @@ impl<'db, DB: HirDatabase> Semantics<'db, DB> {
Some(node)
}

pub fn expand_hypothetical(
&self,
actual_macro_call: &ast::MacroCall,
hypothetical_call: &ast::MacroCall,
token_to_map: SyntaxToken,
) -> Option<(SyntaxNode, SyntaxToken)> {
let macro_call =
self.find_file(actual_macro_call.syntax().clone()).with_value(actual_macro_call);
let sa = self.analyze2(macro_call.map(|it| it.syntax()), None);
let macro_call_id = macro_call
.as_call_id(self.db, |path| sa.resolver.resolve_path_as_macro(self.db, &path))?;
let macro_file = macro_call_id.as_file().macro_file().unwrap();
let (tt, tmap_1) =
hir_expand::syntax_node_to_token_tree(hypothetical_call.token_tree().unwrap().syntax())
.unwrap();
let range = token_to_map
.text_range()
.checked_sub(hypothetical_call.token_tree().unwrap().syntax().text_range().start())?;
let token_id = tmap_1.token_by_range(range)?;
let macro_def = hir_expand::db::expander(self.db, macro_call_id)?;
let (node, tmap_2) = hir_expand::db::parse_macro_with_arg(
self.db,
macro_file,
Some(std::sync::Arc::new((tt, tmap_1))),
)?;
let token_id = macro_def.0.map_id_down(token_id);
let range = tmap_2.range_by_token(token_id)?.by_kind(token_to_map.kind())?;
let token = algo::find_covering_element(&node.syntax_node(), range).into_token()?;
Some((node.syntax_node(), token))
}

Copy link
Member

@edwin0cheng edwin0cheng Mar 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat!
So what this function does is, replace the original macro call with the hypothetical_call arguments. And I see the limitation here: we don't want to pollute the salsa database such that we can't convert the hypothetical_call to an actual MacroCallId. So we have to expand it manually.

One small nit: we might change the arguments to reflect it only replace the arguments :

    pub fn expand_hypothetical(
        &self,
        actual_macro_call: &ast::MacroCall,
        hypotheticall_args: &ast::TokenTree,
        token_to_map: SyntaxToken,
)

And I can imagine we could move some of the logic here to hir_expand but overall it is awesome !

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good points, I'll move most of it to hir_expand, then I think it can be pretty contained there.

pub fn descend_into_macros(&self, token: SyntaxToken) -> SyntaxToken {
let parent = token.parent();
let parent = self.find_file(parent);
Expand Down Expand Up @@ -104,6 +136,25 @@ impl<'db, DB: HirDatabase> Semantics<'db, DB> {
node.ancestors_with_macros(self.db).map(|it| it.value)
}

pub fn ancestors_at_offset_with_macros(
&self,
node: &SyntaxNode,
offset: TextUnit,
) -> impl Iterator<Item = SyntaxNode> + '_ {
use itertools::Itertools;
node.token_at_offset(offset)
.map(|token| self.ancestors_with_macros(token.parent()))
.kmerge_by(|node1, node2| node1.text_range().len() < node2.text_range().len())
}

pub fn find_node_at_offset_with_macros<N: AstNode>(
&self,
node: &SyntaxNode,
offset: TextUnit,
) -> Option<N> {
self.ancestors_at_offset_with_macros(node, offset).find_map(N::cast)
}

pub fn type_of_expr(&self, expr: &ast::Expr) -> Option<Type> {
self.analyze(expr.syntax()).type_of(self.db, &expr)
}
Expand Down
53 changes: 45 additions & 8 deletions crates/ra_hir_expand/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,43 @@ pub(crate) fn macro_arg(
pub(crate) fn macro_expand(
db: &dyn AstDatabase,
id: MacroCallId,
) -> Result<Arc<tt::Subtree>, String> {
macro_expand_with_arg(db, id, None)
}

// TODO hack
pub fn expander(
db: &dyn AstDatabase,
id: MacroCallId,
) -> Option<Arc<(TokenExpander, mbe::TokenMap)>> {
let lazy_id = match id {
MacroCallId::LazyMacro(id) => id,
MacroCallId::EagerMacro(_id) => {
// TODO
unimplemented!()
}
};

let loc = db.lookup_intern_macro(lazy_id);
let macro_rules = db.macro_def(loc.def)?;
Some(macro_rules)
}

pub(crate) fn macro_expand_with_arg(
db: &dyn AstDatabase,
id: MacroCallId,
arg: Option<Arc<(tt::Subtree, mbe::TokenMap)>>,
) -> Result<Arc<tt::Subtree>, String> {
let lazy_id = match id {
MacroCallId::LazyMacro(id) => id,
MacroCallId::EagerMacro(id) => {
// TODO
return Ok(db.lookup_intern_eager_expansion(id).subtree);
}
};

let loc = db.lookup_intern_macro(lazy_id);
let macro_arg = db.macro_arg(id).ok_or("Fail to args in to tt::TokenTree")?;
let macro_arg = arg.or_else(|| db.macro_arg(id)).ok_or("Fail to args in to tt::TokenTree")?;

let macro_rules = db.macro_def(loc.def).ok_or("Fail to find macro definition")?;
let tt = macro_rules.0.expand(db, lazy_id, &macro_arg.0).map_err(|err| format!("{:?}", err))?;
Expand All @@ -162,12 +189,24 @@ pub(crate) fn parse_or_expand(db: &dyn AstDatabase, file_id: HirFileId) -> Optio
pub(crate) fn parse_macro(
db: &dyn AstDatabase,
macro_file: MacroFile,
) -> Option<(Parse<SyntaxNode>, Arc<mbe::TokenMap>)> {
parse_macro_with_arg(db, macro_file, None)
}

pub fn parse_macro_with_arg(
db: &dyn AstDatabase,
macro_file: MacroFile,
arg: Option<Arc<(tt::Subtree, mbe::TokenMap)>>,
) -> Option<(Parse<SyntaxNode>, Arc<mbe::TokenMap>)> {
let _p = profile("parse_macro_query");

let macro_call_id = macro_file.macro_call_id;
let tt = db
.macro_expand(macro_call_id)
let expansion = if let Some(arg) = arg {
macro_expand_with_arg(db, macro_call_id, Some(arg))
} else {
db.macro_expand(macro_call_id)
};
let tt = expansion
.map_err(|err| {
// Note:
// The final goal we would like to make all parse_macro success,
Expand All @@ -185,15 +224,13 @@ pub(crate) fn parse_macro(
.collect::<Vec<_>>()
.join("\n");

log::warn!(
eprintln!(
"fail on macro_parse: (reason: {} macro_call: {:#}) parents: {}",
err,
node.value,
parents
err, node.value, parents
);
}
_ => {
log::warn!("fail on macro_parse: (reason: {})", err);
eprintln!("fail on macro_parse: (reason: {})", err);
}
}
})
Expand Down
9 changes: 8 additions & 1 deletion crates/ra_hir_expand/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,13 @@ impl HirFileId {
}
}
}

pub fn macro_file(self) -> Option<MacroFile> {
match self.0 {
HirFileIdRepr::FileId(_) => None,
HirFileIdRepr::MacroFile(m) => Some(m),
}
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
Expand Down Expand Up @@ -296,7 +303,7 @@ pub struct ExpansionInfo {
exp_map: Arc<mbe::TokenMap>,
}

pub use mbe::Origin;
pub use mbe::{syntax_node_to_token_tree, Origin};
use ra_parser::FragmentKind;

impl ExpansionInfo {
Expand Down
98 changes: 98 additions & 0 deletions crates/ra_ide/src/completion/complete_dot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -584,4 +584,102 @@ mod tests {
"###
);
}

#[test]
fn works_in_simple_macro_1() {
assert_debug_snapshot!(
do_ref_completion(
r"
macro_rules! m { ($e:expr) => { $e } }
struct A { the_field: u32 }
fn foo(a: A) {
m!(a.x<|>)
}
",
),
@r###"
[
CompletionItem {
label: "the_field",
source_range: [156; 157),
delete: [156; 157),
insert: "the_field",
kind: Field,
detail: "u32",
},
]
"###
);
}

#[test]
fn works_in_simple_macro_recursive() {
assert_debug_snapshot!(
do_ref_completion(
r"
macro_rules! m { ($e:expr) => { $e } }
struct A { the_field: u32 }
fn foo(a: A) {
m!(a.x<|>)
}
",
),
@r###"
[
CompletionItem {
label: "the_field",
source_range: [156; 157),
delete: [156; 157),
insert: "the_field",
kind: Field,
detail: "u32",
},
]
"###
);
}

#[test]
fn works_in_simple_macro_2() {
// this doesn't work yet because the macro doesn't expand without the token -- maybe it can be fixed with better recovery
assert_debug_snapshot!(
do_ref_completion(
r"
macro_rules! m { ($e:expr) => { $e } }
struct A { the_field: u32 }
fn foo(a: A) {
m!(a.<|>)
}
",
),
@r###"[]"###
);
}

#[test]
fn works_in_simple_macro_recursive_1() {
assert_debug_snapshot!(
do_ref_completion(
r"
macro_rules! m { ($e:expr) => { $e } }
struct A { the_field: u32 }
fn foo(a: A) {
m!(m!(m!(a.x<|>)))
}
",
),
@r###"
[
CompletionItem {
label: "the_field",
source_range: [162; 163),
delete: [162; 163),
insert: "the_field",
kind: Field,
detail: "u32",
},
]
"###
);
}
}
1 change: 1 addition & 0 deletions crates/ra_ide/src/completion/complete_keyword.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ pub(super) fn complete_expr_keyword(acc: &mut Completions, ctx: &CompletionConte
}

fn is_in_loop_body(leaf: &SyntaxToken) -> bool {
// FIXME move this to CompletionContext and make it handle macros
for node in leaf.parent().ancestors() {
if node.kind() == FN_DEF || node.kind() == LAMBDA_EXPR {
break;
Expand Down
35 changes: 34 additions & 1 deletion crates/ra_ide/src/completion/complete_path.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Completion of paths, including when writing a single name.
//! Completion of paths, i.e. `some::prefix::<|>`.

use hir::{Adt, PathResolution, ScopeDef};
use ra_syntax::AstNode;
Expand Down Expand Up @@ -835,4 +835,37 @@ mod tests {
"###
);
}

#[test]
fn completes_in_simple_macro_call() {
let completions = do_reference_completion(
r#"
macro_rules! m { ($e:expr) => { $e } }
fn main() { m!(self::f<|>); }
fn foo() {}
"#,
);
assert_debug_snapshot!(completions, @r###"
[
CompletionItem {
label: "foo()",
source_range: [93; 94),
delete: [93; 94),
insert: "foo()$0",
kind: Function,
lookup: "foo",
detail: "fn foo()",
},
CompletionItem {
label: "main()",
source_range: [93; 94),
delete: [93; 94),
insert: "main()$0",
kind: Function,
lookup: "main",
detail: "fn main()",
},
]
"###);
}
}
18 changes: 18 additions & 0 deletions crates/ra_ide/src/completion/complete_pattern.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,22 @@ mod tests {
]
"###);
}

#[test]
fn completes_in_simple_macro_call() {
// FIXME: doesn't work yet because of missing error recovery in macro expansion
let completions = complete(
r"
macro_rules! m { ($e:expr) => { $e } }
enum E { X }

fn foo() {
m!(match E::X {
<|>
})
}
",
);
assert_debug_snapshot!(completions, @r###"[]"###);
}
}
Loading