A Neovim plugin enabling you to solve LeetCode problems.
fix: spinner hang-ups
Refactor spinner logic to eliminate spinner hanging up.
10 files changed, +158 -96
+5-0README.md
| @@ -97,6 +97,11 @@ To see full configuration types see [template.lua](./lua/leetcode/config/templat | ||
| 97 | 97 | update_interval = 60 * 60 * 24 * 7, ---@type integer 7 days |
| 98 | 98 | }, |
| 99 | 99 | |
| 100 | + editor = { | |
| 101 | + reset_previous_code = true, ---@type boolean | |
| 102 | + fold_imports = true, ---@type boolean | |
| 103 | + }, | |
| 104 | + | |
| 100 | 105 | console = { |
| 101 | 106 | open_on_runcode = true, ---@type boolean |
| 102 | 107 |
+3-3lua/leetcode-ui/lines/calendar.lua
| @@ -188,12 +188,12 @@ function Calendar:fetch() | ||
| 188 | 188 | end |
| 189 | 189 | |
| 190 | 190 | function Calendar:update() |
| 191 | - local spinner = Spinner:init("updating calendar") | |
| 191 | + local spinner = Spinner:start("updating calendar") | |
| 192 | 192 | statistics.calendar(function(res, err) |
| 193 | 193 | if err then |
| 194 | - spinner:stop(err.msg, false) | |
| 194 | + spinner:error(err.msg) | |
| 195 | 195 | else |
| 196 | - spinner:stop("calendar updated", true, { timeout = 200 }) | |
| 196 | + spinner:success("calendar updated") | |
| 197 | 197 | self:handle_res(res) |
| 198 | 198 | end |
| 199 | 199 | end) |
+3-3lua/leetcode-ui/lines/solved.lua
| @@ -79,12 +79,12 @@ function Solved:handle_res(res) | ||
| 79 | 79 | end |
| 80 | 80 | |
| 81 | 81 | function Solved:update() |
| 82 | - local spinner = Spinner:init("updating solved problems...") | |
| 82 | + local spinner = Spinner:start("updating solved problems") | |
| 83 | 83 | statistics.solved(function(res, err) |
| 84 | 84 | if err then |
| 85 | - spinner:stop(err.msg, false) | |
| 85 | + spinner:error(err.msg) | |
| 86 | 86 | else |
| 87 | - spinner:stop("solved problems updated", true, { timeout = 200 }) | |
| 87 | + spinner:success("solved problems updated") | |
| 88 | 88 | self:handle_res(res) |
| 89 | 89 | end |
| 90 | 90 | end) |
+0-2lua/leetcode-ui/popup/info.lua
| @@ -8,8 +8,6 @@ local Parser = require("leetcode.parser") | ||
| 8 | 8 | |
| 9 | 9 | local config = require("leetcode.config") |
| 10 | 10 | local keys = config.user.keys |
| 11 | -local utils = require("leetcode.utils") | |
| 12 | -local log = require("leetcode.logger") | |
| 13 | 11 | |
| 14 | 12 | ---@class lc.ui.InfoPopup : lc.ui.Popup |
| 15 | 13 | ---@field popup NuiPopup |
+3-3lua/leetcode-ui/popup/languages.lua
| @@ -63,13 +63,13 @@ end | ||
| 63 | 63 | function Languages:mount() |
| 64 | 64 | Languages.super.mount(self) |
| 65 | 65 | |
| 66 | - local spinner = Spinner:init("fetching user languages", "dot") | |
| 66 | + local spinner = Spinner:start("fetching user languages", "dot") | |
| 67 | 67 | stats_api.languages(function(res, err) |
| 68 | 68 | if err then |
| 69 | - spinner:stop(err.msg, false) | |
| 69 | + spinner:error(err.msg) | |
| 70 | 70 | else |
| 71 | 71 | self:populate(res) |
| 72 | - spinner:stop(nil, true, { timeout = 500 }) | |
| 72 | + spinner:success("languages fetched") | |
| 73 | 73 | Languages.super.draw(self) |
| 74 | 74 | end |
| 75 | 75 | end) |
+3-3lua/leetcode-ui/popup/skills.lua
| @@ -47,13 +47,13 @@ end | ||
| 47 | 47 | function Skills:mount() |
| 48 | 48 | Skills.super.mount(self) |
| 49 | 49 | |
| 50 | - local spinner = Spinner:init("fetching user skills", "dot") | |
| 50 | + local spinner = Spinner:start("fetching user skills", "dot") | |
| 51 | 51 | stats_api.skills(function(res, err) |
| 52 | 52 | if err then |
| 53 | - spinner:stop(err.msg, false) | |
| 53 | + spinner:error(err.msg) | |
| 54 | 54 | else |
| 55 | 55 | self:populate(res) |
| 56 | - spinner:stop(nil, true, { timeout = 200 }) | |
| 56 | + spinner:success("skills fetched") | |
| 57 | 57 | Skills.super.draw(self) |
| 58 | 58 | end |
| 59 | 59 | end) |
+9-11lua/leetcode/api/problems.lua
| @@ -4,8 +4,6 @@ local config = require("leetcode.config") | ||
| 4 | 4 | local urls = require("leetcode.api.urls") |
| 5 | 5 | local Spinner = require("leetcode.logger.spinner") |
| 6 | 6 | |
| 7 | -local log = require("leetcode.logger") | |
| 8 | - | |
| 9 | 7 | ---@class lc.ProblemsApi |
| 10 | 8 | local Problems = {} |
| 11 | 9 | |
| @@ -18,7 +16,7 @@ function Problems.all(cb, noti) | ||
| 18 | 16 | |
| 19 | 17 | local spinner |
| 20 | 18 | if noti then |
| 21 | - spinner = Spinner:init("updating problemlist cache...", "points") | |
| 19 | + spinner = Spinner:start("updating problemlist cache", "points") | |
| 22 | 20 | end |
| 23 | 21 | |
| 24 | 22 | if cb then |
| @@ -26,7 +24,7 @@ function Problems.all(cb, noti) | ||
| 26 | 24 | callback = function(res, err) |
| 27 | 25 | if err then |
| 28 | 26 | if spinner then |
| 29 | - spinner:stop(err.msg, false) | |
| 27 | + spinner:error(err.msg) | |
| 30 | 28 | end |
| 31 | 29 | return cb(nil, err) |
| 32 | 30 | end |
| @@ -40,21 +38,21 @@ function Problems.all(cb, noti) | ||
| 40 | 38 | Problems.translated_titles(function(titles, terr) |
| 41 | 39 | if terr then |
| 42 | 40 | if spinner then |
| 43 | - spinner:stop(terr.msg, false) | |
| 41 | + spinner:error(terr.msg) | |
| 44 | 42 | end |
| 45 | 43 | return cb(nil, terr) |
| 46 | 44 | end |
| 47 | 45 | |
| 48 | 46 | problems = utils.translate_titles(problems, titles) |
| 49 | 47 | if spinner then |
| 50 | - spinner:stop("cache updated") | |
| 48 | + spinner:success("cache updated") | |
| 51 | 49 | end |
| 52 | 50 | |
| 53 | 51 | cb(problems) |
| 54 | 52 | end) |
| 55 | 53 | else |
| 56 | 54 | if spinner then |
| 57 | - spinner:stop("cache updated") | |
| 55 | + spinner:success("cache updated") | |
| 58 | 56 | end |
| 59 | 57 | |
| 60 | 58 | cb(problems) |
| @@ -65,7 +63,7 @@ function Problems.all(cb, noti) | ||
| 65 | 63 | local res, err = utils.get(endpoint) |
| 66 | 64 | if err then |
| 67 | 65 | if spinner then |
| 68 | - spinner:stop(err.msg, false) | |
| 66 | + spinner:error(err.msg) | |
| 69 | 67 | end |
| 70 | 68 | return nil, err |
| 71 | 69 | end |
| @@ -76,18 +74,18 @@ function Problems.all(cb, noti) | ||
| 76 | 74 | local titles, terr = Problems.translated_titles() |
| 77 | 75 | if terr then |
| 78 | 76 | if spinner then |
| 79 | - spinner:stop(terr.msg, false) | |
| 77 | + spinner:error(terr.msg) | |
| 80 | 78 | end |
| 81 | 79 | return nil, terr |
| 82 | 80 | end |
| 83 | 81 | |
| 84 | 82 | if spinner then |
| 85 | - spinner:stop("problems cache updated") | |
| 83 | + spinner:success("problems cache updated") | |
| 86 | 84 | end |
| 87 | 85 | return utils.translate_titles(problems, titles) |
| 88 | 86 | else |
| 89 | 87 | if spinner then |
| 90 | - spinner:stop("problems cache updated") | |
| 88 | + spinner:success("problems cache updated") | |
| 91 | 89 | end |
| 92 | 90 | return problems |
| 93 | 91 | end |
+118-63lua/leetcode/logger/spinner/init.lua
| @@ -1,13 +1,16 @@ | ||
| 1 | 1 | local config = require("leetcode.config") |
| 2 | 2 | local t = require("leetcode.translator") |
| 3 | +local lvls = vim.log.levels | |
| 3 | 4 | |
| 4 | 5 | ---@class lc.Spinner |
| 5 | ----@field spinner lc.spinner | nil | |
| 6 | +---@field spinner lc.spinner | |
| 7 | +---@field stype lc.spinner_types | |
| 6 | 8 | ---@field index integer |
| 7 | ----@field noti any | |
| 9 | +---@field notif any | |
| 10 | +---@field timer uv.uv_timer_t | |
| 8 | 11 | ---@field msg string |
| 9 | -local spinner = {} | |
| 10 | -spinner.__index = spinner | |
| 12 | +local Spinner = {} | |
| 13 | +Spinner.__index = Spinner | |
| 11 | 14 | |
| 12 | 15 | ---@class lc.spinner |
| 13 | 16 | ---@field frames string[] |
| @@ -39,96 +42,148 @@ local spinners = { | ||
| 39 | 42 | }, |
| 40 | 43 | } |
| 41 | 44 | |
| 45 | +---@param msg? string | |
| 46 | +---@param opts? table | |
| 47 | +function Spinner:error(msg, opts) | |
| 48 | + self:set_icon("") | |
| 49 | + self:stop(msg, opts) | |
| 50 | +end | |
| 51 | + | |
| 52 | +---@param msg? string | |
| 53 | +---@param opts? table | |
| 54 | +function Spinner:success(msg, opts) | |
| 55 | + self:set_icon("") | |
| 56 | + self:stop(msg, opts) | |
| 57 | +end | |
| 58 | + | |
| 42 | 59 | ---@private |
| 43 | -function spinner:spin() | |
| 44 | - local stype = self.spinner | |
| 45 | - if not stype then | |
| 46 | - return | |
| 47 | - end | |
| 60 | +---@param msg? string | |
| 61 | +---@param opts? table | |
| 62 | +function Spinner:stop(msg, opts) | |
| 63 | + self:set_message(msg) | |
| 64 | + self.opts.timeout = 2500 | |
| 65 | + self:extend(opts) | |
| 48 | 66 | |
| 49 | - self:set(nil, nil, { | |
| 50 | - icon = stype.frames[self.index + 1], | |
| 51 | - }) | |
| 67 | + if self.timer:is_active() then | |
| 68 | + self.timer:stop() | |
| 69 | + end | |
| 52 | 70 | |
| 53 | - self.index = (self.index + 1) % #stype.frames | |
| 71 | + if not self._closed then | |
| 72 | + self._closed = true | |
| 73 | + self.timer:close(function() | |
| 74 | + self:replace() | |
| 75 | + end) | |
| 76 | + end | |
| 77 | +end | |
| 54 | 78 | |
| 55 | - local fps = math.floor(1000 / stype.fps) | |
| 56 | - vim.defer_fn(function() | |
| 57 | - self:spin() | |
| 58 | - end, fps) | |
| 79 | +---@private | |
| 80 | +function Spinner:set_icon(icon) | |
| 81 | + self:extend({ icon = icon }) | |
| 59 | 82 | end |
| 60 | 83 | |
| 61 | 84 | ---@private |
| 62 | ---- | |
| 63 | ----@param msg? string | |
| 64 | ----@param lvl? integer | |
| 65 | ----@param opts? table | |
| 66 | -function spinner:set(msg, lvl, opts) | |
| 67 | - if not self.spinner then | |
| 68 | - return | |
| 85 | +function Spinner:set_lvl(lvl) | |
| 86 | + if lvl then | |
| 87 | + self.lvl = lvl | |
| 69 | 88 | end |
| 89 | +end | |
| 70 | 90 | |
| 91 | +---@private | |
| 92 | +function Spinner:set_message(msg) | |
| 71 | 93 | if msg then |
| 72 | - self:update(msg) | |
| 94 | + self.msg = msg | |
| 73 | 95 | end |
| 74 | - lvl = lvl or vim.log.levels.INFO | |
| 75 | - | |
| 76 | - local id = type(self.noti) == "table" and self.noti.id or self.noti | |
| 96 | +end | |
| 77 | 97 | |
| 78 | - opts = vim.tbl_deep_extend("force", { | |
| 79 | - hide_from_history = true, | |
| 80 | - history = false, | |
| 81 | - title = config.name, | |
| 82 | - timeout = false, | |
| 83 | - replace = id, | |
| 84 | - id = id, | |
| 85 | - }, opts or {}) | |
| 98 | +function Spinner:soft_update(msg, lvl, opts) | |
| 99 | + self:set_message(msg) | |
| 100 | + self:set_lvl(lvl) | |
| 101 | + self:extend(opts) | |
| 102 | +end | |
| 86 | 103 | |
| 87 | - self.noti = vim.notify(self.msg, lvl, opts) | |
| 104 | +function Spinner:update(msg, lvl, opts) | |
| 105 | + self:soft_update(msg, lvl, opts) | |
| 106 | + self:reset_loop() | |
| 88 | 107 | end |
| 89 | 108 | |
| 90 | ----@param spinner_type lc.spinner_types | |
| 91 | -function spinner:change(spinner_type) | |
| 92 | - self.spinner = spinners[spinner_type] | |
| 109 | +---@private | |
| 110 | +---@param opts? table | |
| 111 | +function Spinner:extend(opts) | |
| 112 | + if opts then | |
| 113 | + self.opts = vim.tbl_deep_extend("force", self.opts, opts) | |
| 114 | + end | |
| 93 | 115 | end |
| 94 | 116 | |
| 95 | ----@param msg any | |
| 96 | -function spinner:update(msg) | |
| 97 | - self.msg = t(tostring(msg)) | |
| 117 | +---@private | |
| 118 | +function Spinner:replace() | |
| 119 | + if self.notif then | |
| 120 | + local replace_id = type(self.notif) == "table" and self.notif.id or self.notif | |
| 121 | + assert(replace_id, "Unknown notification format, please open an issue.") | |
| 122 | + self:extend({ replace = replace_id, id = replace_id }) | |
| 123 | + end | |
| 124 | + | |
| 125 | + local msg = self.msg | |
| 126 | + if self.timer:is_active() then | |
| 127 | + msg = msg .. "…" | |
| 128 | + end | |
| 129 | + | |
| 130 | + self.notif = vim.notify(msg, self.lvl, self.opts) | |
| 98 | 131 | end |
| 99 | 132 | |
| 100 | -function spinner:start() | |
| 101 | - self:spin() | |
| 102 | - return self | |
| 133 | +---@private | |
| 134 | +function Spinner:reset_loop() | |
| 135 | + if self.timer:is_active() then | |
| 136 | + self.timer:stop() | |
| 137 | + end | |
| 138 | + self:loop() | |
| 103 | 139 | end |
| 104 | 140 | |
| 105 | ----@param msg? string | |
| 106 | ----@param success? boolean | |
| 107 | ----@param opts? table | |
| 108 | -function spinner:stop(msg, success, opts) | |
| 109 | - success = success == nil and true or success | |
| 141 | +---@param spinner_type lc.spinner_types | |
| 142 | +function Spinner:use(spinner_type) | |
| 143 | + if self.stype == spinner_type then | |
| 144 | + return | |
| 145 | + end | |
| 146 | + assert(spinners[spinner_type], "Unknown spinner type: " .. spinner_type) | |
| 110 | 147 | |
| 111 | - opts = vim.tbl_deep_extend("force", { | |
| 112 | - icon = success and "" or "", | |
| 113 | - timeout = 1500, | |
| 114 | - }, opts or {}) | |
| 148 | + self.index = 0 | |
| 149 | + self.stype = spinner_type | |
| 150 | + self:reset_loop() | |
| 151 | +end | |
| 115 | 152 | |
| 116 | - local lvl = vim.log.levels[success and "INFO" or "ERROR"] | |
| 153 | +---@private | |
| 154 | +function Spinner:loop() | |
| 155 | + local fps = math.floor(1000 / spinners[self.stype].fps) | |
| 156 | + | |
| 157 | + local function update_spinner() | |
| 158 | + self.index = (self.index + 1) % #spinners[self.stype].frames | |
| 159 | + self:set_icon(spinners[self.stype].frames[self.index + 1]) | |
| 160 | + self:replace() | |
| 161 | + end | |
| 117 | 162 | |
| 118 | - self:set(msg, lvl, opts) | |
| 119 | - self.spinner = nil | |
| 163 | + self.timer:start(0, fps, vim.schedule_wrap(update_spinner)) | |
| 120 | 164 | end |
| 121 | 165 | |
| 122 | 166 | ---@param msg? string |
| 123 | 167 | ---@param spinner_type? lc.spinner_types |
| 124 | -function spinner:init(msg, spinner_type) | |
| 168 | +function Spinner:start(msg, spinner_type) | |
| 169 | + local opts = { | |
| 170 | + hide_from_history = true, | |
| 171 | + history = false, | |
| 172 | + title = config.name, | |
| 173 | + timeout = false, | |
| 174 | + } | |
| 175 | + | |
| 125 | 176 | self = setmetatable({ |
| 126 | 177 | index = 0, |
| 127 | - spinner = spinners[spinner_type or "dot"], | |
| 178 | + timer = vim.loop.new_timer(), | |
| 179 | + msg = msg, | |
| 180 | + stype = spinner_type or "dot", | |
| 181 | + lvl = lvls.INFO, | |
| 182 | + opts = opts, | |
| 128 | 183 | }, self) |
| 129 | 184 | |
| 130 | - self:update(msg or "") | |
| 131 | - return self:start() | |
| 185 | + self:loop() | |
| 186 | + return self | |
| 132 | 187 | end |
| 133 | 188 | |
| 134 | -return spinner | |
| 189 | +return Spinner |
+6-6lua/leetcode/logger/spinner/judge.lua
| @@ -6,24 +6,24 @@ Judge.__index = Judge | ||
| 6 | 6 | setmetatable(Judge, Spinner) |
| 7 | 7 | |
| 8 | 8 | local check_state = { |
| 9 | - ["PENDING"] = "Pending…", | |
| 10 | - ["STARTED"] = "Judging…", | |
| 9 | + ["PENDING"] = "Pending", | |
| 10 | + ["STARTED"] = "Judging", | |
| 11 | 11 | ["SUCCESS"] = "Finished", |
| 12 | 12 | ["FAILURE"] = "Failed", -- CODE: 16 |
| 13 | 13 | } |
| 14 | 14 | |
| 15 | 15 | function Judge:from_state(state) |
| 16 | - self:update(check_state[state]) | |
| 16 | + self:soft_update(check_state[state]) | |
| 17 | 17 | |
| 18 | 18 | if state == "PENDING" then |
| 19 | - self:change("points") | |
| 19 | + self:use("points") | |
| 20 | 20 | else |
| 21 | - self:change("dot") | |
| 21 | + self:use("dot") | |
| 22 | 22 | end |
| 23 | 23 | end |
| 24 | 24 | |
| 25 | 25 | function Judge:init() |
| 26 | - local spinner = Spinner:init(check_state["PENDING"], "points") | |
| 26 | + local spinner = Spinner:start(check_state["PENDING"], "points") | |
| 27 | 27 | return setmetatable(spinner, Judge) |
| 28 | 28 | end |
| 29 | 29 |
+8-2lua/leetcode/runner/init.lua
| @@ -1,3 +1,5 @@ | ||
| 1 | +---@module 'plenary' | |
| 2 | + | |
| 1 | 3 | local log = require("leetcode.logger") |
| 2 | 4 | local interpreter = require("leetcode.api.interpreter") |
| 3 | 5 | local config = require("leetcode.config") |
| @@ -50,11 +52,15 @@ function Runner:handle(submit) | ||
| 50 | 52 | end |
| 51 | 53 | |
| 52 | 54 | if item then |
| 53 | - judge:stop(item.status_msg, item._.success) | |
| 55 | + if item._.success then | |
| 56 | + judge:success(item.status_msg) | |
| 57 | + else | |
| 58 | + judge:error(item.status_msg) | |
| 59 | + end | |
| 54 | 60 | elseif state then |
| 55 | 61 | judge:from_state(state) |
| 56 | 62 | elseif err then |
| 57 | - judge:stop(err.msg or "Something went wrong", false) | |
| 63 | + judge:error(err.msg or "Something went wrong") | |
| 58 | 64 | end |
| 59 | 65 | |
| 60 | 66 | if item then |