From e6e4e41489c8ceffe49f57abcae8b295ede3d8bc Mon Sep 17 00:00:00 2001 From: Antonio Bennett Date: Tue, 23 Sep 2025 14:03:17 -0400 Subject: [PATCH 01/10] =?UTF-8?q?Add=20per-problem=20Cargo.toml=20and=20su?= =?UTF-8?q?bdir=20structure=20for=20Rust=20LSP=20support=20=20=E2=80=A2=20?= =?UTF-8?q?For=20lang=3D'rust',=20generate=20code=20at=20code/{fid}-{slug}?= =?UTF-8?q?/src/lib.rs=20and=20tests.dat.=20=20=E2=80=A2=20Create=20Cargo.?= =?UTF-8?q?toml=20with=20basic=20config=20on=20first=20edit.=20=20?= =?UTF-8?q?=E2=80=A2=20Preserves=20flat=20structure=20for=20other=20langua?= =?UTF-8?q?ges.=20=20=E2=80=A2=20Enables=20rust-analyzer=20integration=20w?= =?UTF-8?q?ithout=20breaking=20API=20submissions.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cmds/edit.rs | 42 +++++++++++++++++++++++++++++++++++++----- src/helper.rs | 48 +++++++++++++++++++++++++++++++++--------------- 2 files changed, 70 insertions(+), 20 deletions(-) diff --git a/src/cmds/edit.rs b/src/cmds/edit.rs index f611d6c..4045601 100644 --- a/src/cmds/edit.rs +++ b/src/cmds/edit.rs @@ -62,9 +62,9 @@ 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 std::fs::{self, File}; +use std::io::Write; +use std::path::Path; let cache = Cache::new()?; @@ -105,9 +105,41 @@ 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" { + let sanitized_slug = problem.slug.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!("leetcode-{}-{}", problem.fid, sanitized_slug); + let cargo_content = format!( +r#"[package] +name = "{}" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/lib.rs" + +[dependencies] +"#, + 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)?; diff --git a/src/helper.rs b/src/helper.rs index 321828c..91edf37 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -194,35 +194,53 @@ mod file { use crate::{cache::models::Problem, Error}; /// Generate test cases path by fid - pub fn test_cases_path(problem: &Problem) -> crate::Result { - 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 { + let conf = crate::config::Config::locate()?; + let lang = conf.code.lang.clone(); + let code_base = conf.storage.code()?; + let path = if lang == "rust" { + let sanitized_slug = problem.slug.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) -> crate::Result { - 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) -> crate::Result { + let conf = crate::config::Config::locate()?; + let mut lang = conf.code.lang.clone(); + if let Some(lang_opt) = l { + lang = lang_opt; + } + + let code_base = conf.storage.code()?; + let path = if lang == "rust" { + let sanitized_slug = problem.slug.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 { From c6a904a0668a07bac09535ba1d182d46d79ac509 Mon Sep 17 00:00:00 2001 From: Antonio Bennett Date: Tue, 23 Sep 2025 14:04:58 -0400 Subject: [PATCH 02/10] Fix: Remove unused Error import in helper.rs - Cleans up compilation warning from file module. --- src/helper.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helper.rs b/src/helper.rs index 91edf37..c7d80c3 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -191,7 +191,7 @@ 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 { From 61b8b5722a0880d5d8ec2138182d6e57febf3921 Mon Sep 17 00:00:00 2001 From: Antonio Bennett Date: Tue, 23 Sep 2025 15:41:22 -0400 Subject: [PATCH 03/10] Enhance: Improve slug sanitization for valid Cargo package names - Lowercase slugs and prefix with 'prob-' (e.g., 'prob-1_two_sum'). - Prevents Cargo.toml errors; applied to paths and package names. - Ensures cross-file consistency. --- src/cmds/edit.rs | 4 ++-- src/helper.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cmds/edit.rs b/src/cmds/edit.rs index 4045601..e2d4a0e 100644 --- a/src/cmds/edit.rs +++ b/src/cmds/edit.rs @@ -108,7 +108,7 @@ use std::path::Path; let question: Question = qr?; if *lang == "rust" { - let sanitized_slug = problem.slug.replace(|c: char| !c.is_alphanumeric(), "_"); + 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)?; @@ -120,7 +120,7 @@ if *lang == "rust" { 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!("leetcode-{}-{}", problem.fid, sanitized_slug); + let package_name = format!("prob-{}-{}", problem.fid, sanitized_slug); let cargo_content = format!( r#"[package] name = "{}" diff --git a/src/helper.rs b/src/helper.rs index c7d80c3..f545ff2 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -200,7 +200,7 @@ pub fn test_cases_path(problem: &Problem) -> crate::Result { let code_base = conf.storage.code()?; let path = if lang == "rust" { - let sanitized_slug = problem.slug.replace(|c: char| !c.is_alphanumeric(), "_"); + 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 { @@ -224,7 +224,7 @@ pub fn code_path(problem: &Problem, l: Option) -> crate::Result let code_base = conf.storage.code()?; let path = if lang == "rust" { - let sanitized_slug = problem.slug.replace(|c: char| !c.is_alphanumeric(), "_"); + 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 { From aa7401d30ce9282bcc4990d87806a0e1747f6090 Mon Sep 17 00:00:00 2001 From: Antonio Bennett Date: Tue, 23 Sep 2025 15:41:29 -0400 Subject: [PATCH 04/10] Enhance: Add commented dependencies section to generated Cargo.toml - Includes examples for common LeetCode crates (itertools, regex). - Allows easy extension without editing boilerplate. --- src/cmds/edit.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/cmds/edit.rs b/src/cmds/edit.rs index e2d4a0e..f1d2e11 100644 --- a/src/cmds/edit.rs +++ b/src/cmds/edit.rs @@ -121,7 +121,7 @@ if *lang == "rust" { 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!( +let cargo_content = format!( r#"[package] name = "{}" version = "0.1.0" @@ -131,9 +131,12 @@ edition = "2021" path = "src/lib.rs" [dependencies] +# Uncomment and add crates as needed for LeetCode problems, e.g.: +# itertools = "0.12" +# regex = "1" "#, - package_name - ); + package_name +); let mut cargo_file = File::create(&cargo_path_str)?; cargo_file.write_all(cargo_content.as_bytes())?; } From 94250d6e203919b849519261193e3668306969fe Mon Sep 17 00:00:00 2001 From: Antonio Bennett Date: Tue, 23 Sep 2025 15:41:38 -0400 Subject: [PATCH 05/10] Improve: Add note for existing flat files during Rust edit - Detects old flat paths and suggests migration without auto-moving. - Helps users with pre-existing problems. --- src/cmds/edit.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/cmds/edit.rs b/src/cmds/edit.rs index f1d2e11..bb51da2 100644 --- a/src/cmds/edit.rs +++ b/src/cmds/edit.rs @@ -62,6 +62,7 @@ impl Command for EditCommand { /// `edit` handler async fn handler(m: &ArgMatches) -> Result<()> { use crate::{cache::models::Question, Cache}; +use crate::helper::suffix; use std::fs::{self, File}; use std::io::Write; use std::path::Path; @@ -108,6 +109,13 @@ use std::path::Path; let question: Question = qr?; if *lang == "rust" { + 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); From e9e1f36f8387c9a45e4113d0f30fda1ecad293fd Mon Sep 17 00:00:00 2001 From: Antonio Bennett Date: Tue, 23 Sep 2025 15:41:44 -0400 Subject: [PATCH 06/10] Fix: Export suffix from helper for use in edit.rs - Allows importing suffix in cmds/edit.rs for flat path calculation. --- src/helper.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helper.rs b/src/helper.rs index f545ff2..ad339e9 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -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, }; From 96981dedda51bb5e226cb13d61e9aa2a1db67fe8 Mon Sep 17 00:00:00 2001 From: Antonio Bennett Date: Tue, 23 Sep 2025 15:42:01 -0400 Subject: [PATCH 07/10] Feature: Make Rust crate generation configurable - Add [code] enable_rust_crates = true (default) in config. - Allows opting out of subdir structure for Rust. - Updates paths and creation logic accordingly. - Fix serde attribute for the new field. --- src/cmds/edit.rs | 2 +- src/config/code.rs | 6 ++++++ src/helper.rs | 6 ++++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/cmds/edit.rs b/src/cmds/edit.rs index bb51da2..15ee1d2 100644 --- a/src/cmds/edit.rs +++ b/src/cmds/edit.rs @@ -108,7 +108,7 @@ use std::path::Path; let question: Question = qr?; -if *lang == "rust" { +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); diff --git a/src/config/code.rs b/src/config/code.rs index b842657..06d7399 100644 --- a/src/config/code.rs +++ b/src/config/code.rs @@ -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 { @@ -34,6 +38,7 @@ pub struct Code { pub comment_leading: String, #[serde(default, skip_serializing)] pub test: bool, + pub enable_rust_crates: bool, pub lang: String, #[serde(default = "default_pick", skip_serializing)] pub pick: String, @@ -55,6 +60,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(), diff --git a/src/helper.rs b/src/helper.rs index ad339e9..e3063e2 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -197,9 +197,10 @@ mod file { pub fn test_cases_path(problem: &Problem) -> crate::Result { 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" { + 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) @@ -221,9 +222,10 @@ pub fn code_path(problem: &Problem, l: Option) -> crate::Result lang = lang_opt; } + let use_crates = conf.code.enable_rust_crates; let code_base = conf.storage.code()?; - let path = if lang == "rust" { + 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) From b3c1d6a3c6b4ae542989a93022e1fa65c9eef61b Mon Sep 17 00:00:00 2001 From: Antonio Bennett Date: Tue, 23 Sep 2025 15:45:33 -0400 Subject: [PATCH 08/10] Docs: Add language-specific configuration section to README - Focus on Rust LSP support with subdir/Cargo.toml details, config option, and usage. - Positions Rust first; extensible for other languages. --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 315587a..34b9451 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,22 @@ pub fn three_sum(nums: Vec) -> Vec> { +## 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. Future support for similar setups (e.g., Python virtualenvs) can be added via config. +
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: From 2ed647f51a746fa2670d9f563913bfab9cd04e11 Mon Sep 17 00:00:00 2001 From: Antonio Bennett Date: Tue, 23 Sep 2025 16:02:39 -0400 Subject: [PATCH 09/10] slight language change --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 34b9451..90f37a2 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ For `lang = 'rust'`, leetcode-cli generates per-problem crate structures to enab This keeps submissions unchanged (sends code snippet to LeetCode API) while improving local editing. -For other languages, files remain flat. Future support for similar setups (e.g., Python virtualenvs) can be added via config. +For other languages, files remain flat. Please contribute if needed!
From 078b9dad7502af5bea7e2fa63287ce8997ba5872 Mon Sep 17 00:00:00 2001 From: Antonio Bennett Date: Tue, 23 Sep 2025 16:07:14 -0400 Subject: [PATCH 10/10] Fix: Add serde default for enable_rust_crates to handle legacy configs - Ensures missing field in old leetcode.toml defaults to true without error. - Improves backward compatibility. --- src/config/code.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/config/code.rs b/src/config/code.rs index 06d7399..e24a8d9 100644 --- a/src/config/code.rs +++ b/src/config/code.rs @@ -38,7 +38,8 @@ pub struct Code { pub comment_leading: String, #[serde(default, skip_serializing)] pub test: bool, - pub enable_rust_crates: bool, + #[serde(default = "default_enable_rust_crates")] +pub enable_rust_crates: bool, pub lang: String, #[serde(default = "default_pick", skip_serializing)] pub pick: String,