From d54f9da6090cd6309899ae37777982b4442edae9 Mon Sep 17 00:00:00 2001 From: LukaszMoskwaCTW Date: Thu, 27 Jun 2024 16:05:37 +0900 Subject: [PATCH 1/2] feat: add favorite lists --- lua/leetcode-ui/group/page/problems.lua | 33 ++- lua/leetcode-ui/types.lua | 1 + lua/leetcode/api/problems.lua | 205 +++++++------ lua/leetcode/api/queries.lua | 52 ++++ lua/leetcode/api/types.lua | 9 + lua/leetcode/api/utils.lua | 377 +++++++++++++----------- lua/leetcode/command/init.lua | 24 ++ lua/leetcode/pickers/favorite.lua | 82 ++++++ 8 files changed, 505 insertions(+), 278 deletions(-) create mode 100644 lua/leetcode/pickers/favorite.lua diff --git a/lua/leetcode-ui/group/page/problems.lua b/lua/leetcode-ui/group/page/problems.lua index 8e0cfcdc..c8b4e226 100644 --- a/lua/leetcode-ui/group/page/problems.lua +++ b/lua/leetcode-ui/group/page/problems.lua @@ -16,30 +16,37 @@ page:insert(header) page:insert(Title({ "Menu" }, "Problems")) local list = Button("List", { - icon = "", - sc = "p", - on_press = cmd.problems, + icon = "", + sc = "p", + on_press = cmd.problems, +}) + +local favorite = Button("Favorite", { + icon = "★", + sc = "f", + on_press = cmd.favorite_list, }) local random = Button("Random", { - icon = "", - sc = "r", - on_press = cmd.random_question, + icon = "", + sc = "r", + on_press = cmd.random_question, }) local daily = Button("Daily", { - icon = "󰃭", - sc = "d", - on_press = cmd.qot, + icon = "󰃭", + sc = "d", + on_press = cmd.qot, }) local back = BackButton("menu") page:insert(Buttons({ - list, - random, - daily, - back, + list, + favorite, + random, + daily, + back, })) page:insert(footer) diff --git a/lua/leetcode-ui/types.lua b/lua/leetcode-ui/types.lua index c2536ec0..3bf0d639 100644 --- a/lua/leetcode-ui/types.lua +++ b/lua/leetcode-ui/types.lua @@ -32,6 +32,7 @@ ---| "cache" ---| "signin" ---| "loading" +---| "favorite" -------------------------------------------- --- Leet Component diff --git a/lua/leetcode/api/problems.lua b/lua/leetcode/api/problems.lua index 9049bb8e..799a8e60 100644 --- a/lua/leetcode/api/problems.lua +++ b/lua/leetcode/api/problems.lua @@ -14,121 +14,150 @@ local Problems = {} --- ---@return lc.cache.Question[], lc.err function Problems.all(cb, noti) - local endpoint = urls.problems:format("algorithms") + local endpoint = urls.problems:format("algorithms") - local spinner - if noti then - spinner = Spinner:init("updating problemlist cache...", "points") - end + local spinner + if noti then + spinner = Spinner:init("updating problemlist cache...", "points") + end - if cb then - utils.get(endpoint, { - callback = function(res, err) - if err then - if spinner then - spinner:stop(err.msg, false) - end - return cb(nil, err) - end - - local problems = utils.normalize_problems(res.stat_status_pairs) - - if config.is_cn then - if spinner then - spinner:update("fetching title translations") - end - Problems.translated_titles(function(titles, terr) - if terr then - if spinner then - spinner:stop(terr.msg, false) - end - return cb(nil, terr) - end - - problems = utils.translate_titles(problems, titles) - if spinner then - spinner:stop("cache updated") - end - - cb(problems) - end) - else - if spinner then - spinner:stop("cache updated") - end - - cb(problems) - end - end, - }) - else - local res, err = utils.get(endpoint) + if cb then + utils.get(endpoint, { + callback = function(res, err) if err then - if spinner then - spinner:stop(err.msg, false) - end - return nil, err + if spinner then + spinner:stop(err.msg, false) + end + return cb(nil, err) end local problems = utils.normalize_problems(res.stat_status_pairs) if config.is_cn then - local titles, terr = Problems.translated_titles() + if spinner then + spinner:update("fetching title translations") + end + Problems.translated_titles(function(titles, terr) if terr then - if spinner then - spinner:stop(terr.msg, false) - end - return nil, terr + if spinner then + spinner:stop(terr.msg, false) + end + return cb(nil, terr) end + problems = utils.translate_titles(problems, titles) if spinner then - spinner:stop("problems cache updated") + spinner:stop("cache updated") end - return utils.translate_titles(problems, titles) + + cb(problems) + end) else - if spinner then - spinner:stop("problems cache updated") - end - return problems + if spinner then + spinner:stop("cache updated") + end + + cb(problems) + end + end, + }) + else + local res, err = utils.get(endpoint) + if err then + if spinner then + spinner:stop(err.msg, false) + end + return nil, err + end + + local problems = utils.normalize_problems(res.stat_status_pairs) + + if config.is_cn then + local titles, terr = Problems.translated_titles() + if terr then + if spinner then + spinner:stop(terr.msg, false) end + return nil, terr + end + + if spinner then + spinner:stop("problems cache updated") + end + return utils.translate_titles(problems, titles) + else + if spinner then + spinner:stop("problems cache updated") + end + return problems end + end end function Problems.question_of_today(cb) - local query = queries.qot + local query = queries.qot + + utils.query(query, {}, { + callback = function(res, err) + if err then + return cb(nil, err) + end + + local tday_record = res.data["todayRecord"] + local question = config.is_cn and tday_record[1].question or tday_record.question + cb(question) + end, + }) +end - utils.query(query, {}, { - callback = function(res, err) - if err then - return cb(nil, err) - end +function Problems.favorite_list(cb) + local query = queries.favorite_list - local tday_record = res.data["todayRecord"] - local question = config.is_cn and tday_record[1].question or tday_record.question - cb(question) - end, - }) + utils.query(query, {}, { + callback = function(res, err) + if err then + return cb(nil, err) + end + + local data = res.data + cb(data.myCollectedFavoriteList.favorites) + end, + }) +end + +function Problems.favorite_question_list(favorite_slug, cb) + local query = queries.favorite_question_list + + utils.query(query, { favoriteSlug = favorite_slug }, { + callback = function(res, err) + if err then + return cb(nil, err) + end + local normalized = utils.normalize_favorites(res.data.favoriteQuestionList.questions) + cb(normalized) + end, + }) end function Problems.translated_titles(cb) - local query = queries.translations - - if cb then - utils.query(query, {}, { - callback = function(res, err) - if err then - return cb(nil, err) - end - cb(res.data.translations) - end, - }) - else - local res, err = utils.query(query, {}) + local query = queries.translations + + if cb then + utils.query(query, {}, { + callback = function(res, err) if err then - return nil, err + return cb(nil, err) end - return res.data.translations + cb(res.data.translations) + end, + }) + else + local res, err = utils.query(query, {}) + if err then + return nil, err end + return res.data.translations + end end return Problems diff --git a/lua/leetcode/api/queries.lua b/lua/leetcode/api/queries.lua index b8356208..7ea07016 100644 --- a/lua/leetcode/api/queries.lua +++ b/lua/leetcode/api/queries.lua @@ -179,4 +179,56 @@ queries.session_progress = [[ } ]] +queries.favorite_list = [[ + query myFavoriteList { + myCreatedFavoriteList { + favorites { + coverUrl + coverEmoji + coverBackgroundColor + hasCurrentQuestion + isPublicFavorite + lastQuestionAddedAt + name + slug + } + hasMore + totalLength + } + myCollectedFavoriteList { + hasMore + totalLength + favorites { + coverUrl + coverEmoji + coverBackgroundColor + hasCurrentQuestion + isPublicFavorite + name + slug + lastQuestionAddedAt + } + } + } + ]] + +queries.favorite_question_list = [[ + query favoriteQuestionList($favoriteSlug: String!) { + favoriteQuestionList(favoriteSlug: $favoriteSlug) { + questions { + id + questionFrontendId + paidOnly + title + titleSlug + difficulty + topicTags { + name + slug + } + } + } + } + ]] + return queries diff --git a/lua/leetcode/api/types.lua b/lua/leetcode/api/types.lua index 7cd2f84d..98f0c445 100644 --- a/lua/leetcode/api/types.lua +++ b/lua/leetcode/api/types.lua @@ -393,6 +393,15 @@ ---@field lvl integer ---@alias lc.err lc.Api.err|nil +-- +-------------------------------------------- +--- Favorite +-------------------------------------------- +---@class lc.Favorite +---@field name string +---@field slug string +---@field isPublicFavorite boolean +---@field hasCurrentQuestion boolean -------------------------------------------- --- Sessions diff --git a/lua/leetcode/api/utils.lua b/lua/leetcode/api/utils.lua index 01210743..d3ef6461 100644 --- a/lua/leetcode/api/utils.lua +++ b/lua/leetcode/api/utils.lua @@ -9,243 +9,266 @@ local utils = {} ---@param endpoint string function utils.put(endpoint, opts) - local options = vim.tbl_deep_extend("force", { - endpoint = endpoint, - }, opts or {}) + local options = vim.tbl_deep_extend("force", { + endpoint = endpoint, + }, opts or {}) - return utils.curl("put", options) + return utils.curl("put", options) end ---@param endpoint string function utils.post(endpoint, opts) - local options = vim.tbl_deep_extend("force", { - endpoint = endpoint, - }, opts or {}) + local options = vim.tbl_deep_extend("force", { + endpoint = endpoint, + }, opts or {}) - return utils.curl("post", options) + return utils.curl("post", options) end ---@param endpoint string ---@param opts? table function utils.get(endpoint, opts) - local options = vim.tbl_deep_extend("force", { - endpoint = endpoint, - }, opts or {}) + local options = vim.tbl_deep_extend("force", { + endpoint = endpoint, + }, opts or {}) - return utils.curl("get", options) + return utils.curl("get", options) end ---@param query string ---@param variables? table optional ---@param opts? { callback?: function, endpoint?: string } function utils.query(query, variables, opts) - opts = vim.tbl_deep_extend("force", { - body = { - query = query, - variables = variables, - }, - }, opts or {}) - - return utils.curl("post", opts) + opts = vim.tbl_deep_extend("force", { + body = { + query = query, + variables = variables, + }, + }, opts or {}) + + return utils.curl("post", opts) end ---@private ---@param method string ---@param params table function utils.curl(method, params) - local params_cpy = vim.deepcopy(params) - - params = vim.tbl_deep_extend("force", { - headers = headers.get(), - compressed = false, - retry = 5, - endpoint = urls.base, - http_version = "HTTP/2", - }, params or {}) - local url = ("https://leetcode.%s%s"):format(config.domain, params.endpoint) - - if type(params.body) == "table" then - params.body = vim.json.encode(params.body) + local params_cpy = vim.deepcopy(params) + + params = vim.tbl_deep_extend("force", { + headers = headers.get(), + compressed = false, + retry = 5, + endpoint = urls.base, + http_version = "HTTP/2", + }, params or {}) + local url = ("https://leetcode.%s%s"):format(config.domain, params.endpoint) + + if type(params.body) == "table" then + params.body = vim.json.encode(params.body) + end + + local tries = params.retry + local function should_retry(err) + return err and err.status >= 500 and tries > 0 + end + + if params.callback then + local cb = vim.schedule_wrap(params.callback) + params.callback = function(out, _) + local res, err = utils.handle_res(out) + + if should_retry(err) then + log.debug("retry " .. tries) + params_cpy.retry = tries - 1 + utils.curl(method, params_cpy) + else + cb(res, err) + end end - local tries = params.retry - local function should_retry(err) - return err and err.status >= 500 and tries > 0 - end + curl[method](url, params) + else + local out = curl[method](url, params) + local res, err = utils.handle_res(out) - if params.callback then - local cb = vim.schedule_wrap(params.callback) - params.callback = function(out, _) - local res, err = utils.handle_res(out) - - if should_retry(err) then - log.debug("retry " .. tries) - params_cpy.retry = tries - 1 - utils.curl(method, params_cpy) - else - cb(res, err) - end - end - - curl[method](url, params) + if should_retry(err) then + log.debug("retry " .. tries) + params_cpy.retry = tries - 1 + utils.curl(method, params_cpy) else - local out = curl[method](url, params) - local res, err = utils.handle_res(out) - - if should_retry(err) then - log.debug("retry " .. tries) - params_cpy.retry = tries - 1 - utils.curl(method, params_cpy) - else - return res, err - end + return res, err end + end end ---@private ---@return table, lc.err function utils.handle_res(out) - local res, err - log.debug(out) - - if out.exit ~= 0 then - err = { - code = out.exit, - msg = "curl failed", - } - elseif out.status >= 300 then - local ok, msg = pcall(function() - local dec = utils.decode(out.body) - - if dec.error then - return dec.error - end - - local tbl = {} - for _, e in ipairs(dec.errors) do - table.insert(tbl, e.message) - end - - return table.concat(tbl, "\n") - end) - - res = out.body - err = { - code = 0, - status = out.status, - response = out, - msg = "http error " .. out.status .. (ok and ("\n\n" .. msg) or ""), - out = out.body, - } - else - res = utils.decode(out.body) - end - - return res, utils.check_err(err) + local res, err + log.debug(out) + + if out.exit ~= 0 then + err = { + code = out.exit, + msg = "curl failed", + } + elseif out.status >= 300 then + local ok, msg = pcall(function() + local dec = utils.decode(out.body) + + if dec.error then + return dec.error + end + + local tbl = {} + for _, e in ipairs(dec.errors) do + table.insert(tbl, e.message) + end + + return table.concat(tbl, "\n") + end) + + res = out.body + err = { + code = 0, + status = out.status, + response = out, + msg = "http error " .. out.status .. (ok and ("\n\n" .. msg) or ""), + out = out.body, + } + else + res = utils.decode(out.body) + end + + return res, utils.check_err(err) end ---@param err lc.err function utils.check_err(err) - if not err then - return - end - - if err.status then - if err.status == 401 or err.status == 403 then - require("leetcode.command").expire() - err.msg = "Session expired? Enter a new cookie to keep using `leetcode.nvim`" - end + if not err then + return + end + + if err.status then + if err.status == 401 or err.status == 403 then + require("leetcode.command").expire() + err.msg = "Session expired? Enter a new cookie to keep using `leetcode.nvim`" end + end - return err + return err end function utils.decode(str) - return vim.json.decode(str) - -- local ok, res = pcall(vim.json.decode, str) - -- assert(ok, str) - -- return res + return vim.json.decode(str) + -- local ok, res = pcall(vim.json.decode, str) + -- assert(ok, str) + -- return res end function utils.normalize_similar_cn(s) - s = select(2, pcall(utils.decode, s)) - - return vim.tbl_map(function(sq) - return { - title = sq.title, - translated_title = sq.translatedTitle, - paid_only = sq.isPaidOnly, - title_slug = sq.titleSlug, - difficulty = sq.difficulty, - } - end, s) + s = select(2, pcall(utils.decode, s)) + + return vim.tbl_map(function(sq) + return { + title = sq.title, + translated_title = sq.translatedTitle, + paid_only = sq.isPaidOnly, + title_slug = sq.titleSlug, + difficulty = sq.difficulty, + } + end, s) end function utils.lvl_to_name(lvl) - return ({ "Easy", "Medium", "Hard" })[lvl] + return ({ "Easy", "Medium", "Hard" })[lvl] +end + +function utils.normalize_favorites(questions) + local diff = { + EASY = 1, + MEDIUM = 2, + HARD = 3, + } + return vim.tbl_map(function(q) + return { + id = q.id, + frontend_id = tonumber(q.questionFrontendId), + title = q.title, + title_slug = q.titleSlug, + title_cn = "", + paid_only = q.paidOnly, + link = ("https://leetcode.%s/problems/%s/"):format(config.domain, q.titleSlug), + ac_rate = 0, + difficulty = utils.lvl_to_name(diff[q.difficulty]), + starred = true, + topic_tags = {}, + } + end, questions) end ---@return lc.cache.Question[] function utils.normalize_problems(problems) - problems = vim.tbl_filter(function(p) - return not p.stat.question__hide - end, problems) - - local comp = function(a, b) - local a_fid = a.stat.frontend_question_id - local b_fid = b.stat.frontend_question_id - - local is_num_a = tonumber(a_fid) - local is_num_b = tonumber(b_fid) - - if is_num_a and is_num_b then - return tonumber(a_fid) < tonumber(b_fid) - elseif is_num_a then - return true - elseif is_num_b then - return false - else - return a_fid < b_fid - end + problems = vim.tbl_filter(function(p) + return not p.stat.question__hide + end, problems) + + local comp = function(a, b) + local a_fid = a.stat.frontend_question_id + local b_fid = b.stat.frontend_question_id + + local is_num_a = tonumber(a_fid) + local is_num_b = tonumber(b_fid) + + if is_num_a and is_num_b then + return tonumber(a_fid) < tonumber(b_fid) + elseif is_num_a then + return true + elseif is_num_b then + return false + else + return a_fid < b_fid end - table.sort(problems, comp) - - return vim.tbl_map(function(p) - return { - status = p.status, - id = p.stat.question_id, - frontend_id = p.stat.frontend_question_id, - title = p.stat.question__title, - title_cn = "", - title_slug = p.stat.question__title_slug, - link = ("https://leetcode.%s/problems/%s/"):format( - config.domain, - p.stat.question__title_slug - ), - paid_only = p.paid_only, - ac_rate = p.stat.total_acs * 100 / math.max(p.stat.total_submitted, 1), - difficulty = utils.lvl_to_name(p.difficulty.level), - starred = p.is_favor, - topic_tags = {}, - } - end, problems) + end + table.sort(problems, comp) + + return vim.tbl_map(function(p) + return { + status = p.status, + id = p.stat.question_id, + frontend_id = p.stat.frontend_question_id, + title = p.stat.question__title, + title_cn = "", + title_slug = p.stat.question__title_slug, + link = ("https://leetcode.%s/problems/%s/"):format( + config.domain, + p.stat.question__title_slug + ), + paid_only = p.paid_only, + ac_rate = p.stat.total_acs * 100 / math.max(p.stat.total_submitted, 1), + difficulty = utils.lvl_to_name(p.difficulty.level), + starred = p.is_favor, + topic_tags = {}, + } + end, problems) end ---@param problems lc.cache.Question[] ---@param titles { questionId: integer, title: string }[] function utils.translate_titles(problems, titles) - local map = {} - for _, title in ipairs(titles) do - map[title.questionId] = title.title + local map = {} + for _, title in ipairs(titles) do + map[title.questionId] = title.title + end + + return vim.tbl_map(function(p) + local title = map[tostring(p.id)] + if title then + p.title_cn = title end - - return vim.tbl_map(function(p) - local title = map[tostring(p.id)] - if title then - p.title_cn = title - end - return p - end, problems) + return p + end, problems) end return utils diff --git a/lua/leetcode/command/init.lua b/lua/leetcode/command/init.lua index fd56ae71..cf648960 100644 --- a/lua/leetcode/command/init.lua +++ b/lua/leetcode/command/init.lua @@ -140,6 +140,30 @@ function cmd.qot() end) end +function cmd.favorite_list() + require("leetcode.utils").auth_guard() + + local problems = require("leetcode.api.problems") + + problems.favorite_list(function(favorites, fav_err) + if fav_err then + return log.err(fav_err) + end + require("leetcode.pickers.favorite").pick(favorites, function(selection) + if not selection then + return + end + local slug = selection.slug + problems.favorite_question_list(slug, function(data, fav_q_err) + if fav_q_err then + return log.err(fav_q_err) + end + require("leetcode.pickers.question").pick(data) + end) + end) + end) +end + function cmd.random_question(opts) require("leetcode.utils").auth_guard() diff --git a/lua/leetcode/pickers/favorite.lua b/lua/leetcode/pickers/favorite.lua new file mode 100644 index 00000000..112c861c --- /dev/null +++ b/lua/leetcode/pickers/favorite.lua @@ -0,0 +1,82 @@ +local log = require("leetcode.logger") +local t = require("leetcode.translator") + +local pickers = require("telescope.pickers") +local finders = require("telescope.finders") +local conf = require("telescope.config").values +local config = require("leetcode.config") + +local entry_display = require("telescope.pickers.entry_display") +local actions = require("telescope.actions") +local action_state = require("telescope.actions.state") + +---@param f lc.Favorite +--- +---@return string +local function favorite_formatter(f) + return string.format("%s - %s", f.slug, f.name) +end + + +---@param favorite lc.Favorite +local function display_favorite(favorite) + return { + favorite.name, + favorite.slug, + } +end + +local displayer = entry_display.create({ + separator = " ", + items = { + { remaining = true }, + }, +}) + +-- @param entry lc.Favorite +local function make_display(entry) + ---@type lc.Favorite + local f = entry.value + + return displayer({ + display_favorite(f), + }) +end + +-- @param entry lc.Favorite +local function entry_maker(entry) + return { + value = entry, + display = make_display, + ordinal = favorite_formatter(entry), + } +end + +local opts = require("telescope.themes").get_dropdown() + +return { + pick = function(favorites, cb) + pickers + .new(opts, { + prompt_title = t("Select a Favorite List"), + finder = finders.new_table({ + results = favorites, + entry_maker = entry_maker, + }), + sorter = conf.generic_sorter(theme), + attach_mappings = function(prompt_bufnr) + actions.select_default:replace(function() + actions.close(prompt_bufnr) + local selection = action_state.get_selected_entry() + + if not selection then + return + end + cb(selection.value) + end) + return true + end, + }) + :find() + end, +} From 594853df9ac0a57cb41926e27c5a2e6b7004e31a Mon Sep 17 00:00:00 2001 From: LukaszMoskwaCTW Date: Thu, 27 Jun 2024 16:25:24 +0900 Subject: [PATCH 2/2] fix: formatting --- lua/leetcode-ui/group/page/problems.lua | 34 +-- lua/leetcode/api/problems.lua | 226 +++++++------- lua/leetcode/api/utils.lua | 390 ++++++++++++------------ lua/leetcode/command/init.lua | 34 +-- lua/leetcode/pickers/favorite.lua | 87 +++--- 5 files changed, 384 insertions(+), 387 deletions(-) diff --git a/lua/leetcode-ui/group/page/problems.lua b/lua/leetcode-ui/group/page/problems.lua index c8b4e226..72d12c4f 100644 --- a/lua/leetcode-ui/group/page/problems.lua +++ b/lua/leetcode-ui/group/page/problems.lua @@ -16,37 +16,37 @@ page:insert(header) page:insert(Title({ "Menu" }, "Problems")) local list = Button("List", { - icon = "", - sc = "p", - on_press = cmd.problems, + icon = "", + sc = "p", + on_press = cmd.problems, }) local favorite = Button("Favorite", { - icon = "★", - sc = "f", - on_press = cmd.favorite_list, + icon = "★", + sc = "f", + on_press = cmd.favorite_list, }) local random = Button("Random", { - icon = "", - sc = "r", - on_press = cmd.random_question, + icon = "", + sc = "r", + on_press = cmd.random_question, }) local daily = Button("Daily", { - icon = "󰃭", - sc = "d", - on_press = cmd.qot, + icon = "󰃭", + sc = "d", + on_press = cmd.qot, }) local back = BackButton("menu") page:insert(Buttons({ - list, - favorite, - random, - daily, - back, + list, + favorite, + random, + daily, + back, })) page:insert(footer) diff --git a/lua/leetcode/api/problems.lua b/lua/leetcode/api/problems.lua index 799a8e60..b34f57cd 100644 --- a/lua/leetcode/api/problems.lua +++ b/lua/leetcode/api/problems.lua @@ -14,150 +14,150 @@ local Problems = {} --- ---@return lc.cache.Question[], lc.err function Problems.all(cb, noti) - local endpoint = urls.problems:format("algorithms") + local endpoint = urls.problems:format("algorithms") - local spinner - if noti then - spinner = Spinner:init("updating problemlist cache...", "points") - end + local spinner + if noti then + spinner = Spinner:init("updating problemlist cache...", "points") + end - if cb then - utils.get(endpoint, { - callback = function(res, err) + if cb then + utils.get(endpoint, { + callback = function(res, err) + if err then + if spinner then + spinner:stop(err.msg, false) + end + return cb(nil, err) + end + + local problems = utils.normalize_problems(res.stat_status_pairs) + + if config.is_cn then + if spinner then + spinner:update("fetching title translations") + end + Problems.translated_titles(function(titles, terr) + if terr then + if spinner then + spinner:stop(terr.msg, false) + end + return cb(nil, terr) + end + + problems = utils.translate_titles(problems, titles) + if spinner then + spinner:stop("cache updated") + end + + cb(problems) + end) + else + if spinner then + spinner:stop("cache updated") + end + + cb(problems) + end + end, + }) + else + local res, err = utils.get(endpoint) if err then - if spinner then - spinner:stop(err.msg, false) - end - return cb(nil, err) + if spinner then + spinner:stop(err.msg, false) + end + return nil, err end local problems = utils.normalize_problems(res.stat_status_pairs) if config.is_cn then - if spinner then - spinner:update("fetching title translations") - end - Problems.translated_titles(function(titles, terr) + local titles, terr = Problems.translated_titles() if terr then - if spinner then - spinner:stop(terr.msg, false) - end - return cb(nil, terr) + if spinner then + spinner:stop(terr.msg, false) + end + return nil, terr end - problems = utils.translate_titles(problems, titles) if spinner then - spinner:stop("cache updated") + spinner:stop("problems cache updated") end - - cb(problems) - end) + return utils.translate_titles(problems, titles) else - if spinner then - spinner:stop("cache updated") - end - - cb(problems) - end - end, - }) - else - local res, err = utils.get(endpoint) - if err then - if spinner then - spinner:stop(err.msg, false) - end - return nil, err - end - - local problems = utils.normalize_problems(res.stat_status_pairs) - - if config.is_cn then - local titles, terr = Problems.translated_titles() - if terr then - if spinner then - spinner:stop(terr.msg, false) + if spinner then + spinner:stop("problems cache updated") + end + return problems end - return nil, terr - end - - if spinner then - spinner:stop("problems cache updated") - end - return utils.translate_titles(problems, titles) - else - if spinner then - spinner:stop("problems cache updated") - end - return problems end - end end function Problems.question_of_today(cb) - local query = queries.qot - - utils.query(query, {}, { - callback = function(res, err) - if err then - return cb(nil, err) - end - - local tday_record = res.data["todayRecord"] - local question = config.is_cn and tday_record[1].question or tday_record.question - cb(question) - end, - }) + local query = queries.qot + + utils.query(query, {}, { + callback = function(res, err) + if err then + return cb(nil, err) + end + + local tday_record = res.data["todayRecord"] + local question = config.is_cn and tday_record[1].question or tday_record.question + cb(question) + end, + }) end function Problems.favorite_list(cb) - local query = queries.favorite_list - - utils.query(query, {}, { - callback = function(res, err) - if err then - return cb(nil, err) - end - - local data = res.data - cb(data.myCollectedFavoriteList.favorites) - end, - }) + local query = queries.favorite_list + + utils.query(query, {}, { + callback = function(res, err) + if err then + return cb(nil, err) + end + + local data = res.data + cb(data.myCollectedFavoriteList.favorites) + end, + }) end function Problems.favorite_question_list(favorite_slug, cb) - local query = queries.favorite_question_list - - utils.query(query, { favoriteSlug = favorite_slug }, { - callback = function(res, err) - if err then - return cb(nil, err) - end - local normalized = utils.normalize_favorites(res.data.favoriteQuestionList.questions) - cb(normalized) - end, - }) + local query = queries.favorite_question_list + + utils.query(query, { favoriteSlug = favorite_slug }, { + callback = function(res, err) + if err then + return cb(nil, err) + end + local normalized = utils.normalize_favorites(res.data.favoriteQuestionList.questions) + cb(normalized) + end, + }) end function Problems.translated_titles(cb) - local query = queries.translations - - if cb then - utils.query(query, {}, { - callback = function(res, err) + local query = queries.translations + + if cb then + utils.query(query, {}, { + callback = function(res, err) + if err then + return cb(nil, err) + end + cb(res.data.translations) + end, + }) + else + local res, err = utils.query(query, {}) if err then - return cb(nil, err) + return nil, err end - cb(res.data.translations) - end, - }) - else - local res, err = utils.query(query, {}) - if err then - return nil, err + return res.data.translations end - return res.data.translations - end end return Problems diff --git a/lua/leetcode/api/utils.lua b/lua/leetcode/api/utils.lua index d3ef6461..25d71dd7 100644 --- a/lua/leetcode/api/utils.lua +++ b/lua/leetcode/api/utils.lua @@ -9,266 +9,266 @@ local utils = {} ---@param endpoint string function utils.put(endpoint, opts) - local options = vim.tbl_deep_extend("force", { - endpoint = endpoint, - }, opts or {}) + local options = vim.tbl_deep_extend("force", { + endpoint = endpoint, + }, opts or {}) - return utils.curl("put", options) + return utils.curl("put", options) end ---@param endpoint string function utils.post(endpoint, opts) - local options = vim.tbl_deep_extend("force", { - endpoint = endpoint, - }, opts or {}) + local options = vim.tbl_deep_extend("force", { + endpoint = endpoint, + }, opts or {}) - return utils.curl("post", options) + return utils.curl("post", options) end ---@param endpoint string ---@param opts? table function utils.get(endpoint, opts) - local options = vim.tbl_deep_extend("force", { - endpoint = endpoint, - }, opts or {}) + local options = vim.tbl_deep_extend("force", { + endpoint = endpoint, + }, opts or {}) - return utils.curl("get", options) + return utils.curl("get", options) end ---@param query string ---@param variables? table optional ---@param opts? { callback?: function, endpoint?: string } function utils.query(query, variables, opts) - opts = vim.tbl_deep_extend("force", { - body = { - query = query, - variables = variables, - }, - }, opts or {}) - - return utils.curl("post", opts) + opts = vim.tbl_deep_extend("force", { + body = { + query = query, + variables = variables, + }, + }, opts or {}) + + return utils.curl("post", opts) end ---@private ---@param method string ---@param params table function utils.curl(method, params) - local params_cpy = vim.deepcopy(params) - - params = vim.tbl_deep_extend("force", { - headers = headers.get(), - compressed = false, - retry = 5, - endpoint = urls.base, - http_version = "HTTP/2", - }, params or {}) - local url = ("https://leetcode.%s%s"):format(config.domain, params.endpoint) - - if type(params.body) == "table" then - params.body = vim.json.encode(params.body) - end - - local tries = params.retry - local function should_retry(err) - return err and err.status >= 500 and tries > 0 - end - - if params.callback then - local cb = vim.schedule_wrap(params.callback) - params.callback = function(out, _) - local res, err = utils.handle_res(out) - - if should_retry(err) then - log.debug("retry " .. tries) - params_cpy.retry = tries - 1 - utils.curl(method, params_cpy) - else - cb(res, err) - end + local params_cpy = vim.deepcopy(params) + + params = vim.tbl_deep_extend("force", { + headers = headers.get(), + compressed = false, + retry = 5, + endpoint = urls.base, + http_version = "HTTP/2", + }, params or {}) + local url = ("https://leetcode.%s%s"):format(config.domain, params.endpoint) + + if type(params.body) == "table" then + params.body = vim.json.encode(params.body) end - curl[method](url, params) - else - local out = curl[method](url, params) - local res, err = utils.handle_res(out) + local tries = params.retry + local function should_retry(err) + return err and err.status >= 500 and tries > 0 + end - if should_retry(err) then - log.debug("retry " .. tries) - params_cpy.retry = tries - 1 - utils.curl(method, params_cpy) + if params.callback then + local cb = vim.schedule_wrap(params.callback) + params.callback = function(out, _) + local res, err = utils.handle_res(out) + + if should_retry(err) then + log.debug("retry " .. tries) + params_cpy.retry = tries - 1 + utils.curl(method, params_cpy) + else + cb(res, err) + end + end + + curl[method](url, params) else - return res, err + local out = curl[method](url, params) + local res, err = utils.handle_res(out) + + if should_retry(err) then + log.debug("retry " .. tries) + params_cpy.retry = tries - 1 + utils.curl(method, params_cpy) + else + return res, err + end end - end end ---@private ---@return table, lc.err function utils.handle_res(out) - local res, err - log.debug(out) - - if out.exit ~= 0 then - err = { - code = out.exit, - msg = "curl failed", - } - elseif out.status >= 300 then - local ok, msg = pcall(function() - local dec = utils.decode(out.body) - - if dec.error then - return dec.error - end - - local tbl = {} - for _, e in ipairs(dec.errors) do - table.insert(tbl, e.message) - end - - return table.concat(tbl, "\n") - end) - - res = out.body - err = { - code = 0, - status = out.status, - response = out, - msg = "http error " .. out.status .. (ok and ("\n\n" .. msg) or ""), - out = out.body, - } - else - res = utils.decode(out.body) - end + local res, err + log.debug(out) + + if out.exit ~= 0 then + err = { + code = out.exit, + msg = "curl failed", + } + elseif out.status >= 300 then + local ok, msg = pcall(function() + local dec = utils.decode(out.body) + + if dec.error then + return dec.error + end + + local tbl = {} + for _, e in ipairs(dec.errors) do + table.insert(tbl, e.message) + end + + return table.concat(tbl, "\n") + end) + + res = out.body + err = { + code = 0, + status = out.status, + response = out, + msg = "http error " .. out.status .. (ok and ("\n\n" .. msg) or ""), + out = out.body, + } + else + res = utils.decode(out.body) + end - return res, utils.check_err(err) + return res, utils.check_err(err) end ---@param err lc.err function utils.check_err(err) - if not err then - return - end - - if err.status then - if err.status == 401 or err.status == 403 then - require("leetcode.command").expire() - err.msg = "Session expired? Enter a new cookie to keep using `leetcode.nvim`" + if not err then + return end - end - return err + if err.status then + if err.status == 401 or err.status == 403 then + require("leetcode.command").expire() + err.msg = "Session expired? Enter a new cookie to keep using `leetcode.nvim`" + end + end + + return err end function utils.decode(str) - return vim.json.decode(str) - -- local ok, res = pcall(vim.json.decode, str) - -- assert(ok, str) - -- return res + return vim.json.decode(str) + -- local ok, res = pcall(vim.json.decode, str) + -- assert(ok, str) + -- return res end function utils.normalize_similar_cn(s) - s = select(2, pcall(utils.decode, s)) - - return vim.tbl_map(function(sq) - return { - title = sq.title, - translated_title = sq.translatedTitle, - paid_only = sq.isPaidOnly, - title_slug = sq.titleSlug, - difficulty = sq.difficulty, - } - end, s) + s = select(2, pcall(utils.decode, s)) + + return vim.tbl_map(function(sq) + return { + title = sq.title, + translated_title = sq.translatedTitle, + paid_only = sq.isPaidOnly, + title_slug = sq.titleSlug, + difficulty = sq.difficulty, + } + end, s) end function utils.lvl_to_name(lvl) - return ({ "Easy", "Medium", "Hard" })[lvl] + return ({ "Easy", "Medium", "Hard" })[lvl] end function utils.normalize_favorites(questions) - local diff = { - EASY = 1, - MEDIUM = 2, - HARD = 3, - } - return vim.tbl_map(function(q) - return { - id = q.id, - frontend_id = tonumber(q.questionFrontendId), - title = q.title, - title_slug = q.titleSlug, - title_cn = "", - paid_only = q.paidOnly, - link = ("https://leetcode.%s/problems/%s/"):format(config.domain, q.titleSlug), - ac_rate = 0, - difficulty = utils.lvl_to_name(diff[q.difficulty]), - starred = true, - topic_tags = {}, + local diff = { + EASY = 1, + MEDIUM = 2, + HARD = 3, } - end, questions) + return vim.tbl_map(function(q) + return { + id = q.id, + frontend_id = tonumber(q.questionFrontendId), + title = q.title, + title_slug = q.titleSlug, + title_cn = "", + paid_only = q.paidOnly, + link = ("https://leetcode.%s/problems/%s/"):format(config.domain, q.titleSlug), + ac_rate = 0, + difficulty = utils.lvl_to_name(diff[q.difficulty]), + starred = true, + topic_tags = {}, + } + end, questions) end ---@return lc.cache.Question[] function utils.normalize_problems(problems) - problems = vim.tbl_filter(function(p) - return not p.stat.question__hide - end, problems) - - local comp = function(a, b) - local a_fid = a.stat.frontend_question_id - local b_fid = b.stat.frontend_question_id - - local is_num_a = tonumber(a_fid) - local is_num_b = tonumber(b_fid) - - if is_num_a and is_num_b then - return tonumber(a_fid) < tonumber(b_fid) - elseif is_num_a then - return true - elseif is_num_b then - return false - else - return a_fid < b_fid + problems = vim.tbl_filter(function(p) + return not p.stat.question__hide + end, problems) + + local comp = function(a, b) + local a_fid = a.stat.frontend_question_id + local b_fid = b.stat.frontend_question_id + + local is_num_a = tonumber(a_fid) + local is_num_b = tonumber(b_fid) + + if is_num_a and is_num_b then + return tonumber(a_fid) < tonumber(b_fid) + elseif is_num_a then + return true + elseif is_num_b then + return false + else + return a_fid < b_fid + end end - end - table.sort(problems, comp) - - return vim.tbl_map(function(p) - return { - status = p.status, - id = p.stat.question_id, - frontend_id = p.stat.frontend_question_id, - title = p.stat.question__title, - title_cn = "", - title_slug = p.stat.question__title_slug, - link = ("https://leetcode.%s/problems/%s/"):format( - config.domain, - p.stat.question__title_slug - ), - paid_only = p.paid_only, - ac_rate = p.stat.total_acs * 100 / math.max(p.stat.total_submitted, 1), - difficulty = utils.lvl_to_name(p.difficulty.level), - starred = p.is_favor, - topic_tags = {}, - } - end, problems) + table.sort(problems, comp) + + return vim.tbl_map(function(p) + return { + status = p.status, + id = p.stat.question_id, + frontend_id = p.stat.frontend_question_id, + title = p.stat.question__title, + title_cn = "", + title_slug = p.stat.question__title_slug, + link = ("https://leetcode.%s/problems/%s/"):format( + config.domain, + p.stat.question__title_slug + ), + paid_only = p.paid_only, + ac_rate = p.stat.total_acs * 100 / math.max(p.stat.total_submitted, 1), + difficulty = utils.lvl_to_name(p.difficulty.level), + starred = p.is_favor, + topic_tags = {}, + } + end, problems) end ---@param problems lc.cache.Question[] ---@param titles { questionId: integer, title: string }[] function utils.translate_titles(problems, titles) - local map = {} - for _, title in ipairs(titles) do - map[title.questionId] = title.title - end - - return vim.tbl_map(function(p) - local title = map[tostring(p.id)] - if title then - p.title_cn = title + local map = {} + for _, title in ipairs(titles) do + map[title.questionId] = title.title end - return p - end, problems) + + return vim.tbl_map(function(p) + local title = map[tostring(p.id)] + if title then + p.title_cn = title + end + return p + end, problems) end return utils diff --git a/lua/leetcode/command/init.lua b/lua/leetcode/command/init.lua index cf648960..ee3ffd18 100644 --- a/lua/leetcode/command/init.lua +++ b/lua/leetcode/command/init.lua @@ -141,27 +141,27 @@ function cmd.qot() end function cmd.favorite_list() - require("leetcode.utils").auth_guard() + require("leetcode.utils").auth_guard() - local problems = require("leetcode.api.problems") + local problems = require("leetcode.api.problems") - problems.favorite_list(function(favorites, fav_err) - if fav_err then - return log.err(fav_err) - end - require("leetcode.pickers.favorite").pick(favorites, function(selection) - if not selection then - return - end - local slug = selection.slug - problems.favorite_question_list(slug, function(data, fav_q_err) - if fav_q_err then - return log.err(fav_q_err) + problems.favorite_list(function(favorites, fav_err) + if fav_err then + return log.err(fav_err) end - require("leetcode.pickers.question").pick(data) - end) + require("leetcode.pickers.favorite").pick(favorites, function(selection) + if not selection then + return + end + local slug = selection.slug + problems.favorite_question_list(slug, function(data, fav_q_err) + if fav_q_err then + return log.err(fav_q_err) + end + require("leetcode.pickers.question").pick(data) + end) + end) end) - end) end function cmd.random_question(opts) diff --git a/lua/leetcode/pickers/favorite.lua b/lua/leetcode/pickers/favorite.lua index 112c861c..8dd0ba8d 100644 --- a/lua/leetcode/pickers/favorite.lua +++ b/lua/leetcode/pickers/favorite.lua @@ -1,10 +1,8 @@ -local log = require("leetcode.logger") local t = require("leetcode.translator") local pickers = require("telescope.pickers") local finders = require("telescope.finders") local conf = require("telescope.config").values -local config = require("leetcode.config") local entry_display = require("telescope.pickers.entry_display") local actions = require("telescope.actions") @@ -14,69 +12,68 @@ local action_state = require("telescope.actions.state") --- ---@return string local function favorite_formatter(f) - return string.format("%s - %s", f.slug, f.name) + return string.format("%s - %s", f.slug, f.name) end - ---@param favorite lc.Favorite local function display_favorite(favorite) - return { - favorite.name, - favorite.slug, - } + return { + favorite.name, + favorite.slug, + } end local displayer = entry_display.create({ - separator = " ", - items = { - { remaining = true }, - }, + separator = " ", + items = { + { remaining = true }, + }, }) -- @param entry lc.Favorite local function make_display(entry) - ---@type lc.Favorite - local f = entry.value + ---@type lc.Favorite + local f = entry.value - return displayer({ - display_favorite(f), - }) + return displayer({ + display_favorite(f), + }) end -- @param entry lc.Favorite local function entry_maker(entry) - return { - value = entry, - display = make_display, - ordinal = favorite_formatter(entry), - } + return { + value = entry, + display = make_display, + ordinal = favorite_formatter(entry), + } end local opts = require("telescope.themes").get_dropdown() return { - pick = function(favorites, cb) - pickers - .new(opts, { - prompt_title = t("Select a Favorite List"), - finder = finders.new_table({ - results = favorites, - entry_maker = entry_maker, - }), - sorter = conf.generic_sorter(theme), - attach_mappings = function(prompt_bufnr) - actions.select_default:replace(function() - actions.close(prompt_bufnr) - local selection = action_state.get_selected_entry() + pick = function(favorites, cb) + pickers + .new(opts, { + prompt_title = t("Select a Favorite List"), + finder = finders.new_table({ + results = favorites, + entry_maker = entry_maker, + }), + sorter = conf.generic_sorter(theme), + attach_mappings = function(prompt_bufnr) + actions.select_default:replace(function() + actions.close(prompt_bufnr) + local selection = action_state.get_selected_entry() - if not selection then - return - end - cb(selection.value) - end) - return true - end, - }) - :find() - end, + if not selection then + return + end + cb(selection.value) + end) + return true + end, + }) + :find() + end, }