A Neovim plugin enabling you to solve LeetCode problems.
| 1 | local log = require("leetcode.logger") |
| 2 | local arguments = require("leetcode.command.arguments") |
| 3 | local config = require("leetcode.config") |
| 4 | local event = require("nui.utils.autocmd").event |
| 5 | local api = vim.api |
| 6 | |
| 7 | local t = require("leetcode.translator") |
| 8 | |
| 9 | ---@class lc.Commands |
| 10 | local cmd = {} |
| 11 | |
| 12 | ---@param old_name string |
| 13 | ---@param new_name string |
| 14 | |
| 15 | log.warn(("`%s` is deprecated, use `%s` instead."):format(old_name, new_name)) |
| 16 | end |
| 17 | |
| 18 | |
| 19 | require("leetcode.utils").auth_guard() |
| 20 | |
| 21 | require("leetcode.cache").update() |
| 22 | end |
| 23 | |
| 24 | ---@param options table<string, string[]> |
| 25 | |
| 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) |
| 31 | end |
| 32 | |
| 33 | ---@param cb? function |
| 34 | |
| 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) |
| 82 | end |
| 83 | |
| 84 | |
| 85 | cmd.menu() |
| 86 | |
| 87 | log.warn("You're now signed out") |
| 88 | cmd.delete_cookie() |
| 89 | cmd.set_menu_page("signin") |
| 90 | end |
| 91 | |
| 92 | ---Sign out |
| 93 | |
| 94 | config.auth = {} |
| 95 | local cookie = require("leetcode.cache.cookie") |
| 96 | cookie.delete() |
| 97 | end |
| 98 | |
| 99 | cmd.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 |
| 106 | end |
| 107 | |
| 108 | |
| 109 | local leetcode = require("leetcode") |
| 110 | leetcode.stop() |
| 111 | end |
| 112 | |
| 113 | cmd.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) |
| 127 | end) |
| 128 | |
| 129 | |
| 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) |
| 142 | end |
| 143 | |
| 144 | |
| 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() |
| 169 | end |
| 170 | |
| 171 | |
| 172 | local leetcode = require("leetcode") |
| 173 | if leetcode.start(false) then |
| 174 | cmd.menu() |
| 175 | end |
| 176 | end |
| 177 | |
| 178 | |
| 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 |
| 189 | end |
| 190 | |
| 191 | |
| 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() |
| 199 | end |
| 200 | |
| 201 | ---@param page lc-menu.page |
| 202 | |
| 203 | _Lc_state.menu:set_page(page) |
| 204 | end |
| 205 | |
| 206 | -- |
| 207 | cmd.set_menu_page("menu") |
| 208 | config.stats.update() |
| 209 | end |
| 210 | |
| 211 | |
| 212 | local picker = require("leetcode.picker") |
| 213 | picker.tabs() |
| 214 | end |
| 215 | |
| 216 | |
| 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 |
| 223 | end |
| 224 | |
| 225 | |
| 226 | local utils = require("leetcode.utils") |
| 227 | local q = utils.curr_question() |
| 228 | if q then |
| 229 | q.description:toggle() |
| 230 | end |
| 231 | end |
| 232 | |
| 233 | |
| 234 | local utils = require("leetcode.utils") |
| 235 | local q = utils.curr_question() |
| 236 | if q then |
| 237 | q.description:toggle_stats() |
| 238 | end |
| 239 | end |
| 240 | |
| 241 | |
| 242 | local utils = require("leetcode.utils") |
| 243 | local q = utils.curr_question() |
| 244 | if q then |
| 245 | q.console:toggle() |
| 246 | end |
| 247 | end |
| 248 | |
| 249 | |
| 250 | local utils = require("leetcode.utils") |
| 251 | local q = utils.curr_question() |
| 252 | if q then |
| 253 | q.info:toggle() |
| 254 | end |
| 255 | end |
| 256 | |
| 257 | |
| 258 | -- cmd.deprecate("Leet hints", "Leet info") |
| 259 | cmd.info() |
| 260 | end |
| 261 | |
| 262 | |
| 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 |
| 269 | end |
| 270 | |
| 271 | |
| 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 |
| 278 | end |
| 279 | |
| 280 | |
| 281 | if config.is_cn then |
| 282 | return |
| 283 | end |
| 284 | local skills = require("leetcode-ui.popup.skills") |
| 285 | skills:show() |
| 286 | end |
| 287 | |
| 288 | |
| 289 | local languages = require("leetcode-ui.popup.languages") |
| 290 | languages:show() |
| 291 | end |
| 292 | |
| 293 | |
| 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 |
| 317 | end |
| 318 | |
| 319 | |
| 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() |
| 328 | end |
| 329 | |
| 330 | |
| 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) |
| 358 | end |
| 359 | |
| 360 | |
| 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 |
| 384 | end |
| 385 | |
| 386 | |
| 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 |
| 405 | end |
| 406 | |
| 407 | |
| 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) |
| 415 | end |
| 416 | |
| 417 | |
| 418 | local sessions = config.sessions.all |
| 419 | return vim.tbl_filter(function(s) |
| 420 | return s.is_active |
| 421 | end, sessions)[1] |
| 422 | end |
| 423 | |
| 424 | |
| 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] |
| 434 | end |
| 435 | |
| 436 | |
| 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) |
| 454 | end |
| 455 | |
| 456 | |
| 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) |
| 471 | end |
| 472 | |
| 473 | |
| 474 | require("leetcode.utils").auth_guard() |
| 475 | |
| 476 | config.stats.update_sessions() |
| 477 | end |
| 478 | |
| 479 | |
| 480 | require("leetcode.cache.cookie").delete() |
| 481 | require("leetcode.cache.problemlist").delete() |
| 482 | vim.cmd("qa!") |
| 483 | end |
| 484 | |
| 485 | ---@return string[], string[] |
| 486 | |
| 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 |
| 501 | end |
| 502 | |
| 503 | ---@param tbl table |
| 504 | local |
| 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)) |
| 518 | end |
| 519 | |
| 520 | ---@param _ string |
| 521 | ---@param line string |
| 522 | --- |
| 523 | ---@return string[] |
| 524 | |
| 525 | local args, options = cmd.parse(line:gsub("Leet%s", "")) |
| 526 | return cmd.rec_complete(args, options, cmd.commands) |
| 527 | end |
| 528 | |
| 529 | ---@param args string[] |
| 530 | ---@param options string[] |
| 531 | ---@param cmds table<string,any> |
| 532 | --- |
| 533 | ---@return string[] |
| 534 | |
| 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) |
| 566 | end |
| 567 | |
| 568 | |
| 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 |
| 589 | end |
| 590 | |
| 591 | |
| 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 | }) |
| 599 | end |
| 600 | |
| 601 | cmd.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 | |
| 660 | return cmd |