A Neovim plugin enabling you to solve LeetCode problems.
feat: open lang picker when session lang is not available
10 files changed, +140 -89
+0-7LICENSE
| @@ -1,7 +0,0 @@ | ||
| 1 | -Copyright 2023 kawre | |
| 2 | - | |
| 3 | -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | |
| 4 | - | |
| 5 | -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | |
| 6 | - | |
| 7 | -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
+4-0lua/leetcode.lua
| @@ -59,6 +59,10 @@ local function start() | ||
| 59 | 59 | setup_highlights() |
| 60 | 60 | setup_cmds() |
| 61 | 61 | |
| 62 | + local utils = require("leetcode.utils") | |
| 63 | + assert(utils.get_lang(config.lang), "Unknown language: " .. config.lang) | |
| 64 | + assert(utils.get_lang(config.sql), "Unknown sql dialect: " .. config.sql) | |
| 65 | + | |
| 62 | 66 | require("leetcode-menu"):init() |
| 63 | 67 | end |
| 64 | 68 |
+13-2lua/leetcode/command.lua
| @@ -100,12 +100,18 @@ function cmd.question_tabs() require("leetcode.pickers.question-tabs").pick() en | ||
| 100 | 100 | function cmd.change_lang() |
| 101 | 101 | local utils = require("leetcode.utils") |
| 102 | 102 | local q = utils.curr_question() |
| 103 | - | |
| 104 | 103 | if not q then return log.warn("No current question found") end |
| 104 | + | |
| 105 | 105 | require("leetcode.pickers.language").pick(q) |
| 106 | 106 | end |
| 107 | 107 | |
| 108 | -function cmd.desc_toggle() _Lc_questions[_Lc_curr_question].description:toggle() end | |
| 108 | +function cmd.desc_toggle() | |
| 109 | + local utils = require("leetcode.utils") | |
| 110 | + local q = utils.curr_question() | |
| 111 | + if not q then return log.error("No current question found") end | |
| 112 | + | |
| 113 | + q.description:toggle() | |
| 114 | +end | |
| 109 | 115 | |
| 110 | 116 | function cmd.console() |
| 111 | 117 | local utils = require("leetcode.utils") |
| @@ -128,4 +134,9 @@ function cmd.q_submit() | ||
| 128 | 134 | q.console:submit() |
| 129 | 135 | end |
| 130 | 136 | |
| 137 | +function cmd.fix() | |
| 138 | + local config = require("leetcode.config") | |
| 139 | + config.home:rmdir() | |
| 140 | +end | |
| 141 | + | |
| 131 | 142 | return cmd |
+4-0lua/leetcode/config/icons.lua
| @@ -0,0 +1,4 @@ | ||
| 1 | +return { | |
| 2 | + current = "", | |
| 3 | + dot = "", | |
| 4 | +} |
+3-0lua/leetcode/config/init.lua
| @@ -9,8 +9,11 @@ local config = { | ||
| 9 | 9 | domain = "https://leetcode.com", |
| 10 | 10 | debug = false, |
| 11 | 11 | lang = "cpp", |
| 12 | + sql = "mysql", | |
| 12 | 13 | home = {}, ---@type Path |
| 14 | + | |
| 13 | 15 | langs = require("leetcode.config.langs"), |
| 16 | + icons = require("leetcode.config.icons"), | |
| 14 | 17 | } |
| 15 | 18 | |
| 16 | 19 | ---@class lc.UserAuth |
+2-2lua/leetcode/config/langs.lua
| @@ -51,7 +51,7 @@ return { | ||
| 51 | 51 | slug = "swift", |
| 52 | 52 | icon = "", |
| 53 | 53 | color = "#FFAC45", |
| 54 | - short = "swft", | |
| 54 | + short = "swift", | |
| 55 | 55 | ft = "swift", |
| 56 | 56 | }, |
| 57 | 57 | { |
| @@ -70,7 +70,7 @@ return { | ||
| 70 | 70 | slug = "scala", |
| 71 | 71 | icon = "", |
| 72 | 72 | color = "#DC322F", |
| 73 | - short = "scla", | |
| 73 | + short = "scala", | |
| 74 | 74 | ft = "scala", |
| 75 | 75 | }, |
| 76 | 76 | { lang = "Rust", slug = "rust", icon = "", color = "#DEA584", short = "rust", ft = "rs" }, |
+9-4lua/leetcode/config/template.lua
| @@ -18,8 +18,13 @@ | ||
| 18 | 18 | ---| "racket" |
| 19 | 19 | ---| "erlang" |
| 20 | 20 | ---| "elixir" |
| 21 | +---| "bash" | |
| 22 | +---| "html" | |
| 23 | +---| "pythonml" | |
| 24 | +---| "react" | |
| 25 | +---| "vanillajs" | |
| 21 | 26 | |
| 22 | ----@alias lc.sql_lang | |
| 27 | +---@alias lc.sql | |
| 23 | 28 | ---| "pythondata" |
| 24 | 29 | ---| "mysql" |
| 25 | 30 | ---| "mssql" |
| @@ -41,7 +46,7 @@ local M = { | ||
| 41 | 46 | ---@type lc.lang |
| 42 | 47 | lang = "cpp", |
| 43 | 48 | |
| 44 | - ---@type lc.sql_lang | |
| 49 | + ---@type lc.sql | |
| 45 | 50 | sql = "mysql", |
| 46 | 51 | |
| 47 | 52 | ---@type string |
| @@ -51,13 +56,13 @@ local M = { | ||
| 51 | 56 | logging = true, |
| 52 | 57 | |
| 53 | 58 | console = { |
| 54 | - ---@type boolean | |
| 55 | - open_on_runcode = false, | |
| 59 | + open_on_runcode = false, ---@type boolean | |
| 56 | 60 | |
| 57 | 61 | size = { |
| 58 | 62 | width = "75%", ---@type string | integer |
| 59 | 63 | height = "75%", ---@type string | integer |
| 60 | 64 | }, |
| 65 | + | |
| 61 | 66 | dir = "row", ---@type "col" | "row" |
| 62 | 67 | }, |
| 63 | 68 |
+65-37lua/leetcode/pickers/language.lua
| @@ -12,6 +12,8 @@ local entry_display = require("telescope.pickers.entry_display") | ||
| 12 | 12 | local actions = require("telescope.actions") |
| 13 | 13 | local action_state = require("telescope.actions.state") |
| 14 | 14 | |
| 15 | +local M = {} | |
| 16 | + | |
| 15 | 17 | ---@param snippet lc.QuestionCodeSnippet |
| 16 | 18 | --- |
| 17 | 19 | ---@return string |
| @@ -64,40 +66,66 @@ end | ||
| 64 | 66 | |
| 65 | 67 | local opts = require("telescope.themes").get_dropdown() |
| 66 | 68 | |
| 67 | -return { | |
| 68 | - ---@param question lc.Question | |
| 69 | - pick = function(question) | |
| 70 | - pickers | |
| 71 | - .new(opts, { | |
| 72 | - prompt_title = "Available Languages", | |
| 73 | - finder = finders.new_table({ | |
| 74 | - results = question.q.code_snippets, | |
| 75 | - entry_maker = entry_maker, | |
| 76 | - }), | |
| 77 | - sorter = conf.generic_sorter(opts), | |
| 78 | - attach_mappings = function(prompt_bufnr, map) | |
| 79 | - actions.select_default:replace(function() | |
| 80 | - local selection = action_state.get_selected_entry() | |
| 81 | - if not selection then return end | |
| 82 | - local snippet = selection.value | |
| 83 | - | |
| 84 | - if question.lang == snippet.t.slug then | |
| 85 | - log.warn("Language already set to: " .. snippet.t.lang) | |
| 86 | - return | |
| 87 | - end | |
| 88 | - | |
| 89 | - if snippet.t.sql then | |
| 90 | - config.sql = snippet.t.slug | |
| 91 | - else | |
| 92 | - config.lang = snippet.t.slug | |
| 93 | - end | |
| 94 | - | |
| 95 | - actions.close(prompt_bufnr) | |
| 96 | - Question:init(question.cache) | |
| 97 | - end) | |
| 98 | - return true | |
| 99 | - end, | |
| 100 | - }) | |
| 101 | - :find() | |
| 102 | - end, | |
| 103 | -} | |
| 69 | +---@param question lc.Question | |
| 70 | +M.pick_lang = function(question, callback) | |
| 71 | + pickers | |
| 72 | + .new(opts, { | |
| 73 | + prompt_title = "Available Languages", | |
| 74 | + finder = finders.new_table({ | |
| 75 | + results = question.q.code_snippets, | |
| 76 | + entry_maker = entry_maker, | |
| 77 | + }), | |
| 78 | + sorter = conf.generic_sorter(opts), | |
| 79 | + attach_mappings = function(prompt_bufnr, map) | |
| 80 | + actions.select_default:replace(function() | |
| 81 | + local selection = action_state.get_selected_entry() | |
| 82 | + if not selection then return end | |
| 83 | + | |
| 84 | + local snippet = selection.value | |
| 85 | + if question.lang == snippet.t.slug then | |
| 86 | + return log.warn("Language already set to: " .. snippet.t.lang) | |
| 87 | + end | |
| 88 | + | |
| 89 | + config[snippet.t.sql and "sql" or "lang"] = snippet.t.slug | |
| 90 | + actions.close(prompt_bufnr) | |
| 91 | + callback(snippet) | |
| 92 | + end) | |
| 93 | + | |
| 94 | + return true | |
| 95 | + end, | |
| 96 | + }) | |
| 97 | + :find() | |
| 98 | +end | |
| 99 | + | |
| 100 | +---@param question lc.Question | |
| 101 | +M.pick = function(question) | |
| 102 | + pickers | |
| 103 | + .new(opts, { | |
| 104 | + prompt_title = "Available Languages", | |
| 105 | + finder = finders.new_table({ | |
| 106 | + results = question.q.code_snippets, | |
| 107 | + entry_maker = entry_maker, | |
| 108 | + }), | |
| 109 | + sorter = conf.generic_sorter(opts), | |
| 110 | + attach_mappings = function(prompt_bufnr, map) | |
| 111 | + actions.select_default:replace(function() | |
| 112 | + local selection = action_state.get_selected_entry() | |
| 113 | + if not selection then return end | |
| 114 | + | |
| 115 | + local snippet = selection.value | |
| 116 | + if question.lang == snippet.t.slug then | |
| 117 | + return log.warn("Language already set to: " .. snippet.t.lang) | |
| 118 | + end | |
| 119 | + | |
| 120 | + config[snippet.t.sql and "sql" or "lang"] = snippet.t.slug | |
| 121 | + actions.close(prompt_bufnr) | |
| 122 | + Question:init(question.cache) | |
| 123 | + end) | |
| 124 | + | |
| 125 | + return true | |
| 126 | + end, | |
| 127 | + }) | |
| 128 | + :find() | |
| 129 | +end | |
| 130 | + | |
| 131 | +return M |
+0-3lua/leetcode/pickers/question-tabs.lua
| @@ -1,12 +1,9 @@ | ||
| 1 | 1 | local log = require("leetcode.logger") |
| 2 | 2 | local utils = require("leetcode.utils") |
| 3 | 3 | |
| 4 | -local Question = require("leetcode.ui.question") | |
| 5 | - | |
| 6 | 4 | local pickers = require("telescope.pickers") |
| 7 | 5 | local finders = require("telescope.finders") |
| 8 | 6 | local conf = require("telescope.config").values |
| 9 | -local config = require("leetcode.config") | |
| 10 | 7 | |
| 11 | 8 | local entry_display = require("telescope.pickers.entry_display") |
| 12 | 9 | local actions = require("telescope.actions") |
+40-34lua/leetcode/ui/question.lua
| @@ -4,6 +4,7 @@ local Description = require("leetcode.ui.description") | ||
| 4 | 4 | local api_question = require("leetcode.api.question") |
| 5 | 5 | local Console = require("leetcode.ui.console") |
| 6 | 6 | local utils = require("leetcode.utils") |
| 7 | +local async = require("plenary.async") | |
| 7 | 8 | |
| 8 | 9 | ---@class lc.Question |
| 9 | 10 | ---@field file Path |
| @@ -22,29 +23,37 @@ _Lc_questions = {} | ||
| 22 | 23 | ---@type integer |
| 23 | 24 | _Lc_curr_question = 0 |
| 24 | 25 | |
| 26 | +---@param q lc.QuestionResponse | |
| 27 | +--- | |
| 28 | +---@return boolean | |
| 29 | +local function is_sql(q) | |
| 30 | + for _, value in ipairs(q.topic_tags or {}) do | |
| 31 | + if value.slug == "database" then return true end | |
| 32 | + end | |
| 33 | + | |
| 34 | + return false | |
| 35 | +end | |
| 36 | + | |
| 37 | +function Question:get_snippet() | |
| 38 | + local snippets = self.q.code_snippets ~= vim.NIL and self.q.code_snippets or {} | |
| 39 | + return vim.tbl_filter(function(snip) return snip.lang_slug == self.lang end, snippets)[1] | |
| 40 | +end | |
| 41 | + | |
| 25 | 42 | ---@private |
| 26 | 43 | function Question:create_file() |
| 27 | - local snippets = self.q.code_snippets | |
| 28 | - local snippet = {} | |
| 29 | - | |
| 30 | - for _, snip in pairs(snippets ~= vim.NIL and snippets or {}) do | |
| 31 | - if snip.lang_slug == self.lang then | |
| 32 | - snippet.code = snip.code | |
| 33 | - snippet.lang = snip.lang_slug | |
| 34 | - break | |
| 35 | - end | |
| 36 | - end | |
| 44 | + local lang = utils.get_lang(is_sql(self.q) and config.sql or config.lang) | |
| 45 | + local suffix = lang.sql and "-" .. lang.short or "" | |
| 46 | + local fn = string.format("%s.%s%s.%s", self.q.frontend_id, self.q.title_slug, suffix, lang.ft) | |
| 37 | 47 | |
| 38 | - if not snippet then return log.error("failed to fetch code snippet") end | |
| 39 | - self.file:write(snippet.code, "w") | |
| 48 | + self.file = config.home:joinpath(fn) | |
| 40 | 49 | end |
| 41 | 50 | |
| 42 | 51 | function Question:mount() |
| 43 | - if not self.file:exists() then self:create_file() end | |
| 52 | + self:create_file() | |
| 53 | + if not self.file:exists() then self.file:write(self:get_snippet().code, "w") end | |
| 44 | 54 | |
| 45 | - vim.api.nvim_set_current_dir(self.file:parent().filename) | |
| 55 | + vim.api.nvim_set_current_dir(config.home:absolute()) | |
| 46 | 56 | vim.cmd("$tabe " .. self.file:absolute()) |
| 47 | - _Lc_tabpage = vim.api.nvim_get_current_tabpage() | |
| 48 | 57 | |
| 49 | 58 | self.bufnr = vim.api.nvim_get_current_buf() |
| 50 | 59 | _Lc_curr_question = self.bufnr |
| @@ -52,6 +61,7 @@ function Question:mount() | ||
| 52 | 61 | |
| 53 | 62 | self.description = Description:init(self) |
| 54 | 63 | self.console = Console:init(self) |
| 64 | + | |
| 55 | 65 | self:autocmds() |
| 56 | 66 | return self |
| 57 | 67 | end |
| @@ -84,44 +94,40 @@ function Question:autocmds() | ||
| 84 | 94 | }) |
| 85 | 95 | end |
| 86 | 96 | |
| 87 | ----@param q lc.QuestionResponse | |
| 88 | ---- | |
| 89 | ----@return boolean | |
| 90 | -local function is_sql(q) | |
| 91 | - for _, value in ipairs(q.topic_tags or {}) do | |
| 92 | - if value.slug == "database" then return true end | |
| 97 | +function Question:handle_mount() | |
| 98 | + if self:get_snippet() then | |
| 99 | + self:mount() | |
| 100 | + else | |
| 101 | + local msg = | |
| 102 | + string.format("Snippet for `%s` not found. Select a different language", self.lang) | |
| 103 | + log.warn(msg) | |
| 104 | + | |
| 105 | + require("leetcode.pickers.language").pick_lang(self, function(snippet) | |
| 106 | + self.lang = snippet.t.slug | |
| 107 | + self:mount() | |
| 108 | + end) | |
| 93 | 109 | end |
| 94 | - | |
| 95 | - return false | |
| 96 | 110 | end |
| 97 | 111 | |
| 98 | 112 | ---@param problem lc.Cache.Question |
| 99 | 113 | function Question:init(problem) |
| 100 | 114 | local tabp = utils.detect_duplicate_question(problem.title_slug) |
| 101 | - if tabp then | |
| 102 | - pcall(vim.cmd.tabnext, tabp) | |
| 103 | - return | |
| 104 | - end | |
| 115 | + if tabp then return pcall(vim.cmd.tabnext, tabp) end | |
| 105 | 116 | |
| 106 | 117 | local q = api_question.by_title_slug(problem.title_slug) |
| 107 | 118 | if q.is_paid_only and not config.auth.is_premium then |
| 108 | - log.warn("Question is for premium users only") | |
| 109 | - return | |
| 119 | + return log.warn("Question is for premium users only") | |
| 110 | 120 | end |
| 111 | 121 | |
| 112 | 122 | local lang = utils.get_lang(is_sql(q) and config.sql or config.lang) |
| 113 | - local suffix = lang.sql and "-" .. lang.short or "" | |
| 114 | - local fn = string.format("%s.%s%s.%s", q.frontend_id, q.title_slug, suffix, lang.ft) | |
| 115 | - local file = config.home:joinpath(fn) | |
| 116 | 123 | |
| 117 | 124 | local obj = setmetatable({ |
| 118 | - file = file, | |
| 119 | 125 | q = q, |
| 120 | 126 | lang = lang.slug, |
| 121 | 127 | cache = problem, |
| 122 | 128 | }, self) |
| 123 | 129 | |
| 124 | - return obj:mount() | |
| 130 | + return obj:handle_mount() | |
| 125 | 131 | end |
| 126 | 132 | |
| 127 | 133 | return Question |