irongit

A Neovim plugin enabling you to solve LeetCode problems.

feat: neoconf.nvim

kawreauthored
parent 7362176commit f94b9543afd5735ed814cb0c451ab4b44bf1d812Browse files

17 files changed, +355 -89

+1-0.gitignore
@@ -0,0 +1 @@
1+/.luarc.json
+3-13.luarc.json
@@ -1,20 +1,10 @@
11 {
2- "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
3- "diagnostics": {
4- "globals": [
5- "vim"
6- ]
7- },
8- "format": {
9- "enable": false
10- },
11- "runtime": {
12- "version": "LuaJIT"
13- },
142 "workspace.library": [
153 "/home/kawre/.local/share/nvim/lazy/neodev.nvim/types/stable",
164 "/usr/share/nvim/runtime/lua",
175 "/home/kawre/projects/leetcode.nvim/lua",
6+ "/home/kawre/.local/share/nvim/lazy/neoconf.nvim/types",
7+ "/home/kawre/.local/share/nvim/lazy/neoconf.nvim/types/lua",
188 "${3rd}/luv/library"
199 ]
20-}
10+}
+25-0.neoconf.json
@@ -0,0 +1,25 @@
1+{
2+ "neodev": {
3+ "library": {
4+ "enabled": true,
5+ "plugins": [
6+ "telescope",
7+ "notify",
8+ "plenary",
9+ "nui"
10+ ]
11+ }
12+ },
13+ "neoconf": {
14+ "plugins": {
15+ "lua_ls": {
16+ "enabled": true
17+ }
18+ }
19+ },
20+ "lspconfig": {
21+ "lua_ls": {
22+ "Lua.completion.callSnippet": "Replace"
23+ }
24+ }
25+}
+4-0lua/leetcode.lua
@@ -7,6 +7,10 @@ local M = {}
77 function M.setup(cfg)
88 config.apply(cfg or {})
99
10+ vim.cmd([[highlight LcProblemEasy guifg=##00b8a3]])
11+ vim.cmd([[highlight LcProblemMedium guifg=#ffb800]])
12+ vim.cmd([[highlight LcProblemHard guifg=#ef4743]])
13+
1014 vim.api.nvim_create_autocmd("VimEnter", {
1115 pattern = "*",
1216 nested = true,
+31-5lua/leetcode/api/command/init.lua
@@ -1,5 +1,5 @@
11 local cache = require("leetcode.cache")
2-local logger = require("leetcode.logger")
2+local log = require("leetcode.logger")
33 local config = require("leetcode.config")
44
55 local ui = require("leetcode.ui")
@@ -29,8 +29,8 @@ function M.lc_problems()
2929
3030 if not file:exists() then
3131 local editor_data = gql.question.editor_data(problem.title_slug)
32- local code
3332
33+ local code
3434 if editor_data.code_snippets ~= vim.NIL then
3535 for _, v in ipairs(editor_data.code_snippets) do
3636 if v.langSlug == config.user.lang or v.langSlug == config.user.sql then
@@ -39,20 +39,46 @@ function M.lc_problems()
3939 end
4040 end
4141 end
42- assert(code, "Failed to fetch code snippet")
4342
44- file:write(code, "w")
43+ if not code then
44+ log.error("failed to fetch code snippet")
45+ else
46+ file:write(code, "w")
47+ end
4548 end
4649
4750 vim.cmd("cd " .. dir)
4851 vim.cmd("e " .. file:absolute())
52+
53+ ui.create_leetcode_win(problem)
4954 end
5055 )
5156 end
5257
5358 function M.leetcode()
59+ M.authenticate()
5460 dashboard.setup()
55- vim.cmd("Alpha | bd #")
61+ vim.cmd("Alpha | bd#")
5662 end
5763
64+function M.cookie_prompt()
65+ local cookie = require("leetcode.cache.cookie")
66+
67+ ui.input(
68+ "Enter cookie",
69+ ---@param cookie_str string
70+ function(cookie_str)
71+ if not cookie_str then return end
72+
73+ cookie.new(cookie_str)
74+ require("leetcode.ui.dashboard").update()
75+ end
76+ )
77+end
78+
79+---Merge configurations into default configurations and set it as user configurations.
80+---
81+---@return lc.UserStatus | nil
82+function M.authenticate() gql.auth.user_status() end
83+
5884 return M
+14-1lua/leetcode/config/init.lua
@@ -16,9 +16,22 @@ Config.default = template
1616 ---@type lc.Config
1717 Config.user = Config.default
1818
19+---@type lc.UserStatus|nil
20+Config.auth = nil
21+
1922 ---Merge configurations into default configurations and set it as user configurations.
2023 ---
2124 ---@param cfg lc.Config Configurations to be merged.
22-function Config.apply(cfg) Config.user = vim.tbl_deep_extend("force", template, cfg) end
25+function Config.apply(cfg)
26+ Config.user = vim.tbl_deep_extend("force", template, cfg)
27+
28+ local ok, notify = pcall(require, "notify")
29+ if ok then vim.notify = notify end
30+end
31+
32+---Merge configurations into default configurations and set it as user configurations.
33+---
34+---@param status lc.UserStatus | nil
35+function Config.authenticate(status) Config.auth = status end
2336
2437 return Config
+4-0lua/leetcode/config/template.lua
@@ -16,7 +16,11 @@ local M = {
1616 ---| "oraclesql"
1717 sql = "mysql",
1818
19+ ---@type string
1920 directory = vim.fn.stdpath("data") .. "/leetcode",
21+
22+ ---@type boolean
23+ logging = true,
2024 }
2125
2226 return M
+13-5lua/leetcode/graphql/auth.lua
@@ -1,22 +1,27 @@
11 local gql = require("leetcode.graphql.utils")
2+local config = require("leetcode.config")
23
34 ---@class lc.AuthApi
45 local M = {}
56
6----@return table | nil
7+---@class lc.UserStatus
8+---@field username string
9+---@field is_signed_in boolean
10+---@field is_premium boolean
11+---@field user_id integer
12+
13+---@return lc.UserStatus | nil
714 function M.user_status()
815 local query = [[
916 query globalData {
1017 userStatus {
1118 userId
12- isSignedIn
19+ is_signed_in: isSignedIn
1320 isMockUser
14- isPremium
21+ is_premium: isPremium
1522 isVerified
1623 username
17- isTranslator
1824 activeSessionId
19- checkedInToday
2025 }
2126 }
2227 ]]
@@ -24,6 +29,9 @@ function M.user_status()
2429 local ok, res = pcall(gql.query, query)
2530 if not ok then return end
2631
32+ local status = res["userStatus"]
33+ config.authenticate(status)
34+
2735 return res["userStatus"]
2836 end
2937
+33-5lua/leetcode/graphql/question.lua
@@ -1,10 +1,15 @@
11 local gql = require("leetcode.graphql.utils")
2-local logger = require("leetcode.logger")
2+local log = require("leetcode.logger")
33
44 local M = {}
55
6+---@class lc.Question.Content
7+---@field content string
8+
69 ---@param title_slug string
7-function M.get_by_title_slug(title_slug)
10+---
11+---@return lc.Question.Content
12+function M.content(title_slug)
813 local variables = {
914 titleSlug = title_slug,
1015 }
@@ -22,9 +27,7 @@ function M.get_by_title_slug(title_slug)
2227 local ok, res = pcall(gql.query, query, variables)
2328 assert(ok)
2429
25- logger.inspect(res)
26-
27- return res["questionContent"]
30+ return res["question"]
2831 end
2932
3033 ---@param title_slug string
@@ -57,4 +60,29 @@ function M.editor_data(title_slug)
5760 return res["question"]
5861 end
5962
63+---@param title_slug string
64+function M.title(title_slug)
65+ local variables = {
66+ titleSlug = title_slug,
67+ }
68+
69+ local query = [[
70+ query questionTitle($titleSlug: String!) {
71+ question(titleSlug: $titleSlug) {
72+ questionId
73+ questionFrontendId
74+ title
75+ titleSlug
76+ isPaidOnly
77+ difficulty
78+ likes
79+ dislikes
80+ categoryTitle
81+ }
82+ }
83+ ]]
84+
85+ return gql.query(query, variables)["question"]
86+end
87+
6088 return M
+1-1lua/leetcode/graphql/utils.lua
@@ -22,7 +22,7 @@ function M.query(query, variables)
2222 })
2323
2424 local ok, data = pcall(vim.json.decode, response["body"])
25- assert(ok and data and data["data"])
25+ assert(ok and data and data["data"], "Failed to query data")
2626
2727 return data["data"]
2828 end
+14-7lua/leetcode/logger/init.lua
@@ -1,25 +1,32 @@
11 local config = require("leetcode.config")
2+local levels = vim.log.levels
23
34 ---@class lc.Logger
45 local Logger = {}
56
67 ---@param msg string
7----@param lvl? string
8-Logger.log = function(msg, lvl) require("notify")(config.name .. ": " .. msg, lvl or "info") end
8+---@param lvl? integer
9+Logger.log = function(msg, lvl)
10+ if not config.user.logging then return end
11+ vim.notify(msg, lvl or levels.INFO, { title = config.name })
12+end
913
1014 ---@param msg string
1115 Logger.info = function(msg) Logger.log(msg) end
1216
1317 ---@param msg string
14-Logger.warn = function(msg) Logger.log(msg, "warn") end
15-
16----@param msg string
17-Logger.debug = function(msg) Logger.log(msg, "debug") end
18+Logger.warn = function(msg) Logger.log(msg, levels.WARN) end
1819
1920 ---@param msg table
2021 Logger.inspect = function(msg) Logger.log(vim.inspect(msg)) end
2122
2223 ---@param msg string
23-Logger.error = function(msg) Logger.log(msg, "error") end
24+Logger.error = function(msg) Logger.log(msg, levels.ERROR) end
25+
26+---@param msg string
27+Logger.debug = function(msg)
28+ if not config.debug then return end
29+ Logger.log(msg, levels.DEBUG)
30+end
2431
2532 return Logger
+29-0lua/leetcode/theme/default.lua
@@ -0,0 +1,29 @@
1+---Get highlight group value by name.
2+---
3+---@param name string String for looking up highlight group.
4+---@return table
5+local function hl(name)
6+ local highlight = vim.api.nvim_get_hl_by_name(name, true)
7+ setmetatable(highlight, {
8+ __index = function(self, key)
9+ if key == "bg" then return self.background end
10+ if key == "fg" then return self.foreground end
11+
12+ return nil
13+ end,
14+ })
15+
16+ return highlight
17+end
18+
19+---@alias lc.Theme table<string, table>
20+---@type lc.Theme
21+local M = {
22+ problem = {
23+ easy = { fg = hl("LcProblemEasy").fg },
24+ medium = { fg = hl("LeetCodeProblemMedium").fg },
25+ hard = { fg = hl("LeetCodeProblemHard").fg },
26+ },
27+}
28+
29+return M
+6-0lua/leetcode/ui/components/init.lua
@@ -0,0 +1,6 @@
1+---@class lc.Ui.Components
2+local Components = {
3+ problem = require("leetcode.ui.components.problem"),
4+}
5+
6+return Components
+108-0lua/leetcode/ui/components/problem.lua
@@ -0,0 +1,108 @@
1+local log = require("leetcode.logger")
2+
3+local Text = require("nui.text")
4+local Line = require("nui.line")
5+local Layout = require("nui.layout")
6+
7+local utils = require("leetcode.utils")
8+
9+---@class lc.Ui.Components.Problem
10+local M = {}
11+
12+---@param title any
13+function M.title(title)
14+ local line = Line()
15+
16+ line:append(title.questionFrontendId .. ". " .. title.title)
17+
18+ return line
19+end
20+
21+---@param title_slug string
22+function M.link(title_slug)
23+ local line = Line()
24+
25+ line:append("https://leetcode.com/problems/" .. title_slug .. "/", "Comment")
26+ line:append("")
27+
28+ return line
29+end
30+
31+---@param title any
32+function M.stats(title)
33+ local line = Line()
34+
35+ line:append(
36+ title.difficulty,
37+ title.difficulty == "Easy" and "DiagnosticOk"
38+ or title.difficulty == "Medium" and "DiagnosticWarn"
39+ or "DiagnosticError"
40+ )
41+ line:append(" | ")
42+ line:append(title.likes .. "  ", "Comment")
43+ line:append(title.dislikes .. "  ", "Comment")
44+
45+ return line
46+end
47+
48+---@param content any
49+function M.content(content)
50+ local line = Line()
51+
52+ local s = vim.split(content.content, "\n\n<p>&nbsp;</p>\n", { trimempty = true })
53+
54+ local desc = M.description(s[1])
55+ local exam = M.examples(s[2])
56+ local cons = M.constrains(s[3])
57+ local foll = M.follow_up(s[4])
58+
59+ line:append({ desc, exam, cons, foll })
60+
61+ return line
62+end
63+
64+---@param p string
65+local parse_paragraph = function(p)
66+ -- for s in vim.gsplit(p, " ", { trimempty = true }) do
67+ -- log.info(s)
68+ -- end
69+
70+ local ts = vim.treesitter
71+ local parser = ts.get_string_parser(p, "html")
72+
73+ local tree = parser:parse()[1]
74+ local function get_text(node)
75+ local text = {}
76+ for _, child in ipairs(node) do
77+ if child:type() == "text" then
78+ table.insert(text, child:subtree_text())
79+ elseif child:named() then
80+ table.insert(text, get_text(child))
81+ end
82+ end
83+ return table.concat(text)
84+ end
85+ local text = get_text(tree:root())
86+
87+ log.info(text)
88+end
89+
90+---@param description string
91+function M.description(description)
92+ local lines = { "Description:" }
93+
94+ for v in vim.gsplit(description, "\n\n", { trimempty = true }) do
95+ parse_paragraph(utils.strip_html_tag(v, "p"))
96+ end
97+end
98+
99+---@param examples string
100+function M.examples(examples) end
101+
102+---@param constrains string
103+function M.constrains(constrains) end
104+
105+---@param follow_up string
106+function M.follow_up(follow_up) end
107+
108+return M
+10-8lua/leetcode/ui/dashboard.lua
@@ -1,5 +1,7 @@
11 local utils = require("leetcode.utils")
22 local gql = require("leetcode.graphql")
3+local config = require("leetcode.config")
4+local log = require("leetcode.logger")
35
46 local M = {}
57
@@ -48,8 +50,8 @@ local footer = {
4850 }
4951
5052 function M.setup()
51- local user = gql.auth.user_status()
52- local is_signed_in = (user ~= nil and user.isSignedIn)
53+ local auth = config.auth
54+ local is_signed_in = auth and auth.is_signed_in
5355
5456 local alpha = require("alpha")
5557 local dashboard = require("alpha.themes.dashboard")
@@ -72,7 +74,7 @@ function M.setup()
7274 dashboard.button(
7375 "c",
7476 "󰆘 " .. " Update cookie",
75- "<cmd>lua require('leetcode.utils').prompt_for_cookie()<cr>"
77+ "<cmd>lua require('leetcode.api').cmd.cookie_prompt()<cr>"
7678 ),
7779 dashboard.button(
7880 "s",
@@ -85,12 +87,12 @@ function M.setup()
8587 dashboard.button(
8688 "s",
8789 " " .. " Sign in (By Cookie)",
88- "<cmd>lua require('leetcode.utils').prompt_for_cookie()<cr>"
90+ "<cmd>lua require('leetcode.api').cmd.cookie_prompt()<cr>"
8991 ),
9092 dashboard.button("q", "󰩈 " .. " Exit LeetCode", "<cmd>qa!<CR>"),
9193 }
9294
93- footer.val = is_signed_in and "Signed in as: " .. user.username or ""
95+ footer.val = is_signed_in and "Signed in as: " .. auth.username or ""
9496
9597 local section = {
9698 header = header,
@@ -99,10 +101,10 @@ function M.setup()
99101 footer = footer,
100102 }
101103
102- local config = {
104+ local cfg = {
103105 layout = {
104106 -- header
105- { type = "padding", val = 4 },
107+ { type = "padding", val = 2 },
106108 section.header,
107109
108110 -- notifications
@@ -121,7 +123,7 @@ function M.setup()
121123 },
122124 }
123125
124- alpha.setup(config)
126+ alpha.setup(cfg)
125127 end
126128
127129 function M.update()
+41-27lua/leetcode/ui/init.lua
@@ -1,3 +1,7 @@
1+local logger = require("leetcode.logger")
2+local components = require("leetcode.ui.components")
3+local gql = require("leetcode.graphql")
4+
15 ---@class lc.UI
26 local Ui = {}
37
@@ -15,40 +19,50 @@ end
1519
1620 function Ui.input(opts, callback) vim.ui.input(opts, callback) end
1721
18-function Ui.oldfiles() Ui.create_win() end
22+---@param problem lc.Problem
23+function Ui.create_leetcode_win(problem)
24+ local start_win = vim.api.nvim_get_current_win()
1925
20-function Ui.create_win()
21- -- We save handle to window from which we open the navigation
22- start_win = vim.api.nvim_get_current_win()
26+ local Split = require("nui.split")
27+ local split = Split({
28+ relative = "win",
29+ position = "left",
30+ size = "30%",
31+ })
32+ split:mount()
2333
24- vim.api.nvim_command("topleft vnew") -- We open a new vertical window at the far right
25- win = vim.api.nvim_get_current_win() -- We save our navigation window handle...
26- buf = vim.api.nvim_get_current_buf() -- ...and it's buffer handle.
34+ local title = gql.question.title(problem.title_slug)
35+ local content = gql.question.content(problem.title_slug)
2736
28- -- We should name our buffer. All buffers in vim must have unique names.
29- -- The easiest solution will be adding buffer handle to it
30- -- because it is already unique and it's just a number.
31- vim.api.nvim_buf_set_name(buf, "Oldfiles #" .. buf)
37+ local win = vim.api.nvim_get_current_win() -- We save our navigation window handle...
38+ local buf = vim.api.nvim_get_current_buf() -- ...and it's buffer handle.
39+
40+ vim.api.nvim_buf_set_name(buf, "LeetCode")
3241
33- -- Now we set some options for our buffer.
34- -- nofile prevent mark buffer as modified so we never get warnings about not saved changes.
35- -- Also some plugins treat nofile buffers different.
36- -- For example coc.nvim don't triggers aoutcompletation for these.
37- vim.api.nvim_buf_set_option(buf, "buftype", "nofile")
38- -- We do not need swapfile for this buffer.
39- vim.api.nvim_buf_set_option(buf, "swapfile", false)
40- -- And we would rather prefer that this buffer will be destroyed when hide.
41- vim.api.nvim_buf_set_option(buf, "bufhidden", "wipe")
42- -- It's not necessary but it is good practice to set custom filetype.
43- -- This allows users to create their own autocommand or colorschemes on filetype.
44- -- and prevent collisions with other plugins.
4542 vim.api.nvim_buf_set_option(buf, "filetype", "leetcode.nvim")
43+ vim.api.nvim_buf_set_option(buf, "swapfile", false)
44+ vim.api.nvim_buf_set_option(buf, "buftype", "nofile")
45+ vim.api.nvim_buf_set_option(buf, "buflisted", false)
46+ -- vim.api.nvim_buf_set_option(buf, "modifiable", true)
47+
48+ vim.api.nvim_win_set_option(win, "wrap", true)
49+ vim.api.nvim_win_set_option(win, "number", false)
50+ vim.api.nvim_win_set_option(win, "signcolumn", "no")
51+
52+ vim.api.nvim_buf_set_keymap(buf, "n", "<esc>", "<cmd>hide<CR>", { noremap = true })
53+ vim.api.nvim_buf_set_keymap(buf, "n", "q", "<cmd>hide<CR>", { noremap = true })
54+
55+ local line = require("nui.line")()
56+ line:append("")
57+
58+ components.problem.link(problem.title_slug):render(buf, -1, 1)
59+ line:render(buf, -1, 2)
4660
47- -- For better UX we will turn off line wrap and turn on current line highlight.
48- vim.api.nvim_win_set_option(win, "wrap", false)
49- vim.api.nvim_win_set_option(win, "cursorline", true)
61+ components.problem.title(title):render(buf, -1, 3)
62+ components.problem.stats(title):render(buf, -1, 4)
63+ line:render(buf, -1, 5)
5064
51- set_mappings() -- At end we will set mappings for our navigation.
65+ components.problem.content(content):render(buf, -1, 6)
5266 end
5367
5468 return Ui
+18-17lua/leetcode/utils.lua
@@ -1,23 +1,6 @@
1-local ui = require("leetcode.ui")
2-
31 ---@class lc.Utils
42 local Utils = {}
53
6-function Utils.prompt_for_cookie()
7- local cookie = require("leetcode.cache.cookie")
8-
9- ui.input(
10- "Enter cookie",
11- ---@param cookie_str string
12- function(cookie_str)
13- if not cookie_str then return end
14-
15- cookie.new(cookie_str)
16- require("leetcode.ui.dashboard").update()
17- end
18- )
19-end
20-
214 function Utils.remove_cookie()
225 require("leetcode.cache.cookie").delete()
236 require("leetcode.ui.dashboard").update()
@@ -29,4 +12,22 @@ function Utils.alpha_move_cursor_top()
2912 require("alpha").move_cursor(curr_win)
3013 end
3114
15+---Extracts an HTML tag from a string.
16+---
17+---@param str string The input string containing HTML tags.
18+---@return string | nil The extracted HTML tag, or nil if no tag is found.
19+function Utils.get_html_tag(str) return str:match("^<(.-)>") end
20+
21+---@param str string
22+---@param tag string
23+---
24+---@return string
25+function Utils.strip_html_tag(str, tag)
26+ local regex = string.format("^<%s>(.*)</%s>$", tag, tag)
27+ assert(str:match(regex))
28+
29+ local offset = 3 + tag:len()
30+ return str:sub(offset, str:len() - offset)
31+end
32+
3233 return Utils