Skip to content

feat: adds hover hint to ".." in record pattern #13638

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 10 commits into from
Nov 25, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
101 changes: 58 additions & 43 deletions crates/ide/src/hover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ pub(crate) fn hover(
original_token.parent().and_then(ast::TokenTree::cast),
Some(tt) if tt.syntax().ancestors().any(|it| ast::Meta::can_cast(it.kind()))
);

// prefer descending the same token kind in attribute expansions, in normal macros text
// equivalency is more important
let descended = if in_attr {
Expand All @@ -135,54 +136,67 @@ pub(crate) fn hover(
sema.descend_into_macros_with_same_text(original_token.clone())
};

// FIXME: Definition should include known lints and the like instead of having this special case here
let hovered_lint = descended.iter().find_map(|token| {
let attr = token.parent_ancestors().find_map(ast::Attr::cast)?;
render::try_for_lint(&attr, token)
});
if let Some(res) = hovered_lint {
return Some(RangeInfo::new(original_token.text_range(), res));
}

// try lint hover
let result = descended
.iter()
.filter_map(|token| {
let node = token.parent()?;
let class = IdentClass::classify_token(sema, token)?;
if let IdentClass::Operator(OperatorClass::Await(_)) = class {
// It's better for us to fall back to the keyword hover here,
// rendering poll is very confusing
return None;
}
Some(class.definitions().into_iter().zip(iter::once(node).cycle()))
.find_map(|token| {
// FIXME: Definition should include known lints and the like instead of having this special case here
let attr = token.parent_ancestors().find_map(ast::Attr::cast)?;
render::try_for_lint(&attr, token)
})
.flatten()
.unique_by(|&(def, _)| def)
.filter_map(|(def, node)| hover_for_definition(sema, file_id, def, &node, config))
.reduce(|mut acc: HoverResult, HoverResult { markup, actions }| {
acc.actions.extend(actions);
acc.markup = Markup::from(format!("{}\n---\n{}", acc.markup, markup));
acc
});
// try item definitions
.or_else(|| {
descended
.iter()
.filter_map(|token| {
let node = token.parent()?;
let class = IdentClass::classify_token(sema, token)?;
if let IdentClass::Operator(OperatorClass::Await(_)) = class {
// It's better for us to fall back to the keyword hover here,
// rendering poll is very confusing
return None;
}
Some(class.definitions().into_iter().zip(iter::once(node).cycle()))
})
.flatten()
.unique_by(|&(def, _)| def)
.filter_map(|(def, node)| hover_for_definition(sema, file_id, def, &node, config))
.reduce(|mut acc: HoverResult, HoverResult { markup, actions }| {
acc.actions.extend(actions);
acc.markup = Markup::from(format!("{}\n---\n{}", acc.markup, markup));
acc
})
})
// try keywords
.or_else(|| descended.iter().find_map(|token| render::keyword(sema, config, token)))
// try rest item hover
.or_else(|| {
descended.iter().find_map(|token| {
if token.kind() != DOT2 {
return None;
}

if result.is_none() {
// fallbacks, show keywords or types
let rest_pat = token.parent().and_then(ast::RestPat::cast)?;
let record_pat_field_list =
rest_pat.syntax().parent().and_then(ast::RecordPatFieldList::cast)?;

let res = descended.iter().find_map(|token| render::keyword(sema, config, token));
if let Some(res) = res {
return Some(RangeInfo::new(original_token.text_range(), res));
}
let res = descended
.iter()
.find_map(|token| hover_type_fallback(sema, config, token, &original_token));
if let Some(_) = res {
return res;
}
}
result.map(|mut res: HoverResult| {
res.actions = dedupe_or_merge_hover_actions(res.actions);
RangeInfo::new(original_token.text_range(), res)
})
let record_pat =
record_pat_field_list.syntax().parent().and_then(ast::RecordPat::cast)?;

Some(render::struct_rest_pat(sema, config, &record_pat))
})
});

result
.map(|mut res: HoverResult| {
res.actions = dedupe_or_merge_hover_actions(res.actions);
RangeInfo::new(original_token.text_range(), res)
})
// fallback to type hover if there aren't any other suggestions
// this finds its own range instead of using the closest token's range
.or_else(|| {
descended.iter().find_map(|token| hover_type_fallback(sema, config, token, &token))
})
}

pub(crate) fn hover_for_definition(
Expand Down Expand Up @@ -269,6 +283,7 @@ fn hover_type_fallback(
};

let res = render::type_info(sema, config, &expr_or_pat)?;

let range = sema
.original_range_opt(&node)
.map(|frange| frange.range)
Expand Down
48 changes: 47 additions & 1 deletion crates/ide/src/hover/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ use ide_db::{
use itertools::Itertools;
use stdx::format_to;
use syntax::{
algo, ast, match_ast, AstNode, Direction,
algo,
ast::{self, RecordPat},
match_ast, AstNode, Direction,
SyntaxKind::{LET_EXPR, LET_STMT},
SyntaxToken, T,
};
Expand Down Expand Up @@ -250,6 +252,50 @@ pub(super) fn keyword(
Some(HoverResult { markup, actions })
}

/// Returns missing types in a record pattern.
/// Only makes sense when there's a rest pattern in the record pattern.
/// i.e. `let S {a, ..} = S {a: 1, b: 2}`
pub(super) fn struct_rest_pat(
sema: &Semantics<'_, RootDatabase>,
config: &HoverConfig,
pattern: &RecordPat,
) -> HoverResult {
let missing_fields = sema.record_pattern_missing_fields(pattern);

// if there are no missing fields, the end result is a hover that shows ".."
// should be left in to indicate that there are no more fields in the pattern
// example, S {a: 1, b: 2, ..} when struct S {a: u32, b: u32}

let mut res = HoverResult::default();
let mut targets: Vec<hir::ModuleDef> = Vec::new();
let mut push_new_def = |item: hir::ModuleDef| {
if !targets.contains(&item) {
targets.push(item);
}
};
for (_, t) in &missing_fields {
walk_and_push_ty(sema.db, &t, &mut push_new_def);
}

res.markup = {
let mut s = String::from(".., ");
for (f, _) in &missing_fields {
s += f.display(sema.db).to_string().as_ref();
s += ", ";
}
// get rid of trailing comma
s.truncate(s.len() - 2);

if config.markdown() {
Markup::fenced_block(&s)
} else {
s.into()
}
};
res.actions.push(HoverAction::goto_type_from_targets(sema.db, targets));
res
}

pub(super) fn try_for_lint(attr: &ast::Attr, token: &SyntaxToken) -> Option<HoverResult> {
let (path, tt) = attr.as_simple_call()?;
if !tt.syntax().text_range().contains(token.text_range().start()) {
Expand Down
35 changes: 35 additions & 0 deletions crates/ide/src/hover/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5307,3 +5307,38 @@ fn main() { $0V; }
"#]],
);
}

#[test]
fn hover_rest_pat() {
check(
r#"
struct Struct {a: u32, b: u32, c: u8, d: u16};

fn main() {
let Struct {a, c, .$0.} = Struct {a: 1, b: 2, c: 3, d: 4};
}
"#,
expect![[r#"
*..*
```rust
.., b: u32, d: u16
```
"#]],
);

check(
r#"
struct Struct {a: u32, b: u32, c: u8, d: u16};

fn main() {
let Struct {a, b, c, d, .$0.} = Struct {a: 1, b: 2, c: 3, d: 4};
}
"#,
expect![[r#"
*..*
```rust
..
```
"#]],
);
}