Skip to content
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,22 @@ pub fn three_sum(nums: Vec<i32>) -> Vec<Vec<i32>> {

</details>

## Language-Specific Configuration

### Rust
For `lang = 'rust'`, leetcode-cli generates per-problem crate structures to enable full LSP support (e.g., rust-analyzer in editors like Helix or VS Code).

- **Structure**: `code/{fid}-{slug}/src/lib.rs` (code), `tests.dat` (test cases), and `Cargo.toml` (basic crate config with commented dependencies for common crates like `itertools` or `regex`).
- **Example**: For problem 1 ("Two Sum"), creates `code/1-two_sum/` with `prob-1-two_sum` as package name.
- **Config Option**: Set `enable_rust_crates = false` in `[code]` to fall back to flat files (e.g., `1.two-sum.rs`).
- **Usage**: Run `leetcode edit 1`, then open the dir: `hx code/1-two_sum/` for LSP features (autocomplete, diagnostics, etc.).
- **Migration**: If flat files exist, the tool notes them—manually move content to `lib.rs` if needed.
- **Local Testing**: Edit `Cargo.toml` to add deps, then `cargo check` or `cargo test` (tests.dat can be adapted for unit tests).

This keeps submissions unchanged (sends code snippet to LeetCode API) while improving local editing.

For other languages, files remain flat. Please contribute if needed!

<br>

Some linting tools/lsps will throw errors unless the necessary libraries are imported. leetcode-cli can generate this boilerplate automatically if the `inject_before` key is set. Similarly, if you want to test out your code locally, you can automate that with `inject_after`. For c++ this might look something like:
Expand Down
53 changes: 48 additions & 5 deletions src/cmds/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ impl Command for EditCommand {
/// `edit` handler
async fn handler(m: &ArgMatches) -> Result<()> {
use crate::{cache::models::Question, Cache};
use std::fs::File;
use std::io::Write;
use std::path::Path;
use crate::helper::suffix;
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;

let cache = Cache::new()?;

Expand Down Expand Up @@ -105,9 +106,51 @@ impl Command for EditCommand {
qr = Ok(cache.get_question(id).await?);
}

let question: Question = qr?;
let question: Question = qr?;

let mut file_code = File::create(&path)?;
if *lang == "rust" && conf.code.enable_rust_crates {
let flat_suffix = suffix(&lang).map_err(anyhow::Error::msg)?; // Since suffix returns Result<&str>
let pick_replaced = conf.code.pick.replace("${fid}", &problem.fid.to_string()).replace("${slug}", &problem.slug.to_string());
let flat_path_str = format!("{}/{}.{}", conf.storage.code()?, pick_replaced, flat_suffix);
if Path::new(&flat_path_str).exists() {
println!("Note: Existing flat file at {}. Consider migrating content to new subdir structure.", flat_path_str);
}

let sanitized_slug = problem.slug.to_lowercase().replace(|c: char| !c.is_alphanumeric(), "_");
let code_dir_str = format!("{}/{}-{}", conf.storage.code()?, problem.fid, sanitized_slug);
let code_dir = Path::new(&code_dir_str);
fs::create_dir_all(code_dir)?;

let src_dir_str = format!("{}/src", code_dir_str);
let src_dir = Path::new(&src_dir_str);
fs::create_dir_all(src_dir)?;

let cargo_path_str = format!("{}/Cargo.toml", code_dir_str);
let cargo_path = Path::new(&cargo_path_str);
if !cargo_path.exists() {
let package_name = format!("prob-{}-{}", problem.fid, sanitized_slug);
let cargo_content = format!(
r#"[package]
name = "{}"
version = "0.1.0"
edition = "2021"

[lib]
path = "src/lib.rs"

[dependencies]
# Uncomment and add crates as needed for LeetCode problems, e.g.:
# itertools = "0.12"
# regex = "1"
"#,
package_name
);
let mut cargo_file = File::create(&cargo_path_str)?;
cargo_file.write_all(cargo_content.as_bytes())?;
}
}

let mut file_code = File::create(&path)?;
let question_desc = question.desc_comment(&conf) + "\n";

let test_path = crate::helper::test_cases_path(&problem)?;
Expand Down
7 changes: 7 additions & 0 deletions src/config/code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ fn default_submission() -> String {
"${fid}.${slug}.${sid}.${ac}".into()
}

fn default_enable_rust_crates() -> bool {
true
}

/// Code config
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Code {
Expand All @@ -34,6 +38,8 @@ pub struct Code {
pub comment_leading: String,
#[serde(default, skip_serializing)]
pub test: bool,
#[serde(default = "default_enable_rust_crates")]
pub enable_rust_crates: bool,
pub lang: String,
#[serde(default = "default_pick", skip_serializing)]
pub pick: String,
Expand All @@ -55,6 +61,7 @@ impl Default for Code {
comment_problem_desc: false,
comment_leading: "".into(),
test: true,
enable_rust_crates: default_enable_rust_crates(),
lang: "rust".into(),
pick: "${fid}.${slug}".into(),
submission: "${fid}.${slug}.${sid}.${ac}".into(),
Expand Down
54 changes: 37 additions & 17 deletions src/helper.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! A set of helper traits
pub use self::{
digit::Digit,
file::{code_path, load_script, test_cases_path},
file::{code_path, load_script, suffix, test_cases_path},
filter::{filter, squash},
html::HTML,
};
Expand Down Expand Up @@ -191,38 +191,58 @@ mod file {
}
}

use crate::{cache::models::Problem, Error};
use crate::cache::models::Problem;

/// Generate test cases path by fid
pub fn test_cases_path(problem: &Problem) -> crate::Result<String> {
let conf = crate::config::Config::locate()?;
let mut path = format!("{}/{}.tests.dat", conf.storage.code()?, conf.code.pick);
pub fn test_cases_path(problem: &Problem) -> crate::Result<String> {
let conf = crate::config::Config::locate()?;
let lang = conf.code.lang.clone();
let use_crates = conf.code.enable_rust_crates;
let code_base = conf.storage.code()?;

let path = if lang == "rust" && use_crates {
let sanitized_slug = problem.slug.to_lowercase().replace(|c: char| !c.is_alphanumeric(), "_");
let subdir = format!("{}-{}/tests.dat", problem.fid, sanitized_slug);
format!("{}/{}", code_base, subdir)
} else {
let mut path = format!("{}/{}.tests.dat", code_base, conf.code.pick);
path = path.replace("${fid}", &problem.fid.to_string());
path = path.replace("${slug}", &problem.slug.to_string());
Ok(path)
}
path
};

Ok(path)
}

/// Generate code path by fid
pub fn code_path(problem: &Problem, l: Option<String>) -> crate::Result<String> {
let conf = crate::config::Config::locate()?;
let mut lang = conf.code.lang;
if l.is_some() {
lang = l.ok_or(Error::NoneError)?;
}
pub fn code_path(problem: &Problem, l: Option<String>) -> crate::Result<String> {
let conf = crate::config::Config::locate()?;
let mut lang = conf.code.lang.clone();
if let Some(lang_opt) = l {
lang = lang_opt;
}

let use_crates = conf.code.enable_rust_crates;
let code_base = conf.storage.code()?;

let path = if lang == "rust" && use_crates {
let sanitized_slug = problem.slug.to_lowercase().replace(|c: char| !c.is_alphanumeric(), "_");
let subdir = format!("{}-{}/src/lib.rs", problem.fid, sanitized_slug);
format!("{}/{}", code_base, subdir)
} else {
let mut path = format!(
"{}/{}.{}",
conf.storage.code()?,
code_base,
conf.code.pick,
suffix(&lang)?,
);

path = path.replace("${fid}", &problem.fid.to_string());
path = path.replace("${slug}", &problem.slug.to_string());
path
};

Ok(path)
}
Ok(path)
}

/// Load python scripts
pub fn load_script(module: &str) -> crate::Result<String> {
Expand Down
Loading