leetcode_cli/cmds/
edit.rs

1//! Edit command
2use super::Command;
3use crate::{Error, Result};
4use anyhow::anyhow;
5use async_trait::async_trait;
6use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command as ClapCommand};
7use std::collections::HashMap;
8
9/// Abstract `edit` command
10///
11/// ```sh
12/// leetcode-edit
13/// Edit question by id
14///
15/// USAGE:
16///     leetcode edit <id>
17///
18/// FLAGS:
19///     -h, --help       Prints help information
20///     -V, --version    Prints version information
21///
22/// ARGS:
23///     <id>    question id
24/// ```
25pub struct EditCommand;
26
27#[async_trait]
28impl Command for EditCommand {
29    /// `edit` usage
30    fn usage() -> ClapCommand {
31        ClapCommand::new("edit")
32            .about("Edit question")
33            .visible_alias("e")
34            .arg(
35                Arg::new("lang")
36                    .short('l')
37                    .long("lang")
38                    .num_args(1)
39                    .help("Edit with specific language"),
40            )
41            .arg(
42                Arg::new("id")
43                    .num_args(1)
44                    .value_parser(clap::value_parser!(i32))
45                    .help("question id"),
46            )
47            .arg(
48                Arg::new("daily")
49                    .short('d')
50                    .long("daily")
51                    .help("Edit today's daily challenge")
52                    .action(ArgAction::SetTrue),
53            )
54            .group(
55                ArgGroup::new("question-id")
56                    .args(["id", "daily"])
57                    .multiple(false)
58                    .required(true),
59            )
60    }
61
62    /// `edit` handler
63    async fn handler(m: &ArgMatches) -> Result<()> {
64        use crate::{cache::models::Question, Cache};
65        use std::fs::File;
66        use std::io::Write;
67        use std::path::Path;
68
69        let cache = Cache::new()?;
70
71        let daily = m.get_one::<bool>("daily").unwrap_or(&false);
72        let daily_id = if *daily {
73            Some(cache.get_daily_problem_id().await?)
74        } else {
75            None
76        };
77
78        let id = m
79            .get_one::<i32>("id")
80            .copied()
81            .or(daily_id)
82            .ok_or(Error::NoneError)?;
83
84        let problem = cache.get_problem(id)?;
85        let mut conf = cache.to_owned().0.conf;
86
87        let test_flag = conf.code.test;
88
89        let p_desc_comment = problem.desc_comment(&conf);
90        // condition language
91        if m.contains_id("lang") {
92            conf.code.lang = m
93                .get_one::<String>("lang")
94                .ok_or(Error::NoneError)?
95                .to_string();
96            conf.sync()?;
97        }
98
99        let lang = &conf.code.lang;
100        let path = crate::helper::code_path(&problem, Some(lang.to_owned()))?;
101
102        if !Path::new(&path).exists() {
103            let mut qr = serde_json::from_str(&problem.desc);
104            if qr.is_err() {
105                qr = Ok(cache.get_question(id).await?);
106            }
107
108            let question: Question = qr?;
109
110            let mut file_code = File::create(&path)?;
111            let question_desc = question.desc_comment(&conf) + "\n";
112
113            let test_path = crate::helper::test_cases_path(&problem)?;
114
115            let mut flag = false;
116            for d in question.defs.0 {
117                if d.value == *lang {
118                    flag = true;
119                    if conf.code.comment_problem_desc {
120                        file_code.write_all(p_desc_comment.as_bytes())?;
121                        file_code.write_all(question_desc.as_bytes())?;
122                    }
123                    if let Some(inject_before) = &conf.code.inject_before {
124                        for line in inject_before {
125                            file_code.write_all((line.to_string() + "\n").as_bytes())?;
126                        }
127                    }
128                    if conf.code.edit_code_marker {
129                        file_code.write_all(
130                            (conf.code.comment_leading.clone()
131                                + " "
132                                + &conf.code.start_marker
133                                + "\n")
134                                .as_bytes(),
135                        )?;
136                    }
137                    file_code.write_all((d.code.to_string() + "\n").as_bytes())?;
138                    if conf.code.edit_code_marker {
139                        file_code.write_all(
140                            (conf.code.comment_leading.clone()
141                                + " "
142                                + &conf.code.end_marker
143                                + "\n")
144                                .as_bytes(),
145                        )?;
146                    }
147                    if let Some(inject_after) = &conf.code.inject_after {
148                        for line in inject_after {
149                            file_code.write_all((line.to_string() + "\n").as_bytes())?;
150                        }
151                    }
152
153                    if test_flag {
154                        let mut file_tests = File::create(&test_path)?;
155                        file_tests.write_all(question.all_cases.as_bytes())?;
156                    }
157                }
158            }
159
160            // if language is not found in the list of supported languges clean up files
161            if !flag {
162                std::fs::remove_file(&path)?;
163
164                if test_flag {
165                    std::fs::remove_file(&test_path)?;
166                }
167
168                return Err(
169                    anyhow!("This question doesn't support {lang}, please try another").into(),
170                );
171            }
172        }
173
174        // Get arguments of the editor
175        //
176        // for example:
177        //
178        // ```toml
179        // [code]
180        // editor = "emacsclient"
181        // editor_args = [ "-n", "-s", "doom" ]
182        // ```
183        //
184        // ```rust
185        // Command::new("emacsclient").args(&[ "-n", "-s", "doom", "<problem>" ])
186        // ```
187        let mut args: Vec<String> = Default::default();
188        if let Some(editor_args) = conf.code.editor_args {
189            args.extend_from_slice(&editor_args);
190        }
191
192        // Set environment variables for editor
193        //
194        // for example:
195        //
196        // ```toml
197        // [code]
198        // editor = "nvim"
199        // editor_envs = [ "XDG_DATA_HOME=...", "XDG_CONFIG_HOME=...", "XDG_STATE_HOME=..." ]
200        // ```
201        //
202        // ```rust
203        // Command::new("nvim").envs(&[ ("XDG_DATA_HOME", "..."), ("XDG_CONFIG_HOME", "..."), ("XDG_STATE_HOME", "..."), ]);
204        // ```
205        let mut envs: HashMap<String, String> = Default::default();
206        if let Some(editor_envs) = &conf.code.editor_envs {
207            for env in editor_envs.iter() {
208                let parts: Vec<&str> = env.split('=').collect();
209                if parts.len() == 2 {
210                    let name = parts[0].trim();
211                    let value = parts[1].trim();
212                    envs.insert(name.to_string(), value.to_string());
213                } else {
214                    return Err(anyhow!("Invalid editor environment variable: {env}").into());
215                }
216            }
217        }
218
219        args.push(path);
220        std::process::Command::new(conf.code.editor)
221            .envs(envs)
222            .args(args)
223            .status()?;
224        Ok(())
225    }
226}