irongit

A Neovim plugin enabling you to solve LeetCode problems.

feat: ui components

kawreauthored
parent 2e58b27commit 9fd3cf41c39abe5c94c81908a180208901c892deBrowse files

28 files changed, +1009 -265

+2-1.neoconf.json
@@ -6,7 +6,8 @@
66 "nvim-notify",
77 "telescope.nvim",
88 "plenary.nvim",
9- "nui.nvim"
9+ "nui.nvim",
10+ "alpha-nvim"
1011 ]
1112 }
1213 }
+42-0lua/leetcode-ui/component/button.lua
@@ -0,0 +1,42 @@
1+local component = require("leetcode-ui.component")
2+local log = require("leetcode.logger")
3+local Line = require("nui.line")
4+
5+---@class lc.db.Button: lc.db.Component
6+local button = {}
7+button.__index = button
8+setmetatable(button, component)
9+
10+--- @param sc string
11+--- @param txt string
12+--- @param icon string
13+--- @param expandable boolean? optional
14+--- @param keybind string? optional
15+--- @param keybind_opts table? optional
16+function button:init(sc, txt, icon, expandable, keybind, keybind_opts)
17+ -- local sc_ = sc:gsub("%s", ""):gsub(leader, "<leader>")
18+
19+ local opts = {
20+ position = "center",
21+ }
22+ local width = 40
23+
24+ local line = Line()
25+ local padding = string.rep(" ", width - txt:len() - sc:len() - 3)
26+ -- line:append(string.format("%s ", icon))
27+ line:append("x " .. txt .. (expandable and "_" or " ") .. padding .. sc)
28+ -- txt = string.format("%s%s%s", icon, " " .. txt, expandable and "…" or "")
29+ -- line:append(txt .. padding .. sc)
30+
31+ local o = setmetatable({
32+ opts = opts,
33+ lines = { line },
34+ }, button)
35+
36+ self.lines = o.lines
37+ self.opts = o.opts
38+
39+ return o
40+end
41+
42+return button
+40-0lua/leetcode-ui/component/group.lua
@@ -0,0 +1,40 @@
1+local component = require("leetcode-ui.component")
2+local utils = require("leetcode-ui.utils")
3+local padding = require("leetcode-ui.component.padding")
4+---@class lc.db.Group: lc.db.Component
5+local group = {}
6+group.__index = group
7+setmetatable(group, component)
8+
9+---@param config lc.db.Group.config
10+---
11+---@return lc.db.Group
12+function group:init(config)
13+ local o = setmetatable(config, self)
14+ self.__index = self
15+
16+ -- self:append(config.components or {})
17+ self.components = config.components or {}
18+ self.opts = config.opts or {}
19+
20+ return o
21+end
22+
23+---@param cmp lc.db.Component | lc.db.Component[]
24+---
25+---@return lc.db.Group
26+function group:append(cmp)
27+ table.insert(self.components, cmp)
28+
29+ return self
30+end
31+
32+function group:draw(split)
33+ for i, cmp in pairs(self.components) do
34+ local opts = self.opts
35+ if opts.spacing and i ~= 1 then padding:init(opts.spacing):draw(split) end
36+ cmp:draw(split)
37+ end
38+end
39+
40+return group
+44-0lua/leetcode-ui/component/init.lua
@@ -0,0 +1,44 @@
1+local log = require("leetcode.logger")
2+local Line = require("nui.line")
3+local utils = require("leetcode-ui.utils")
4+
5+---@class lc.db.Component
6+---@field lines NuiLine[]
7+---@field opts lc.db.Component.config.opts
8+---@field type string
9+local component = {} ---@diagnostic disable-line
10+component.__index = component
11+
12+--------------------------------------------------------
13+--- Methods
14+--------------------------------------------------------
15+
16+---@param content NuiLine | string
17+---@param highlight? string
18+function component:append(content, highlight)
19+ if type(content) == "table" then
20+ table.insert(self.lines, content)
21+ else
22+ local line = Line():append(content, highlight)
23+ table.insert(self.lines, line)
24+ end
25+
26+ return self
27+end
28+
29+---@param split NuiSplit
30+function component:draw(split)
31+ local padding = utils.get_padding(self.lines, self.opts.position, split)
32+
33+ for _, line in pairs(self.lines) do
34+ local new_line = Line()
35+ new_line:append(padding)
36+ new_line:append(line)
37+
38+ new_line:render(split.bufnr, -1, line_idx, line_idx)
39+
40+ line_idx = line_idx + 1
41+ end
42+end
43+
44+return component
+35-0lua/leetcode-ui/component/padding.lua
@@ -0,0 +1,35 @@
1+local component = require("leetcode-ui.component")
2+local utils = require("leetcode-ui.utils")
3+local Line = require("nui.line")
4+
5+---@class lc.db.Padding: lc.db.Component
6+local padding = {}
7+padding.__index = padding
8+setmetatable(padding, component)
9+
10+--------------------------------------------------------
11+--- Methods
12+--------------------------------------------------------
13+
14+--------------------------------------------------------
15+--- Constructor
16+--------------------------------------------------------
17+
18+---@param int integer
19+---
20+---@return lc.db.Component
21+function padding:init(int)
22+ local t = {}
23+ for _ = 1, int, 1 do
24+ table.insert(t, Line():append(""))
25+ end
26+
27+ local o = setmetatable({ lines = t, opts = {} }, self)
28+ self.__index = self
29+ self.lines = t
30+ self.opts = {}
31+
32+ return o
33+end
34+
35+return padding
+33-0lua/leetcode-ui/component/text.lua
@@ -0,0 +1,33 @@
1+local component = require("leetcode-ui.component")
2+local utils = require("leetcode-ui.utils")
3+
4+---@class lc.db.Text: lc.db.Component
5+local text = {}
6+text.__index = text
7+setmetatable(text, component)
8+
9+--------------------------------------------------------
10+--- Methods
11+--------------------------------------------------------
12+
13+--------------------------------------------------------
14+--- Constructor
15+--------------------------------------------------------
16+
17+---@param config lc.db.Component.config
18+---
19+---@return lc.db.Text
20+function text:init(config)
21+ local parsed_lines = utils.parse_lines(config)
22+ config.lines = parsed_lines
23+
24+ local o = setmetatable(config, self)
25+ self.__index = self
26+
27+ self.lines = parsed_lines
28+ self.opts = config.opts or {}
29+
30+ return o
31+end
32+
33+return text
+73-0lua/leetcode-ui/init.lua
@@ -0,0 +1,73 @@
1+local component = require("leetcode-ui.component")
2+local log = require("leetcode.logger")
3+
4+local Split = require("nui.split")
5+local Text = require("nui.text")
6+local Line = require("nui.line")
7+
8+local dashboard = {}
9+
10+---@type NuiSplit
11+local split
12+
13+line_idx = 1
14+
15+---@type lc.db.Component[]
16+local state = {}
17+
18+function dashboard.redraw() end
19+
20+function dashboard.clear()
21+ vim.api.nvim_buf_set_lines(split.bufnr, 0, -1, false, {})
22+ line_idx = 1
23+end
24+
25+function dashboard.draw()
26+ dashboard.clear()
27+
28+ -- local menu = require("leetcode-ui.theme.menu")
29+ local problems = require("leetcode-ui.theme.problems")
30+ -- local menu = require("leetcode-ui.theme.menu")
31+ -- local menu = require("leetcode-ui.theme.menu")
32+ problems:draw(split)
33+end
34+
35+function dashboard.setup()
36+ split = Split({
37+ relative = "win",
38+ size = "100%",
39+ enter = true,
40+ focusable = true,
41+ buf_options = {
42+ -- modifiable = false,
43+ readonly = false,
44+ filetype = "leetcode.nvim",
45+ swapfile = false,
46+ buftype = "nofile",
47+ buflisted = false,
48+ },
49+ win_options = {
50+ -- winblend = 10,
51+ -- winhighlight = "Normal:Normal,FloatBorder:FloatBorder",
52+ foldcolumn = "1",
53+ wrap = true,
54+ number = false,
55+ signcolumn = "no",
56+ cursorline = false,
57+ },
58+ })
59+ split:mount()
60+
61+ dashboard.split = split
62+
63+ vim.cmd("bd#")
64+
65+ vim.api.nvim_create_autocmd("WinResized", {
66+ -- group = group_id,
67+ callback = function() dashboard.draw() end,
68+ })
69+
70+ dashboard.draw()
71+end
72+
73+return dashboard
+26-0lua/leetcode-ui/layout.lua
@@ -0,0 +1,26 @@
1+local log = require("leetcode.logger")
2+
3+---@class lc.db.Layout
4+---@field contents lc.db.Component[]
5+---@field opts lc.db.Layout.opts
6+local layout = {}
7+layout.__index = layout
8+
9+---@param split NuiSplit
10+function layout:draw(split)
11+ for _, component in ipairs(self.contents) do
12+ component:draw(split)
13+ end
14+end
15+
16+---@param config lc.db.Layout.config
17+function layout:init(config)
18+ local o = setmetatable(config, self)
19+
20+ self.contents = config.contents
21+ self.opts = config.opts
22+
23+ return o
24+end
25+
26+return layout
+48-0lua/leetcode-ui/theme/cache.lua
@@ -0,0 +1,48 @@
1+local config = require("leetcode.config")
2+
3+local template = require("leetcode.ui.dashboard.template")
4+local section = template.section
5+local auth = config.auth
6+
7+section.title.val = "Cache"
8+
9+section.buttons.val = {
10+ template.button(
11+ "c",
12+ "󱘴 " .. " Update",
13+ "<cmd>lua require('leetcode.api').cmd.cookie_prompt()<cr>"
14+ ),
15+ template.button(
16+ "s",
17+ " " .. " Sign out",
18+ "<cmd>lua require('leetcode.utils').remove_cookie()<cr>"
19+ ),
20+ template.button("q", "󰩈 " .. " Exit LeetCode", "<cmd>qa<CR>"),
21+}
22+
23+section.footer.val = "Signed in as: " .. auth.username
24+
25+return {
26+ layout = {
27+ -- header
28+ { type = "padding", val = 2 },
29+ section.header,
30+
31+ -- notifications
32+ -- { type = "padding", val = 2 },
33+ -- section.notifications,
34+ { type = "padding", val = 2 },
35+
36+ -- title
37+ section.title,
38+
39+ -- buttons
40+ section.buttons,
41+
42+ --footer
43+ section.footer,
44+ },
45+ opts = {
46+ margin = 5,
47+ },
48+}
+50-0lua/leetcode-ui/theme/cookie.lua
@@ -0,0 +1,50 @@
1+local config = require("leetcode.config")
2+local log = require("leetcode.logger")
3+
4+local template = require("leetcode.ui.dashboard.template")
5+local section = template.section
6+
7+section.title.val = "Cookie"
8+
9+section.buttons.val = {
10+ template.button(
11+ "s",
12+ "󱛪 " .. " Sign out / Delete cookie ",
13+ "<cmd>lua require('leetcode.utils').remove_cookie()<cr>"
14+ ),
15+ template.button(
16+ "s",
17+ "󱛬 " .. " Update ",
18+ "<cmd>lua require('leetcode.api').cmd.cookie_prompt()<cr>"
19+ ),
20+ template.button(
21+ "q",
22+ " " .. " Back",
23+ "<cmd>lua require('leetcode.api').cmd.dashboard('menu')<cr>"
24+ ),
25+}
26+
27+return {
28+ layout = {
29+ -- header
30+ { type = "padding", val = 2 },
31+ section.header,
32+
33+ -- notifications
34+ { type = "padding", val = 2 },
35+ section.notifications,
36+ { type = "padding", val = 2 },
37+
38+ -- title
39+ section.title,
40+
41+ -- buttons
42+ section.buttons,
43+
44+ --footer
45+ section.footer,
46+ },
47+ opts = {
48+ margin = 5,
49+ },
50+}
+39-0lua/leetcode-ui/theme/default.lua
@@ -0,0 +1,39 @@
1+local config = require("leetcode.config")
2+local log = require("leetcode.logger")
3+local component = require("leetcode-ui.component")
4+
5+local template = require("leetcode.ui.dashboard.template")
6+local section = template.section
7+
8+section.notifications.val = { " Sign in to use LeetCode.nvim" }
9+
10+section.buttons.val = {
11+ template.button(
12+ "s",
13+ " " .. " Sign in (By Cookie)",
14+ "<cmd>lua require('leetcode.api').cmd.cookie_prompt()<cr>"
15+ ),
16+ template.button("q", "󰩈 " .. " Exit LeetCode", "<cmd>qa!<CR>"),
17+}
18+
19+return {
20+ layout = {
21+ -- header
22+ { type = "padding", val = 2 },
23+ section.header,
24+
25+ -- notifications
26+ { type = "padding", val = 2 },
27+ section.notifications,
28+ { type = "padding", val = 2 },
29+
30+ -- buttons
31+ section.buttons,
32+
33+ --footer
34+ section.footer,
35+ },
36+ opts = {
37+ margin = 5,
38+ },
39+}
+30-0lua/leetcode-ui/theme/init.lua
@@ -0,0 +1,30 @@
1+local utils = require("leetcode.utils")
2+local gql = require("leetcode.graphql")
3+local config = require("leetcode.config")
4+local log = require("leetcode.logger")
5+
6+local M = {}
7+
8+---@alias lc.db.Theme
9+---| "cache"
10+---| "cookie"
11+---| "default"
12+---| "menu"
13+---| "problems"
14+---| "stats"
15+
16+---@param theme lc.db.Theme
17+function M.apply(theme)
18+ local alpha = require("alpha")
19+ utils.alpha_move_cursor_top()
20+
21+ local ok, cfg = pcall(require, "leetcode.ui.dashboard." .. theme)
22+ if not ok then log.error("Failed to load dashboard theme: " .. theme) end
23+ alpha.setup(cfg)
24+
25+ pcall(vim.cmd, [[AlphaRedraw]])
26+end
27+
28+-- function M.update() M.apply() end
29+
30+return M
+78-0lua/leetcode-ui/theme/menu.lua
@@ -0,0 +1,78 @@
1+local layout = require("leetcode-ui.layout")
2+local template = require("leetcode-ui.theme.template")
3+local padding = require("leetcode-ui.component.padding")
4+local button = require("leetcode-ui.component.button")
5+local group = require("leetcode-ui.component.group")
6+
7+local section = template.section
8+
9+local problems = button:init(
10+ "p",
11+ "Problems",
12+ "",
13+ true
14+ -- "<cmd>lua require('leetcode.api').cmd.dashboard('problems')<CR>"
15+)
16+local statistics = button:init(
17+ "s",
18+ "Statistics",
19+ "󰄪",
20+ true
21+ -- "<cmd>lua require('leetcode.api').cmd.dashboard('stats')<cr>"
22+)
23+local cookie = button:init(
24+ "c",
25+ "Cookie",
26+ "󰆘",
27+ true
28+ -- "<cmd>lua require('leetcode.api').cmd.dashboard('cookie')<cr>"
29+)
30+local cache = button:init(
31+ "n",
32+ "Cache",
33+ "",
34+ true
35+ -- "<cmd>lua require('leetcode.api').cmd.dashboard('cache')<cr>"
36+)
37+local exit = button:init("q", "Exit", "󰩈")
38+
39+local buttons = group:init({
40+ components = {
41+ problems,
42+ statistics,
43+ cookie,
44+ cache,
45+ exit,
46+ },
47+ opts = {
48+ spacing = 1,
49+ },
50+})
51+
52+section.title:append("Menu", "Comment")
53+
54+section.footer:append("Signed in as: " .. "kawre", "Keyword")
55+
56+local menu = layout:init({
57+ contents = {
58+ padding:init(2),
59+ section.header,
60+ padding:init(2),
61+
62+ -- section.notifications,
63+ -- padding:init(2),
64+
65+ section.title,
66+
67+ padding:init(1),
68+ buttons,
69+ padding:init(1),
70+
71+ section.footer,
72+ },
73+ opts = {
74+ margin = 5,
75+ },
76+})
77+
78+return menu
+42-0lua/leetcode-ui/theme/problems.lua
@@ -0,0 +1,42 @@
1+local log = require("leetcode.logger")
2+local group = require("leetcode-ui.component.group")
3+local button = require("leetcode-ui.component.button")
4+local layout = require("leetcode-ui.layout")
5+
6+local template = require("leetcode-ui.theme.template")
7+local section = template.section
8+
9+section.title:append("Problems")
10+
11+local buttons = group:init({
12+ components = {
13+ button:init("p", "List", ""),
14+ button:init("t", "Question of Today", "󰃭 "),
15+ button:init("t", "Random", " "),
16+ button:init("r", "Recent", " "),
17+ button:init("q", "Back", "… "),
18+ },
19+ opts = { spacing = 1 },
20+})
21+
22+section.footer:append("Signed in as: ")
23+
24+return layout:init({
25+ contents = {
26+ -- header
27+ section.header,
28+
29+ -- notifications
30+ section.notifications,
31+
32+ -- title
33+ section.title,
34+
35+ -- buttons
36+ buttons,
37+
38+ --footer
39+ section.footer,
40+ },
41+ opts = {},
42+})
+53-0lua/leetcode-ui/theme/stats.lua
@@ -0,0 +1,53 @@
1+local config = require("leetcode.config")
2+
3+local template = require("leetcode.ui.dashboard.template")
4+local section = template.section
5+local auth = config.auth
6+
7+section.title.val = "Statistics"
8+
9+section.buttons.val = {
10+ template.button(
11+ "p",
12+ " " .. " Problems ",
13+ "<cmd>lua require('leetcode.api').cmd.lc_problems()<CR>"
14+ ),
15+ template.button(
16+ "c",
17+ "󰆘 " .. " Cookie ",
18+ "<cmd>lua require('leetcode.api').cmd.cookie_prompt()<cr>"
19+ ),
20+ template.button(
21+ "c",
22+ " " .. " Cache ",
23+ "<cmd>lua require('leetcode.api').cmd.cookie_prompt()<cr>"
24+ ),
25+ template.button("q", "󰩈 " .. " Exit LeetCode", "<cmd>qa<CR>"),
26+}
27+
28+section.footer.val = "Signed in as: " .. auth.username
29+
30+return {
31+ layout = {
32+ -- header
33+ { type = "padding", val = 2 },
34+ section.header,
35+
36+ -- notifications
37+ -- { type = "padding", val = 2 },
38+ -- section.notifications,
39+ { type = "padding", val = 2 },
40+
41+ -- title
42+ section.title,
43+
44+ -- buttons
45+ section.buttons,
46+
47+ --footer
48+ section.footer,
49+ },
50+ opts = {
51+ margin = 5,
52+ },
53+}
+61-0lua/leetcode-ui/theme/template.lua
@@ -0,0 +1,61 @@
1+local text = require("leetcode-ui.component.text")
2+local group = require("leetcode-ui.component.group")
3+
4+local header = text:init({
5+ lines = {
6+ [[ /$$ /$$ /$$$$$$ /$$ ]],
7+ [[| $$ | $$ /$$__ $$ | $$ ]],
8+ [[| $$ /$$$$$$ /$$$$$$ /$$$$$$ | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ ]],
9+ [[| $$ /$$__ $$/$$__ $|_ $$_/ | $$ /$$__ $$/$$__ $$/$$__ $$]],
10+ [[| $$ | $$$$$$$| $$$$$$$$ | $$ | $$ | $$ \ $| $$ | $| $$$$$$$$]],
11+ [[| $$ | $$_____| $$_____/ | $$ /$| $$ $| $$ | $| $$ | $| $$_____/]],
12+ [[| $$$$$$$| $$$$$$| $$$$$$$ | $$$$| $$$$$$| $$$$$$| $$$$$$| $$$$$$$]],
13+ [[|________/\_______/\_______/ \___/ \______/ \______/ \_______/\_______/]],
14+ },
15+ opts = {
16+ position = "center",
17+ hl = "Keyword",
18+ padding = {
19+ bot = 2,
20+ },
21+ },
22+})
23+
24+local notifications = text:init({
25+ opts = {
26+ position = "center",
27+ hl = "DiagnosticInfo",
28+ },
29+})
30+
31+local title = text:init({
32+ opts = {
33+ position = "center",
34+ hl = "Comment",
35+ },
36+})
37+
38+local footer = text:init({
39+ opts = {
40+ position = "center",
41+ hl = "Keyword",
42+ },
43+})
44+
45+---@alias section table<string, lc.db.Component>
46+
47+---@type section
48+local section = {
49+ header = header,
50+ notifications = notifications,
51+ title = title,
52+ footer = footer,
53+}
54+
55+---@class lc.db.Template
56+---@field section section
57+---@field button function
58+return {
59+ section = section,
60+ button = require("alpha.themes.dashboard").button,
61+}
+34-0lua/leetcode-ui/types.lua
@@ -0,0 +1,34 @@
1+---@alias position
2+---| "left"
3+---| "center"
4+---| "right"
5+
6+---@class lc.db.Layout.padding
7+---@field top
8+---@field right
9+---@field bot
10+---@field left
11+
12+---@class lc.db.Component.config.opts
13+---@field position position
14+---@field hl string
15+
16+---@class lc.db.Group.config.opts
17+---@field spacing integer
18+--@field position position
19+
20+---@class lc.db.Component.config
21+---@field lines? NuiLine[] | string[]
22+---@field opts? lc.db.Component.config.opts
23+
24+---@class lc.db.Group.config
25+---@field components? lc.db.Component[]
26+---@field opts? lc.db.Group.config.opts
27+
28+---@class lc.db.Layout.opts
29+---@field margin? integer
30+---@field padding? lc.db.Layout.padding | integer
31+
32+---@class lc.db.Layout.config
33+---@field opts lc.db.Layout.opts
34+---@field contents lc.db.Component[]
+70-0lua/leetcode-ui/utils.lua
@@ -0,0 +1,70 @@
1+local Line = require("nui.line")
2+local log = require("leetcode.logger")
3+
4+local utils = {}
5+
6+---@param lines NuiLine[]
7+function utils.longest_line(lines)
8+ local width = 0
9+
10+ for _, line in pairs(lines) do
11+ local text = line:content()
12+ width = math.max(text:len(), width)
13+ end
14+
15+ return width
16+end
17+
18+---@param split NuiSplit
19+function utils.win_width(split) return vim.api.nvim_win_get_width(split.winid) end
20+
21+---@param config lc.db.Component.config
22+---
23+---@return NuiLine[]
24+function utils.parse_lines(config)
25+ local lines = config.lines
26+ local t = {}
27+
28+ if type(lines) == "table" then
29+ for _, line in pairs(lines) do
30+ if type(line) == "string" then
31+ local nui_line = Line()
32+ nui_line:append(line, config.opts and config.opts.hl or nil)
33+ table.insert(t, nui_line)
34+ else
35+ table.insert(t, line)
36+ end
37+ end
38+ end
39+
40+ return t
41+end
42+
43+---@param lines NuiLine[]
44+---@param position position
45+---@param split NuiSplit
46+---
47+---@return string
48+function utils.get_padding(lines, position, split)
49+ local padding = ""
50+
51+ if position ~= "left" then
52+ local longest_line = utils.longest_line(lines)
53+
54+ if position == "center" then
55+ local width = utils.win_width(split)
56+ local mid = (width - longest_line) / 2
57+ local spaces = string.rep(" ", mid)
58+ padding = spaces
59+ elseif position == "right" then
60+ local width = utils.win_width(split)
61+ local mid = width - longest_line - 1
62+ local spaces = string.rep(" ", mid)
63+ padding = spaces
64+ end
65+ end
66+
67+ return padding
68+end
69+
70+return utils
+2-1lua/leetcode.lua
@@ -1,5 +1,6 @@
11 local config = require("leetcode.config")
22 local api = require("leetcode.api")
3+local dashboard = require("leetcode-ui")
34
45 local M = {}
56
@@ -15,7 +16,7 @@ function M.setup(cfg)
1516 pattern = "*",
1617 nested = true,
1718 callback = function()
18- if config.user.invoke_name == vim.fn.expand("%") then api.cmd.leetcode() end
19+ if config.user.invoke_name == vim.fn.expand("%") then dashboard.setup() end
1920 end,
2021 })
2122 end
+17-5lua/leetcode/api/command/init.lua
@@ -3,7 +3,7 @@ local log = require("leetcode.logger")
33 local config = require("leetcode.config")
44
55 local ui = require("leetcode.ui")
6-local dashboard = require("leetcode.ui.dashboard")
6+-- local dashboard = require("leetcode.ui.dashboard")
77 local gql = require("leetcode.graphql")
88
99 local path = require("plenary.path")
@@ -11,7 +11,7 @@ local path = require("plenary.path")
1111 ---@class lc.Commands
1212 local M = {}
1313
14-function M.lc_problems()
14+function M.problems()
1515 local _, res = assert(pcall(cache.problems.read))
1616
1717 ui.pick_one(
@@ -57,8 +57,10 @@ end
5757
5858 function M.leetcode()
5959 M.authenticate()
60- dashboard.setup()
61- vim.cmd("Alpha | bd#")
60+ -- M.dashboard(config.auth.is_signed_in and "menu" or "default")
61+
62+ local ok, _ = pcall(vim.cmd, "Alpha | bd#")
63+ if not ok then log.error("Failed to launch Alpha") end
6264 end
6365
6466 function M.cookie_prompt()
@@ -71,7 +73,7 @@ function M.cookie_prompt()
7173 if not cookie_str then return end
7274
7375 cookie.new(cookie_str)
74- require("leetcode.ui.dashboard").update()
76+ M.dashboard("menu")
7577 end
7678 )
7779 end
@@ -81,4 +83,14 @@ end
8183 ---@return lc.UserStatus | nil
8284 function M.authenticate() gql.auth.user_status() end
8385
86+---Merge configurations into default configurations and set it as user configurations.
87+---
88+---@param theme lc.db.Theme
89+function M.dashboard(theme) dashboard.apply(theme) end
90+
91+---Merge configurations into default configurations and set it as user configurations.
92+---
93+---@param theme lc.db.Theme
94+function M.qot(theme) ui.open_qot() end
95+
8496 return M
+1-1lua/leetcode/config/init.lua
@@ -31,7 +31,7 @@ function Config.apply(cfg)
3131 vim.api.nvim_set_hl(0, "LeetCodePTag", { link = "Comment" })
3232 vim.api.nvim_set_hl(0, "LeetCodeEmTag", { italic = true })
3333 vim.api.nvim_set_hl(0, "LeetCodeStrongTag", { bold = true })
34- vim.api.nvim_set_hl(0, "LeetCodeCodeTag", { link = "MatchParen" })
34+ vim.api.nvim_set_hl(0, "LeetCodeCodeTag", { link = "DiagnosticHint" })
3535 vim.api.nvim_set_hl(0, "LeetCodeSupTag", { link = "MatchParen" })
3636 vim.api.nvim_set_hl(0, "LeetCodePreTag", { link = "@text" })
3737 end
+32-32lua/leetcode/graphql/problems.lua
@@ -29,22 +29,22 @@ function M.all()
2929 }
3030
3131 local query = [[
32- query problemsetQuestionList($limit: Int) {
33- problemsetQuestionList: questionList(
34- categorySlug: ""
35- limit: $limit
36- filters: {}
37- ) {
38- questions: data {
39- index: questionFrontendId
40- title
41- title_slug: titleSlug
42- ac_rate: acRate
43- difficulty
32+ query problemsetQuestionList($limit: Int) {
33+ problemsetQuestionList: questionList(
34+ categorySlug: ""
35+ limit: $limit
36+ filters: {}
37+ ) {
38+ questions: data {
39+ index: questionFrontendId
40+ title
41+ title_slug: titleSlug
42+ ac_rate: acRate
43+ difficulty
44+ }
45+ }
4446 }
45- }
46- }
47- ]]
47+ ]]
4848
4949 local ok, res = pcall(gql.query, query, variables)
5050 assert(ok)
@@ -54,24 +54,24 @@ end
5454
5555 function M.question_of_today()
5656 local query = [[
57- query questionOfToday {
58- activeDailyCodingChallengeQuestion {
59- userStatus
60- link
61- question {
62- acRate
63- difficulty
64- freqBar
65- index: questionFrontendId
66- isFavor
67- paidOnly: isPaidOnly
68- status
69- title
70- titleSlug
57+ query questionOfToday {
58+ activeDailyCodingChallengeQuestion {
59+ userStatus
60+ link
61+ question {
62+ acRate
63+ difficulty
64+ freqBar
65+ index: questionFrontendId
66+ isFavor
67+ paidOnly: isPaidOnly
68+ status
69+ title
70+ titleSlug
71+ }
72+ }
7173 }
72- }
73- }
74- ]]
74+ ]]
7575
7676 local ok, res = pcall(gql.query, query)
7777 assert(ok)
+18-14lua/leetcode/logger/init.lua
@@ -2,31 +2,35 @@ local config = require("leetcode.config")
22 local levels = vim.log.levels
33
44 ---@class lc.Logger
5-local Logger = {}
5+local logger = {}
66
7----@param msg string
7+---@alias msg string | number
8+
9+---@param msg msg
810 ---@param lvl? integer
9-Logger.log = function(msg, lvl)
11+logger.log = function(msg, lvl)
1012 if not config.user.logging then return end
13+
14+ if type(msg) == "number" then msg = tostring(msg) end
1115 vim.notify(msg, lvl or levels.INFO, { title = config.name })
1216 end
1317
14----@param msg string
15-Logger.info = function(msg) Logger.log(msg) end
18+---@param msg msg
19+logger.info = function(msg) logger.log(msg) end
1620
17----@param msg string
18-Logger.warn = function(msg) Logger.log(msg, levels.WARN) end
21+---@param msg msg
22+logger.warn = function(msg) logger.log(msg, levels.WARN) end
1923
2024 ---@param msg table
21-Logger.inspect = function(msg) Logger.log(vim.inspect(msg)) end
25+logger.inspect = function(msg) logger.log(vim.inspect(msg)) end
2226
23----@param msg string
24-Logger.error = function(msg) Logger.log(msg, levels.ERROR) end
27+---@param msg msg
28+logger.error = function(msg) logger.log(msg, levels.ERROR) end
2529
26----@param msg string
27-Logger.debug = function(msg)
30+---@param msg msg
31+logger.debug = function(msg)
2832 if not config.debug then return end
29- Logger.log(msg, levels.DEBUG)
33+ logger.log(msg, levels.DEBUG)
3034 end
3135
32-return Logger
36+return logger
+18-11lua/leetcode/parser/init.lua
@@ -1,11 +1,12 @@
11 local log = require("leetcode.logger")
22 local Text = require("nui.text")
3+local Line = require("nui.line")
34
45 ---@class lc.Parser
56 local M = {}
67
7----@type TreesitterModule, string, NuiText[]
8-local ts, html, nui_texts
8+---@type TreesitterModule, string, NuiLine
9+local ts, html, line
910
1011 local theme = {
1112 ["p"] = "LeetCodePTag",
@@ -20,6 +21,7 @@ local entities = {
2021 ["&lt;"] = "<",
2122 ["&gt;"] = ">",
2223 ["&nbsp;"] = " ",
24+ ["&quot;"] = "\"",
2325 }
2426
2527 ---@param node TSNode
@@ -76,26 +78,30 @@ end
7678 ---@param node TSNode
7779 ---@param tag_data lc.Parser.Tag
7880 ---
79----@return NuiText
81+---@return nil
8082 local function highlight_node(node, tag_data)
81- if not tag_data then return Text("") end
83+ local text = get_text(node)
84+ if not tag_data then
85+ text = text:gsub("&nbsp;", " ")
86+ return line:append(text)
87+ end
8288
8389 local data = tag_data
84- local text = get_text(node)
8590 local tag = data.tag
8691
8792 if node:type() == "entity" then text = entities[text] or text end
8893 if tag == "sup" then text = "^" .. text end
94+ if tag == "sub" then text = "_" .. text end
8995
9096 local nui_text = Text(text, theme[tag] or "Normal")
9197
9298 for _, attr in ipairs(data.attrs) do
93- if attr.name == "class" and attr.value == "example" then
94- nui_text = Text(text, "DiagnosticInfo")
99+ if attr.name == "class" then
100+ if attr.value == "example" then nui_text = Text(text, "DiagnosticInfo") end
95101 end
96102 end
97103
98- table.insert(nui_texts, nui_text)
104+ line:append(nui_text)
99105 end
100106
101107 ---@param node TSNode
@@ -118,16 +124,17 @@ end
118124
119125 ---@param to_parse string
120126 ---
121----@return NuiText[]
127+---@return NuiLine
122128 function M.parse(to_parse)
123- ts, html, nui_texts = vim.treesitter, to_parse, {}
129+ to_parse = to_parse:gsub(" ", "&nbsp;")
130+ ts, html, line = vim.treesitter, to_parse, Line()
124131
125132 local parser = ts.get_string_parser(html, "html")
126133 local tree = parser:parse()[1]
127134
128135 parse_nodes(tree:root())
129136
130- return nui_texts
137+ return line
131138 end
132139
133140 return M
+78-16lua/leetcode/ui/components/question.lua
@@ -1,15 +1,21 @@
11 local log = require("leetcode.logger")
2+local gql = require("leetcode.graphql")
23
3-local Text = require("nui.text")
44 local Line = require("nui.line")
5-local Layout = require("nui.layout")
5+local Split = require("nui.split")
66 local parser = require("leetcode.parser")
77
8-local utils = require("leetcode.utils")
9-
108 ---@class lc.Ui.Components.Problem
119 local M = {}
1210
11+---@type integer, NuiSplit
12+local curr_line, split
13+
14+---Increment current line
15+---
16+---@return nil
17+local function inc_line() curr_line = curr_line + 1 end
18+
1319 ---@param title any
1420 function M.title(title)
1521 local line = Line()
@@ -46,19 +52,16 @@ function M.stats(title)
4652 return line
4753 end
4854
49----@param question any
55+---@param content string
5056 ---
51----@return NuiLine[]
52-function M.content(question)
53- local s = vim.split(question.content, "\n\n<p>&nbsp;</p>\n")
54- local lines = {}
57+---@return nil
58+function M.content(content)
59+ local s = vim.gsplit(content, "\n", {})
5560
56- vim.list_extend(lines, M.description(s[1]))
57- vim.list_extend(lines, M.examples(s[2]))
58- -- local cons = parser.parse(s[3])
59- -- local foll = parser.parse(s[4])
60-
61- return lines
61+ for l in s do
62+ parser.parse(l):render(split.bufnr, -1, curr_line)
63+ inc_line()
64+ end
6265 end
6366
6467 ---@param html string
@@ -90,11 +93,70 @@ end
9093 ---@param html string
9194 ---
9295 ---@return NuiLine[]
93-function M.constrains(html) end
96+function M.constrains(html)
97+ local lines = {}
98+
99+ for s in vim.gsplit(html, "\n\n") do
100+ log.info(s)
101+ table.insert(lines, Line(parser.parse(s:gsub("s(?![^<>]*>)", "&nbsp;"))))
102+ end
103+
104+ return lines
105+end
94106
95107 ---@param html string
96108 ---
97109 ---@return NuiLine[]
98110 function M.follow_up(html) end
99111
112+---Render question split
113+---
114+---@param question lc.Problem
115+---
116+---@return nil
117+function M.open(question)
118+ curr_line = 1
119+
120+ split = Split({
121+ relative = "editor",
122+ position = "left",
123+ size = "40%",
124+ buf_options = {
125+ modifiable = true,
126+ readonly = false,
127+ filetype = "leetcode.nvim",
128+ swapfile = false,
129+ buftype = "nofile",
130+ buflisted = false,
131+ },
132+ win_options = {
133+ -- winblend = 10,
134+ -- winhighlight = "Normal:Normal,FloatBorder:FloatBorder",
135+ foldcolumn = "1",
136+ wrap = true,
137+ number = false,
138+ signcolumn = "no",
139+ },
140+ enter = true,
141+ focusable = true,
142+ })
143+ split:mount()
144+
145+ vim.api.nvim_buf_set_name(split.bufnr, "LeetCode")
146+
147+ local title = gql.question.title(question.title_slug)
148+ local content = gql.question.content(question.title_slug).content
149+
150+ M.link(question.title_slug):render(split.bufnr, -1, curr_line)
151+ inc_line()
152+
153+ M.title(title):render(split.bufnr, -1, curr_line)
154+ inc_line()
155+
156+ M.stats(title):render(split.bufnr, -1, curr_line)
157+ inc_line()
158+
159+ M.content(content)
160+end
161+
100162 return M
+0-135lua/leetcode/ui/dashboard.lua
@@ -1,135 +0,0 @@
1-local utils = require("leetcode.utils")
2-local gql = require("leetcode.graphql")
3-local config = require("leetcode.config")
4-local log = require("leetcode.logger")
5-
6-local M = {}
7-
8-local header = {
9- type = "text",
10- val = {
11- [[ /$$ /$$ /$$$$$$ /$$ ]],
12- [[| $$ | $$ /$$__ $$ | $$ ]],
13- [[| $$ /$$$$$$ /$$$$$$ /$$$$$$ | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ ]],
14- [[| $$ /$$__ $$/$$__ $|_ $$_/ | $$ /$$__ $$/$$__ $$/$$__ $$]],
15- [[| $$ | $$$$$$$| $$$$$$$$ | $$ | $$ | $$ \ $| $$ | $| $$$$$$$$]],
16- [[| $$ | $$_____| $$_____/ | $$ /$| $$ $| $$ | $| $$ | $| $$_____/]],
17- [[| $$$$$$$| $$$$$$| $$$$$$$ | $$$$| $$$$$$| $$$$$$| $$$$$$| $$$$$$$]],
18- [[|________/\_______/\_______/ \___/ \______/ \______/ \_______/\_______/]],
19- },
20- opts = {
21- position = "center",
22- hl = "Keyword",
23- },
24-}
25-
26-local notifications = {
27- type = "text",
28- val = {},
29- opts = {
30- position = "center",
31- hl = "DiagnosticInfo",
32- },
33-}
34-
35-local buttons = {
36- type = "group",
37- val = {},
38- opts = {
39- spacing = 1,
40- },
41-}
42-
43-local footer = {
44- type = "text",
45- val = {},
46- opts = {
47- position = "center",
48- hl = "Number",
49- },
50-}
51-
52-function M.setup()
53- local auth = config.auth
54- local is_signed_in = auth and auth.is_signed_in
55-
56- local alpha = require("alpha")
57- local dashboard = require("alpha.themes.dashboard")
58-
59- notifications.val = not is_signed_in and { " Sign in to use LeetCode.nvim" } or {}
60-
61- buttons.val = is_signed_in
62- and {
63- dashboard.button(
64- "p",
65- " " .. " All Problems",
66- "<cmd>lua require('leetcode.api').cmd.lc_problems()<CR>"
67- ),
68- dashboard.button(
69- "t",
70- "󰃭 " .. " Question of today",
71- "<cmd>lua require('leetcode.api').gql.problems.question_of_today()<CR>"
72- ),
73- dashboard.button("r", " " .. " Recent problems", ""),
74- dashboard.button(
75- "c",
76- "󰆘 " .. " Update cookie",
77- "<cmd>lua require('leetcode.api').cmd.cookie_prompt()<cr>"
78- ),
79- dashboard.button(
80- "s",
81- " " .. " Sign out",
82- "<cmd>lua require('leetcode.utils').remove_cookie()<cr>"
83- ),
84- dashboard.button("q", "󰩈 " .. " Exit LeetCode", "<cmd>qa<CR>"),
85- }
86- or {
87- dashboard.button(
88- "s",
89- " " .. " Sign in (By Cookie)",
90- "<cmd>lua require('leetcode.api').cmd.cookie_prompt()<cr>"
91- ),
92- dashboard.button("q", "󰩈 " .. " Exit LeetCode", "<cmd>qa!<CR>"),
93- }
94-
95- footer.val = is_signed_in and "Signed in as: " .. auth.username or ""
96-
97- local section = {
98- header = header,
99- notifications = notifications,
100- buttons = buttons,
101- footer = footer,
102- }
103-
104- local cfg = {
105- layout = {
106- -- header
107- { type = "padding", val = 2 },
108- section.header,
109-
110- -- notifications
111- { type = "padding", val = 2 },
112- section.notifications,
113- { type = "padding", val = 2 },
114-
115- -- buttons
116- section.buttons,
117-
118- --footer
119- section.footer,
120- },
121- opts = {
122- margin = 5,
123- },
124- }
125-
126- alpha.setup(cfg)
127-end
128-
129-function M.update()
130- utils.alpha_move_cursor_top()
131- M.setup()
132- pcall(vim.cmd, "AlphaRedraw")
133-end
134-
135-return M
+42-48lua/leetcode/ui/init.lua
@@ -1,7 +1,8 @@
1-local log = require("leetcode.logger")
21 local components = require("leetcode.ui.components")
2+
3+local Split = require("nui.split")
34 local gql = require("leetcode.graphql")
4-local Line = require("nui.line")
5+local Popup = require("nui.popup")
56
67 ---@class lc.UI
78 local Ui = {}
@@ -20,54 +21,47 @@ end
2021
2122 function Ui.input(opts, callback) vim.ui.input(opts, callback) end
2223
23----@param problem lc.Problem
24-function Ui.create_leetcode_win(problem)
25- local start_win = vim.api.nvim_get_current_win()
26-
27- local Split = require("nui.split")
28- local split = Split({
29- relative = "win",
30- position = "left",
31- size = "30%",
32- })
33- split:mount()
34-
35- local title = gql.question.title(problem.title_slug)
36- local content = gql.question.content(problem.title_slug)
37-
38- local win = vim.api.nvim_get_current_win() -- We save our navigation window handle...
39- local buf = vim.api.nvim_get_current_buf() -- ...and it's buffer handle.
40-
41- vim.api.nvim_buf_set_name(buf, "LeetCode")
24+---@param question lc.Problem
25+function Ui.create_leetcode_win(question) components.question.open(question) end
4226
43- vim.api.nvim_buf_set_option(buf, "filetype", "leetcode.nvim")
44- vim.api.nvim_buf_set_option(buf, "swapfile", false)
45- vim.api.nvim_buf_set_option(buf, "buftype", "nofile")
46- vim.api.nvim_buf_set_option(buf, "buflisted", false)
47- -- vim.api.nvim_buf_set_option(buf, "modifiable", true)
27+---@param question lc.Problem
28+function Ui.open_qot()
29+ local qot = gql.problems.question_of_today()
4830
49- vim.api.nvim_win_set_option(win, "wrap", true)
50- vim.api.nvim_win_set_option(win, "number", false)
51- vim.api.nvim_win_set_option(win, "signcolumn", "no")
52-
53- -- vim.api.nvim_buf_set_keymap(buf, "n", "<esc>", "<cmd>hide<CR>", { noremap = true })
54- -- vim.api.nvim_buf_set_keymap(buf, "n", "q", "<cmd>hide<CR>", { noremap = true })
55- local sep = Line()
56- sep:append("")
57-
58- components.question.link(problem.title_slug):render(buf, -1, 1)
59- sep:render(buf, -1, 2)
60-
61- components.question.title(title):render(buf, -1, 3)
62- components.question.stats(title):render(buf, -1, 4)
63- sep:render(buf, -1, 5)
64-
65- local line_index = 6
66- for _, line in ipairs(components.question.content(content)) do
67- line:render(buf, -1, line_index)
68- sep:render(buf, -1, line_index + 1)
69- line_index = line_index + 2
70- end
31+ local popup = Popup({
32+ position = "50%",
33+ size = {
34+ width = 80,
35+ height = 40,
36+ },
37+ enter = true,
38+ focusable = true,
39+ zindex = 50,
40+ relative = "editor",
41+ border = {
42+ padding = {
43+ top = 2,
44+ bottom = 2,
45+ left = 3,
46+ right = 3,
47+ },
48+ style = "rounded",
49+ text = {
50+ top = " I am top title ",
51+ top_align = "center",
52+ bottom = "I am bottom title",
53+ bottom_align = "left",
54+ },
55+ },
56+ buf_options = {
57+ modifiable = true,
58+ readonly = false,
59+ },
60+ win_options = {
61+ winblend = 10,
62+ winhighlight = "Normal:Normal,FloatBorder:FloatBorder",
63+ },
64+ })
7165 end
7266
7367 return Ui
+1-1lua/leetcode/utils.lua
@@ -3,7 +3,7 @@ local Utils = {}
33
44 function Utils.remove_cookie()
55 require("leetcode.cache.cookie").delete()
6- require("leetcode.ui.dashboard").update()
6+ -- require("leetcode.ui.dashboard").apply("default")
77 end
88
99 function Utils.alpha_move_cursor_top()