Skip to content

Commit

Permalink
Add lsp command to fix rust-analyzer
Browse files Browse the repository at this point in the history
  • Loading branch information
jackos committed Jun 17, 2022
1 parent b19f74e commit be87cc9
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 22 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ target/
*.pdb
exercises/clippy/Cargo.toml
exercises/clippy/Cargo.lock
rust-project.json
.idea
.vscode
*.iml
19 changes: 15 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ notify = "4.0"
toml = "0.5"
regex = "1.5"
serde= { version = "1.0", features = ["derive"] }
serde_json = "1.0.81"
home = "0.5.3"
glob = "0.3.0"

[[bin]]
name = "rustlings"
Expand Down
19 changes: 1 addition & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,24 +126,7 @@ After every couple of sections, there will be a quiz that'll test your knowledge

## Enabling `rust-analyzer`

`rust-analyzer` support is provided, but it depends on your editor
whether it's enabled by default. (RLS support is not provided)

To enable `rust-analyzer`, you'll need to make Cargo build the project
with the `exercises` feature, which will automatically include the `exercises/`
subfolder in the project. The easiest way to do this is to tell your editor to
build the project with all features (the equivalent of `cargo build --all-features`).
For specific editor instructions:

- **VSCode**: Add a `.vscode/settings.json` file with the following:
```json
{
"rust-analyzer.cargo.features": ["exercises"]
}
```
- **IntelliJ-based Editors**: Using the Rust plugin, everything should work
by default.
- _Missing your editor? Feel free to contribute more instructions!_
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.

## Continuing On

Expand Down
31 changes: 31 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::exercise::{Exercise, ExerciseList};
use crate::project::RustAnalyzerProject;
use crate::run::run;
use crate::verify::verify;
use argh::FromArgs;
Expand All @@ -20,6 +21,7 @@ use std::time::Duration;
mod ui;

mod exercise;
mod project;
mod run;
mod verify;

Expand Down Expand Up @@ -47,6 +49,7 @@ enum Subcommands {
Run(RunArgs),
Hint(HintArgs),
List(ListArgs),
Lsp(LspArgs),
}

#[derive(FromArgs, PartialEq, Debug)]
Expand Down Expand Up @@ -77,6 +80,12 @@ struct HintArgs {
name: String,
}

#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "lsp")]
/// Enable rust-analyzer for exercises
struct LspArgs {}


#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "list")]
/// Lists the exercises available in Rustlings
Expand Down Expand Up @@ -206,6 +215,25 @@ fn main() {
verify(&exercises, (0, exercises.len()), verbose).unwrap_or_else(|_| std::process::exit(1));
}

Subcommands::Lsp(_subargs) => {
let mut project = RustAnalyzerProject::new();
project
.get_sysroot_src()
.expect("Couldn't find toolchain path, do you have `rustc` installed?");
project
.exercies_to_json()
.expect("Couldn't parse rustlings exercises files");

if project.crates.is_empty() {
println!("Failed find any exercises, make sure you're in the `rustlings` folder");
} else if project.write_to_disk().is_err() {
println!("Failed to write rust-project.json to disk for rust-analyzer");
} else {
println!("Successfully generated rust-project.json");
println!("rust-analyzer will now parse exercises, restart your language server or editor")
}
}

Subcommands::Watch(_subargs) => match watch(&exercises, verbose) {
Err(e) => {
println!("Error: Could not watch your progress. Error message was {:?}.", e);
Expand All @@ -224,6 +252,7 @@ fn main() {
}
}


fn spawn_watch_shell(failed_exercise_hint: &Arc<Mutex<Option<String>>>, should_quit: Arc<AtomicBool>) {
let failed_exercise_hint = Arc::clone(failed_exercise_hint);
println!("Welcome to watch mode! You can type 'help' to get an overview of the commands you can use here.");
Expand Down Expand Up @@ -367,6 +396,8 @@ started, here's a couple of notes about how Rustlings operates:
4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub!
(https://github.com/rust-lang/rustlings/issues/new). We look at every issue,
and sometimes, other learners do too so you can help each other out!
5. If you want to use `rust-analyzer` with exercises, which provides features like
autocompletion, run the command `rustlings lsp`.
Got all that? Great! To get started, run `rustlings watch` in order to get the first
exercise. Make sure to have your editor open!"#;
Expand Down
90 changes: 90 additions & 0 deletions src/project.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use glob::glob;
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::process::Command;

/// Contains the structure of resulting rust-project.json file
/// and functions to build the data required to create the file
#[derive(Serialize, Deserialize)]
pub struct RustAnalyzerProject {
sysroot_src: String,
pub crates: Vec<Crate>,
}

#[derive(Serialize, Deserialize)]
pub struct Crate {
root_module: String,
edition: String,
deps: Vec<String>,
cfg: Vec<String>,
}

impl RustAnalyzerProject {
pub fn new() -> RustAnalyzerProject {
RustAnalyzerProject {
sysroot_src: String::new(),
crates: Vec::new(),
}
}

/// Write rust-project.json to disk
pub fn write_to_disk(&self) -> Result<(), std::io::Error> {
std::fs::write(
"./rust-project.json",
serde_json::to_vec(&self).expect("Failed to serialize to JSON"),
)?;
Ok(())
}

/// If path contains .rs extension, add a crate to `rust-project.json`
fn path_to_json(&mut self, path: String) {
if let Some((_, ext)) = path.split_once('.') {
if ext == "rs" {
self.crates.push(Crate {
root_module: path,
edition: "2021".to_string(),
deps: Vec::new(),
// This allows rust_analyzer to work inside #[test] blocks
cfg: vec!["test".to_string()],
})
}
}
}

/// Parse the exercises folder for .rs files, any matches will create
/// a new `crate` in rust-project.json which allows rust-analyzer to
/// treat it like a normal binary
pub fn exercies_to_json(&mut self) -> Result<(), Box<dyn Error>> {
for e in glob("./exercises/**/*")? {
let path = e?.to_string_lossy().to_string();
self.path_to_json(path);
}
Ok(())
}

/// Use `rustc` to determine the default toolchain
pub fn get_sysroot_src(&mut self) -> Result<(), Box<dyn Error>> {
let toolchain = Command::new("rustc")
.arg("--print")
.arg("sysroot")
.output()?
.stdout;

let toolchain = String::from_utf8_lossy(&toolchain);
let mut whitespace_iter = toolchain.split_whitespace();

let toolchain = whitespace_iter.next().unwrap_or(&toolchain);

println!("Determined toolchain: {}\n", &toolchain);

self.sysroot_src = (std::path::Path::new(&*toolchain)
.join("lib")
.join("rustlib")
.join("src")
.join("rust")
.join("library")
.to_string_lossy())
.to_string();
Ok(())
}
}

0 comments on commit be87cc9

Please sign in to comment.