diff --git a/.gitignore b/.gitignore index 8db40be2..fbe24427 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /.luarc.json +README.zh.md diff --git a/.stylua.toml b/.stylua.toml index e9668dde..8459d457 100644 --- a/.stylua.toml +++ b/.stylua.toml @@ -4,4 +4,3 @@ indent_type = "Spaces" indent_width = 4 quote_style = "ForceDouble" call_parentheses = "Always" -collapse_simple_statement = "Always" diff --git a/README.md b/README.md index ea1f8bb4..57ed6f9f 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,10 @@
-🚨 **leetcode.nvim is currently in the _alpha stage_ of development** 🚨 - ---- - # leetcode.nvim 🔥 Solve [LeetCode] problems within [Neovim] 🔥 -🇺🇸 English, 🇨🇳 简体中文 +
@@ -30,15 +26,15 @@ https://github.com/kawre/leetcode.nvim/assets/69250723/aee6584c-e099-4409-b114-1 - [Neovim] >= 0.9.0 -- [telescope.nvim] +- [telescope.nvim] or [fzf-lua] + +- [plenary.nvim] - [nui.nvim] -- [nvim-treesitter] _**(optional, but highly recommended)**_ +- [tree-sitter-html] _**(optional, but highly recommended)**_ used for formatting the question description. - Make sure to install the parser for `html`. - -- [nvim-notify] _**(optional)**_ + Can be installed with [nvim-treesitter]. - [Nerd Font][nerd-font] & [nvim-web-devicons] _**(optional)**_ @@ -49,16 +45,12 @@ https://github.com/kawre/leetcode.nvim/assets/69250723/aee6584c-e099-4409-b114-1 ```lua { "kawre/leetcode.nvim", - build = ":TSUpdate html", + build = ":TSUpdate html", -- if you have `nvim-treesitter` installed dependencies = { "nvim-telescope/telescope.nvim", - "nvim-lua/plenary.nvim", -- required by telescope + -- "ibhagwan/fzf-lua", + "nvim-lua/plenary.nvim", "MunifTanjim/nui.nvim", - - -- optional - "nvim-treesitter/nvim-treesitter", - "rcarriga/nvim-notify", - "nvim-tree/nvim-web-devicons", }, opts = { -- configuration goes here @@ -86,8 +78,16 @@ To see full configuration types see [template.lua](./lua/leetcode/config/templat translate_problems = true, ---@type boolean }, - ---@type string - directory = vim.fn.stdpath("data") .. "/leetcode/", + ---@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, @@ -127,16 +127,22 @@ To see full configuration types see [template.lua](./lua/leetcode/config/templat show_stats = true, ---@type boolean }, + ---@type lc.picker + picker = { provider = nil }, + hooks = { ---@type fun()[] - LeetEnter = {}, + ["enter"] = {}, ---@type fun(question: lc.ui.Question)[] - LeetQuestionNew = {}, + ["question_enter"] = {}, + + ---@type fun()[] + ["leave"] = {}, }, keys = { - toggle = { "q", "" }, ---@type string|string[] + toggle = { "q" }, ---@type string|string[] confirm = { "" }, ---@type string|string[] reset_testcases = "r", ---@type string @@ -145,8 +151,11 @@ To see full configuration types see [template.lua](./lua/leetcode/config/templat focus_result = "L", ---@type string }, + ---@type lc.highlights + theme = {}, + ---@type boolean - image_support = false, -- setting this to `true` will disable question description wrap + image_support = false, } ``` @@ -170,6 +179,34 @@ Language to start your session with 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] @@ -182,13 +219,27 @@ cn = { -- leetcode.cn }, ``` -### directory +### storage -Where to store [leetcode.nvim] data +storage directories ```lua ----@type string -directory = vim.fn.stdpath("data") .. "/leetcode/" +---@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 @@ -204,8 +255,18 @@ logging = true Inject code before or after your solution, injected code won't be submitted or run. +#### default imports + +You can also pass `before = true` to inject default imports for the language. +Supported languages are `python`, `python3`, `java` + +Access default imports via `require("leetcode.config.imports")` + ```lua injector = { ---@type table + ["python3"] = { + before = true + }, ["cpp"] = { before = { "#include ", "using namespace std;" }, after = "int main() {}", @@ -216,6 +277,17 @@ injector = { ---@type table } ``` +### picker + +Supported picker providers are `telescope` and `fzf-lua`. +When provider is `nil`, [leetcode.nvim] will first try to use `fzf-lua`, +if not found it will fallback to `telescope`. + +```lua +---@type lc.picker +picker = { provider = nil }, +``` + ### hooks List of functions that get executed on specified event @@ -223,10 +295,31 @@ List of functions that get executed on specified event ```lua hooks = { ---@type fun()[] - LeetEnter = {}, + ["enter"] = {}, ---@type fun(question: lc.ui.Question)[] - LeetQuestionNew = {}, + ["question_enter"] = {}, + + ---@type fun()[] + ["leave"] = {}, +}, +``` + +### theme + +Override the [default theme](./lua/leetcode/theme/default.lua). + +Each value is the same type as val parameter in `:help nvim_set_hl` + +```lua +---@type lc.highlights +theme = { + ["alt"] = { + bg = "#FFFFFF", + }, + ["normal"] = { + fg = "#EA4AAA", + }, }, ``` @@ -234,9 +327,13 @@ hooks = { 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, -- setting this to `true` will disable question description wrap +image_support = false, ``` ## 📋 Commands @@ -245,12 +342,16 @@ image_support = false, -- setting this to `true` will disable question descripti - `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 current question solution + - `lang` opens a picker to change the language of the current question - `run` run currently opened question @@ -263,7 +364,24 @@ image_support = false, -- setting this to `true` will disable question descripti - `daily` opens the question of today -- [`list`](#leet-list) opens a problemlist picker +- `list` opens a problem list picker + +- `open` opens the current question in a default browser + +- `reset` reset current question to default code definition + +- `last_submit` retrieve last submitted code for the current question + +- `restore` try to restore default question layout + +- `inject` re-inject code for the current question + +- `session` + + - `create` create a new session + - `change` change the current session + + - `update` update the current session in case it went out of sync - `desc` toggle question description @@ -281,23 +399,34 @@ image_support = false, -- setting this to `true` will disable question descripti - `update` updates cache -#### `Leet list` +#### Some commands can take optional arguments. To stack argument values separate them by a `,` -Can take optional arguments. To stack argument values separate them by a `,` +- `Leet list` -``` -Leet list status= difficulty= -``` + ``` + Leet list status= difficulty= + ``` + +- `Leet random` + + ``` + Leet random status= difficulty= tags= + ``` ## 🚀 Usage -This plugin is meant to be used within a **fresh** [Neovim] instance. -Meaning that to launch [leetcode.nvim] you **have** to pass -[`arg`](#arg) as the _first and **only**_ [Neovim] argument +This plugin can be initiated in two ways: -``` -nvim leetcode.nvim -``` +- To start [leetcode.nvim], simply pass [`arg`](#arg) + as the _first and **only**_ [Neovim] argument + + ``` + nvim leetcode.nvim + ``` + +- _**(Experimental)**_ Alternatively, you can 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. ### Switching between questions @@ -311,30 +440,54 @@ https://github.com/kawre/leetcode.nvim/assets/69250723/b7be8b95-5e2c-4153-8845-4 ## 🍴 Recipes -### lazy loading +### 💤 lazy loading with [lazy.nvim] -- proper lazy loading with [lazy.nvim] +> [!WARNING] +> opting for either option makes the alternative +> launch method unavailable due to lazy loading -```lua -local leet_arg = "leetcode.nvim" +- with [`arg`](#arg) -return { - "kawre/leetcode.nvim", - ... - lazy = leet_arg ~= vim.fn.argv()[1], - opts = { - arg = leet_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, } ``` -## ✅ Todo - -- \[x\] CN version -- \[x\] Statistics menu page -- \[ \] Docs -- \[x\] Hints pop-up +You can then exit [leetcode.nvim] using `:Leet exit` command ## 🙌 Credits @@ -350,7 +503,9 @@ return { [neovim]: https://github.com/neovim/neovim [nerd-font]: https://www.nerdfonts.com [nui.nvim]: https://github.com/MunifTanjim/nui.nvim -[nvim-notify]: https://github.com/rcarriga/nvim-notify [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 +[tree-sitter-html]: https://github.com/tree-sitter/tree-sitter-html +[plenary.nvim]: https://github.com/nvim-lua/plenary.nvim diff --git a/README.zh.md b/README.zh.md deleted file mode 100644 index 84e5fd4b..00000000 --- a/README.zh.md +++ /dev/null @@ -1,359 +0,0 @@ -
- -🚨 **leetcode.nvim 目前处于 _alpha 阶段_ 开发中** 🚨 - ---- - -# leetcode.nvim - -🔥 在 [Neovim] 中解决 [LeetCode] 问题 🔥 - -🇺🇸 English, 🇨🇳 简体中文 - -
- -https://github.com/kawre/leetcode.nvim/assets/69250723/aee6584c-e099-4409-b114-123cb32b7563 - -## ✨ 特性 - -- 📌 直观的仪表板,轻松导航 [leetcode.nvim] 内 - -- 😍 更好的可读性的问题描述格式 - -- 📈 在 [Neovim] 中显示 [LeetCode] 个人统计信息 - -- 🔀 支持每日和随机问题 - -- 💾 缓存以优化性能 - -## 📬 环境要求 - -- [Neovim] >= 0.9.0 - -- [telescope.nvim] - -- [nui.nvim] - -- [nvim-treesitter] _**(可选,但强烈推荐)**_ - 用于格式化问题描述。 - 确保安装 `html` 解析器。 - -- [nvim-notify] _**(可选)**_ - -- [Nerd Font][nerd-font] & [nvim-web-devicons] _**(可选)**_ - -## 📦 安装 - -- [lazy.nvim] - -```lua -{ - "kawre/leetcode.nvim", - build = ":TSUpdate html", - dependencies = { - "nvim-telescope/telescope.nvim", - "nvim-lua/plenary.nvim", -- telescope 所需 - "MunifTanjim/nui.nvim", - - -- 可选 - "nvim-treesitter/nvim-treesitter", - "rcarriga/nvim-notify", - "nvim-tree/nvim-web-devicons", - }, - opts = { - -- 配置放在这里 - cn = { - enabled = true, - }, - }, -} -``` - -## 🛠️ 配置 - -要查看完整的配置类型,请参见 [template.lua](./lua/leetcode/config/template.lua) - -### ⚙️ 默认配置 - -```lua -{ - ---@type string - arg = "leetcode.nvim", - - ---@type lc.lang - lang = "cpp", - - cn = { -- leetcode.cn - enabled = false, ---@type boolean - translator = true, ---@type boolean - translate_problems = true, ---@type boolean - }, - - ---@type string - directory = vim.fn.stdpath("data") .. "/leetcode/", - - ---@type boolean - logging = true, - - injector = {}, ---@type table - - cache = { - update_interval = 60 * 60 * 24 * 7, ---@type integer 7 days - }, - - 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 - }, - }, - - description = { - position = "left", ---@type lc.position - - width = "40%", ---@type lc.size - - show_stats = true, ---@type boolean - }, - - hooks = { - ---@type fun()[] - LeetEnter = {}, - - ---@type fun(question: lc.ui.Question)[] - LeetQuestionNew = {}, - }, - - 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 boolean - image_support = false, -- setting this to `true` will disable question description wrap -} -``` - -### arg - -[Neovim] 的参数 - -```lua ----@type string -arg = "leetcode.nvim" -``` - -有关更多信息,请参见 [usage](#-usage) - -### lang - -会话开始时使用的语言 - -```lua ----@type lc.lang -lang = "cpp" -``` - -### cn - -将 [leetcode.com][leetcode] 替换为 [leetcode.cn] - -```lua -cn = { -- leetcode.cn - enabled = false, ---@type boolean - translator = true, ---@type boolean - translate_problems = true, ---@type boolean -}, -``` - -### directory - -存储 [leetcode.nvim] 数据的位置 - -```lua ----@type string -directory = vim.fn.stdpath("data") .. "/leetcode/" -``` - -### logging - -是否记录 [leetcode.nvim] 状态通知 - -```lua ----@type boolean -logging = true -``` - -### injector - -在你的答案前后注入额外代码,注入的代码不会被提交或测试。 - -```lua -injector = { ---@type table - ["cpp"] = { - before = { "#include ", "using namespace std;" }, - after = "int main() {}", - }, - ["java"] = { - before = "import java.util.*;", - }, -} -``` - -### hooks - -在指定事件上执行的函数列表 - -```lua -hooks = { - ---@type fun()[] - LeetEnter = {}, - - ---@type fun(question: lc.ui.Question)[] - LeetQuestionNew = {}, -}, -``` - -### image support - -是否使用 [image.nvim] 渲染问题描述中的图片 - -```lua ----@type boolean -image_support = false, -- 将此设置为 `true` 将禁用问题描述的换行 -``` - -## 📋 命令 - -### `Leet` 打开菜单仪表板 - -- `menu` 与 `Leet` 相同 - -- `console` 打开当前打开问题的控制台弹出窗口 - -- `info` 打开包含当前打开问题信息的弹出窗口 - -- `tabs` 打开所有当前打开问题选项卡的选择器 - -- `lang` 打开更改当前问题语言的选择器 - -- `run` 运行当前打开的问题 - -- `test` 与 `Leet run` 相同 - -- `submit` 提交当前打开的问题 - -- `random` 打开一个随机问题 - -- `daily` 打开今天的问题 - -- [`list`](#leet-list) 打开问题列表选择器 - -- `desc` 切换问题描述 - - - `toggle` 与 `Leet desc` 相同 - - - `stats` 切换描述统计可见性 - -- `cookie` - - - `update` 打开提示输入新 cookie - - - `delete` 注销 - -- `cache` - - - `update` 更新缓存 - -#### `Leet list` - -可以带有可选参数。要堆叠参数值,请使用 , 将它们分隔开 - -``` -Leet list status= difficulty= -``` - -## 🚀 使用方法 - -此插件应该在 **全新** 的 [Neovim] 实例中使用。 -这意味着要启动 [leetcode.nvim],您 **必须** 将 -[`arg`](#arg) 作为 _第一个且 **唯一**_ [Neovim] 参数 - -``` -nvim leetcode.nvim -``` - -### 切换问题 - -要在问题之间切换,请使用 `Leet tabs`。 - -### 登录 - -使用 [leetcode.nvim] 必须 **登录** - -https://github.com/kawre/leetcode.nvim/assets/69250723/b7be8b95-5e2c-4153-8845-4ad3abeda5c3 - -## 🍴 示例 - -### 懒加载 - -- 使用 [lazy.nvim] 实现正确的懒加载 - -```lua -local leet_arg = "leetcode.nvim" - -return { - "kawre/leetcode.nvim", - ... - lazy = leet_arg ~= vim.fn.argv()[1], - opts = { - arg = leet_arg, - }, - ... -} -``` - -## ✅ 待办事项 - -- \[x\] 中文版本 -- \[x\] 统计菜单页面 -- \[ \] 文档 -- \[x\] 提示弹出窗口 - -## 🙌 鸣谢 - -- [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-notify]: https://github.com/rcarriga/nvim-notify -[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 diff --git a/lua/leetcode-plugins/cn/api.lua b/lua/leetcode-plugins/cn/api.lua index 1fd6e137..ee93f294 100644 --- a/lua/leetcode-plugins/cn/api.lua +++ b/lua/leetcode-plugins/cn/api.lua @@ -17,7 +17,9 @@ statistics.solved = function(cb) ---@diagnostic disable-line utils.query(query, variables, { endpoint = urls.solved, callback = function(res, err) - if err then return cb(nil, err) end + if err then + return cb(nil, err) + end local data = res.data local submit_stats = data["submit_stats"] @@ -28,6 +30,27 @@ statistics.solved = function(cb) ---@diagnostic disable-line }) end +---@param cb fun(res: lc.Stats.QuestionCount[], err: lc.err) +statistics.session_progress = function(cb) + local variables = { + userSlug = config.auth.name, + } + + local query = queries.session_progress + + utils.query(query, variables, { + callback = function(res, err) + if err then + return cb(nil, err) + end + + local data = res.data + local session_progress = data["userProfileUserQuestionProgress"]["numAcceptedQuestions"] + cb(session_progress) + end, + }) +end + statistics.calendar = function(cb) ---@diagnostic disable-line local variables = { username = config.auth.name, @@ -38,7 +61,9 @@ statistics.calendar = function(cb) ---@diagnostic disable-line utils.query(query, variables, { endpoint = urls.calendar, callback = function(res, err) - if err then return cb(nil, err) end + if err then + return cb(nil, err) + end local data = res.data local calendar = data["calendar"] @@ -64,7 +89,9 @@ statistics.languages = function(cb) ---@diagnostic disable-line utils.query(query, variables, { endpoint = urls.languages, callback = function(res, err) - if err then return cb(nil, err) end + if err then + return cb(nil, err) + end local data = res.data local lang_prob_count = data["languageProblemCount"] diff --git a/lua/leetcode-plugins/cn/init.lua b/lua/leetcode-plugins/cn/init.lua index b0434d36..6735a3a9 100644 --- a/lua/leetcode-plugins/cn/init.lua +++ b/lua/leetcode-plugins/cn/init.lua @@ -1,10 +1,17 @@ ---@class lc.plugins.cn local cn = {} +cn.opts = { + lazy = true, +} + function cn.load() local config = require("leetcode.config") + + config.translator = config.user.cn.translator config.domain = "cn" config.is_cn = true + config.sessions.default = "匿名" require("leetcode-plugins.cn.urls") require("leetcode-plugins.cn.queries") diff --git a/lua/leetcode-plugins/cn/queries.lua b/lua/leetcode-plugins/cn/queries.lua index 076207b9..f9d9d1f9 100644 --- a/lua/leetcode-plugins/cn/queries.lua +++ b/lua/leetcode-plugins/cn/queries.lua @@ -139,3 +139,24 @@ queries.skills = [[ } } ]] + +queries.streak = [[ + query getStreakCounter { + streakCounter: problemsetStreakCounter { + streakCount + daysSkipped + todayCompleted + } + } + ]] + +queries.session_progress = [[ + query userSessionProgress($userSlug: String!) { + userProfileUserQuestionProgress(userSlug: $userSlug) { + numAcceptedQuestions { + difficulty + count + } + } + } + ]] diff --git a/lua/leetcode-plugins/cn/urls.lua b/lua/leetcode-plugins/cn/urls.lua index 083d32f9..2285fba3 100644 --- a/lua/leetcode-plugins/cn/urls.lua +++ b/lua/leetcode-plugins/cn/urls.lua @@ -12,3 +12,4 @@ urls.interpret = "/problems/%s/interpret_solution/" urls.submit = "/problems/%s/submit/" urls.run = "/problems/%s/interpret_solution/" urls.check = "/submissions/detail/%s/check/" +urls.streak_counter = "/graphql/noj-go/" diff --git a/lua/leetcode-plugins/cn/utils.lua b/lua/leetcode-plugins/cn/utils.lua index 663200a3..b668e065 100644 --- a/lua/leetcode-plugins/cn/utils.lua +++ b/lua/leetcode-plugins/cn/utils.lua @@ -3,7 +3,9 @@ local log = require("leetcode.logger") local cn_utils = {} ---@param str string -local function capitalizeFirst(str) return str:lower():gsub("^%l", string.upper) end +local function capitalizeFirst(str) + return str:lower():gsub("^%l", string.upper) +end ---@return table function cn_utils.calc_question_count(stats) diff --git a/lua/leetcode-plugins/non_standalone/init.lua b/lua/leetcode-plugins/non_standalone/init.lua new file mode 100644 index 00000000..de095f4b --- /dev/null +++ b/lua/leetcode-plugins/non_standalone/init.lua @@ -0,0 +1,12 @@ +---@class lc.plugins.non_standalone +local non_standalone = {} + +non_standalone.opts = { + lazy = false, +} + +function non_standalone.load() + require("leetcode-plugins.non_standalone.leetcode") +end + +return non_standalone diff --git a/lua/leetcode-plugins/non_standalone/leetcode.lua b/lua/leetcode-plugins/non_standalone/leetcode.lua new file mode 100644 index 00000000..9cb95d61 --- /dev/null +++ b/lua/leetcode-plugins/non_standalone/leetcode.lua @@ -0,0 +1,65 @@ +---@diagnostic disable: invisible, duplicate-set-field + +local leetcode = require("leetcode") +local config = require("leetcode.config") + +local is_standalone = true +local prev_cwd = nil + +---@param on_vimenter boolean +leetcode.start = function(on_vimenter) + local skip, standalone = leetcode.should_skip(on_vimenter) + if skip then + return false + end + + config.setup() + + leetcode.setup_cmds() + + local theme = require("leetcode.theme") + theme.setup() + + if not on_vimenter then + if not standalone then + prev_cwd = vim.fn.getcwd() + vim.cmd.tabe() + else + vim.cmd.enew() + end + + is_standalone = standalone ---@diagnostic disable-line: cast-local-type + end + + vim.api.nvim_set_current_dir(config.storage.home:absolute()) + + local Menu = require("leetcode-ui.renderer.menu") + Menu():mount() + + local utils = require("leetcode.utils") + utils.exec_hooks("enter") + + return true +end + +leetcode.stop = vim.schedule_wrap(function() + if is_standalone then + return vim.cmd("qa!") + end + + _Lc_state.menu:unmount() + + vim.api.nvim_create_user_command("Leet", require("leetcode.command").start_with_cmd, { + bar = true, + bang = true, + desc = "Open leetcode.nvim", + }) + + local utils = require("leetcode.utils") + utils.exec_hooks("leave") + + if prev_cwd then + vim.api.nvim_set_current_dir(prev_cwd) + prev_cwd = nil + end +end) diff --git a/lua/leetcode-ui/group/buttons/menu.lua b/lua/leetcode-ui/group/buttons/menu.lua index a9a62582..9de3710a 100644 --- a/lua/leetcode-ui/group/buttons/menu.lua +++ b/lua/leetcode-ui/group/buttons/menu.lua @@ -6,7 +6,7 @@ local MenuButtons = Group:extend("LeetMenuButtons") function MenuButtons:init(buttons, opts) opts = vim.tbl_deep_extend("force", { padding = { - bot = 1, + bot = 2, }, spacing = 1, }, opts or {}) diff --git a/lua/leetcode-ui/group/case.lua b/lua/leetcode-ui/group/case.lua index e8b10046..8da0169b 100644 --- a/lua/leetcode-ui/group/case.lua +++ b/lua/leetcode-ui/group/case.lua @@ -1,15 +1,15 @@ -local log = require("leetcode.logger") -local t = require("leetcode.translator") local utils = require("leetcode.utils") local Pre = require("leetcode-ui.group.pre") +local Input = require("leetcode-ui.group.pre.input") local Stdout = require("leetcode-ui.group.pre.stdout") local Group = require("leetcode-ui.group") -local Lines = require("leetcode-ui.lines") - local Line = require("leetcode-ui.line") ----@alias case_body { input: string, raw_input: string, output: string, expected: string, std_output: string } +local t = require("leetcode.translator") +local log = require("leetcode.logger") + +---@alias case_body { input: string[], raw_input: string, output: string, expected: string, std_output: string } ---@class lc.ui.Case : lc.ui.Group ---@field pre lc.ui.Lines @@ -21,22 +21,10 @@ local Line = require("leetcode-ui.line") local Case = Group:extend("LeetCase") ---@private ----@param input string +---@param input string[] function Case:input(input) - local key = t("Input") - - local group = Group({}, { spacing = 1 }) - local s = vim.split(input, " ") - for i = 1, #s do - local ok, param = pcall(function() return self.question.q.meta_data.params[i].name end) - if ok then group:append(param .. " =", "leetcode_normal"):endl() end - group:append(s[i]):endgrp() - end - - local title = Line():append(key, "leetcode_normal") - local pre = Pre(title, group) - - return pre + input = vim.tbl_map(utils.norm_ins, input) + return Input("Input", input, self.question.q.meta_data.params) end ---@private @@ -46,7 +34,7 @@ function Case:output(output, expected) local key = t("Output") local title = Line():append(key, "leetcode_normal") - local pre = Pre(title, Line():append(output)) + local pre = Pre(title, Line():append(utils.norm_ins(output))) return pre end @@ -58,7 +46,7 @@ function Case:expected(expected, output) local key = t("Expected") local title = Line():append(key, "leetcode_normal") - local pre = Pre(title, Line():append(expected)) + local pre = Pre(title, Line():append(utils.norm_ins(expected))) return pre end @@ -67,11 +55,11 @@ end ---@param passed boolean --- ---@return lc.ui.Case -function Case:init(body, passed) +function Case:init(body, passed, question) Case.super.init(self, {}, { spacing = 1 }) self.body = body - self.question = utils.curr_question() + self.question = question self:insert(self:input(body.input)) self:insert(self:output(body.output, body.expected)) @@ -83,7 +71,7 @@ function Case:init(body, passed) self.passed = passed end ----@alias lc.Result.Case.constructor fun(body: case_body, passed: boolean): lc.ui.Case +---@alias lc.Result.Case.constructor fun(body: case_body, passed: boolean, question: lc.ui.Question): lc.ui.Case ---@type lc.Result.Case.constructor local LeetCase = Case diff --git a/lua/leetcode-ui/group/cases.lua b/lua/leetcode-ui/group/cases.lua index d7be7fbf..604df840 100644 --- a/lua/leetcode-ui/group/cases.lua +++ b/lua/leetcode-ui/group/cases.lua @@ -20,13 +20,17 @@ function Cases:make_nav() local nav = Lines({}, { padding = { top = 1 } }) for i, case in ipairs(self.cases) do - self.console.result:map("n", i, function() self:change(i) end, { clearable = true }) + self.console.result:map("n", i, function() + self:change(i) + end, { clearable = true }) local hl = self:nav_case_hl(case, i) - local msg = (" Case (%d) "):format(i) + local msg = (" Case (%d) "):format(i) nav:append(msg, hl) - if i ~= #self.cases then nav:append(" ") end + if i ~= #self.cases then + nav:append(" ") + end end return nav @@ -43,7 +47,9 @@ end ---@param idx integer function Cases:change(idx) - if not self.cases[idx] or idx == self.idx then return end + if not self.cases[idx] or idx == self.idx then + return + end self.idx = idx self.console.result:draw() @@ -59,15 +65,15 @@ function Cases:init(item, parent) self.console = parent local total = item.total_testcases ~= vim.NIL and item.total_testcases or 0 + local testcases = parent.testcase:by_id(item.submission_id) + for i = 1, total do self.cases[i] = Case({ - -- TODO: cache the testcases on submission, - -- so it doesn't get out of sync - input = self.console.testcase.testcases[i], + input = testcases[i], output = item.code_answer[i], expected = item.expected_code_answer[i], std_output = item.std_output_list[i], - }, item.compare_result:sub(i, i) == "1") + }, item.compare_result:sub(i, i) == "1", parent.question) end self:change(1) diff --git a/lua/leetcode-ui/group/init.lua b/lua/leetcode-ui/group/init.lua index f5a6e010..43802e3d 100644 --- a/lua/leetcode-ui/group/init.lua +++ b/lua/leetcode-ui/group/init.lua @@ -36,7 +36,9 @@ function Group:contents() local items = utils.shallowcopy(self._.items) local contents = Group.super.contents(self) - if not vim.tbl_isempty(contents) then table.insert(items, Lines(contents)) end + if not vim.tbl_isempty(contents) then + table.insert(items, Lines(contents)) + end return items end @@ -51,27 +53,35 @@ function Group:draw(layout, opts) local toppad = padding and padding.top local botpad = padding and padding.bot - if toppad then Pad(toppad):draw(layout) end + if toppad then + Pad(toppad):draw(layout) + end local items = self:contents() for i, item in ipairs(items) do item:draw(layout, options:get()) - if i ~= #items and spacing then Pad(spacing):draw(layout) end + if i ~= #items and spacing then + Pad(spacing):draw(layout) + end end - if botpad then Pad(botpad):draw(layout) end + if botpad then + Pad(botpad):draw(layout) + end end ---@param item lc.ui.Lines function Group:insert(item) - if not vim.tbl_isempty(Group.super.contents(self)) then self:endgrp() end + if not vim.tbl_isempty(Group.super.contents(self)) then + self:endgrp() + end table.insert(self._.items, item) return self end function Group:append(content, highlight) - if type(content) == "table" and O.is_instance(content, Group) then -- + if type(content) == "table" and O.is_instance(content, Group) then local items = content:contents() for _, item in ipairs(items) do diff --git a/lua/leetcode-ui/group/page/cache.lua b/lua/leetcode-ui/group/page/cache.lua index e5c2da23..2b46ffc2 100644 --- a/lua/leetcode-ui/group/page/cache.lua +++ b/lua/leetcode-ui/group/page/cache.lua @@ -1,24 +1,26 @@ -local Header = require("leetcode-ui.lines.menu-header") +local cmd = require("leetcode.command") + local Title = require("leetcode-ui.lines.title") -local Footer = require("leetcode-ui.lines.footer") local Buttons = require("leetcode-ui.group.buttons.menu") local Page = require("leetcode-ui.group.page") - local Button = require("leetcode-ui.lines.button.menu") local BackButton = require("leetcode-ui.lines.button.menu.back") -local cmd = require("leetcode.command") +local header = require("leetcode-ui.lines.menu-header") +local footer = require("leetcode-ui.lines.footer") local page = Page() -page:insert(Header()) +page:insert(header) page:insert(Title({ "Menu" }, "Cache")) local update_btn = Button("Update", { icon = "󱘴", sc = "u", - on_press = function() cmd.cache_update() end, + on_press = function() + cmd.cache_update() + end, }) local back_btn = BackButton("menu") @@ -28,6 +30,6 @@ page:insert(Buttons({ back_btn, })) -page:insert(Footer()) +page:insert(footer) return page diff --git a/lua/leetcode-ui/group/page/cookie.lua b/lua/leetcode-ui/group/page/cookie.lua index 8c7163da..81cab3da 100644 --- a/lua/leetcode-ui/group/page/cookie.lua +++ b/lua/leetcode-ui/group/page/cookie.lua @@ -1,17 +1,17 @@ -local Header = require("leetcode-ui.lines.menu-header") +local cmd = require("leetcode.command") + local Title = require("leetcode-ui.lines.title") -local Footer = require("leetcode-ui.lines.footer") local Buttons = require("leetcode-ui.group.buttons.menu") local Page = require("leetcode-ui.group.page") - local Button = require("leetcode-ui.lines.button.menu") local BackButton = require("leetcode-ui.lines.button.menu.back") -local cmd = require("leetcode.command") +local header = require("leetcode-ui.lines.menu-header") +local footer = require("leetcode-ui.lines.footer") local page = Page() -page:insert(Header()) +page:insert(header) page:insert(Title({ "Menu" }, "Cookie")) @@ -35,6 +35,6 @@ page:insert(Buttons({ back, })) -page:insert(Footer()) +page:insert(footer) return page diff --git a/lua/leetcode-ui/group/page/loading.lua b/lua/leetcode-ui/group/page/loading.lua index c5f3e354..fc63a6b0 100644 --- a/lua/leetcode-ui/group/page/loading.lua +++ b/lua/leetcode-ui/group/page/loading.lua @@ -1,14 +1,13 @@ local Page = require("leetcode-ui.group.page") - -local Header = require("leetcode-ui.lines.menu-header") local Buttons = require("leetcode-ui.group.buttons.menu") - local ExitButton = require("leetcode-ui.lines.button.menu.exit") local Title = require("leetcode-ui.lines.title") +local header = require("leetcode-ui.lines.menu-header") + local page = Page() -page:insert(Header()) +page:insert(header) page:insert(Title({}, "Loading...")) diff --git a/lua/leetcode-ui/group/page/menu.lua b/lua/leetcode-ui/group/page/menu.lua index d61e6be6..bc87ead7 100644 --- a/lua/leetcode-ui/group/page/menu.lua +++ b/lua/leetcode-ui/group/page/menu.lua @@ -1,45 +1,53 @@ +local cmd = require("leetcode.command") + local Page = require("leetcode-ui.group.page") local Title = require("leetcode-ui.lines.title") local Buttons = require("leetcode-ui.group.buttons.menu") -local Header = require("leetcode-ui.lines.menu-header") -local Footer = require("leetcode-ui.lines.footer") - local Button = require("leetcode-ui.lines.button.menu") local ExitButton = require("leetcode-ui.lines.button.menu.exit") -local cmd = require("leetcode.command") +local header = require("leetcode-ui.lines.menu-header") +local footer = require("leetcode-ui.lines.footer") local page = Page() -page:insert(Header()) +page:insert(header) page:insert(Title({}, "Menu")) local problems = Button("Problems", { icon = "", sc = "p", - on_press = function() cmd.menu_layout("problems") end, + on_press = function() + cmd.set_menu_page("problems") + end, expandable = true, }) local statistics = Button("Statistics", { icon = "󰄪", sc = "s", - on_press = function() cmd.menu_layout("stats") end, + on_press = function() + cmd.set_menu_page("stats") + end, expandable = true, }) local cookie = Button("Cookie", { icon = "󰆘", sc = "i", - on_press = function() cmd.menu_layout("cookie") end, + on_press = function() + cmd.set_menu_page("cookie") + end, expandable = true, }) local cache = Button("Cache", { icon = "", sc = "c", - on_press = function() cmd.menu_layout("cache") end, + on_press = function() + cmd.set_menu_page("cache") + end, expandable = true, }) @@ -53,6 +61,6 @@ page:insert(Buttons({ exit, })) -page:insert(Footer()) +page:insert(footer) return page diff --git a/lua/leetcode-ui/group/page/problems.lua b/lua/leetcode-ui/group/page/problems.lua index 470c6192..8e0cfcdc 100644 --- a/lua/leetcode-ui/group/page/problems.lua +++ b/lua/leetcode-ui/group/page/problems.lua @@ -1,17 +1,17 @@ local cmd = require("leetcode.command") local Title = require("leetcode-ui.lines.title") -local Footer = require("leetcode-ui.lines.footer") -local Header = require("leetcode-ui.lines.menu-header") - local Button = require("leetcode-ui.lines.button.menu") local BackButton = require("leetcode-ui.lines.button.menu.back") local Buttons = require("leetcode-ui.group.buttons.menu") local Page = require("leetcode-ui.group.page") +local footer = require("leetcode-ui.lines.footer") +local header = require("leetcode-ui.lines.menu-header") + local page = Page() -page:insert(Header()) +page:insert(header) page:insert(Title({ "Menu" }, "Problems")) @@ -42,6 +42,6 @@ page:insert(Buttons({ back, })) -page:insert(Footer()) +page:insert(footer) return page diff --git a/lua/leetcode-ui/group/page/signin.lua b/lua/leetcode-ui/group/page/signin.lua index 48f93693..0ad0a4bc 100644 --- a/lua/leetcode-ui/group/page/signin.lua +++ b/lua/leetcode-ui/group/page/signin.lua @@ -1,17 +1,18 @@ +local cmd = require("leetcode.command") +local config = require("leetcode.config") + local Page = require("leetcode-ui.group.page") local Title = require("leetcode-ui.lines.title") local Buttons = require("leetcode-ui.group.buttons.menu") -local Header = require("leetcode-ui.lines.menu-header") -local Footer = require("leetcode-ui.lines.footer") - +local Group = require("leetcode-ui.group") local Button = require("leetcode-ui.lines.button.menu") local ExitButton = require("leetcode-ui.lines.button.menu.exit") -local cmd = require("leetcode.command") +local header = require("leetcode-ui.lines.menu-header") local page = Page() -page:insert(Header()) +page:insert(header) page:insert(Title({}, "Sign in")) @@ -28,6 +29,10 @@ page:insert(Buttons({ exit, })) -page:insert(Footer()) +local footer = Group({}, { + hl = "Number", +}) +footer:append("leetcode." .. config.domain) +page:insert(footer) return page diff --git a/lua/leetcode-ui/group/page/stats.lua b/lua/leetcode-ui/group/page/stats.lua index 0d182bb1..853517af 100644 --- a/lua/leetcode-ui/group/page/stats.lua +++ b/lua/leetcode-ui/group/page/stats.lua @@ -1,15 +1,16 @@ +local cmd = require("leetcode.command") +local config = require("leetcode.config") + local Solved = require("leetcode-ui.lines.solved") local Calendar = require("leetcode-ui.lines.calendar") local Group = require("leetcode-ui.group") -local Footer = require("leetcode-ui.lines.footer") local Page = require("leetcode-ui.group.page") local Buttons = require("leetcode-ui.group.buttons.menu") local Button = require("leetcode-ui.lines.button.menu") local BackButton = require("leetcode-ui.lines.button.menu.back") local Title = require("leetcode-ui.lines.title") -local cmd = require("leetcode.command") -local config = require("leetcode.config") +local footer = require("leetcode-ui.lines.footer") local page = Page() @@ -37,7 +38,9 @@ local skills = Button("Skills", { sc = "s", on_press = cmd.ui_skills, }) -if not config.is_cn then buttons:insert(skills) end +if not config.is_cn then + buttons:insert(skills) +end local languages = Button("Languages", { icon = "", @@ -52,6 +55,7 @@ local update = Button("Update", { on_press = function() calendar:update() solved:update() + config.stats.update() end, }) buttons:insert(update) @@ -61,6 +65,6 @@ buttons:insert(back) page:insert(buttons) -page:insert(Footer()) +page:insert(footer) return page diff --git a/lua/leetcode-ui/group/pre/init.lua b/lua/leetcode-ui/group/pre/init.lua index 0e088989..bf3aab12 100644 --- a/lua/leetcode-ui/group/pre/init.lua +++ b/lua/leetcode-ui/group/pre/init.lua @@ -1,5 +1,6 @@ local NuiText = require("nui.text") local Group = require("leetcode-ui.group") +local config = require("leetcode.config") local log = require("leetcode.logger") @@ -8,7 +9,7 @@ local Pre = Group:extend("LeetPre") function Pre:add_margin(item) if item.class.name == "LeetLine" then - table.insert(item._texts, 1, NuiText("\t▎\t", "leetcode_indent")) + table.insert(item._texts, 1, NuiText(config.icons.indent, "leetcode_indent")) return end @@ -22,7 +23,7 @@ end function Pre:init(title, item) Pre.super.init(self, {}, { spacing = 1, position = "left" }) - if title then -- + if title then self:insert(title) end diff --git a/lua/leetcode-ui/group/pre/input.lua b/lua/leetcode-ui/group/pre/input.lua new file mode 100644 index 00000000..34d71d97 --- /dev/null +++ b/lua/leetcode-ui/group/pre/input.lua @@ -0,0 +1,33 @@ +local Pre = require("leetcode-ui.group.pre") +local Group = require("leetcode-ui.group") +local Line = require("leetcode-ui.line") + +local t = require("leetcode.translator") + +---@class lc.ui.Input : lc.ui.Pre +local Input = Pre:extend("LeetSimilarQuestions") + +---@param title string +---@param input string[] +function Input:init(title, input, params) -- + local group = Group({}, { spacing = 1 }) + + for i, case in ipairs(input) do + local ok, param = pcall(function() + return params[i].name + end) + if ok then + group:append(param .. " =", "leetcode_normal"):endl() + end + group:append(case):endgrp() + end + + local title_line = Line():append(t(title), "leetcode_normal") + + Input.super.init(self, title_line, group) +end + +---@type fun(title: string, input: string[], params: lc.QuestionResponse.metadata.param): lc.ui.Padding +local LeetInput = Input + +return LeetInput diff --git a/lua/leetcode-ui/group/pre/stdout.lua b/lua/leetcode-ui/group/pre/stdout.lua index 021c6d07..2f1f1ea2 100644 --- a/lua/leetcode-ui/group/pre/stdout.lua +++ b/lua/leetcode-ui/group/pre/stdout.lua @@ -3,6 +3,9 @@ local t = require("leetcode.translator") local Line = require("leetcode-ui.line") local Lines = require("leetcode-ui.lines") +local utils = require("leetcode.utils") +local log = require("leetcode.logger") + ---@class lc.ui.Stdout : lc.ui.Pre local Stdout = Pre:extend("LeetStdout") @@ -19,7 +22,7 @@ function Stdout:init(output) local lines = Lines() for i = 1, #output_list do - lines:append(output_list[i]):endl() + lines:append(utils.norm_ins(output_list[i])):endl() end local title = Line():append((" %s"):format(t("Stdout")), "leetcode_alt") diff --git a/lua/leetcode-ui/group/similar-questions.lua b/lua/leetcode-ui/group/similar-questions.lua index deb34d04..9bcf45f7 100644 --- a/lua/leetcode-ui/group/similar-questions.lua +++ b/lua/leetcode-ui/group/similar-questions.lua @@ -9,7 +9,7 @@ local ui_utils = require("leetcode-ui.utils") local t = require("leetcode.translator") ----@class lc.ui.SimilarQuestions : lc.ui.Lines +---@class lc.ui.SimilarQuestions : lc.ui.Group local SimilarQuestions = Group:extend("LeetSimilarQuestions") ---@param questions lc.QuestionResponse.similar @@ -32,12 +32,20 @@ function SimilarQuestions:init(questions) local fid = p.frontend_id .. "." fid = fid .. (" "):rep(5 - vim.api.nvim_strwidth(fid)) - button:append("󱓻 ", ui_utils.diff_to_hl(p.difficulty)) + button:append(config.icons.square .. " ", ui_utils.diff_to_hl(p.difficulty)) button:append(fid .. " ", "leetcode_normal") button:append(utils.translate(p.title, p.title_cn)) - if not config.auth.is_premium and q.paid_only then - button:append("  " .. t("Premium"), "leetcode_medium") + if q.paid_only then + local txt + + if config.auth.is_premium then + txt = " " .. config.icons.unlock + else + txt = (" %s "):format(config.icons.lock) .. t("Premium") + end + + button:append(txt, "leetcode_medium") end self:insert(button) diff --git a/lua/leetcode-ui/group/tag/a.lua b/lua/leetcode-ui/group/tag/a.lua index e8b3d5c7..f0b4cb08 100644 --- a/lua/leetcode-ui/group/tag/a.lua +++ b/lua/leetcode-ui/group/tag/a.lua @@ -6,11 +6,15 @@ local log = require("leetcode.logger") local A = Tag:extend("LeetTagA") ---@param url string -local function norm_url(url) return url:lower():gsub("/$", ""):gsub("#.-$", "") end +local function norm_url(url) + return url:lower():gsub("/$", ""):gsub("#.-$", "") +end ---@param u1 string ---@param u2 string -local function is_same_url(u1, u2) return norm_url(u1) == norm_url(u2) end +local function is_same_url(u1, u2) + return norm_url(u1) == norm_url(u2) +end function A:contents() local Group = require("leetcode-ui.group") diff --git a/lua/leetcode-ui/group/tag/init.lua b/lua/leetcode-ui/group/tag/init.lua index bba4df38..38dc9e3c 100644 --- a/lua/leetcode-ui/group/tag/init.lua +++ b/lua/leetcode-ui/group/tag/init.lua @@ -1,12 +1,9 @@ -local theme = require("leetcode.theme") -local u = require("leetcode-ui.utils") -local ts = vim.treesitter - -local utils = require("leetcode.parser.utils") local Group = require("leetcode-ui.group") local Indent = require("nui.text") local Normalizer = require("leetcode.parser.normalizer") +local ts = vim.treesitter +local utils = require("leetcode.parser.utils") local log = require("leetcode.logger") ---@class lc.ui.Tag : lc.ui.Group @@ -54,7 +51,9 @@ function Tag.normalize(text) :gsub("\n", "&lcnl;") :gsub("\t", "&lctab;") :gsub("%s", " ") - :gsub("<[^>]*>", function(match) return match:gsub(" ", " ") end) + :gsub("<[^>]*>", function(match) + return match:gsub(" ", " ") + end) -- :gsub("]*>(.-)", function(match) return match:gsub("&#?%w+;", utils.entity) end) log.debug(text) @@ -74,7 +73,9 @@ function Tag:add_indent(item, text) end end -function Tag:get_text(node) return ts.get_node_text(node, self.text) end +function Tag:get_text(node) + return ts.get_node_text(node, self.text) +end ---@param node TSNode --- @@ -98,7 +99,9 @@ end -- 1206 ---@param node TSNode function Tag:get_el_data(node) - if node:type() ~= "element" then return {} end + if node:type() ~= "element" then + return {} + end local start_tag for child in node:iter_children() do @@ -110,7 +113,9 @@ function Tag:get_el_data(node) end end - if not start_tag then return {} end + if not start_tag then + return {} + end local tag, attrs = nil, {} for child in start_tag:iter_children() do @@ -150,7 +155,9 @@ function Tag:parse_node() -- end function Tag.trim(lines) -- - if not lines or vim.tbl_isempty(lines) then return {} end + if not lines or vim.tbl_isempty(lines) then + return {} + end while not vim.tbl_isempty(lines) and lines[1]:content() == "" do table.remove(lines, 1) @@ -163,7 +170,9 @@ function Tag.trim(lines) -- return lines end -local function req_tag(str) return require("leetcode-ui.group.tag." .. str) end +local function req_tag(str) + return require("leetcode-ui.group.tag." .. str) +end function Tag:contents() local items = Tag.super.contents(self) @@ -227,13 +236,13 @@ end local LeetTag = Tag ---@param text string -function Tag.static:parse(text) -- +function Tag.static:parse(text) ---@type string local normalized = Normalizer:norm(text) local ok, parser = pcall(ts.get_string_parser, normalized, "html") - if not ok then -- + if not ok then local Plain = require("leetcode.parser.plain") return Plain:parse(text) end diff --git a/lua/leetcode-ui/group/tag/pre.lua b/lua/leetcode-ui/group/tag/pre.lua index e054cd7a..ee616e15 100644 --- a/lua/leetcode-ui/group/tag/pre.lua +++ b/lua/leetcode-ui/group/tag/pre.lua @@ -1,6 +1,5 @@ local Tag = require("leetcode-ui.group.tag") -local Lines = require("leetcode-ui.lines") -local Line = require("nui.line") +local config = require("leetcode.config") local log = require("leetcode.logger") @@ -11,7 +10,7 @@ function Pre:contents() local items = Pre.super.contents(self) for _, item in ipairs(items) do - self:add_indent(item, "\t▎\t") + self:add_indent(item, config.icons.indent) end return items diff --git a/lua/leetcode-ui/layout/console.lua b/lua/leetcode-ui/layout/console.lua index f7801e44..d10700bd 100644 --- a/lua/leetcode-ui/layout/console.lua +++ b/lua/leetcode-ui/layout/console.lua @@ -44,15 +44,28 @@ function ConsoleLayout:mount() ConsoleLayout.super.mount(self) self:set_keymaps({ - [keys.reset_testcases] = function() self.testcase:reset() end, - [keys.use_testcase] = function() self:use_testcase() end, - [keys.focus_testcases] = function() self.testcase:focus() end, - [keys.focus_result] = function() self.result:focus() end, + [keys.reset_testcases] = function() + self.testcase:reset() + end, + [keys.use_testcase] = function() + self:use_testcase() + end, + [keys.focus_testcases] = function() + self.testcase:focus() + end, + [keys.focus_result] = function() + self.result:focus() + end, }) end function ConsoleLayout:run(submit) - if config.user.console.open_on_runcode then self:show() end + if config.user.console.open_on_runcode then + self:show() + end + + self.result:focus() + Runner:init(self.question):run(submit) end diff --git a/lua/leetcode-ui/layout/init.lua b/lua/leetcode-ui/layout/init.lua index 85bcaa43..193bb45e 100644 --- a/lua/leetcode-ui/layout/init.lua +++ b/lua/leetcode-ui/layout/init.lua @@ -17,7 +17,9 @@ function Layout:show() end function Layout:hide() - if not self.visible then return end + if not self.visible then + return + end Layout.super.hide(self) self.visible = false end diff --git a/lua/leetcode-ui/line/init.lua b/lua/leetcode-ui/line/init.lua index e725985c..23799faf 100644 --- a/lua/leetcode-ui/line/init.lua +++ b/lua/leetcode-ui/line/init.lua @@ -7,10 +7,14 @@ local log = require("leetcode.logger") ---@class lc.ui.Line : NuiLine local Line = NuiLine:extend("LeetLine") -function Line:contents() return self._texts end +function Line:contents() + return self._texts +end function Line:longest() - if self.class.name == "LeetLine" then return vim.api.nvim_strwidth(self:content()) end + if self.class.name == "LeetLine" then + return vim.api.nvim_strwidth(self:content()) + end local max_len = 0 for _, item in pairs(self:contents()) do @@ -29,7 +33,7 @@ function Line:draw(layout, opts) local pad = options:get_padding() if pad then - if pad.left then -- + if pad.left then table.insert(texts, 1, NuiText((" "):rep(pad.left))) elseif pad.right then table.insert(texts, NuiText((" "):rep(pad.right))) diff --git a/lua/leetcode-ui/lines/button/init.lua b/lua/leetcode-ui/lines/button/init.lua index fe994fd2..4343ae8f 100644 --- a/lua/leetcode-ui/lines/button/init.lua +++ b/lua/leetcode-ui/lines/button/init.lua @@ -24,7 +24,9 @@ function Button:draw(renderer, opts) Button.super.draw(self, renderer, opts) end -function Button:press() self._.opts.on_press() end +function Button:press() + self._.opts.on_press() +end ---@param lines lc.ui.Line[] ---@param opts lc.ui.Button.opts diff --git a/lua/leetcode-ui/lines/button/menu/back.lua b/lua/leetcode-ui/lines/button/menu/back.lua index c9d09644..453dd9c1 100644 --- a/lua/leetcode-ui/lines/button/menu/back.lua +++ b/lua/leetcode-ui/lines/button/menu/back.lua @@ -9,7 +9,9 @@ function MenuBackButton:init(page) MenuBackButton.super.init(self, "Back", { icon = "", sc = "q", - on_press = function() cmd.menu_layout(page) end, + on_press = function() + cmd.set_menu_page(page) + end, }) end diff --git a/lua/leetcode-ui/lines/button/menu/exit.lua b/lua/leetcode-ui/lines/button/menu/exit.lua index f0c25ec7..2aafeb9a 100644 --- a/lua/leetcode-ui/lines/button/menu/exit.lua +++ b/lua/leetcode-ui/lines/button/menu/exit.lua @@ -1,4 +1,5 @@ local MenuButton = require("leetcode-ui.lines.button.menu") +local leetcode = require("leetcode") ---@class lc.ui.Button.Menu.Exit : lc.ui.Button.Menu local MenuExitButton = MenuButton:extend("LeetMenuExitButton") @@ -7,8 +8,8 @@ local MenuExitButton = MenuButton:extend("LeetMenuExitButton") function MenuExitButton:init() MenuExitButton.super.init(self, "Exit", { icon = "󰩈", - sc = "q", - on_press = vim.cmd.quitall, + sc = "qa", + on_press = leetcode.stop, }) end diff --git a/lua/leetcode-ui/lines/button/menu/init.lua b/lua/leetcode-ui/lines/button/menu/init.lua index 4f508fcd..219f49e8 100644 --- a/lua/leetcode-ui/lines/button/menu/init.lua +++ b/lua/leetcode-ui/lines/button/menu/init.lua @@ -25,13 +25,17 @@ function MenuButton:init(text, opts) self:append(" ") self:append(text) - if opts.expandable then self:append(" " .. opts.expand_icon, "leetcode_alt") end + if opts.expandable then + self:append(" " .. opts.expand_icon, "leetcode_alt") + end local len = vim.api.nvim_strwidth(self:content()) + vim.api.nvim_strwidth(opts.sc or "") local padding = (" "):rep(opts.width - len) self:append(padding) - if opts.sc then self:append(opts.sc, "leetcode_info") end + if opts.sc then + self:append(opts.sc, "leetcode_info") + end end ---@type fun(text: string, opts: lc.ui.Button.Menu.opts): lc.ui.Button diff --git a/lua/leetcode-ui/lines/calendar.lua b/lua/leetcode-ui/lines/calendar.lua index 2509b13c..7720d701 100644 --- a/lua/leetcode-ui/lines/calendar.lua +++ b/lua/leetcode-ui/lines/calendar.lua @@ -18,7 +18,9 @@ local Calendar = Lines:extend("LeetCalendar") local function get_days_in_month(month, year) local days_in_month = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } - if (year % 4 == 0 and year % 100 ~= 0) or (year % 400 == 0) then days_in_month[2] = 29 end + if (year % 4 == 0 and year % 100 ~= 0) or (year % 400 == 0) then + days_in_month[2] = 29 + end return days_in_month[month] end @@ -57,7 +59,9 @@ function Calendar:handle_res(res) hour = 1, isdst = false, }) - if self.threshold <= os.time(time) then self.threshold = self.threshold + 24 * 60 * 60 end + if self.threshold <= os.time(time) then + self.threshold = self.threshold + 24 * 60 * 60 + end self.last_year_sub_count = 0 self.month_lens = {} @@ -83,7 +87,7 @@ function Calendar:handle_res(res) self:append(line):endl() end - _Lc_Menu:draw() + _Lc_state.menu:draw() end function Calendar:handle_submissions() @@ -161,7 +165,9 @@ local function square_hl(count, max_count) end function Calendar:handle_weekdays() - if self.curr_time > self.threshold then return end + if self.curr_time > self.threshold then + return + end local curr = os.date("*t", self.curr_time) local count = self:get_submission(curr) or 0 @@ -174,7 +180,9 @@ end function Calendar:fetch() statistics.calendar(function(res, err) - if err then return log.err(err) end + if err then + return log.err(err) + end self:handle_res(res) end) end diff --git a/lua/leetcode-ui/lines/footer.lua b/lua/leetcode-ui/lines/footer.lua index 38160edb..26f62926 100644 --- a/lua/leetcode-ui/lines/footer.lua +++ b/lua/leetcode-ui/lines/footer.lua @@ -1,24 +1,34 @@ -local Lines = require("leetcode-ui.lines") -local config = require("leetcode.config") local t = require("leetcode.translator") +local Group = require("leetcode-ui.group") + +local config = require("leetcode.config") +local stats = require("leetcode-ui.lines.stats") ----@class lc.ui.menu.Footer : lc.ui.Lines -local Footer = Lines:extend("LeetFooter") +---@class lc.ui.menu.Footer : lc.ui.Group +local Footer = Group:extend("LeetFooter") + +function Footer:contents() + self:clear() -function Footer:draw(layout, opts) if config.auth.is_signed_in then - self:clear() + self:append(stats) + self:endgrp() + self:append(t("Signed in as") .. ": ", "leetcode_alt") + if config.auth.is_premium then + self:append(config.icons.star .. " ", "leetcode_medium") + end self:append(config.auth.name):endl() end - Footer.super.draw(self, layout, opts) + return Footer.super.contents(self) end ---@param opts? any function Footer:init(opts) opts = vim.tbl_deep_extend("force", { hl = "Number", + spacing = 1, }, opts or {}) Footer.super.init(self, {}, opts) @@ -27,4 +37,4 @@ end ---@type fun(): lc.ui.menu.Footer local LeetFooter = Footer -return LeetFooter +return LeetFooter() diff --git a/lua/leetcode-ui/lines/init.lua b/lua/leetcode-ui/lines/init.lua index e53926bb..192f64ff 100644 --- a/lua/leetcode-ui/lines/init.lua +++ b/lua/leetcode-ui/lines/init.lua @@ -14,7 +14,9 @@ function Lines:contents() local lines = utils.shallowcopy(self._.lines) local contents = Lines.super.contents(self) - if not vim.tbl_isempty(contents) then table.insert(lines, Line(contents)) end + if not vim.tbl_isempty(contents) then + table.insert(lines, Line(contents)) + end return lines end @@ -57,10 +59,14 @@ function Lines:draw(layout, opts) options:set({ padding = { left = leftpad } }) local toppad = padding and padding.top - if toppad then lines = vim.list_extend(create_pad(toppad), lines) end + if toppad then + lines = vim.list_extend(create_pad(toppad), lines) + end local botpad = padding and padding.bot - if botpad then lines = vim.list_extend(lines, create_pad(botpad)) end + if botpad then + lines = vim.list_extend(lines, create_pad(botpad)) + end for _, line in pairs(lines) do line:draw(layout, options:get()) @@ -68,12 +74,14 @@ function Lines:draw(layout, opts) end function Lines:append(content, highlight) - if type(content) == "table" and O.is_instance(content, Lines) then -- + if type(content) == "table" and O.is_instance(content, Lines) then local lines = content:contents() for i, line in ipairs(lines) do Lines.super.append(self, line) - if i ~= #lines then self:endl() end + if i ~= #lines then + self:endl() + end end else Lines.super.append(self, content, highlight) @@ -90,7 +98,9 @@ function Lines:clear() end function Lines:insert(item) -- - if not vim.tbl_isempty(Lines.super.contents(self)) then self:endl() end + if not vim.tbl_isempty(Lines.super.contents(self)) then + self:endl() + end table.insert(self._.lines, item) return self diff --git a/lua/leetcode-ui/lines/menu-header.lua b/lua/leetcode-ui/lines/menu-header.lua index 8c66a709..bf51212d 100644 --- a/lua/leetcode-ui/lines/menu-header.lua +++ b/lua/leetcode-ui/lines/menu-header.lua @@ -27,4 +27,4 @@ end ---@type fun(): lc.ui.menu.Header local LeetMenuHeader = MenuHeader -return LeetMenuHeader +return LeetMenuHeader() diff --git a/lua/leetcode-ui/lines/solved.lua b/lua/leetcode-ui/lines/solved.lua index 6f572edc..2c40ab76 100644 --- a/lua/leetcode-ui/lines/solved.lua +++ b/lua/leetcode-ui/lines/solved.lua @@ -34,10 +34,9 @@ function Solved:handle_res(res) local max_count_len = 0 for _, stat in ipairs(res.submit_stats.acSubmissionNum) do - local total_count = vim.tbl_filter( - function(c) return c.difficulty == stat.difficulty end, - res.questions_count - )[1].count + local total_count = vim.tbl_filter(function(c) + return c.difficulty == stat.difficulty + end, res.questions_count)[1].count local solved_line = Line() solved_line:append(tostring(stat.count)) @@ -76,7 +75,7 @@ function Solved:handle_res(res) self:endl() end - _Lc_Menu:draw() + _Lc_state.menu:draw() end function Solved:update() @@ -93,7 +92,9 @@ end function Solved:fetch() statistics.solved(function(res, err) - if err then return log.err(err) end + if err then + return log.err(err) + end self:handle_res(res) end) end diff --git a/lua/leetcode-ui/lines/stats.lua b/lua/leetcode-ui/lines/stats.lua new file mode 100644 index 00000000..1fd089f3 --- /dev/null +++ b/lua/leetcode-ui/lines/stats.lua @@ -0,0 +1,46 @@ +local Lines = require("leetcode-ui.lines") +local t = require("leetcode.translator") +local log = require("leetcode.logger") +local cmd = require("leetcode.command") +local config = require("leetcode.config") + +---@class lc.ui.menu.Stats : lc.ui.Lines +local Stats = Lines:extend("LeetMenuTitle") + +function Stats:contents() + self:clear() + + local stats = config.stats + local daily, progress = stats.daily, stats.progress + + local hl = daily.today_completed and "leetcode_hard" or "leetcode_alt" + self:append("󰈸 ", hl) + self:append(daily.streak and tostring(daily.streak) or "-") + + self:append((" %s "):format(config.icons.bar)) + + self:append(t("session") .. ": ", "leetcode_alt") + local session = cmd.get_active_session() + local session_name = session + and (session.name == "" and config.sessions.default or session.name) + or "-" + self:append(session_name) + + local icon = (" %s "):format(config.icons.square) + local function create_progress(key) + self:append(icon, "leetcode_" .. key) + local count = progress[key] and tostring(progress[key].count) or "-" + self:append(count) + end + + create_progress("easy") + create_progress("medium") + create_progress("hard") + + return Stats.super.contents(self) +end + +---@type fun(): lc.ui.menu.Stats +local LeetMenuStats = Stats + +return LeetMenuStats() diff --git a/lua/leetcode-ui/opts.lua b/lua/leetcode-ui/opts.lua index e4423643..673eebc9 100644 --- a/lua/leetcode-ui/opts.lua +++ b/lua/leetcode-ui/opts.lua @@ -30,9 +30,13 @@ function Opts:merge(opts) -- return self end -function Opts:set(opts) self.opts = vim.tbl_deep_extend("force", self.opts, opts) end +function Opts:set(opts) + self.opts = vim.tbl_deep_extend("force", self.opts, opts) +end -function Opts:get() return self.opts end +function Opts:get() + return self.opts +end function Opts:init(opts) -- self.opts = vim.tbl_deep_extend("force", { diff --git a/lua/leetcode-ui/popup/console/init.lua b/lua/leetcode-ui/popup/console/init.lua index be030f48..ba55fa91 100644 --- a/lua/leetcode-ui/popup/console/init.lua +++ b/lua/leetcode-ui/popup/console/init.lua @@ -8,7 +8,9 @@ local ConsolePopup = Popup:extend("LeetConsolePopup") ConsolePopup.handle_leave = vim.schedule_wrap(function(self) local curr_bufnr = vim.api.nvim_get_current_buf() for _, p in pairs(self.console.popups) do - if p.bufnr == curr_bufnr then return end + if p.bufnr == curr_bufnr then + return + end end self.console:hide() end) diff --git a/lua/leetcode-ui/popup/console/result.lua b/lua/leetcode-ui/popup/console/result.lua index e4233bbb..520113d2 100644 --- a/lua/leetcode-ui/popup/console/result.lua +++ b/lua/leetcode-ui/popup/console/result.lua @@ -4,6 +4,7 @@ local t = require("leetcode.translator") local problemlist = require("leetcode.cache.problemlist") local log = require("leetcode.logger") +local config = require("leetcode.config") ---@class lc.ui.Console.ResultPopup : lc.ui.Console.Popup ---@field renderer lc.ui.Result @@ -15,11 +16,16 @@ function ResultPopup:handle(item) self.border:set_highlight(item._.hl) self.renderer:handle_res(item) - if item.last_testcase then self.last_testcase = item.last_testcase end + if item.last_testcase then + self.last_testcase = item.last_testcase + end if item._.submission then local status = item.status_code == 10 and "ac" or "notac" problemlist.change_status(self.console.question.q.title_slug, status) + if status == "ac" then + config.stats.update_streak() + end end self:draw() @@ -51,6 +57,8 @@ function ResultPopup:init(parent) }, win_options = { winhighlight = "Normal:NormalSB,FloatBorder:FloatBorder", + wrap = true, + linebreak = true, }, }) diff --git a/lua/leetcode-ui/popup/console/testcase.lua b/lua/leetcode-ui/popup/console/testcase.lua index 41062df9..fe696d81 100644 --- a/lua/leetcode-ui/popup/console/testcase.lua +++ b/lua/leetcode-ui/popup/console/testcase.lua @@ -4,52 +4,74 @@ local ConsolePopup = require("leetcode-ui.popup.console") local t = require("leetcode.translator") ---@class lc.ui.Console.TestcasePopup : lc.ui.Console.Popup ----@field testcases string[] +---@field testcases table +---@field testcase_len integer ---@field extmarks integer[] +---@field snapshots table local Testcase = ConsolePopup:extend("LeetTestcasePopup") function Testcase:content() - self.testcases = {} + local lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) + lines = vim.tbl_filter(function(line) + return line ~= "" + end, lines) + return table.concat(lines, "\n") +end + +function Testcase:snapshot(id, data) -- + if not data.test_case then + return + end + self.testcases[id] = data.test_case +end + +---@return string[][] +function Testcase:by_id(id) -- + local testcases = {} ---@type string[][] - local tbl = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) - local str = table.concat(tbl, "\n") + local i, n = 0, self.testcase_len + for case in vim.gsplit(self.testcases[id] or "", "\n") do + local j = math.floor(i / n) + 1 + + if not testcases[j] then + testcases[j] = {} + end + table.insert(testcases[j], case) - local testcases = {} - for tcase in vim.gsplit(str, "\n\n") do - local case = tcase:gsub("\n", " ") - table.insert(self.testcases, case) - testcases = vim.list_extend(testcases, vim.split(tcase, "\n")) + i = i + 1 end return testcases end function Testcase:populate() - local tbl = {} - for i, case in ipairs(self.console.question.q.testcase_list) do - if i ~= 1 then table.insert(tbl, "") end + local lines = {} - -- TODO: Think of a better way to do this. Don't store testcases as a single strings - table.insert(self.testcases, case:gsub("\n", " ")[1]) + local t_list = self.console.question.q.testcase_list + self.testcase_len = #vim.split(t_list[1] or "", "\n") + for i, case in ipairs(t_list) do + if i ~= 1 then + table.insert(lines, "") + end for s in vim.gsplit(case, "\n", { trimempty = true }) do - table.insert(tbl, s) + table.insert(lines, s) end end - vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, tbl) - + vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, lines) return self:draw_extmarks() end function Testcase:clear_extmarks() - if not config.user.console.testcase.virt_text then return end + if not config.user.console.testcase.virt_text then + return + end local ns = vim.api.nvim_create_namespace("leetcode_extmarks") - self.extmarks = vim.tbl_filter( - function(extmark) return not vim.api.nvim_buf_del_extmark(self.bufnr, ns, extmark) end, - self.extmarks - ) + self.extmarks = vim.tbl_filter(function(extmark) + return not vim.api.nvim_buf_del_extmark(self.bufnr, ns, extmark) + end, self.extmarks) end ---@param line integer @@ -62,7 +84,9 @@ function Testcase:add_extmark(line, col, opts) end function Testcase:draw_extmarks() - if not config.user.console.testcase.virt_text then return end + if not config.user.console.testcase.virt_text then + return + end self:clear_extmarks() local bufnr = self.bufnr @@ -70,44 +94,41 @@ function Testcase:draw_extmarks() local md = self.console.question.q.meta_data local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - if not md.params then return end - - local max_lens = {} - local j, k = 1, 1 - for _, line in ipairs(lines) do - if line == "" then k = k + 1 end - max_lens[k] = math.max(max_lens[k] or 1, line:len() + 1) + if not md.params then + return end - local function get_param(idx, param_idx, len) + local j = 1 + local pad = (" "):rep(2) + local function get_param(idx) return { - { (" "):rep(max_lens[idx] - len) }, + { pad }, { "", "Operator" }, { " " }, - { md.params[param_idx].name, "Comment" }, + { md.params[idx].name, "Comment" }, { " " }, - { md.params[param_idx].type, "Type" }, + { md.params[idx].type, "Type" }, } end local invalid = false - k = 1 for i, line in ipairs(lines) do pcall(function() - if lines[i - 1] == "" and lines[i] == "" then invalid = true end + if lines[i - 1] == "" and lines[i] == "" then + invalid = true + end end) if line ~= "" then - local ok, text = pcall(get_param, k, j, line:len()) + local ok, text = pcall(get_param, j) if not ok or invalid then - text = { { (" %s"):format(t("invalid")), "leetcode_error" } } + text = { { pad }, { (" %s"):format(t("invalid")), "leetcode_error" } } end self:add_extmark(i - 1, -1, { virt_text = text }) j = j + 1 else - k = k + 1 j = 1 end end @@ -121,6 +142,8 @@ function Testcase:reset() end function Testcase:append(input) + -- pcall(vim.cmd.undojoin) + local s = vim.split(input, "\n", { trimempty = true }) local lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, true) @@ -134,10 +157,14 @@ function Testcase:append(input) end function Testcase:autocmds() - self:on( - { "TextChanged", "TextChangedI", "TextChangedP", "TextChangedT" }, - function() self:draw_extmarks() end - ) + self:on({ + "TextChanged", + "TextChangedI", + "TextChangedP", + "InsertLeave", + }, function() + self:draw_extmarks() + end) end function Testcase:mount() @@ -145,6 +172,7 @@ function Testcase:mount() self.testcases = {} self.extmarks = {} + self.testcase_len = 0 self:autocmds() self:populate() @@ -170,11 +198,14 @@ function Testcase:init(parent) }, win_options = { winhighlight = "Normal:NormalSB,FloatBorder:FloatBorder", + wrap = true, + linebreak = true, }, }) self.testcases = {} self.extmarks = {} + self.testcase_len = 0 self:populate() end diff --git a/lua/leetcode-ui/popup/info.lua b/lua/leetcode-ui/popup/info.lua index 4a9dae11..82bc68f2 100644 --- a/lua/leetcode-ui/popup/info.lua +++ b/lua/leetcode-ui/popup/info.lua @@ -92,7 +92,9 @@ function Info:populate() if type(node.text) == "string" then line:append(" ") local txt = Parser:parse(node.text) - if txt:lines()[1] then line:append(txt:lines()[1]) end + if txt:lines()[1] then + line:append(txt:lines()[1]) + end else line:append(node.text) end @@ -106,9 +108,13 @@ function Info:populate() self:map("n", keys.confirm, function() local node = tree:get_node() - if not node then return end + if not node then + return + end - if node.text and node.text.press then node.text:press() end + if node.text and node.text.press then + node.text:press() + end if not node:is_expanded() then node:expand() @@ -128,12 +134,12 @@ function Info:mount() local ui_utils = require("leetcode-ui.utils") local winhighlight = "Normal:NormalSB,FloatBorder:FloatBorder" - ui_utils.set_win_opts(self.winid, { + ui_utils.win_set_opts(self.winid, { winhighlight = winhighlight, wrap = true, }) - ui_utils.set_win_opts(self.border.winid, { + ui_utils.win_set_opts(self.border.winid, { winhighlight = winhighlight, }) diff --git a/lua/leetcode-ui/popup/init.lua b/lua/leetcode-ui/popup/init.lua index 712b8a69..cf226d70 100644 --- a/lua/leetcode-ui/popup/init.lua +++ b/lua/leetcode-ui/popup/init.lua @@ -31,8 +31,9 @@ function Popup:_buf_destory() end function Popup:focus() - if not vim.api.nvim_win_is_valid(self.winid) then return end - vim.api.nvim_set_current_win(self.winid) + if self.winid and vim.api.nvim_win_is_valid(self.winid) then + vim.api.nvim_set_current_win(self.winid) + end end function Popup:clear() -- @@ -56,19 +57,27 @@ function Popup:unmount() self.visible = false end -function Popup:map(...) self.renderer:map(...) end +function Popup:map(...) + self.renderer:map(...) +end function Popup:mount() Popup.super.mount(self) self.visible = true - self:on({ "BufLeave", "WinLeave" }, function() self:handle_leave() end) - self:map("n", keys.toggle, function() self:hide() end) + self:on({ "BufLeave", "WinLeave" }, function() + self:handle_leave() + end) + self:map("n", keys.toggle, function() + self:hide() + end) end function Popup:hide() - if not self.visible then return end + if not self.visible then + return + end Popup.super.hide(self) self.visible = false @@ -82,9 +91,13 @@ function Popup:toggle() end end -function Popup:handle_leave() self:hide() end +function Popup:handle_leave() + self:hide() +end -function Popup:draw() self.renderer:draw(self) end +function Popup:draw() + self.renderer:draw(self) +end function Popup:update_renderer() self.renderer.bufnr = self.bufnr diff --git a/lua/leetcode-ui/popup/languages.lua b/lua/leetcode-ui/popup/languages.lua index 869d3391..81886d1b 100644 --- a/lua/leetcode-ui/popup/languages.lua +++ b/lua/leetcode-ui/popup/languages.lua @@ -32,7 +32,11 @@ function Languages:handle(res) lines:append(" " .. res.problems_solved) else lines:append("" .. res.problems_solved) - lines:append(" problems solved", "leetcode_alt") + if res.problems_solved == 1 then + lines:append(" problem solved", "leetcode_alt") + else + lines:append(" problems solved", "leetcode_alt") + end end return lines @@ -42,9 +46,13 @@ end ---@param res lc.Languages.Res function Languages:populate(res) local group = Group({}, { spacing = 1 }) - if res == vim.NIL then return end + if res == vim.NIL then + return + end - table.sort(res, function(a, b) return a.problems_solved > b.problems_solved end) + table.sort(res, function(a, b) + return a.problems_solved > b.problems_solved + end) for _, lang in ipairs(res) do group:insert(self:handle(lang)) end diff --git a/lua/leetcode-ui/popup/skills.lua b/lua/leetcode-ui/popup/skills.lua index 49e12fbb..4a2fb28f 100644 --- a/lua/leetcode-ui/popup/skills.lua +++ b/lua/leetcode-ui/popup/skills.lua @@ -16,8 +16,10 @@ local Skills = Popup:extend("LeetSkills") function Skills:handle(name, skills) local lines = Lines() - table.sort(skills, function(a, b) return a.problems_solved > b.problems_solved end) - lines:append("󱓻", utils.diff_to_hl(name)) + table.sort(skills, function(a, b) + return a.problems_solved > b.problems_solved + end) + lines:append(config.icons.square, utils.diff_to_hl(name)) lines:append(" " .. name) for _, skill in ipairs(skills) do diff --git a/lua/leetcode-ui/question.lua b/lua/leetcode-ui/question.lua index 0c17fda0..e8204746 100644 --- a/lua/leetcode-ui/question.lua +++ b/lua/leetcode-ui/question.lua @@ -5,6 +5,7 @@ local Object = require("nui.object") local api_question = require("leetcode.api.question") local utils = require("leetcode.utils") +local ui_utils = require("leetcode-ui.utils") local config = require("leetcode.config") local log = require("leetcode.logger") @@ -16,106 +17,204 @@ local log = require("leetcode.logger") ---@field console lc.ui.Console ---@field lang string ---@field cache lc.cache.Question +---@field reset boolean local Question = Object("LeetQuestion") -function Question:get_snippet() +---@param raw? boolean +function Question:snippet(raw) local snippets = self.q.code_snippets ~= vim.NIL and self.q.code_snippets or {} - local snip = vim.tbl_filter(function(snip) return snip.lang_slug == self.lang end, snippets)[1] - if not snip then return end + local snip = vim.tbl_filter(function(snip) + return snip.lang_slug == self.lang + end, snippets)[1] + if not snip then + return + end - local lang = utils.get_lang(self.lang) - snip.code = (snip.code or ""):gsub("\r\n", "\n") + local code = snip.code:gsub("\r\n", "\n") + return raw and code or self:injector(code) +end + +---@param code? string +function Question:set_lines(code) + if not (self.bufnr and vim.api.nvim_buf_is_valid(self.bufnr)) then + return + end - return self:injector( - ("%s @leet start\n%s\n%s @leet end"):format(lang.comment, snip.code, lang.comment) - ) + pcall(vim.cmd.undojoin) + local s_i, e_i, lines = self:range() + s_i = s_i or 1 + e_i = e_i or #lines + code = code and code or (self:snippet(true) or "") + vim.api.nvim_buf_set_lines(self.bufnr, s_i - 1, e_i, false, vim.split(code, "\n")) end ----@private -function Question:create_file() +function Question:reset_lines() + local new_lines = self:snippet(true) or "" + + vim.schedule(function() -- + log.info("Previous code found and reset\nTo undo, simply press `u`") + end) + + self:set_lines(new_lines) +end + +---@return string path, boolean existed +function Question:path() local lang = utils.get_lang(self.lang) - local fn = ("%s.%s-%s.%s"):format(self.q.frontend_id, self.q.title_slug, lang.slug, lang.ft) + local alt = lang.alt and ("." .. lang.alt) or "" + + -- handle legacy file names first + local fn_legacy = -- + ("%s.%s-%s.%s"):format(self.q.frontend_id, self.q.title_slug, lang.slug, lang.ft) + self.file = config.storage.home:joinpath(fn_legacy) - self.file = config.home:joinpath(fn) - if not self.file:exists() then self.file:write(self:get_snippet(), "w") end + if self.file:exists() then + return self.file:absolute(), true + end + + local fn = ("%s.%s%s.%s"):format(self.q.frontend_id, self.q.title_slug, alt, lang.ft) + self.file = config.storage.home:joinpath(fn) + local existed = self.file:exists() + + if not existed then + self.file:write(self:snippet(), "w") + end + + return self.file:absolute(), existed end ----@private ----@param code string -function Question:injector(code) - local injector = config.user.injector +function Question:create_buffer() + local path, existed = self:path() - local inject = injector[self.lang] - if not inject or vim.tbl_isempty(inject) then return code end + vim.cmd("$tabe " .. path) + self.bufnr = vim.api.nvim_get_current_buf() + self.winid = vim.api.nvim_get_current_win() + ui_utils.win_set_winfixbuf(self.winid) - ---@param inj? string|string[] - ---@param before boolean - local function norm_inject(inj, before) - local res + self:open_buffer(existed) +end - if type(inj) == "table" then - res = table.concat(inj, "\n") - elseif type(inj) == "string" then - res = inj - end +---@param existed boolean +function Question:open_buffer(existed) + ui_utils.buf_set_opts(self.bufnr, { buflisted = true }) + ui_utils.win_set_buf(self.winid, self.bufnr, true) - if res and res ~= "" then - return before and (res .. "\n\n") or ("\n\n" .. res) - else - return "" - end + local i = self:fold_range() + if i then + pcall(vim.cmd, ("%d,%dfold"):format(1, i)) end - return norm_inject(inject.before, true) -- - .. code - .. norm_inject(inject.after, false) + if existed and self.cache.status == "ac" then + self:reset_lines() + end end -Question.unmount = vim.schedule_wrap(function(self) - self.info:unmount() - self.console:unmount() - self.description:unmount() +---@param before boolean +function Question:inject(before) + local inject = config.user.injector[self.lang] or {} + local inj = before and inject.before or inject.after - if vim.api.nvim_buf_is_valid(self.bufnr) then - vim.api.nvim_buf_delete(self.bufnr, { force = true }) + local res + + if type(inj) == "boolean" and inj == true and before then + inj = config.imports[self.lang] end - if vim.api.nvim_win_is_valid(self.winid) then -- - vim.api.nvim_win_close(self.winid, true) + + if type(inj) == "table" then + res = table.concat(inj, "\n") + elseif type(inj) == "string" then + res = inj end - _Lc_questions = vim.tbl_filter(function(q) return q.bufnr ~= self.bufnr end, _Lc_questions) -end) + if res and res ~= "" then + return before and (res .. "\n") or ("\n" .. res) + else + return nil + end +end -function Question:handle_mount() - self:create_file() - vim.cmd("$tabe " .. self.file:absolute()) +---@param code string +function Question:injector(code) + local lang = utils.get_lang(self.lang) + + local parts = { + ("%s @leet start"):format(lang.comment), + code, + ("%s @leet end"):format(lang.comment), + } - -- https://github.com/kawre/leetcode.nvim/issues/14 - if self.lang == "rust" then - pcall(function() require("rust-tools.standalone").start_standalone_client() end) + local before = self:inject(true) + if before then + table.insert(parts, 1, before) end - self.bufnr = vim.api.nvim_get_current_buf() - self.winid = vim.api.nvim_get_current_win() - table.insert(_Lc_questions, self) + local after = self:inject(false) + if after then + table.insert(parts, after) + end - vim.api.nvim_create_autocmd("QuitPre", { - buffer = self.bufnr, - callback = function() self:unmount() end, + return table.concat(parts, "\n") +end + +function Question:_unmount() + if vim.v.dying ~= 0 then + return + end + + vim.schedule(function() + self.info:unmount() + self.console:unmount() + self.description:unmount() + + if self.bufnr and vim.api.nvim_buf_is_valid(self.bufnr) then + vim.api.nvim_buf_delete(self.bufnr, { force = true, unload = false }) + end + + _Lc_state.questions = vim.tbl_filter(function(q) + return q.bufnr ~= self.bufnr + end, _Lc_state.questions) + + self = nil + end) +end + +function Question:unmount() + if self.winid and vim.api.nvim_win_is_valid(self.winid) then + vim.api.nvim_win_close(self.winid, true) + end +end + +local group = vim.api.nvim_create_augroup("leetcode_questions", { clear = true }) +function Question:autocmds() + vim.api.nvim_create_autocmd("WinClosed", { + group = group, + pattern = tostring(self.winid), + callback = function() + self:_unmount() + end, }) +end + +function Question:handle_mount() + self:create_buffer() self.description = Description(self):mount() self.console = Console(self) self.info = Info(self) - utils.exec_hooks("LeetQuestionNew", self) + table.insert(_Lc_state.questions, self) + + self:autocmds() + utils.exec_hooks("question_enter", self) return self end function Question:mount() local tabp = utils.detect_duplicate_question(self.cache.title_slug, config.lang) - if tabp then return pcall(vim.api.nvim_set_current_tabpage, tabp) end + if tabp then + return pcall(vim.api.nvim_set_current_tabpage, tabp) + end local q = api_question.by_title_slug(self.cache.title_slug) if not q or q.is_paid_only and not config.auth.is_premium then @@ -123,14 +222,15 @@ function Question:mount() end self.q = q - if self:get_snippet() then + if self:snippet() then self:handle_mount() else local msg = ("Snippet for `%s` not found. Select a different language"):format(self.lang) log.warn(msg) - require("leetcode.pickers.language").pick_lang(self, function(snippet) - self.lang = snippet.t.slug + local picker = require("leetcode.picker") + picker.language(self, function(slug) + self.lang = slug self:handle_mount() end) end @@ -138,33 +238,78 @@ function Question:mount() return self end ----@return string -function Question:lines() +---@param inclusive? boolean +---@return integer, integer, string[] +function Question:range(inclusive) local lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) - local start_i, end_i = 1, #lines + local start_i, end_i for i, line in ipairs(lines) do if line:match("@leet start") then - start_i = i + 1 - else - if line:match("@leet end") then end_i = i - 1 end + start_i = i + (inclusive and 0 or 1) + elseif line:match("@leet end") then + end_i = i - (inclusive and 0 or 1) end end - return table.concat(lines, "\n", start_i, end_i) + return start_i, end_i, lines end +function Question:fold_range() + local start_i, _, lines = self:range(true) + if start_i == nil or start_i <= 1 then + return + end + + local i = start_i - 1 + while lines[i] == "" do + i = i - 1 + end + + if 1 < i then + return i + end +end + +---@param submit boolean +---@return string +function Question:lines(submit) + local start_i, end_i, lines = self:range() + + start_i = start_i or 1 + end_i = end_i or #lines + + local prefix = not submit and ("\n"):rep(start_i - 1) or "" + return prefix .. table.concat(lines, "\n", start_i, end_i) +end + +---@param self lc.ui.Question ---@param lang lc.lang Question.change_lang = vim.schedule_wrap(function(self, lang) - self.lang = lang - self:create_file() + local old_lang, old_bufnr = self.lang, self.bufnr - local new_bufnr = vim.fn.bufadd(self.file:absolute()) - if new_bufnr ~= 0 then - vim.api.nvim_win_set_buf(self.winid, new_bufnr) - self.bufnr = new_bufnr - else - log.error("Changing language failed") + local ok, err = pcall(function() + self.lang = lang + local path, existed = self:path() + + self.bufnr = vim.fn.bufadd(path) + assert(self.bufnr ~= 0, "Failed to create buffer " .. path) + + local loaded = vim.api.nvim_buf_is_loaded(self.bufnr) + vim.fn.bufload(self.bufnr) + + vim.api.nvim_set_option_value("buflisted", false, { buf = old_bufnr }) + self:open_buffer(existed) + + if not loaded then + utils.exec_hooks("question_enter", self) + end + end) + + if not ok then + log.error("Failed to change language\n" .. err) + self.lang = old_lang + self.bufnr = old_bufnr end end) diff --git a/lua/leetcode-ui/renderer/init.lua b/lua/leetcode-ui/renderer/init.lua index 6f424960..c1c201ac 100644 --- a/lua/leetcode-ui/renderer/init.lua +++ b/lua/leetcode-ui/renderer/init.lua @@ -24,38 +24,56 @@ function Renderer:draw(component) self.bufnr = component.bufnr self.winid = component.winid - self:map("n", keys.confirm, function() self:handle_press() end) + if not (self.bufnr and vim.api.nvim_buf_is_valid(self.bufnr)) then + return + end + + self:map("n", keys.confirm, function() + self:handle_press() + end) self:clear_keymaps() self._.buttons = {} self._.line_idx = 1 local c_ok, c = pcall(vim.api.nvim_win_get_cursor, self.winid) - self:modifiable(function() + self:with_modifiable(function() vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, {}) vim.api.nvim_buf_clear_namespace(self.bufnr, -1, 0, -1) Renderer.super.draw(self, self, self._.opts) end) - if c_ok then pcall(vim.api.nvim_win_set_cursor, self.winid, c) end + if c_ok then + pcall(vim.api.nvim_win_set_cursor, self.winid, c) + end end ---@private --- ---@param fn function -function Renderer:modifiable(fn) +function Renderer:with_modifiable(fn) local bufnr = self.bufnr - if not (bufnr and vim.api.nvim_buf_is_valid(bufnr)) then return end + if not (bufnr and vim.api.nvim_buf_is_valid(bufnr)) then + return + end local modi = vim.api.nvim_buf_get_option(bufnr, "modifiable") - if not modi then vim.api.nvim_buf_set_option(bufnr, "modifiable", true) end + if not modi then + vim.api.nvim_buf_set_option(bufnr, "modifiable", true) + end fn() - if not modi then vim.api.nvim_buf_set_option(bufnr, "modifiable", false) end + if not modi then + vim.api.nvim_buf_set_option(bufnr, "modifiable", false) + end end function Renderer:map(mode, key, handler, opts) -- - if not self.bufnr then return end - if type(key) == "number" then key = tostring(key) end + if not self.bufnr then + return + end + if type(key) == "number" then + key = tostring(key) + end if type(key) == "table" then for _, k in ipairs(key) do @@ -68,15 +86,16 @@ function Renderer:map(mode, key, handler, opts) -- local clearable = options.clearable options.clearable = nil - if clearable then self._.keymaps[key] = mode end + if clearable then + self._.keymaps[key] = mode + end vim.keymap.set(mode, key, handler, options) vim.keymap.set(mode, key, handler, options) end end function Renderer:unmap(mode, key) -- - local ok, err = pcall(vim.api.nvim_buf_del_keymap, self.bufnr, mode, key) - if not ok then log.error(err) end + pcall(vim.api.nvim_buf_del_keymap, self.bufnr, mode, key) end function Renderer:clear_keymaps() @@ -93,7 +112,9 @@ function Renderer:clear() self._.line_idx = 1 self._.buttons = {} self:clear_keymaps() - self:modifiable(function() vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, {}) end) + self:with_modifiable(function() + vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, {}) + end) end ---@param val? integer Optional value to increment line index by @@ -109,15 +130,25 @@ function Renderer:handle_press(line_idx) vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "n", true) end - if not self.bufnr or not self.winid then feedenter() end - if not line_idx and not vim.api.nvim_win_is_valid(self.winid) then return feedenter() end + if not self.bufnr or not self.winid then + feedenter() + end + if not line_idx and not vim.api.nvim_win_is_valid(self.winid) then + return feedenter() + end line_idx = line_idx or vim.api.nvim_win_get_cursor(self.winid)[1] - if not self._.buttons[line_idx] then return feedenter() end + if not self._.buttons[line_idx] then + return feedenter() + end - local ok, err = pcall(function() self._.buttons[line_idx]:press() end) - if not ok then log.error(err) end + local ok, err = pcall(function() + self._.buttons[line_idx]:press() + end) + if not ok then + log.error(err) + end end ---@param button lc.ui.Button @@ -130,7 +161,9 @@ function Renderer:apply_button(button) -- end ---@param layout lc.ui.Renderer -function Renderer:set(layout) self._.items = layout._.items end +function Renderer:set(layout) + self._.items = layout._.items +end ---@param components lc.ui.Lines[] ---@param opts? lc.ui.opts diff --git a/lua/leetcode-ui/renderer/menu.lua b/lua/leetcode-ui/renderer/menu.lua index 4caf4771..4b0d9eac 100644 --- a/lua/leetcode-ui/renderer/menu.lua +++ b/lua/leetcode-ui/renderer/menu.lua @@ -1,17 +1,21 @@ local log = require("leetcode.logger") local cookie = require("leetcode.cache.cookie") local config = require("leetcode.config") -local utils = require("leetcode-ui.utils") +local ui_utils = require("leetcode-ui.utils") +local utils = require("leetcode.utils") local Renderer = require("leetcode-ui.renderer") +local api = vim.api ----@class lc.ui.menu : lc.ui.Renderer @field tabpage integer +---@class lc.ui.Menu : lc.ui.Renderer ---@field cursor lc-menu.cursor ---@field maps table local Menu = Renderer:extend("LeetMenu") local function tbl_keys(t) local keys = vim.tbl_keys(t) - if vim.tbl_isempty(keys) then return end + if vim.tbl_isempty(keys) then + return + end table.sort(keys) return keys end @@ -23,43 +27,79 @@ end ---@private function Menu:autocmds() - local group_id = vim.api.nvim_create_augroup("leetcode_menu", { clear = true }) + local group_id = api.nvim_create_augroup("leetcode_menu", { clear = true }) + + api.nvim_create_autocmd("WinResized", { + group = group_id, + buffer = self.bufnr, + callback = function() + self:draw() + end, + }) - vim.api.nvim_create_autocmd("WinResized", { + api.nvim_create_autocmd("CursorMoved", { group = group_id, buffer = self.bufnr, - callback = function() self:draw() end, + callback = function() + self:cursor_move() + end, }) - vim.api.nvim_create_autocmd("CursorMoved", { + api.nvim_create_autocmd("QuitPre", { group = group_id, buffer = self.bufnr, - callback = function() self:cursor_move() end, + callback = function() + self.winid = nil + self.bufnr = nil + self:clear_keymaps() + end, }) end function Menu:cursor_move() - if not self.winid or not vim.api.nvim_win_is_valid(self.winid) then return end + if not (self.winid and api.nvim_win_is_valid(self.winid)) then + return + end - local curr = vim.api.nvim_win_get_cursor(self.winid) + local curr = api.nvim_win_get_cursor(self.winid) local prev = self.cursor.prev local keys = tbl_keys(self._.buttons) - if not keys then return end + if not keys then + return + end + + local function find_nearest(l, r) + while l < r do + local m = math.floor((l + r) / 2) + + if keys[m] < curr[1] then + l = m + 1 + else + r = m + end + end + + return math.max(r, 1) + end if prev then - if curr[1] > prev[1] then - self.cursor.idx = math.min(self.cursor.idx + 1, #keys) - elseif curr[1] < prev[1] then - self.cursor.idx = math.max(self.cursor.idx - 1, 1) + local next_idx = self.cursor.idx + + if curr[1] < prev[1] then + next_idx = find_nearest(1, self.cursor.idx - 1) + elseif curr[1] > prev[1] then + next_idx = find_nearest(self.cursor.idx + 1, #keys) end + + self.cursor.idx = next_idx end local row = keys[self.cursor.idx] local col = #vim.fn.getline(row):match("^%s*") self.cursor.prev = { row, col } - vim.api.nvim_win_set_cursor(self.winid, self.cursor.prev) + api.nvim_win_set_cursor(self.winid, self.cursor.prev) end function Menu:cursor_reset() @@ -82,10 +122,9 @@ function Menu:set_page(name) end function Menu:apply_options() - vim.api.nvim_buf_set_name(self.bufnr, "") - pcall(vim.diagnostic.disable, self.bufnr) + api.nvim_buf_set_name(self.bufnr, "") - utils.set_buf_opts(self.bufnr, { + ui_utils.buf_set_opts(self.bufnr, { modifiable = false, buflisted = false, matchpairs = "", @@ -94,7 +133,7 @@ function Menu:apply_options() filetype = config.name, synmaxcol = 0, }) - utils.set_win_opts(self.winid, { + ui_utils.win_set_opts(self.winid, { wrap = false, colorcolumn = "", foldlevel = 999, @@ -107,6 +146,42 @@ function Menu:apply_options() spell = false, signcolumn = "no", }) + vim.schedule(function() + ui_utils.win_set_winfixbuf(self.winid) + end) +end + +function Menu:unmount() + if vim.v.dying ~= 0 then + return + end + + require("leetcode.command").q_close_all() + + vim.schedule(function() + if self.winid and vim.api.nvim_win_is_valid(self.winid) then + vim.api.nvim_win_close(self.winid, true) + end + + if self.bufnr and vim.api.nvim_buf_is_valid(self.bufnr) then + vim.api.nvim_buf_delete(self.bufnr, { force = true }) + end + end) +end + +function Menu:remount() + if self.winid and api.nvim_win_is_valid(self.winid) then + api.nvim_win_close(self.winid, true) + end + if self.bufnr and api.nvim_buf_is_valid(self.bufnr) then + api.nvim_buf_delete(self.bufnr, { force = true }) + end + + vim.cmd("0tabnew") + self.bufnr = api.nvim_get_current_buf() + self.winid = api.nvim_get_current_win() + + self:_mount() end function Menu:mount() @@ -119,18 +194,23 @@ function Menu:mount() self:set_page("signin") log.err(err) else - self:set_page("menu") + local cmd = require("leetcode.command") + cmd.start_user_session() end end) else self:set_page("signin") end + self:_mount() + + return self +end + +function Menu:_mount() self:apply_options() self:autocmds() self:draw() - - return self end function Menu:init() @@ -144,13 +224,13 @@ function Menu:init() } self.maps = {} - self.bufnr = vim.api.nvim_get_current_buf() - self.winid = vim.api.nvim_get_current_win() + self.bufnr = api.nvim_get_current_buf() + self.winid = api.nvim_get_current_win() - _Lc_Menu = self + _Lc_state.menu = self end ----@type fun(): lc.ui.menu +---@type fun(): lc.ui.Menu local LeetMenu = Menu return LeetMenu diff --git a/lua/leetcode-ui/renderer/result.lua b/lua/leetcode-ui/renderer/result.lua index 7803f27e..23110ad3 100644 --- a/lua/leetcode-ui/renderer/result.lua +++ b/lua/leetcode-ui/renderer/result.lua @@ -5,7 +5,7 @@ local SimilarQuestions = require("leetcode-ui.group.similar-questions") local Renderer = require("leetcode-ui.renderer") local Pre = require("leetcode-ui.group.pre") -local Group = require("leetcode-ui.group") +local Input = require("leetcode-ui.group.pre.input") local Stdout = require("leetcode-ui.group.pre.stdout") local Case = require("leetcode-ui.group.case") @@ -54,7 +54,9 @@ function ResultLayout:handle_accepted(item) local lang = utils.get_lang_by_name(item.pretty_lang) local lang_text = { item.pretty_lang, "Structure" } - if lang then lang_text = { lang.icon .. " " .. lang.lang, lang.hl or "Structure" } end + if lang then + lang_text = { lang.icon .. " " .. lang.lang, lang.hl or "Structure" } + end if config.translator then runtime @@ -108,7 +110,9 @@ end --- ---@param item lc.runtime function ResultLayout:handle_runtime(item) -- status code = 10 - if item._.submission then return self:handle_accepted(item) end + if item._.submission then + return self:handle_accepted(item) + end local header = Header(item) self:insert(header) @@ -125,12 +129,12 @@ function ResultLayout:handle_submission_error(item) -- status code = 11 self:insert(header) self:insert(Case({ ---@diagnostic disable-line - input = item.input_formatted, + input = vim.split(item.input, "\n"), raw_input = item.last_testcase, output = item.code_output, expected = item.expected_output, std_output = item.std_output, - }, false)) + }, false, self.parent.question)) end ---@private @@ -141,15 +145,12 @@ function ResultLayout:handle_limit_exceeded(item) -- status code = 12,13,14 self:insert(header) if item._.submission then - local last_testcase = Line() - last_testcase:append(item.last_testcase:gsub("\n", " "), "leetcode_indent") - - local pre_header = Line() - pre_header:append((" %s"):format(t("Last Executed Input")), "leetcode_normal") - - local last_exec = Pre(pre_header, last_testcase) - - self:insert(last_exec) + local input = Input( + (" %s"):format(t("Last Executed Input")), + vim.split(item.last_testcase, "\n"), + self.parent.question.q.meta_data.params + ) + self:insert(input) local stdout = Stdout(item.std_output or "") self:insert(stdout) @@ -252,7 +253,9 @@ function ResultLayout:handle_res(item) end, -- unknown - ["unknown"] = function() log.error("unknown runner status code: " .. item.status_code) end, + ["unknown"] = function() + log.error("unknown runner status code: " .. item.status_code) + end, } local handler = handlers[item.status_code] diff --git a/lua/leetcode-ui/split/description.lua b/lua/leetcode-ui/split/description.lua index 25887b7d..a5c16c05 100644 --- a/lua/leetcode-ui/split/description.lua +++ b/lua/leetcode-ui/split/description.lua @@ -23,7 +23,9 @@ function Description:autocmds() vim.api.nvim_create_autocmd("WinResized", { group = group_id, buffer = self.bufnr, - callback = function() self:draw() end, + callback = function() + self:draw() + end, }) end @@ -32,7 +34,7 @@ function Description:mount() self:populate() local ui_utils = require("leetcode-ui.utils") - ui_utils.set_buf_opts(self.bufnr, { + ui_utils.buf_set_opts(self.bufnr, { modifiable = false, buflisted = false, matchpairs = "", @@ -41,7 +43,7 @@ function Description:mount() filetype = config.name, synmaxcol = 0, }) - ui_utils.set_win_opts(self.winid, { + ui_utils.win_set_opts(self.winid, { winhighlight = "Normal:NormalFloat,FloatBorder:FloatBorder", wrap = not img_sup, colorcolumn = "", @@ -56,6 +58,8 @@ function Description:mount() signcolumn = "no", linebreak = true, }) + ui_utils.win_set_winfixbuf(self.winid) + if not img_ok and config.user.image_support then log.error("image.nvim not found but `image_support` is enabled") end @@ -71,7 +75,9 @@ function Description:draw() end function Description:draw_imgs() - if not img_sup then return end + if not img_sup then + return + end local lines = vim.api.nvim_buf_get_lines(self.bufnr, 1, -1, false) for i, line in ipairs(lines) do @@ -86,7 +92,9 @@ function Description:draw_imgs() window = self.winid, with_virtual_padding = true, }, function(image) - if not image then return end + if not image then + return + end self.images[link] = image image:render({ y = i + 1 }) @@ -119,6 +127,9 @@ function Description:populate() header:append(q.frontend_id .. ". ", "leetcode_normal") header:append(utils.translate(q.title, q.translated_title)) + if q.is_paid_only then + header:append(" " .. t("Premium"), "leetcode_medium") + end header:endgrp() local show_stats = self.show_stats @@ -129,31 +140,28 @@ function Description:populate() header:append("????", "leetcode_list") end - local user_status = { - ac = { "", "leetcode_easy" }, - notac = { "󱎖", "leetcode_medium" }, - todo = { "", "leetcode_alt" }, - } - if user_status[self.question.cache.status] then - local s = user_status[self.question.cache.status] + if config.icons.hl.status[self.question.cache.status] then + local s = config.icons.hl.status[self.question.cache.status] header:append(" "):append(s[1], s[2]) end - header:append(" | ") + header:append((" %s "):format(config.icons.bar)) local likes = show_stats and q.likes or "___" header:append(likes .. " ", "leetcode_alt") local dislikes = show_stats and q.dislikes or "___" - if not config.is_cn then header:append((" %s "):format(dislikes), "leetcode_alt") end + if not config.is_cn then + header:append((" %s "):format(dislikes), "leetcode_alt") + end - header:append(" | ") + header:append((" %s "):format(config.icons.bar)) local ac_rate = show_stats and q.stats.acRate or "__%" local total_sub = show_stats and q.stats.totalSubmission or "__" header:append(("%s %s %s"):format(ac_rate, t("of"), total_sub), "leetcode_alt") if not vim.tbl_isempty(q.hints) then - header:append(" | ") + header:append((" %s "):format(config.icons.bar)) header:append("󰛨 " .. t("Hints"), "leetcode_hint") end header:endgrp() diff --git a/lua/leetcode-ui/split/init.lua b/lua/leetcode-ui/split/init.lua index ec7f5be8..413480c7 100644 --- a/lua/leetcode-ui/split/init.lua +++ b/lua/leetcode-ui/split/init.lua @@ -49,7 +49,9 @@ function Split:show() end function Split:hide() - if not self.visible then return end + if not self.visible then + return + end Split.super.hide(self) self.visible = false @@ -60,10 +62,14 @@ function Split:mount() self.visible = true - self:map("n", keys.toggle, function() self:toggle() end) + self:map("n", keys.toggle, function() + self:toggle() + end) end -function Split:map(...) self.renderer:map(...) end +function Split:map(...) + self.renderer:map(...) +end function Split:unmount() Split.super.unmount(self) @@ -71,9 +77,13 @@ function Split:unmount() self.visible = false end -function Split:draw() self.renderer:draw(self) end +function Split:draw() + self.renderer:draw(self) +end -function Split:clear() self.renderer:clear() end +function Split:clear() + self.renderer:clear() +end function Split:update_renderer() self.renderer.bufnr = self.bufnr diff --git a/lua/leetcode-ui/utils.lua b/lua/leetcode-ui/utils.lua index 29b15fcf..b6d2aa4e 100644 --- a/lua/leetcode-ui/utils.lua +++ b/lua/leetcode-ui/utils.lua @@ -1,5 +1,6 @@ local O = require("nui.object") local log = require("leetcode.logger") +local config = require("leetcode.config") ---@class lc-ui.Utils local utils = {} @@ -16,7 +17,9 @@ end ---@param item lc.ui.Lines function utils.longest_line(item) - if item.class.name == "LeetLine" then return vim.api.nvim_strwidth(item:content()) end + if item.class.name == "LeetLine" then + return vim.api.nvim_strwidth(item:content()) + end local max_len = 0 for _, line in pairs(item:contents()) do @@ -46,20 +49,18 @@ end ---@param status string function utils.status_to_hl(status) - if not status then return end - - local user_status = { - ac = { "", "leetcode_easy" }, - notac = { "󱎖", "leetcode_medium" }, - todo = { "", "leetcode_alt" }, - } + if not status then + return + end - return table.unpack(user_status[status]) + return table.unpack(config.icons.hl.status[status]) end ---@param layout lc.ui.Renderer function utils.win_width(layout) - if not vim.api.nvim_win_is_valid(layout.winid) then return 0 end + if not (layout.winid and vim.api.nvim_win_is_valid(layout.winid)) then + return 0 + end return vim.api.nvim_win_get_width(layout.winid) end @@ -102,25 +103,62 @@ function utils.get_padding(lines, layout) return padding end -function utils.set_buf_opts(bufnr, options) - if not vim.api.nvim_buf_is_valid(bufnr) then return end +function utils.buf_set_opts(bufnr, options) + if not vim.api.nvim_buf_is_valid(bufnr) then + return + end for opt, value in pairs(options) do local ok, err = pcall(vim.api.nvim_set_option_value, opt, value, { buf = bufnr }) - if not ok then log.error(err) end + if not ok then + log.error(err) + end end end -function utils.set_win_opts(winid, options) - if not vim.api.nvim_win_is_valid(winid) then return end +function utils.win_set_opts(winid, options) + if not vim.api.nvim_win_is_valid(winid) then + return + end for opt, value in pairs(options) do local ok, err = pcall(vim.api.nvim_set_option_value, opt, value, { win = winid, scope = "local" }) - if not ok then log.error(err) end + if not ok then + log.error(err) + end end end +---@param winid number +function utils.win_set_winfixbuf(winid) + local u = require("leetcode.utils") + u.with_version("0.10.0", function() + utils.win_set_opts(winid, { winfixbuf = true }) + end) +end + +---@param winid number +---@param bufnr number +---@param force? boolean +function utils.win_set_buf(winid, bufnr, force) + local u = require("leetcode.utils") + + u.with_version("0.10.0", function() + local wfb = vim.api.nvim_win_get_option(winid, "winfixbuf") + + if not wfb then + vim.api.nvim_win_set_buf(winid, bufnr) + elseif force then + utils.win_set_opts(winid, { winfixbuf = false }) + vim.api.nvim_win_set_buf(winid, bufnr) + utils.win_set_opts(winid, { winfixbuf = true }) + end + end, function() + vim.api.nvim_win_set_buf(winid, bufnr) + end) +end + function utils.is_instance(instance, class) return type(instance) == "table" and O.is_instance(instance, class) end diff --git a/lua/leetcode.lua b/lua/leetcode.lua index d1f66a8d..4d55f27e 100644 --- a/lua/leetcode.lua +++ b/lua/leetcode.lua @@ -3,85 +3,138 @@ local config = require("leetcode.config") ---@class lc.LeetCode local leetcode = {} -function leetcode.should_skip() - if vim.fn.argc() ~= 1 then return true end - - local usr_arg, arg = config.user.arg, vim.fn.argv()[1] - if usr_arg ~= arg then return true end +---@private +local function log_failed_to_init() + local log = require("leetcode.logger") + log.warn("Failed to initialize: `neovim` contains listed buffers") +end - local lines = vim.api.nvim_buf_get_lines(0, 0, -1, true) - if #lines > 1 or (#lines == 1 and lines[1]:len() > 0) then - local log = require("leetcode.logger") - log.warn(("Failed to initialize: `%s` is not an empty buffer"):format(usr_arg)) - return true +local function log_buf_not_empty(bufname) + local log = require("leetcode.logger") + if bufname and bufname ~= "" then + log.warn(("Failed to initialize: `%s` is not an empty buffer"):format(bufname)) + else + log.warn("Failed to initialize: not an empty buffer") end - - return false end -function leetcode.setup_cmds() - local cmd = require("leetcode.command") - cmd.setup() +local function buf_is_empty(buf) + local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, true) + return not (#lines > 1 or (#lines == 1 and lines[1]:len() > 0)) end -function leetcode.validate() - local utils = require("leetcode.utils") - - assert(vim.fn.has("nvim-0.9.0") == 1, "Neovim >= 0.9.0 required") - - if not utils.get_lang(config.lang) then -- - ---@type lc.lang[] - local lang_slugs = vim.tbl_map(function(lang) return lang.slug end, config.langs) +---@param on_vimenter boolean +--- +---@return boolean, boolean? (skip, standalone) +function leetcode.should_skip(on_vimenter) + if on_vimenter then + if vim.fn.argc(-1) ~= 1 then + return true + end - local matches = {} - for _, slug in ipairs(lang_slugs) do - local percent = slug:match(config.lang) or config.lang:match(slug) - if percent then table.insert(matches, slug) end + local usr_arg, arg = config.user.arg, vim.fn.argv(0, -1) + if usr_arg ~= arg then + return true end - if not vim.tbl_isempty(matches) then - local log = require("leetcode.logger") - log.warn("Did you mean: { " .. table.concat(matches, ", ") .. " }?") + if not buf_is_empty(0) then + log_buf_not_empty(usr_arg) + return true end - error("Unsupported Language: " .. config.lang) + return false, true + else + local listed_bufs = vim.tbl_filter(function(info) + return info.listed == 1 + end, vim.fn.getbufinfo()) + + if #listed_bufs == 0 then + return false, true + elseif vim.fn.argc(-1) == 0 and #listed_bufs == 1 then + local buf = listed_bufs[1] + + if vim.api.nvim_get_current_buf() ~= buf.bufnr then + if config.plugins.non_standalone then + return false, false + else + log_failed_to_init() + return true + end + end + + vim.schedule(function() + if buf.changed == 1 then + vim.api.nvim_buf_delete(buf.bufnr, { force = true }) + end + end) + + return false, true + elseif #listed_bufs >= 1 then + if config.plugins.non_standalone then + return false, false + else + log_failed_to_init() + return true + end + end end end -function leetcode.start() - if leetcode.should_skip() then return end +function leetcode.setup_cmds() + require("leetcode.command").setup() +end - leetcode.validate() +---@param on_vimenter boolean +function leetcode.start(on_vimenter) + local skip = leetcode.should_skip(on_vimenter) + if skip then + return false + end - local path = require("plenary.path") - config.home = path:new(config.user.directory) ---@diagnostic disable-line - config.home:mkdir() + config.setup() - vim.api.nvim_set_current_dir(config.home:absolute()) + vim.api.nvim_set_current_dir(config.storage.home:absolute()) leetcode.setup_cmds() - config.load_plugins() - - local utils = require("leetcode.utils") - utils.exec_hooks("LeetEnter") local theme = require("leetcode.theme") theme.setup() + if not on_vimenter then + vim.cmd.enew() + end + local Menu = require("leetcode-ui.renderer.menu") Menu():mount() + + local utils = require("leetcode.utils") + utils.exec_hooks("enter") + + return true +end + +function leetcode.stop() + vim.cmd("qa!") end ---@param cfg? lc.UserConfig function leetcode.setup(cfg) config.apply(cfg or {}) + vim.api.nvim_create_user_command("Leet", require("leetcode.command").start_with_cmd, { + bar = true, + bang = true, + desc = "Open leetcode.nvim", + }) + local group_id = vim.api.nvim_create_augroup("leetcode_start", { clear = true }) vim.api.nvim_create_autocmd("VimEnter", { group = group_id, pattern = "*", nested = true, - callback = leetcode.start, + callback = function() + leetcode.start(true) + end, }) end diff --git a/lua/leetcode/api/auth.lua b/lua/leetcode/api/auth.lua index 9ac11d70..cef2f941 100644 --- a/lua/leetcode/api/auth.lua +++ b/lua/leetcode/api/auth.lua @@ -13,7 +13,9 @@ function Auth.user(cb) if cb then utils.query(query, {}, { - callback = function(res, err) cb(Auth.handle(res, err)) end, + callback = function(res, err) + cb(Auth.handle(res, err)) + end, endpoint = urls.auth, }) else @@ -26,13 +28,15 @@ end ---@private ---@return lc.UserStatus, lc.err function Auth.handle(res, err) - if err then return res, err end + if err then + return res, err + end local auth = res.data.userStatus err = {} if (not config.is_cn and auth.id == vim.NIL) or (config.is_cn and auth.slug == vim.NIL) then - err.msg = "Session expired?" + err.msg = "Cookie expired?" elseif not auth.is_signed_in then err.msg = "Sign-in failed" elseif not auth.is_verified then diff --git a/lua/leetcode/api/headers.lua b/lua/leetcode/api/headers.lua index 2d9f6b71..a476a12f 100644 --- a/lua/leetcode/api/headers.lua +++ b/lua/leetcode/api/headers.lua @@ -7,11 +7,13 @@ function headers.get() local cookie = Cookie.get() return vim.tbl_extend("force", { + ["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0", ["Referer"] = ("https://leetcode.%s"):format(config.domain), ["Origin"] = ("https://leetcode.%s/"):format(config.domain), - ["content-type"] = "application/json", + ["Content-Type"] = "application/json", ["Accept"] = "application/json", ["Host"] = ("leetcode.%s"):format(config.domain), + -- ["X-Requested-With"] = "XMLHttpRequest", }, cookie and { ["Cookie"] = cookie.str, ["x-csrftoken"] = cookie.csrftoken, diff --git a/lua/leetcode/api/interpreter.lua b/lua/leetcode/api/interpreter.lua index 10ad8076..a90438d7 100644 --- a/lua/leetcode/api/interpreter.lua +++ b/lua/leetcode/api/interpreter.lua @@ -1,21 +1,14 @@ local log = require("leetcode.logger") local urls = require("leetcode.api.urls") +local config = require("leetcode.config") local utils = require("leetcode.api.utils") -local spinner = require("leetcode.logger.spinner") local t = require("leetcode.translator") ---@class lc.Interpreter local interpreter = {} -local check_state = { - ["PENDING"] = "Pending…", - ["STARTED"] = "Judging…", - ["SUCCESS"] = "Finished", - ["FAILURE"] = "Failed", -- CODE: 16 -} - ---@param item lc.interpreter_response --- ---@return lc.interpreter_response @@ -49,25 +42,18 @@ end ---@param id string ---@param callback function function interpreter.listener(id, callback) - local noti = spinner:init(check_state["PENDING"], "points") - local function listen() interpreter.check(id, function(item, err) - if err then return noti:stop(err.msg, false) end - - if item.status_code then + if err then -- error + callback(nil, nil, err) + elseif item.status_code then -- got results item = interpreter:handle_item(item) - noti:stop(item.status_msg, item._.success) - return callback(item) - else - noti:update(check_state[item.state]) - if item.state == "PENDING" then - noti:change("points") - elseif item.state == "STARTED" then - noti:change("dot") - end - - vim.defer_fn(listen, 500) + callback(item) + else -- still judging + local intervals = config.auth.is_premium and { 450, 450 } or { 450, 900 } + local interval = item.sate == "STARTED" and intervals[2] or intervals[1] + callback(nil, item.state) + vim.defer_fn(listen, interval) end end) end @@ -80,32 +66,28 @@ end ---@field typed_code string ---@field data_input string ----@param title_slug string ----@param body lc.Interpret.body|string ----@param callback function -function interpreter.interpret_solution(title_slug, body, callback) - local url = urls.interpret:format(title_slug) - - interpreter.fetch(url, { - body = body, - callback = function(res, success) - callback(success) - if success then interpreter.listener(res.interpret_id, callback) end - end, - }) -end - ----@param title_slug string +---@param submit boolean +---@param q lc.ui.Question ---@param body lc.Interpret.body|string ---@param callback function -function interpreter.submit(title_slug, body, callback) - local url = urls.submit:format(title_slug) +function interpreter.run(submit, q, body, callback) + local url = (submit and urls.submit or urls.interpret):format(q.q.title_slug) interpreter.fetch(url, { body = body, - callback = function(res, success) - callback(success) - if success then interpreter.listener(res.submission_id, callback) end + callback = function(res, err) + if err then + if err.status == 429 then + err.msg = "You have attempted to run code too soon" + err.lvl = vim.log.levels.WARN + end + callback(nil, nil, err) + else + local id = submit and res.submission_id or res.interpret_id + q.console.testcase:snapshot(id, res) + q.console.result:clear() + interpreter.listener(id, callback) + end end, }) end @@ -122,17 +104,7 @@ end function interpreter.fetch(url, opts) utils.post(url, { body = opts.body, - callback = function(res, err) - if err then - if err.status == 429 then - err.msg = "You have attempted to run code too soon" - err.lvl = vim.log.levels.WARN - end - opts.callback(log.err(err), false) - else - opts.callback(res, true) - end - end, + callback = opts.callback, }) end diff --git a/lua/leetcode/api/problems.lua b/lua/leetcode/api/problems.lua index 5c74a497..9049bb8e 100644 --- a/lua/leetcode/api/problems.lua +++ b/lua/leetcode/api/problems.lua @@ -17,33 +17,45 @@ function Problems.all(cb, noti) local endpoint = urls.problems:format("algorithms") local spinner - if noti then spinner = Spinner:init("updating problemlist cache...", "points") end + if noti then + spinner = Spinner:init("updating problemlist cache...", "points") + end if cb then utils.get(endpoint, { callback = function(res, err) if err then - if spinner then spinner:stop(err.msg, false) end + if spinner then + spinner:stop(err.msg, false) + end return cb(nil, err) end local problems = utils.normalize_problems(res.stat_status_pairs) if config.is_cn then - if spinner then spinner:update("fetching title translations") end + if spinner then + spinner:update("fetching title translations") + end Problems.translated_titles(function(titles, terr) if terr then - if spinner then spinner:stop(terr.msg, false) end + if spinner then + spinner:stop(terr.msg, false) + end return cb(nil, terr) end problems = utils.translate_titles(problems, titles) - if spinner then spinner:stop("cache updated") end + if spinner then + spinner:stop("cache updated") + end cb(problems) end) else - if spinner then spinner:stop("cache updated") end + if spinner then + spinner:stop("cache updated") + end cb(problems) end @@ -52,7 +64,9 @@ function Problems.all(cb, noti) else local res, err = utils.get(endpoint) if err then - if spinner then spinner:stop(err.msg, false) end + if spinner then + spinner:stop(err.msg, false) + end return nil, err end @@ -61,14 +75,20 @@ function Problems.all(cb, noti) if config.is_cn then local titles, terr = Problems.translated_titles() if terr then - if spinner then spinner:stop(terr.msg, false) end + if spinner then + spinner:stop(terr.msg, false) + end return nil, terr end - if spinner then spinner:stop("problems cache updated") end + if spinner then + spinner:stop("problems cache updated") + end return utils.translate_titles(problems, titles) else - if spinner then spinner:stop("problems cache updated") end + if spinner then + spinner:stop("problems cache updated") + end return problems end end @@ -79,7 +99,9 @@ function Problems.question_of_today(cb) utils.query(query, {}, { callback = function(res, err) - if err then return cb(nil, err) end + if err then + return cb(nil, err) + end local tday_record = res.data["todayRecord"] local question = config.is_cn and tday_record[1].question or tday_record.question @@ -94,13 +116,17 @@ function Problems.translated_titles(cb) if cb then utils.query(query, {}, { callback = function(res, err) - if err then return cb(nil, err) end + if err then + return cb(nil, err) + end cb(res.data.translations) end, }) else local res, err = utils.query(query, {}) - if err then return nil, err end + if err then + return nil, err + end return res.data.translations end end diff --git a/lua/leetcode/api/queries.lua b/lua/leetcode/api/queries.lua index ebf222a9..b8356208 100644 --- a/lua/leetcode/api/queries.lua +++ b/lua/leetcode/api/queries.lua @@ -9,6 +9,7 @@ queries.auth = [[ is_signed_in: isSignedIn is_premium: isPremium is_verified: isVerified + session_id: activeSessionId } } ]] @@ -155,4 +156,27 @@ queries.languages = [[ } ]] +queries.streak = [[ + query getStreakCounter { + streakCounter { + streakCount + daysSkipped + todayCompleted: currentDayCompleted + } + } + ]] + +queries.session_progress = [[ + query userSessionProgress($username: String!) { + matchedUser(username: $username) { + submitStats { + acSubmissionNum { + difficulty + count + } + } + } + } + ]] + return queries diff --git a/lua/leetcode/api/question.lua b/lua/leetcode/api/question.lua index 109690d8..f76f5611 100644 --- a/lua/leetcode/api/question.lua +++ b/lua/leetcode/api/question.lua @@ -2,6 +2,7 @@ local utils = require("leetcode.api.utils") local log = require("leetcode.logger") local queries = require("leetcode.api.queries") local problemlist = require("leetcode.cache.problemlist") +local urls = require("leetcode.api.urls") local question = {} @@ -19,12 +20,16 @@ function question.by_title_slug(title_slug) local query = queries.question local res, err = utils.query(query, variables) - if not res or err then return log.err(err) end + if not res or err then + return log.err(err) + end local q = res.data.question q.meta_data = select(2, pcall(utils.decode, q.meta_data)) q.stats = select(2, pcall(utils.decode, q.stats)) - if type(q.similar) == "string" then q.similar = utils.normalize_similar_cn(q.similar) end + if type(q.similar) == "string" then + q.similar = utils.normalize_similar_cn(q.similar) + end return q end @@ -40,14 +45,18 @@ function question.random(filters) local config = require("leetcode.config") local res, err = utils.query(query, variables) - if err then return nil, err end + if err then + return nil, err + end local q = res.data.randomQuestion if q == vim.NIL then local msg = "Random question fetch responded with `null`" - if filters then msg = msg .. ".\n\nMaybe invalid filters?\n" .. vim.inspect(filters) end + if filters then + msg = msg .. ".\n\nMaybe invalid filters?\n" .. vim.inspect(filters) + end return nil, { msg = msg, lvl = vim.log.levels.ERROR } end @@ -69,4 +78,12 @@ function question.random(filters) return q end +---@param qid integer +---@param lang lc.lang +---@param cb function +function question.latest_submission(qid, lang, cb) + local url = urls.latest_submission:format(qid, lang) + utils.get(url, { callback = cb }) +end + return question diff --git a/lua/leetcode/api/statistics.lua b/lua/leetcode/api/statistics.lua index 3e7f0ec9..6ce24db8 100644 --- a/lua/leetcode/api/statistics.lua +++ b/lua/leetcode/api/statistics.lua @@ -4,7 +4,7 @@ local log = require("leetcode.logger") local urls = require("leetcode.api.urls") local queries = require("leetcode.api.queries") ----@lc.api.statistics +---@class lc.api.statistics local statistics = {} function statistics.calendar(cb) @@ -17,7 +17,9 @@ function statistics.calendar(cb) utils.query(query, variables, { endpoint = urls.calendar, callback = function(res, err) - if err then return cb(nil, err) end + if err then + return cb(nil, err) + end local data = res.data local calendar = data["matchedUser"]["calendar"] @@ -32,6 +34,27 @@ function statistics.calendar(cb) }) end +---@param cb fun(res: lc.Stats.QuestionCount[], err: lc.err) +function statistics.session_progress(cb) + local variables = { + username = config.auth.name, + } + + local query = queries.session_progress + + utils.query(query, variables, { + callback = function(res, err) + if err then + return cb(nil, err) + end + + local data = res.data + local session_progress = data["matchedUser"]["submitStats"]["acSubmissionNum"] + cb(session_progress) + end, + }) +end + ---@param cb fun(res: lc.Stats.Res, err: lc.err) function statistics.solved(cb) local variables = { @@ -43,7 +66,9 @@ function statistics.solved(cb) utils.query(query, variables, { endpoint = urls.solved, callback = function(res, err) - if err then return cb(nil, err) end + if err then + return cb(nil, err) + end local data = res.data local questions_count = data["allQuestionsCount"] @@ -68,7 +93,9 @@ function statistics.skills(cb) utils.query(query, variables, { endpoint = urls.skills, callback = function(res, err) - if err then return cb(nil, err) end + if err then + return cb(nil, err) + end local data = res.data local tag_problems_counts = data["matchedUser"]["tag_problems_counts"] cb(tag_problems_counts) @@ -87,7 +114,9 @@ function statistics.languages(cb) utils.query(query, variables, { endpoint = urls.languages, callback = function(res, err) - if err then return cb(nil, err) end + if err then + return cb(nil, err) + end local data = res.data local lang_prob_count = data["matchedUser"]["languageProblemCount"] cb(lang_prob_count) @@ -95,4 +124,85 @@ function statistics.languages(cb) }) end +function statistics.streak(cb) + local variables = vim.empty_dict() + + local query = queries.streak + + utils.query(query, variables, { + endpoint = urls.streak_counter, + callback = function(res, err) + if err then + return cb(nil, err) + end + + local data = res.data + local streak = data["streakCounter"] + + if streak == vim.NIL then + err = { msg = "Failed to load streak counter" } + cb(nil, err) + else + cb(streak) + end + end, + }) +end + +---@param cb fun(res: lc.res.session[], err: lc.err) +function statistics.sessions(cb) + local url = urls.session + + utils.post(url, { + body = vim.empty_dict(), + callback = function(res, err) + if err then + return cb(nil, err) + end + config.sessions.update(res.sessions) + cb(res.sessions) + end, + }) +end + +function statistics.change_session(id, cb) + local body = { + func = "activate", + target = id, + } + + local url = urls.session + + utils.put(url, { + body = body, + callback = function(res, err) + if err then + return cb(nil, err) + end + config.sessions.update(res.sessions) + cb(res.sessions) + end, + }) +end + +function statistics.create_session(name, cb) + local body = { + func = "create", + name = name, + } + + local url = urls.session + + utils.put(url, { + body = body, + callback = function(res, err) + if err then + return cb(nil, err) + end + config.sessions.update(res.sessions) + cb(res.sessions) + end, + }) +end + return statistics diff --git a/lua/leetcode/api/types.lua b/lua/leetcode/api/types.lua index d6b95103..7cd2f84d 100644 --- a/lua/leetcode/api/types.lua +++ b/lua/leetcode/api/types.lua @@ -332,6 +332,7 @@ ---@field is_premium boolean ---@field is_verified boolean ---@field id integer +---@field session_id integer -------------------------------------------- --- Statistics @@ -392,3 +393,15 @@ ---@field lvl integer ---@alias lc.err lc.Api.err|nil + +-------------------------------------------- +--- Sessions +-------------------------------------------- +---@class lc.res.session +---@field ac_questions integer +---@field id integer +---@field is_active boolean +---@field name string +---@field submitted_questions integer +---@field total_acs integer +---@field total_submitted integer diff --git a/lua/leetcode/api/urls.lua b/lua/leetcode/api/urls.lua index 6f2fc3dc..ea7de617 100644 --- a/lua/leetcode/api/urls.lua +++ b/lua/leetcode/api/urls.lua @@ -13,5 +13,8 @@ urls.interpret = "/problems/%s/interpret_solution/" urls.submit = "/problems/%s/submit/" urls.run = "/problems/%s/interpret_solution/" urls.check = "/submissions/detail/%s/check/" +urls.latest_submission = "/submissions/latest/?qid=%s&lang=%s" +urls.streak_counter = "/graphql/" +urls.session = "/session/" return urls diff --git a/lua/leetcode/api/utils.lua b/lua/leetcode/api/utils.lua index 371baf35..74c9d5b1 100644 --- a/lua/leetcode/api/utils.lua +++ b/lua/leetcode/api/utils.lua @@ -1,13 +1,21 @@ local curl = require("plenary.curl") local log = require("leetcode.logger") local config = require("leetcode.config") -local path = require("plenary.path") local headers = require("leetcode.api.headers") local urls = require("leetcode.api.urls") ---@class lc.Api.Utils local utils = {} +---@param endpoint string +function utils.put(endpoint, opts) + local options = vim.tbl_deep_extend("force", { + endpoint = endpoint, + }, opts or {}) + + return utils.curl("put", options) +end + ---@param endpoint string function utils.post(endpoint, opts) local options = vim.tbl_deep_extend("force", { @@ -55,10 +63,14 @@ function utils.curl(method, params) }, params or {}) local url = ("https://leetcode.%s%s"):format(config.domain, params.endpoint) - if type(params.body) == "table" then params.body = vim.json.encode(params.body) end + if type(params.body) == "table" then + params.body = vim.json.encode(params.body) + end local tries = params.retry - local function should_retry(err) return err and err.status >= 500 and tries > 0 end + local function should_retry(err) + return err and err.status >= 500 and tries > 0 + end if params.callback then local cb = vim.schedule_wrap(params.callback) @@ -104,7 +116,9 @@ function utils.handle_res(out) local ok, msg = pcall(function() local dec = utils.decode(out.body) - if dec.error then return dec.error end + if dec.error then + return dec.error + end local tbl = {} for _, e in ipairs(dec.errors) do @@ -131,12 +145,15 @@ end ---@param err lc.err function utils.check_err(err) - if not err then return end + if not err then + return + end if err.status then if err.status == 401 or err.status == 403 then - require("leetcode.command").expire() - err.msg = "Session expired? Enter a new cookie to keep using `leetcode.nvim`" + -- require("leetcode.command").expire() + err.msg = + "Your cookie may have expired, or LeetCode has temporarily restricted API access" end end @@ -153,25 +170,26 @@ end function utils.normalize_similar_cn(s) s = select(2, pcall(utils.decode, s)) - return vim.tbl_map( - function(sq) - return { - title = sq.title, - translated_title = sq.translatedTitle, - paid_only = sq.isPaidOnly, - title_slug = sq.titleSlug, - difficulty = sq.difficulty, - } - end, - s - ) + return vim.tbl_map(function(sq) + return { + title = sq.title, + translated_title = sq.translatedTitle, + paid_only = sq.isPaidOnly, + title_slug = sq.titleSlug, + difficulty = sq.difficulty, + } + end, s) end -function utils.lvl_to_name(lvl) return ({ "Easy", "Medium", "Hard" })[lvl] end +function utils.lvl_to_name(lvl) + return ({ "Easy", "Medium", "Hard" })[lvl] +end ---@return lc.cache.Question[] function utils.normalize_problems(problems) - problems = vim.tbl_filter(function(p) return not p.stat.question__hide end, problems) + problems = vim.tbl_filter(function(p) + return not p.stat.question__hide + end, problems) local comp = function(a, b) local a_fid = a.stat.frontend_question_id @@ -192,28 +210,25 @@ function utils.normalize_problems(problems) end table.sort(problems, comp) - return vim.tbl_map( - function(p) - return { - status = p.status, - id = p.stat.question_id, - frontend_id = p.stat.frontend_question_id, - title = p.stat.question__title, - title_cn = "", - title_slug = p.stat.question__title_slug, - link = ("https://leetcode.%s/problems/%s/"):format( - config.domain, - p.stat.question__title_slug - ), - paid_only = p.paid_only, - ac_rate = p.stat.total_acs * 100 / math.max(p.stat.total_submitted, 1), - difficulty = utils.lvl_to_name(p.difficulty.level), - starred = p.is_favor, - topic_tags = {}, - } - end, - problems - ) + return vim.tbl_map(function(p) + return { + status = p.status == vim.NIL and "todo" or p.status, -- api returns nil for todo + id = p.stat.question_id, + frontend_id = p.stat.frontend_question_id, + title = p.stat.question__title, + title_cn = "", + title_slug = p.stat.question__title_slug, + link = ("https://leetcode.%s/problems/%s/"):format( + config.domain, + p.stat.question__title_slug + ), + paid_only = p.paid_only, + ac_rate = p.stat.total_acs * 100 / math.max(p.stat.total_submitted, 1), + difficulty = utils.lvl_to_name(p.difficulty.level), + starred = p.is_favor, + topic_tags = {}, + } + end, problems) end ---@param problems lc.cache.Question[] @@ -226,7 +241,9 @@ function utils.translate_titles(problems, titles) return vim.tbl_map(function(p) local title = map[tostring(p.id)] - if title then p.title_cn = title end + if title then + p.title_cn = title + end return p end, problems) end diff --git a/lua/leetcode/cache/cookie.lua b/lua/leetcode/cache/cookie.lua index e6b2d667..4962dfaa 100644 --- a/lua/leetcode/cache/cookie.lua +++ b/lua/leetcode/cache/cookie.lua @@ -3,7 +3,7 @@ local log = require("leetcode.logger") local config = require("leetcode.config") ---@type Path -local file = config.home:joinpath((".cookie%s"):format(config.is_cn and "_cn" or "")) +local file = config.storage.cache:joinpath(("cookie%s"):format(config.is_cn and "_cn" or "")) local hist = {} @@ -20,33 +20,54 @@ local Cookie = {} ---@return string|nil function Cookie.set(str) local _, cerr = Cookie.parse(str) - if cerr then return cerr end + if cerr then + return cerr + end file:write(str, "w") local auth_api = require("leetcode.api.auth") local _, aerr = auth_api.user() - if aerr then return aerr.msg end + if aerr then + return aerr.msg + end end ---@return boolean function Cookie.delete() - if not file:exists() then return false end + if not file:exists() then + return false + end return pcall(path.rm, file) end +---@return string|nil +function Cookie.read() + local contents = file:read() + + if not contents or type(contents) ~= "string" then + return + end + + return select(1, contents:gsub("^%s*(.-)%s*$", "%1")) +end + ---@return lc.cache.Cookie | nil function Cookie.get() - if not file:exists() then return end + if not file:exists() then + return + end local fstats = file:_stat() local ftime = fstats.mtime.sec local hcookie = hist[ftime] - if hcookie then return hcookie end + if hcookie then + return hcookie + end - local contents = file:read() - if not contents or type(contents) ~= "string" then + local contents = Cookie.read() + if not contents then require("leetcode.command").delete_cookie() return end @@ -66,10 +87,14 @@ end ---@return lc.cache.Cookie|nil, string|nil function Cookie.parse(str) local csrf = str:match("csrftoken=([^;]+)") - if not csrf or csrf == "" then return nil, "Bad csrf token format" end + if not csrf or csrf == "" then + return nil, "Bad csrf token format" + end local ls = str:match("LEETCODE_SESSION=([^;]+)") - if not ls or ls == "" then return nil, "Bad leetcode session token format" end + if not ls or ls == "" then + return nil, "Bad leetcode session token format" + end return { csrftoken = csrf, leetcode_session = ls, str = str } end diff --git a/lua/leetcode/cache/init.lua b/lua/leetcode/cache/init.lua index 6cbda0fd..b7052a10 100644 --- a/lua/leetcode/cache/init.lua +++ b/lua/leetcode/cache/init.lua @@ -3,6 +3,8 @@ local Problemlist = require("leetcode.cache.problemlist") ---@class lc.Cache local cache = {} -function cache.update() Problemlist.update() end +function cache.update() + Problemlist.update() +end return cache diff --git a/lua/leetcode/cache/problemlist.lua b/lua/leetcode/cache/problemlist.lua index a01ad384..13cbf0d1 100644 --- a/lua/leetcode/cache/problemlist.lua +++ b/lua/leetcode/cache/problemlist.lua @@ -6,7 +6,7 @@ local config = require("leetcode.config") local interval = config.user.cache.update_interval ---@type Path -local file = config.home:joinpath((".problemlist%s"):format(config.is_cn and "_cn" or "")) +local file = config.storage.cache:joinpath(("problemlist%s"):format(config.is_cn and "_cn" or "")) ---@type { at: integer, payload: lc.cache.payload } local hist = nil @@ -28,17 +28,25 @@ local hist = nil local Problemlist = {} ---@return lc.cache.Question[] -function Problemlist.get() return Problemlist.read().data end +function Problemlist.get() + return Problemlist.read().data +end ---@return lc.cache.payload function Problemlist.read() - if not file:exists() then return Problemlist.populate() end + if not file:exists() then + return Problemlist.populate() + end local time = os.time() - if hist and (time - hist.at) <= math.min(60, interval) then return hist.payload end + if hist and (time - hist.at) <= math.min(60, interval) then + return hist.payload + end local contents = file:read() - if not contents or type(contents) ~= "string" then return Problemlist.populate() end + if not contents or type(contents) ~= "string" then + return Problemlist.populate() + end local cached = Problemlist.parse(contents) @@ -47,7 +55,9 @@ function Problemlist.read() end hist = { at = time, payload = cached } - if (time - cached.updated_at) > interval then Problemlist.update() end + if (time - cached.updated_at) > interval then + Problemlist.update() + end return cached end @@ -67,7 +77,9 @@ end function Problemlist.update() problems_api.all(function(res, err) - if not err then Problemlist.write({ data = res }) end + if not err then + Problemlist.write({ data = res }) + end end, true) end @@ -75,7 +87,9 @@ end function Problemlist.get_by_title_slug(title_slug) local problems = Problemlist.get() - local problem = vim.tbl_filter(function(e) return e.title_slug == title_slug end, problems)[1] + local problem = vim.tbl_filter(function(e) + return e.title_slug == title_slug + end, problems)[1] assert(problem, ("Problem `%s` not found. Try updating cache?"):format(title_slug)) return problem @@ -89,7 +103,9 @@ function Problemlist.write(payload) username = config.auth.name, }, payload) - if not payload.data then payload.data = Problemlist.get() end + if not payload.data then + payload.data = Problemlist.get() + end file:write(vim.json.encode(payload), "w") hist = { at = os.time(), payload = payload } @@ -100,7 +116,9 @@ end ---@param str string --- ---@return lc.cache.payload -function Problemlist.parse(str) return vim.json.decode(str) end +function Problemlist.parse(str) + return vim.json.decode(str) +end ---@param title_slug string ---@param status "ac" | "notac" @@ -108,7 +126,9 @@ Problemlist.change_status = vim.schedule_wrap(function(title_slug, status) local cached = Problemlist.read() cached.data = vim.tbl_map(function(p) - if p.title_slug == title_slug then p.status = status end + if p.title_slug == title_slug then + p.status = status + end return p end, cached.data) @@ -116,7 +136,9 @@ Problemlist.change_status = vim.schedule_wrap(function(title_slug, status) end) function Problemlist.delete() - if not file:exists() then return false end + if not file:exists() then + return false + end return pcall(path.rm, file) end diff --git a/lua/leetcode/cache/solutions.lua b/lua/leetcode/cache/solutions.lua deleted file mode 100644 index e69de29b..00000000 diff --git a/lua/leetcode/command/arguments.lua b/lua/leetcode/command/arguments.lua index e147e52e..f065be70 100644 --- a/lua/leetcode/command/arguments.lua +++ b/lua/leetcode/command/arguments.lua @@ -1,3 +1,5 @@ +local config = require("leetcode.config") + local arguments = {} local topics = { @@ -85,4 +87,12 @@ arguments.random = { tags = topics, } +arguments.session_change = { + name = config.sessions.names, +} + +arguments.session_create = { + name = {}, +} + return arguments diff --git a/lua/leetcode/command/init.lua b/lua/leetcode/command/init.lua index 707bdec5..a04d1725 100644 --- a/lua/leetcode/command/init.lua +++ b/lua/leetcode/command/init.lua @@ -2,7 +2,7 @@ local log = require("leetcode.logger") local arguments = require("leetcode.command.arguments") local config = require("leetcode.config") local event = require("nui.utils.autocmd").event -local keys = config.user.keys +local api = vim.api local t = require("leetcode.translator") @@ -26,7 +26,8 @@ function cmd.problems(options) require("leetcode.utils").auth_guard() local p = require("leetcode.cache.problemlist").get() - require("leetcode.pickers.question").pick(p, options) + local picker = require("leetcode.picker") + picker.question(p, options) end ---@param cb? function @@ -60,26 +61,32 @@ function cmd.cookie_prompt(cb) if not err then log.info("Sign-in successful") - cmd.menu_layout("menu") - pcall(cb, true) + cmd.start_user_session() else log.error("Sign-in failed: " .. err) - pcall(cb, false) end + + pcall(cb, not err and true or false) end, }) input:mount() - input:map("n", keys.toggle, function() input:unmount() end) - input:on(event.BufLeave, function() input:unmount() end) + local keys = config.user.keys + input:map("n", keys.toggle, function() + input:unmount() + end) + input:on(event.BufLeave, function() + input:unmount() + end) end function cmd.sign_out() + cmd.menu() + log.warn("You're now signed out") cmd.delete_cookie() - cmd.menu_layout("signin") - cmd.q_close_all() + cmd.set_menu_page("signin") end ---Sign out @@ -89,34 +96,36 @@ function cmd.delete_cookie() cookie.delete() end -cmd.q_close_all = vim.schedule_wrap(function() +cmd.q_close_all = function() local utils = require("leetcode.utils") local qs = utils.question_tabs() for _, tabp in ipairs(qs) do tabp.question:unmount() end -end) +end + +function cmd.exit() + local leetcode = require("leetcode") + leetcode.stop() +end cmd.expire = vim.schedule_wrap(function() - local tabp = vim.api.nvim_get_current_tabpage() + local tabp = api.nvim_get_current_tabpage() cmd.menu() cmd.cookie_prompt(function(success) if success then - if vim.api.nvim_tabpage_is_valid(tabp) then vim.api.nvim_set_current_tabpage(tabp) end + if api.nvim_tabpage_is_valid(tabp) then + api.nvim_set_current_tabpage(tabp) + end log.info("Successful re-login") else - cmd.delete_cookie() - cmd.menu_layout("signin") - cmd.q_close_all() + cmd.sign_out() end end) end) ----Merge configurations into default configurations and set it as user configurations. ---- ---@param theme lc-db.Theme function cmd.qot() require("leetcode.utils").auth_guard() @@ -124,7 +133,9 @@ function cmd.qot() local Question = require("leetcode-ui.question") problems.question_of_today(function(qot, err) - if err then return log.err(err) end + if err then + return log.err(err) + end local problemlist = require("leetcode.cache.problemlist") Question(problemlist.get_by_title_slug(qot.title_slug)):mount() end) @@ -136,7 +147,9 @@ function cmd.random_question(opts) local problems = require("leetcode.cache.problemlist") local question = require("leetcode.api.question") - if opts and opts.difficulty then opts.difficulty = opts.difficulty[1]:upper() end + if opts and opts.difficulty then + opts.difficulty = opts.difficulty[1]:upper() + end if opts and opts.status then opts.status = ({ ac = "AC", @@ -146,55 +159,110 @@ function cmd.random_question(opts) end local q, err = question.random(opts) - if err then return log.err(err) end + if err then + return log.err(err) + end local item = problems.get_by_title_slug(q.title_slug) or {} local Question = require("leetcode-ui.question") Question(item):mount() end +function cmd.start_with_cmd() + local leetcode = require("leetcode") + if leetcode.start(false) then + cmd.menu() + end +end + function cmd.menu() - local ok, tabp = pcall(vim.api.nvim_win_get_tabpage, _Lc_Menu.winid) + local winid, bufnr = _Lc_state.menu.winid, _Lc_state.menu.bufnr + local ok, tabp = pcall(api.nvim_win_get_tabpage, winid) + local ui = require("leetcode-ui.utils") + if ok then - vim.api.nvim_set_current_tabpage(tabp) + api.nvim_set_current_tabpage(tabp) + ui.win_set_buf(winid, bufnr) else - log.error(tabp) + _Lc_state.menu:remount() + end +end + +function cmd.yank() + local utils = require("leetcode.utils") + local q = utils.curr_question() + if not q then + return + end + + if + (q.bufnr and api.nvim_buf_is_valid(q.bufnr)) + and (q.winid and api.nvim_win_is_valid(q.winid)) + then + api.nvim_set_current_win(q.winid) + utils.with_version("0.10.0", nil, function() + api.nvim_set_current_buf(q.bufnr) + end) + + local start_i, end_i, lines = q:range() + vim.cmd(("%d,%dyank"):format(start_i or 1, end_i or #lines)) end end ---@param page lc-menu.page -function cmd.menu_layout(page) _Lc_Menu:set_page(page) end +function cmd.set_menu_page(page) + _Lc_state.menu:set_page(page) +end + +function cmd.start_user_session() -- + cmd.set_menu_page("menu") + config.stats.update() +end -function cmd.question_tabs() require("leetcode.pickers.question-tabs").pick() end +function cmd.question_tabs() + local picker = require("leetcode.picker") + picker.tabs() +end function cmd.change_lang() local utils = require("leetcode.utils") local q = utils.curr_question() - if q then require("leetcode.pickers.language").pick(q) end + if q then + local picker = require("leetcode.picker") + picker.language(q) + end end function cmd.desc_toggle() local utils = require("leetcode.utils") local q = utils.curr_question() - if q then q.description:toggle() end + if q then + q.description:toggle() + end end function cmd.desc_toggle_stats() local utils = require("leetcode.utils") local q = utils.curr_question() - if q then q.description:toggle_stats() end + if q then + q.description:toggle_stats() + end end function cmd.console() local utils = require("leetcode.utils") local q = utils.curr_question() - if q then q.console:toggle() end + if q then + q.console:toggle() + end end function cmd.info() local utils = require("leetcode.utils") local q = utils.curr_question() - if q then q.info:toggle() end + if q then + q.info:toggle() + end end function cmd.hints() @@ -206,18 +274,24 @@ function cmd.q_run() local utils = require("leetcode.utils") utils.auth_guard() local q = utils.curr_question() - if q then q.console:run() end + if q then + q.console:run() + end end function cmd.q_submit() local utils = require("leetcode.utils") utils.auth_guard() local q = utils.curr_question() - if q then q.console:run(true) end + if q then + q.console:run(true) + end end function cmd.ui_skills() - if config.is_cn then return end + if config.is_cn then + return + end local skills = require("leetcode-ui.popup.skills") skills:show() end @@ -227,6 +301,195 @@ function cmd.ui_languages() languages:show() end +function cmd.open() + local utils = require("leetcode.utils") + utils.auth_guard() + local q = utils.curr_question() + + if q then + if vim.ui.open then + vim.ui.open(q.cache.link) + else + local command + local os_name = vim.loop.os_uname().sysname + + if os_name == "Linux" then + command = string.format("xdg-open '%s'", q.cache.link) + elseif os_name == "Darwin" then + command = string.format("open '%s'", q.cache.link) + else + -- Fallback to Windows if uname is not available or does not match Linux/Darwin. + command = string.format("start \"\" \"%s\"", q.cache.link) + end + + vim.fn.jobstart(command, { detach = true }) + end + end +end + +function cmd.reset() + local utils = require("leetcode.utils") + utils.auth_guard() + local q = utils.curr_question() + if not q then + return + end + + q:set_lines() +end + +function cmd.last_submit() + local utils = require("leetcode.utils") + utils.auth_guard() + local q = utils.curr_question() + if not q then + return + end + + local question_api = require("leetcode.api.question") + question_api.latest_submission(q.q.id, q.lang, function(res, err) -- + if err then + if err.status == 404 then + log.error("You haven't submitted any code!") + else + log.err(err) + end + + return + end + + if type(res) == "table" and res.code then + q:set_lines(res.code) + else + log.error("Something went wrong") + end + end) +end + +function cmd.restore() + local utils = require("leetcode.utils") + local ui = require("leetcode-ui.utils") + local q = utils.curr_question() + if not q then + return + end + + if + (q.winid and api.nvim_win_is_valid(q.winid)) + and (q.bufnr and api.nvim_buf_is_valid(q.bufnr)) + then + ui.win_set_buf(q.winid, q.bufnr) + end + + q.description:show() + local winid, bufnr = q.description.winid, q.description.bufnr + + if + (winid and api.nvim_win_is_valid(winid)) -- + and (bufnr and api.nvim_buf_is_valid(bufnr)) + then + ui.win_set_buf(q.winid, q.bufnr) + end +end + +function cmd.inject() + local utils = require("leetcode.utils") + local q = utils.curr_question() + if not q then + return + end + + if q.bufnr and api.nvim_buf_is_valid(q.bufnr) then + local start_i, end_i = q:range(true) + local not_found = {} + + if not start_i then + table.insert(not_found, "`@leet start`") + else + local before = q:inject(true) + if before then + api.nvim_buf_set_lines(q.bufnr, 0, start_i - 1, false, vim.split(before, "\n")) + _, end_i = q:range(true) + end + end + + if not end_i then + table.insert(not_found, "`@leet end`") + else + local after = q:inject(false) + if after then + api.nvim_buf_set_lines(q.bufnr, end_i, -1, false, vim.split(after, "\n")) + end + end + + if not vim.tbl_isempty(not_found) then + log.error(table.concat(not_found, " and ") .. " not found") + end + end +end + +function cmd.get_active_session() + local sessions = config.sessions.all + return vim.tbl_filter(function(s) + return s.is_active + end, sessions)[1] +end + +function cmd.get_session_by_name(name) + local sessions = config.sessions.all + + name = name:lower() + if name == config.sessions.default then + name = "" + end + return vim.tbl_filter(function(s) + return s.name:lower() == name + end, sessions)[1] +end + +function cmd.change_session(opts) + require("leetcode.utils").auth_guard() + + local name = opts.name[1] or config.sessions.default + + local session = cmd.get_session_by_name(name) + if not session then + return log.error("Session not found") + end + + local stats_api = require("leetcode.api.statistics") + stats_api.change_session(session.id, function(_, err) + if err then + return log.err(err) + end + log.info(("Session changed to `%s`"):format(name)) + config.stats.update() + end) +end + +function cmd.create_session(opts) + require("leetcode.utils").auth_guard() + + local name = opts.name[1] + if not name then + return log.error("Session name not provided") + end + + local stats_api = require("leetcode.api.statistics") + stats_api.create_session(name, function(_, err) + if err then + return log.err(err) + end + log.info(("session `%s` created"):format(name)) + end) +end + +function cmd.update_sessions() + require("leetcode.utils").auth_guard() + + config.stats.update_sessions() +end + function cmd.fix() require("leetcode.cache.cookie").delete() require("leetcode.cache.problemlist").delete() @@ -236,25 +499,36 @@ end ---@return string[], string[] function cmd.parse(args) local parts = vim.split(vim.trim(args), "%s+") - if args:sub(-1) == " " then parts[#parts + 1] = "" end + if args:sub(-1) == " " then + parts[#parts + 1] = "" + end local options = {} for _, part in ipairs(parts) do local opt = part:match("(.-)=.-") - if opt then table.insert(options, opt) end + if opt then + table.insert(options, opt) + end end return parts, options end ----@param t table -local function cmds_keys(t) +---@param tbl table +local function cmds_keys(tbl) return vim.tbl_filter(function(key) - if type(key) ~= "string" then return false end - if key:sub(1, 1) == "_" then return false end + if type(key) ~= "string" then + return false + end + if key:sub(1, 1) == "_" then + return false + end + if tbl[key]._private then + return false + end return true - end, vim.tbl_keys(t)) + end, vim.tbl_keys(tbl)) end ---@param _ string @@ -272,7 +546,9 @@ end --- ---@return string[] function cmd.rec_complete(args, options, cmds) - if not cmds or vim.tbl_isempty(args) then return {} end + if not cmds or vim.tbl_isempty(args) then + return {} + end if not cmds._args and cmds[args[1]] then return cmd.rec_complete(args, options, cmds[table.remove(args, 1)]) @@ -281,29 +557,26 @@ function cmd.rec_complete(args, options, cmds) local txt, keys = args[#args], cmds_keys(cmds) if cmds._args then local option_keys = cmds_keys(cmds._args) - option_keys = vim.tbl_filter( - function(key) return not vim.tbl_contains(options, key) end, - option_keys - ) - option_keys = vim.tbl_map(function(key) return ("%s="):format(key) end, option_keys) + option_keys = vim.tbl_filter(function(key) + return not vim.tbl_contains(options, key) + end, option_keys) + option_keys = vim.tbl_map(function(key) + return ("%s="):format(key) + end, option_keys) keys = vim.tbl_extend("force", keys, option_keys) local s = vim.split(txt, "=") if s[2] and cmds._args[s[1]] then local vals = vim.split(s[2], ",") - return vim.tbl_filter( - function(key) - return not vim.tbl_contains(vals, key) and key:find(vals[#vals], 1, true) == 1 - end, - cmds._args[s[1]] - ) + return vim.tbl_filter(function(key) + return not vim.tbl_contains(vals, key) and key:find(vals[#vals], 1, true) == 1 + end, cmds._args[s[1]]) end end - return vim.tbl_filter( - function(key) return not vim.tbl_contains(args, key) and key:find(txt, 1, true) == 1 end, - keys - ) + return vim.tbl_filter(function(key) + return not vim.tbl_contains(args, key) and key:find(txt, 1, true) == 1 + end, keys) end function cmd.exec(args) @@ -330,7 +603,7 @@ function cmd.exec(args) end function cmd.setup() - vim.api.nvim_create_user_command("Leet", cmd.exec, { + api.nvim_create_user_command("Leet", cmd.exec, { bar = true, bang = true, nargs = "?", @@ -343,6 +616,7 @@ cmd.commands = { cmd.menu, menu = { cmd.menu }, + exit = { cmd.exit }, console = { cmd.console }, info = { cmd.info }, hints = { cmd.hints }, @@ -352,36 +626,48 @@ cmd.commands = { test = { cmd.q_run }, submit = { cmd.q_submit }, daily = { cmd.qot }, - fix = { cmd.fix }, - + yank = { cmd.yank }, + open = { cmd.open }, + reset = { cmd.reset }, + last_submit = { cmd.last_submit }, + restore = { cmd.restore }, + inject = { cmd.inject }, + -- session = { + -- change = { + -- cmd.change_session, + -- _args = arguments.session_change, + -- }, + -- create = { + -- cmd.create_session, + -- _args = arguments.session_create, + -- }, + -- update = { cmd.update_sessions }, + -- }, list = { cmd.problems, - _args = arguments.list, }, - random = { cmd.random_question, - _args = arguments.random, }, - desc = { cmd.desc_toggle, stats = { cmd.desc_toggle_stats }, - toggle = { cmd.desc_toggle }, }, - cookie = { update = { cmd.cookie_prompt }, delete = { cmd.sign_out }, }, - cache = { update = { cmd.cache_update }, }, + fix = { + cmd.fix, + _private = true, + }, } return cmd diff --git a/lua/leetcode/config/hooks.lua b/lua/leetcode/config/hooks.lua new file mode 100644 index 00000000..05b4406e --- /dev/null +++ b/lua/leetcode/config/hooks.lua @@ -0,0 +1,15 @@ +---@class lc.Hooks +local hooks = {} + +hooks["question_enter"] = { + function(q) + -- https://github.com/kawre/leetcode.nvim/issues/14 + if q.lang == "rust" then + pcall(function() + require("rust-tools.standalone").start_standalone_client() + end) + end + end, +} + +return hooks diff --git a/lua/leetcode/config/icons.lua b/lua/leetcode/config/icons.lua index bf85c0db..ac912bd8 100644 --- a/lua/leetcode/config/icons.lua +++ b/lua/leetcode/config/icons.lua @@ -1,4 +1,30 @@ -return { - current = "", - dot = "", +local icons = { + bar = "│", + circle = "", + square = "󱓻", + lock = "", + unlock = "", + star = "", + status = { + ac = "", + notac = "󱎖", + todo = "", + }, + caret = { + right = "", + }, } + +icons.hl = { + status = { + ac = { icons.status.ac, "leetcode_easy" }, + notac = { icons.status.notac, "leetcode_medium" }, + -- todo = { icons.status.todo, "leetcode_alt" }, + }, + lock = { icons.lock, "leetcode_medium" }, + unlock = { icons.unlock, "leetcode_medium" }, +} + +icons.indent = ("\t%s "):format(icons.bar) + +return icons diff --git a/lua/leetcode/config/imports.lua b/lua/leetcode/config/imports.lua new file mode 100644 index 00000000..438e8278 --- /dev/null +++ b/lua/leetcode/config/imports.lua @@ -0,0 +1,73 @@ +---@class lc.Imports +local imports = {} + +imports["python3"] = { + "from string import *", + "from re import *", + "from datetime import *", + "from collections import *", + "from heapq import *", + "from bisect import *", + "from copy import *", + "from math import *", + "from random import *", + "from statistics import *", + "from itertools import *", + "from functools import *", + "from operator import *", + "from io import *", + "from sys import *", + "from json import *", + "from builtins import *", + "import string", + "import re", + "import datetime", + "import collections", + "import heapq", + "import bisect", + "import copy", + "import math", + "import random", + "import statistics", + "import itertools", + "import functools", + "import operator", + "import io", + "import sys", + "import json", + "from typing import *", +} + +imports["python"] = { + "from bisect import *", + "from collections import *", + "from copy import *", + "from datetime import *", + "from heapq import *", + "from math import *", + "from re import *", + "from string import *", + "from random import *", + "from itertools import *", + "from functools import *", + "from operator import *", + "import string", + "import re", + "import datetime", + "import collections", + "import heapq", + "import bisect", + "import copy", + "import math", + "import random", + "import itertools", + "import functools", + "import operator", +} + +imports["java"] = { + "import java.util.*;", + "import java.math.*;", +} + +return imports diff --git a/lua/leetcode/config/init.lua b/lua/leetcode/config/init.lua index 9ba43a72..0dfad2a6 100644 --- a/lua/leetcode/config/init.lua +++ b/lua/leetcode/config/init.lua @@ -1,12 +1,14 @@ local template = require("leetcode.config.template") +local P = require("plenary.path") ----@type lc.ui.Question[] -_Lc_questions = {} +_Lc_state = { + menu = nil, ---@type lc.ui.Menu + questions = {}, ---@type lc.ui.Question[] +} ----@type lc.ui.menu -_Lc_Menu = {} ---@diagnostic disable-line +local lazy_plugs = {} ----@class lc.Settings +---@class lc.Config local config = { default = template, user = template, @@ -16,13 +18,19 @@ local config = { is_cn = false, debug = false, lang = "cpp", - home = {}, ---@type Path version = "1.0.1", + storage = {}, ---@type table + theme = {}, ---@type lc.highlights + plugins = {}, translator = false, langs = require("leetcode.config.langs"), icons = require("leetcode.config.icons"), + sessions = require("leetcode.config.sessions"), + stats = require("leetcode.config.stats"), + imports = require("leetcode.config.imports"), + hooks = require("leetcode.config.hooks"), ---@type lc.UserStatus auth = {}, ---@diagnostic disable-line @@ -32,27 +40,93 @@ local config = { --- ---@param cfg lc.UserConfig Configurations to be merged. function config.apply(cfg) - config.user = vim.tbl_deep_extend("force", config.default, cfg) + config.user = vim.tbl_deep_extend("force", config.default, cfg or {}) + config.load_plugins() +end + +function config.setup() + config.validate() + + -- deprecate `directory` config + if config.user.directory then + local log = require("leetcode.logger") + log.warn("leetcode.nvim config: `directory` is deprecated. Use `storage.home` instead.") + config.user.storage.home = config.user.directory + end + + config.user.storage = vim.tbl_map(vim.fn.expand, config.user.storage) config.debug = config.user.debug or false ---@diagnostic disable-line config.lang = config.user.lang + + config.storage.home = P:new(config.user.storage.home) ---@diagnostic disable-line + config.storage.home:mkdir() + + config.storage.cache = P:new(config.user.storage.cache) ---@diagnostic disable-line + config.storage.cache:mkdir() + + for _, plug_load_fn in ipairs(lazy_plugs) do + plug_load_fn() + end +end + +function config.validate() + local utils = require("leetcode.utils") + + assert(vim.fn.has("nvim-0.9.0") == 1, "Neovim >= 0.9.0 required") + + if not utils.get_lang(config.lang) then + ---@type lc.lang[] + local lang_slugs = vim.tbl_map(function(lang) + return lang.slug + end, config.langs) + + local matches = {} + for _, slug in ipairs(lang_slugs) do + local percent = slug:match(config.lang) or config.lang:match(slug) + if percent then + table.insert(matches, slug) + end + end + + if not vim.tbl_isempty(matches) then + local log = require("leetcode.logger") + log.warn("Did you mean: { " .. table.concat(matches, ", ") .. " }?") + end + + error("Unsupported Language: " .. config.lang) + end end function config.load_plugins() + config.plugins = {} local plugins = {} if config.user.cn.enabled then - config.translator = config.user.cn.translator table.insert(plugins, "cn") end + for plugin, enabled in pairs(config.user.plugins) do + if enabled then + table.insert(plugins, plugin) + end + end + for _, plugin in ipairs(plugins) do local ok, plug = pcall(require, "leetcode-plugins." .. plugin) + if ok then - plug.load() + if not (plug.opts or {}).lazy then + plug.load() + else + table.insert(lazy_plugs, plug.load) + end + config.plugins[plugin] = true else - local log = require("leetcode.logger") - log.error(plug) + table.insert(lazy_plugs, function() + local log = require("leetcode.logger") + log.error(plug) + end) end end end diff --git a/lua/leetcode/config/langs.lua b/lua/leetcode/config/langs.lua index 052b13e9..c3db2f48 100644 --- a/lua/leetcode/config/langs.lua +++ b/lua/leetcode/config/langs.lua @@ -3,11 +3,10 @@ ---@field slug string ---@field icon string ---@field color string ----@field short string ---@field hl? string ---@field comment string ----@field sql boolean|nil ---@field ft string +---@field alt? string ---@type lc.language[] return { @@ -16,7 +15,6 @@ return { slug = "cpp", icon = "", color = "#00599C", - short = "cpp", ft = "cpp", comment = "//", }, @@ -25,7 +23,6 @@ return { slug = "java", icon = "", color = "#E76F00", - short = "java", ft = "java", comment = "//", }, @@ -34,16 +31,15 @@ return { slug = "python", icon = "", color = "#306998", - short = "pythn", ft = "py", comment = "#", + alt = "python2", }, { lang = "Python3", slug = "python3", icon = "", color = "#306998", - short = "pyth3", ft = "py", comment = "#", }, @@ -52,7 +48,6 @@ return { slug = "c", icon = "", color = "#555555", - short = "clang", ft = "c", comment = "//", }, @@ -61,7 +56,6 @@ return { slug = "csharp", icon = "󰌛", color = "#68217A", - short = "cshrp", ft = "cs", comment = "//", }, @@ -70,7 +64,6 @@ return { slug = "javascript", icon = "", color = "#F0DB4F", - short = "js", ft = "js", comment = "//", }, @@ -79,7 +72,6 @@ return { slug = "typescript", icon = "", color = "#3178C6", - short = "ts", ft = "ts", comment = "//", }, @@ -88,7 +80,6 @@ return { slug = "php", icon = "", color = "#777BB4", - short = "php", ft = "php", comment = "//", }, @@ -97,7 +88,6 @@ return { slug = "swift", icon = "", color = "#FFAC45", - short = "swift", ft = "swift", comment = "//", }, @@ -106,7 +96,6 @@ return { slug = "kotlin", icon = "", color = "#7F52FF", - short = "ktlin", ft = "kt", comment = "//", }, @@ -115,7 +104,6 @@ return { slug = "dart", icon = "", color = "#1057A7", - short = "dart", ft = "dart", comment = "//", }, @@ -124,7 +112,6 @@ return { slug = "golang", icon = "", color = "#00ADD8", - short = "golng", ft = "go", comment = "//", }, @@ -133,7 +120,6 @@ return { slug = "ruby", icon = "", color = "#CC342D", - short = "ruby", ft = "rb", comment = "#", }, @@ -142,7 +128,6 @@ return { slug = "scala", icon = "", color = "#DC322F", - short = "scala", ft = "scala", comment = "//", }, @@ -151,7 +136,6 @@ return { slug = "rust", icon = "", color = "#DEA584", - short = "rust", ft = "rs", comment = "//", }, @@ -160,7 +144,6 @@ return { slug = "racket", icon = "󰰟", color = "#22228F", - short = "rcket", ft = "rkt", comment = ";;", }, @@ -169,7 +152,6 @@ return { slug = "erlang", icon = "", color = "#A90533", - short = "erlng", ft = "erl", comment = "%", }, @@ -178,7 +160,6 @@ return { slug = "elixir", icon = "", color = "#6E4A7E", - short = "elixr", ft = "ex", comment = "#", }, @@ -187,44 +168,7 @@ return { slug = "bash", icon = "󱆃", color = "#000000", - short = "bash", ft = "sh", comment = "#", }, - -- { - -- lang = "HTML", - -- slug = "html", - -- icon = "", - -- color = "#E44D26", - -- short = "html", - -- ft = "html", - -- comment = "