A Neovim plugin enabling you to solve LeetCode problems.
feat: chinese problemlist
12 files changed, +115 -69
+3-3lua/leetcode/api/interpreter.lua
| @@ -51,14 +51,14 @@ end | ||
| 51 | 51 | ---@param body lc.Interpret.body |
| 52 | 52 | ---@param callback function |
| 53 | 53 | function interpreter.interpret_solution(title_slug, body, callback) |
| 54 | - local url = (config.domain .. "/problems/%s/interpret_solution/"):format(title_slug) | |
| 54 | + local url = ("%s/problems/%s/interpret_solution/"):format(config.domain, title_slug) | |
| 55 | 55 | local res = interpreter.fetch(url, body) |
| 56 | 56 | |
| 57 | 57 | if res then interpreter.listener(res.interpret_id, callback) end |
| 58 | 58 | end |
| 59 | 59 | |
| 60 | 60 | function interpreter.submit(title_slug, body, callback) |
| 61 | - local url = (config.domain .. "/problems/%s/submit/"):format(title_slug) | |
| 61 | + local url = ("%s/problems/%s/submit/"):format(config.domain, title_slug) | |
| 62 | 62 | local res = interpreter.fetch(url, body) |
| 63 | 63 | |
| 64 | 64 | if res then interpreter.listener(res.submission_id, callback) end |
| @@ -69,7 +69,7 @@ end | ||
| 69 | 69 | --- |
| 70 | 70 | ---@return lc.Interpreter.Response |
| 71 | 71 | function interpreter.check(id, cb) |
| 72 | - local url = (config.domain .. "/submissions/detail/%s/check/"):format(id) | |
| 72 | + local url = ("%s/submissions/detail/%s/check/"):format(config.domain, id) | |
| 73 | 73 | utils.get(url, cb) |
| 74 | 74 | end |
| 75 | 75 |
+36-12lua/leetcode/api/problems.lua
| @@ -1,5 +1,7 @@ | ||
| 1 | 1 | local utils = require("leetcode.api.utils") |
| 2 | 2 | local queries = require("leetcode.api.queries") |
| 3 | +local config = require("leetcode.config") | |
| 4 | + | |
| 3 | 5 | local log = require("leetcode.logger") |
| 4 | 6 | |
| 5 | 7 | ---@class lc.ProblemsApi |
| @@ -7,22 +9,33 @@ local M = {} | ||
| 7 | 9 | |
| 8 | 10 | ---@return lc.Cache.Question[] |
| 9 | 11 | function M.all(cb) |
| 10 | - local variables = { | |
| 11 | - limit = 9999, | |
| 12 | - } | |
| 13 | - | |
| 14 | - local query = queries.problemset() | |
| 12 | + local url = ("%s/api/problems/algorithms/"):format(config.domain) | |
| 15 | 13 | |
| 16 | 14 | if cb then |
| 17 | - utils.query(query, variables, function(res, err) | |
| 18 | - local data = res.data | |
| 19 | - local questions = data["problemsetQuestionList"]["questions"] | |
| 20 | - cb(utils.normalize_cn_problemlist(questions)) | |
| 15 | + utils.get(url, function(res, err) | |
| 16 | + if err then return end | |
| 17 | + | |
| 18 | + local problems = utils.normalize_problems(res.stat_status_pairs) | |
| 19 | + | |
| 20 | + if config.is_cn and config.user.cn.translate then | |
| 21 | + M.translated_titles(function(titles) | |
| 22 | + problems = utils.translate_titles(problems, titles) | |
| 23 | + cb(problems) | |
| 24 | + end) | |
| 25 | + else | |
| 26 | + cb(problems) | |
| 27 | + end | |
| 21 | 28 | end) |
| 22 | 29 | else |
| 23 | - local res, err = utils.query(query, variables) | |
| 24 | - local questions = res.data["problemsetQuestionList"]["questions"] | |
| 25 | - return utils.normalize_cn_problemlist(questions) | |
| 30 | + local res, err = utils.get(url) | |
| 31 | + local problems = utils.normalize_problems(res.stat_status_pairs) | |
| 32 | + | |
| 33 | + if config.is_cn and config.user.cn.translate then | |
| 34 | + local titles = M.translated_titles() | |
| 35 | + return utils.translate_titles(problems, titles) | |
| 36 | + else | |
| 37 | + return problems | |
| 38 | + end | |
| 26 | 39 | end |
| 27 | 40 | end |
| 28 | 41 | |
| @@ -37,4 +50,15 @@ function M.question_of_today(cb) | ||
| 37 | 50 | utils.query(query, {}, callback) |
| 38 | 51 | end |
| 39 | 52 | |
| 53 | +function M.translated_titles(cb) | |
| 54 | + local query = queries.translations() | |
| 55 | + | |
| 56 | + if cb then | |
| 57 | + utils.query(query, {}, function(res, err) cb(res.data.translations) end) | |
| 58 | + else | |
| 59 | + local res, err = utils.query(query, {}) | |
| 60 | + return res.data.translations | |
| 61 | + end | |
| 62 | +end | |
| 63 | + | |
| 40 | 64 | return M |
+14-1lua/leetcode/api/queries.lua
| @@ -3,7 +3,7 @@ local config = require("leetcode.config") | ||
| 3 | 3 | ---@class lc.Api.Queries |
| 4 | 4 | local queries = {} |
| 5 | 5 | |
| 6 | -local function com_or(query, query_cn) return config.user.domain == "com" and query or query_cn end | |
| 6 | +local function com_or(query, query_cn) return not config.is_cn and query or query_cn end | |
| 7 | 7 | |
| 8 | 8 | function queries.auth() |
| 9 | 9 | local query = [[ |
| @@ -238,4 +238,17 @@ function queries.random_question() | ||
| 238 | 238 | return com_or(query, query_cn) |
| 239 | 239 | end |
| 240 | 240 | |
| 241 | +function queries.translations() | |
| 242 | + local query_cn = [[ | |
| 243 | + query getQuestionTranslation($lang: String) { | |
| 244 | + translations: allAppliedQuestionTranslations(lang: $lang) { | |
| 245 | + title | |
| 246 | + questionId | |
| 247 | + } | |
| 248 | + } | |
| 249 | + ]] | |
| 250 | + | |
| 251 | + return com_or(nil, query_cn) | |
| 252 | +end | |
| 253 | + | |
| 241 | 254 | return queries |
+1-1lua/leetcode/api/question.lua
| @@ -40,7 +40,7 @@ function question.random() | ||
| 40 | 40 | local res, err = utils.query(query, variables) |
| 41 | 41 | |
| 42 | 42 | local q = res.data.randomQuestion |
| 43 | - if config.user.domain == "cn" then | |
| 43 | + if config.is_cn then | |
| 44 | 44 | q = { |
| 45 | 45 | title_slug = q, |
| 46 | 46 | paid_only = problemlist.get_by_title_slug(q).paid_only, |
+46-39lua/leetcode/api/utils.lua
| @@ -115,45 +115,6 @@ function utils.auth_guard() | ||
| 115 | 115 | assert(auth and auth.is_signed_in, "User not signed in") |
| 116 | 116 | end |
| 117 | 117 | |
| 118 | -local rename_cn_keys = { | |
| 119 | - acRate = "ac_rate", | |
| 120 | - frontendQuestionId = "frontend_id", | |
| 121 | - titleCn = "title_cn", | |
| 122 | - titleSlug = "title_slug", | |
| 123 | - topicTags = "topic_tags", | |
| 124 | - paidOnly = "paid_only", | |
| 125 | -} | |
| 126 | - | |
| 127 | -local status_cn = { | |
| 128 | - ["NOT_STARTED"] = vim.NIL, | |
| 129 | -} | |
| 130 | - | |
| 131 | -local difficulty_cn = { | |
| 132 | - ["EASY"] = "Easy", | |
| 133 | - ["MEDIUM"] = "Medium", | |
| 134 | - ["HARD"] = "Hard", | |
| 135 | -} | |
| 136 | - | |
| 137 | -function utils.normalize_cn_problemlist(problems) | |
| 138 | - if config.user.domain == "com" then return problems end | |
| 139 | - | |
| 140 | - for _, question in ipairs(problems) do | |
| 141 | - for key, value in pairs(question) do | |
| 142 | - if vim.tbl_contains(vim.tbl_keys(rename_cn_keys), key) then | |
| 143 | - question[rename_cn_keys[key]] = value | |
| 144 | - question[key] = nil | |
| 145 | - end | |
| 146 | - end | |
| 147 | - | |
| 148 | - question.difficulty = difficulty_cn[question.difficulty] | |
| 149 | - question.status = status_cn[question.status] | |
| 150 | - question.ac_rate = question.ac_rate * 100 | |
| 151 | - question.extra = nil | |
| 152 | - end | |
| 153 | - | |
| 154 | - return problems | |
| 155 | -end | |
| 156 | - | |
| 157 | 118 | function utils.normalize_similar_cn(s) |
| 158 | 119 | s = select(2, pcall(utils.decode, s)) |
| 159 | 120 | |
| @@ -165,4 +126,50 @@ function utils.normalize_similar_cn(s) | ||
| 165 | 126 | end, s) |
| 166 | 127 | end |
| 167 | 128 | |
| 129 | +function utils.lvl_to_name(lvl) return ({ "Easy", "Medium", "Hard" })[lvl] end | |
| 130 | + | |
| 131 | +---@return lc.Cache.Question[] | |
| 132 | +function utils.normalize_problems(problems) | |
| 133 | + problems = vim.tbl_filter(function(p) return not p.stat.question__hide end, problems) | |
| 134 | + | |
| 135 | + table.sort( | |
| 136 | + problems, | |
| 137 | + function(a, b) return a.stat.frontend_question_id < b.stat.frontend_question_id end | |
| 138 | + ) | |
| 139 | + | |
| 140 | + return vim.tbl_map( | |
| 141 | + function(p) | |
| 142 | + return { | |
| 143 | + status = p.status, | |
| 144 | + id = p.stat.question_id, | |
| 145 | + frontend_id = p.stat.frontend_question_id, | |
| 146 | + title = p.stat.question__title, | |
| 147 | + title_slug = p.stat.question__title_slug, | |
| 148 | + link = ("%s/problems/%s/"):format(config.domain, p.stat.question__title_slug), | |
| 149 | + paid_only = p.paid_only, | |
| 150 | + ac_rate = p.stat.total_acs * 100 / p.stat.total_submitted, | |
| 151 | + difficulty = utils.lvl_to_name(p.difficulty.level), | |
| 152 | + starred = p.is_favor, | |
| 153 | + topic_tags = {}, | |
| 154 | + } | |
| 155 | + end, | |
| 156 | + problems | |
| 157 | + ) | |
| 158 | +end | |
| 159 | + | |
| 160 | +---@param problems lc.Cache.Question[] | |
| 161 | +---@param titles { questionId: integer, title: string }[] | |
| 162 | +function utils.translate_titles(problems, titles) | |
| 163 | + local map = {} | |
| 164 | + for _, t in ipairs(titles) do | |
| 165 | + map[t.questionId] = t.title | |
| 166 | + end | |
| 167 | + | |
| 168 | + return vim.tbl_map(function(p) | |
| 169 | + local title = map[tostring(p.id)] | |
| 170 | + if title then p.title = title end | |
| 171 | + return p | |
| 172 | + end, problems) | |
| 173 | +end | |
| 174 | + | |
| 168 | 175 | return utils |
+1-1lua/leetcode/cache/cookie.lua
| @@ -2,7 +2,7 @@ local path = require("plenary.path") | ||
| 2 | 2 | local log = require("leetcode.logger") |
| 3 | 3 | |
| 4 | 4 | local config = require("leetcode.config") |
| 5 | -local file = config.home:joinpath((".cookie%s"):format(config.user.domain == "cn" and "_cn" or "")) | |
| 5 | +local file = config.home:joinpath((".cookie%s"):format(config.is_cn and "_cn" or "")) | |
| 6 | 6 | |
| 7 | 7 | ---@class lc.Cookie |
| 8 | 8 | ---@field csrftoken string |
+3-2lua/leetcode/cache/problemlist.lua
| @@ -2,8 +2,7 @@ local path = require("plenary.path") | ||
| 2 | 2 | local log = require("leetcode.logger") |
| 3 | 3 | |
| 4 | 4 | local config = require("leetcode.config") |
| 5 | -local file = | |
| 6 | - config.home:joinpath((".problemlist%s"):format(config.user.domain == "cn" and "_cn" or "")) | |
| 5 | +local file = config.home:joinpath((".problemlist%s"):format(config.is_cn and "_cn" or "")) | |
| 7 | 6 | |
| 8 | 7 | local problems_api = require("leetcode.api.problems") |
| 9 | 8 | |
| @@ -12,7 +11,9 @@ local problems_api = require("leetcode.api.problems") | ||
| 12 | 11 | ---@field slug string |
| 13 | 12 | |
| 14 | 13 | ---@class lc.Cache.Question |
| 14 | +---@field id integer | |
| 15 | 15 | ---@field frontend_id string |
| 16 | +---@field link string | |
| 16 | 17 | ---@field title string |
| 17 | 18 | ---@field title_cn string |
| 18 | 19 | ---@field title_slug string |
+2-0lua/leetcode/config/init.lua
| @@ -34,6 +34,8 @@ function config.apply(cfg) | ||
| 34 | 34 | config.debug = config.user.debug or false ---@diagnostic disable-line |
| 35 | 35 | config.domain = "https://leetcode." .. config.user.domain |
| 36 | 36 | |
| 37 | + config.is_cn = config.user.domain == "cn" | |
| 38 | + | |
| 37 | 39 | config.lang = config.user.lang |
| 38 | 40 | config.sql = config.user.sql |
| 39 | 41 | end |
+4-0lua/leetcode/config/template.lua
| @@ -93,6 +93,10 @@ local M = { | ||
| 93 | 93 | ---@type fun(question: { lang: string })[] |
| 94 | 94 | LeetQuestionNew = {}, |
| 95 | 95 | }, |
| 96 | + | |
| 97 | + cn = { | |
| 98 | + translate = true, ---@type boolean | |
| 99 | + }, | |
| 96 | 100 | } |
| 97 | 101 | |
| 98 | 102 | return M |
+2-4lua/leetcode/pickers/question.lua
| @@ -15,8 +15,7 @@ local action_state = require("telescope.actions.state") | ||
| 15 | 15 | --- |
| 16 | 16 | ---@return string |
| 17 | 17 | local function question_formatter(question) |
| 18 | - local title = config.user.domain == "com" and question.title or question.title_cn | |
| 19 | - return ("%d. %s %s"):format(question.frontend_id, title, question.title_slug) | |
| 18 | + return ("%s. %s %s"):format(tostring(question.frontend_id), question.title, question.title_slug) | |
| 20 | 19 | end |
| 21 | 20 | |
| 22 | 21 | ---@param question lc.Cache.Question |
| @@ -50,8 +49,7 @@ end | ||
| 50 | 49 | local function display_question(question) |
| 51 | 50 | local ac_rate = { ("%.1f%%"):format(question.ac_rate), "leetcode_ref" } |
| 52 | 51 | local index = { question.frontend_id .. ".", "leetcode_normal" } |
| 53 | - local title_txt = config.user.domain == "com" and question.title or question.title_cn | |
| 54 | - local title = { title_txt } | |
| 52 | + local title = { question.title } | |
| 55 | 53 | |
| 56 | 54 | return unpack({ index, title, ac_rate }) |
| 57 | 55 | end |
+1-1lua/leetcode/translator/init.lua
| @@ -42,7 +42,7 @@ local function t(text) | ||
| 42 | 42 | return "?" |
| 43 | 43 | end |
| 44 | 44 | |
| 45 | - return config.user.domain == "com" and text or translate[text] | |
| 45 | + return not config.is_cn and text or translate[text] | |
| 46 | 46 | end |
| 47 | 47 | |
| 48 | 48 | ---@param text string|string[] |
+2-5lua/leetcode/ui/description.lua
| @@ -91,8 +91,7 @@ function description:populate() | ||
| 91 | 91 | local q = self.parent.q |
| 92 | 92 | |
| 93 | 93 | local linkline = NuiLine() |
| 94 | - local question_link = string.format("%s/problems/%s/", config.domain, q.title_slug) | |
| 95 | - linkline:append(question_link, "leetcode_alt") | |
| 94 | + linkline:append(self.parent.cache.link, "leetcode_alt") | |
| 96 | 95 | |
| 97 | 96 | local titleline = NuiLine() |
| 98 | 97 | titleline:append(q.frontend_id .. ". ", "leetcode_normal") |
| @@ -110,9 +109,7 @@ function description:populate() | ||
| 110 | 109 | statsline:append(" | ") |
| 111 | 110 | |
| 112 | 111 | statsline:append(q.likes .. " ", "leetcode_alt") |
| 113 | - if config.user.domain == "com" then | |
| 114 | - statsline:append(" " .. q.dislikes .. " ", "leetcode_alt") | |
| 115 | - end | |
| 112 | + if not config.is_cn then statsline:append(" " .. q.dislikes .. " ", "leetcode_alt") end | |
| 116 | 113 | statsline:append(" | ") |
| 117 | 114 | |
| 118 | 115 | statsline:append( |