From b7890256b667525c80d24cc1be02b32c713cd0d7 Mon Sep 17 00:00:00 2001 From: fenix <693909080@qq.com> Date: Fri, 24 Apr 2026 00:59:52 +0800 Subject: [PATCH 1/2] feat: company question list picker (cn) Add the ability to browse and solve company interview question lists from leetcode.cn. Users can select a company from a searchable picker and view the company's curated problem set. - Add InterviewHotCompanyCards and favoriteQuestionList GraphQL queries - New company picker module with fzf-lua, telescope, mini.pick, and snacks.picker provider support - `Leet company` command and "Company" button on the Problems page Co-Authored-By: Claude Opus 4.7 (1M context) --- lua/leetcode-plugins/cn/queries.lua | 57 +++++++++++++++++++ lua/leetcode-ui/group/page/problems.lua | 7 +++ lua/leetcode/api/problems.lua | 69 +++++++++++++++++++++++ lua/leetcode/command/init.lua | 7 +++ lua/leetcode/picker/company/fzf.lua | 44 +++++++++++++++ lua/leetcode/picker/company/init.lua | 68 ++++++++++++++++++++++ lua/leetcode/picker/company/mini.lua | 42 ++++++++++++++ lua/leetcode/picker/company/snacks.lua | 63 +++++++++++++++++++++ lua/leetcode/picker/company/telescope.lua | 68 ++++++++++++++++++++++ lua/leetcode/picker/init.lua | 17 ++++++ 10 files changed, 442 insertions(+) create mode 100644 lua/leetcode/picker/company/fzf.lua create mode 100644 lua/leetcode/picker/company/init.lua create mode 100644 lua/leetcode/picker/company/mini.lua create mode 100644 lua/leetcode/picker/company/snacks.lua create mode 100644 lua/leetcode/picker/company/telescope.lua diff --git a/lua/leetcode-plugins/cn/queries.lua b/lua/leetcode-plugins/cn/queries.lua index f9d9d1f9..6ef75c43 100644 --- a/lua/leetcode-plugins/cn/queries.lua +++ b/lua/leetcode-plugins/cn/queries.lua @@ -160,3 +160,60 @@ queries.session_progress = [[ } } ]] + +queries.company_list = [[ + query InterviewHotCompanyCards($in: InterviewHotCompanyCardsInput!) { + interviewHotCompanyCards(in: $in) { + hasMore + total + cards { + companyInfo { + companyTag { + name + slug + imgUrl + translatedName + questionCount + } + } + } + } + } + ]] + +queries.company_questions = [[ + query favoriteQuestionList($favoriteSlug: String!, $filter: FavoriteQuestionFilterInput, $searchKeyword: String, $filtersV2: QuestionFilterInput, $sortBy: QuestionSortByInput, $limit: Int, $skip: Int, $version: String = "v2") { + favoriteQuestionList( + favoriteSlug: $favoriteSlug + filter: $filter + filtersV2: $filtersV2 + searchKeyword: $searchKeyword + sortBy: $sortBy + limit: $limit + skip: $skip + version: $version + ) { + questions { + difficulty + id + paidOnly + questionFrontendId + status + title + titleSlug + translatedTitle + isInMyFavorites + frequency + acRate + contestPoint + topicTags { + name + nameTranslated + slug + } + } + totalLength + hasMore + } + } + ]] diff --git a/lua/leetcode-ui/group/page/problems.lua b/lua/leetcode-ui/group/page/problems.lua index 8e0cfcdc..2d35672b 100644 --- a/lua/leetcode-ui/group/page/problems.lua +++ b/lua/leetcode-ui/group/page/problems.lua @@ -33,12 +33,19 @@ local daily = Button("Daily", { on_press = cmd.qot, }) +local company = Button("Company", { + icon = "", + sc = "c", + on_press = cmd.company, +}) + local back = BackButton("menu") page:insert(Buttons({ list, random, daily, + company, back, })) diff --git a/lua/leetcode/api/problems.lua b/lua/leetcode/api/problems.lua index 75a1969a..bbadb32c 100644 --- a/lua/leetcode/api/problems.lua +++ b/lua/leetcode/api/problems.lua @@ -108,6 +108,75 @@ function Problems.question_of_today(cb) }) end +---@param cb function +function Problems.company_list(cb) + local query = queries.company_list + local variables = { ["in"] = { offset = 0, limit = 100 } } + + utils.query(query, variables, { + endpoint = "/graphql/noj-go", + callback = function(res, err) + if err then + return cb(nil, err) + end + + local data = res.data.interviewHotCompanyCards + local companies = {} + for _, card in ipairs(data.cards) do + local tag = card.companyInfo.companyTag + table.insert(companies, { + name = tag.name ~= "" and tag.name or tag.translatedName, + slug = tag.slug, + question_count = tag.questionCount, + }) + end + cb(companies) + end, + }) +end + +---@param company_slug string +---@param cb function +function Problems.company_questions(company_slug, cb) + local query = queries.company_questions + local favorite_slug = company_slug .. "-thirty-days" + local variables = { + favoriteSlug = favorite_slug, + skip = 0, + limit = 500, + filtersV2 = { + filterCombineType = "ALL", + statusFilter = { questionStatuses = {}, operator = "IS" }, + difficultyFilter = { difficulties = {}, operator = "IS" }, + languageFilter = { languageSlugs = {}, operator = "IS" }, + topicFilter = { topicSlugs = {}, operator = "IS" }, + acceptanceFilter = {}, + frequencyFilter = {}, + frontendIdFilter = {}, + lastSubmittedFilter = {}, + publishedFilter = {}, + companyFilter = { companySlugs = {}, operator = "IS" }, + positionFilter = { positionSlugs = {}, operator = "IS" }, + positionLevelFilter = { positionLevelSlugs = {}, operator = "IS" }, + contestPointFilter = { contestPoints = {}, operator = "IS" }, + premiumFilter = { premiumStatus = {} }, + }, + searchKeyword = "", + sortBy = { sortField = "CUSTOM", sortOrder = "ASCENDING" }, + } + + utils.query(query, variables, { + callback = function(res, err) + if err then + return cb(nil, err) + end + + local data = res.data.favoriteQuestionList + cb(data.questions) + end, + }) +end + function Problems.translated_titles(cb) local query = queries.translations diff --git a/lua/leetcode/command/init.lua b/lua/leetcode/command/init.lua index 11824456..9e7cf922 100644 --- a/lua/leetcode/command/init.lua +++ b/lua/leetcode/command/init.lua @@ -168,6 +168,12 @@ function cmd.random_question(opts) Question(item):mount() end +function cmd.company() + require("leetcode.utils").auth_guard() + local picker = require("leetcode.picker") + picker.company() +end + function cmd.start_with_cmd() local leetcode = require("leetcode") if leetcode.start(false) then @@ -634,6 +640,7 @@ cmd.commands = { cmd.problems, _args = arguments.list, }, + company = { cmd.company }, random = { cmd.random_question, _args = arguments.random, diff --git a/lua/leetcode/picker/company/fzf.lua b/lua/leetcode/picker/company/fzf.lua new file mode 100644 index 00000000..602e4bac --- /dev/null +++ b/lua/leetcode/picker/company/fzf.lua @@ -0,0 +1,44 @@ +local fzf = require("fzf-lua") +local t = require("leetcode.translator") +local company_picker = require("leetcode.picker.company") +local Picker = require("leetcode.picker") + +local deli = " " + +return function(companies) + local items = {} + for _, company in ipairs(companies) do + local entry = company_picker.entry(company) + local normalized = Picker.normalize({ { entry = entry, value = company } })[1] + .. deli + .. Picker.apply_hl(company.slug, "leetcode_alt") + table.insert(items, normalized) + end + + fzf.fzf_exec(items, { + prompt = t("Select a Company") .. "> ", + winopts = { + height = company_picker.height, + width = company_picker.width, + }, + fzf_opts = { + ["--delimiter"] = deli, + ["--nth"] = "3..-2", + }, + actions = { + ["default"] = function(selected) + local slug = Picker.hidden_field(selected[1], deli) + local company = nil + for _, c in ipairs(companies) do + if c.slug == slug then + company = c + break + end + end + if company then + company_picker.select(company) + end + end, + }, + }) +end diff --git a/lua/leetcode/picker/company/init.lua b/lua/leetcode/picker/company/init.lua new file mode 100644 index 00000000..3c130d47 --- /dev/null +++ b/lua/leetcode/picker/company/init.lua @@ -0,0 +1,68 @@ +local config = require("leetcode.config") +local Picker = require("leetcode.picker") + +---@class leet.Picker.Company: leet.Picker +local P = {} + +P.width = 100 +P.height = 0.6 + +---@param company { name: string, slug: string, question_count: number } +local function display_entry(company) + local name = { company.name, "leetcode_normal" } + local count = { ("(%d)"):format(company.question_count), "leetcode_ref" } + return { name, count } +end + +---@param company { name: string, slug: string, question_count: number } +local function ordinal(company) + return ("%s %s"):format(company.name, company.slug) +end + +---@param company { name: string, slug: string, question_count: number } +function P.entry(company) + return display_entry(company) +end + +function P.ordinal(company) + return ordinal(company) +end + +function P.select(selection, close) + if close then + close() + end + + local problems_api = require("leetcode.api.problems") + local problemlist = require("leetcode.cache.problemlist") + local log = require("leetcode.logger") + + problems_api.company_questions(selection.slug, function(questions, err) + if err then + return log.err(err) + end + + if not questions or vim.tbl_isempty(questions) then + return log.warn("No questions found for this company") + end + + local slug_order = {} + for i, q in ipairs(questions) do + slug_order[q.titleSlug] = i + end + + local cached = problemlist.get() + local company_questions = vim.tbl_filter(function(q) + return slug_order[q.title_slug] ~= nil + end, cached) + + table.sort(company_questions, function(a, b) + return slug_order[a.title_slug] < slug_order[b.title_slug] + end) + + local question_picker = require("leetcode.picker") + question_picker.question(company_questions, {}) + end) +end + +return P diff --git a/lua/leetcode/picker/company/mini.lua b/lua/leetcode/picker/company/mini.lua new file mode 100644 index 00000000..17062c43 --- /dev/null +++ b/lua/leetcode/picker/company/mini.lua @@ -0,0 +1,42 @@ +local log = require("leetcode.logger") +local t = require("leetcode.translator") +local company_picker = require("leetcode.picker.company") + +local picker = require("mini.pick") + +return function(companies) + local items = {} + for _, company in ipairs(companies) do + local entry = company_picker.entry(company) + local text = company_picker.ordinal(company) + table.insert(items, { + entry = entry, + text = text, + value = company, + }) + end + + local ns_id = vim.api.nvim_create_namespace("MiniPick LeetCode Companies Picker") + + picker.start({ + source = { + items = items, + name = t("Select a Company"), + choose = function(item) + vim.schedule(function() + company_picker.select(item.value) + end) + return true + end, + show = function(buf_id, source_items) + require("leetcode.picker.mini_pick_utils").show_items(buf_id, ns_id, source_items) + end, + }, + window = { + config = { + width = company_picker.width, + height = math.floor(vim.o.lines * company_picker.height), + }, + }, + }) +end diff --git a/lua/leetcode/picker/company/snacks.lua b/lua/leetcode/picker/company/snacks.lua new file mode 100644 index 00000000..b3eabeed --- /dev/null +++ b/lua/leetcode/picker/company/snacks.lua @@ -0,0 +1,63 @@ +local log = require("leetcode.logger") +local t = require("leetcode.translator") +local company_picker = require("leetcode.picker.company") + +local picker = require("snacks.picker") + +return function(companies) + local items = {} + local completed = false + + for _, company in ipairs(companies) do + local entry = company_picker.entry(company) + local text = company_picker.ordinal(company) + table.insert(items, { + text = text, + value = company, + }) + end + + picker.pick({ + items = items, + format = function(item) + local ret = {} + vim.tbl_map(function(col) + if type(col) == "table" then + ret[#ret + 1] = col + else + ret[#ret + 1] = { col } + end + ret[#ret + 1] = { " " } + end, item.entry) + return ret + end, + title = t("Select a Company"), + layout = { + preset = "select", + preview = false, + layout = { + height = company_picker.height, + width = company_picker.width, + }, + }, + actions = { + confirm = function(p, item) + if completed then + return + end + completed = true + p:close() + vim.schedule(function() + company_picker.select(item.value) + end) + end, + }, + on_close = function() + if completed then + return + end + completed = true + log.warn("No selection") + end, + }) +end diff --git a/lua/leetcode/picker/company/telescope.lua b/lua/leetcode/picker/company/telescope.lua new file mode 100644 index 00000000..99527083 --- /dev/null +++ b/lua/leetcode/picker/company/telescope.lua @@ -0,0 +1,68 @@ +local log = require("leetcode.logger") +local t = require("leetcode.translator") +local company_picker = require("leetcode.picker.company") + +local pickers = require("telescope.pickers") +local finders = require("telescope.finders") +local conf = require("telescope.config").values + +local entry_display = require("telescope.pickers.entry_display") +local actions = require("telescope.actions") +local action_state = require("telescope.actions.state") + +local displayer = entry_display.create({ + separator = " ", + items = { + { remaining = true }, + { remaining = true }, + }, +}) + +local function entry_maker(item) + return { + value = item.value, + display = function() + return displayer(item.entry) + end, + ordinal = company_picker.ordinal(item.value), + } +end + +local theme = require("telescope.themes").get_dropdown({ + layout_config = { + width = company_picker.width, + height = company_picker.height, + }, +}) + +return function(companies) + local items = {} + for _, company in ipairs(companies) do + local entry = company_picker.entry(company) + table.insert(items, { entry = entry, value = company }) + end + + pickers + .new(theme, { + prompt_title = t("Select a Company"), + finder = finders.new_table({ + results = items, + entry_maker = entry_maker, + }), + sorter = conf.generic_sorter(theme), + attach_mappings = function(prompt_bufnr) + actions.select_default:replace(function() + local selection = action_state.get_selected_entry() + if not selection then + log.warn("No selection") + return + end + company_picker.select(selection.value, function() + actions.close(prompt_bufnr) + end) + end) + return true + end, + }) + :find() +end diff --git a/lua/leetcode/picker/init.lua b/lua/leetcode/picker/init.lua index 9af5ec92..dc62382d 100644 --- a/lua/leetcode/picker/init.lua +++ b/lua/leetcode/picker/init.lua @@ -134,6 +134,23 @@ function P.tabs() P.pick("tabs", tabs) end +function P.company() + local problems_api = require("leetcode.api.problems") + local log = require("leetcode.logger") + + problems_api.company_list(function(companies, err) + if err then + return log.err(err) + end + + if not companies or vim.tbl_isempty(companies) then + return log.warn("No companies found") + end + + P.pick("company", companies) + end) +end + function P.hidden_field(text, deli) return text:match(("([^%s]+)$"):format(deli)) end From a2ed8d2e2acd6411a75de44b340203ae8bdc7225 Mon Sep 17 00:00:00 2001 From: fenix <693909080@qq.com> Date: Sun, 26 Apr 2026 10:31:39 +0800 Subject: [PATCH 2/2] fix(company picker): fix garbled icon and GraphQL 400 error - Replace garbled company button icon with nf-fa-building (U+F1AD) - Use vim.empty_dict() for filter fields that must be JSON objects, not arrays, to fix GraphQL 400 error Co-Authored-By: Claude Opus 4.7 (1M context) --- lua/leetcode-ui/group/page/problems.lua | 2 +- lua/leetcode/api/problems.lua | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lua/leetcode-ui/group/page/problems.lua b/lua/leetcode-ui/group/page/problems.lua index 2d35672b..6782236a 100644 --- a/lua/leetcode-ui/group/page/problems.lua +++ b/lua/leetcode-ui/group/page/problems.lua @@ -34,7 +34,7 @@ local daily = Button("Daily", { }) local company = Button("Company", { - icon = "", + icon = "", sc = "c", on_press = cmd.company, }) diff --git a/lua/leetcode/api/problems.lua b/lua/leetcode/api/problems.lua index bbadb32c..9c1c340c 100644 --- a/lua/leetcode/api/problems.lua +++ b/lua/leetcode/api/problems.lua @@ -150,11 +150,11 @@ function Problems.company_questions(company_slug, cb) difficultyFilter = { difficulties = {}, operator = "IS" }, languageFilter = { languageSlugs = {}, operator = "IS" }, topicFilter = { topicSlugs = {}, operator = "IS" }, - acceptanceFilter = {}, - frequencyFilter = {}, - frontendIdFilter = {}, - lastSubmittedFilter = {}, - publishedFilter = {}, + acceptanceFilter = vim.empty_dict(), + frequencyFilter = vim.empty_dict(), + frontendIdFilter = vim.empty_dict(), + lastSubmittedFilter = vim.empty_dict(), + publishedFilter = vim.empty_dict(), companyFilter = { companySlugs = {}, operator = "IS" }, positionFilter = { positionSlugs = {}, operator = "IS" }, positionLevelFilter = { positionLevelSlugs = {}, operator = "IS" },