Skip to content

Add lsp command to fix rust-analyzer #1026

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(())
}
}