A Neovim plugin enabling you to solve LeetCode problems.
feat: better mount and reset lines on `random` and `daily`
5 files changed, +138 -96
+121-75lua/leetcode-ui/question.lua
| @@ -16,10 +16,11 @@ local log = require("leetcode.logger") | ||
| 16 | 16 | ---@field console lc.ui.Console |
| 17 | 17 | ---@field lang string |
| 18 | 18 | ---@field cache lc.cache.Question |
| 19 | +---@field reset boolean | |
| 19 | 20 | local Question = Object("LeetQuestion") |
| 20 | 21 | |
| 21 | 22 | ---@param raw? boolean |
| 22 | -function Question:get_snippet(raw) | |
| 23 | +function Question:snippet(raw) | |
| 23 | 24 | local snippets = self.q.code_snippets ~= vim.NIL and self.q.code_snippets or {} |
| 24 | 25 | local snip = vim.tbl_filter(function(snip) return snip.lang_slug == self.lang end, snippets)[1] |
| 25 | 26 | if not snip then return end |
| @@ -28,7 +29,30 @@ function Question:get_snippet(raw) | ||
| 28 | 29 | return raw and code or self:injector(code) |
| 29 | 30 | end |
| 30 | 31 | |
| 31 | -function Question:create_file() | |
| 32 | +---@param code? string | |
| 33 | +function Question:set_lines(code) | |
| 34 | + if not vim.api.nvim_buf_is_valid(self.bufnr) then return end | |
| 35 | + | |
| 36 | + pcall(vim.cmd.undojoin) | |
| 37 | + local s_i, e_i = self:range() | |
| 38 | + code = code and code or (self:snippet(true) or "") | |
| 39 | + vim.api.nvim_buf_set_lines(self.bufnr, s_i - 1, e_i, false, vim.split(code, "\n")) | |
| 40 | +end | |
| 41 | + | |
| 42 | +function Question:reset_lines() | |
| 43 | + if not self.reset then return end | |
| 44 | + | |
| 45 | + local new_lines = self:snippet(true) or "" | |
| 46 | + | |
| 47 | + vim.schedule(function() -- | |
| 48 | + log.info("Previous code found, resetting…\nTo undo, simply press `u`") | |
| 49 | + end) | |
| 50 | + | |
| 51 | + self:set_lines(new_lines) | |
| 52 | +end | |
| 53 | + | |
| 54 | +---@return string path, boolean? existed | |
| 55 | +function Question:path() | |
| 32 | 56 | local lang = utils.get_lang(self.lang) |
| 33 | 57 | local alt = lang.alt and ("." .. lang.alt) or "" |
| 34 | 58 | |
| @@ -38,47 +62,39 @@ function Question:create_file() | ||
| 38 | 62 | self.file = config.storage.home:joinpath(fn_legacy) |
| 39 | 63 | |
| 40 | 64 | if self.file:exists() then -- |
| 41 | - return self.file:absolute() | |
| 65 | + return self.file:absolute(), true | |
| 42 | 66 | end |
| 43 | 67 | |
| 44 | 68 | local fn = ("%s.%s%s.%s"):format(self.q.frontend_id, self.q.title_slug, alt, lang.ft) |
| 45 | 69 | self.file = config.storage.home:joinpath(fn) |
| 70 | + local existed = true | |
| 46 | 71 | |
| 47 | 72 | if not self.file:exists() then -- |
| 48 | - self.file:write(self:get_snippet(), "w") | |
| 73 | + self.file:write(self:snippet(), "w") | |
| 74 | + existed = false | |
| 49 | 75 | end |
| 50 | 76 | |
| 51 | - return self.file:absolute() | |
| 77 | + return self.file:absolute(), existed | |
| 52 | 78 | end |
| 53 | 79 | |
| 54 | ----@param new_tabp? boolean | |
| 55 | ----@return boolean was_loaded | |
| 56 | -function Question:create_buffer(new_tabp) | |
| 57 | - local file_name = self:create_file() | |
| 80 | +function Question:create_buffer() | |
| 81 | + local path, existed = self:path() | |
| 58 | 82 | |
| 59 | - local buf = vim.fn.bufadd(file_name) | |
| 60 | - assert(buf ~= 0, "Failed to create buffer") | |
| 83 | + vim.cmd("$tabe " .. path) | |
| 61 | 84 | |
| 62 | - self.bufnr = buf | |
| 63 | - if vim.fn.bufloaded(self.bufnr) == 1 then | |
| 64 | - return true | |
| 65 | - else | |
| 66 | - vim.fn.bufload(self.bufnr) | |
| 67 | - end | |
| 85 | + self.bufnr = vim.api.nvim_get_current_buf() | |
| 86 | + self.winid = vim.api.nvim_get_current_win() | |
| 68 | 87 | |
| 69 | - local cmd | |
| 70 | - if new_tabp then | |
| 71 | - cmd = ("$tabe %s"):format(file_name) | |
| 72 | - else | |
| 73 | - cmd = ("edit %s"):format(file_name) | |
| 74 | - end | |
| 88 | + vim.api.nvim_set_option_value("buflisted", true, { buf = self.bufnr }) | |
| 75 | 89 | |
| 76 | 90 | local i = self:fold_range() |
| 77 | - if i then cmd = cmd .. (" | %d,%dfold"):format(1, i) end | |
| 78 | - | |
| 79 | - vim.api.nvim_exec2(cmd, {}) | |
| 91 | + if i then -- | |
| 92 | + pcall(vim.cmd, ("%d,%dfold"):format(1, i)) | |
| 93 | + end | |
| 80 | 94 | |
| 81 | - return false | |
| 95 | + if existed then -- | |
| 96 | + self:reset_lines() | |
| 97 | + end | |
| 82 | 98 | end |
| 83 | 99 | |
| 84 | 100 | ---@param before boolean |
| @@ -99,7 +115,7 @@ function Question:inject(before) | ||
| 99 | 115 | end |
| 100 | 116 | |
| 101 | 117 | if res and res ~= "" then |
| 102 | - return before and (res .. "\n\n") or ("\n\n" .. res) | |
| 118 | + return before and (res .. "\n") or ("\n" .. res) | |
| 103 | 119 | else |
| 104 | 120 | return nil |
| 105 | 121 | end |
| @@ -109,50 +125,62 @@ end | ||
| 109 | 125 | function Question:injector(code) |
| 110 | 126 | local lang = utils.get_lang(self.lang) |
| 111 | 127 | |
| 112 | - local inj_before = self:inject(true) or "" | |
| 113 | - local inj_after = self:inject(false) or "" | |
| 128 | + local parts = { | |
| 129 | + ("%s @leet start"):format(lang.comment), | |
| 130 | + code, | |
| 131 | + ("%s @leet end"):format(lang.comment), | |
| 132 | + } | |
| 114 | 133 | |
| 115 | - return inj_before -- | |
| 116 | - .. ("%s @leet start\n"):format(lang.comment) | |
| 117 | - .. code | |
| 118 | - .. ("\n%s @leet end"):format(lang.comment) | |
| 119 | - .. inj_after | |
| 120 | -end | |
| 134 | + local before = self:inject(true) | |
| 135 | + if before then table.insert(parts, 1, before) end | |
| 121 | 136 | |
| 122 | ----@param pre? boolean | |
| 123 | -function Question:_unmount(pre) | |
| 124 | - self.info:unmount() | |
| 125 | - self.console:unmount() | |
| 126 | - self.description:unmount() | |
| 137 | + local after = self:inject(false) | |
| 138 | + if after then table.insert(parts, after) end | |
| 127 | 139 | |
| 128 | - if not pre and vim.api.nvim_buf_is_valid(self.bufnr) then | |
| 129 | - vim.api.nvim_buf_delete(self.bufnr, { force = true }) | |
| 130 | - end | |
| 140 | + return table.concat(parts, "\n") | |
| 141 | +end | |
| 131 | 142 | |
| 132 | - if vim.api.nvim_win_is_valid(self.winid) then -- | |
| 133 | - vim.api.nvim_win_close(self.winid, true) | |
| 143 | +function Question:unmount() | |
| 144 | + if vim.v.dying ~= 0 then -- | |
| 145 | + return | |
| 134 | 146 | end |
| 135 | 147 | |
| 136 | - _Lc_questions = vim.tbl_filter(function(q) return q.bufnr ~= self.bufnr end, _Lc_questions) | |
| 148 | + vim.schedule(function() | |
| 149 | + self.info:unmount() | |
| 150 | + self.console:unmount() | |
| 151 | + self.description:unmount() | |
| 152 | + | |
| 153 | + if vim.api.nvim_buf_is_valid(self.bufnr) then | |
| 154 | + vim.api.nvim_buf_delete(self.bufnr, { force = true, unload = false }) | |
| 155 | + end | |
| 156 | + | |
| 157 | + _Lc_questions = vim.tbl_filter(function(q) -- | |
| 158 | + return q.bufnr ~= self.bufnr | |
| 159 | + end, _Lc_questions) | |
| 137 | 160 | |
| 138 | - self = nil | |
| 161 | + self = nil | |
| 162 | + end) | |
| 139 | 163 | end |
| 140 | 164 | |
| 141 | ----@param self lc.ui.Question | |
| 142 | ----@param pre? boolean | |
| 143 | -Question.unmount = vim.schedule_wrap(function(self, pre) self:_unmount(pre) end) | |
| 165 | +function Question:_unmount() | |
| 166 | + if vim.api.nvim_win_is_valid(self.winid) then vim.api.nvim_win_close(self.winid, true) end | |
| 167 | +end | |
| 144 | 168 | |
| 145 | -function Question:handle_mount() | |
| 146 | - self:create_buffer(true) | |
| 169 | +local group = vim.api.nvim_create_augroup("leetcode_questions", { clear = true }) | |
| 170 | +function Question:autocmds() | |
| 171 | + vim.api.nvim_create_autocmd("WinClosed", { | |
| 172 | + group = group, | |
| 173 | + buffer = self.bufnr, | |
| 174 | + callback = function() self:unmount() end, | |
| 175 | + }) | |
| 176 | +end | |
| 147 | 177 | |
| 148 | - self.winid = vim.api.nvim_get_current_win() | |
| 178 | +function Question:handle_mount() | |
| 179 | + self:create_buffer() | |
| 149 | 180 | |
| 150 | 181 | table.insert(_Lc_questions, self) |
| 151 | 182 | |
| 152 | - vim.api.nvim_create_autocmd("QuitPre", { | |
| 153 | - buffer = self.bufnr, | |
| 154 | - callback = function() self:unmount(true) end, | |
| 155 | - }) | |
| 183 | + self:autocmds() | |
| 156 | 184 | |
| 157 | 185 | self.description = Description(self):mount() |
| 158 | 186 | self.console = Console(self) |
| @@ -173,7 +201,7 @@ function Question:mount() | ||
| 173 | 201 | end |
| 174 | 202 | self.q = q |
| 175 | 203 | |
| 176 | - if self:get_snippet() then | |
| 204 | + if self:snippet() then | |
| 177 | 205 | self:handle_mount() |
| 178 | 206 | else |
| 179 | 207 | local msg = ("Snippet for `%s` not found. Select a different language"):format(self.lang) |
| @@ -232,37 +260,55 @@ end | ||
| 232 | 260 | ---@param self lc.ui.Question |
| 233 | 261 | ---@param lang lc.lang |
| 234 | 262 | Question.change_lang = vim.schedule_wrap(function(self, lang) |
| 235 | - if vim.api.nvim_get_current_win() ~= self.winid then | |
| 236 | - vim.api.nvim_set_current_win(self.winid) | |
| 237 | - end | |
| 238 | - | |
| 239 | 263 | local old_lang, old_bufnr = self.lang, self.bufnr |
| 240 | - self.lang = lang | |
| 241 | 264 | |
| 242 | - local ok, was_loaded = pcall(Question.create_buffer, self) | |
| 243 | - if ok then | |
| 244 | - vim.api.nvim_buf_set_option(old_bufnr, "buflisted", false) | |
| 245 | - vim.api.nvim_buf_set_option(self.bufnr, "buflisted", true) | |
| 265 | + local ok, err = pcall(function() | |
| 266 | + self.lang = lang | |
| 267 | + local path, existed = self:path() | |
| 268 | + | |
| 269 | + self.bufnr = vim.fn.bufadd(path) | |
| 270 | + assert(self.bufnr ~= 0, "Failed to create buffer " .. path) | |
| 246 | 271 | |
| 247 | - if was_loaded then | |
| 248 | - vim.api.nvim_win_set_buf(self.winid, self.bufnr) | |
| 249 | - else | |
| 272 | + local loaded = vim.api.nvim_buf_is_loaded(self.bufnr) | |
| 273 | + vim.fn.bufload(self.bufnr) | |
| 274 | + | |
| 275 | + vim.api.nvim_win_set_buf(self.winid, self.bufnr) | |
| 276 | + | |
| 277 | + vim.api.nvim_set_option_value("buflisted", false, { buf = old_bufnr }) | |
| 278 | + vim.api.nvim_set_option_value("buflisted", true, { buf = self.bufnr }) | |
| 279 | + | |
| 280 | + local i = self:fold_range() | |
| 281 | + if i then -- | |
| 282 | + pcall(vim.cmd, ("%d,%dfold"):format(1, i)) | |
| 283 | + end | |
| 284 | + | |
| 285 | + if existed then -- | |
| 286 | + self:reset_lines() | |
| 287 | + end | |
| 288 | + | |
| 289 | + if not loaded then -- | |
| 250 | 290 | utils.exec_hook("question_enter", self) |
| 251 | 291 | end |
| 252 | - else | |
| 253 | - log.error("Changing language failed") | |
| 292 | + | |
| 293 | + self:autocmds() | |
| 294 | + end) | |
| 295 | + | |
| 296 | + if not ok then | |
| 297 | + log.error("Failed to change language\n" .. err) | |
| 254 | 298 | self.lang = old_lang |
| 255 | 299 | self.bufnr = old_bufnr |
| 256 | 300 | end |
| 257 | 301 | end) |
| 258 | 302 | |
| 259 | 303 | ---@param problem lc.cache.Question |
| 260 | -function Question:init(problem) | |
| 304 | +---@param reset boolean | |
| 305 | +function Question:init(problem, reset) | |
| 261 | 306 | self.cache = problem |
| 262 | 307 | self.lang = config.lang |
| 308 | + self.reset = reset and true or false | |
| 263 | 309 | end |
| 264 | 310 | |
| 265 | ----@type fun(question: lc.cache.Question): lc.ui.Question | |
| 311 | +---@type fun(question: lc.cache.Question, reset?: boolean): lc.ui.Question | |
| 266 | 312 | local LeetQuestion = Question |
| 267 | 313 | |
| 268 | 314 | return LeetQuestion |
+9-10lua/leetcode/command/init.lua
| @@ -90,14 +90,14 @@ function cmd.delete_cookie() | ||
| 90 | 90 | cookie.delete() |
| 91 | 91 | end |
| 92 | 92 | |
| 93 | -cmd.q_close_all = vim.schedule_wrap(function() | |
| 93 | +cmd.q_close_all = function() | |
| 94 | 94 | local utils = require("leetcode.utils") |
| 95 | 95 | local qs = utils.question_tabs() |
| 96 | 96 | |
| 97 | 97 | for _, tabp in ipairs(qs) do |
| 98 | - tabp.question:unmount() | |
| 98 | + tabp.question:_unmount() | |
| 99 | 99 | end |
| 100 | -end) | |
| 100 | +end | |
| 101 | 101 | |
| 102 | 102 | cmd.expire = vim.schedule_wrap(function() |
| 103 | 103 | local tabp = api.nvim_get_current_tabpage() |
| @@ -127,7 +127,7 @@ function cmd.qot() | ||
| 127 | 127 | problems.question_of_today(function(qot, err) |
| 128 | 128 | if err then return log.err(err) end |
| 129 | 129 | local problemlist = require("leetcode.cache.problemlist") |
| 130 | - Question(problemlist.get_by_title_slug(qot.title_slug)):mount() | |
| 130 | + Question(problemlist.get_by_title_slug(qot.title_slug), true):mount() | |
| 131 | 131 | end) |
| 132 | 132 | end |
| 133 | 133 | |
| @@ -151,7 +151,7 @@ function cmd.random_question(opts) | ||
| 151 | 151 | |
| 152 | 152 | local item = problems.get_by_title_slug(q.title_slug) or {} |
| 153 | 153 | local Question = require("leetcode-ui.question") |
| 154 | - Question(item):mount() | |
| 154 | + Question(item, true):mount() | |
| 155 | 155 | end |
| 156 | 156 | |
| 157 | 157 | function cmd.start_with_cmd() |
| @@ -281,8 +281,7 @@ function cmd.reset() | ||
| 281 | 281 | local q = utils.curr_question() |
| 282 | 282 | if not q then return end |
| 283 | 283 | |
| 284 | - local snip = q:get_snippet(true) | |
| 285 | - utils.set_question_lines(q, snip) | |
| 284 | + q:set_lines() | |
| 286 | 285 | end |
| 287 | 286 | |
| 288 | 287 | function cmd.last_submit() |
| @@ -303,8 +302,8 @@ function cmd.last_submit() | ||
| 303 | 302 | return |
| 304 | 303 | end |
| 305 | 304 | |
| 306 | - if type(res) == "table" and res.code and api.nvim_buf_is_valid(q.bufnr) then | |
| 307 | - utils.set_question_lines(q, res.code) | |
| 305 | + if type(res) == "table" and res.code then | |
| 306 | + q:set_lines(res.code) | |
| 308 | 307 | else |
| 309 | 308 | log.error("Something went wrong") |
| 310 | 309 | end |
| @@ -359,7 +358,7 @@ function cmd.inject() | ||
| 359 | 358 | end |
| 360 | 359 | |
| 361 | 360 | if end_i == nil then |
| 362 | - log.error("`@leet start` not found") | |
| 361 | + log.error("`@leet end` not found") | |
| 363 | 362 | else |
| 364 | 363 | local after = q:inject(false) |
| 365 | 364 | if after then |
+7-3lua/leetcode/pickers/question.lua
| @@ -124,7 +124,7 @@ return { | ||
| 124 | 124 | }), |
| 125 | 125 | sorter = conf.generic_sorter(theme), |
| 126 | 126 | attach_mappings = function(prompt_bufnr, map) |
| 127 | - actions.select_default:replace(function() | |
| 127 | + local function mount_question(reset) | |
| 128 | 128 | local selection = action_state.get_selected_entry() |
| 129 | 129 | if not selection then return end |
| 130 | 130 | |
| @@ -134,8 +134,12 @@ return { | ||
| 134 | 134 | end |
| 135 | 135 | |
| 136 | 136 | actions.close(prompt_bufnr) |
| 137 | - Question(q):mount() | |
| 138 | - end) | |
| 137 | + Question(q, reset):mount() | |
| 138 | + end | |
| 139 | + | |
| 140 | + actions.select_default:replace(function() mount_question() end) | |
| 141 | + map({ "n", "i" }, "<C-Enter>", function() mount_question(true) end) | |
| 142 | + | |
| 139 | 143 | return true |
| 140 | 144 | end, |
| 141 | 145 | }) |
+0-7lua/leetcode/utils.lua
| @@ -115,11 +115,4 @@ function utils.norm_ins(str) | ||
| 115 | 115 | return ins:sub(2, #ins - 1) |
| 116 | 116 | end |
| 117 | 117 | |
| 118 | -function utils.set_question_lines(q, code) | |
| 119 | - if not vim.api.nvim_buf_is_valid(q.bufnr) then return end | |
| 120 | - | |
| 121 | - local s_i, e_i = q:range() | |
| 122 | - vim.api.nvim_buf_set_lines(q.bufnr, s_i - 1, e_i, false, vim.split(code, "\n")) | |
| 123 | -end | |
| 124 | - | |
| 125 | 118 | return utils |