But these tools also can work as a way to teach the user about what alternatives they have that they would otherwise not know about. It doesn't remove the responsibility of the user to learn, it just adds another opportunity to do so.

If you look at the structured suggestions that rustc gives, the message succinctly explains what concept you're changing, giving you some keywords that you can Google for, and the transformed code allowing you to continue without having to consider the implications necessarily. You can't get that immediacy from a book.

For example, the following code

fn foo() -> Iterator {
vec![1, 2, 3].into_iter()
}

will tell you that Iterator is a trait (so you should use the appropriate style and mark it with dyn to make it clear) and that it has an associated type that must be specified (showing you the syntax you'd need)

warning: trait objects without an explicit `dyn` are deprecated
--> src/main.rs:1:13
|
1 | fn foo() -> Iterator {
| ^^^^^^^^ help: use `dyn`: `dyn Iterator`
|
= note: `#[warn(bare_trait_objects)]` on by default

error[E0191]: the value of the associated type `Item` (from trait `std::iter::Iterator`) must be specified
--> src/main.rs:1:13
|
1 | fn foo() -> Iterator {
| ^^^^^^^^ help: specify the associated type: `Iterator<Item = Type>`

If you follow it's advice

fn foo() -> dyn Iterator<Item = i32> {
vec![1, 2, 3].into_iter()
}

Then it will tell you that you cannot ever return a trait directly, because its unsized (?Sized), but the compiler peeks at the function body and checks which of the 3 solutions could apply. In this case, it introduces you to the impl Trait feature (opaque type that implements a given trait):

error[E0746]: return type cannot have an unboxed trait object
--> src/main.rs:1:13
|
1 | fn foo() -> dyn Iterator<Item = usize> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
|
= note: for information on `impl Trait`, see <https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits>
help: use `impl Iterator<Item = usize>` as the return type, as all return paths are of type `std::vec::IntoIter<{integer}>`, which implements `Iterator<Item = usize>`
|
1 | fn foo() -> impl Iterator<Item = usize> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^

But if the implementation were different it would show you how to use boxed trait objects:

error[E0746]: return type cannot have an unboxed trait object
--> src/main.rs:1:13
|
1 | fn foo() -> dyn Iterator<Item = usize> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
|
= note: for information on trait objects, see <https://doc.rust-lang.org/book/ch17-02-trait-objects.html#using-trait-objects-that-allow-for-values-of-different-types>
= note: if all the returned values were of the same type you could use `impl Iterator<Item = usize>` as the return type
= note: for information on `impl Trait`, see <https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits>
= note: you can create a new `enum` with a variant for each returned type
help: return a boxed trait object instead
|
1 | fn foo() -> Box<dyn Iterator<Item = usize>> {
2 | if true {
3 | Box::new(vec![1, 2, 3].into_iter())
4 | } else {
5 | Box::new(None.into_iter())

This is not perfect, as it is a complex topic and its done in the cli output, but there's no reason that having this level of interactive learning in rust-analyzer wouldn't be a net positive.