Skip to content

Commit be87cc9

Browse files
committed
Add lsp command to fix rust-analyzer
1 parent b19f74e commit be87cc9

File tree

6 files changed

+141
-22
lines changed

6 files changed

+141
-22
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ target/
55
*.pdb
66
exercises/clippy/Cargo.toml
77
exercises/clippy/Cargo.lock
8+
rust-project.json
89
.idea
910
.vscode
1011
*.iml

Cargo.lock

Lines changed: 15 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ notify = "4.0"
1212
toml = "0.5"
1313
regex = "1.5"
1414
serde= { version = "1.0", features = ["derive"] }
15+
serde_json = "1.0.81"
16+
home = "0.5.3"
17+
glob = "0.3.0"
1518

1619
[[bin]]
1720
name = "rustlings"

README.md

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -126,24 +126,7 @@ After every couple of sections, there will be a quiz that'll test your knowledge
126126

127127
## Enabling `rust-analyzer`
128128

129-
`rust-analyzer` support is provided, but it depends on your editor
130-
whether it's enabled by default. (RLS support is not provided)
131-
132-
To enable `rust-analyzer`, you'll need to make Cargo build the project
133-
with the `exercises` feature, which will automatically include the `exercises/`
134-
subfolder in the project. The easiest way to do this is to tell your editor to
135-
build the project with all features (the equivalent of `cargo build --all-features`).
136-
For specific editor instructions:
137-
138-
- **VSCode**: Add a `.vscode/settings.json` file with the following:
139-
```json
140-
{
141-
"rust-analyzer.cargo.features": ["exercises"]
142-
}
143-
```
144-
- **IntelliJ-based Editors**: Using the Rust plugin, everything should work
145-
by default.
146-
- _Missing your editor? Feel free to contribute more instructions!_
129+
Run the command `rustlings lsp` which will generate a `rust-project.json` at the root of the project, this allows [rust-analyzer](https://rust-analyzer.github.io/) to parse each exercise.
147130

148131
## Continuing On
149132

src/main.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::exercise::{Exercise, ExerciseList};
2+
use crate::project::RustAnalyzerProject;
23
use crate::run::run;
34
use crate::verify::verify;
45
use argh::FromArgs;
@@ -20,6 +21,7 @@ use std::time::Duration;
2021
mod ui;
2122

2223
mod exercise;
24+
mod project;
2325
mod run;
2426
mod verify;
2527

@@ -47,6 +49,7 @@ enum Subcommands {
4749
Run(RunArgs),
4850
Hint(HintArgs),
4951
List(ListArgs),
52+
Lsp(LspArgs),
5053
}
5154

5255
#[derive(FromArgs, PartialEq, Debug)]
@@ -77,6 +80,12 @@ struct HintArgs {
7780
name: String,
7881
}
7982

83+
#[derive(FromArgs, PartialEq, Debug)]
84+
#[argh(subcommand, name = "lsp")]
85+
/// Enable rust-analyzer for exercises
86+
struct LspArgs {}
87+
88+
8089
#[derive(FromArgs, PartialEq, Debug)]
8190
#[argh(subcommand, name = "list")]
8291
/// Lists the exercises available in Rustlings
@@ -206,6 +215,25 @@ fn main() {
206215
verify(&exercises, (0, exercises.len()), verbose).unwrap_or_else(|_| std::process::exit(1));
207216
}
208217

218+
Subcommands::Lsp(_subargs) => {
219+
let mut project = RustAnalyzerProject::new();
220+
project
221+
.get_sysroot_src()
222+
.expect("Couldn't find toolchain path, do you have `rustc` installed?");
223+
project
224+
.exercies_to_json()
225+
.expect("Couldn't parse rustlings exercises files");
226+
227+
if project.crates.is_empty() {
228+
println!("Failed find any exercises, make sure you're in the `rustlings` folder");
229+
} else if project.write_to_disk().is_err() {
230+
println!("Failed to write rust-project.json to disk for rust-analyzer");
231+
} else {
232+
println!("Successfully generated rust-project.json");
233+
println!("rust-analyzer will now parse exercises, restart your language server or editor")
234+
}
235+
}
236+
209237
Subcommands::Watch(_subargs) => match watch(&exercises, verbose) {
210238
Err(e) => {
211239
println!("Error: Could not watch your progress. Error message was {:?}.", e);
@@ -224,6 +252,7 @@ fn main() {
224252
}
225253
}
226254

255+
227256
fn spawn_watch_shell(failed_exercise_hint: &Arc<Mutex<Option<String>>>, should_quit: Arc<AtomicBool>) {
228257
let failed_exercise_hint = Arc::clone(failed_exercise_hint);
229258
println!("Welcome to watch mode! You can type 'help' to get an overview of the commands you can use here.");
@@ -367,6 +396,8 @@ started, here's a couple of notes about how Rustlings operates:
367396
4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub!
368397
(https://github.com/rust-lang/rustlings/issues/new). We look at every issue,
369398
and sometimes, other learners do too so you can help each other out!
399+
5. If you want to use `rust-analyzer` with exercises, which provides features like
400+
autocompletion, run the command `rustlings lsp`.
370401
371402
Got all that? Great! To get started, run `rustlings watch` in order to get the first
372403
exercise. Make sure to have your editor open!"#;

src/project.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use glob::glob;
2+
use serde::{Deserialize, Serialize};
3+
use std::error::Error;
4+
use std::process::Command;
5+
6+
/// Contains the structure of resulting rust-project.json file
7+
/// and functions to build the data required to create the file
8+
#[derive(Serialize, Deserialize)]
9+
pub struct RustAnalyzerProject {
10+
sysroot_src: String,
11+
pub crates: Vec<Crate>,
12+
}
13+
14+
#[derive(Serialize, Deserialize)]
15+
pub struct Crate {
16+
root_module: String,
17+
edition: String,
18+
deps: Vec<String>,
19+
cfg: Vec<String>,
20+
}
21+
22+
impl RustAnalyzerProject {
23+
pub fn new() -> RustAnalyzerProject {
24+
RustAnalyzerProject {
25+
sysroot_src: String::new(),
26+
crates: Vec::new(),
27+
}
28+
}
29+
30+
/// Write rust-project.json to disk
31+
pub fn write_to_disk(&self) -> Result<(), std::io::Error> {
32+
std::fs::write(
33+
"./rust-project.json",
34+
serde_json::to_vec(&self).expect("Failed to serialize to JSON"),
35+
)?;
36+
Ok(())
37+
}
38+
39+
/// If path contains .rs extension, add a crate to `rust-project.json`
40+
fn path_to_json(&mut self, path: String) {
41+
if let Some((_, ext)) = path.split_once('.') {
42+
if ext == "rs" {
43+
self.crates.push(Crate {
44+
root_module: path,
45+
edition: "2021".to_string(),
46+
deps: Vec::new(),
47+
// This allows rust_analyzer to work inside #[test] blocks
48+
cfg: vec!["test".to_string()],
49+
})
50+
}
51+
}
52+
}
53+
54+
/// Parse the exercises folder for .rs files, any matches will create
55+
/// a new `crate` in rust-project.json which allows rust-analyzer to
56+
/// treat it like a normal binary
57+
pub fn exercies_to_json(&mut self) -> Result<(), Box<dyn Error>> {
58+
for e in glob("./exercises/**/*")? {
59+
let path = e?.to_string_lossy().to_string();
60+
self.path_to_json(path);
61+
}
62+
Ok(())
63+
}
64+
65+
/// Use `rustc` to determine the default toolchain
66+
pub fn get_sysroot_src(&mut self) -> Result<(), Box<dyn Error>> {
67+
let toolchain = Command::new("rustc")
68+
.arg("--print")
69+
.arg("sysroot")
70+
.output()?
71+
.stdout;
72+
73+
let toolchain = String::from_utf8_lossy(&toolchain);
74+
let mut whitespace_iter = toolchain.split_whitespace();
75+
76+
let toolchain = whitespace_iter.next().unwrap_or(&toolchain);
77+
78+
println!("Determined toolchain: {}\n", &toolchain);
79+
80+
self.sysroot_src = (std::path::Path::new(&*toolchain)
81+
.join("lib")
82+
.join("rustlib")
83+
.join("src")
84+
.join("rust")
85+
.join("library")
86+
.to_string_lossy())
87+
.to_string();
88+
Ok(())
89+
}
90+
}

0 commit comments

Comments
 (0)