A Neovim plugin enabling you to solve LeetCode problems.
init
21 files changed, +768 -0
+20-0.luarc.json
| @@ -0,0 +1,20 @@ | ||
| 1 | +{ | |
| 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 | + }, | |
| 14 | + "workspace.library": [ | |
| 15 | + "/home/kawre/.local/share/nvim/lazy/neodev.nvim/types/stable", | |
| 16 | + "/usr/share/nvim/runtime/lua", | |
| 17 | + "/home/kawre/projects/leetcode.nvim/lua", | |
| 18 | + "${3rd}/luv/library" | |
| 19 | + ] | |
| 20 | +} |
+7-0.stylua.toml
| @@ -0,0 +1,7 @@ | ||
| 1 | +column_width = 100 | |
| 2 | +line_endings = "Unix" | |
| 3 | +indent_type = "Spaces" | |
| 4 | +indent_width = 2 | |
| 5 | +quote_style = "ForceDouble" | |
| 6 | +call_parentheses = "Always" | |
| 7 | +collapse_simple_statement = "Always" |
+19-0lua/leetcode.lua
| @@ -0,0 +1,19 @@ | ||
| 1 | +local config = require("leetcode.config") | |
| 2 | +local api = require("leetcode.api") | |
| 3 | + | |
| 4 | +local M = {} | |
| 5 | + | |
| 6 | +---@param cfg? lc.Config | |
| 7 | +function M.setup(cfg) | |
| 8 | + config.apply(cfg or {}) | |
| 9 | + | |
| 10 | + vim.api.nvim_create_autocmd("VimEnter", { | |
| 11 | + pattern = "*", | |
| 12 | + nested = true, | |
| 13 | + callback = function() | |
| 14 | + if config.user.invoke_name == vim.fn.expand("%") then api.cmd.leetcode() end | |
| 15 | + end, | |
| 16 | + }) | |
| 17 | +end | |
| 18 | + | |
| 19 | +return M |
+58-0lua/leetcode/api/command/init.lua
| @@ -0,0 +1,58 @@ | ||
| 1 | +local cache = require("leetcode.cache") | |
| 2 | +local logger = require("leetcode.logger") | |
| 3 | +local config = require("leetcode.config") | |
| 4 | + | |
| 5 | +local ui = require("leetcode.ui") | |
| 6 | +local dashboard = require("leetcode.ui.dashboard") | |
| 7 | +local gql = require("leetcode.graphql") | |
| 8 | + | |
| 9 | +local path = require("plenary.path") | |
| 10 | + | |
| 11 | +---@class lc.Commands | |
| 12 | +local M = {} | |
| 13 | + | |
| 14 | +function M.lc_problems() | |
| 15 | + local _, res = assert(pcall(cache.problems.read)) | |
| 16 | + | |
| 17 | + ui.pick_one( | |
| 18 | + res, | |
| 19 | + "Select a Problem", | |
| 20 | + cache.problems.formatter, | |
| 21 | + | |
| 22 | + ---@param problem lc.Problem | |
| 23 | + function(problem) | |
| 24 | + if not problem then return end | |
| 25 | + | |
| 26 | + local dir = config.user.directory .. "/solutions/" | |
| 27 | + local fn = problem.index .. "." .. problem.title_slug .. ".java" | |
| 28 | + local file = path:new(dir .. fn) | |
| 29 | + | |
| 30 | + if not file:exists() then | |
| 31 | + local editor_data = gql.question.editor_data(problem.title_slug) | |
| 32 | + local code | |
| 33 | + | |
| 34 | + if editor_data.code_snippets ~= vim.NIL then | |
| 35 | + for _, v in ipairs(editor_data.code_snippets) do | |
| 36 | + if v.langSlug == config.user.lang or v.langSlug == config.user.sql then | |
| 37 | + code = v.code | |
| 38 | + break | |
| 39 | + end | |
| 40 | + end | |
| 41 | + end | |
| 42 | + assert(code, "Failed to fetch code snippet") | |
| 43 | + | |
| 44 | + file:write(code, "w") | |
| 45 | + end | |
| 46 | + | |
| 47 | + vim.cmd("cd " .. dir) | |
| 48 | + vim.cmd("e " .. file:absolute()) | |
| 49 | + end | |
| 50 | + ) | |
| 51 | +end | |
| 52 | + | |
| 53 | +function M.leetcode() | |
| 54 | + dashboard.setup() | |
| 55 | + vim.cmd("Alpha | bd #") | |
| 56 | +end | |
| 57 | + | |
| 58 | +return M |
+19-0lua/leetcode/api/headers.lua
| @@ -0,0 +1,19 @@ | ||
| 1 | +local M = {} | |
| 2 | + | |
| 3 | +M.get = function() | |
| 4 | + local cookie = require("leetcode.cache.cookie").read() | |
| 5 | + | |
| 6 | + return { | |
| 7 | + ["Cookie"] = cookie and string.format( | |
| 8 | + "LEETCODE_SESSION=%s;csrftoken=%s", | |
| 9 | + cookie.leetcode_session, | |
| 10 | + cookie.csrftoken | |
| 11 | + ) or "", | |
| 12 | + ["Content-Type"] = "application/json", | |
| 13 | + ["Accept"] = "application/json", | |
| 14 | + ["x-csrftoken"] = cookie and cookie.csrftoken or nil, | |
| 15 | + ["Referer"] = "https://leetcode.com", | |
| 16 | + } | |
| 17 | +end | |
| 18 | + | |
| 19 | +return M |
+6-0lua/leetcode/api/init.lua
| @@ -0,0 +1,6 @@ | ||
| 1 | +---@class lc.Api | |
| 2 | +local Api = { | |
| 3 | + cmd = require("leetcode.api.command"), | |
| 4 | +} | |
| 5 | + | |
| 6 | +return Api |
+52-0lua/leetcode/cache/cookie.lua
| @@ -0,0 +1,52 @@ | ||
| 1 | +local path = require("plenary.path") | |
| 2 | + | |
| 3 | +local file = path:new(vim.fn.stdpath("data") .. "/leetcode/.cookie") | |
| 4 | + | |
| 5 | +---@class lc.Cookie | |
| 6 | +---@field csrftoken string | |
| 7 | +---@field leetcode_session string | |
| 8 | + | |
| 9 | +local M = {} | |
| 10 | + | |
| 11 | +---@param cookie_str string | |
| 12 | +--- | |
| 13 | +---@return lc.Cookie | |
| 14 | +function M.new(cookie_str) | |
| 15 | + local _, t = assert(pcall(M.parse, cookie_str)) | |
| 16 | + | |
| 17 | + if not file:exists() then file:touch() end | |
| 18 | + file:write(cookie_str, "w") | |
| 19 | + | |
| 20 | + return t | |
| 21 | +end | |
| 22 | + | |
| 23 | +---@return lc.Cookie | nil | |
| 24 | +function M.read() | |
| 25 | + local r_ok, contents = pcall(path.read, file) | |
| 26 | + if not r_ok then return end | |
| 27 | + | |
| 28 | + local n_ok, cookie = pcall(M.new, contents) | |
| 29 | + if not n_ok then return end | |
| 30 | + | |
| 31 | + return cookie | |
| 32 | +end | |
| 33 | + | |
| 34 | +function M.delete() file:rm() end | |
| 35 | + | |
| 36 | +---@param cookie_str string | |
| 37 | +--- | |
| 38 | +function M.parse(cookie_str) | |
| 39 | + local cookie = {} | |
| 40 | + | |
| 41 | + local csrf_ok, csrf = pcall(string.match, cookie_str, "csrftoken=[^;]+") | |
| 42 | + assert(csrf_ok and csrf, "Bad csrf token format") | |
| 43 | + cookie.csrftoken = csrf:sub(11) | |
| 44 | + | |
| 45 | + local ls_ok, ls = pcall(string.match, cookie_str, "LEETCODE_SESSION=[^;]+") | |
| 46 | + assert(ls_ok and ls, "Bad leetcode session token format") | |
| 47 | + cookie.leetcode_session = ls:sub(18) | |
| 48 | + | |
| 49 | + return cookie | |
| 50 | +end | |
| 51 | + | |
| 52 | +return M |
+8-0lua/leetcode/cache/init.lua
| @@ -0,0 +1,8 @@ | ||
| 1 | +---@class lc.Cache | |
| 2 | +local Cache = { | |
| 3 | + cookie = require("leetcode.cache.cookie"), | |
| 4 | + problems = require("leetcode.cache.problems"), | |
| 5 | + -- solutions = require("leetcode.cache.solutions"), | |
| 6 | +} | |
| 7 | + | |
| 8 | +return Cache |
+77-0lua/leetcode/cache/problems.lua
| @@ -0,0 +1,77 @@ | ||
| 1 | +local path = require("plenary.path") | |
| 2 | +local async = require("plenary.async") | |
| 3 | + | |
| 4 | +local config = require("leetcode.config") | |
| 5 | +local gql = require("leetcode.graphql") | |
| 6 | + | |
| 7 | +local file = path:new(config.user.directory .. "/.problems") | |
| 8 | + | |
| 9 | +---@class lc.Problem | |
| 10 | +---@field index string | |
| 11 | +---@field title string | |
| 12 | +---@field title_slug string | |
| 13 | +---@field ac_rate number | |
| 14 | +---@field difficulty "Easy" | "Medium" | "Hard" | |
| 15 | + | |
| 16 | +local M = {} | |
| 17 | + | |
| 18 | +local function populate() | |
| 19 | + file:touch() | |
| 20 | + local problems = assert(gql.problems.all()) | |
| 21 | + file:write(vim.json.encode(problems), "w") | |
| 22 | +end | |
| 23 | + | |
| 24 | +---@return lc.Problem[] | nil | |
| 25 | +function M.read() | |
| 26 | + async.run(M.update) | |
| 27 | + | |
| 28 | + local contents = file:read() | |
| 29 | + | |
| 30 | + local ok, problems = pcall(M.parse, contents) | |
| 31 | + if not ok then return end | |
| 32 | + | |
| 33 | + return problems | |
| 34 | +end | |
| 35 | + | |
| 36 | +function M.update() | |
| 37 | + local stats = file:_stat() | |
| 38 | + | |
| 39 | + if vim.tbl_isempty(stats) then | |
| 40 | + assert(populate()) | |
| 41 | + return | |
| 42 | + end | |
| 43 | + | |
| 44 | + local mod_time = stats.mtime.sec | |
| 45 | + local curr_time = os.time() | |
| 46 | + | |
| 47 | + if (curr_time - mod_time) > 60 * 60 * 24 * 7 then | |
| 48 | + -- logger.info("Updating cache. This way take a while...") | |
| 49 | + assert(populate()) | |
| 50 | + end | |
| 51 | +end | |
| 52 | + | |
| 53 | +---@param problem lc.Problem | |
| 54 | +--- | |
| 55 | +---@return string | |
| 56 | +M.formatter = function(problem) | |
| 57 | + return string.format( | |
| 58 | + "%d. %s (%d%%, %s)", | |
| 59 | + problem.index, | |
| 60 | + problem.title, | |
| 61 | + problem.ac_rate, | |
| 62 | + problem.difficulty | |
| 63 | + ) | |
| 64 | +end | |
| 65 | + | |
| 66 | +---@param problems_str string | |
| 67 | +--- | |
| 68 | +---@return lc.Problem[] | |
| 69 | +function M.parse(problems_str) | |
| 70 | + ---@type boolean, lc.Problem[] | |
| 71 | + local ok, problems = pcall(vim.json.decode, problems_str) | |
| 72 | + assert(ok) | |
| 73 | + | |
| 74 | + return problems | |
| 75 | +end | |
| 76 | + | |
| 77 | +return M |
+0-0lua/leetcode/cache/solutions.lua
No content changes (mode or rename only).
+24-0lua/leetcode/config/init.lua
| @@ -0,0 +1,24 @@ | ||
| 1 | +local template = require("leetcode.config.template") | |
| 2 | + | |
| 3 | +---@class lc.Settings | |
| 4 | +local Config = { | |
| 5 | + name = "leetcode.nvim", | |
| 6 | + debug = true, | |
| 7 | +} | |
| 8 | + | |
| 9 | +---Default configurations. | |
| 10 | +--- | |
| 11 | +---@type lc.Config | |
| 12 | +Config.default = template | |
| 13 | + | |
| 14 | +---User configurations. | |
| 15 | +--- | |
| 16 | +---@type lc.Config | |
| 17 | +Config.user = Config.default | |
| 18 | + | |
| 19 | +---Merge configurations into default configurations and set it as user configurations. | |
| 20 | +--- | |
| 21 | +---@param cfg lc.Config Configurations to be merged. | |
| 22 | +function Config.apply(cfg) Config.user = vim.tbl_deep_extend("force", template, cfg) end | |
| 23 | + | |
| 24 | +return Config |
+22-0lua/leetcode/config/template.lua
| @@ -0,0 +1,22 @@ | ||
| 1 | +---@class lc.Config | |
| 2 | +local M = { | |
| 3 | + ---@type string | |
| 4 | + domain = "com", | |
| 5 | + | |
| 6 | + ---@type string | |
| 7 | + invoke_name = "leetcode.nvim", | |
| 8 | + | |
| 9 | + ---@type string | |
| 10 | + lang = "java", | |
| 11 | + | |
| 12 | + ---@type | |
| 13 | + ---| "pythondata" | |
| 14 | + ---| "mysql" | |
| 15 | + ---| "mssql" | |
| 16 | + ---| "oraclesql" | |
| 17 | + sql = "mysql", | |
| 18 | + | |
| 19 | + directory = vim.fn.stdpath("data") .. "/leetcode", | |
| 20 | +} | |
| 21 | + | |
| 22 | +return M |
+30-0lua/leetcode/graphql/auth.lua
| @@ -0,0 +1,30 @@ | ||
| 1 | +local gql = require("leetcode.graphql.utils") | |
| 2 | + | |
| 3 | +---@class lc.AuthApi | |
| 4 | +local M = {} | |
| 5 | + | |
| 6 | +---@return table | nil | |
| 7 | +function M.user_status() | |
| 8 | + local query = [[ | |
| 9 | + query globalData { | |
| 10 | + userStatus { | |
| 11 | + userId | |
| 12 | + isSignedIn | |
| 13 | + isMockUser | |
| 14 | + isPremium | |
| 15 | + isVerified | |
| 16 | + username | |
| 17 | + isTranslator | |
| 18 | + activeSessionId | |
| 19 | + checkedInToday | |
| 20 | + } | |
| 21 | + } | |
| 22 | + ]] | |
| 23 | + | |
| 24 | + local ok, res = pcall(gql.query, query) | |
| 25 | + if not ok then return end | |
| 26 | + | |
| 27 | + return res["userStatus"] | |
| 28 | +end | |
| 29 | + | |
| 30 | +return M |
+8-0lua/leetcode/graphql/init.lua
| @@ -0,0 +1,8 @@ | ||
| 1 | +---@class lc.Graphql | |
| 2 | +local Gql = { | |
| 3 | + problems = require("leetcode.graphql.problems"), | |
| 4 | + question = require("leetcode.graphql.question"), | |
| 5 | + auth = require("leetcode.graphql.auth"), | |
| 6 | +} | |
| 7 | + | |
| 8 | +return Gql |
+84-0lua/leetcode/graphql/problems.lua
| @@ -0,0 +1,84 @@ | ||
| 1 | +local logger = require("leetcode.logger") | |
| 2 | +local gql = require("leetcode.graphql.utils") | |
| 3 | + | |
| 4 | +---@class lc.ProblemsApi | |
| 5 | +local M = {} | |
| 6 | + | |
| 7 | +---@return integer | |
| 8 | +function M.total_num() | |
| 9 | + local query = [[ | |
| 10 | + query problemsetQuestionList { | |
| 11 | + problemsetQuestionList: questionList( | |
| 12 | + categorySlug: "" | |
| 13 | + filters: {} | |
| 14 | + ) { | |
| 15 | + total: totalNum | |
| 16 | + } | |
| 17 | + } | |
| 18 | + ]] | |
| 19 | + | |
| 20 | + local total = gql.query(query)["problemsetQuestionList"]["total"] | |
| 21 | + | |
| 22 | + return total | |
| 23 | +end | |
| 24 | + | |
| 25 | +---@return lc.Problem[] | |
| 26 | +function M.all() | |
| 27 | + local variables = { | |
| 28 | + limit = 3000, | |
| 29 | + } | |
| 30 | + | |
| 31 | + 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 | |
| 44 | + } | |
| 45 | + } | |
| 46 | + } | |
| 47 | + ]] | |
| 48 | + | |
| 49 | + local ok, res = pcall(gql.query, query, variables) | |
| 50 | + assert(ok) | |
| 51 | + | |
| 52 | + return res["problemsetQuestionList"]["questions"] | |
| 53 | +end | |
| 54 | + | |
| 55 | +function M.question_of_today() | |
| 56 | + 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 | |
| 71 | + } | |
| 72 | + } | |
| 73 | + } | |
| 74 | + ]] | |
| 75 | + | |
| 76 | + local ok, res = pcall(gql.query, query) | |
| 77 | + assert(ok) | |
| 78 | + | |
| 79 | + logger.inspect(res) | |
| 80 | + | |
| 81 | + return res["activeDailyCodingChallengeQuestion"] | |
| 82 | +end | |
| 83 | + | |
| 84 | +return M |
+60-0lua/leetcode/graphql/question.lua
| @@ -0,0 +1,60 @@ | ||
| 1 | +local gql = require("leetcode.graphql.utils") | |
| 2 | +local logger = require("leetcode.logger") | |
| 3 | + | |
| 4 | +local M = {} | |
| 5 | + | |
| 6 | +---@param title_slug string | |
| 7 | +function M.get_by_title_slug(title_slug) | |
| 8 | + local variables = { | |
| 9 | + titleSlug = title_slug, | |
| 10 | + } | |
| 11 | + | |
| 12 | + local query = [[ | |
| 13 | + query questionContent($titleSlug: String!) { | |
| 14 | + question(titleSlug: $titleSlug) { | |
| 15 | + content | |
| 16 | + mysqlSchemas | |
| 17 | + dataSchemas | |
| 18 | + } | |
| 19 | + } | |
| 20 | + ]] | |
| 21 | + | |
| 22 | + local ok, res = pcall(gql.query, query, variables) | |
| 23 | + assert(ok) | |
| 24 | + | |
| 25 | + logger.inspect(res) | |
| 26 | + | |
| 27 | + return res["questionContent"] | |
| 28 | +end | |
| 29 | + | |
| 30 | +---@param title_slug string | |
| 31 | +function M.editor_data(title_slug) | |
| 32 | + local variables = { | |
| 33 | + titleSlug = title_slug, | |
| 34 | + } | |
| 35 | + | |
| 36 | + local query = [[ | |
| 37 | + query questionEditorData($titleSlug: String!) { | |
| 38 | + question(titleSlug: $titleSlug) { | |
| 39 | + questionId | |
| 40 | + questionFrontendId | |
| 41 | + code_snippets: codeSnippets { | |
| 42 | + lang | |
| 43 | + langSlug | |
| 44 | + code | |
| 45 | + } | |
| 46 | + envInfo | |
| 47 | + enableRunCode | |
| 48 | + hasFrontendPreview | |
| 49 | + frontendPreviews | |
| 50 | + } | |
| 51 | + } | |
| 52 | + ]] | |
| 53 | + | |
| 54 | + local ok, res = pcall(gql.query, query, variables) | |
| 55 | + assert(ok) | |
| 56 | + | |
| 57 | + return res["question"] | |
| 58 | +end | |
| 59 | + | |
| 60 | +return M |
+30-0lua/leetcode/graphql/utils.lua
| @@ -0,0 +1,30 @@ | ||
| 1 | +local headers = require("leetcode.api.headers") | |
| 2 | +local config = require("leetcode.config") | |
| 3 | + | |
| 4 | +local curl = require("plenary.curl") | |
| 5 | + | |
| 6 | +local lc = "https://leetcode." .. config.user.domain | |
| 7 | +local endpoint = lc .. "/graphql" | |
| 8 | + | |
| 9 | +local M = {} | |
| 10 | + | |
| 11 | +---@param query string | |
| 12 | +---@param variables? table optional | |
| 13 | +--- | |
| 14 | +---@return table | |
| 15 | +function M.query(query, variables) | |
| 16 | + local response = curl.post(endpoint, { | |
| 17 | + headers = headers.get(), | |
| 18 | + body = vim.json.encode({ | |
| 19 | + query = query, | |
| 20 | + variables = variables or {}, | |
| 21 | + }), | |
| 22 | + }) | |
| 23 | + | |
| 24 | + local ok, data = pcall(vim.json.decode, response["body"]) | |
| 25 | + assert(ok and data and data["data"]) | |
| 26 | + | |
| 27 | + return data["data"] | |
| 28 | +end | |
| 29 | + | |
| 30 | +return M |
+25-0lua/leetcode/logger/init.lua
| @@ -0,0 +1,25 @@ | ||
| 1 | +local config = require("leetcode.config") | |
| 2 | + | |
| 3 | +---@class lc.Logger | |
| 4 | +local Logger = {} | |
| 5 | + | |
| 6 | +---@param msg string | |
| 7 | +---@param lvl? string | |
| 8 | +Logger.log = function(msg, lvl) require("notify")(config.name .. ": " .. msg, lvl or "info") end | |
| 9 | + | |
| 10 | +---@param msg string | |
| 11 | +Logger.info = function(msg) Logger.log(msg) end | |
| 12 | + | |
| 13 | +---@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 | + | |
| 19 | +---@param msg table | |
| 20 | +Logger.inspect = function(msg) Logger.log(vim.inspect(msg)) end | |
| 21 | + | |
| 22 | +---@param msg string | |
| 23 | +Logger.error = function(msg) Logger.log(msg, "error") end | |
| 24 | + | |
| 25 | +return Logger |
+133-0lua/leetcode/ui/dashboard.lua
| @@ -0,0 +1,133 @@ | ||
| 1 | +local utils = require("leetcode.utils") | |
| 2 | +local gql = require("leetcode.graphql") | |
| 3 | + | |
| 4 | +local M = {} | |
| 5 | + | |
| 6 | +local header = { | |
| 7 | + type = "text", | |
| 8 | + val = { | |
| 9 | + [[ /$$ /$$ /$$$$$$ /$$ ]], | |
| 10 | + [[| $$ | $$ /$$__ $$ | $$ ]], | |
| 11 | + [[| $$ /$$$$$$ /$$$$$$ /$$$$$$ | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ ]], | |
| 12 | + [[| $$ /$$__ $$/$$__ $|_ $$_/ | $$ /$$__ $$/$$__ $$/$$__ $$]], | |
| 13 | + [[| $$ | $$$$$$$| $$$$$$$$ | $$ | $$ | $$ \ $| $$ | $| $$$$$$$$]], | |
| 14 | + [[| $$ | $$_____| $$_____/ | $$ /$| $$ $| $$ | $| $$ | $| $$_____/]], | |
| 15 | + [[| $$$$$$$| $$$$$$| $$$$$$$ | $$$$| $$$$$$| $$$$$$| $$$$$$| $$$$$$$]], | |
| 16 | + [[|________/\_______/\_______/ \___/ \______/ \______/ \_______/\_______/]], | |
| 17 | + }, | |
| 18 | + opts = { | |
| 19 | + position = "center", | |
| 20 | + hl = "Keyword", | |
| 21 | + }, | |
| 22 | +} | |
| 23 | + | |
| 24 | +local notifications = { | |
| 25 | + type = "text", | |
| 26 | + val = {}, | |
| 27 | + opts = { | |
| 28 | + position = "center", | |
| 29 | + hl = "DiagnosticInfo", | |
| 30 | + }, | |
| 31 | +} | |
| 32 | + | |
| 33 | +local buttons = { | |
| 34 | + type = "group", | |
| 35 | + val = {}, | |
| 36 | + opts = { | |
| 37 | + spacing = 1, | |
| 38 | + }, | |
| 39 | +} | |
| 40 | + | |
| 41 | +local footer = { | |
| 42 | + type = "text", | |
| 43 | + val = {}, | |
| 44 | + opts = { | |
| 45 | + position = "center", | |
| 46 | + hl = "Number", | |
| 47 | + }, | |
| 48 | +} | |
| 49 | + | |
| 50 | +function M.setup() | |
| 51 | + local user = gql.auth.user_status() | |
| 52 | + local is_signed_in = (user ~= nil and user.isSignedIn) | |
| 53 | + | |
| 54 | + local alpha = require("alpha") | |
| 55 | + local dashboard = require("alpha.themes.dashboard") | |
| 56 | + | |
| 57 | + notifications.val = not is_signed_in and { " Sign in to use LeetCode.nvim" } or {} | |
| 58 | + | |
| 59 | + buttons.val = is_signed_in | |
| 60 | + and { | |
| 61 | + dashboard.button( | |
| 62 | + "p", | |
| 63 | + " " .. " All Problems", | |
| 64 | + "<cmd>lua require('leetcode.api').cmd.lc_problems()<CR>" | |
| 65 | + ), | |
| 66 | + dashboard.button( | |
| 67 | + "t", | |
| 68 | + " " .. " Question of today", | |
| 69 | + "<cmd>lua require('leetcode.api').gql.problems.question_of_today()<CR>" | |
| 70 | + ), | |
| 71 | + dashboard.button("r", " " .. " Recent problems", ""), | |
| 72 | + dashboard.button( | |
| 73 | + "c", | |
| 74 | + " " .. " Update cookie", | |
| 75 | + "<cmd>lua require('leetcode.utils').prompt_for_cookie()<cr>" | |
| 76 | + ), | |
| 77 | + dashboard.button( | |
| 78 | + "s", | |
| 79 | + " " .. " Sign out", | |
| 80 | + "<cmd>lua require('leetcode.utils').remove_cookie()<cr>" | |
| 81 | + ), | |
| 82 | + dashboard.button("q", " " .. " Exit LeetCode", "<cmd>qa<CR>"), | |
| 83 | + } | |
| 84 | + or { | |
| 85 | + dashboard.button( | |
| 86 | + "s", | |
| 87 | + " " .. " Sign in (By Cookie)", | |
| 88 | + "<cmd>lua require('leetcode.utils').prompt_for_cookie()<cr>" | |
| 89 | + ), | |
| 90 | + dashboard.button("q", " " .. " Exit LeetCode", "<cmd>qa!<CR>"), | |
| 91 | + } | |
| 92 | + | |
| 93 | + footer.val = is_signed_in and "Signed in as: " .. user.username or "" | |
| 94 | + | |
| 95 | + local section = { | |
| 96 | + header = header, | |
| 97 | + notifications = notifications, | |
| 98 | + buttons = buttons, | |
| 99 | + footer = footer, | |
| 100 | + } | |
| 101 | + | |
| 102 | + local config = { | |
| 103 | + layout = { | |
| 104 | + -- header | |
| 105 | + { type = "padding", val = 4 }, | |
| 106 | + section.header, | |
| 107 | + | |
| 108 | + -- notifications | |
| 109 | + { type = "padding", val = 2 }, | |
| 110 | + section.notifications, | |
| 111 | + { type = "padding", val = 2 }, | |
| 112 | + | |
| 113 | + -- buttons | |
| 114 | + section.buttons, | |
| 115 | + | |
| 116 | + --footer | |
| 117 | + section.footer, | |
| 118 | + }, | |
| 119 | + opts = { | |
| 120 | + margin = 5, | |
| 121 | + }, | |
| 122 | + } | |
| 123 | + | |
| 124 | + alpha.setup(config) | |
| 125 | +end | |
| 126 | + | |
| 127 | +function M.update() | |
| 128 | + utils.alpha_move_cursor_top() | |
| 129 | + M.setup() | |
| 130 | + pcall(vim.cmd, "AlphaRedraw") | |
| 131 | +end | |
| 132 | + | |
| 133 | +return M |
+54-0lua/leetcode/ui/init.lua
| @@ -0,0 +1,54 @@ | ||
| 1 | +---@class lc.UI | |
| 2 | +local Ui = {} | |
| 3 | + | |
| 4 | +---@param items table | |
| 5 | +---@param prompt? string | |
| 6 | +---@param fn? fun(table) | |
| 7 | +---@param callback fun(table) | |
| 8 | +function Ui.pick_one(items, prompt, fn, callback) | |
| 9 | + vim.ui.select(items, { | |
| 10 | + prompt = prompt or "", | |
| 11 | + format_item = fn or function(item) return item end, | |
| 12 | + telescope = require("telescope.themes").get_dropdown(), | |
| 13 | + }, callback) | |
| 14 | +end | |
| 15 | + | |
| 16 | +function Ui.input(opts, callback) vim.ui.input(opts, callback) end | |
| 17 | + | |
| 18 | +function Ui.oldfiles() Ui.create_win() end | |
| 19 | + | |
| 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() | |
| 23 | + | |
| 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. | |
| 27 | + | |
| 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) | |
| 32 | + | |
| 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. | |
| 45 | + vim.api.nvim_buf_set_option(buf, "filetype", "leetcode.nvim") | |
| 46 | + | |
| 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) | |
| 50 | + | |
| 51 | + set_mappings() -- At end we will set mappings for our navigation. | |
| 52 | +end | |
| 53 | + | |
| 54 | +return Ui |
+32-0lua/leetcode/utils.lua
| @@ -0,0 +1,32 @@ | ||
| 1 | +local ui = require("leetcode.ui") | |
| 2 | + | |
| 3 | +---@class lc.Utils | |
| 4 | +local Utils = {} | |
| 5 | + | |
| 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 | + | |
| 21 | +function Utils.remove_cookie() | |
| 22 | + require("leetcode.cache.cookie").delete() | |
| 23 | + require("leetcode.ui.dashboard").update() | |
| 24 | +end | |
| 25 | + | |
| 26 | +function Utils.alpha_move_cursor_top() | |
| 27 | + local curr_win = vim.api.nvim_get_current_win() | |
| 28 | + vim.api.nvim_win_set_cursor(curr_win, { 1, 0 }) | |
| 29 | + require("alpha").move_cursor(curr_win) | |
| 30 | +end | |
| 31 | + | |
| 32 | +return Utils |