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 = "