leetcode_tui_core/content/
question.rs1pub(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 if update_result.is_some() {
192 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 if update_result.is_some() {
206 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 "{} {} {}", 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}