We use Kotlin language for the plugin. If you can program in Java, you should be able to read and write Kotlin code right away. Kotlin is deeply similar to Java, but has less verbose syntax and better safety. It also shares some characteristics with Rust: type inference, immutability by default, no null pointers (apart from those that come from Java).
If you are unsure how to implement something in Kotlin, ask a question in our Gitter, send a PR in Java, or use Convert Java to Kotlin action in the IDE.
Java 17 is required for development. For example, you can install openJDK or Amazon Corretto. You can also install Java just from your IntelliJ IDEA.
git clone https://github.com/intellij-rust/intellij-rust.git
cd intellij-rust
Run RunIDEA
or RunCLion
run configuration to build and launch development IDE (IntelliJ IDEA or CLion respectively) with the plugin.
We use gradle with gradle-intellij plugin to build the plugin.
It comes with a wrapper script (gradlew
in the root of the repository) which downloads appropriate version of gradle
automatically as long as you have JDK installed.
Common Gradle tasks are:
-
./gradlew :plugin:buildPlugin
-- fully build plugin and create an archive atplugin/build/distributions
which can be installed into your IDE viaInstall plugin from disk...
action found inSettings > Plugins
. -
./gradlew :plugin:runIde
-- run a development IDE with the plugin installed. Can run IntelliJ IDEA or CLion. The particular IDE depends onbaseIDE
property ingradle.properties
. -
./gradlew :test
-- more than five thousands tests. We love tests!
Note the :
in front of the task name. The repository contains several modules that belong to two independent
plugins for Rust and TOML, which are organized as gradle subprojects. Running
./gradlew :task
executes the task only for root module (core module of Rust plugin), ./gradlew :intellij-toml:task
will run
the task for TOML module and ./gradlew task
will do for all modules.
You can get the latest IntelliJ IDEA Community Edition here, it is free.
Import the plugin project as you would do with any other gradle based project.
For example, Ctrl + Shift + A, Import project
and select build.gradle.kts
from
the root directory of the plugin.
There are Test
, RunIDEA
, RunCLion
and Generate Parser
run configurations for the most
common tasks. However, try executing ./gradlew :test
first, to download all
necessary dependencies and launch all code generation tasks. Unfortunately
during import IDEA may delete .idea/runConfigurations
, just revert changes in
the directory if this happens.
You might want to install the following plugins:
- Grammar-Kit to get highlighting for the files with BNFish grammar.
- PsiViewer to view the AST of Rust files right in the IDE.
To find a problem to work on, look for
help wanted
issues on Github, or, even better, try to fix a problem you face yourself when
using the plugin.
When choosing an issue, you can navigate by the E-
labels. They describe an
experience needed to solve an issue (E-easy
, E-medium
, E-hard
or E-unknown
).
The E-mentor
label means that someone knows how to fix the issue and most likely
they provided some instructions about how to fix it, links to the relevant code
and so on. If you are looking for a good first issue, E-mentor
-labeled one is the
best choice.
To familiarize yourself with the plugin source code, read the architecture document and look at some existing pull requests. Please do ask questions in our Gitter!
Work in progress pull requests are very welcome! It is also a great way to ask questions.
Here are some example pull requests:
And also a tutorial series "Contributing to Intellij-Rust" by @Kobzol:
- #0: Intro & setup.
- #1: Fixing a bug in Nest Use intention.
- #2: Intention to substitute an associated type.
- #3: Quick fix to attach file to a module.
- #4: Introduce constant refactoring.
- #5: Lint attribute completion.
Please use reformat code action to maintain consistent style. Pay attention to IDEA's warning and suggestions, and try to keep the code green. If you are sure that the warning is false positive, use an annotation to suppress it.
Try to avoid copy-paste and boilerplate as much as possible. For example,
proactively use ?:
to deal with nullable values.
If you add a new file, please make sure that it contains a license preamble, as all other files do.
Consider prefixing commit with a TAG:
which describes the area of the
change. Common tags are:
Tags list
-
GRAM for changes to
.bnf
files -
PSI for other PSI related changes
-
RES for name resolution
-
TY for type inference
-
COMP for code completion
-
STUB for PSI stubs
-
MACRO for macro expansion
-
FMT for formatter
-
TYPE for editor-related functions
-
ANN for error highlighting and annotators
-
INSP for inspections
-
INT for intentions
-
REF for refactorings
-
RUN for run configurations
-
QDOC for quick documentation
-
PERF for performance optimizations
-
PRJ for project creation changes
-
ACT for actions
-
DBG for debugger
-
LI for language injections
-
REPL for REPL integration
-
CARGO for cargo-related changes
-
GRD for build changes
-
T for tests
-
DOC for documentation
-
L10N for changes related to localization
Try to keep the summary line of a commit message under 72 characters.
Rust plugin sources are divided into several modules. Almost all modules (except root and plugin
ones) support
some functionality in particular IDE or integrate with another plugin. Like debugging in CLion or
integration with TOML
plugin.
The main goal is to separate code that can be compiled only with specific dependencies (IDEA, CLion or another plugin) from each other. It helps to avoid accidental using of code from wrong optional dependency. Also, it allows us to compile and run tests of core module with different platforms like IDEA and CLion.
The current Rust plugin modules:
:
- root/core module:plugin
- module to build/run/publish Rust plugin:idea
- contains code available only in IDEA:clion
- contains code available only in CLion:debugger
- debugger related code:profiler
- profiler related code:toml
- integration with TOML plugin:intelliLang
- integration with intelliLang plugin:copyright
- integration with copyright plugin:duplicates
- supportDuplicated code fragment
inspection:coverage
- integration with coverage plugin:grazie
- integration with grazie plugin:js
- interop with JavaScript language:ml-completion
- integration with Machine Learning Code Completion plugin
If you want to implement integration with another plugin/IDE, you should create a new gradle module for that.
You can build the plugin for different major platform versions.
We usually support the latest release platform version and EAPs of the next one.
But each plugin artifact is compatible only with single major version.
platformVersion
property in gradle.properties
is used to specify major version the plugin artifact will be compatible with.
Supported values are the same as platform branch numbers.
Sometimes there are not compatible changes in new platform version. To avoid creating several parallel vcs branches for each version, we have separate folders for each version to keep platform dependent code.
For example, current platform version is 181, next version is 182
and org.rust.ide.navigation.goto.RsImplsSearch
should have separate implementations
for each version. Then project source code structure will be
+-- src
| +-- 181/kotlin
| +-- org/rust/ide/navigation/goto
| +-- RsImplsSearch.kt
| +-- 182/kotlin
| +-- org/rust/ide/navigation/goto
| +-- RsImplsSearch.kt
| +-- main/kotlin
| +-- other platfrom independent code
Of course, only one batch of platform dependent code will be used in compilation.
It is much easier to understand code changes if they are accompanied with tests. Most tests are fixture-driven. They typically:
- Load a rust file that represents the initial state
- Execute your method under test
- Verify the final state, which may also be represented as a fixture
All test classes are placed in the src/test/kotlin
directory. There are two
ways of providing fixtures for the tests. The first one is to put Rust files
in src/test/resources
.
In the example below RsFormatterTest.kt
is the test class, blocks.rs
is
the fixture for the initial state and blocks_after.rs
is the fixture for the
final state. It is good practice to put fixtures in the same package as tests.
+-- src/test/kotlin
| +-- org/rust/ide/formatter
| +-- RsFormatterTest.kt
|
+-- src/test/resources
+-- org/rust/ide/formatter
+-- fixtures
+-- blocks.rs
+-- blocks_after.rs
Another way of providing fixtures is to use Kotlin's triple quoted multiline
string literals. You can get Rust syntax highlighting inside them if you have a
@Language("Rust")
annotation applied. You can see an example
here.
In general, triple quoted string fixtures should be preferred over separate Rust files.
Fixture files are very simple: they're rust code! Output fixtures on the other
hand, can be rust code over which you've run an action, HTML (for generated
documentation) or any other output you'd like to verify. Output fixtures have
the same filename as the initial fixture, but with _after
appended.
Continuing with our example above, our initial fixture blocks.rs
could look
like:
pub fn main() {
let x = {
92
};
x;
}
While our expected-output fixture blocks_after.rs
contains:
pub fn main() {
let x = {
92
};
x;
}
Some tests are dependent on the position of the editor caret. Fixtures support a
special marker <caret>
for this purpose. Multiple such markers for more
complex tests. An example of a fixture with a caret:
pub fn main>() {
let _ = S {
<caret>
};
Test classes are JUnit and written in Kotlin. They specify the resource path in
which fixtures are found and contain a number of test methods. Test methods
follow a simple convention: their name is the initial fixture name camel-cased.
For example, RsFormatterTest.kt
would look like:
class RsFormatterTest : FormatterTestCase() {
override fun getTestDataPath() = "src/test/resources"
override fun getFileExtension() = "rs"
fun testBlocks() = stubOnlyResolve()
}
The test method testBlocks
states that this test uses blocks.rs
as the
initial fixture and blocks_after.rs
as the expected output fixture. A more
complicated fixture name like a_longer_fixture_name.rs
would use the test
method testALongerFixtureName()
.
It's much easier to review small, focused pull requests. If you can split your changes into several pull requests then please do it. There is no such thing as a "too small" pull request.
Here is my typical workflow for submitting a pull request. You don't need to follow it exactly. I will show command line commands, but you can use any git client of your choice.
First, I press the fork button on the GitHub website to fork
https://github.com/intellij-rust/intellij-rust
to
https://github.com/matklad/intellij-rust
. Then I clone my fork:
$ git clone git://github.com/matklad/intellij-rust && cd intellij-rust
The next thing is usually creating a branch:
$ git checkout -b "useful-fix"
I can work directly on my fork's master branch, but having a dedicated PR branch helps if I want to synchronize my work with upstream repository or if I want to submit several pull requests.
Usually I try to keep my PRs one commit long:
$ hack hack hack
$ git commit -am"(INSP): add a useful inspection"
$ ./gradlew test && git push -u origin useful-fix
Now I am ready to press "create pull request" button on the GitHub website!
If my pull request consists of a single commit then to address the review I just push additional commits to the pull request branch:
$ more hacking
$ git commit -am "Fix code style issues"
$ ./gradlew test && git push
I don't pay much attention to the commit messages, because after everything is fine the PR will be squash merged as a single good commit.
If my PR consists of several commits, then the situation is a bit tricky. I like to keep the history clean, so I do some form of the rebasing:
$ more hacking
$ git add .
$ git commit --fixup aef92cc
$ git rebase --autosquash -i HEAD~3
And then I force push the branch
$ ./gradlew test && git push --force-with-lease
If my PR starts to conflict with the upstream changes, I need to update it. First, I add the original repository as a remote, so that I can pull changes from it.
$ git remote add upstream https://github.com/intellij-rust/intellij-rust
$ git fetch upstream
$ git merge upstream/master master # The dedicated PR branch helps a lot here.
Then I rebase my work on top of the updated master:
$ git rebase master useful-fix
And now I need to force push the PR branch:
$ ./gradlew test && git push --force-with-lease