leetcode_tui_core/content/
question.rs

1pub(super) mod sol_dir;
2mod stats;
3use crate::utils::string_ops::replace_script_tags;
4use crate::SendError;
5use crate::{emit, utils::Paginate};
6use fuzzy_matcher::skim::SkimMatcherV2;
7use fuzzy_matcher::FuzzyMatcher;
8use html2md::parse_html;
9use leetcode_core::graphql::query::{daily_coding_challenge, RunOrSubmitCodeCheckResult};
10use leetcode_core::types::run_submit_response::display::CustomDisplay;
11use leetcode_core::types::run_submit_response::ParsedResponse;
12use leetcode_core::{
13    GQLLeetcodeRequest, QuestionContentRequest, RunCodeRequest, SubmitCodeRequest,
14};
15use leetcode_tui_config::log;
16use leetcode_tui_db::{DbQuestion, DbTopic};
17use leetcode_tui_shared::layout::Window;
18pub(crate) use sol_dir::init;
19use sol_dir::SOLUTION_FILE_MANAGER;
20use stats::Stats;
21use std::rc::Rc;
22
23pub struct Questions {
24    paginate: Paginate<Rc<DbQuestion>>,
25    ques_haystack: Vec<Rc<DbQuestion>>,
26    needle: Option<String>,
27    matcher: SkimMatcherV2,
28    show_stats: bool,
29}
30
31impl Default for Questions {
32    fn default() -> Self {
33        Self {
34            paginate: Paginate::new(vec![]),
35            needle: Default::default(),
36            ques_haystack: vec![],
37            matcher: Default::default(),
38            show_stats: Default::default(),
39        }
40    }
41}
42
43impl Questions {
44    pub fn prev_ques(&mut self) -> bool {
45        self.paginate.prev_elem(self.widget_height())
46    }
47
48    pub fn next_ques(&mut self) -> bool {
49        self.paginate.next_elem(self.widget_height())
50    }
51
52    pub fn rand_ques(&mut self) -> bool {
53        self.paginate.rand_elem(self.widget_height())
54    }
55
56    pub fn window(&self) -> &[Rc<DbQuestion>] {
57        self.paginate.window(self.widget_height())
58    }
59
60    pub fn hovered(&self) -> Option<&Rc<DbQuestion>> {
61        self.paginate.hovered()
62    }
63
64    pub fn set_adhoc(&mut self, question: DbQuestion) -> bool {
65        if let Some(id) = self.ques_haystack.iter().position(|x| x.id == question.id) {
66            self.needle = None;
67            self.filter_questions();
68            self.paginate.set_element_by_index(id, self.widget_height());
69            return true;
70        } else {
71            emit!(Popup(
72                "not",
73                vec![format!(
74                    "Question not found with id={}, title={}",
75                    question.id, question.title
76                )]
77            ));
78        };
79        return false;
80    }
81
82    fn widget_height(&self) -> usize {
83        let window = Window::default();
84        let height = window.root.center_layout.question.inner.height;
85        height as usize
86    }
87}
88
89impl Questions {
90    async fn get_question_content(slug: &str) -> Vec<String> {
91        let qc = QuestionContentRequest::new(slug.to_string());
92        if let Ok(content) = qc.send().await.emit_if_error() {
93            let lines = content
94                .data
95                .question
96                .html_to_text()
97                .lines()
98                .map(|l| replace_script_tags(l))
99                .collect::<Vec<String>>();
100            return lines;
101        }
102        return vec!["".into()];
103    }
104
105    pub fn show_question_content(&self) -> bool {
106        if let Some(_hovered) = self.hovered() {
107            let slug = _hovered.title_slug.clone();
108            let title = _hovered.title.clone();
109            tokio::spawn(async move {
110                let lines = Self::get_question_content(slug.as_str()).await;
111                emit!(Popup(title, lines));
112            });
113        } else {
114            log::debug!("hovered question is none");
115        }
116        true
117    }
118
119    pub fn run_solution(&self) -> bool {
120        self._run_solution(false)
121    }
122
123    pub fn submit_solution(&self) -> bool {
124        self._run_solution(true)
125    }
126
127    fn _run_solution(&self, is_submit: bool) -> bool {
128        if let Some(_hovered) = self.hovered() {
129            let mut cloned_quest = _hovered.as_ref().clone();
130            let id = _hovered.id.to_string();
131            if let Ok(lang_refs) = SOLUTION_FILE_MANAGER
132                .get()
133                .unwrap()
134                .read()
135                .unwrap()
136                .get_available_languages(id.as_str())
137                .emit_if_error()
138            {
139                let cloned_langs = lang_refs.iter().map(|v| v.to_string()).collect();
140                tokio::spawn(async move {
141                    if let Some(selected_lang) =
142                        emit!(SelectPopup("Available solutions in", cloned_langs)).await
143                    {
144                        let selected_sol_file = SOLUTION_FILE_MANAGER
145                            .get()
146                            .unwrap()
147                            .read()
148                            .unwrap()
149                            .get_solution_file(id.as_str(), selected_lang)
150                            .cloned();
151                        if let Ok(f) = selected_sol_file.emit_if_error() {
152                            if let Ok(contents) = f.read_contents().await.emit_if_error() {
153                                let lang = f.language;
154                                let request = if is_submit {
155                                    SubmitCodeRequest::new(
156                                        lang,
157                                        f.question_id,
158                                        contents,
159                                        f.title_slug,
160                                    )
161                                    .poll_check_response()
162                                    .await
163                                } else {
164                                    let mut run_code_req = RunCodeRequest::new(
165                                        lang,
166                                        None,
167                                        f.question_id,
168                                        contents,
169                                        f.title_slug,
170                                    );
171                                    if let Err(e) = run_code_req
172                                        .set_sample_test_cases_if_none()
173                                        .await
174                                        .emit_if_error()
175                                    {
176                                        log::info!(
177                                            "error while setting the sample testcase list {}",
178                                            e
179                                        );
180                                        return;
181                                    } else {
182                                        run_code_req.poll_check_response().await
183                                    }
184                                };
185
186                                if let Ok(response) = request.emit_if_error() {
187                                    if let Ok(update_result) =
188                                        cloned_quest.mark_attempted().emit_if_error()
189                                    {
190                                        // when solution is just run against sample cases
191                                        if update_result.is_some() {
192                                            // fetches latest result from db
193                                            emit!(QuestionUpdate);
194                                        }
195                                    }
196
197                                    if is_submit {
198                                        let is_submission_accepted =
199                                            matches!(response, ParsedResponse::SubmitAccepted(..));
200                                        if is_submission_accepted {
201                                            if let Ok(update_result) =
202                                                cloned_quest.mark_accepted().emit_if_error()
203                                            {
204                                                // when solution is accepted
205                                                if update_result.is_some() {
206                                                    // fetches latest result from db
207                                                    emit!(QuestionUpdate);
208                                                }
209                                            };
210                                        }
211                                    }
212                                    emit!(Popup(response.get_display_lines()));
213                                }
214                            }
215                        }
216                    }
217                });
218            }
219        }
220        false
221    }
222
223    pub fn solve_for_language(&self) -> bool {
224        if let Some(_hovered) = self.hovered() {
225            let slug = _hovered.title_slug.clone();
226            tokio::spawn(async move {
227                if let Ok(editor_data) = leetcode_core::EditorDataRequest::new(slug)
228                    .send()
229                    .await
230                    .emit_if_error()
231                {
232                    if let Some(selected) = emit!(SelectPopup(
233                        "Select Language",
234                        editor_data
235                            .get_languages()
236                            .iter()
237                            .map(|l| l.to_string())
238                            .collect()
239                    ))
240                    .await
241                    {
242                        let selected_lang = editor_data.get_languages()[selected];
243                        let editor_content = editor_data.get_editor_data_by_language(selected_lang);
244                        let question_content = editor_data.data.question.content.as_str();
245
246                        if let Ok(file_name) =
247                            editor_data.get_filename(selected_lang).emit_if_error()
248                        {
249                            if let Some(e_data) = editor_content {
250                                let file_contents = format!(
251                                    "{}\n\n\n{}",
252                                    selected_lang.comment_text(&replace_script_tags(&parse_html(
253                                        question_content
254                                    ))),
255                                    e_data
256                                );
257                                if let Ok(written_path) = SOLUTION_FILE_MANAGER
258                                    .get()
259                                    .unwrap()
260                                    .write()
261                                    .unwrap()
262                                    .create_solution_file(
263                                        file_name.as_str(),
264                                        file_contents.as_str(),
265                                    )
266                                    .emit_if_error()
267                                {
268                                    emit!(Open(written_path));
269                                }
270                            };
271                        };
272                    } else {
273                        log::info!("quitting popup unselected");
274                    }
275                }
276            });
277        }
278        false
279    }
280
281    pub fn set_questions(&mut self, questions: Vec<DbQuestion>) {
282        self.ques_haystack = questions.into_iter().map(Rc::new).collect();
283        self.filter_questions();
284    }
285
286    pub fn add_question(&mut self, question: DbQuestion) {
287        self.ques_haystack.push(Rc::new(question));
288    }
289
290    pub fn toggle_daily_question(&self) -> bool {
291        tokio::spawn(async move {
292            let daily_challenge_question = daily_coding_challenge::Query::new()
293                .send()
294                .await
295                .emit_if_error()
296                .unwrap();
297
298            let mut db_question: DbQuestion = daily_challenge_question
299                .data
300                .active_daily_coding_challenge_question
301                .question
302                .try_into()
303                .emit_if_error()
304                .unwrap();
305
306            db_question.save_to_db().unwrap();
307            emit!(Topic(DbTopic { slug: "all".into() }));
308            emit!(AdhocQuestion(db_question));
309        });
310        false
311    }
312}
313
314impl Questions {
315    pub fn toggle_search(&mut self) -> bool {
316        let existing_needle = self.needle.clone();
317        tokio::spawn(async move {
318            let mut rx = emit!(Input(existing_needle));
319            while let Some(maybe_needle) = rx.recv().await {
320                if let Some(needle) = maybe_needle {
321                    emit!(QuestionFilter(Some(needle)));
322                } else {
323                    break;
324                }
325            }
326        });
327        false
328    }
329
330    pub fn filter_by(&mut self, string: Option<String>) {
331        if self.needle != string {
332            self.needle = string;
333            self.filter_questions();
334        }
335    }
336
337    fn filter_questions(&mut self) {
338        self.ques_haystack.sort();
339        let fil_quests = if let Some(needle) = self.needle.as_ref() {
340            let quests: Vec<Rc<DbQuestion>> = self
341                .ques_haystack
342                .iter()
343                .filter(|q| {
344                    let search_string = format!(
345                        "{} {} {}", // id, topics, title
346                        q.id,
347                        q.topics
348                            .iter()
349                            .map(|t| t.slug.as_str())
350                            .collect::<Vec<&str>>()
351                            .join(", "),
352                        q.title
353                    );
354
355                    self.matcher
356                        .fuzzy_match(search_string.as_str(), &needle)
357                        .is_some()
358                })
359                .cloned()
360                .collect();
361            quests
362        } else {
363            self.ques_haystack.clone()
364        };
365        self.paginate.update_list(fil_quests);
366    }
367}
368
369impl Questions {
370    pub fn get_stats(&self) -> Stats<'_> {
371        Stats::new(&self.ques_haystack)
372    }
373
374    pub fn toggle_stats(&mut self) -> bool {
375        self.show_stats = !self.show_stats;
376        true
377    }
378
379    pub fn is_stats_visible(&self) -> bool {
380        self.show_stats
381    }
382}