Contributing to Intellij-Rust #2: Intention to substitute an associated type
This post is part of a series in which I describe my contributions to the IntelliJ Rust plugin.
- Previous post: #1 Fixing a simple bug in Nest Use intention
- Next post: #3 Quick fix to attach file to a module
In this post we’ll build a complete intention from scratch, based on one of my recent PRs. It will be a relatively simple intention because there are a lot of concepts that need to be explained along the way. In later posts I want to explain more complicated features, but first we need to understand the basic building blocks of IntelliJ APIs and the general concepts of the plugin. Therefore a lot of this post will be about explaining useful tips and tricks for writing intentions and working with the plugin in general.
You can find the original PR that I will go through here (spoiler alert!).
Finding an issue
We will be solving this issue created by matklad two years ago. He suggested to create an intention that would substitute (or “inline”) an associated type. Here is an example of how it should work:
impl Iterator for Foo {
type Item = i32;
fn next(&mut self) -> Option<<Self as Iterator>::Item/*caret*/> {
unimplemented!()
}
}
// ^ turns into v
impl Iterator for Foo {
type Item = i32;
fn next(&mut self) -> Option<i32> {
unimplemented!()
}
}
If you invoke it on a specific use of an associated type, it should replace (inline) the associated type with its actual value. Later we will see that the same intention will be able to also inline type aliases because they are represented in the plugin in the same way as associated types.
Associated types
Associated types is a Rust feature that allows you to declare one or more type placeholders in a trait and use them in the signature of the trait’s methods. Each implementor of the trait then has to choose a concrete type for each type placeholder. The first place where Rust beginners meet an associated type is usually in the ubiquitous Iterator trait. I won’t go into more detail here, as it’s out of scope for this post, but you can find more information about associated types for example here.
Intentions
I already covered this in the last post, but to recap: intention is an action that can be invoked
by the user over some piece of code. If you press Alt + Enter
, a list of available intentions is
displayed (based on where your caret is located). You can then select some intention from the list and
invoke it. Intentions typically operate in a very localized scope. Larger scale actions that can potentially
change each file in your project are usually handled by refactorings (we will implement one of those in a
later post). For example, the plugin contains intentions that add an impl
block for a struct, add
an else
branch to an if
statement, change a reference to be mutable or nest use statements (we have seen this one
in the previous post). Currently there are about
50 different intentions in the plugin. Now let’s see how we can add a new one.
Bootstrapping an intention
Naming things is the basis of programming, so first we should come up with a name.
I decided to name the intention SubstituteAssociatedTypeIntention
. I know, stunning :) Now that we
have a name, where should we create a file for the intention? If we search for *Intention*
, we will
get back a lot of files in the src/main/kotlin/org/rust/ide/intentions
directory, which looks like
a reasonable place where intentions might live, so let’s create a new file named
SubstituteAssociatedTypeIntention.kt
inside that directory.
Hmm, but what should we put into the file? How does an intention even look like? Well, there are already
over 50 different intentions, so why not copy one of them and modify it? I looked through the intentions
to find some short one, for example AddElseIntention
, which adds an else
branch to an if
statement:
class AddElseIntention : RsElementBaseIntentionAction<RsIfExpr>() {
override fun getText() = "Add else branch to this if statement"
override fun getFamilyName(): String = text
override fun findApplicableContext(
project: Project,
editor: Editor,
element: PsiElement
): RsIfExpr? {
...
}
override fun invoke(project: Project, editor: Editor, ctx: RsIfExpr) {
...
}
}
Right, so an intention should inherit from RsElementBaseIntentionAction
, parametrized with a generic
Context
argument, which states the type of an object that will be passed from findApplicableContext
to invoke
.
I have talked about context and these two methods already in the previous post,
but to recap, each intention needs to implement two methods:
-
findApplicableContext
- when you pressAlt + Enter
over some piece of code, the plugin iterates all intentions and calls this method on each one of them. If the method returns some non-null context, the intention will be offered in an intention list to the user. If the method returns null, the intention will not be offered. -
invoke
- if the user selects an intention to be executed from the intention list, this method will be called to actually perform the intention. It will receive the context fromfindApplicableContext
as one of its arguments.
Aside: How did I know that these are the important methods and what are they for?
Just click on the upward pointing arrow next to the method to go to the base method in the parent interface,
as it usually has a documentation comment. Intentions are also well documented on the RsElementBaseIntentionAction
class.
Sadly, sometimes documentation is missing or it’s not very descriptive. In such cases, you can try
to search through the IntelliJ SDK documentation,
although that’s also sometimes pretty terse. If you want to do something, but do not know which IntelliJ API
to use, a useful tactic that often helps is to check the source code of other IntelliJ plugins or IDEs
and search for a similar feature. I recommend to check out the source code of the Kotlin plugin
or the free Java IntelliJ community edition.
So we will have to implement these two methods for our intention. There are also two additional methods
that must be implemented: getText
and getFamilyName
. getText
Should return a string that will be displayed
as the intention’s text in the intention list after the user presses Alt + Enter
. getFamilyName
contains
a family name of the intention. Intentions with the same family name can be disabled/enabled in bulk.
Unless the intention has a specialized text based on where it is invoked, these two methods
usually return the same string (as is the case in AddElseIntention
).
Now that we are wiser, let’s create a skeleton of SubstituteAssociatedTypeIntention
with a reasonable
text
:
class SubstituteAssociatedTypeIntention
: RsElementBaseIntentionAction<SubstituteAssociatedTypeIntention.Context>() {
override fun getText() = "Substitute associated type"
override fun getFamilyName() = text
class Context
override fun findApplicableContext(
project: Project,
editor: Editor,
element: PsiElement
): Context? {
return null
}
override fun invoke(project: Project, editor: Editor, ctx: Context) {}
}
We don’t know what the Context
type will be, so I just created an empty inner class, we’ll change
it later. findApplicableContext
currently returns null
anyway, so the intention will never even
be offered!
If you use IDEA, you might notice that there is a warning around the intention’s name with the text
Intention does not have a description
. Each intention is required to have a short description in
HTML format, along with two template files with example code before and after applying the intention to it.
This description and the templates can then be displayed in Settings
, where you can find a list of all
intentions for a given language. The intention descriptions are located in src/main/resources/intentionDescriptions
,
each intention has a separate directory named after itself.
To fix this warning, let’s copy the description of an existing intention, for example our favourite AddElseIntention
.
The directory contains three files:
-
description.html
- a short description of the intention -
before.rs.template
- Rust code sample on which the intention will be applied -
after.rs.template
- Rust code sample showing how the code looks after the intention runs
Note that instead of /*caret*/
, the templates use <spot>
to mark the caret position.
Now we just have to provide some short text describing what our intention does and modify the templates.
I used the code from the original issue for the templates.
If we forgot to include a description or a template, CI tests would later loudly complain about their absence. EDIT: turns out that this wasn’t the case, as the tests for missing intention descriptions were.. well, missing . But this is now resolved since this PR got merged.
Now that we have an intention skeleton and its description, let’s write the first test for it!
Writing a failing test
Again, we first have to deal with naming and location of the test. Naming is pretty simple - just
append Test
after the name of the intention. But where should the test live? Let’s use
the same tactic as before - look for existing intention tests. If we open AddElseIntention
and press
Ctrl + Shift + T
on it, it will navigate us to its corresponding test, the unexpectedly named
AddElseIntentionTest
, located in src/test/kotlin/org/rust/ide/intentions/AddElseIntentionTest.kt
.
It looks something like this:
class AddElseIntentionTest : RsIntentionTestBase(AddElseIntention()) {
fun test1() = doUnavailableTest("""
fn main() {
42/*caret*/;
}
""")
fun `test simple`() = doAvailableTest("""
fn foo(a: i32, b: i32) {
if a == b {
println!("Equally");/*caret*/
}
}
""", """
fn foo(a: i32, b: i32) {
if a == b {
println!("Equally");
} else {/*caret*/}
}
""")
}
This shows us three things:
- Intention tests should inherit from
RsIntentionTestBase
and they should pass an instance of the tested intention to it. - We can use the
doUnavailableTest
method to check that the intention is not offered at the specified caret location. This basically checks that thefindApplicableContext
method of the intention is working properly. - We can use the
doAvailableTest
method to check that the intention is offered at the specified caret location and that the code looks as expected after the intention is executed (the first parameter of the method contains the original code and the second parameter contains the expected output after the intention is invoked on the original code). The original code has to include a/*caret*/
to specify where should the intention be invoked. The expected result code can also include a caret marker to test where has the caret moved after the intention is invoked, although it is optional.
Now that we know the basic structure, let’s create a test class named SubstituteAssociatedTypeIntentionTest
and let’s write a first test:
class SubstituteAssociatedTypeIntentionTest
: RsIntentionTestBase(SubstituteAssociatedTypeIntention()) {
fun `test associated type in type context`() = doAvailableTest("""
trait Trait {
type Item;
fn foo(&self) -> Self::Item;
}
impl Trait for () {
type Item = i32;
fn foo(&self) -> <Self as Trait>::/*caret*/Item { 0 }
}
""", """
trait Trait {
type Item;
fn foo(&self) -> Self::Item;
}
impl Trait for () {
type Item = i32;
fn foo(&self) -> i32 { 0 }
}
""")
}
The plugin names tests with spaces in backticks:
test associated type in type context
, so better get used to it (I personally quite like it).doAvailableTest
also callstrimIndent()
on both of the passed codes, so don’t worry about the indentation.
Deciding when to offer the intention
Now that we have a test that we can debug, let’s start writing the intention. First we have to implement
the findApplicableContext
method and decide what context type it will return. Let’s go through the
signature of the method:
override fun findApplicableContext(
project: Project,
editor: Editor,
element: PsiElement
): Context? {}
Here is a description of its parameters:
-
project
represents the opened IntelliJ project. This is a very important object from which you can access everything from the current project - its files, configuration, theRust
crate/workspace, etc. Conversely, without it you do not have access to pretty much anything, so it’s often passed as a parameter to various methods that you need to implement. -
editor
represents the opened editor tab with source code in whichAlt + Enter
was pressed. Through it you can for example query and change the caret position and you can also access the file that is currently opened in the editor. -
element
represents the PSI element located at the caret whenAlt + Enter
was pressed. This will be the parameter that interests us the most, because based on it we have to decide if the intention should be offered.
The basics of PSI have been explained in the previous post.
The intention should be available if element
resolves to an associated type. But what does that mean?
Name resolution
In simple terms, A
resolves to B
if the caret moves to B
when you Ctrl + <click>
on A
. In such
case we can say that A
has a reference to B
and it can resolve it to find B
. However, do not
confuse this term with Rust references like &x
or &mut x
. In this context a reference from A
to
B
means that B
is a declaration of something (module, local variable, structure) and
A
is some usage of B
that refers to it. An example to make this clear:
struct S {
a: u32
}
fn foo(s: S) {
let x = s.a;
let y = x + 1;
let z = std::vec::Vec::<u32>::new();
}
-
S
ins: S
resolves to the structstruct S
-
s
ins.a
resolves to the parameters: S
-
x
inx + 1
resolves to the local variablelet x
-
std
instd::vec::Vec::<u32>::new()
resolves to thestd
module -
std::vec::Vec
instd::vec::Vec::<u32>::new()
resolves to theVec
struct -
std::vec::Vec::<u32>::new()
instd::vec::Vec::<u32>::new()
resolves to the associated methodnew
ofVec
The system that resolves references from usages of items to their declarations is usually called
Name resolution and it is implemented both by rustc
and the IntelliJ APIs, where it is a first-class
concept.
So, how do we recognize if element
is something that resolves to an associated type? In the example
above, maybe you have noticed that all of the references had a similar look and feel: S
, s
, x
,
std::vec::Vec
, etc. If you paid attention
in the previous post, this should look familiar - they are all paths! If we put a caret on x
in the above example and use the PsiViewer
plugin to examine the PSI, we quickly find out that paths
are represented in the plugin by the RsPath
class, which (like all PSI elements) inherits from PsiElement
.
Therefore, we first have to find out if element
is a RsPath
. You might be tempted to simply perform
a (safe) cast: element as? RsPath
, but it is not that simple. It might happen that the caret was located at
an element that is a child of a path (the PSI elements form a tree), and in such case the cast would not succeed.
For example in std:/*caret*/:vec::Vec<u32>
, the element
would be ::
, a simple text token that is a child
of the path that we want to resolve.
Paths are represented hierarchically; in
std::vec::Vec
:std
is actually a path that is a child of thestd::vec
path, which is a child ofstd::vec::Vec
path, etc. We will see this in action in future posts, but it’s not important for now.
Luckily, the IntelliJ PSI offers a large set of APIs for navigating the PSI tree. You can use them to search for a parent of a specific PSI type:
val path = element.parentOfType<RsPath>() ?: return null
If element
has some parent that is a path, it will be returned, otherwise we return null
and
the intention will not be offered.
There are multiple ways of asking for parents, you can ask for an ancestor, a parent or a context. To be honest, I do not understand the differences in detail, but so far using parent lookup was usually the right choice by default.
Now that we have a path, we have to resolve it and check if the result is an associated type. How do you
resolve a path? By implementing a deeply complex logic that iterates over scopes and tries to match items
in each scope to the name/path that is being resolved according to the rules of Rust™. Luckily, we do not have
to implement it by ourselves, as the plugin has an implementation of name resolution. Paths inherit from
RsReferenceElement
, which provides them with a reference
attribute that has a resolve
method which does
the job. Associated types are represented by RsTypeAlias
(you can again find this with PsiViewer
),
so let’s use something like this:
val typeAlias = path.reference?.resolve() as? RsTypeAlias ?: return null
If the reference cannot be resolved or if it does not resolve to an associated type, return null
to
disable the intention.
The last thing to check is whether the associated type has some type actually assigned to it. If not,
we do not have anything to substitute, so the intention should not be offered. Let’s access the typeReference
attribute, which is of type RsTypeReference
(more on that later):
val type = typeAlias.typeReference ?: return null
How did I know that typeReference
is the correct attribute that contains the assigned type? I opened
RsTypeAlias
to see what attributes it has, then put a breakpoint in this method to examine the typeAlias
variable to see which of its attributes contains something that looks like the assigned type. If you have
some PsiElement
and you’re not sure what is it, read its text
attribute, which contains the original
raw source text. Then it’s usually easy to see what the element represents. In this case:
type A = u32;
typeAlias.typeReference
would represent the type u32
.
Now we have a path that resolves to an associated type and the concrete type that should be substituted.
Since we will need those things for the actual functionality of the intention, let’s return those two things
as context to have them available in the invoke
method.
This is the final code of the findApplicableContext
method along with the modified Context
class:
data class Context(val path: RsPath,
val typeAliasReference: RsTypeReference)
override fun findApplicableContext(
project: Project,
editor: Editor,
element: PsiElement
): Context? {
val path = element.parentOfType<RsPath>() ?: return null
val typeAlias = path.reference?.resolve() as? RsTypeAlias ?: return null
val type = typeAlias.typeReference ?: return null
return Context(path, type)
}
Why do we store
typeAliasReference
inContext
when we can get it from thepath
? Simply to reduce code and error handling in theinvoke
method.
Implementing the invoke
method
As a reminder, here is the signature of the invoke
method:
override fun invoke(project: Project, editor: Editor, ctx: Context)
We already know project
and editor
and ctx
is exactly the thing that we have returned
from findApplicableContext
. Easy. Now we just need to replace ctx.path
with the type
stored in the context. In theory, we could just replace it textually, i.e. literally take the string
containing the text
of the path and replace it with text
of the type. However, this is usually
not how it should be done. The proper solution is to manipulate the PSI tree and not the raw text.
There is a very useful utility class for this, RsPsiFactory
. It allows you to build PSI nodes from
other nodes or from raw strings. We need to create a new path that will represent the type that we are
substituting and then replace the original path with the newly created path.
First, how do we get the type that we want to substitute? In Context
, we have a RsTypeReference
,
which represents an element that refers to a Rust type. It has a type
attribute, which returns the actual
type it’s referencing. Note that the classes representing Rust types inherit from the Ty
class, not from
PsiElement
, as they live completely outside of the PSI world. RsTypeReference
simply maps PSI type elements to Ty
objects (this mapping is implemented by the type inference subsystem).
How do we create a path from a type? RsPsiFactory
has a method called tryCreatePath
that takes a string
and tries to build a PSI RsPath
object out of it. But we don’t have a string yet, we have a type, so
first we have to convert it to a string. There is a very customizable type rendering API for this,
you can use it via the renderInsertionSafe
method, which renders a type to a string in such a way
that it is safe to be inserted into Rust code. Let’s combine all of this in invoke
:
val factory = RsPsiFactory(project)
val typeRef = ctx.typeAliasReference
// if the path couldn't be parsed, do not continue
val createdPath = factory.tryCreatePath(
typeRef.type.renderInsertionSafe()
) ?: return
Now we just replace the original path with the new one using the replace
method:
ctx.path.replace(createdPath)
And that’s it! If we try to run the first test, it passes . We have a basic implementation of the intention in ~15 lines, not bad. Of course in a while we’ll see that the implementation will need to grow somewhat once we’ll have to account for the nasty edge cases :)
By the way, have you noticed that the element representing an associated type is called RsTypeAlias
?
That is because associated types and type aliases are basically the same thing1, with the same syntax (type X = Y
).
The only difference is that associated types are associated with a trait, but that does not really matter to
our intention. Therefore our intention should just work also for substituting/inlining type aliases out
of the box!
Adding more tests
Now that we have a basic implementation of the intention, we should add tests for various edge cases that could happen. I will just post short snippets and not the whole test methods, since they are quite repetitive.
What happens if the type has a generic parameter?
impl<T> Trait<T> for () {
type Item = S<T>;
fn foo(&self, item: Self::/*caret*/Item) -> T {}
// turns into
fn foo(&self, item: S<T>) -> T {}
}
Awesome, both the type rendering and path creation handled the generic type, and we have another passing test!
What if the type is used in an expression context, i.e. a function call?
impl Trait for () {
type Item = S;
fn foo(&self) {
<Self as Trait>::/*caret*/Item::bar();
}
// turns into
fn foo(&self) {
S::bar();
}
}
Also works, nice.
What if it has a generic parameter AND it is used in an expression context?
impl Trait for () {
type Item = S<u32>;
fn foo(&self) {
<Self as Trait>::/*caret*/Item::bar();
}
// turns into
fn foo(&self) {
S<u32>::bar();
}
}
Now although this may look correct, this is in fact not valid Rust code. In an expression context,
the <
in S<u32>
is parsed as a comparison operator, so we have to use the turbofish syntax:
S::<u32>::bar();
The test will actually fail because the inserted path could not even be parsed by the plugin. So how do we handle this in the intention?
We need to find out if the path that we are replacing is in a type or expression context. If you compare
paths used in a type context (e.g. fn foo(s: <Path>)
) and in an expression context (i.e. let a = <Path>::new()
)
with the PsiViewer
, you’ll notice that in type contexts the paths have a RsTypeReference
2 as a PSI parent,
which we have already met in findApplicableContext
. So let’s use this knowledge to check if we are
indeed inside a type context or not:
val isTypeContext = ctx.path.parentOfType<RsTypeReference>() != null
After that, we have to check if the created path has any generic arguments (if not, we don’t need to add
turbofish). You can find that by examining the typeArgumentList
attribute of a path. If we find that we are indeed
inside expression context and that the created path has some generic arguments, we’ll just insert ::
in the
middle of it like it’s no big deal:
// identifier: PSI element with the path segment name
// endOffsetInParent: offset where the identifier ends, relative to its parent
val end = createdPath.identifier?.endOffsetInParent ?: 0
val pathText = createdPath.text
// I'm not even sorry for this
val newPath = pathText.substring(0, end) + "::" + pathText.substring(end)
For example in S<u32>
, the identifier
is S
and it ends at offset 1
. Therefore this code will
insert ::
at position 1
and change this path to S::<u32>
.
With this change, the test passes. This is how the invoke
method looks now:
val factory = RsPsiFactory(project)
val typeRef = ctx.typeAliasReference
val isTypeContext = ctx.path.parentOfType<RsTypeReference>() != null
val createdPath = factory.tryCreatePath(typeRef.type.renderInsertionSafe())
?: return
// S<u32> -> S::<u32> in expression context
val insertedPath: RsPath = if (!isTypeContext &&
createdPath.typeArgumentList != null) {
val end = createdPath.identifier?.endOffsetInParent ?: 0
val pathText = createdPath.text
val newPath = pathText.substring(0, end) + "::" + pathText.substring(end)
val path = factory.tryCreatePath(newPath) ?: return
ctx.path.replace(path) as RsPath
} else {
ctx.path.replace(createdPath) as RsPath
}
So, any more edge cases that come to mind? What if you substitute a type that is not available in the scope where the intention was invoked?
use foo::B;
mod foo {
pub struct A;
pub type B = A;
}
fn foo() -> /*caret*/B { unreachable!() }
// turns into
fn foo() -> A { unreachable!() }
Oops! A
is not available in the scope where we have used the intention. Luckily, the plugin already
contains powerful API to import missing types from a given PsiElement
or a type reference, so it’s enough
to add this one liner to the end of the invoke
method:
RsImportHelper.importTypeReferencesFromTy(insertedPath, typeRef.type)
The first parameter is a context where should the import happen, and the second parameter is the type to be imported (if necessary). With this change the above the intention auto-imports any necessary types:
use foo::{B, A};
What if the type that is being substituted already has some generic argument?
type Type<T> = Vec<T>;
fn bar(t: Type<u32>) {}
// turns into
fn bar(t: Vec<T>) {}
This also doesn’t work properly, as it should generate Vec<u32>
.
However, I will not show how to implement support for this case, because I only found
about it while writing this blog post and I have yet to send another PR to fix this . Also
this post is already pretty long – but don’t worry, we’re almost at the end!
Edit: it turned out that there are multiple issues with this intention being applied to type aliases (mainly because of generics). So for now I have enabled it only for associated types.
We should also add some tests that check that the intention is not offered on places where it shouldn’t be.
Does that mean that we should add a test for each possible Rust element that does not resolve to an associated type?
Probably not a good idea, as that would be a lot of (repetitive) tests.
I generally only tend to include sanity checks and edge cases that I know the intention worries about. Basically for
every if
condition in the intention that checks some special case, there should be a test that tests if that
special case is handled correctly. We now know that findApplicableContext
should filter out invalid references
and associated types without an actual assigned type, so let’s add a test for the latter situation:
fun `test unavailable on trait associated type`() = doUnavailableTest("""
trait Trait { type Item; }
fn foo<T: Trait>() -> T::/*caret*/Item { unimplemented!() }
""")
I have also added an additional test that checks if the intention works for associated types in traits that
have a default value. This is currently a nightly
-only feature, but why not make the intention future-proof?
Testing the intention in GUI
Great, so now we have an implemented intention, we have test coverage, but before we proclaim victory
and send a PR, let’s test the intention in the GUI first, to experience the satisfaction of seeing it in action.
The repository of the plugin contains an exampleProject
directory with a trivial Rust project that is
commonly used to test the plugin manually. Let’s launch the RunClion
action to start up CLion, open
the exampleProject
in it, copy paste the code from the original issue into it and BEHOLD:
Sigh . The intention does not seem to appear in the intention list. By putting a breakpoint
inside the findApplicableContext
method, we can quickly realize that the method is not even being
called at all. Although it works in tests, tt seems that something else must be done for the intention
to be registered by the IDE.
When working on the plugin, you might often meet this situation where you suspect that there’s some configuration that needs to be added for something to work, but you don’t know what or where it is. There are several ways of tackling this:
- Look in the official documentation. I often find it lacking, there are some good resources about explaining high-level concepts, but usually it doesn’t help me very much.
- Look how the problem was handled before. This is not the first intention in the plugin, so it might be a good idea
to check some previous PR that authored an intention to see what it had to do to make the intention available in the GUI.
For example let’s take our venerable
AddElseIntention
. How do we find out in which PR it was authored?- Use
git blame
, which can find the last commit that modified a given file or even a specific line. You can find a tutorial for its usage here. - Right click on the column with line numbers in IDEA, click on
Annotate
and it will show you the last commit that modified each line in the currently opened file. - Look through merged PRs in the project’s repository.
- Use
- Often the fastest solution, and the one that I have used, is to take an existing intention and
simply grep for its name. If it is registered in the plugin, its name has to be mentioned somewhere in the project, right?
If we search for files (
Ctrl + Shift + F
) containing the stringAddElseIntention
, we get these three results:-
src/.../intentions/AddElseIntention.kt
- the intention itself, no surprise there -
src/test/.../intentions/AddElseIntentionTest.kt
- the intention’s test, we already know this one -
src/main/resources/META-INF/rust-core.xml
- A-HA! We didn’t modify this file yet. It looks like some XML with configuration of the plugin and amongst other things it contains this record that seems to tell the plugin about the existence of the intention:
<intentionAction> <className>org.rust.ide.intentions.AddElseIntention</className> <category>Rust</category> </intentionAction>
-
We have found the missing piece! Now let’s just add this:
<intentionAction>
<className>org.rust.ide.intentions.SubstituteAssociatedTypeIntention</className>
<category>Rust</category>
</intentionAction>
to rust-core.xml
and voilà, our intention is offered in the GUI!
After you put the intention into
rust-core.xml
, a little connector icon appears next to the intention class in IDEA, so that you can go back and forth between the intention and it’s registration point. There are also additional icons for going to the intention’s description and templates.
Wrapping it up
After implementing the intention, I sent a PR
named INT: add intention to substitute an associated type
to the plugin (you can find the whole source code
of the intention in that PR). The discussion was fairly short, mchernyavsky
just noticed that the intention doesn’t import types, I fixed that and that was all. Even then,
it took about a month until the PR was merged, as sometimes it takes time for the maintainers to find
bandwidth for reviewing. So have patience and do not despair!
If you’re reading this, thanks for sticking with me up until the end of this post. If you have any comments, let me know on Reddit.