Add lsp command to fix rust-analyzer

This commit is contained in:
Jack Clayton 2022-06-16 11:53:41 +08:00
parent b19f74e8cf
commit be87cc9fa6
6 changed files with 141 additions and 22 deletions

1
.gitignore vendored
View file

@ -5,6 +5,7 @@ target/
*.pdb *.pdb
exercises/clippy/Cargo.toml exercises/clippy/Cargo.toml
exercises/clippy/Cargo.lock exercises/clippy/Cargo.lock
rust-project.json
.idea .idea
.vscode .vscode
*.iml *.iml

19
Cargo.lock generated
View file

@ -186,6 +186,15 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "home"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654"
dependencies = [
"winapi 0.3.9",
]
[[package]] [[package]]
name = "indicatif" name = "indicatif"
version = "0.16.2" version = "0.16.2"
@ -229,9 +238,9 @@ dependencies = [
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "0.4.8" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
[[package]] [[package]]
name = "kernel32-sys" name = "kernel32-sys"
@ -456,11 +465,13 @@ dependencies = [
"assert_cmd", "assert_cmd",
"console", "console",
"glob", "glob",
"home",
"indicatif", "indicatif",
"notify", "notify",
"predicates", "predicates",
"regex", "regex",
"serde", "serde",
"serde_json",
"toml", "toml",
] ]
@ -501,9 +512,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.66" version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",

View file

@ -12,6 +12,9 @@ notify = "4.0"
toml = "0.5" toml = "0.5"
regex = "1.5" regex = "1.5"
serde= { version = "1.0", features = ["derive"] } serde= { version = "1.0", features = ["derive"] }
serde_json = "1.0.81"
home = "0.5.3"
glob = "0.3.0"
[[bin]] [[bin]]
name = "rustlings" name = "rustlings"

View file

@ -126,24 +126,7 @@ After every couple of sections, there will be a quiz that'll test your knowledge
## Enabling `rust-analyzer` ## Enabling `rust-analyzer`
`rust-analyzer` support is provided, but it depends on your editor 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.
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!_
## Continuing On ## Continuing On

View file

@ -1,4 +1,5 @@
use crate::exercise::{Exercise, ExerciseList}; use crate::exercise::{Exercise, ExerciseList};
use crate::project::RustAnalyzerProject;
use crate::run::run; use crate::run::run;
use crate::verify::verify; use crate::verify::verify;
use argh::FromArgs; use argh::FromArgs;
@ -20,6 +21,7 @@ use std::time::Duration;
mod ui; mod ui;
mod exercise; mod exercise;
mod project;
mod run; mod run;
mod verify; mod verify;
@ -47,6 +49,7 @@ enum Subcommands {
Run(RunArgs), Run(RunArgs),
Hint(HintArgs), Hint(HintArgs),
List(ListArgs), List(ListArgs),
Lsp(LspArgs),
} }
#[derive(FromArgs, PartialEq, Debug)] #[derive(FromArgs, PartialEq, Debug)]
@ -77,6 +80,12 @@ struct HintArgs {
name: String, name: String,
} }
#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "lsp")]
/// Enable rust-analyzer for exercises
struct LspArgs {}
#[derive(FromArgs, PartialEq, Debug)] #[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "list")] #[argh(subcommand, name = "list")]
/// Lists the exercises available in Rustlings /// Lists the exercises available in Rustlings
@ -206,6 +215,25 @@ fn main() {
verify(&exercises, (0, exercises.len()), verbose).unwrap_or_else(|_| std::process::exit(1)); 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) { Subcommands::Watch(_subargs) => match watch(&exercises, verbose) {
Err(e) => { Err(e) => {
println!("Error: Could not watch your progress. Error message was {:?}.", e); println!("Error: Could not watch your progress. Error message was {:?}.", e);
@ -224,6 +252,7 @@ fn main() {
} }
} }
fn spawn_watch_shell(failed_exercise_hint: &Arc<Mutex<Option<String>>>, should_quit: Arc<AtomicBool>) { fn spawn_watch_shell(failed_exercise_hint: &Arc<Mutex<Option<String>>>, should_quit: Arc<AtomicBool>) {
let failed_exercise_hint = Arc::clone(failed_exercise_hint); 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."); 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:
4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub! 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, (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! 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 Got all that? Great! To get started, run `rustlings watch` in order to get the first
exercise. Make sure to have your editor open!"#; exercise. Make sure to have your editor open!"#;

90
src/project.rs Normal file
View file

@ -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(())
}
}