irongit

A Neovim plugin enabling you to solve LeetCode problems.

660 lines15 KBLua
1local log = require("leetcode.logger")
2local arguments = require("leetcode.command.arguments")
3local config = require("leetcode.config")
4local event = require("nui.utils.autocmd").event
5local api = vim.api
6
7local t = require("leetcode.translator")
8
9---@class lc.Commands
10local cmd = {}
11
12---@param old_name string
13---@param new_name string
14function cmd.deprecate(old_name, new_name)
15 log.warn(("`%s` is deprecated, use `%s` instead."):format(old_name, new_name))
16end
17
18function cmd.cache_update()
19 require("leetcode.utils").auth_guard()
20
21 require("leetcode.cache").update()
22end
23
24---@param options table<string, string[]>
25function cmd.problems(options)
26 require("leetcode.utils").auth_guard()
27
28 local p = require("leetcode.cache.problemlist").get()
29 local picker = require("leetcode.picker")
30 picker.question(p, options)
31end
32
33---@param cb? function
34function cmd.cookie_prompt(cb)
35 local cookie = require("leetcode.cache.cookie")
36
37 local popup_options = {
38 relative = "editor",
39 position = {
40 row = "50%",
41 col = "50%",
42 },
43 size = 40,
44 border = {
45 style = "rounded",
46 text = {
47 top = (" %s "):format(t("Enter cookie")),
48 top_align = "left",
49 },
50 },
51 win_options = {
52 winhighlight = "Normal:Normal",
53 },
54 }
55
56 local NuiInput = require("nui.input")
57 local input = NuiInput(popup_options, {
58 prompt = " 󰆘 ",
59 on_submit = function(value)
60 local err = cookie.set(value)
61
62 if not err then
63 log.info("Sign-in successful")
64 cmd.start_user_session()
65 else
66 log.error("Sign-in failed: " .. err)
67 end
68
69 pcall(cb, not err and true or false)
70 end,
71 })
72
73 input:mount()
74
75 local keys = config.user.keys
76 input:map("n", keys.toggle, function()
77 input:unmount()
78 end)
79 input:on(event.BufLeave, function()
80 input:unmount()
81 end)
82end
83
84function cmd.sign_out()
85 cmd.menu()
86
87 log.warn("You're now signed out")
88 cmd.delete_cookie()
89 cmd.set_menu_page("signin")
90end
91
92---Sign out
93function cmd.delete_cookie()
94 config.auth = {}
95 local cookie = require("leetcode.cache.cookie")
96 cookie.delete()
97end
98
99cmd.q_close_all = function()
100 local utils = require("leetcode.utils")
101 local qs = utils.question_tabs()
102
103 for _, tabp in ipairs(qs) do
104 tabp.question:unmount()
105 end
106end
107
108function cmd.exit()
109 local leetcode = require("leetcode")
110 leetcode.stop()
111end
112
113cmd.expire = vim.schedule_wrap(function()
114 local tabp = api.nvim_get_current_tabpage()
115 cmd.menu()
116
117 cmd.cookie_prompt(function(success)
118 if success then
119 if api.nvim_tabpage_is_valid(tabp) then
120 api.nvim_set_current_tabpage(tabp)
121 end
122 log.info("Successful re-login")
123 else
124 cmd.sign_out()
125 end
126 end)
127end)
128
129function cmd.qot()
130 require("leetcode.utils").auth_guard()
131
132 local problems = require("leetcode.api.problems")
133 local Question = require("leetcode-ui.question")
134
135 problems.question_of_today(function(qot, err)
136 if err then
137 return log.err(err)
138 end
139 local problemlist = require("leetcode.cache.problemlist")
140 Question(problemlist.get_by_title_slug(qot.title_slug)):mount()
141 end)
142end
143
144function cmd.random_question(opts)
145 require("leetcode.utils").auth_guard()
146
147 local problems = require("leetcode.cache.problemlist")
148 local question = require("leetcode.api.question")
149
150 if opts and opts.difficulty then
151 opts.difficulty = opts.difficulty[1]:upper()
152 end
153 if opts and opts.status then
154 opts.status = ({
155 ac = "AC",
156 notac = "TRIED",
157 todo = "NOT_STARTED",
158 })[opts.status[1]]
159 end
160
161 local q, err = question.random(opts)
162 if err then
163 return log.err(err)
164 end
165
166 local item = problems.get_by_title_slug(q.title_slug) or {}
167 local Question = require("leetcode-ui.question")
168 Question(item):mount()
169end
170
171function cmd.start_with_cmd()
172 local leetcode = require("leetcode")
173 if leetcode.start(false) then
174 cmd.menu()
175 end
176end
177
178function cmd.menu()
179 local winid, bufnr = _Lc_state.menu.winid, _Lc_state.menu.bufnr
180 local ok, tabp = pcall(api.nvim_win_get_tabpage, winid)
181 local ui = require("leetcode-ui.utils")
182
183 if ok then
184 api.nvim_set_current_tabpage(tabp)
185 ui.win_set_buf(winid, bufnr)
186 else
187 _Lc_state.menu:remount()
188 end
189end
190
191function cmd.yank()
192 local utils = require("leetcode.utils")
193 local q = utils.curr_question()
194 if not q then
195 return
196 end
197
198 q:editor_yank_code()
199end
200
201---@param page lc-menu.page
202function cmd.set_menu_page(page)
203 _Lc_state.menu:set_page(page)
204end
205
206function cmd.start_user_session() --
207 cmd.set_menu_page("menu")
208 config.stats.update()
209end
210
211function cmd.question_tabs()
212 local picker = require("leetcode.picker")
213 picker.tabs()
214end
215
216function cmd.change_lang()
217 local utils = require("leetcode.utils")
218 local q = utils.curr_question()
219 if q then
220 local picker = require("leetcode.picker")
221 picker.language(q)
222 end
223end
224
225function cmd.desc_toggle()
226 local utils = require("leetcode.utils")
227 local q = utils.curr_question()
228 if q then
229 q.description:toggle()
230 end
231end
232
233function cmd.desc_toggle_stats()
234 local utils = require("leetcode.utils")
235 local q = utils.curr_question()
236 if q then
237 q.description:toggle_stats()
238 end
239end
240
241function cmd.console()
242 local utils = require("leetcode.utils")
243 local q = utils.curr_question()
244 if q then
245 q.console:toggle()
246 end
247end
248
249function cmd.info()
250 local utils = require("leetcode.utils")
251 local q = utils.curr_question()
252 if q then
253 q.info:toggle()
254 end
255end
256
257function cmd.hints()
258 -- cmd.deprecate("Leet hints", "Leet info")
259 cmd.info()
260end
261
262function cmd.q_run()
263 local utils = require("leetcode.utils")
264 utils.auth_guard()
265 local q = utils.curr_question()
266 if q then
267 q.console:run()
268 end
269end
270
271function cmd.q_submit()
272 local utils = require("leetcode.utils")
273 utils.auth_guard()
274 local q = utils.curr_question()
275 if q then
276 q.console:run(true)
277 end
278end
279
280function cmd.ui_skills()
281 if config.is_cn then
282 return
283 end
284 local skills = require("leetcode-ui.popup.skills")
285 skills:show()
286end
287
288function cmd.ui_languages()
289 local languages = require("leetcode-ui.popup.languages")
290 languages:show()
291end
292
293function cmd.open()
294 local utils = require("leetcode.utils")
295 utils.auth_guard()
296 local q = utils.curr_question()
297
298 if q then
299 if vim.ui.open then
300 vim.ui.open(q.cache.link)
301 else
302 local command
303 local os_name = vim.loop.os_uname().sysname
304
305 if os_name == "Linux" then
306 command = string.format("xdg-open '%s'", q.cache.link)
307 elseif os_name == "Darwin" then
308 command = string.format("open '%s'", q.cache.link)
309 else
310 -- Fallback to Windows if uname is not available or does not match Linux/Darwin.
311 command = string.format("start \"\" \"%s\"", q.cache.link)
312 end
313
314 vim.fn.jobstart(command, { detach = true })
315 end
316 end
317end
318
319function cmd.reset()
320 local utils = require("leetcode.utils")
321 utils.auth_guard()
322 local q = utils.curr_question()
323 if not q then
324 return
325 end
326
327 q:editor_reset_code()
328end
329
330function cmd.last_submit()
331 local utils = require("leetcode.utils")
332 utils.auth_guard()
333 local q = utils.curr_question()
334 if not q then
335 return
336 end
337
338 local question_api = require("leetcode.api.question")
339 question_api.latest_submission(q.q.id, q.lang, function(res, err) --
340 if err then
341 if err.status == 404 then
342 log.error("You haven't submitted any code!")
343 else
344 log.err(err)
345 end
346
347 return
348 end
349
350 if type(res) == "table" and res.code then
351 ---@type string
352 local lines = res.code
353 q:editor_section_replace(lines, "code")
354 else
355 log.error("Something went wrong")
356 end
357 end)
358end
359
360function cmd.restore()
361 local utils = require("leetcode.utils")
362 local ui = require("leetcode-ui.utils")
363 local q = utils.curr_question()
364 if not q then
365 return
366 end
367
368 if
369 (q.winid and api.nvim_win_is_valid(q.winid))
370 and (q.bufnr and api.nvim_buf_is_valid(q.bufnr))
371 then
372 ui.win_set_buf(q.winid, q.bufnr)
373 end
374
375 q.description:show()
376 local winid, bufnr = q.description.winid, q.description.bufnr
377
378 if
379 (winid and api.nvim_win_is_valid(winid)) --
380 and (bufnr and api.nvim_buf_is_valid(bufnr))
381 then
382 ui.win_set_buf(q.winid, q.bufnr)
383 end
384end
385
386function cmd.inject()
387 local utils = require("leetcode.utils")
388 local q = utils.curr_question()
389 if not q then
390 return
391 end
392
393 if q.bufnr and api.nvim_buf_is_valid(q.bufnr) then
394 local range = q:editor_section_range("code")
395
396 if not range:is_valid_or_log() then
397 return
398 end
399
400 local lines = table.concat(range.lines, "\n", range.start_i, range.end_i)
401
402 q:editor_reset()
403 q:editor_section_replace(lines, "code")
404 end
405end
406
407function cmd.fold()
408 local utils = require("leetcode.utils")
409 local q = utils.curr_question()
410 if not q then
411 return
412 end
413
414 q:editor_fold_imports(true)
415end
416
417function cmd.get_active_session()
418 local sessions = config.sessions.all
419 return vim.tbl_filter(function(s)
420 return s.is_active
421 end, sessions)[1]
422end
423
424function cmd.get_session_by_name(name)
425 local sessions = config.sessions.all
426
427 name = name:lower()
428 if name == config.sessions.default then
429 name = ""
430 end
431 return vim.tbl_filter(function(s)
432 return s.name:lower() == name
433 end, sessions)[1]
434end
435
436function cmd.change_session(opts)
437 require("leetcode.utils").auth_guard()
438
439 local name = opts.name[1] or config.sessions.default
440
441 local session = cmd.get_session_by_name(name)
442 if not session then
443 return log.error("Session not found")
444 end
445
446 local stats_api = require("leetcode.api.statistics")
447 stats_api.change_session(session.id, function(_, err)
448 if err then
449 return log.err(err)
450 end
451 log.info(("Session changed to `%s`"):format(name))
452 config.stats.update()
453 end)
454end
455
456function cmd.create_session(opts)
457 require("leetcode.utils").auth_guard()
458
459 local name = opts.name[1]
460 if not name then
461 return log.error("Session name not provided")
462 end
463
464 local stats_api = require("leetcode.api.statistics")
465 stats_api.create_session(name, function(_, err)
466 if err then
467 return log.err(err)
468 end
469 log.info(("session `%s` created"):format(name))
470 end)
471end
472
473function cmd.update_sessions()
474 require("leetcode.utils").auth_guard()
475
476 config.stats.update_sessions()
477end
478
479function cmd.fix()
480 require("leetcode.cache.cookie").delete()
481 require("leetcode.cache.problemlist").delete()
482 vim.cmd("qa!")
483end
484
485---@return string[], string[]
486function cmd.parse(args)
487 local parts = vim.split(vim.trim(args), "%s+")
488 if args:sub(-1) == " " then
489 parts[#parts + 1] = ""
490 end
491
492 local options = {}
493 for _, part in ipairs(parts) do
494 local opt = part:match("(.-)=.-")
495 if opt then
496 table.insert(options, opt)
497 end
498 end
499
500 return parts, options
501end
502
503---@param tbl table
504local function cmds_keys(tbl)
505 return vim.tbl_filter(function(key)
506 if type(key) ~= "string" then
507 return false
508 end
509 if key:sub(1, 1) == "_" then
510 return false
511 end
512 if tbl[key]._private then
513 return false
514 end
515
516 return true
517 end, vim.tbl_keys(tbl))
518end
519
520---@param _ string
521---@param line string
522---
523---@return string[]
524function cmd.complete(_, line)
525 local args, options = cmd.parse(line:gsub("Leet%s", ""))
526 return cmd.rec_complete(args, options, cmd.commands)
527end
528
529---@param args string[]
530---@param options string[]
531---@param cmds table<string,any>
532---
533---@return string[]
534function cmd.rec_complete(args, options, cmds)
535 if not cmds or vim.tbl_isempty(args) then
536 return {}
537 end
538
539 if not cmds._args and cmds[args[1]] then
540 return cmd.rec_complete(args, options, cmds[table.remove(args, 1)])
541 end
542
543 local txt, keys = args[#args], cmds_keys(cmds)
544 if cmds._args then
545 local option_keys = cmds_keys(cmds._args)
546 option_keys = vim.tbl_filter(function(key)
547 return not vim.tbl_contains(options, key)
548 end, option_keys)
549 option_keys = vim.tbl_map(function(key)
550 return ("%s="):format(key)
551 end, option_keys)
552 keys = vim.tbl_extend("force", keys, option_keys)
553
554 local s = vim.split(txt, "=")
555 if s[2] and cmds._args[s[1]] then
556 local vals = vim.split(s[2], ",")
557 return vim.tbl_filter(function(key)
558 return not vim.tbl_contains(vals, key) and key:find(vals[#vals], 1, true) == 1
559 end, cmds._args[s[1]])
560 end
561 end
562
563 return vim.tbl_filter(function(key)
564 return not vim.tbl_contains(args, key) and key:find(txt, 1, true) == 1
565 end, keys)
566end
567
568function cmd.exec(args)
569 local cmds = cmd.commands
570
571 local options = vim.empty_dict()
572 for s in vim.gsplit(args.args:lower(), "%s+", { trimempty = true }) do
573 local opt = vim.split(s, "=")
574
575 if opt[2] then
576 options[opt[1]] = vim.split(opt[2], ",", { trimempty = true })
577 elseif cmds then
578 cmds = cmds[s]
579 else
580 break
581 end
582 end
583
584 if cmds and type(cmds[1]) == "function" then
585 cmds[1](options) ---@diagnostic disable-line
586 else
587 log.error(("Invalid command: `%s %s`"):format(args.name, args.args))
588 end
589end
590
591function cmd.setup()
592 api.nvim_create_user_command("Leet", cmd.exec, {
593 bar = true,
594 bang = true,
595 nargs = "?",
596 desc = "Leet",
597 complete = cmd.complete,
598 })
599end
600
601cmd.commands = {
602 cmd.menu,
603
604 menu = { cmd.menu },
605 exit = { cmd.exit },
606 console = { cmd.console },
607 info = { cmd.info },
608 hints = { cmd.hints },
609 tabs = { cmd.question_tabs },
610 lang = { cmd.change_lang },
611 run = { cmd.q_run },
612 test = { cmd.q_run },
613 submit = { cmd.q_submit },
614 daily = { cmd.qot },
615 yank = { cmd.yank },
616 open = { cmd.open },
617 reset = { cmd.reset },
618 last_submit = { cmd.last_submit },
619 restore = { cmd.restore },
620 inject = { cmd.inject },
621 fold = { cmd.fold },
622 -- session = {
623 -- change = {
624 -- cmd.change_session,
625 -- _args = arguments.session_change,
626 -- },
627 -- create = {
628 -- cmd.create_session,
629 -- _args = arguments.session_create,
630 -- },
631 -- update = { cmd.update_sessions },
632 -- },
633 list = {
634 cmd.problems,
635 _args = arguments.list,
636 },
637 random = {
638 cmd.random_question,
639 _args = arguments.random,
640 },
641 desc = {
642 cmd.desc_toggle,
643
644 stats = { cmd.desc_toggle_stats },
645 toggle = { cmd.desc_toggle },
646 },
647 cookie = {
648 update = { cmd.cookie_prompt },
649 delete = { cmd.sign_out },
650 },
651 cache = {
652 update = { cmd.cache_update },
653 },
654 fix = {
655 cmd.fix,
656 _private = true,
657 },
658}
659
660return cmd