A Neovim plugin enabling you to solve LeetCode problems.
feat(question): option to disable translated contents for cn
14 files changed, +99 -121
+4-28lua/leetcode-plugins/cn/queries.lua
| @@ -13,32 +13,6 @@ queries.auth = [[ | ||
| 13 | 13 | } |
| 14 | 14 | ]] |
| 15 | 15 | |
| 16 | -queries.problemset = [[ | |
| 17 | - query problemsetQuestionList($limit: Int) { | |
| 18 | - problemsetQuestionList( | |
| 19 | - categorySlug: "" | |
| 20 | - limit: $limit | |
| 21 | - filters: { } | |
| 22 | - ) { | |
| 23 | - questions { | |
| 24 | - frontend_id: frontendQuestionId | |
| 25 | - title | |
| 26 | - title_slug: titleSlug | |
| 27 | - title_cn: titleCn | |
| 28 | - status | |
| 29 | - paid_only: paidOnly | |
| 30 | - ac_rate: acRate | |
| 31 | - difficulty | |
| 32 | - topic_tags: topicTags { | |
| 33 | - name | |
| 34 | - slug | |
| 35 | - id | |
| 36 | - } | |
| 37 | - } | |
| 38 | - } | |
| 39 | - } | |
| 40 | - ]] | |
| 41 | - | |
| 42 | 16 | queries.qot = [[ |
| 43 | 17 | query questionOfToday { |
| 44 | 18 | todayRecord { |
| @@ -54,14 +28,16 @@ queries.question = [[ | ||
| 54 | 28 | question(titleSlug: $titleSlug) { |
| 55 | 29 | id: questionId |
| 56 | 30 | frontend_id: questionFrontendId |
| 57 | - title: translatedTitle | |
| 58 | 31 | title_slug: titleSlug |
| 59 | 32 | is_paid_only: isPaidOnly |
| 60 | 33 | difficulty |
| 61 | 34 | likes |
| 62 | 35 | dislikes |
| 63 | 36 | category_title: categoryTitle |
| 64 | - content: translatedContent | |
| 37 | + content | |
| 38 | + translated_content: translatedContent | |
| 39 | + title | |
| 40 | + translated_title: translatedTitle | |
| 65 | 41 | mysql_schemas: mysqlSchemas |
| 66 | 42 | data_schemas: dataSchemas |
| 67 | 43 | code_snippets: codeSnippets { |
+1-0lua/leetcode/api/auth.lua
| @@ -18,6 +18,7 @@ function Auth.user(cb) | ||
| 18 | 18 | end |
| 19 | 19 | end |
| 20 | 20 | |
| 21 | +---@private | |
| 21 | 22 | ---@return lc.UserStatus|nil, lc.err|nil |
| 22 | 23 | function Auth.handle(res, err) |
| 23 | 24 | if err then return res, err end |
+2-2lua/leetcode/api/problems.lua
| @@ -18,7 +18,7 @@ function M.all(cb) | ||
| 18 | 18 | |
| 19 | 19 | local problems = utils.normalize_problems(res.stat_status_pairs) |
| 20 | 20 | |
| 21 | - if config.is_cn and config.user.cn.translate then | |
| 21 | + if config.is_cn then | |
| 22 | 22 | M.translated_titles(function(titles) |
| 23 | 23 | problems = utils.translate_titles(problems, titles) |
| 24 | 24 | cb(problems) |
| @@ -31,7 +31,7 @@ function M.all(cb) | ||
| 31 | 31 | local res, err = utils.get(endpoint) |
| 32 | 32 | local problems = utils.normalize_problems(res.stat_status_pairs) |
| 33 | 33 | |
| 34 | - if config.is_cn and config.user.cn.translate then | |
| 34 | + if config.is_cn then | |
| 35 | 35 | local titles = M.translated_titles() |
| 36 | 36 | return utils.translate_titles(problems, titles) |
| 37 | 37 | else |
+0-25lua/leetcode/api/queries.lua
| @@ -13,31 +13,6 @@ queries.auth = [[ | ||
| 13 | 13 | } |
| 14 | 14 | ]] |
| 15 | 15 | |
| 16 | -queries.problemset = [[ | |
| 17 | - query problemsetQuestionList($limit: Int) { | |
| 18 | - problemsetQuestionList: questionList( | |
| 19 | - categorySlug: "" | |
| 20 | - limit: $limit | |
| 21 | - filters: { } | |
| 22 | - ) { | |
| 23 | - questions: data { | |
| 24 | - frontend_id: questionFrontendId | |
| 25 | - title | |
| 26 | - title_slug: titleSlug | |
| 27 | - status | |
| 28 | - paid_only: isPaidOnly | |
| 29 | - ac_rate: acRate | |
| 30 | - difficulty | |
| 31 | - topic_tags: topicTags { | |
| 32 | - name | |
| 33 | - slug | |
| 34 | - id | |
| 35 | - } | |
| 36 | - } | |
| 37 | - } | |
| 38 | - } | |
| 39 | - ]] | |
| 40 | - | |
| 41 | 16 | queries.qot = [[ |
| 42 | 17 | query questionOfToday { |
| 43 | 18 | todayRecord: activeDailyCodingChallengeQuestion { |
+2-0lua/leetcode/api/types.lua
| @@ -255,6 +255,7 @@ | ||
| 255 | 255 | ---@field id integer |
| 256 | 256 | ---@field frontend_id integer |
| 257 | 257 | ---@field title string |
| 258 | +---@field translated_title string | |
| 258 | 259 | ---@field title_slug string |
| 259 | 260 | ---@field is_paid_only boolean |
| 260 | 261 | ---@field difficulty string |
| @@ -262,6 +263,7 @@ | ||
| 262 | 263 | ---@field dislikes integer |
| 263 | 264 | ---@field category_title string |
| 264 | 265 | ---@field content string |
| 266 | +---@field translated_content string | |
| 265 | 267 | ---@field mysql_schemas string[] |
| 266 | 268 | ---@field data_schemas string[] |
| 267 | 269 | ---@field code_snippets code_snippet[] |
+2-5lua/leetcode/api/utils.lua
| @@ -165,7 +165,7 @@ function utils.normalize_problems(problems) | ||
| 165 | 165 | id = p.stat.question_id, |
| 166 | 166 | frontend_id = p.stat.frontend_question_id, |
| 167 | 167 | title = p.stat.question__title, |
| 168 | - sec_title = "", | |
| 168 | + title_cn = "", | |
| 169 | 169 | title_slug = p.stat.question__title_slug, |
| 170 | 170 | link = ("%s/problems/%s/"):format(config.domain, p.stat.question__title_slug), |
| 171 | 171 | paid_only = p.paid_only, |
| @@ -189,10 +189,7 @@ function utils.translate_titles(problems, titles) | ||
| 189 | 189 | |
| 190 | 190 | return vim.tbl_map(function(p) |
| 191 | 191 | local title = map[tostring(p.id)] |
| 192 | - if title then | |
| 193 | - p.sec_title = p.title | |
| 194 | - p.title = title | |
| 195 | - end | |
| 192 | + if title then p.title_cn = title end | |
| 196 | 193 | return p |
| 197 | 194 | end, problems) |
| 198 | 195 | end |
+2-6lua/leetcode/cache/init.lua
| @@ -1,12 +1,8 @@ | ||
| 1 | -local cookie = require("leetcode.cache.cookie") | |
| 2 | -local problems = require("leetcode.cache.problemlist") | |
| 1 | +local Problemlist = require("leetcode.cache.problemlist") | |
| 3 | 2 | |
| 4 | 3 | ---@class lc.Cache |
| 5 | 4 | local cache = {} |
| 6 | 5 | |
| 7 | -function cache.update() | |
| 8 | - problems.update(true) | |
| 9 | - -- cookie.update() | |
| 10 | -end | |
| 6 | +function cache.update() Problemlist.update() end | |
| 11 | 7 | |
| 12 | 8 | return cache |
+48-46lua/leetcode/cache/problemlist.lua
| @@ -2,8 +2,11 @@ local path = require("plenary.path") | ||
| 2 | 2 | local log = require("leetcode.logger") |
| 3 | 3 | |
| 4 | 4 | local config = require("leetcode.config") |
| 5 | +---@type Path | |
| 5 | 6 | local file = config.home:joinpath((".problemlist%s"):format(config.is_cn and "_cn" or "")) |
| 6 | 7 | |
| 8 | +local hist = {} | |
| 9 | + | |
| 7 | 10 | local problems_api = require("leetcode.api.problems") |
| 8 | 11 | |
| 9 | 12 | ---@class lc.Cache.Question.topicTags |
| @@ -15,7 +18,6 @@ local problems_api = require("leetcode.api.problems") | ||
| 15 | 18 | ---@field frontend_id string |
| 16 | 19 | ---@field link string |
| 17 | 20 | ---@field title string |
| 18 | ----@field sec_title string | |
| 19 | 21 | ---@field title_cn string |
| 20 | 22 | ---@field title_slug string |
| 21 | 23 | ---@field status string |
| @@ -24,25 +26,25 @@ local problems_api = require("leetcode.api.problems") | ||
| 24 | 26 | ---@field difficulty "Easy" | "Medium" | "Hard" |
| 25 | 27 | ---@field topic_tags lc.Cache.Question.topicTags[] |
| 26 | 28 | |
| 27 | -local problemlist = {} | |
| 29 | +---@class lc.cache.Problemlist | |
| 30 | +local Problemlist = {} | |
| 28 | 31 | |
| 29 | -local function populate() | |
| 32 | +function Problemlist.populate() | |
| 30 | 33 | local spinner = require("leetcode.logger.spinner") |
| 31 | 34 | |
| 32 | - local noti = spinner:init("Fetching Problem List", "points") | |
| 35 | + local noti = spinner:init("fetching problem list", "points") | |
| 33 | 36 | local data = problems_api.all() |
| 34 | - problemlist.write(data) | |
| 35 | - noti:stop("Problems Cache Updated!") | |
| 37 | + noti:stop("problems cache updated") | |
| 38 | + Problemlist.write(data) | |
| 39 | + return data | |
| 36 | 40 | end |
| 37 | 41 | |
| 38 | 42 | ---@param data string|table |
| 39 | -function problemlist.write(data) | |
| 43 | +function Problemlist.write(data) | |
| 40 | 44 | local str |
| 41 | 45 | |
| 42 | 46 | if type(data) == "table" then |
| 43 | - local ok, dec = pcall(vim.json.encode, data) | |
| 44 | - assert(ok, dec) | |
| 45 | - str = dec | |
| 47 | + str = vim.json.encode(data) | |
| 46 | 48 | else |
| 47 | 49 | str = data |
| 48 | 50 | end |
| @@ -51,65 +53,65 @@ function problemlist.write(data) | ||
| 51 | 53 | end |
| 52 | 54 | |
| 53 | 55 | ---@return lc.Cache.Question[] |
| 54 | -function problemlist.get() | |
| 55 | - problemlist.update() | |
| 56 | +function Problemlist.get() | |
| 57 | + if not file:exists() then return Problemlist.populate() end | |
| 56 | 58 | |
| 57 | - local r_ok, contents = pcall(path.read, file) | |
| 58 | - assert(r_ok) | |
| 59 | + local fstats = file:_stat() | |
| 60 | + local ftime = fstats.mtime.sec | |
| 61 | + local curr_time = os.time() | |
| 59 | 62 | |
| 60 | - local ok, problems = pcall(problemlist.parse, contents) | |
| 61 | - assert(ok) | |
| 63 | + if (curr_time - ftime) > config.user.cache.update_interval then Problemlist.update() end | |
| 62 | 64 | |
| 63 | - return problems | |
| 64 | -end | |
| 65 | + local hproblem = hist[ftime] | |
| 66 | + if hproblem then return hproblem end | |
| 65 | 67 | |
| 66 | ----@param force? boolean | |
| 67 | -function problemlist.update(force) | |
| 68 | - local stats = file:_stat() | |
| 69 | - if vim.tbl_isempty(stats) then return populate() end | |
| 68 | + local contents = file:read() | |
| 69 | + if not contents or type(contents) ~= "string" then return Problemlist.populate() end | |
| 70 | 70 | |
| 71 | - local mod_time = stats.mtime.sec | |
| 72 | - local curr_time = os.time() | |
| 71 | + local problems = Problemlist.parse(contents) | |
| 72 | + if not problems then return Problemlist.populate() end | |
| 73 | 73 | |
| 74 | - if force or (curr_time - mod_time) > 60 * 60 * 24 * 7 then | |
| 75 | - local spinner = require("leetcode.logger.spinner") | |
| 76 | - local noti = spinner:init("Fetching Problem List", "points") | |
| 74 | + hist[ftime] = problems | |
| 75 | + return problems | |
| 76 | +end | |
| 77 | 77 | |
| 78 | - problems_api.all(function(data) | |
| 79 | - problemlist.write(data) | |
| 80 | - noti:stop("Problems Cache Updated!") | |
| 81 | - end) | |
| 82 | - end | |
| 78 | +function Problemlist.update() | |
| 79 | + local spinner = require("leetcode.logger.spinner") | |
| 80 | + local noti = spinner:init("fetching problemlist", "points") | |
| 81 | + | |
| 82 | + problems_api.all(function(data) | |
| 83 | + Problemlist.write(data) | |
| 84 | + noti:stop("problems cache updated!") | |
| 85 | + end) | |
| 83 | 86 | end |
| 84 | 87 | |
| 85 | 88 | ---@return lc.Cache.Question |
| 86 | -function problemlist.get_by_title_slug(title_slug) | |
| 87 | - local problems = problemlist.get() | |
| 89 | +function Problemlist.get_by_title_slug(title_slug) | |
| 90 | + local problems = Problemlist.get() | |
| 88 | 91 | return vim.tbl_filter(function(e) return e.title_slug == title_slug end, problems)[1] or {} |
| 89 | 92 | end |
| 90 | 93 | |
| 91 | ----@param problems_str string | |
| 94 | +---@param str string | |
| 92 | 95 | --- |
| 93 | 96 | ---@return lc.Cache.Question[] |
| 94 | -function problemlist.parse(problems_str) | |
| 95 | - ---@type boolean, lc.Cache.Question[] | |
| 96 | - local ok, problems = pcall(vim.json.decode, problems_str) | |
| 97 | - assert(ok, "Failed to parse problems") | |
| 98 | - | |
| 99 | - return problems | |
| 97 | +function Problemlist.parse(str) | |
| 98 | + return vim.json.decode(str) ---@diagnostic disable-line | |
| 100 | 99 | end |
| 101 | 100 | |
| 102 | 101 | ---@param title_slug string |
| 103 | 102 | ---@param status "ac" | "notac" |
| 104 | -function problemlist.change_status(title_slug, status) | |
| 105 | - local problist = problemlist.get() | |
| 103 | +function Problemlist.change_status(title_slug, status) | |
| 104 | + local problist = Problemlist.get() | |
| 106 | 105 | |
| 107 | - problemlist.write(vim.tbl_map(function(p) | |
| 106 | + Problemlist.write(vim.tbl_map(function(p) | |
| 108 | 107 | if p.title_slug == title_slug then p.status = status end |
| 109 | 108 | return p |
| 110 | 109 | end, problist)) |
| 111 | 110 | end |
| 112 | 111 | |
| 113 | -function problemlist.delete() file:rm() end | |
| 112 | +function Problemlist.delete() | |
| 113 | + if not file:exists() then return false end | |
| 114 | + return pcall(path.rm, file) | |
| 115 | +end | |
| 114 | 116 | |
| 115 | -return problemlist | |
| 117 | +return Problemlist |
+13-2lua/leetcode/config/template.lua
| @@ -87,9 +87,20 @@ local M = { | ||
| 87 | 87 | ---@type boolean |
| 88 | 88 | image_support = false, -- setting this to `true` will disable question description wrap |
| 89 | 89 | |
| 90 | - cn = { | |
| 91 | - translate = true, ---@type boolean | |
| 90 | + cache = { | |
| 91 | + ---@type integer | |
| 92 | + update_interval = 60 * 60 * 24 * 7, -- 7 days | |
| 92 | 93 | }, |
| 94 | + | |
| 95 | + plugins = { | |
| 96 | + cn = { | |
| 97 | + enabled = true, | |
| 98 | + translate_problems = true, ---@type boolean | |
| 99 | + }, | |
| 100 | + }, | |
| 101 | + -- cn = { | |
| 102 | + -- translate = true, ---@type boolean | |
| 103 | + -- }, | |
| 93 | 104 | } |
| 94 | 105 | |
| 95 | 106 | return M |
+2-1lua/leetcode/pickers/language.lua
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | local log = require("leetcode.logger") |
| 2 | 2 | local Question = require("leetcode.ui.question") |
| 3 | 3 | local utils = require("leetcode.utils") |
| 4 | +local t = require("leetcode.translator") | |
| 4 | 5 | |
| 5 | 6 | local pickers = require("telescope.pickers") |
| 6 | 7 | local finders = require("telescope.finders") |
| @@ -69,7 +70,7 @@ local opts = require("telescope.themes").get_dropdown() | ||
| 69 | 70 | M.pick_lang = function(question, callback) |
| 70 | 71 | pickers |
| 71 | 72 | .new(opts, { |
| 72 | - prompt_title = "Available Languages", | |
| 73 | + prompt_title = t("Available Languages"), | |
| 73 | 74 | finder = finders.new_table({ |
| 74 | 75 | results = question.q.code_snippets, |
| 75 | 76 | entry_maker = entry_maker, |
+4-2lua/leetcode/pickers/question.lua
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | local log = require("leetcode.logger") |
| 2 | 2 | local t = require("leetcode.translator") |
| 3 | +local utils = require("leetcode.utils") | |
| 3 | 4 | |
| 4 | 5 | local Question = require("leetcode.ui.question") |
| 5 | 6 | |
| @@ -19,7 +20,7 @@ local function question_formatter(question) | ||
| 19 | 20 | return ("%s. %s %s %s"):format( |
| 20 | 21 | tostring(question.frontend_id), |
| 21 | 22 | question.title, |
| 22 | - question.sec_title, | |
| 23 | + question.title_cn, | |
| 23 | 24 | question.title_slug |
| 24 | 25 | ) |
| 25 | 26 | end |
| @@ -55,7 +56,8 @@ end | ||
| 55 | 56 | local function display_question(question) |
| 56 | 57 | local ac_rate = { ("%.1f%%"):format(question.ac_rate), "leetcode_ref" } |
| 57 | 58 | local index = { question.frontend_id .. ".", "leetcode_normal" } |
| 58 | - local title = { question.title } | |
| 59 | + | |
| 60 | + local title = { utils.translate(question.title, question.title_cn) } | |
| 59 | 61 | |
| 60 | 62 | return unpack({ index, title, ac_rate }) |
| 61 | 63 | end |
+1-0lua/leetcode/translator/init.lua
| @@ -36,6 +36,7 @@ local translate = { | ||
| 36 | 36 | ["no hints available"] = "没有提示可用", |
| 37 | 37 | ["drawn question is for premium users only. please try again"] = "抽选的问题仅供高级用户使用。请再试一次。", |
| 38 | 38 | ["please verify your email address in order to use your account"] = "请验证您的电子邮件地址以便使用您的账户", |
| 39 | + ["available languages"] = "可用语言", | |
| 39 | 40 | ["languages"] = "语言", |
| 40 | 41 | ["skills"] = "技能", |
| 41 | 42 | ["runtime"] = "时间", |
+4-4lua/leetcode/ui/description.lua
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | local config = require("leetcode.config") |
| 2 | 2 | local log = require("leetcode.logger") |
| 3 | +local utils = require("leetcode.utils") | |
| 3 | 4 | |
| 4 | 5 | local img_ok, image_api = pcall(require, "image") |
| 5 | 6 | local img_sup = img_ok and config.user.image_support |
| @@ -17,8 +18,6 @@ local Split = require("nui.split") | ||
| 17 | 18 | ---@class lc.Description |
| 18 | 19 | ---@field split NuiSplit |
| 19 | 20 | ---@field parent lc.Question |
| 20 | ----@field content any | |
| 21 | ----@field title any | |
| 22 | 21 | ---@field layout lc-ui.Layout |
| 23 | 22 | ---@field visible boolean |
| 24 | 23 | ---@field images table<string, Image> |
| @@ -125,7 +124,8 @@ function description:populate() | ||
| 125 | 124 | |
| 126 | 125 | local titleline = NuiLine() |
| 127 | 126 | titleline:append(q.frontend_id .. ". ", "leetcode_normal") |
| 128 | - titleline:append(q.title) | |
| 127 | + | |
| 128 | + titleline:append(utils.translate(q.title, q.translated_title)) | |
| 129 | 129 | |
| 130 | 130 | local statsline = NuiLine() |
| 131 | 131 | statsline:append( |
| @@ -155,7 +155,7 @@ function description:populate() | ||
| 155 | 155 | local statscomp = Text:init({ statsline }, { position = "center" }) |
| 156 | 156 | local linkcomp = Text:init({ linkline }, { position = "center" }) |
| 157 | 157 | |
| 158 | - local contents = parser:parse(q.content) | |
| 158 | + local contents = parser:parse(utils.translate(q.content, q.translated_content)) | |
| 159 | 159 | |
| 160 | 160 | self.layout = Layout:init({ |
| 161 | 161 | linkcomp, |
+14-0lua/leetcode/utils.lua
| @@ -141,4 +141,18 @@ function utils.log_err(err) | ||
| 141 | 141 | end |
| 142 | 142 | end |
| 143 | 143 | |
| 144 | +---@param content string | |
| 145 | +---@param translated_content string | |
| 146 | +function utils.translate(content, translated_content) | |
| 147 | + if config.is_cn then | |
| 148 | + if config.user.plugins.cn.translate_problems then | |
| 149 | + return translated_content or content | |
| 150 | + else | |
| 151 | + return content or translated_content | |
| 152 | + end | |
| 153 | + else | |
| 154 | + return content | |
| 155 | + end | |
| 156 | +end | |
| 157 | + | |
| 144 | 158 | return utils |