From 3251676d2529445e1c2e47ad7e42d50336f8bc25 Mon Sep 17 00:00:00 2001 From: Tuhin Mandal <73009536+MandalTuhin@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:35:56 +0530 Subject: [PATCH 1/8] Add shuffle funtionality to Leetcode.nvim I have made changes to the questions ui. --- lua/leetcode-ui/question.lua | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/lua/leetcode-ui/question.lua b/lua/leetcode-ui/question.lua index e8204746..d7debead 100644 --- a/lua/leetcode-ui/question.lua +++ b/lua/leetcode-ui/question.lua @@ -48,6 +48,39 @@ function Question:set_lines(code) vim.api.nvim_buf_set_lines(self.bufnr, s_i - 1, e_i, false, vim.split(code, "\n")) end +function Question:shuffle() + local api_question = require("leetcode.api.question") + local problems = require("leetcode.cache.problemlist") + + -- Fetch a new random question + local q, err = api_question.random() + if err then + log.err(err) + return + end + + -- Update self fields + self.q = q + self.cache = problems.get_by_title_slug(q.title_slug) or {} + + -- Overwrite buffer contents + self:set_lines(self:snippet(true)) + + -- (Optionally update info, description, etc. if needed) + if self.description and self.description.update then + self.description:update(self) + end + if self.info and self.info.update then + self.info:update(self) + end + if self.console and self.console.update then + self.console:update(self) + end + + log.info("Shuffled to a new random question: " .. q.title) +end + + function Question:reset_lines() local new_lines = self:snippet(true) or "" From 4a2fadcb56bc3b4db611077486bf25d1290794d4 Mon Sep 17 00:00:00 2001 From: Tuhin Mandal <73009536+MandalTuhin@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:42:20 +0530 Subject: [PATCH 2/8] Fix Shuffle function in leetcode.nvim ui This commit tries to fix the error shown by nvim when trying to shuffle questions. --- lua/leetcode-ui/question.lua | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/lua/leetcode-ui/question.lua b/lua/leetcode-ui/question.lua index d7debead..ce5f9f6a 100644 --- a/lua/leetcode-ui/question.lua +++ b/lua/leetcode-ui/question.lua @@ -52,13 +52,40 @@ function Question:shuffle() local api_question = require("leetcode.api.question") local problems = require("leetcode.cache.problemlist") - -- Fetch a new random question + -- Fetch a new random question (minimal info) local q, err = api_question.random() if err then log.err(err) return end + -- Get full question info by title_slug + local full_q = api_question.by_title_slug(q.title_slug) + if not full_q then + log.err("Failed to fetch full question info for: " .. (q.title_slug or "nil")) + return + end + + -- Update self fields + self.q = full_q + self.cache = problems.get_by_title_slug(q.title_slug) or {} + + -- Overwrite buffer contents + self:set_lines(self:snippet(true)) + + -- (Optionally update info, description, etc. if needed) + if self.description and self.description.update then + self.description:update(self) + end + if self.info and self.info.update then + self.info:update(self) + end + if self.console and self.console.update then + self.console:update(self) + end + + log.info("Shuffled to a new random question: " .. (full_q.title or q.title_slug or "unknown")) +end -- Update self fields self.q = q self.cache = problems.get_by_title_slug(q.title_slug) or {} From e03e11c294f146ff6b9b5e131c891c9ab579dbb5 Mon Sep 17 00:00:00 2001 From: Tuhin Mandal <73009536+MandalTuhin@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:47:42 +0530 Subject: [PATCH 3/8] Try adding the breaking function at the bottom of the file. This commit attempts to fix the random question functionality of leetcode.nvim. This puts the function at the bottom of the question.lua so that it doesn't interfere with other files. --- lua/leetcode-ui/question.lua | 101 ++++++++++++++--------------------- 1 file changed, 41 insertions(+), 60 deletions(-) diff --git a/lua/leetcode-ui/question.lua b/lua/leetcode-ui/question.lua index ce5f9f6a..d82d9215 100644 --- a/lua/leetcode-ui/question.lua +++ b/lua/leetcode-ui/question.lua @@ -48,66 +48,6 @@ function Question:set_lines(code) vim.api.nvim_buf_set_lines(self.bufnr, s_i - 1, e_i, false, vim.split(code, "\n")) end -function Question:shuffle() - local api_question = require("leetcode.api.question") - local problems = require("leetcode.cache.problemlist") - - -- Fetch a new random question (minimal info) - local q, err = api_question.random() - if err then - log.err(err) - return - end - - -- Get full question info by title_slug - local full_q = api_question.by_title_slug(q.title_slug) - if not full_q then - log.err("Failed to fetch full question info for: " .. (q.title_slug or "nil")) - return - end - - -- Update self fields - self.q = full_q - self.cache = problems.get_by_title_slug(q.title_slug) or {} - - -- Overwrite buffer contents - self:set_lines(self:snippet(true)) - - -- (Optionally update info, description, etc. if needed) - if self.description and self.description.update then - self.description:update(self) - end - if self.info and self.info.update then - self.info:update(self) - end - if self.console and self.console.update then - self.console:update(self) - end - - log.info("Shuffled to a new random question: " .. (full_q.title or q.title_slug or "unknown")) -end - -- Update self fields - self.q = q - self.cache = problems.get_by_title_slug(q.title_slug) or {} - - -- Overwrite buffer contents - self:set_lines(self:snippet(true)) - - -- (Optionally update info, description, etc. if needed) - if self.description and self.description.update then - self.description:update(self) - end - if self.info and self.info.update then - self.info:update(self) - end - if self.console and self.console.update then - self.console:update(self) - end - - log.info("Shuffled to a new random question: " .. q.title) -end - - function Question:reset_lines() local new_lines = self:snippet(true) or "" @@ -379,6 +319,47 @@ function Question:init(problem) self.lang = config.lang end +function Question:shuffle() + local api_question = require("leetcode.api.question") + local problems = require("leetcode.cache.problemlist") + + -- Fetch a new random question (minimal info) + local q, err = api_question.random() + if err then + log.err(err) + return + end + + -- Get full question info by title_slug + local full_q = api_question.by_title_slug(q.title_slug) + if not full_q then + log.err("Failed to fetch full question info for: " .. (q.title_slug or "nil")) + return + end + + -- Update self fields + self.q = full_q + self.cache = problems.get_by_title_slug(q.title_slug) or {} + + -- Overwrite buffer contents + self:set_lines(self:snippet(true)) + + -- (Optionally update info, description, etc. if needed) + if self.description and self.description.update then + self.description:update(self) + end + if self.info and self.info.update then + self.info:update(self) + end + if self.console and self.console.update then + self.console:update(self) + end + + log.info("Shuffled to a new random question: " .. (full_q.title or q.title_slug or "unknown")) +end + + + ---@type fun(question: lc.cache.Question): lc.ui.Question local LeetQuestion = Question From 5855617fc4201b2c00d4e120cfdbb319c7c325f8 Mon Sep 17 00:00:00 2001 From: Tuhin Mandal <73009536+MandalTuhin@users.noreply.github.com> Date: Mon, 23 Jun 2025 21:14:25 +0530 Subject: [PATCH 4/8] fix: try to reload the question description panel with each suffle. This commit tries to reload the description buffer/panel with each shuffle to correctly represent the problem we are currently in. --- lua/leetcode-ui/question.lua | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/lua/leetcode-ui/question.lua b/lua/leetcode-ui/question.lua index d82d9215..a631aa6e 100644 --- a/lua/leetcode-ui/question.lua +++ b/lua/leetcode-ui/question.lua @@ -319,6 +319,8 @@ function Question:init(problem) self.lang = config.lang end +-- start of shuffle functionality. + function Question:shuffle() local api_question = require("leetcode.api.question") local problems = require("leetcode.cache.problemlist") @@ -344,21 +346,23 @@ function Question:shuffle() -- Overwrite buffer contents self:set_lines(self:snippet(true)) - -- (Optionally update info, description, etc. if needed) - if self.description and self.description.update then - self.description:update(self) - end - if self.info and self.info.update then - self.info:update(self) - end - if self.console and self.console.update then - self.console:update(self) - end + -- Unmount and remount description, info, console + if self.description and self.description.unmount then self.description:unmount() end + if self.info and self.info.unmount then self.info:unmount() end + if self.console and self.console.unmount then self.console:unmount() end + + -- Recreate description, info, console for the new question + local Description = require("leetcode-ui.split.description") + local Console = require("leetcode-ui.layout.console") + local Info = require("leetcode-ui.popup.info") + self.description = Description(self):mount() + self.console = Console(self) + self.info = Info(self) log.info("Shuffled to a new random question: " .. (full_q.title or q.title_slug or "unknown")) end - +-- end of shuffle functionality. ---@type fun(question: lc.cache.Question): lc.ui.Question local LeetQuestion = Question From 963c22299dc19e5dfb7eee7406480382ddbf6ba1 Mon Sep 17 00:00:00 2001 From: Tuhin Mandal <73009536+MandalTuhin@users.noreply.github.com> Date: Thu, 24 Jul 2025 12:35:36 +0530 Subject: [PATCH 5/8] feat(question): implement question shuffle functionality Adds a new `:Leet shuffle` command that allows users to quickly load a new random problem in the current buffer. This feature mimics the behavior of the shuffle button on the LeetCode website, providing a seamless way to practice different problems without navigating back to the main menu or opening a new tab. The implementation includes: - A new `Question:shuffle()` method to handle fetching the new problem, updating the buffer state, and recreating the UI. - Correct buffer file path management (`nvim_buf_set_name`) to ensure state consistency and prevent saving work to the wrong file. - Proper unmounting and remounting of UI components (Description, Info, Console). fix(ui): resolve memory leak in console unmount Additionally, this commit fixes a memory leak in the console layout. The `unmount` function was incorrectly creating new UI popups instead of destroying the existing ones. This has been corrected to prevent orphaned windows from accumulating during operations like shuffling. --- lua/leetcode-ui/layout/console.lua | 12 +++++++----- lua/leetcode-ui/question.lua | 25 +++++++++++++------------ lua/leetcode/command/init.lua | 9 +++++++++ 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/lua/leetcode-ui/layout/console.lua b/lua/leetcode-ui/layout/console.lua index b7e88c01..f1619d2d 100644 --- a/lua/leetcode-ui/layout/console.lua +++ b/lua/leetcode-ui/layout/console.lua @@ -17,11 +17,13 @@ local log = require("leetcode.logger") local ConsoleLayout = Layout:extend("LeetConsoleLayout") function ConsoleLayout:unmount() -- - ConsoleLayout.super.unmount(self) - - self.testcase = Testcase(self) - self.result = Result(self) - self.popups = { self.testcase, self.result } + if self.testcase and self.testcase.unmount then + self.testcase:unmount() + end + if self.result and self.result.unmount then + self.result:unmount() + end + ConsoleLayout.super.unmount(self) end function ConsoleLayout:hide() diff --git a/lua/leetcode-ui/question.lua b/lua/leetcode-ui/question.lua index b99ab6cd..911285c0 100644 --- a/lua/leetcode-ui/question.lua +++ b/lua/leetcode-ui/question.lua @@ -472,26 +472,27 @@ function Question:shuffle() -- Get full question info by title_slug local full_q = api_question.by_title_slug(q.title_slug) if not full_q then - log.err("Failed to fetch full question info for: " .. (q.title_slug or "nil")) - return + return log.err("Failed to fetch full question info for: " .. (q.title_slug or "nil")) end -- Update self fields self.q = full_q self.cache = problems.get_by_title_slug(q.title_slug) or {} - -- Overwrite buffer contents - self:set_lines(self:snippet(true)) + -- Update buffer path and name. This also writes the file if it doesn't exist. + local new_path, existed = self:path() + vim.api.nvim_buf_set_name(self.bufnr, new_path) + + -- Overwrite buffer contents with new snippet and apply settings + self:editor_reset() + self:open_buffer(existed) - -- Unmount and remount description, info, console - if self.description and self.description.unmount then self.description:unmount() end - if self.info and self.info.unmount then self.info:unmount() end - if self.console and self.console.unmount then self.console:unmount() end + -- Unmount old UI components. The console unmount was fixed to prevent leaks. + self.description:unmount() + self.info:unmount() + self.console:unmount() - -- Recreate description, info, console for the new question - local Description = require("leetcode-ui.split.description") - local Console = require("leetcode-ui.layout.console") - local Info = require("leetcode-ui.popup.info") + -- Recreate UI components for the new question self.description = Description(self):mount() self.console = Console(self) self.info = Info(self) diff --git a/lua/leetcode/command/init.lua b/lua/leetcode/command/init.lua index 11824456..421bd7b6 100644 --- a/lua/leetcode/command/init.lua +++ b/lua/leetcode/command/init.lua @@ -277,6 +277,14 @@ function cmd.q_submit() end end +function cmd.q_shuffle() + local utils = require("leetcode.utils") + local q = utils.curr_question() + if q then + q:shuffle() + end +end + function cmd.ui_skills() if config.is_cn then return @@ -611,6 +619,7 @@ cmd.commands = { run = { cmd.q_run }, test = { cmd.q_run }, submit = { cmd.q_submit }, + shuffle = { cmd.q_shuffle }, daily = { cmd.qot }, yank = { cmd.yank }, open = { cmd.open }, From bed9ae962a8dd42a648edb5e8315ca0d13da0019 Mon Sep 17 00:00:00 2001 From: Tuhin Mandal <73009536+MandalTuhin@users.noreply.github.com> Date: Thu, 24 Jul 2025 14:17:53 +0530 Subject: [PATCH 6/8] fix(shuffle): resolve command parsing and LSP errors This commit addresses two follow-up issues discovered after the initial implementation of the question shuffle feature: 1. The `:Leet shuffle` command was not working due to a bug in the command parser that incorrectly handled single-word commands. The parser has been made more robust to correctly execute the command. 2. Shuffling a question caused LSP errors (e.g., from `clangd`) because the language server would not recognize the buffer's new file path. A `BufRead` autocommand is now triggered after shuffling to force the LSP to re-synchronize with the buffer. --- lua/leetcode-ui/question.lua | 6 ++++++ lua/leetcode/command/init.lua | 32 +++++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/lua/leetcode-ui/question.lua b/lua/leetcode-ui/question.lua index 911285c0..7ac91d22 100644 --- a/lua/leetcode-ui/question.lua +++ b/lua/leetcode-ui/question.lua @@ -497,6 +497,12 @@ function Question:shuffle() self.console = Console(self) self.info = Info(self) + -- HACK: Force LSP client to re-attach to the buffer after renaming it. + -- This prevents errors like "trying to get AST for non-added document". + if #vim.lsp.get_active_clients({ bufnr = self.bufnr }) > 0 then + vim.cmd.doautocmd("BufRead") + end + log.info("Shuffled to a new random question: " .. (full_q.title or q.title_slug or "unknown")) end diff --git a/lua/leetcode/command/init.lua b/lua/leetcode/command/init.lua index 421bd7b6..22aad699 100644 --- a/lua/leetcode/command/init.lua +++ b/lua/leetcode/command/init.lua @@ -574,25 +574,39 @@ function cmd.rec_complete(args, options, cmds) end function cmd.exec(args) + local input_args = vim.split(args.args, "%s+", { trimempty = true }) local cmds = cmd.commands + local arg_idx = 1 + + -- Find the command by consuming parts of the input + while arg_idx <= #input_args do + local current_part = input_args[arg_idx]:lower() + if not string.find(current_part, "=") and cmds[current_part] then + cmds = cmds[current_part] + arg_idx = arg_idx + 1 + else + -- Stop when we hit an option (like status=ac) or an unknown subcommand + break + end + end + -- The rest are options local options = vim.empty_dict() - for s in vim.gsplit(args.args:lower(), "%s+", { trimempty = true }) do - local opt = vim.split(s, "=") - + for i = arg_idx, #input_args do + local opt = vim.split(input_args[i], "=") if opt[2] then - options[opt[1]] = vim.split(opt[2], ",", { trimempty = true }) - elseif cmds then - cmds = cmds[s] + options[opt[1]:lower()] = vim.split(opt[2], ",", { trimempty = true }) else - break + -- This part is not a valid option, and wasn't a valid subcommand + return log.error(("Invalid command or argument: `%s`"):format(input_args[i])) end end if cmds and type(cmds[1]) == "function" then - cmds[1](options) ---@diagnostic disable-line + cmds1 else - log.error(("Invalid command: `%s %s`"):format(args.name, args.args)) + -- This happens if the command was empty or only contained unknown subcommands + return log.error(("Invalid command: `:Leet %s`"):format(args.args)) end end From f9ac0dcdacec4440a0780941550a902ff725ebe2 Mon Sep 17 00:00:00 2001 From: Tuhin Mandal <73009536+MandalTuhin@users.noreply.github.com> Date: Thu, 24 Jul 2025 14:27:11 +0530 Subject: [PATCH 7/8] fix(lsp): ensure stable shuffle by restarting LSP client This commit provides a definitive fix for LSP errors that occurred during a question shuffle. Previously, language servers like `clangd` could enter a confused state, resulting in "non-added document" errors. The root cause was that the LSP client was not properly detached before the buffer's underlying file path was changed. The new implementation resolves this by: - Explicitly stopping the active LSP client before any modifications. - Re-triggering LSP attachment via `BufRead` only after the shuffle is complete. This ensures a clean lifecycle for the LSP session, making the shuffle feature stable and error-free. q --- lua/leetcode-ui/question.lua | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lua/leetcode-ui/question.lua b/lua/leetcode-ui/question.lua index 7ac91d22..22623da7 100644 --- a/lua/leetcode-ui/question.lua +++ b/lua/leetcode-ui/question.lua @@ -479,6 +479,15 @@ function Question:shuffle() self.q = full_q self.cache = problems.get_by_title_slug(q.title_slug) or {} + -- Detach LSP clients before renaming the buffer to prevent errors. + -- The clients will be re-attached later. + local clients = vim.lsp.get_active_clients({ bufnr = self.bufnr }) + if #clients > 0 then + for _, client in ipairs(clients) do + vim.lsp.stop_client(client.id) + end + end + -- Update buffer path and name. This also writes the file if it doesn't exist. local new_path, existed = self:path() vim.api.nvim_buf_set_name(self.bufnr, new_path) @@ -497,9 +506,8 @@ function Question:shuffle() self.console = Console(self) self.info = Info(self) - -- HACK: Force LSP client to re-attach to the buffer after renaming it. - -- This prevents errors like "trying to get AST for non-added document". - if #vim.lsp.get_active_clients({ bufnr = self.bufnr }) > 0 then + -- Re-attach LSP clients by triggering the autocommands that lspconfig uses. + if #clients > 0 then vim.cmd.doautocmd("BufRead") end From c55ffc070670229e8b6bceb97e0bf10c5aa89183 Mon Sep 17 00:00:00 2001 From: Tuhin Mandal <73009536+MandalTuhin@users.noreply.github.com> Date: Thu, 24 Jul 2025 14:33:35 +0530 Subject: [PATCH 8/8] fix(command): correct typo in command executor A typo (`cmds1` instead of `cmds[1]`) was introduced in the command execution logic, causing a Lua error on startup and preventing the plugin from loading. This commit corrects the typo, restoring the plugin's functionality. --- lua/leetcode/command/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/leetcode/command/init.lua b/lua/leetcode/command/init.lua index 22aad699..cab463ec 100644 --- a/lua/leetcode/command/init.lua +++ b/lua/leetcode/command/init.lua @@ -603,7 +603,7 @@ function cmd.exec(args) end if cmds and type(cmds[1]) == "function" then - cmds1 + cmds[1](options) else -- This happens if the command was empty or only contained unknown subcommands return log.error(("Invalid command: `:Leet %s`"):format(args.args))