From 6a59fe42ed74dc0f35d1c62fca2dd50c11df57c8 Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Mon, 22 Sep 2025 23:19:51 -0700 Subject: [PATCH 01/24] feat: submit hook for capture both successful, failed submissions --- lua/leetcode/config/template.lua | 4 ++++ lua/leetcode/runner/init.lua | 2 ++ 2 files changed, 6 insertions(+) diff --git a/lua/leetcode/config/template.lua b/lua/leetcode/config/template.lua index 3d7c5c74..705237ce 100644 --- a/lua/leetcode/config/template.lua +++ b/lua/leetcode/config/template.lua @@ -24,6 +24,7 @@ ---| "enter" ---| "question_enter" ---| "leave" +---| "submit" ---@alias lc.size ---| string @@ -120,6 +121,9 @@ local M = { ---@type fun()[] ["leave"] = {}, + + ---@type fun(question: lc.ui.Question, buffer: string, status_msg: string|nil, success: string|nil)[] + ["submit"] = {}, }, keys = { diff --git a/lua/leetcode/runner/init.lua b/lua/leetcode/runner/init.lua index 69fd5438..12722200 100644 --- a/lua/leetcode/runner/init.lua +++ b/lua/leetcode/runner/init.lua @@ -4,6 +4,7 @@ local log = require("leetcode.logger") local interpreter = require("leetcode.api.interpreter") local config = require("leetcode.config") local Judge = require("leetcode.logger.spinner.judge") +local utils = require("leetcode.utils") ---@type Path local leetbody = config.storage.cache:joinpath("body") @@ -52,6 +53,7 @@ function Runner:handle(submit) end if item then + utils.exec_hooks("submit", question, body.typed_code, item.status_msg, item._.success) if item._.success then judge:success(item.status_msg) else From 606ac3d38d8dc29c34de49bdf7880c70e7f3d73b Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Sun, 7 Dec 2025 14:30:49 -0800 Subject: [PATCH 02/24] fix: bundle entire item into the submit hook and delegate logic outside --- lua/leetcode/config/template.lua | 2 +- lua/leetcode/runner/init.lua | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lua/leetcode/config/template.lua b/lua/leetcode/config/template.lua index 705237ce..82230441 100644 --- a/lua/leetcode/config/template.lua +++ b/lua/leetcode/config/template.lua @@ -122,7 +122,7 @@ local M = { ---@type fun()[] ["leave"] = {}, - ---@type fun(question: lc.ui.Question, buffer: string, status_msg: string|nil, success: string|nil)[] + ---@type fun(question: lc.ui.Question, buffer: string, item_json: table)[] ["submit"] = {}, }, diff --git a/lua/leetcode/runner/init.lua b/lua/leetcode/runner/init.lua index 12722200..e61a89ad 100644 --- a/lua/leetcode/runner/init.lua +++ b/lua/leetcode/runner/init.lua @@ -53,7 +53,10 @@ function Runner:handle(submit) end if item then - utils.exec_hooks("submit", question, body.typed_code, item.status_msg, item._.success) + -- print(vim.inspect(item)) + -- utils.exec_hooks("submit", question, body.typed_code, item.status_msg, item._.success) + utils.exec_hooks("submit", question, body.typed_code, item) + if item._.success then judge:success(item.status_msg) else From ec9c0a734aa541a865148f205871f36cf654ce9a Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Mon, 23 Feb 2026 00:15:15 -0800 Subject: [PATCH 03/24] feat: add upload commands for test and submit results with hook triggering --- lua/leetcode-ui/layout/console.lua | 4 ++-- lua/leetcode/command/init.lua | 20 ++++++++++++++++++++ lua/leetcode/runner/init.lua | 11 +++++++---- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/lua/leetcode-ui/layout/console.lua b/lua/leetcode-ui/layout/console.lua index b7e88c01..92cab6e4 100644 --- a/lua/leetcode-ui/layout/console.lua +++ b/lua/leetcode-ui/layout/console.lua @@ -59,7 +59,7 @@ function ConsoleLayout:mount() }) end -function ConsoleLayout:run(submit) +function ConsoleLayout:run(submit, trigger_hook) local range = self.question:editor_section_range("code") if not range:is_valid_or_log() then return @@ -71,7 +71,7 @@ function ConsoleLayout:run(submit) self.result:focus() - Runner:init(self.question):run(submit) + Runner:init(self.question):run(submit, trigger_hook) end function ConsoleLayout:use_testcase() diff --git a/lua/leetcode/command/init.lua b/lua/leetcode/command/init.lua index 11824456..88fc11e1 100644 --- a/lua/leetcode/command/init.lua +++ b/lua/leetcode/command/init.lua @@ -277,6 +277,24 @@ function cmd.q_submit() end end +function cmd.q_upload_test_result() + local utils = require("leetcode.utils") + utils.auth_guard() + local q = utils.curr_question() + if q then + q.console:run(false, true) -- test flow but trigger hook + end +end + +function cmd.q_upload_submit_result() + local utils = require("leetcode.utils") + utils.auth_guard() + local q = utils.curr_question() + if q then + q.console:run(true, true) -- submit flow and trigger hook + end +end + function cmd.ui_skills() if config.is_cn then return @@ -610,7 +628,9 @@ cmd.commands = { lang = { cmd.change_lang }, run = { cmd.q_run }, test = { cmd.q_run }, + upload_test_result = { cmd.q_upload_test_result }, submit = { cmd.q_submit }, + upload_submit_result = { cmd.q_upload_submit_result }, daily = { cmd.qot }, yank = { cmd.yank }, open = { cmd.open }, diff --git a/lua/leetcode/runner/init.lua b/lua/leetcode/runner/init.lua index e61a89ad..170e503b 100644 --- a/lua/leetcode/runner/init.lua +++ b/lua/leetcode/runner/init.lua @@ -19,12 +19,13 @@ Runner.running = false ---@param self lc.Runner ---@param submit boolean -Runner.run = vim.schedule_wrap(function(self, submit) +---@param trigger_hook? boolean +Runner.run = vim.schedule_wrap(function(self, submit, trigger_hook) if Runner.running then return log.warn("Runner is busy") end - local ok, err = pcall(Runner.handle, self, submit) + local ok, err = pcall(Runner.handle, self, submit, trigger_hook) if not ok then self:stop() log.error(err) @@ -35,7 +36,7 @@ Runner.stop = function() Runner.running = false end -function Runner:handle(submit) +function Runner:handle(submit, trigger_hook) Runner.running = true local question = self.question @@ -55,7 +56,9 @@ function Runner:handle(submit) if item then -- print(vim.inspect(item)) -- utils.exec_hooks("submit", question, body.typed_code, item.status_msg, item._.success) - utils.exec_hooks("submit", question, body.typed_code, item) + if trigger_hook then + utils.exec_hooks("submit", question, body.typed_code, item) + end if item._.success then judge:success(item.status_msg) From c77b282c4ee425ec57e5a9b3ff2effd5810015a9 Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Mon, 23 Feb 2026 01:09:37 -0800 Subject: [PATCH 04/24] feat: implement timer functionality with hooks for question sessions --- lua/leetcode-ui/question.lua | 42 ++++++++++++++++++++++++++++++++ lua/leetcode/command/init.lua | 24 ++++++++++++++++++ lua/leetcode/config/hooks.lua | 8 ++++++ lua/leetcode/config/template.lua | 8 +++++- lua/leetcode/runner/init.lua | 4 +-- 5 files changed, 83 insertions(+), 3 deletions(-) diff --git a/lua/leetcode-ui/question.lua b/lua/leetcode-ui/question.lua index a72ccafb..b3fb97b4 100644 --- a/lua/leetcode-ui/question.lua +++ b/lua/leetcode-ui/question.lua @@ -270,6 +270,9 @@ function Question:_unmount() return end + self:stop_timer_display() + utils.exec_hooks("question_leave", self) + vim.schedule(function() self.info:unmount() self.console:unmount() @@ -304,6 +307,45 @@ function Question:autocmds() }) end +function Question:start_timer_display() + self:stop_timer_display() -- reset if already running + + local start_ms = vim.loop.hrtime() / 1e6 -- milliseconds + + local function update_winbar() + local elapsed_s = math.floor((vim.loop.hrtime() / 1e6 - start_ms) / 1000) + local mins = math.floor(elapsed_s / 60) + local secs = elapsed_s % 60 + local label = string.format(" ⏱ %02d:%02d", mins, secs) + vim.schedule(function() + if not (self.winid and vim.api.nvim_win_is_valid(self.winid)) then + self:stop_timer_display() + return + end + vim.api.nvim_set_option_value("winbar", label, { win = self.winid }) + end) + end + + update_winbar() -- render immediately + self._session_timer = vim.loop.new_timer() + self._session_timer:start(1000, 1000, update_winbar) +end + +function Question:stop_timer_display() + if self._session_timer then + self._session_timer:stop() + if not self._session_timer:is_closing() then + self._session_timer:close() + end + self._session_timer = nil + end + vim.schedule(function() + if self.winid and vim.api.nvim_win_is_valid(self.winid) then + vim.api.nvim_set_option_value("winbar", "", { win = self.winid }) + end + end) +end + function Question:handle_mount() self:create_buffer() diff --git a/lua/leetcode/command/init.lua b/lua/leetcode/command/init.lua index 88fc11e1..ee1c84d6 100644 --- a/lua/leetcode/command/init.lua +++ b/lua/leetcode/command/init.lua @@ -277,6 +277,24 @@ function cmd.q_submit() end end +function cmd.timer_start() + local utils = require("leetcode.utils") + local q = utils.curr_question() + if not q then + return + end + utils.exec_hooks("timer_start", q) +end + +function cmd.timer_stop() + local utils = require("leetcode.utils") + local q = utils.curr_question() + if not q then + return + end + utils.exec_hooks("question_leave", q) +end + function cmd.q_upload_test_result() local utils = require("leetcode.utils") utils.auth_guard() @@ -631,6 +649,8 @@ cmd.commands = { upload_test_result = { cmd.q_upload_test_result }, submit = { cmd.q_submit }, upload_submit_result = { cmd.q_upload_submit_result }, + timer_start = { cmd.timer_start }, + timer_stop = { cmd.timer_stop }, daily = { cmd.qot }, yank = { cmd.yank }, open = { cmd.open }, @@ -668,6 +688,10 @@ cmd.commands = { update = { cmd.cookie_prompt }, delete = { cmd.sign_out }, }, + session = { + start = { cmd.timer_start }, + stop = { cmd.timer_stop }, + }, cache = { update = { cmd.cache_update }, }, diff --git a/lua/leetcode/config/hooks.lua b/lua/leetcode/config/hooks.lua index 05b4406e..0d818654 100644 --- a/lua/leetcode/config/hooks.lua +++ b/lua/leetcode/config/hooks.lua @@ -12,4 +12,12 @@ hooks["question_enter"] = { end, } +hooks["upload_submit_result"] = {} + +hooks["upload_test_result"] = {} + +hooks["timer_start"] = {} + +hooks["question_leave"] = {} + return hooks diff --git a/lua/leetcode/config/template.lua b/lua/leetcode/config/template.lua index 82230441..5b990439 100644 --- a/lua/leetcode/config/template.lua +++ b/lua/leetcode/config/template.lua @@ -23,8 +23,11 @@ ---@alias lc.hook ---| "enter" ---| "question_enter" +---| "question_leave" ---| "leave" ----| "submit" +---| "upload_submit_result" +---| "upload_test_result" +---| "timer_start" ---@alias lc.size ---| string @@ -119,6 +122,9 @@ local M = { ---@type fun(question: lc.ui.Question)[] ["question_enter"] = {}, + ---@type fun(question: lc.ui.Question)[] + ["question_leave"] = {}, + ---@type fun()[] ["leave"] = {}, diff --git a/lua/leetcode/runner/init.lua b/lua/leetcode/runner/init.lua index 170e503b..ebb9fba3 100644 --- a/lua/leetcode/runner/init.lua +++ b/lua/leetcode/runner/init.lua @@ -55,9 +55,9 @@ function Runner:handle(submit, trigger_hook) if item then -- print(vim.inspect(item)) - -- utils.exec_hooks("submit", question, body.typed_code, item.status_msg, item._.success) if trigger_hook then - utils.exec_hooks("submit", question, body.typed_code, item) + local hook_event = submit and "upload_submit_result" or "upload_test_result" + utils.exec_hooks(hook_event, question, body.typed_code, item) end if item._.success then From e2891f5296e3477910bc15e9c10ba64104f51c49 Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Mon, 23 Feb 2026 01:19:10 -0800 Subject: [PATCH 05/24] docs: update README to reflect extended features and new hooks for session management --- README.md | 604 +++++++++--------------------------------------------- 1 file changed, 102 insertions(+), 502 deletions(-) diff --git a/README.md b/README.md index efd575ca..6abb18c2 100644 --- a/README.md +++ b/README.md @@ -1,549 +1,149 @@
-# leetcode.nvim +# 🍴 leetcode.nvim (Extended Fork) -🔥 Solve [LeetCode] problems within [Neovim] 🔥 +**Extended hooks and external integration for [kawre/leetcode.nvim](https://github.com/kawre/leetcode.nvim)** - +The original project seems not be not actively maintained thats why i forked it +Track problems, time spent, and submissions via external services.
-https://github.com/kawre/leetcode.nvim/assets/69250723/aee6584c-e099-4409-b114-123cb32b7563 +## ✨ What's New -## ✨ Features +This fork adds submission tracking and session management capabilities to leetcode.nvim: -- 📌 an intuitive dashboard for effortless navigation within [leetcode.nvim] +- **Session Timer Commands**: `:Leet timer_start` / `:Leet timer_stop` +- **Extended Hooks**: `timer_start`, `question_leave`, and improved `upload_submit_result` / `upload_test_result` +- **External Integration**: Wire hooks to external APIs for persistence +- **Bug Fix**: Resolved `nvim_win_is_valid` fast event crash in timer display -- 😍 question description formatting for a better readability +## 🎯 New Hooks -- 📈 [LeetCode] profile statistics within [Neovim] +Beyond the original hooks, this fork adds: -- 🔀 support for daily and random questions +| Hook | Signature | When | +|------|-----------|------| +| `timer_start` | `fun(question: lc.ui.Question)` | Session timer started (`:Leet session start`) | +| `question_leave` | `fun(question: lc.ui.Question)` | Question window closed or session stopped | -- 💾 caching for optimized performance +Existing hooks also trigger on submissions: -## 📬 Requirements +| Hook | Signature | When | +|------|-----------|------| +| `upload_submit_result` | `fun(question, buffer, item_json)` | After `:Leet submit` | +| `upload_test_result` | `fun(question, buffer, item_json)` | After `:Leet run` | -- [Neovim] >= 0.9.0 +## 🛠️ New Commands -- [Picker](#picker) - -- [plenary.nvim] - -- [nui.nvim] - -- [tree-sitter-html] _**(optional, but highly recommended)**_ - used for formatting the question description. - Can be installed with [nvim-treesitter]. - -- [Nerd Font][nerd-font] & [nvim-web-devicons] _**(optional)**_ - -## 📦 Installation - -- [lazy.nvim] - -```lua -{ - "kawre/leetcode.nvim", - build = ":TSUpdate html", -- if you have `nvim-treesitter` installed - dependencies = { - -- include a picker of your choice, see picker section for more details - "nvim-lua/plenary.nvim", - "MunifTanjim/nui.nvim", - }, - opts = { - -- configuration goes here - }, -} +```vim +:Leet timer_start " Start tracking session time +:Leet timer_stop " Stop tracking (drop session without saving) +:Leet session start " Alias for timer_start +:Leet session stop " Alias for timer_stop ``` -## 🛠️ Configuration - -To see full configuration types see [template.lua](./lua/leetcode/config/template.lua) +## 📡 External Integration Example -### ⚙️ default configuration +Connect to a submission server to save data: ```lua -{ - ---@type string - arg = "leetcode.nvim", - - ---@type lc.lang - lang = "cpp", +-- ~/.config/nvim/init.lua or plugin spec +local db_saver = require("submission_db_saver") - cn = { -- leetcode.cn - enabled = false, ---@type boolean - translator = true, ---@type boolean - translate_problems = true, ---@type boolean - }, - - ---@type lc.storage - storage = { - home = vim.fn.stdpath("data") .. "/leetcode", - cache = vim.fn.stdpath("cache") .. "/leetcode", - }, - - ---@type table - plugins = { - non_standalone = false, - }, - - ---@type boolean - logging = true, - - injector = {}, ---@type table - - cache = { - update_interval = 60 * 60 * 24 * 7, ---@type integer 7 days - }, - - editor = { - reset_previous_code = true, ---@type boolean - fold_imports = true, ---@type boolean - }, - - console = { - open_on_runcode = true, ---@type boolean - - dir = "row", ---@type lc.direction - - size = { ---@type lc.size - width = "90%", - height = "75%", - }, - - result = { - size = "60%", ---@type lc.size - }, - - testcase = { - virt_text = true, ---@type boolean - - size = "40%", ---@type lc.size +return { + "kawre/leetcode.nvim", + opts = { + hooks = { + -- Track time on a problem + ["timer_start"] = { + function(question) + db_saver.timer_start(question) + question:start_timer_display() + end, + }, + -- Clean up timer on close + ["question_leave"] = { + function(question) + db_saver.drop_session(question) + end, + }, + -- Save test runs + ["upload_test_result"] = { + function(question, buffer, item) + db_saver.save_submission(question, buffer, item) + end, + }, + -- Save final submissions + ["upload_submit_result"] = { + function(question, buffer, item) + db_saver.save_submission(question, buffer, item) + end, + }, }, }, - - description = { - position = "left", ---@type lc.position - - width = "40%", ---@type lc.size - - show_stats = true, ---@type boolean - }, - - ---@type lc.picker - picker = { provider = nil }, - - hooks = { - ---@type fun()[] - ["enter"] = {}, - - ---@type fun(question: lc.ui.Question)[] - ["question_enter"] = {}, - - ---@type fun()[] - ["leave"] = {}, - }, - - keys = { - toggle = { "q" }, ---@type string|string[] - confirm = { "" }, ---@type string|string[] - - reset_testcases = "r", ---@type string - use_testcase = "U", ---@type string - focus_testcases = "H", ---@type string - focus_result = "L", ---@type string - }, - - ---@type lc.highlights - theme = {}, - - ---@type boolean - image_support = false, } ``` -### arg +## 🖥️ Server API -Argument for [Neovim] +Expected server endpoints: -```lua ----@type string -arg = "leetcode.nvim" ``` - -See [usage](#-usage) for more info - -### lang - -Language to start your session with - -```lua ----@type lc.lang -lang = "cpp" -``` - -
- available languages - -| Language | lang | -| ---------- | ---------- | -| C++ | cpp | -| Java | java | -| Python | python | -| Python3 | python3 | -| C | c | -| C# | csharp | -| JavaScript | javascript | -| TypeScript | typescript | -| PHP | php | -| Swift | swift | -| Kotlin | kotlin | -| Dart | dart | -| Go | golang | -| Ruby | ruby | -| Scala | scala | -| Rust | rust | -| Racket | racket | -| Erlang | erlang | -| Elixir | elixir | -| Bash | bash | - -
- -### cn - -Use [leetcode.cn] instead of [leetcode.com][leetcode] - -```lua -cn = { -- leetcode.cn - enabled = false, ---@type boolean - translator = true, ---@type boolean - translate_problems = true, ---@type boolean -}, -``` - -### storage - -storage directories - -```lua ----@type lc.storage -storage = { - home = vim.fn.stdpath("data") .. "/leetcode", - cache = vim.fn.stdpath("cache") .. "/leetcode", -}, -``` - -### plugins - -[plugins list](#-plugins) - -```lua ----@type table -plugins = { - non_standalone = false, -}, -``` - -### logging - -Whether to log [leetcode.nvim] status notifications - -```lua ----@type boolean -logging = true -``` - -### injector - -Inject code before or after your solution, injected code won't be submitted or run. - -Imports will be injected at the top of the buffer, automatically -folded by default. - -```lua -injector = { ---@type table - ["python3"] = { - imports = function(default_imports) - vim.list_extend(default_imports, { "from .leetcode import *" }) - return default_imports - end, - after = { "def test():", " print('test')" }, - }, - ["cpp"] = { - imports = function() - -- return a different list to omit default imports - return { "#include ", "using namespace std;" } - end, - after = "int main() {}", - }, -}, +POST /api/session +{ + "action": "start_timer" | "drop_timer" | "save_submission", + "title_slug": "two-sum", + "content": "...", // for save_submission + "item": {...} // for save_submission +} ``` -### picker - -Supported picker providers are: - -- [`snacks-picker`][snacks.nvim] -- [`fzf-lua`][fzf-lua] -- [`telescope`][telescope.nvim] -- [`mini-picker`][mini-picker] - -If `provider` is `nil`, [leetcode.nvim] will try to resolve the first -available one in the order above. - -```lua ----@type lc.picker -picker = { provider = nil }, +Server should respond with: +```json +{ + "success": true, + "action": "start_timer" +} ``` -### hooks - -List of functions that get executed on specified event +## 📝 Implementation Details -```lua -hooks = { - ---@type fun()[] - ["enter"] = {}, +### Files Modified - ---@type fun(question: lc.ui.Question)[] - ["question_enter"] = {}, +- `lua/leetcode/command/init.lua` - Added `timer_start` / `timer_stop` commands +- `lua/leetcode/config/hooks.lua` - Registered new hook events +- `lua/leetcode/config/template.lua` - Hook type aliases +- `lua/leetcode-ui/question.lua` - Fire `question_leave` on unmount, fixed timer fast event bug +- `packages/submission_server/src/submission_server.py` - Added `DROP_TIMER` action +- `packages/submission_server/src/submission_db_saver.lua` - Added `timer_start()` and `drop_session()` - ---@type fun()[] - ["leave"] = {}, -}, -``` +### Usage Pattern -### theme +1. User runs `:Leet session start` → `timer_start` hook fires → server starts session timer +2. User closes question or runs `:Leet session stop` → `question_leave` hook fires → server drops timer +3. User submits code → `upload_submit_result` hook fires → server saves submission +4. Time and submissions are persisted in external database -Override the [default theme](./lua/leetcode/theme/default.lua). +## 🔧 Installation -Each value is the same type as val parameter in `:help nvim_set_hl` +Use this fork instead of the original: ```lua ----@type lc.highlights -theme = { - ["alt"] = { - bg = "#FFFFFF", +{ + "jingyi-zhao-01/leetcode.nvim", -- or your fork + build = ":TSUpdate html", + dependencies = { + "nvim-lua/plenary.nvim", + "MunifTanjim/nui.nvim", }, - ["normal"] = { - fg = "#EA4AAA", + opts = { + -- See hooks example above }, -}, -``` - -### image support - -Whether to render question description images using [image.nvim] - -> [!WARNING] -> Enabling this will disable question description wrap, -> because of https://github.com/3rd/image.nvim/issues/62#issuecomment-1778082534 - -```lua ----@type boolean -image_support = false, -``` - -## 📋 Commands - -### `Leet` opens menu dashboard - -- `menu` same as `Leet` - -- `exit` close [leetcode.nvim] - -- `console` opens console pop-up for currently opened question - -- `info` opens a pop-up containing information about the currently opened question - -- `tabs` opens a picker with all currently opened question tabs - -- `yank` yanks the code section - -- `lang` opens a picker to change the language of the current question - -- `run` run currently opened question - -- `test` same as `Leet run` - -- `submit` submit currently opened question - -- `random` opens a random question - -- `daily` opens the question of today problem - -- `list` opens a picker with all available leetcode problems - -- `open` opens the current question in a default browser - -- `restore` try to restore default question layout - -- `last_submit` tries to replace the editor code section with the latest submitted code - -- `reset` resets editor code section to the default snippet - -- `inject` re-injects editor code, keeping the code section intact - -- `fold` applies folding to the current question imports section - - - - - - - -- `desc` toggle question description - - `toggle` same as `Leet desc` - - - `stats` toggle description stats visibility - -- `cookie` - - `update` opens a prompt to enter a new cookie - - - `delete` deletes stored cookie and logs out of [leetcode.nvim] - -- `cache` - - `update` fetches all available problems and updates the local cache of [leetcode.nvim] - -#### Some commands can take optional arguments. To stack argument values separate them by a `,` - -- `Leet list` - - ``` - Leet list status= difficulty= - ``` - -- `Leet random` - - ``` - Leet random status= difficulty= tags= - ``` - -## 🚀 Usage - -This plugin can be initiated in two ways: - -- To start [leetcode.nvim], simply pass [`arg`](#arg) - as the _first and **only**_ [Neovim] argument - - ``` - nvim leetcode.nvim - ``` - -- Use `:Leet` command to open [leetcode.nvim] - within your preferred dashboard plugin. The only requirement is that [Neovim] - must not have any listed buffers open. - To bypass this requirement use [`non_standalone`](#non-standalone-mode) plugin. - -### Sign In - -> [!WARNING] -> Be sure to copy the `Cookie` from request headers, not the `set-cookie` from -> response headers. - -> [!WARNING] -> If you are using **brave browser**, see [this -> issue](https://github.com/kawre/leetcode.nvim/issues/160#issuecomment-2619611920) - -https://github.com/kawre/leetcode.nvim/assets/69250723/b7be8b95-5e2c-4153-8845-4ad3abeda5c3 - -## ❓ FAQ - -### I keep getting `cookie expired` error - -If you keep getting `Your cookie may have expired, or LeetCode has temporarily -restricted API access`, it most likely means that LeetCode website is under -heavy load and is restricting API access (mostly during contests). - -All you can do is wait it out, try disabling a VPN if you’re using one, and if -the problem is persistent, open an issue. - -### Switching between test cases - -To switch between test cases, just press the number of the corresponding case: -`1` for `Case (1)`, `2` for `Case (2)`, and so on. - -### Switching between questions - -To switch between questions, use `Leet tabs` - -### I'm not getting LSP completions - -Some languages require additional setup to get LSP completions. -For example, Rust needs extra configuration — see [this issue](https://github.com/kawre/leetcode.nvim/issues/86). - -## 🍴 Recipes - -### 💤 lazy loading with [lazy.nvim] - -> [!WARNING] -> opting for either option makes the alternative -> launch method unavailable due to lazy loading - -- with [`arg`](#arg) - - ```lua - local leet_arg = "leetcode.nvim" - ``` - - ```lua - { - "kawre/leetcode.nvim", - lazy = leet_arg ~= vim.fn.argv(0, -1), - opts = { arg = leet_arg }, - } - ``` - -- with `:Leet` - - ```lua - { - "kawre/leetcode.nvim", - cmd = "Leet", - } - ``` - -### 🪟 Windows - -If you are using Windows, -it is recommended to use [Cygwin](https://www.cygwin.com/) for a more consistent and Unix-like experience. - -## 🧩 Plugins - -### Non-Standalone mode - -To run [leetcode.nvim] in a non-standalone mode (i.e. not with argument or an empty Neovim session), -enable the `non_standalone` plugin in your config: - -```lua -plugins = { - non_standalone = true, } ``` -You can then exit [leetcode.nvim] using `:Leet exit` command - -## 🙌 Credits - -- [Leetbuddy.nvim](https://github.com/Dhanus3133/Leetbuddy.nvim) - -- [alpha-nvim](https://github.com/goolord/alpha-nvim) - -[image.nvim]: https://github.com/3rd/image.nvim -[lazy.nvim]: https://github.com/folke/lazy.nvim -[leetcode]: https://leetcode.com -[leetcode.cn]: https://leetcode.cn -[leetcode.nvim]: https://github.com/kawre/leetcode.nvim -[neovim]: https://github.com/neovim/neovim -[nerd-font]: https://www.nerdfonts.com -[nui.nvim]: https://github.com/MunifTanjim/nui.nvim -[nvim-treesitter]: https://github.com/nvim-treesitter/nvim-treesitter -[nvim-web-devicons]: https://github.com/nvim-tree/nvim-web-devicons -[telescope.nvim]: https://github.com/nvim-telescope/telescope.nvim -[fzf-lua]: https://github.com/ibhagwan/fzf-lua -[snacks.nvim]: https://github.com/folke/snacks.nvim -[tree-sitter-html]: https://github.com/tree-sitter/tree-sitter-html -[plenary.nvim]: https://github.com/nvim-lua/plenary.nvim -[mini-picker]: https://github.com/nvim-mini/mini.pick +## 📚 Refer to Original + +For core leetcode.nvim features, installation, and configuration, see: +https://github.com/kawre/leetcode.nvim From 2eca73feffbd884cdfd8217440c1f43f07138622 Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Mon, 23 Feb 2026 01:28:10 -0800 Subject: [PATCH 06/24] - hightlight thought sytnax in the question buffer --- lua/leetcode-ui/question.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/leetcode-ui/question.lua b/lua/leetcode-ui/question.lua index b3fb97b4..a5fa5e34 100644 --- a/lua/leetcode-ui/question.lua +++ b/lua/leetcode-ui/question.lua @@ -135,7 +135,7 @@ function Question:open_buffer(existed) ui_utils.buf_set_opts(self.bufnr, { buflisted = true }) ui_utils.win_set_buf(self.winid, self.bufnr, true) - vim.cmd([[match DiagnosticHint /@leet/]]) + vim.cmd([[match DiagnosticHint /@leet\|@thought/]]) if config.user.editor.fold_imports then self:editor_fold_imports(false) From 28f2383a5a4bbfc7b2f62d392ff33a6f79248d30 Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Tue, 24 Feb 2026 20:35:08 -0800 Subject: [PATCH 07/24] feat: implement timer stop hook and update related commands --- lua/leetcode-ui/question.lua | 2 +- lua/leetcode/command/init.lua | 2 +- lua/leetcode/config/hooks.lua | 6 ++++++ lua/leetcode/config/template.lua | 1 + 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lua/leetcode-ui/question.lua b/lua/leetcode-ui/question.lua index a5fa5e34..327dfcf1 100644 --- a/lua/leetcode-ui/question.lua +++ b/lua/leetcode-ui/question.lua @@ -270,7 +270,7 @@ function Question:_unmount() return end - self:stop_timer_display() + utils.exec_hooks("timer_stop", self) utils.exec_hooks("question_leave", self) vim.schedule(function() diff --git a/lua/leetcode/command/init.lua b/lua/leetcode/command/init.lua index ee1c84d6..99144c54 100644 --- a/lua/leetcode/command/init.lua +++ b/lua/leetcode/command/init.lua @@ -292,7 +292,7 @@ function cmd.timer_stop() if not q then return end - utils.exec_hooks("question_leave", q) + utils.exec_hooks("timer_stop", q) end function cmd.q_upload_test_result() diff --git a/lua/leetcode/config/hooks.lua b/lua/leetcode/config/hooks.lua index 0d818654..5139c1b5 100644 --- a/lua/leetcode/config/hooks.lua +++ b/lua/leetcode/config/hooks.lua @@ -18,6 +18,12 @@ hooks["upload_test_result"] = {} hooks["timer_start"] = {} +hooks["timer_stop"] = { + function(q) + q:stop_timer_display() + end, +} + hooks["question_leave"] = {} return hooks diff --git a/lua/leetcode/config/template.lua b/lua/leetcode/config/template.lua index 5b990439..35b37b7f 100644 --- a/lua/leetcode/config/template.lua +++ b/lua/leetcode/config/template.lua @@ -28,6 +28,7 @@ ---| "upload_submit_result" ---| "upload_test_result" ---| "timer_start" +---| "timer_stop" ---@alias lc.size ---| string From a9def5aa10274a68f5d06a82dae6f869a3eec8a8 Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Wed, 25 Feb 2026 00:56:05 -0800 Subject: [PATCH 08/24] - feat: add timer color --- lua/leetcode-ui/question.lua | 2 +- lua/leetcode/theme/default.lua | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/leetcode-ui/question.lua b/lua/leetcode-ui/question.lua index 327dfcf1..5fd9abee 100644 --- a/lua/leetcode-ui/question.lua +++ b/lua/leetcode-ui/question.lua @@ -316,7 +316,7 @@ function Question:start_timer_display() local elapsed_s = math.floor((vim.loop.hrtime() / 1e6 - start_ms) / 1000) local mins = math.floor(elapsed_s / 60) local secs = elapsed_s % 60 - local label = string.format(" ⏱ %02d:%02d", mins, secs) + local label = string.format(" %%#leetcode_timer#⏱ %02d:%02d%%*", mins, secs) vim.schedule(function() if not (self.winid and vim.api.nvim_win_is_valid(self.winid)) then self:stop_timer_display() diff --git a/lua/leetcode/theme/default.lua b/lua/leetcode/theme/default.lua index 5eb22761..942d40e9 100644 --- a/lua/leetcode/theme/default.lua +++ b/lua/leetcode/theme/default.lua @@ -32,6 +32,7 @@ M.get = function() easy = { fg = "#46c6c2" }, medium = { fg = "#fac31d" }, hard = { fg = "#f8615c" }, + timer = { fg = "#f8615c" }, easy_alt = { fg = "#294d35" }, medium_alt = { fg = "#5e4e25" }, From 551343301036278cdf7d2a9707baa91acf43cc01 Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Mon, 25 May 2026 00:51:43 -0700 Subject: [PATCH 09/24] feat: implement leave hooks for timer management and enhance hook execution --- lua/leetcode-ui/question.lua | 33 ++++++++++++++++++++++++++++++--- lua/leetcode/config/hooks.lua | 17 +++++++++++++++-- lua/leetcode/utils.lua | 2 ++ 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/lua/leetcode-ui/question.lua b/lua/leetcode-ui/question.lua index 5fd9abee..28456c48 100644 --- a/lua/leetcode-ui/question.lua +++ b/lua/leetcode-ui/question.lua @@ -266,13 +266,12 @@ function Question:injector(code) end function Question:_unmount() + self:run_leave_hooks() + if vim.v.dying ~= 0 then return end - utils.exec_hooks("timer_stop", self) - utils.exec_hooks("question_leave", self) - vim.schedule(function() self.info:unmount() self.console:unmount() @@ -297,6 +296,17 @@ function Question:unmount() end local group = vim.api.nvim_create_augroup("leetcode_questions", { clear = true }) + +function Question:run_leave_hooks() + if self._leave_hooks_ran then + return + end + + self._leave_hooks_ran = true + utils.exec_hooks("timer_stop", self) + utils.exec_hooks("question_leave", self) +end + function Question:autocmds() vim.api.nvim_create_autocmd("WinClosed", { group = group, @@ -305,6 +315,23 @@ function Question:autocmds() self:_unmount() end, }) + + vim.api.nvim_create_autocmd("TabLeave", { + group = group, + callback = function() + local tabp = utils.question_tabp(self) + if tabp and tabp == vim.api.nvim_get_current_tabpage() and self._session_timer then + utils.exec_hooks("timer_stop", self) + end + end, + }) + + vim.api.nvim_create_autocmd("VimLeavePre", { + group = group, + callback = function() + self:run_leave_hooks() + end, + }) end function Question:start_timer_display() diff --git a/lua/leetcode/config/hooks.lua b/lua/leetcode/config/hooks.lua index 5139c1b5..7303a956 100644 --- a/lua/leetcode/config/hooks.lua +++ b/lua/leetcode/config/hooks.lua @@ -10,13 +10,26 @@ hooks["question_enter"] = { end) end end, + function(q) + require("leetcode.utils").exec_hooks("timer_start", q) + end, } -hooks["upload_submit_result"] = {} +hooks["upload_submit_result"] = { + function(q, _, item) + if item and item._ and item._.success then + require("leetcode.utils").exec_hooks("timer_stop", q) + end + end, +} hooks["upload_test_result"] = {} -hooks["timer_start"] = {} +hooks["timer_start"] = { + function(q) + q:start_timer_display() + end, +} hooks["timer_stop"] = { function(q) diff --git a/lua/leetcode/utils.lua b/lua/leetcode/utils.lua index e2d9b1b5..ac0cf57c 100644 --- a/lua/leetcode/utils.lua +++ b/lua/leetcode/utils.lua @@ -93,6 +93,8 @@ function utils.get_hooks(event) if type(fns) == "function" then fns = { fns } + else + fns = vim.deepcopy(fns) end return vim.list_extend(fns, config.hooks[event] or {}) From 3bd9b7d59f97c91db401dd0036bedacd9b555774 Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Mon, 25 May 2026 05:28:12 -0700 Subject: [PATCH 10/24] feat: add on_test_result hook and update related commands for improved submission tracking --- README.md | 7 ++++--- lua/leetcode/command/init.lua | 8 ++++---- lua/leetcode/config/hooks.lua | 2 ++ lua/leetcode/config/template.lua | 15 ++++++++++++++- lua/leetcode/runner/init.lua | 17 ++++++++++------- 5 files changed, 34 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 6abb18c2..94eb960f 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Track problems, time spent, and submissions via external services. This fork adds submission tracking and session management capabilities to leetcode.nvim: - **Session Timer Commands**: `:Leet timer_start` / `:Leet timer_stop` -- **Extended Hooks**: `timer_start`, `question_leave`, and improved `upload_submit_result` / `upload_test_result` +- **Extended Hooks**: `timer_start`, `question_leave`, `on_test_result`, and improved upload hooks - **External Integration**: Wire hooks to external APIs for persistence - **Bug Fix**: Resolved `nvim_win_is_valid` fast event crash in timer display @@ -31,8 +31,9 @@ Existing hooks also trigger on submissions: | Hook | Signature | When | |------|-----------|------| +| `on_test_result` | `fun(question, buffer, item_json)` | After `:Leet run` / `:Leet test` | | `upload_submit_result` | `fun(question, buffer, item_json)` | After `:Leet submit` | -| `upload_test_result` | `fun(question, buffer, item_json)` | After `:Leet run` | +| `upload_test_result` | `fun(question, buffer, item_json)` | After `:Leet upload_test_result` | ## 🛠️ New Commands @@ -69,7 +70,7 @@ return { end, }, -- Save test runs - ["upload_test_result"] = { + ["on_test_result"] = { function(question, buffer, item) db_saver.save_submission(question, buffer, item) end, diff --git a/lua/leetcode/command/init.lua b/lua/leetcode/command/init.lua index 99144c54..e0bc3eca 100644 --- a/lua/leetcode/command/init.lua +++ b/lua/leetcode/command/init.lua @@ -264,7 +264,7 @@ function cmd.q_run() utils.auth_guard() local q = utils.curr_question() if q then - q.console:run() + q.console:run(false, "on_test_result") end end @@ -273,7 +273,7 @@ function cmd.q_submit() utils.auth_guard() local q = utils.curr_question() if q then - q.console:run(true) + q.console:run(true, "upload_submit_result") end end @@ -300,7 +300,7 @@ function cmd.q_upload_test_result() utils.auth_guard() local q = utils.curr_question() if q then - q.console:run(false, true) -- test flow but trigger hook + q.console:run(false, "upload_test_result") end end @@ -309,7 +309,7 @@ function cmd.q_upload_submit_result() utils.auth_guard() local q = utils.curr_question() if q then - q.console:run(true, true) -- submit flow and trigger hook + q.console:run(true, "upload_submit_result") end end diff --git a/lua/leetcode/config/hooks.lua b/lua/leetcode/config/hooks.lua index 7303a956..dcd3751f 100644 --- a/lua/leetcode/config/hooks.lua +++ b/lua/leetcode/config/hooks.lua @@ -25,6 +25,8 @@ hooks["upload_submit_result"] = { hooks["upload_test_result"] = {} +hooks["on_test_result"] = {} + hooks["timer_start"] = { function(q) q:start_timer_display() diff --git a/lua/leetcode/config/template.lua b/lua/leetcode/config/template.lua index 35b37b7f..de9f0533 100644 --- a/lua/leetcode/config/template.lua +++ b/lua/leetcode/config/template.lua @@ -25,6 +25,7 @@ ---| "question_enter" ---| "question_leave" ---| "leave" +---| "on_test_result" ---| "upload_submit_result" ---| "upload_test_result" ---| "timer_start" @@ -130,7 +131,19 @@ local M = { ["leave"] = {}, ---@type fun(question: lc.ui.Question, buffer: string, item_json: table)[] - ["submit"] = {}, + ["on_test_result"] = {}, + + ---@type fun(question: lc.ui.Question, buffer: string, item_json: table)[] + ["upload_submit_result"] = {}, + + ---@type fun(question: lc.ui.Question, buffer: string, item_json: table)[] + ["upload_test_result"] = {}, + + ---@type fun(question: lc.ui.Question)[] + ["timer_start"] = {}, + + ---@type fun(question: lc.ui.Question)[] + ["timer_stop"] = {}, }, keys = { diff --git a/lua/leetcode/runner/init.lua b/lua/leetcode/runner/init.lua index ebb9fba3..aca2c311 100644 --- a/lua/leetcode/runner/init.lua +++ b/lua/leetcode/runner/init.lua @@ -19,13 +19,13 @@ Runner.running = false ---@param self lc.Runner ---@param submit boolean ----@param trigger_hook? boolean -Runner.run = vim.schedule_wrap(function(self, submit, trigger_hook) +---@param hook_event? lc.hook|boolean +Runner.run = vim.schedule_wrap(function(self, submit, hook_event) if Runner.running then return log.warn("Runner is busy") end - local ok, err = pcall(Runner.handle, self, submit, trigger_hook) + local ok, err = pcall(Runner.handle, self, submit, hook_event) if not ok then self:stop() log.error(err) @@ -36,7 +36,7 @@ Runner.stop = function() Runner.running = false end -function Runner:handle(submit, trigger_hook) +function Runner:handle(submit, hook_event) Runner.running = true local question = self.question @@ -55,9 +55,12 @@ function Runner:handle(submit, trigger_hook) if item then -- print(vim.inspect(item)) - if trigger_hook then - local hook_event = submit and "upload_submit_result" or "upload_test_result" - utils.exec_hooks(hook_event, question, body.typed_code, item) + if hook_event then + local event = hook_event + if hook_event == true then + event = submit and "upload_submit_result" or "upload_test_result" + end + utils.exec_hooks(event, question, body.typed_code, item) end if item._.success then From 0fda2d5a672744f8b62fe757d61dd7d7ec35b4bd Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Mon, 25 May 2026 14:45:53 -0700 Subject: [PATCH 11/24] feat: add submission side panel and hooks for past submissions tracking --- README.md | 17 +- lua/leetcode-ui/question.lua | 18 +++ lua/leetcode-ui/split/submissions.lua | 213 ++++++++++++++++++++++++++ lua/leetcode/config/hooks.lua | 2 + lua/leetcode/config/template.lua | 10 ++ sample.leetcode.lua | 1 + 6 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 lua/leetcode-ui/split/submissions.lua create mode 120000 sample.leetcode.lua diff --git a/README.md b/README.md index 94eb960f..f29823f7 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ This fork adds submission tracking and session management capabilities to leetco - **Session Timer Commands**: `:Leet timer_start` / `:Leet timer_stop` - **Extended Hooks**: `timer_start`, `question_leave`, `on_test_result`, and improved upload hooks +- **Submission Side Panel**: Optional left-bottom panel for past submissions - **External Integration**: Wire hooks to external APIs for persistence - **Bug Fix**: Resolved `nvim_win_is_valid` fast event crash in timer display @@ -24,6 +25,7 @@ Beyond the original hooks, this fork adds: | Hook | Signature | When | |------|-----------|------| +| `problem_description_open` | `fun(question: lc.ui.Question)` | After the problem description buffer mounts | | `timer_start` | `fun(question: lc.ui.Question)` | Session timer started (`:Leet session start`) | | `question_leave` | `fun(question: lc.ui.Question)` | Question window closed or session stopped | @@ -55,6 +57,13 @@ local db_saver = require("submission_db_saver") return { "kawre/leetcode.nvim", opts = { + description = { + submissions = { + enabled = true, + height = "28%", + limit = 10, + }, + }, hooks = { -- Track time on a problem ["timer_start"] = { @@ -63,6 +72,11 @@ return { question:start_timer_display() end, }, + ["problem_description_open"] = { + function(question) + db_saver.show_past_submissions(question, 10) + end, + }, -- Clean up timer on close ["question_leave"] = { function(question) @@ -93,8 +107,9 @@ Expected server endpoints: ``` POST /api/session { - "action": "start_timer" | "drop_timer" | "save_submission", + "action": "start_timer" | "drop_timer" | "save_submission" | "get_past_submissions", "title_slug": "two-sum", + "limit": 10, // optional, for get_past_submissions "content": "...", // for save_submission "item": {...} // for save_submission } diff --git a/lua/leetcode-ui/question.lua b/lua/leetcode-ui/question.lua index 28456c48..505e6014 100644 --- a/lua/leetcode-ui/question.lua +++ b/lua/leetcode-ui/question.lua @@ -1,4 +1,5 @@ local Description = require("leetcode-ui.split.description") +local Submissions = require("leetcode-ui.split.submissions") local Console = require("leetcode-ui.layout.console") local Info = require("leetcode-ui.popup.info") local Object = require("nui.object") @@ -15,6 +16,7 @@ local log = require("leetcode.logger") ---@field file Path ---@field q lc.question_res ---@field description lc.ui.Description +---@field submissions? lc.ui.Submissions ---@field bufnr integer ---@field console lc.ui.Console ---@field lang string @@ -275,6 +277,9 @@ function Question:_unmount() vim.schedule(function() self.info:unmount() self.console:unmount() + if self.submissions then + self.submissions:unmount() + end self.description:unmount() if self.bufnr and vim.api.nvim_buf_is_valid(self.bufnr) then @@ -377,6 +382,11 @@ function Question:handle_mount() self:create_buffer() self.description = Description(self):mount() + if config.user.description.submissions.enabled then + self.submissions = Submissions(self):mount() + end + utils.exec_hooks("problem_description_open", self) + self.console = Console(self) self.info = Info(self) @@ -388,6 +398,14 @@ function Question:handle_mount() return self end +---@param submissions table[] +function Question:set_past_submissions(submissions) + self.past_submissions = submissions or {} + if self.submissions then + self.submissions:update_submissions(self.past_submissions) + end +end + function Question:mount() local tabp = utils.detect_duplicate_question(self.cache.title_slug, config.lang) if tabp then diff --git a/lua/leetcode-ui/split/submissions.lua b/lua/leetcode-ui/split/submissions.lua new file mode 100644 index 00000000..51cdcbad --- /dev/null +++ b/lua/leetcode-ui/split/submissions.lua @@ -0,0 +1,213 @@ +local config = require("leetcode.config") + +local Group = require("leetcode-ui.group") +local Line = require("leetcode-ui.line") +local Padding = require("leetcode-ui.lines.padding") +local Split = require("leetcode-ui.split") + +---@class lc.ui.Submissions : lc-ui.Split +---@field question lc.ui.Question +---@field state "loading"|"ready"|"empty"|"error" +---@field submissions table[] +---@field message? string +local Submissions = Split:extend("LeetSubmissions") + +local function debug_log(msg) + vim.schedule(function() + vim.fn.histadd("message", "[submissions_panel] " .. msg) + end) +end + +local function status_hl(status) + if status == "Accepted" then + return "leetcode_easy" + end + + if status == "Wrong Answer" or status == "Runtime Error" or status == "Time Limit Exceeded" then + return "leetcode_hard" + end + + return "leetcode_normal" +end + +local function trim_timestamp(timestamp) + if not timestamp or timestamp == "" then + return "unknown" + end + + return timestamp:gsub(" %u+$", "") +end + +function Submissions:populate() + local layout = Group({}, { + padding = { + left = 1, + right = 1, + top = 1, + }, + }) + + local header = Line() + header:append("Past Submissions", "leetcode_medium") + layout:insert(header) + layout:insert(Padding(1)) + + if self.state == "loading" then + local line = Line() + line:append("Loading submissions...", "leetcode_alt") + layout:insert(line) + elseif self.state == "error" then + local line = Line() + line:append(self.message or "Submission service unavailable", "leetcode_hard") + layout:insert(line) + elseif self.state == "empty" then + local line = Line() + line:append("No past submissions yet", "leetcode_alt") + layout:insert(line) + else + for index, submission in ipairs(self.submissions) do + local title = Line() + title:append(("%d. "):format(index), "leetcode_list") + title:append(trim_timestamp(submission.submitted_at_pst), "leetcode_alt") + layout:insert(title) + + local details = Line() + details:append("result=", "leetcode_list") + details:append(submission.submit_result or "Unknown", status_hl(submission.submit_result)) + details:append(" time=", "leetcode_list") + + local time_spent = submission.time_spent_minutes + if time_spent == nil or time_spent == vim.NIL then + details:append("n/a", "leetcode_alt") + else + details:append(("%sm"):format(time_spent), "leetcode_normal") + end + + details:append(" test=", "leetcode_list") + details:append(submission.is_test and "yes" or "no", submission.is_test and "leetcode_medium" or "leetcode_normal") + layout:insert(details) + + if index < #self.submissions then + layout:insert(Padding(1)) + end + end + end + + self.renderer:replace({ layout }) +end + +function Submissions:draw() + self:populate() + Submissions.super.draw(self) +end + +function Submissions:set_loading() + debug_log("set_loading") + self.state = "loading" + self.message = nil + self.submissions = {} + self:draw() +end + +---@param msg string +function Submissions:set_error(msg) + debug_log("set_error " .. tostring(msg)) + self.state = "error" + self.message = msg + self.submissions = {} + self:draw() +end + +---@param submissions table[] +function Submissions:update_submissions(submissions) + debug_log("update_submissions count=" .. tostring(submissions and #submissions or 0)) + self.submissions = submissions or {} + self.message = nil + self.state = vim.tbl_isempty(self.submissions) and "empty" or "ready" + self:draw() +end + +function Submissions:fetch() + debug_log("fetch begin for " .. self.question.q.title_slug) + self:set_loading() + + local ok, saver = pcall(require, "submission_db_saver") + if not ok then + self:set_error("submission_db_saver.lua not found") + return + end + + saver.get_past_submissions(self.question, function(response) + vim.schedule(function() + if not self._.mounted then + debug_log("callback ignored because panel unmounted") + return + end + + debug_log("callback response=" .. vim.inspect(response)) + if response.error then + self:set_error(response.error) + return + end + + local submissions = response.submissions or {} + self.question.past_submissions = submissions + self:update_submissions(submissions) + end) + end, config.user.description.submissions.limit) +end + +function Submissions:mount() + Submissions.super.mount(self) + self:fetch() + + local ui_utils = require("leetcode-ui.utils") + ui_utils.buf_set_opts(self.bufnr, { + modifiable = false, + buflisted = false, + swapfile = false, + buftype = "nofile", + filetype = config.name, + }) + ui_utils.win_set_opts(self.winid, { + winhighlight = "Normal:NormalFloat,FloatBorder:FloatBorder", + wrap = false, + colorcolumn = "", + foldcolumn = "0", + cursorcolumn = false, + cursorline = false, + number = false, + relativenumber = false, + list = false, + spell = false, + signcolumn = "no", + linebreak = false, + }) + ui_utils.win_set_winfixbuf(self.winid) + + self:draw() + return self +end + +---@param parent lc.ui.Question +function Submissions:init(parent) + Submissions.super.init(self, { + relative = { + type = "win", + winid = parent.description.winid, + }, + position = "bottom", + size = config.user.description.submissions.height, + enter = false, + focusable = true, + }) + + self.question = parent + self.state = "loading" + self.submissions = parent.past_submissions or {} +end + +---@type fun(parent: lc.ui.Question): lc.ui.Submissions +local LeetSubmissions = Submissions + +return LeetSubmissions diff --git a/lua/leetcode/config/hooks.lua b/lua/leetcode/config/hooks.lua index dcd3751f..aa394573 100644 --- a/lua/leetcode/config/hooks.lua +++ b/lua/leetcode/config/hooks.lua @@ -1,6 +1,8 @@ ---@class lc.Hooks local hooks = {} +hooks["problem_description_open"] = {} + hooks["question_enter"] = { function(q) -- https://github.com/kawre/leetcode.nvim/issues/14 diff --git a/lua/leetcode/config/template.lua b/lua/leetcode/config/template.lua index de9f0533..4a06998e 100644 --- a/lua/leetcode/config/template.lua +++ b/lua/leetcode/config/template.lua @@ -22,6 +22,7 @@ ---@alias lc.hook ---| "enter" +---| "problem_description_open" ---| "question_enter" ---| "question_leave" ---| "leave" @@ -112,6 +113,12 @@ local M = { width = "40%", ---@type lc.size show_stats = true, ---@type boolean + + submissions = { + enabled = true, ---@type boolean + height = 10, ---@type lc.size + limit = 10, ---@type integer + }, }, ---@type lc.picker @@ -121,6 +128,9 @@ local M = { ---@type fun()[] ["enter"] = {}, + ---@type fun(question: lc.ui.Question)[] + ["problem_description_open"] = {}, + ---@type fun(question: lc.ui.Question)[] ["question_enter"] = {}, diff --git a/sample.leetcode.lua b/sample.leetcode.lua new file mode 120000 index 00000000..2bf9b741 --- /dev/null +++ b/sample.leetcode.lua @@ -0,0 +1 @@ +/home/jingyi/dotfiles/.config/nvim/lua/plugins/leetcode.lua \ No newline at end of file From 9d38b4c64822dabbd0b86724ecaad9c36e0d5f4f Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Mon, 25 May 2026 14:57:49 -0700 Subject: [PATCH 12/24] feat: enhance submission details display with status icons --- lua/leetcode-ui/split/submissions.lua | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lua/leetcode-ui/split/submissions.lua b/lua/leetcode-ui/split/submissions.lua index 51cdcbad..e753b118 100644 --- a/lua/leetcode-ui/split/submissions.lua +++ b/lua/leetcode-ui/split/submissions.lua @@ -30,6 +30,14 @@ local function status_hl(status) return "leetcode_normal" end +local function status_icon(status) + if status == "Accepted" then + return config.icons.status.ac, "leetcode_easy" + end + + return config.icons.status.notac, "leetcode_hard" +end + local function trim_timestamp(timestamp) if not timestamp or timestamp == "" then return "unknown" @@ -72,8 +80,8 @@ function Submissions:populate() layout:insert(title) local details = Line() - details:append("result=", "leetcode_list") - details:append(submission.submit_result or "Unknown", status_hl(submission.submit_result)) + local icon, icon_hl = status_icon(submission.submit_result) + details:append(icon, icon_hl) details:append(" time=", "leetcode_list") local time_spent = submission.time_spent_minutes @@ -85,6 +93,8 @@ function Submissions:populate() details:append(" test=", "leetcode_list") details:append(submission.is_test and "yes" or "no", submission.is_test and "leetcode_medium" or "leetcode_normal") + details:append(" ", "leetcode_list") + details:append(submission.submit_result or "Unknown", status_hl(submission.submit_result)) layout:insert(details) if index < #self.submissions then From 227e6ee5a2ee78d0f83d0adea1ffc13819aaec1e Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Sun, 7 Jun 2026 16:48:07 -0700 Subject: [PATCH 13/24] feat: add CodeCompanion integration for enhanced problem context assistance --- README.md | 25 +++ lua/leetcode/command/init.lua | 5 + lua/leetcode/config/template.lua | 9 + lua/leetcode/integrations/codecompanion.lua | 181 ++++++++++++++++++++ sample.codecompanionconfig.lua | 1 + 5 files changed, 221 insertions(+) create mode 100644 lua/leetcode/integrations/codecompanion.lua create mode 120000 sample.codecompanionconfig.lua diff --git a/README.md b/README.md index f29823f7..73839247 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ This fork adds submission tracking and session management capabilities to leetco - **Extended Hooks**: `timer_start`, `question_leave`, `on_test_result`, and improved upload hooks - **Submission Side Panel**: Optional left-bottom panel for past submissions - **External Integration**: Wire hooks to external APIs for persistence +- **CodeCompanion Bridge**: `:Leet companion` can inject LeetCode context into CodeCompanion while delegating the actual LLM call to your own service - **Bug Fix**: Resolved `nvim_win_is_valid` fast event crash in timer display ## 🎯 New Hooks @@ -44,8 +45,32 @@ Existing hooks also trigger on submissions: :Leet timer_stop " Stop tracking (drop session without saving) :Leet session start " Alias for timer_start :Leet session stop " Alias for timer_stop +:Leet companion " Open a CodeCompanion chat seeded with the current LeetCode context ``` +## 🤖 CodeCompanion Bridge + +If you want `CodeCompanion` to be just the UI bridge while your own submission service owns the provider, model, and system prompt, configure a local OpenAI-compatible adapter in `CodeCompanion` and point `leetcode.nvim` at it: + +```lua +require("leetcode").setup({ + companion = { + adapter = "submission_service", + default_prompt = "Help me understand the bug in my current approach.", + }, +}) +``` + +Then run `:Leet companion` on an open problem buffer. The plugin injects: + +- problem title / slug / difficulty +- problem description +- tags and hints +- active testcase +- current code from the editor + +as hidden chat context, while the visible prompt stays focused on what you want help with. + ## 📡 External Integration Example Connect to a submission server to save data: diff --git a/lua/leetcode/command/init.lua b/lua/leetcode/command/init.lua index e0bc3eca..ec12bb85 100644 --- a/lua/leetcode/command/init.lua +++ b/lua/leetcode/command/init.lua @@ -450,6 +450,10 @@ function cmd.fold() q:editor_fold_imports(true) end +function cmd.companion() + require("leetcode.integrations.codecompanion").prompt_and_open() +end + function cmd.get_active_session() local sessions = config.sessions.all return vim.tbl_filter(function(s) @@ -659,6 +663,7 @@ cmd.commands = { restore = { cmd.restore }, inject = { cmd.inject }, fold = { cmd.fold }, + companion = { cmd.companion }, -- session = { -- change = { -- cmd.change_session, diff --git a/lua/leetcode/config/template.lua b/lua/leetcode/config/template.lua index 4a06998e..86a3fc21 100644 --- a/lua/leetcode/config/template.lua +++ b/lua/leetcode/config/template.lua @@ -124,6 +124,15 @@ local M = { ---@type lc.picker picker = { provider = nil }, + companion = { + adapter = "submission_service", ---@type string + model = nil, ---@type string|nil + auto_submit = true, ---@type boolean + prompt_user = true, ---@type boolean + default_prompt = "Help me understand this problem and my current code.", ---@type string + window = nil, ---@type table|nil + }, + hooks = { ---@type fun()[] ["enter"] = {}, diff --git a/lua/leetcode/integrations/codecompanion.lua b/lua/leetcode/integrations/codecompanion.lua new file mode 100644 index 00000000..370f0704 --- /dev/null +++ b/lua/leetcode/integrations/codecompanion.lua @@ -0,0 +1,181 @@ +local config = require("leetcode.config") +local lc_utils = require("leetcode.utils") + +local M = {} + +local function notify(message, level) + vim.notify(message, level or vim.log.levels.INFO, { title = "leetcode.nvim" }) +end + +local function current_code(question) + if not (question and question.bufnr and vim.api.nvim_buf_is_valid(question.bufnr)) then + return question and question:snippet(true) or "" + end + + local range = question:editor_section_range("code") + if range.complete then + return table.concat(range.lines, "\n", range.start_i, range.end_i) + end + + return question:snippet(true) or "" +end + +local function question_description(question) + local q = question.q or {} + local content = type(q.content) == "string" and q.content or "" + local translated = type(q.translated_content) == "string" and q.translated_content or "" + return lc_utils.translate(content, translated) +end + +local function question_tags(question) + local tags = {} + local topic_tags = type(question.q.topic_tags) == "table" and question.q.topic_tags or {} + for _, tag in ipairs(topic_tags) do + table.insert(tags, tag.name or tag.slug or "") + end + return vim.tbl_filter(function(tag) + return tag ~= "" + end, tags) +end + +local function question_hints(question) + local hints = type(question.q.hints) == "table" and question.q.hints or {} + return vim.tbl_map(function(hint) + return tostring(hint) + end, hints) +end + +function M.build_context(question) + return { + title = question.q.title or "", + title_slug = question.q.title_slug or "", + difficulty = question.q.difficulty or "", + lang = question.lang or config.lang, + description = question_description(question), + tags = question_tags(question), + hints = question_hints(question), + code = current_code(question), + testcase = question.console and question.console.testcase and question.console.testcase:content() or "", + } +end + +function M.render_context(context) + local parts = { + "# LeetCode Problem Context", + "", + ("- Title: %s"):format(context.title), + ("- Title Slug: %s"):format(context.title_slug), + ("- Difficulty: %s"):format(context.difficulty), + ("- Language: %s"):format(context.lang), + } + + if not vim.tbl_isempty(context.tags) then + table.insert(parts, ("- Tags: %s"):format(table.concat(context.tags, ", "))) + end + + table.insert(parts, "") + table.insert(parts, "## Problem Description") + table.insert(parts, context.description ~= "" and context.description or "(empty)") + + if not vim.tbl_isempty(context.hints) then + table.insert(parts, "") + table.insert(parts, "## Hints") + for index, hint in ipairs(context.hints) do + table.insert(parts, ("%d. %s"):format(index, hint)) + end + end + + if context.testcase ~= "" then + table.insert(parts, "") + table.insert(parts, "## Active Testcase") + table.insert(parts, "```text") + table.insert(parts, context.testcase) + table.insert(parts, "```") + end + + table.insert(parts, "") + table.insert(parts, "## Current Code") + table.insert(parts, ("```%s"):format(context.lang)) + table.insert(parts, context.code ~= "" and context.code or "") + table.insert(parts, "```") + + return table.concat(parts, "\n") +end + +function M.open(opts) + opts = opts or {} + + local question = lc_utils.curr_question() + if not question then + return + end + + local ok, codecompanion = pcall(require, "codecompanion") + if not ok then + notify("CodeCompanion is not available", vim.log.levels.ERROR) + return + end + + local cc = config.user.companion or {} + local prompt = opts.prompt or cc.default_prompt or "" + local params = { + adapter = cc.adapter, + } + + if cc.model and cc.model ~= "" then + params.model = cc.model + end + + local context = M.build_context(question) + return codecompanion.chat({ + auto_submit = cc.auto_submit ~= false, + ignore_system_prompt = true, + params = params, + title = ("LeetCode: %s"):format(context.title ~= "" and context.title or context.title_slug), + user_prompt = prompt, + window_opts = cc.window, + callbacks = { + on_created = function(chat) + chat:add_context( + { + role = "user", + content = M.render_context(context), + }, + "leetcode", + (""):format(context.title_slug ~= "" and context.title_slug or chat.id), + { + visible = false, + tag = "leetcode_context", + } + ) + end, + }, + }) +end + +function M.prompt_and_open() + local cc = config.user.companion or {} + local default_prompt = cc.default_prompt or "" + + if cc.prompt_user == false then + return M.open({ prompt = default_prompt }) + end + + vim.ui.input({ + prompt = "What do you want help with? ", + default = default_prompt, + }, function(input) + if input == nil then + return + end + + local prompt = vim.trim(input) + if prompt == "" then + prompt = default_prompt + end + + M.open({ prompt = prompt }) + end) +end + +return M diff --git a/sample.codecompanionconfig.lua b/sample.codecompanionconfig.lua new file mode 120000 index 00000000..91874b62 --- /dev/null +++ b/sample.codecompanionconfig.lua @@ -0,0 +1 @@ +/home/jingyi/.config/nvim/lua/plugins/codecompanion.lua \ No newline at end of file From e4323c8bba81c87f78dd3651151e98532305b0b8 Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Sun, 7 Jun 2026 17:17:57 -0700 Subject: [PATCH 14/24] feat: implement current_question and require_question functions for better question handling --- lua/leetcode/integrations/codecompanion.lua | 35 ++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/lua/leetcode/integrations/codecompanion.lua b/lua/leetcode/integrations/codecompanion.lua index 370f0704..284fe69c 100644 --- a/lua/leetcode/integrations/codecompanion.lua +++ b/lua/leetcode/integrations/codecompanion.lua @@ -7,6 +7,24 @@ local function notify(message, level) vim.notify(message, level or vim.log.levels.INFO, { title = "leetcode.nvim" }) end +local function current_question() + local current_tab = vim.api.nvim_get_current_tabpage() + for _, entry in ipairs(lc_utils.question_tabs()) do + if entry.tabpage == current_tab then + return entry.question + end + end +end + +local function require_question() + local question = current_question() + if not question then + notify("CodeCompanionChat requires an active leetcode question", vim.log.levels.WARN) + return nil + end + return question +end + local function current_code(question) if not (question and question.bufnr and vim.api.nvim_buf_is_valid(question.bufnr)) then return question and question:snippet(true) or "" @@ -105,7 +123,7 @@ end function M.open(opts) opts = opts or {} - local question = lc_utils.curr_question() + local question = require_question() if not question then return end @@ -153,6 +171,10 @@ function M.open(opts) }) end +function M.toggle_or_open() + return M.prompt_and_open() +end + function M.prompt_and_open() local cc = config.user.companion or {} local default_prompt = cc.default_prompt or "" @@ -178,4 +200,15 @@ function M.prompt_and_open() end) end +function M.command(opts) + opts = opts or {} + local fargs = opts.fargs or {} + + if #fargs == 0 then + return M.prompt_and_open() + end + + return M.open({ prompt = table.concat(fargs, " ") }) +end + return M From 69042ac94cddf5928ff38d64e69d3d765474c032 Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Sun, 7 Jun 2026 17:53:01 -0700 Subject: [PATCH 15/24] feat: enhance chat management with binding and unbinding functions --- lua/leetcode/integrations/codecompanion.lua | 129 ++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/lua/leetcode/integrations/codecompanion.lua b/lua/leetcode/integrations/codecompanion.lua index 284fe69c..4e5da5aa 100644 --- a/lua/leetcode/integrations/codecompanion.lua +++ b/lua/leetcode/integrations/codecompanion.lua @@ -2,6 +2,7 @@ local config = require("leetcode.config") local lc_utils = require("leetcode.utils") local M = {} +local active_chats_by_slug = {} local function notify(message, level) vim.notify(message, level or vim.log.levels.INFO, { title = "leetcode.nvim" }) @@ -16,6 +17,54 @@ local function current_question() end end +local function question_slug(question) + return question and question.q and question.q.title_slug or "" +end + +local function is_chat_valid(chat) + return chat + and type(chat) == "table" + and chat.bufnr + and vim.api.nvim_buf_is_valid(chat.bufnr) + and vim.api.nvim_buf_is_loaded(chat.bufnr) +end + +local function bind_chat(title_slug, chat) + if title_slug == "" or not is_chat_valid(chat) then + return + end + active_chats_by_slug[title_slug] = chat +end + +local function unbind_chat(title_slug, chat) + if title_slug == "" then + return + end + + if active_chats_by_slug[title_slug] == chat then + active_chats_by_slug[title_slug] = nil + end +end + +local function current_chat_for_slug(title_slug) + local chat = active_chats_by_slug[title_slug] + if not is_chat_valid(chat) then + active_chats_by_slug[title_slug] = nil + return nil + end + return chat +end + +local function chat_has_context_id(chat, context_id) + local items = chat and chat.context_items or {} + for _, item in ipairs(items) do + if item.id == context_id then + return true + end + end + return false +end + local function require_question() local question = current_question() if not question then @@ -120,6 +169,77 @@ function M.render_context(context) return table.concat(parts, "\n") end +function M.render_failure_event_context(event) + local parts = { + "# LeetCode Failure Event", + "", + ("- Title Slug: %s"):format(event.title_slug), + ("- Event ID: %s"):format(event.event_id), + "- Source: submission-service", + "- This failure event is already stored in the active submission-service session memory.", + } + + if event.summary ~= "" then + table.insert(parts, "") + table.insert(parts, "## Latest Failure Summary") + table.insert(parts, event.summary) + end + + if type(event.annotation_count) == "number" and event.annotation_count > 0 then + table.insert(parts, "") + table.insert(parts, ("- Annotation Count: %d"):format(event.annotation_count)) + end + + return table.concat(parts, "\n") +end + +function M.render_failure_event(question, event) + local title_slug = question_slug(question) + local event_id = type(event) == "table" and event.event_id or "" + if title_slug == "" or type(event_id) ~= "string" or event_id == "" then + return false + end + + local chat = current_chat_for_slug(title_slug) + if not chat then + return false + end + + local context_id = (""):format(event_id) + if chat_has_context_id(chat, context_id) then + return true + end + + chat:add_context( + { + role = "user", + content = M.render_failure_event_context({ + title_slug = title_slug, + event_id = event_id, + summary = type(event.summary) == "string" and event.summary or "", + annotation_count = tonumber(event.count) or 0, + }), + }, + "leetcode", + context_id, + { + visible = false, + tag = "leetcode_failure_event", + } + ) + + if chat.refresh_context then + chat:refresh_context() + end + + chat:add_buf_message({ + role = "user", + content = ("Attached failure event `%s` to this session."):format(event_id), + }) + + return true +end + function M.open(opts) opts = opts or {} @@ -145,6 +265,7 @@ function M.open(opts) end local context = M.build_context(question) + local title_slug = question_slug(question) return codecompanion.chat({ auto_submit = cc.auto_submit ~= false, ignore_system_prompt = true, @@ -154,6 +275,7 @@ function M.open(opts) window_opts = cc.window, callbacks = { on_created = function(chat) + bind_chat(title_slug, chat) chat:add_context( { role = "user", @@ -167,6 +289,9 @@ function M.open(opts) } ) end, + on_closed = function(chat) + unbind_chat(title_slug, chat) + end, }, }) end @@ -211,4 +336,8 @@ function M.command(opts) return M.open({ prompt = table.concat(fargs, " ") }) end +function M.clear(question) + unbind_chat(question_slug(question), current_chat_for_slug(question_slug(question))) +end + return M From b2dec77ae0f9756e9c56d8872b20cc1aa00db9a3 Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Sun, 7 Jun 2026 18:05:40 -0700 Subject: [PATCH 16/24] Refactor code structure for improved readability and maintainability --- AGENTS.md | 61 ++++++++ ...ecompanion-session-failure-event-bridge.md | 100 ++++++++++++ docs/d2/codecompanion-bridge.d2 | 142 ++++++++++++++++++ docs/d2/codecompanion-bridge.svg | 123 +++++++++++++++ 4 files changed, 426 insertions(+) create mode 100644 AGENTS.md create mode 100644 docs/adrs/001-codecompanion-session-failure-event-bridge.md create mode 100644 docs/d2/codecompanion-bridge.d2 create mode 100644 docs/d2/codecompanion-bridge.svg diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..0c562791 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,61 @@ +# AGENTS.md + +Agent map for `leetcode.nvim`. + +Read ADRs first when changing session behavior, hooks, or the CodeCompanion bridge. Use `README.md` and nearby Lua modules after that. + +## What This Repo Owns + +- LeetCode UI, commands, and question lifecycle inside Neovim +- hook dispatch around test / submit / timer events +- CodeCompanion bridge for LeetCode context injection +- editor-side rendering for submission-service-backed companion events + +## Start Here + +1. [docs/adrs/001-codecompanion-session-failure-event-bridge.md](./docs/adrs/001-codecompanion-session-failure-event-bridge.md) +2. [README.md](./README.md) +3. [lua/leetcode/integrations/codecompanion.lua](./lua/leetcode/integrations/codecompanion.lua) +4. [lua/leetcode/config/hooks.lua](./lua/leetcode/config/hooks.lua) +5. [lua/leetcode/runner/init.lua](./lua/leetcode/runner/init.lua) + +## File Map + +- `lua/leetcode/integrations/codecompanion.lua` + CodeCompanion bridge, hidden context injection, question-bound chat tracking, failure-event rendering. +- `lua/leetcode/config/hooks.lua` + Canonical hook registration and defaults. +- `lua/leetcode/runner/init.lua` + Execution path for test / submit flows and hook dispatch. +- `lua/leetcode/command/init.lua` + User-facing `:Leet` commands. +- `lua/leetcode-ui/` + Question, result, and side-panel rendering. +- `docs/adrs/` + Durable behavior decisions for this fork. + +## Guardrails + +- Keep `leetcode.nvim` as a UI/context bridge; do not move provider or model ownership into this repo. +- Prefer stable data from submission service over reconstructing session truth inside Lua. +- Keep hook payload shapes backward compatible unless the user explicitly asks for a contract change. +- Prefer hidden context for companion-side lifecycle metadata over visible user-message injection. + +## D2 Diagram Style Preference + +When adding or updating D2 diagrams, prefer the user's established high-level architecture style: + +- Treat D2 as a system overview first, not an implementation-detail dump. +- Group nodes by runtime boundary before drawing flows, for example `Strict local`. +- Clearly separate entry points, runtime surfaces, external systems, and data planes. +- Prefer a left-to-right primary reading direction with straight main flows and minimal crossing lines. +- Optimize for ownership and data flow clarity, not source-file completeness. +- Use a small number of stable responsibility-oriented node labels rather than many file/module names. +- Keep edge labels short and protocol/action-oriented, such as `JSON over TCP`. +- Preserve generous whitespace and a clean overview feel; prefer one readable system map plus optional focused sub-diagrams. + +## When To Update Docs + +- Update ADRs when the companion bridge, session binding, or hook semantics change. +- Update `README.md` when user-facing setup or behavior changes. +- Update this file only when the navigation order or edit guardrails change. diff --git a/docs/adrs/001-codecompanion-session-failure-event-bridge.md b/docs/adrs/001-codecompanion-session-failure-event-bridge.md new file mode 100644 index 00000000..eb03db4e --- /dev/null +++ b/docs/adrs/001-codecompanion-session-failure-event-bridge.md @@ -0,0 +1,100 @@ +# CodeCompanion Session Failure Event Bridge + +日期: 2026-06-07 + +相关背景: + +- [README.md](../../README.md) +- `leetcode-qa` 中 submission service 的 session-bound companion memory 设计 + +## 背景 + +这个 fork 里的 `CodeCompanion` 集成不是一个独立 agent runtime。 + +它的目标是: + +- `leetcode.nvim` 提供题目上下文和 UI bridge +- 真正的 LLM provider / model / prompt / session memory 由 submission service 持有 + +当用户在 companion chat 打开之后又触发了一次失败测试,如果 chat 里看不到这次 failure event,就会出现: + +- 当前 companion chat 不知道刚刚失败了什么 +- 用户只能手动再描述一遍 +- chat buffer 和 submission service 的 active session lifecycle 脱节 + +## 决策 + +### 1. `leetcode.nvim` 只做 bridge,不做 LLM ownership + +`lua/leetcode/integrations/codecompanion.lua` 的职责是: + +- 从当前题目页收集 LeetCode context +- 打开 CodeCompanion chat +- 以隐藏 context 形式注入题面、testcase、当前代码 +- 把 chat 绑定到当前 `title_slug` + +它不拥有: + +- provider 选择 +- model 选择 +- companion system prompt +- solve session truth + +这些都由 submission service 决定。 + +### 2. 当前 companion chat 按 `title_slug` 绑定 + +bridge 需要记住“这道题当前对应哪个 CodeCompanion chat”。 + +当前实现用 `title_slug -> chat instance` 的内存映射来做这件事。 + +这样 submission-related hook 收到 failure event 时,才能把 event 投到正确的 chat,而不是任意最后一个 chat。 + +### 3. failure event 由 submission service 生成,nvim 负责渲染 + +submission service 在 `analyze_failure` 成功后返回稳定的 `event_id`。 + +`leetcode.nvim` 不自己发明 failure id,也不自己推导“哪次 failure 才算最新”。 + +客户端只做两件事: + +1. 把 `>` 作为隐藏 context 加到当前 chat +2. 在 chat buffer 里追加一条可见提示,说明这次 failure event 已经挂进当前会话 + +### 4. failure event 通过现有 analyze_failure 客户端链路自动投递 + +当前自动投递入口放在 `leetcode-qa/lua/submission_db_saver.lua` 的 `analyze_failure()` 回调里。 + +也就是说,只要某次 test / submit failure 最终走到了这条分析路径,并且服务端返回了 `event_id`,bridge 就会自动尝试把它注入当前 companion chat。 + +### 5. 失败事件默认作为隐藏 context,而不是普通聊天正文 + +因为这条信息本质上是 solve lifecycle 里的结构化上下文,不是用户输入的自由文本。 + +把它作为隐藏 context 的好处: + +- 不污染用户正在看的可见聊天正文 +- 可以被 submission service 的 companion endpoint 稳定识别 +- 可以避免它被误当成普通 user question + +## 结果 + +现在 `leetcode.nvim` 里的 companion 行为分成两段: + +1. 打开 chat 时注入 `LeetCode Problem Context` +2. failure 发生并分析成功后,再注入 `LeetCode Failure Event` + +这让单个 CodeCompanion chat 能跟上同一道题在 solve lifecycle 里的最新 failure state。 + +## 当前边界 + +- 这个 bridge 只保证“同题目当前 chat”会收到 event +- 它不负责跨题会话恢复 +- 它不负责 failure event 的长期持久化 +- 它不自己请求 LLM 来解释 failure,解释仍由 submission service 返回 + +## 后续约束 + +- 新增 companion 相关功能时,优先沿用 `title_slug` 绑定,而不是退回“last chat”这种弱绑定 +- 新增 session-aware context 时,优先走隐藏 context 注入 +- 如果 submission service 扩展了更多 session event,客户端优先复用服务端给的 stable ids / payloads,而不是本地重建语义 diff --git a/docs/d2/codecompanion-bridge.d2 b/docs/d2/codecompanion-bridge.d2 new file mode 100644 index 00000000..79f801e2 --- /dev/null +++ b/docs/d2/codecompanion-bridge.d2 @@ -0,0 +1,142 @@ +direction: right + +nvim: { + label: "nvim\nentry point" + shape: cylinder + style: { + fill: "#fecaca" + stroke: "#2563eb" + stroke-width: 2 + } +} + +companion_client: { + label: "CodeCompanion\nexternal client" + shape: circle + style: { + fill: "#f5b4fc" + stroke: "#2563eb" + stroke-width: 2 + } +} + +local_runtime: { + label: "Strict local" + direction: down + style: { + fill: "#fff7ed" + stroke: "#fdba74" + stroke-width: 2 + } + + leetcode_nvim: { + label: "leetcode.nvim" + style: { + fill: "#fdba74" + stroke: "#2563eb" + stroke-width: 2 + } + } + + question_runtime: { + label: "question runtime" + style: { + fill: "#fde68a" + stroke: "#2563eb" + stroke-width: 2 + } + } + + hook_router: { + label: "hook router" + style: { + fill: "#fde68a" + stroke: "#2563eb" + stroke-width: 2 + } + } + + codecompanion_bridge: { + label: "codecompanion bridge" + style: { + fill: "#fde68a" + stroke: "#2563eb" + stroke-width: 2 + } + } + + submission_db_saver: { + label: "submission_db_saver" + style: { + fill: "#fde68a" + stroke: "#2563eb" + stroke-width: 2 + } + } +} + +service_runtime: { + label: "Strict local\nsubmission runtime" + direction: down + style: { + fill: "#eef2ff" + stroke: "#93c5fd" + stroke-width: 2 + } + + submission_service: { + label: "leetcode-submission-service" + style: { + fill: "#bfdbfe" + stroke: "#2563eb" + stroke-width: 2 + } + } + + companion_http: { + label: "companion HTTP" + style: { + fill: "#bfdbfe" + stroke: "#2563eb" + stroke-width: 2 + } + } + + failure_analysis: { + label: "failure analysis" + style: { + fill: "#bfdbfe" + stroke: "#2563eb" + stroke-width: 2 + } + } + + session_scope: { + label: "active session scope\nin-memory" + style: { + fill: "#dbeafe" + stroke: "#2563eb" + stroke-width: 2 + } + } +} + +nvim -> local_runtime.leetcode_nvim: "editor runtime" +companion_client -> service_runtime.companion_http: "OpenAI-compatible chat" + +local_runtime.leetcode_nvim -> local_runtime.question_runtime: "question lifecycle" +local_runtime.leetcode_nvim -> local_runtime.codecompanion_bridge: ":Leet companion" +local_runtime.question_runtime -> local_runtime.hook_router: "test / submit hooks" +local_runtime.hook_router -> local_runtime.submission_db_saver: "failure callbacks" +local_runtime.codecompanion_bridge -> companion_client: "hidden LeetCode context" + +local_runtime.submission_db_saver -> service_runtime.submission_service: "JSON over TCP" +service_runtime.submission_service -> service_runtime.failure_analysis: "analyze_failure" +service_runtime.submission_service -> service_runtime.companion_http: "local companion endpoint" + +service_runtime.failure_analysis -> service_runtime.session_scope: "write latest failure\nstatic analysis\nevent_id" +service_runtime.companion_http -> service_runtime.session_scope: "read / write companion memory" + +service_runtime.submission_service -> local_runtime.submission_db_saver: "failure event payload" +local_runtime.submission_db_saver -> local_runtime.codecompanion_bridge: "render failure event" +local_runtime.codecompanion_bridge -> companion_client: "attach hidden failure context" diff --git a/docs/d2/codecompanion-bridge.svg b/docs/d2/codecompanion-bridge.svg new file mode 100644 index 00000000..3eccf941 --- /dev/null +++ b/docs/d2/codecompanion-bridge.svg @@ -0,0 +1,123 @@ +nvimentry pointCodeCompanionexternal clientStrict localStrict localsubmission runtimeleetcode.nvimquestion runtimehook routercodecompanion bridgesubmission_db_saverleetcode-submission-servicecompanion HTTPfailure analysisactive session scopein-memory editor runtimeOpenAI-compatible chatquestion lifecycle:Leet companiontest / submit hooksfailure callbackshidden LeetCode contextJSON over TCPanalyze_failurelocal companion endpointwrite latest failurestatic analysisevent_idread / write companion memoryfailure event payloadrender failure eventattach hidden failure context + + + + + + + + + + + + + + + + + From 80808d24d28b96cea4e7b82a7097238b52cc545f Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Sun, 7 Jun 2026 20:41:13 -0700 Subject: [PATCH 17/24] feat: enhance submissions and memory handling with new state management and UI updates --- lua/leetcode-ui/question.lua | 1 + lua/leetcode-ui/split/submissions.lua | 238 ++++++++++++++++++++++++-- 2 files changed, 220 insertions(+), 19 deletions(-) diff --git a/lua/leetcode-ui/question.lua b/lua/leetcode-ui/question.lua index 505e6014..b62c105b 100644 --- a/lua/leetcode-ui/question.lua +++ b/lua/leetcode-ui/question.lua @@ -17,6 +17,7 @@ local log = require("leetcode.logger") ---@field q lc.question_res ---@field description lc.ui.Description ---@field submissions? lc.ui.Submissions +---@field mem0_recall_summary? table ---@field bufnr integer ---@field console lc.ui.Console ---@field lang string diff --git a/lua/leetcode-ui/split/submissions.lua b/lua/leetcode-ui/split/submissions.lua index e753b118..a3202b76 100644 --- a/lua/leetcode-ui/split/submissions.lua +++ b/lua/leetcode-ui/split/submissions.lua @@ -4,12 +4,17 @@ local Group = require("leetcode-ui.group") local Line = require("leetcode-ui.line") local Padding = require("leetcode-ui.lines.padding") local Split = require("leetcode-ui.split") +local ui_utils = require("leetcode-ui.utils") ---@class lc.ui.Submissions : lc-ui.Split ---@field question lc.ui.Question ----@field state "loading"|"ready"|"empty"|"error" +---@field active_tab "submissions"|"memory" +---@field submissions_state "loading"|"ready"|"empty"|"error" +---@field memory_state "loading"|"ready"|"empty"|"error" ---@field submissions table[] ---@field message? string +---@field memory_summary? table +---@field memory_message? string local Submissions = Split:extend("LeetSubmissions") local function debug_log(msg) @@ -46,7 +51,94 @@ local function trim_timestamp(timestamp) return timestamp:gsub(" %u+$", "") end +local function panel_content_width(self) + local win_width = ui_utils.win_width(self) + local usable_width = math.max(20, win_width - 6) + return math.min(usable_width, 88) +end + +local function wrap_text(text, max_width) + local chunks = {} + local current = "" + local current_width = 0 + local source = tostring(text or ""):gsub("\r\n", "\n") + + for raw_line in source:gmatch("([^\n]*)\n?") do + if raw_line == "" then + if current ~= "" then + table.insert(chunks, current) + current = "" + current_width = 0 + end + if #chunks == 0 or chunks[#chunks] ~= "" then + table.insert(chunks, "") + end + else + local index = 0 + while true do + local char = vim.fn.strcharpart(raw_line, index, 1) + if char == nil or char == "" then + break + end + + local char_width = vim.api.nvim_strwidth(char) + if current_width > 0 and current_width + char_width > max_width then + table.insert(chunks, current) + current = char + current_width = char_width + else + current = current .. char + current_width = current_width + char_width + end + + index = index + 1 + end + + if current ~= "" then + table.insert(chunks, current) + current = "" + current_width = 0 + end + end + end + + while #chunks > 0 and chunks[#chunks] == "" do + table.remove(chunks) + end + + return vim.tbl_isempty(chunks) and { "" } or chunks +end + +local function insert_wrapped_value(layout, label, content, max_width, value_hl) + local label_width = vim.api.nvim_strwidth(label) + local wrapped = wrap_text(content, math.max(10, max_width - label_width)) + + for index, segment in ipairs(wrapped) do + local line = Line() + if index == 1 then + line:append(label, "leetcode_list") + else + line:append((" "):rep(label_width), "leetcode_list") + end + line:append(segment, value_hl or "leetcode_normal") + layout:insert(line) + end +end + +local function memory_status_hl(status) + if status == "Accepted" then + return "leetcode_easy" + end + + if status == "Wrong Answer" or status == "Runtime Error" or status == "Time Limit Exceeded" then + return "leetcode_hard" + end + + return "leetcode_alt" +end + function Submissions:populate() + local max_width = panel_content_width(self) local layout = Group({}, { padding = { left = 1, @@ -55,24 +147,26 @@ function Submissions:populate() }, }) - local header = Line() - header:append("Past Submissions", "leetcode_medium") - layout:insert(header) + local tabs = Line() + tabs:append("[1] 历史提交", self.active_tab == "submissions" and "leetcode_medium" or "leetcode_alt") + tabs:append(" | ", "leetcode_alt") + tabs:append("[2] 历史记忆", self.active_tab == "memory" and "leetcode_medium" or "leetcode_alt") + layout:insert(tabs) layout:insert(Padding(1)) - if self.state == "loading" then + if self.active_tab == "submissions" and self.submissions_state == "loading" then local line = Line() - line:append("Loading submissions...", "leetcode_alt") + line:append("正在加载历史提交...", "leetcode_alt") layout:insert(line) - elseif self.state == "error" then + elseif self.active_tab == "submissions" and self.submissions_state == "error" then local line = Line() - line:append(self.message or "Submission service unavailable", "leetcode_hard") + line:append(self.message or "提交服务暂不可用", "leetcode_hard") layout:insert(line) - elseif self.state == "empty" then + elseif self.active_tab == "submissions" and self.submissions_state == "empty" then local line = Line() - line:append("No past submissions yet", "leetcode_alt") + line:append("还没有历史提交", "leetcode_alt") layout:insert(line) - else + elseif self.active_tab == "submissions" then for index, submission in ipairs(self.submissions) do local title = Line() title:append(("%d. "):format(index), "leetcode_list") @@ -82,17 +176,17 @@ function Submissions:populate() local details = Line() local icon, icon_hl = status_icon(submission.submit_result) details:append(icon, icon_hl) - details:append(" time=", "leetcode_list") + details:append(" 用时=", "leetcode_list") local time_spent = submission.time_spent_minutes if time_spent == nil or time_spent == vim.NIL then - details:append("n/a", "leetcode_alt") + details:append("未知", "leetcode_alt") else details:append(("%sm"):format(time_spent), "leetcode_normal") end - details:append(" test=", "leetcode_list") - details:append(submission.is_test and "yes" or "no", submission.is_test and "leetcode_medium" or "leetcode_normal") + details:append(" 测试=", "leetcode_list") + details:append(submission.is_test and "是" or "否", submission.is_test and "leetcode_medium" or "leetcode_normal") details:append(" ", "leetcode_list") details:append(submission.submit_result or "Unknown", status_hl(submission.submit_result)) layout:insert(details) @@ -101,6 +195,50 @@ function Submissions:populate() layout:insert(Padding(1)) end end + elseif self.memory_state == "loading" then + local line = Line() + line:append("正在加载历史记忆...", "leetcode_alt") + layout:insert(line) + elseif self.memory_state == "error" then + local line = Line() + line:append(self.memory_message or "历史记忆暂不可用", "leetcode_hard") + layout:insert(line) + elseif self.memory_state == "empty" then + local line = Line() + line:append("这道题还没有可回忆的历史记录", "leetcode_alt") + layout:insert(line) + else + local summary = self.memory_summary or {} + local count_line = Line() + count_line:append("历史记录数:", "leetcode_list") + count_line:append(tostring(summary.record_count or 0), "leetcode_normal") + layout:insert(count_line) + + local sessions = summary.sessions or {} + for index, session in ipairs(sessions) do + layout:insert(Padding(1)) + + local header = Line() + header:append(("%d. "):format(index), "leetcode_list") + header:append(session.endReason or "unknown", "leetcode_medium") + if session.latestFailureStatus and session.latestFailureStatus ~= vim.NIL then + header:append(" ", "leetcode_alt") + header:append(session.latestFailureStatus, memory_status_hl(session.latestFailureStatus)) + end + layout:insert(header) + + if session.failureSummary and session.failureSummary ~= vim.NIL and session.failureSummary ~= "" then + insert_wrapped_value(layout, "failure: ", session.failureSummary, max_width) + end + + if type(session.stuckPoints) == "table" and #session.stuckPoints > 0 then + insert_wrapped_value(layout, "stuck: ", table.concat(session.stuckPoints, " | "), max_width) + end + + if type(session.thoughtProcess) == "table" and #session.thoughtProcess > 0 then + insert_wrapped_value(layout, "thought: ", table.concat(session.thoughtProcess, " | "), max_width) + end + end end self.renderer:replace({ layout }) @@ -113,7 +251,7 @@ end function Submissions:set_loading() debug_log("set_loading") - self.state = "loading" + self.submissions_state = "loading" self.message = nil self.submissions = {} self:draw() @@ -122,7 +260,7 @@ end ---@param msg string function Submissions:set_error(msg) debug_log("set_error " .. tostring(msg)) - self.state = "error" + self.submissions_state = "error" self.message = msg self.submissions = {} self:draw() @@ -133,17 +271,53 @@ function Submissions:update_submissions(submissions) debug_log("update_submissions count=" .. tostring(submissions and #submissions or 0)) self.submissions = submissions or {} self.message = nil - self.state = vim.tbl_isempty(self.submissions) and "empty" or "ready" + self.submissions_state = vim.tbl_isempty(self.submissions) and "empty" or "ready" + self:draw() +end + +function Submissions:set_memory_loading() + self.memory_state = "loading" + self.memory_message = nil + self.memory_summary = nil + self:draw() +end + +---@param msg string +function Submissions:set_memory_error(msg) + self.memory_state = "error" + self.memory_message = msg + self.memory_summary = nil + self:draw() +end + +---@param summary table +function Submissions:update_memory(summary) + self.memory_summary = summary or {} + self.memory_message = nil + self.memory_state = summary and summary.has_history and "ready" or "empty" + self.question.mem0_recall_summary = self.memory_summary + self:draw() +end + +---@param tab "submissions"|"memory" +function Submissions:switch_tab(tab) + if self.active_tab == tab then + return + end + + self.active_tab = tab self:draw() end function Submissions:fetch() debug_log("fetch begin for " .. self.question.q.title_slug) self:set_loading() + self:set_memory_loading() local ok, saver = pcall(require, "submission_db_saver") if not ok then self:set_error("submission_db_saver.lua not found") + self:set_memory_error("submission_db_saver.lua not found") return end @@ -165,11 +339,34 @@ function Submissions:fetch() self:update_submissions(submissions) end) end, config.user.description.submissions.limit) + + saver.get_mem0_recall_summary(self.question, function(response) + vim.schedule(function() + if not self._.mounted then + debug_log("mem0 callback ignored because panel unmounted") + return + end + + debug_log("mem0 callback response=" .. vim.inspect(response)) + if response.error then + self:set_memory_error(response.error) + return + end + + self:update_memory(response) + end) + end) end function Submissions:mount() Submissions.super.mount(self) self:fetch() + self:map("n", { "1", "1" }, function() + self:switch_tab("submissions") + end) + self:map("n", { "2", "2" }, function() + self:switch_tab("memory") + end) local ui_utils = require("leetcode-ui.utils") ui_utils.buf_set_opts(self.bufnr, { @@ -213,8 +410,11 @@ function Submissions:init(parent) }) self.question = parent - self.state = "loading" + self.active_tab = "submissions" + self.submissions_state = "loading" + self.memory_state = "loading" self.submissions = parent.past_submissions or {} + self.memory_summary = parent.mem0_recall_summary end ---@type fun(parent: lc.ui.Question): lc.ui.Submissions From f281bacbf4c49719503a195b5bf9d387f05abf14 Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Sun, 7 Jun 2026 21:03:54 -0700 Subject: [PATCH 18/24] feat: add memory refresh functionality and manage active panels --- lua/leetcode-ui/split/submissions.lua | 45 +++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/lua/leetcode-ui/split/submissions.lua b/lua/leetcode-ui/split/submissions.lua index a3202b76..e9c76d1c 100644 --- a/lua/leetcode-ui/split/submissions.lua +++ b/lua/leetcode-ui/split/submissions.lua @@ -5,6 +5,7 @@ local Line = require("leetcode-ui.line") local Padding = require("leetcode-ui.lines.padding") local Split = require("leetcode-ui.split") local ui_utils = require("leetcode-ui.utils") +local active_panels_by_slug = {} ---@class lc.ui.Submissions : lc-ui.Split ---@field question lc.ui.Question @@ -299,6 +300,32 @@ function Submissions:update_memory(summary) self:draw() end +function Submissions:refresh_memory() + local ok, saver = pcall(require, "submission_db_saver") + if not ok then + self:set_memory_error("submission_db_saver.lua not found") + return + end + + self:set_memory_loading() + saver.get_mem0_recall_summary(self.question, function(response) + vim.schedule(function() + if not self._.mounted then + debug_log("refresh_memory callback ignored because panel unmounted") + return + end + + debug_log("refresh_memory response=" .. vim.inspect(response)) + if response.error then + self:set_memory_error(response.error) + return + end + + self:update_memory(response) + end) + end) +end + ---@param tab "submissions"|"memory" function Submissions:switch_tab(tab) if self.active_tab == tab then @@ -360,6 +387,7 @@ end function Submissions:mount() Submissions.super.mount(self) + active_panels_by_slug[self.question.q.title_slug] = self self:fetch() self:map("n", { "1", "1" }, function() self:switch_tab("submissions") @@ -396,6 +424,13 @@ function Submissions:mount() return self end +function Submissions:unmount() + if active_panels_by_slug[self.question.q.title_slug] == self then + active_panels_by_slug[self.question.q.title_slug] = nil + end + return Submissions.super.unmount(self) +end + ---@param parent lc.ui.Question function Submissions:init(parent) Submissions.super.init(self, { @@ -420,4 +455,14 @@ end ---@type fun(parent: lc.ui.Question): lc.ui.Submissions local LeetSubmissions = Submissions +function LeetSubmissions.refresh_memory_for_slug(title_slug) + local panel = active_panels_by_slug[title_slug] + if not panel or not panel._.mounted then + return false + end + + panel:refresh_memory() + return true +end + return LeetSubmissions From 54cdeff87cbe17feef92f39403654e6345f61265 Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Sun, 7 Jun 2026 22:51:21 -0700 Subject: [PATCH 19/24] feat: add compact_string_list function and enhance session_failure_summaries for improved error handling --- lua/leetcode-ui/split/submissions.lua | 44 +++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/lua/leetcode-ui/split/submissions.lua b/lua/leetcode-ui/split/submissions.lua index e9c76d1c..b61d581f 100644 --- a/lua/leetcode-ui/split/submissions.lua +++ b/lua/leetcode-ui/split/submissions.lua @@ -126,6 +126,41 @@ local function insert_wrapped_value(layout, label, content, max_width, value_hl) end end +local function compact_string_list(values, limit) + if type(values) ~= "table" then + return {} + end + + local result = {} + local seen = {} + + for _, value in ipairs(values) do + if value and value ~= vim.NIL and value ~= "" then + local text = tostring(value) + if not seen[text] then + table.insert(result, text) + seen[text] = true + end + end + + if limit and #result >= limit then + break + end + end + + return result +end + +local function session_failure_summaries(session) + local summaries = compact_string_list(session.failureSummaries, 5) + + if #summaries == 0 and session.failureSummary and session.failureSummary ~= vim.NIL and session.failureSummary ~= "" then + table.insert(summaries, tostring(session.failureSummary)) + end + + return summaries +end + local function memory_status_hl(status) if status == "Accepted" then return "leetcode_easy" @@ -228,8 +263,13 @@ function Submissions:populate() end layout:insert(header) - if session.failureSummary and session.failureSummary ~= vim.NIL and session.failureSummary ~= "" then - insert_wrapped_value(layout, "failure: ", session.failureSummary, max_width) + if session.distinctMistakeCount and session.distinctMistakeCount ~= vim.NIL then + insert_wrapped_value(layout, "mistakes: ", tostring(session.distinctMistakeCount), max_width) + end + + local failure_summaries = session_failure_summaries(session) + if #failure_summaries > 0 then + insert_wrapped_value(layout, "failure: ", table.concat(failure_summaries, " | "), max_width) end if type(session.stuckPoints) == "table" and #session.stuckPoints > 0 then From 7e89b6c0c5b5f75d2697e969dd7e29a8e0a09cd6 Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Sun, 7 Jun 2026 23:11:08 -0700 Subject: [PATCH 20/24] feat: enhance submissions display with similar matches and update memory state logic --- lua/leetcode-ui/split/submissions.lua | 51 ++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/lua/leetcode-ui/split/submissions.lua b/lua/leetcode-ui/split/submissions.lua index b61d581f..7297e40a 100644 --- a/lua/leetcode-ui/split/submissions.lua +++ b/lua/leetcode-ui/split/submissions.lua @@ -280,6 +280,50 @@ function Submissions:populate() insert_wrapped_value(layout, "thought: ", table.concat(session.thoughtProcess, " | "), max_width) end end + + local similar_matches = summary.similar_matches or {} + if #similar_matches > 0 then + layout:insert(Padding(1)) + + local similar_title = Line() + similar_title:append("类似题:", "leetcode_list") + similar_title:append(tostring(summary.similar_match_count or #similar_matches), "leetcode_normal") + layout:insert(similar_title) + + for index, match in ipairs(similar_matches) do + layout:insert(Padding(1)) + + local header = Line() + header:append(("%d. "):format(index), "leetcode_list") + header:append(match.titleSlug or "unknown", "leetcode_medium") + if match.difficulty and match.difficulty ~= vim.NIL then + header:append(" ", "leetcode_alt") + header:append(match.difficulty, "leetcode_alt") + end + if match.score and match.score ~= vim.NIL then + header:append(" score=", "leetcode_list") + header:append(tostring(match.score), "leetcode_normal") + end + layout:insert(header) + + if match.profile and match.profile.problemSummary and match.profile.problemSummary ~= "" then + insert_wrapped_value(layout, "why: ", match.profile.problemSummary, max_width) + end + + local failure_summaries = compact_string_list(match.failureSummaries, 5) + if #failure_summaries > 0 then + insert_wrapped_value(layout, "failure: ", table.concat(failure_summaries, " | "), max_width) + end + + if type(match.stuckPoints) == "table" and #match.stuckPoints > 0 then + insert_wrapped_value(layout, "stuck: ", table.concat(match.stuckPoints, " | "), max_width) + end + + if type(match.thoughtProcess) == "table" and #match.thoughtProcess > 0 then + insert_wrapped_value(layout, "thought: ", table.concat(match.thoughtProcess, " | "), max_width) + end + end + end end self.renderer:replace({ layout }) @@ -335,7 +379,12 @@ end function Submissions:update_memory(summary) self.memory_summary = summary or {} self.memory_message = nil - self.memory_state = summary and summary.has_history and "ready" or "empty" + local has_memory = summary and ( + summary.has_history + or ((summary.similar_match_count or 0) > 0) + or (type(summary.similar_matches) == "table" and #summary.similar_matches > 0) + ) + self.memory_state = has_memory and "ready" or "empty" self.question.mem0_recall_summary = self.memory_summary self:draw() end From 5f7fd782c66ce574a634930df3e6d7d2ef28bf5b Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Sun, 7 Jun 2026 23:15:10 -0700 Subject: [PATCH 21/24] feat: add support for similar questions in submissions panel with updated state management --- lua/leetcode-ui/split/submissions.lua | 111 ++++++++++++++++---------- 1 file changed, 67 insertions(+), 44 deletions(-) diff --git a/lua/leetcode-ui/split/submissions.lua b/lua/leetcode-ui/split/submissions.lua index 7297e40a..952dd806 100644 --- a/lua/leetcode-ui/split/submissions.lua +++ b/lua/leetcode-ui/split/submissions.lua @@ -9,13 +9,15 @@ local active_panels_by_slug = {} ---@class lc.ui.Submissions : lc-ui.Split ---@field question lc.ui.Question ----@field active_tab "submissions"|"memory" +---@field active_tab "submissions"|"memory"|"similar" ---@field submissions_state "loading"|"ready"|"empty"|"error" ---@field memory_state "loading"|"ready"|"empty"|"error" +---@field similar_state "loading"|"ready"|"empty"|"error" ---@field submissions table[] ---@field message? string ---@field memory_summary? table ---@field memory_message? string +---@field similar_message? string local Submissions = Split:extend("LeetSubmissions") local function debug_log(msg) @@ -187,6 +189,8 @@ function Submissions:populate() tabs:append("[1] 历史提交", self.active_tab == "submissions" and "leetcode_medium" or "leetcode_alt") tabs:append(" | ", "leetcode_alt") tabs:append("[2] 历史记忆", self.active_tab == "memory" and "leetcode_medium" or "leetcode_alt") + tabs:append(" | ", "leetcode_alt") + tabs:append("[3] 类似题", self.active_tab == "similar" and "leetcode_medium" or "leetcode_alt") layout:insert(tabs) layout:insert(Padding(1)) @@ -231,19 +235,19 @@ function Submissions:populate() layout:insert(Padding(1)) end end - elseif self.memory_state == "loading" then + elseif self.active_tab == "memory" and self.memory_state == "loading" then local line = Line() line:append("正在加载历史记忆...", "leetcode_alt") layout:insert(line) - elseif self.memory_state == "error" then + elseif self.active_tab == "memory" and self.memory_state == "error" then local line = Line() line:append(self.memory_message or "历史记忆暂不可用", "leetcode_hard") layout:insert(line) - elseif self.memory_state == "empty" then + elseif self.active_tab == "memory" and self.memory_state == "empty" then local line = Line() line:append("这道题还没有可回忆的历史记录", "leetcode_alt") layout:insert(line) - else + elseif self.active_tab == "memory" then local summary = self.memory_summary or {} local count_line = Line() count_line:append("历史记录数:", "leetcode_list") @@ -280,48 +284,57 @@ function Submissions:populate() insert_wrapped_value(layout, "thought: ", table.concat(session.thoughtProcess, " | "), max_width) end end - + elseif self.similar_state == "loading" then + local line = Line() + line:append("正在加载类似题...", "leetcode_alt") + layout:insert(line) + elseif self.similar_state == "error" then + local line = Line() + line:append(self.similar_message or "类似题暂不可用", "leetcode_hard") + layout:insert(line) + elseif self.similar_state == "empty" then + local line = Line() + line:append("这道题还没有可回忆的类似题", "leetcode_alt") + layout:insert(line) + else + local summary = self.memory_summary or {} local similar_matches = summary.similar_matches or {} - if #similar_matches > 0 then - layout:insert(Padding(1)) - - local similar_title = Line() - similar_title:append("类似题:", "leetcode_list") - similar_title:append(tostring(summary.similar_match_count or #similar_matches), "leetcode_normal") - layout:insert(similar_title) + local similar_title = Line() + similar_title:append("类似题:", "leetcode_list") + similar_title:append(tostring(summary.similar_match_count or #similar_matches), "leetcode_normal") + layout:insert(similar_title) - for index, match in ipairs(similar_matches) do - layout:insert(Padding(1)) + for index, match in ipairs(similar_matches) do + layout:insert(Padding(1)) - local header = Line() - header:append(("%d. "):format(index), "leetcode_list") - header:append(match.titleSlug or "unknown", "leetcode_medium") - if match.difficulty and match.difficulty ~= vim.NIL then - header:append(" ", "leetcode_alt") - header:append(match.difficulty, "leetcode_alt") - end - if match.score and match.score ~= vim.NIL then - header:append(" score=", "leetcode_list") - header:append(tostring(match.score), "leetcode_normal") - end - layout:insert(header) + local header = Line() + header:append(("%d. "):format(index), "leetcode_list") + header:append(match.titleSlug or "unknown", "leetcode_medium") + if match.difficulty and match.difficulty ~= vim.NIL then + header:append(" ", "leetcode_alt") + header:append(match.difficulty, "leetcode_alt") + end + if match.score and match.score ~= vim.NIL then + header:append(" score=", "leetcode_list") + header:append(tostring(match.score), "leetcode_normal") + end + layout:insert(header) - if match.profile and match.profile.problemSummary and match.profile.problemSummary ~= "" then - insert_wrapped_value(layout, "why: ", match.profile.problemSummary, max_width) - end + if match.profile and match.profile.problemSummary and match.profile.problemSummary ~= "" then + insert_wrapped_value(layout, "why: ", match.profile.problemSummary, max_width) + end - local failure_summaries = compact_string_list(match.failureSummaries, 5) - if #failure_summaries > 0 then - insert_wrapped_value(layout, "failure: ", table.concat(failure_summaries, " | "), max_width) - end + local failure_summaries = compact_string_list(match.failureSummaries, 5) + if #failure_summaries > 0 then + insert_wrapped_value(layout, "failure: ", table.concat(failure_summaries, " | "), max_width) + end - if type(match.stuckPoints) == "table" and #match.stuckPoints > 0 then - insert_wrapped_value(layout, "stuck: ", table.concat(match.stuckPoints, " | "), max_width) - end + if type(match.stuckPoints) == "table" and #match.stuckPoints > 0 then + insert_wrapped_value(layout, "stuck: ", table.concat(match.stuckPoints, " | "), max_width) + end - if type(match.thoughtProcess) == "table" and #match.thoughtProcess > 0 then - insert_wrapped_value(layout, "thought: ", table.concat(match.thoughtProcess, " | "), max_width) - end + if type(match.thoughtProcess) == "table" and #match.thoughtProcess > 0 then + insert_wrapped_value(layout, "thought: ", table.concat(match.thoughtProcess, " | "), max_width) end end end @@ -362,7 +375,9 @@ end function Submissions:set_memory_loading() self.memory_state = "loading" + self.similar_state = "loading" self.memory_message = nil + self.similar_message = nil self.memory_summary = nil self:draw() end @@ -370,7 +385,9 @@ end ---@param msg string function Submissions:set_memory_error(msg) self.memory_state = "error" + self.similar_state = "error" self.memory_message = msg + self.similar_message = msg self.memory_summary = nil self:draw() end @@ -379,12 +396,14 @@ end function Submissions:update_memory(summary) self.memory_summary = summary or {} self.memory_message = nil - local has_memory = summary and ( - summary.has_history - or ((summary.similar_match_count or 0) > 0) + self.similar_message = nil + local has_memory = summary and summary.has_history + local has_similar = summary and ( + ((summary.similar_match_count or 0) > 0) or (type(summary.similar_matches) == "table" and #summary.similar_matches > 0) ) self.memory_state = has_memory and "ready" or "empty" + self.similar_state = has_similar and "ready" or "empty" self.question.mem0_recall_summary = self.memory_summary self:draw() end @@ -415,7 +434,7 @@ function Submissions:refresh_memory() end) end ----@param tab "submissions"|"memory" +---@param tab "submissions"|"memory"|"similar" function Submissions:switch_tab(tab) if self.active_tab == tab then return @@ -484,6 +503,9 @@ function Submissions:mount() self:map("n", { "2", "2" }, function() self:switch_tab("memory") end) + self:map("n", { "3", "3" }, function() + self:switch_tab("similar") + end) local ui_utils = require("leetcode-ui.utils") ui_utils.buf_set_opts(self.bufnr, { @@ -537,6 +559,7 @@ function Submissions:init(parent) self.active_tab = "submissions" self.submissions_state = "loading" self.memory_state = "loading" + self.similar_state = "loading" self.submissions = parent.past_submissions or {} self.memory_summary = parent.mem0_recall_summary end From 185a99c3f03e6bf550da8d36c49d789a8eaa353b Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Wed, 10 Jun 2026 00:54:21 -0700 Subject: [PATCH 22/24] feat: add auto_fence_code option to enhance code block rendering in chat responses --- README.md | 3 + lua/leetcode/config/template.lua | 1 + lua/leetcode/integrations/codecompanion.lua | 165 +++++++++++++++++++- 3 files changed, 168 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 73839247..453fd70f 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ require("leetcode").setup({ companion = { adapter = "submission_service", default_prompt = "Help me understand the bug in my current approach.", + auto_fence_code = true, }, }) ``` @@ -71,6 +72,8 @@ Then run `:Leet companion` on an open problem buffer. The plugin injects: as hidden chat context, while the visible prompt stays focused on what you want help with. +When `auto_fence_code` is enabled, `leetcode.nvim` will also wrap obviously code-like assistant replies in a fenced code block before re-rendering the CodeCompanion buffer. This gives Treesitter a better chance to apply syntax highlighting even when the submission service forgets to fence the snippet itself. + ## 📡 External Integration Example Connect to a submission server to save data: diff --git a/lua/leetcode/config/template.lua b/lua/leetcode/config/template.lua index 86a3fc21..6b46f7d5 100644 --- a/lua/leetcode/config/template.lua +++ b/lua/leetcode/config/template.lua @@ -128,6 +128,7 @@ local M = { adapter = "submission_service", ---@type string model = nil, ---@type string|nil auto_submit = true, ---@type boolean + auto_fence_code = true, ---@type boolean prompt_user = true, ---@type boolean default_prompt = "Help me understand this problem and my current code.", ---@type string window = nil, ---@type table|nil diff --git a/lua/leetcode/integrations/codecompanion.lua b/lua/leetcode/integrations/codecompanion.lua index 4e5da5aa..62ca0900 100644 --- a/lua/leetcode/integrations/codecompanion.lua +++ b/lua/leetcode/integrations/codecompanion.lua @@ -3,6 +3,7 @@ local lc_utils = require("leetcode.utils") local M = {} local active_chats_by_slug = {} +local FENCE = "````" local function notify(message, level) vim.notify(message, level or vim.log.levels.INFO, { title = "leetcode.nvim" }) @@ -36,6 +37,161 @@ local function bind_chat(title_slug, chat) active_chats_by_slug[title_slug] = chat end +local function has_code_fence(content) + return type(content) == "string" and (content:find("```", 1, true) ~= nil) +end + +local function is_blank(line) + return vim.trim(line) == "" +end + +local function is_code_like_line(line) + local trimmed = vim.trim(line) + if trimmed == "" then + return false + end + + if line:match("^%s%s%s%s+") or line:match("^\t+") then + return true + end + + if trimmed:match("^#") or trimmed:match('^"""') or trimmed:match("^'''") then + return true + end + + if trimmed:match("^class%s+[%w_]+") or trimmed:match("^def%s+[%w_]+") then + return true + end + + if trimmed:match("^[%w_%.]+%s*=%s*.+") then + return true + end + + if trimmed:match("^[%w_%.]+%b()%s*:?$") then + return true + end + + if + trimmed:match("^(from|import|return|yield|break|continue|pass)%f[%W]") + or trimmed:match("^(if|elif|else|for|while|with|try|except|finally)%f[%W]") + then + return true + end + + if trimmed:match("->") or trimmed:match("[%[%]{}]") then + return true + end + + return false +end + +local function find_trailing_code_block(lines) + local block_end + for index = #lines, 1, -1 do + if not is_blank(lines[index]) then + block_end = index + break + end + end + + if not block_end then + return nil + end + + local code_lines = 0 + local nonempty_lines = 0 + local block_start = block_end + + for index = block_end, 1, -1 do + local line = lines[index] + if is_blank(line) then + block_start = index + elseif is_code_like_line(line) then + block_start = index + code_lines = code_lines + 1 + nonempty_lines = nonempty_lines + 1 + else + break + end + end + + if nonempty_lines < 4 or code_lines < 4 then + return nil + end + + if code_lines / nonempty_lines < 0.7 then + return nil + end + + return block_start, block_end +end + +local function auto_fence_code_blocks(content, lang) + if type(content) ~= "string" or content == "" or has_code_fence(content) then + return content, false + end + + local lines = vim.split(content, "\n", { plain = true, trimempty = false }) + local block_start, block_end = find_trailing_code_block(lines) + if not block_start or not block_end then + return content, false + end + + local updated = {} + for index = 1, block_start - 1 do + table.insert(updated, lines[index]) + end + table.insert(updated, ("%s%s"):format(FENCE, lang or "text")) + for index = block_start, block_end do + table.insert(updated, lines[index]) + end + table.insert(updated, FENCE) + for index = block_end + 1, #lines do + table.insert(updated, lines[index]) + end + + return table.concat(updated, "\n"), true +end + +local function rerender_chat(chat) + if not (chat and chat.ui) then + return + end + + chat.ui:render(chat.buffer_context, vim.deepcopy(chat.messages), { + stop_context_insertion = true, + }) + if chat.context then + chat.context:render() + end +end + +local function normalize_latest_llm_message(chat, lang) + if not is_chat_valid(chat) then + return + end + + for index = #chat.messages, 1, -1 do + local message = chat.messages[index] + if message.role == config.constants.LLM_ROLE and type(message.content) == "string" and message.content ~= "" then + message._meta = message._meta or {} + if message._meta.leetcode_auto_fenced then + return + end + + local normalized, changed = auto_fence_code_blocks(message.content, lang) + if not changed then + return + end + + message.content = normalized + message._meta.leetcode_auto_fenced = true + rerender_chat(chat) + return + end + end +end + local function unbind_chat(title_slug, chat) if title_slug == "" then return @@ -256,6 +412,8 @@ function M.open(opts) local cc = config.user.companion or {} local prompt = opts.prompt or cc.default_prompt or "" + local auto_submit = cc.auto_submit ~= false + local auto_fence = cc.auto_fence_code ~= false local params = { adapter = cc.adapter, } @@ -267,7 +425,7 @@ function M.open(opts) local context = M.build_context(question) local title_slug = question_slug(question) return codecompanion.chat({ - auto_submit = cc.auto_submit ~= false, + auto_submit = auto_submit, ignore_system_prompt = true, params = params, title = ("LeetCode: %s"):format(context.title ~= "" and context.title or context.title_slug), @@ -289,6 +447,11 @@ function M.open(opts) } ) end, + on_completed = function(chat) + if auto_fence and auto_submit then + normalize_latest_llm_message(chat, context.lang) + end + end, on_closed = function(chat) unbind_chat(title_slug, chat) end, From 1dc9a5561115daa1446578568f4b5e72446066ff Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Thu, 11 Jun 2026 04:07:27 -0700 Subject: [PATCH 23/24] feat: add .playwright-mcp/ to .gitignore to exclude Playwright configuration files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index fbe24427..6a651a7b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /.luarc.json README.zh.md +.playwright-mcp/ \ No newline at end of file From 6a43c5d5a7a3f46d1cbda1b046add091dedd4ab0 Mon Sep 17 00:00:00 2001 From: Jingyi Zhao Date: Fri, 26 Jun 2026 00:53:58 -0700 Subject: [PATCH 24/24] feat: add AI assist toggle functionality for enhanced error analysis --- README.md | 9 ++++++ lua/leetcode-ui/question.lua | 53 +++++++++++++++++++++++++++++--- lua/leetcode/command/init.lua | 36 ++++++++++++++++++++++ lua/leetcode/config/template.lua | 4 +++ 4 files changed, 98 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 453fd70f..87247f79 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Track problems, time spent, and submissions via external services. This fork adds submission tracking and session management capabilities to leetcode.nvim: - **Session Timer Commands**: `:Leet timer_start` / `:Leet timer_stop` +- **AI Assist Toggle**: `:Leet ai_assist [on|off|toggle]` - **Extended Hooks**: `timer_start`, `question_leave`, `on_test_result`, and improved upload hooks - **Submission Side Panel**: Optional left-bottom panel for past submissions - **External Integration**: Wire hooks to external APIs for persistence @@ -45,6 +46,9 @@ Existing hooks also trigger on submissions: :Leet timer_stop " Stop tracking (drop session without saving) :Leet session start " Alias for timer_start :Leet session stop " Alias for timer_stop +:Leet ai_assist " Toggle auto AI failure analysis for the current question +:Leet ai_assist on " Force auto AI failure analysis on +:Leet ai_assist off " Force auto AI failure analysis off :Leet companion " Open a CodeCompanion chat seeded with the current LeetCode context ``` @@ -59,6 +63,9 @@ require("leetcode").setup({ default_prompt = "Help me understand the bug in my current approach.", auto_fence_code = true, }, + ai_assist = { + enabled = true, + }, }) ``` @@ -74,6 +81,8 @@ as hidden chat context, while the visible prompt stays focused on what you want When `auto_fence_code` is enabled, `leetcode.nvim` will also wrap obviously code-like assistant replies in a fenced code block before re-rendering the CodeCompanion buffer. This gives Treesitter a better chance to apply syntax highlighting even when the submission service forgets to fence the snippet itself. +When the session timer is running, the winbar also shows `AI:on` or `AI:off` so you can see whether failed test and submit events will trigger your external static-analysis assistant. + ## 📡 External Integration Example Connect to a submission server to save data: diff --git a/lua/leetcode-ui/question.lua b/lua/leetcode-ui/question.lua index b62c105b..f7124401 100644 --- a/lua/leetcode-ui/question.lua +++ b/lua/leetcode-ui/question.lua @@ -25,6 +25,54 @@ local log = require("leetcode.logger") ---@field reset boolean local Question = Object("LeetQuestion") +local function ai_assist_default_enabled() + local ai_assist = config.user.ai_assist or {} + return ai_assist.enabled ~= false +end + +local function ai_assist_label(enabled) + local state = enabled and "on" or "off" + local hl = enabled and "leetcode_easy" or "leetcode_error" + return string.format(" %%#leetcode_alt#AI%%*:%%#%s#%s%%*", hl, state) +end + +function Question:is_auto_ai_assist_enabled() + if self._auto_ai_assist_enabled == nil then + self._auto_ai_assist_enabled = ai_assist_default_enabled() + end + return self._auto_ai_assist_enabled +end + +function Question:set_auto_ai_assist_enabled(enabled) + self._auto_ai_assist_enabled = not not enabled + if self._session_timer then + self:render_session_winbar() + end +end + +function Question:toggle_auto_ai_assist() + local enabled = not self:is_auto_ai_assist_enabled() + self:set_auto_ai_assist_enabled(enabled) + return enabled +end + +function Question:render_session_winbar(elapsed_s) + if not (self.winid and vim.api.nvim_win_is_valid(self.winid)) then + return + end + + elapsed_s = elapsed_s or 0 + local mins = math.floor(elapsed_s / 60) + local secs = elapsed_s % 60 + local label = string.format( + " %%#leetcode_timer#⏱ %02d:%02d%%*%s", + mins, + secs, + ai_assist_label(self:is_auto_ai_assist_enabled()) + ) + vim.api.nvim_set_option_value("winbar", label, { win = self.winid }) +end + ---@param raw? boolean function Question:snippet(raw) local snippets = self.q.code_snippets ~= vim.NIL and self.q.code_snippets or {} @@ -347,15 +395,12 @@ function Question:start_timer_display() local function update_winbar() local elapsed_s = math.floor((vim.loop.hrtime() / 1e6 - start_ms) / 1000) - local mins = math.floor(elapsed_s / 60) - local secs = elapsed_s % 60 - local label = string.format(" %%#leetcode_timer#⏱ %02d:%02d%%*", mins, secs) vim.schedule(function() if not (self.winid and vim.api.nvim_win_is_valid(self.winid)) then self:stop_timer_display() return end - vim.api.nvim_set_option_value("winbar", label, { win = self.winid }) + self:render_session_winbar(elapsed_s) end) end diff --git a/lua/leetcode/command/init.lua b/lua/leetcode/command/init.lua index ec12bb85..ed8ae833 100644 --- a/lua/leetcode/command/init.lua +++ b/lua/leetcode/command/init.lua @@ -295,6 +295,36 @@ function cmd.timer_stop() utils.exec_hooks("timer_stop", q) end +local function set_ai_assist(enabled) + local utils = require("leetcode.utils") + local q = utils.curr_question() + if not q then + return + end + + q:set_auto_ai_assist_enabled(enabled) + log.info(("Auto AI assist %s"):format(enabled and "on" or "off")) +end + +function cmd.ai_assist() + local utils = require("leetcode.utils") + local q = utils.curr_question() + if not q then + return + end + + local enabled = q:toggle_auto_ai_assist() + log.info(("Auto AI assist %s"):format(enabled and "on" or "off")) +end + +function cmd.ai_assist_on() + set_ai_assist(true) +end + +function cmd.ai_assist_off() + set_ai_assist(false) +end + function cmd.q_upload_test_result() local utils = require("leetcode.utils") utils.auth_guard() @@ -664,6 +694,12 @@ cmd.commands = { inject = { cmd.inject }, fold = { cmd.fold }, companion = { cmd.companion }, + ai_assist = { + cmd.ai_assist, + on = { cmd.ai_assist_on }, + off = { cmd.ai_assist_off }, + toggle = { cmd.ai_assist }, + }, -- session = { -- change = { -- cmd.change_session, diff --git a/lua/leetcode/config/template.lua b/lua/leetcode/config/template.lua index 6b46f7d5..40f4a149 100644 --- a/lua/leetcode/config/template.lua +++ b/lua/leetcode/config/template.lua @@ -134,6 +134,10 @@ local M = { window = nil, ---@type table|nil }, + ai_assist = { + enabled = true, ---@type boolean + }, + hooks = { ---@type fun()[] ["enter"] = {},