Skip to content
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

Better inlay hints in 'for' loops #5997

Merged
merged 4 commits into from
Oct 3, 2020

Conversation

popzxc
Copy link
Contributor

@popzxc popzxc commented Sep 13, 2020

For #5206 (one part of the fix).

This PR refines the logic of spawning an inlay hints in for loops. We only must provide a hint if the following criteria are met:

  • User already typed in keyword.
  • Type of expression is known and it's not unit.

However: I don't know why, but I was unable to make complete_for_hint test work. Either without or with my changes, I was always getting this test failed because no hint was spawned for the loop variable.

This change works locally, so I would really appreciate an explanation why this test isn't working now and how to fix it.

image

@SomeoneToIgnore
Copy link
Contributor

SomeoneToIgnore commented Sep 13, 2020

I was unable to make complete_for_hint test work

This test uses the IntoIterator trait functionality that is a part of stdlib.
RA does not use stdlib in its tests due to test speed concerns: no real type inference can be done for such cases ergo no inlay hints.

There are existing ways to emulate the functionality you need: https://github.com/rust-analyzer/rust-analyzer/blob/251ef93ac3bbb138a2eedf6090f2f56f1a15d898/crates/hir_ty/src/tests/traits.rs#L164-L201

So I wonder, if you can reuse this somehow.

@popzxc
Copy link
Contributor Author

popzxc commented Sep 14, 2020

Well, after a bit of shallow experimenting (like code below, + several different attempts to play with check function and the fixture contents itself) I still had no success. It seems that the linked test uses TestDb declared in hir_ty crate, and it somehow different from the database used in MockAnalysis.

Sad to admit, but I need more assistance on that matter. ¯\_(ツ)_/¯

    #[test]
    fn complete_for_hint() {
        check(
            r#"
//- /main.rs crate:main deps:core,alloc
fn main() {
    let mut data = Vec::new();
    data.push("foo");
    for i in data {
      //^ &str
      i;
    //^ &str
    }
}

//- /core.rs crate:core
#[prelude_import] use iter::*;
mod iter {
    trait IntoIterator {
        type Item;
    }
}
//- /alloc.rs crate:alloc deps:core
mod collections {
    struct Vec<T> {}
    impl<T> Vec<T> {
        fn new() -> Self { Vec {} }
        fn push(&mut self, t: T) { }
    }
    impl<T> IntoIterator for Vec<T> {
        type Item=T;
    }
}
"#,
        );

@SomeoneToIgnore
Copy link
Contributor

In this case the best bet would be to ping Matklad next week after his vacation.

I might try to take a stab at this later, but cannot guarantee anything now alas.

@popzxc
Copy link
Contributor Author

popzxc commented Sep 15, 2020

Sure, it's not an emergency :)

@SomeoneToIgnore
Copy link
Contributor

SomeoneToIgnore commented Sep 17, 2020

I've checked your test a bit, looks like there was an issue with the compilation.
Now we're able to resolve the main type and have proper //^ marks on the places where inlay hints should be present:

    #[test]
    fn complete_for_hint() {
        check(
            r#"
//- /main.rs crate:main deps:core
pub struct Vec<T> {}

impl<T> Vec<T> {
    pub fn new() -> Self { Vec {} }
    pub fn push(&mut self, t: T) {}
}

impl<T> IntoIterator for Vec<T> {
    type Item=T;
}

fn main() {
    let mut data = Vec::new();
      //^^^^^^^^ Vec<&str>
    data.push("foo");
    for i in data {
      let z = i;
        //^ &str
    }
}

The test fails still, it appears to me that it happens due to the trait solving: the actual IntoIterator trait is different (has more associated types and methods that we need to implement) and inlay hints go throughout the whole system to determine the type: what works for type inference tests fails here due to other things that interfere, such as the trait solving mentioned.

I see the following options:

@popzxc
Copy link
Contributor Author

popzxc commented Sep 19, 2020

Thanks for that info! Unfortunately, after some experimenting I was still unable to make check function work.

I guess I'll just wait for Matklad to get back from vacation, and implement whatever he'll find appropriate.

@SomeoneToIgnore
Copy link
Contributor

@matklad , any approach you can think of?

I think there should be a test, but the most feasible one seems to be the slow test.

@matklad
Copy link
Member

matklad commented Oct 2, 2020

Yeah, I plan to look into this today. The TL;DR is that #5997 (comment) should work, but we probably should fix our testing infra to at least not have two independent testing infras :-)

@SomeoneToIgnore
Copy link
Contributor

Here's somewhat similar demand for the Iterator : https://rust-lang.zulipchat.com/#narrow/stream/185405-t-compiler.2Fwg-rls-2.2E0/topic/.233750

So we might need to come up with some good way to test the iterators' hints.

@matklad
Copy link
Member

matklad commented Oct 2, 2020

Didn't get to making core-aware tests easier per se, but I've at least removed duplication between IDE and hir tests: #6123.

After this, I think #5997 (comment) should work.

@popzxc
Copy link
Contributor Author

popzxc commented Oct 2, 2020

Nice! Will check that tomorrow.

@matklad
Copy link
Member

matklad commented Oct 2, 2020

Yup, the following test now works:

    #[test]
    fn complete_for_hint() {
        check(
            r#"
//- /main.rs crate:main deps:core
pub struct Vec<T> {}

impl<T> Vec<T> {
    pub fn new() -> Self { Vec {} }
    pub fn push(&mut self, t: T) {}
}

impl<T> IntoIterator for Vec<T> {
    type Item=T;
}

fn main() {
    let mut data = Vec::new();
      //^^^^^^^^ Vec<&str>
    data.push("foo");
    for i in data {
      //^ &str
      let z = i;
        //^ &str
    }
}

//- /core.rs crate:core
#[prelude_import] use iter::*;
mod iter {
    trait IntoIterator {
        type Item;
    }
}
//- /alloc.rs crate:alloc deps:core
mod collections {
    struct Vec<T> {}
    impl<T> Vec<T> {
        fn new() -> Self { Vec {} }
        fn push(&mut self, t: T) { }
    }
    impl<T> IntoIterator for Vec<T> {
        type Item=T;
    }
}
"#,
        );
    }

@popzxc popzxc force-pushed the better-inline-hints-for branch from 9241698 to a58441a Compare October 3, 2020 05:38
@popzxc
Copy link
Contributor Author

popzxc commented Oct 3, 2020

Yay, it works!

I also had to remove for_expression test from inlay_hints.rs, because it seems that it relied on the type deduction from the start += increment; statement rather than from the iterator type, which is kind of indirect.

Hopefully, new complete_for_hint can be a (somewhat more correct) substitution for it.

@@ -249,6 +251,14 @@ fn should_not_display_type_hint(
return it.condition().and_then(|condition| condition.pat()).is_some()
&& pat_is_enum_variant(db, bind_pat, pat_ty);
},
ast::ForExpr(it) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Small nit: I think we can make it more readable by writing something like

// We *should* display hint only if user provided "in {expr}" and we know the type of expr (and it's not unit).
// Type of expr should be iterable.
return it.in_token().is_none() ||
    it.iterable()
        .and_then(|iterable_expr|sema.type_of_expr(&iterable_expr))
        .map(|iterable_ty| iterable_ty.is_unknown() || iterable_ty.is_unit())
        .unwrap_or(true)

@SomeoneToIgnore
Copy link
Contributor

Thanks for a good motivation to fix our testing infra, I've managed to submit a long stalled PR because of this.
The change looks good to me, you might improve the code readability in the ast::ForExpr(it) => {, but that's minor and not mandatory.

bors d+

@bors
Copy link
Contributor

bors bot commented Oct 3, 2020

✌️ popzxc can now approve this pull request. To approve and merge a pull request, simply reply with bors r+. More detailed instructions are available here.

@SomeoneToIgnore
Copy link
Contributor

Interesting, I get the () instantly I finish typing in in such case:

image

It looks like RA takes Ok(()) as an iterable() the for expr.
As one way to fix, we can check the children of the ForExpr and display nothing, if there's none (or no block expression).
I don't think this is the best way, so not insisting on anything in this PR (you've anyway mentioned that this is a part of the fix).

@popzxc
Copy link
Contributor Author

popzxc commented Oct 3, 2020

Yeah, I thought about this case and was not sure if such a situation should be handled at all. Once in keyword is typed, it becomes hard to guess whether code below is part of expression or not yet. I considered the following situations:

  1. User has typed some_iterable() and first thought that it would be some_iterable().map(...), but then realized that for would work better. They return to the beginning of the line and add for i in .... Now we should show the type.
  2. User has the function which returns Result<SomeType> and first typed Ok(some_val) and then started writing the loop (as in your example). Logically, we should not show the type.

These examples are roughly equivalent, and the meaning (and expected behavior) is not expressed in the code, it's just in the head of code author. In my opinion, once in keyword is typed, we should not try to predict the future, and just interpret code as usual.

Otherwise we can stuck writing exceptions for heuristics.

@popzxc
Copy link
Contributor Author

popzxc commented Oct 3, 2020

bors r+

@bors
Copy link
Contributor

bors bot commented Oct 3, 2020

@bors bors bot merged commit e5f252a into rust-lang:master Oct 3, 2020
@popzxc popzxc deleted the better-inline-hints-for branch October 3, 2020 11:06
bors bot added a commit that referenced this pull request Oct 17, 2020
6262: Do not spawn redundant hints r=SomeoneToIgnore a=popzxc

Closes #5206

This is a second part of the fix (first was #5997).

This PR adds a new method to the `CompletionContext`: `no_completion_required`. If this method returns `true`, it essentially means that user is unlikely to expect any hints from the IDE at this cursor position.

Currently, checks for the following cases were added:

- Previous item is `fn`: user creates a new function, names of existing functions won't be helpful. Exception for this case is `impl Foo for Bar` -- we must suggest trait function names.
- User entered `for _ i<|>`: it's obviously going to be `in` keyword, any hints here will be confusing.

More checks may be added there later, but currently I've only figured two cases.

![no_redundant_hints](https://user-images.githubusercontent.com/12111581/96332088-da4d2a00-106a-11eb-89a1-1159ece18f9d.png)



Co-authored-by: Igor Aleksanov <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants