A Neovim plugin enabling you to solve LeetCode problems.
feat: `folke/snacks.nvim` picker support (#177)
feat: `folke/snacks.nvim` picker support
7 files changed, +256 -22
+8-7README.md
| @@ -279,9 +279,14 @@ injector = { ---@type table<lc.lang, lc.inject> | ||
| 279 | 279 | |
| 280 | 280 | ### picker |
| 281 | 281 | |
| 282 | -Supported picker providers are `telescope` and `fzf-lua`. | |
| 283 | -When provider is `nil`, [leetcode.nvim] will first try to use `fzf-lua`, | |
| 284 | -if not found it will fallback to `telescope`. | |
| 282 | +Supported picker providers are: | |
| 283 | + | |
| 284 | +- `snacks-picker` | |
| 285 | +- `fzf-lua` | |
| 286 | +- `telescope` | |
| 287 | + | |
| 288 | +If `provider` is `nil`, [leetcode.nvim] will try to resolve the first | |
| 289 | +available one in the order above. | |
| 285 | 290 | |
| 286 | 291 | ```lua |
| 287 | 292 | ---@type lc.picker |
| @@ -377,26 +382,22 @@ image_support = false, | ||
| 377 | 382 | - `inject` re-inject code for the current question |
| 378 | 383 | |
| 379 | 384 | - `session` |
| 380 | - | |
| 381 | 385 | - `create` create a new session |
| 382 | 386 | - `change` change the current session |
| 383 | 387 | |
| 384 | 388 | - `update` update the current session in case it went out of sync |
| 385 | 389 | |
| 386 | 390 | - `desc` toggle question description |
| 387 | - | |
| 388 | 391 | - `toggle` same as `Leet desc` |
| 389 | 392 | |
| 390 | 393 | - `stats` toggle description stats visibility |
| 391 | 394 | |
| 392 | 395 | - `cookie` |
| 393 | - | |
| 394 | 396 | - `update` opens a prompt to enter a new cookie |
| 395 | 397 | |
| 396 | 398 | - `delete` sign-out |
| 397 | 399 | |
| 398 | 400 | - `cache` |
| 399 | - | |
| 400 | 401 | - `update` updates cache |
| 401 | 402 | |
| 402 | 403 | #### Some commands can take optional arguments. To stack argument values separate them by a `,` |
+1-1lua/leetcode/config/template.lua
| @@ -38,7 +38,7 @@ | ||
| 38 | 38 | |
| 39 | 39 | ---@alias lc.storage table<"cache"|"home", string> |
| 40 | 40 | |
| 41 | ----@alias lc.picker { provider?: "fzf-lua" | "telescope" } | |
| 41 | +---@alias lc.picker { provider?: "fzf-lua" | "telescope" | "snacks-picker" } | |
| 42 | 42 | |
| 43 | 43 | ---@class lc.UserConfig |
| 44 | 44 | local M = { |
+52-13lua/leetcode/picker/init.lua
| @@ -1,26 +1,65 @@ | ||
| 1 | 1 | local log = require("leetcode.logger") |
| 2 | 2 | local config = require("leetcode.config") |
| 3 | 3 | |
| 4 | ----@return "fzf" | "telescope" | |
| 4 | +local provider_order = { "snacks-picker", "fzf-lua", "telescope" } | |
| 5 | +local providers = { | |
| 6 | + ["fzf-lua"] = { | |
| 7 | + name = "fzf", | |
| 8 | + is_available = function() | |
| 9 | + return pcall(require, "fzf-lua") | |
| 10 | + end, | |
| 11 | + }, | |
| 12 | + ["snacks-picker"] = { | |
| 13 | + name = "snacks", | |
| 14 | + is_available = function() | |
| 15 | + return pcall(function() ---@diagnostic disable-next-line: undefined-field | |
| 16 | + assert(Snacks.config["picker"].enabled) | |
| 17 | + end) | |
| 18 | + end, | |
| 19 | + }, | |
| 20 | + ["telescope"] = { | |
| 21 | + name = "telescope", | |
| 22 | + is_available = function() | |
| 23 | + return pcall(require, "telescope") | |
| 24 | + end, | |
| 25 | + }, | |
| 26 | +} | |
| 27 | + | |
| 28 | +local available_pickers = table.concat( | |
| 29 | + vim.tbl_map(function(p) | |
| 30 | + return providers[p].name | |
| 31 | + end, provider_order), | |
| 32 | + ", " | |
| 33 | +) | |
| 34 | + | |
| 35 | +---@return "fzf" | "telescope" | "snacks" | |
| 5 | 36 | local function resolve_provider() |
| 6 | 37 | ---@type string |
| 7 | 38 | local provider = config.user.picker.provider |
| 8 | 39 | |
| 9 | 40 | if provider == nil then |
| 10 | - local fzf_ok = pcall(require, "fzf-lua") | |
| 11 | - if fzf_ok then | |
| 12 | - return "fzf" | |
| 13 | - end | |
| 14 | - local telescope_ok = pcall(require, "telescope") | |
| 15 | - if telescope_ok then | |
| 16 | - return "telescope" | |
| 41 | + for _, key in ipairs(provider_order) do | |
| 42 | + local picker = providers[key] | |
| 43 | + | |
| 44 | + if picker.is_available() then | |
| 45 | + return picker.name | |
| 46 | + end | |
| 17 | 47 | end |
| 18 | - error("no supported picker provider found") | |
| 19 | - else | |
| 20 | - local provider_ok = pcall(require, provider) | |
| 21 | - assert(provider_ok, ("specified picker provider not found: `%s`"):format(provider)) | |
| 22 | - return provider == "fzf-lua" and "fzf" or provider | |
| 48 | + | |
| 49 | + error(("No picker is available. Please install one of the following: `%s`") -- | |
| 50 | + :format(available_pickers)) | |
| 23 | 51 | end |
| 52 | + | |
| 53 | + local picker = providers[provider] | |
| 54 | + assert( | |
| 55 | + picker, | |
| 56 | + ("Picker `%s` is not supported. Available pickers: `%s`") -- | |
| 57 | + :format(provider, available_pickers) | |
| 58 | + ) | |
| 59 | + | |
| 60 | + local ok = picker.is_available() | |
| 61 | + assert(ok, ("Picker `%s` is not available. Make sure it is installed"):format(provider)) | |
| 62 | + return picker.name | |
| 24 | 63 | end |
| 25 | 64 | |
| 26 | 65 | ---@class leet.Picker |
+64-0lua/leetcode/picker/language/snacks.lua
| @@ -0,0 +1,64 @@ | ||
| 1 | +local log = require("leetcode.logger") | |
| 2 | +local t = require("leetcode.translator") | |
| 3 | +local picker = require("snacks.picker") | |
| 4 | + | |
| 5 | +local language_picker = require("leetcode.picker.language") | |
| 6 | + | |
| 7 | +---@param question lc.ui.Question | |
| 8 | +return function(question, cb) | |
| 9 | + local items = language_picker.items(question.q.code_snippets) | |
| 10 | + local finder_items = {} | |
| 11 | + local completed = false | |
| 12 | + | |
| 13 | + for _, item in ipairs(items) do | |
| 14 | + local text = language_picker.ordinal(item.value) | |
| 15 | + table.insert(finder_items, { | |
| 16 | + text = text, | |
| 17 | + item = item, | |
| 18 | + }) | |
| 19 | + end | |
| 20 | + | |
| 21 | + picker.pick({ | |
| 22 | + items = finder_items, | |
| 23 | + format = function(item) | |
| 24 | + local ret = {} | |
| 25 | + vim.tbl_map(function(col) | |
| 26 | + if type(col) == "table" then | |
| 27 | + ret[#ret + 1] = col | |
| 28 | + else | |
| 29 | + ret[#ret + 1] = { col } | |
| 30 | + end | |
| 31 | + ret[#ret + 1] = { " " } | |
| 32 | + end, item.item.entry) | |
| 33 | + return ret | |
| 34 | + end, | |
| 35 | + title = t("Available Languages"), | |
| 36 | + layout = { | |
| 37 | + preview = false, | |
| 38 | + preset = "select", | |
| 39 | + layout = { | |
| 40 | + height = language_picker.height, | |
| 41 | + width = language_picker.width, | |
| 42 | + }, | |
| 43 | + }, | |
| 44 | + actions = { | |
| 45 | + confirm = function(p, item) | |
| 46 | + if completed then | |
| 47 | + return | |
| 48 | + end | |
| 49 | + completed = true | |
| 50 | + p:close() | |
| 51 | + vim.schedule(function() | |
| 52 | + language_picker.select(item.item.value.t, question, cb) | |
| 53 | + end) | |
| 54 | + end, | |
| 55 | + }, | |
| 56 | + on_close = function() | |
| 57 | + if completed then | |
| 58 | + return | |
| 59 | + end | |
| 60 | + completed = true | |
| 61 | + log.warn("No selection") | |
| 62 | + end, | |
| 63 | + }) | |
| 64 | +end |
+1-1lua/leetcode/picker/question/init.lua
| @@ -9,7 +9,7 @@ local Picker = require("leetcode.picker") | ||
| 9 | 9 | local P = {} |
| 10 | 10 | |
| 11 | 11 | P.width = 100 |
| 12 | -P.height = 20 | |
| 12 | +P.height = 0.6 | |
| 13 | 13 | |
| 14 | 14 | ---@param items lc.cache.Question[] |
| 15 | 15 | ---@param opts table<string, string[]> |
+64-0lua/leetcode/picker/question/snacks.lua
| @@ -0,0 +1,64 @@ | ||
| 1 | +local log = require("leetcode.logger") | |
| 2 | +local t = require("leetcode.translator") | |
| 3 | +local question_picker = require("leetcode.picker.question") | |
| 4 | + | |
| 5 | +local picker = require("snacks.picker") | |
| 6 | + | |
| 7 | +---@param questions lc.cache.Question[] | |
| 8 | +return function(questions, opts) | |
| 9 | + local items = question_picker.items(questions, opts) | |
| 10 | + local finder_items = {} | |
| 11 | + local completed = false | |
| 12 | + | |
| 13 | + for _, item in ipairs(items) do | |
| 14 | + local text = question_picker.ordinal(item.value) | |
| 15 | + table.insert(finder_items, { | |
| 16 | + text = text, | |
| 17 | + item = item, | |
| 18 | + }) | |
| 19 | + end | |
| 20 | + | |
| 21 | + picker.pick({ | |
| 22 | + items = finder_items, | |
| 23 | + format = function(item) | |
| 24 | + local ret = {} | |
| 25 | + vim.tbl_map(function(col) | |
| 26 | + if type(col) == "table" then | |
| 27 | + ret[#ret + 1] = col | |
| 28 | + else | |
| 29 | + ret[#ret + 1] = { col } | |
| 30 | + end | |
| 31 | + ret[#ret + 1] = { " " } | |
| 32 | + end, item.item.entry) | |
| 33 | + return ret | |
| 34 | + end, | |
| 35 | + title = t("Select a Question"), | |
| 36 | + layout = { | |
| 37 | + preset = "select", | |
| 38 | + preview = false, | |
| 39 | + layout = { | |
| 40 | + height = question_picker.height, | |
| 41 | + width = question_picker.width, | |
| 42 | + }, | |
| 43 | + }, | |
| 44 | + actions = { | |
| 45 | + confirm = function(p, item) | |
| 46 | + if completed then | |
| 47 | + return | |
| 48 | + end | |
| 49 | + completed = true | |
| 50 | + p:close() | |
| 51 | + vim.schedule(function() | |
| 52 | + question_picker.select(item.item.value) | |
| 53 | + end) | |
| 54 | + end, | |
| 55 | + }, | |
| 56 | + on_close = function() | |
| 57 | + if completed then | |
| 58 | + return | |
| 59 | + end | |
| 60 | + completed = true | |
| 61 | + log.warn("No selection") | |
| 62 | + end, | |
| 63 | + }) | |
| 64 | +end |
+66-0lua/leetcode/picker/tabs/snacks.lua
| @@ -0,0 +1,66 @@ | ||
| 1 | +local log = require("leetcode.logger") | |
| 2 | +local t = require("leetcode.translator") | |
| 3 | +local tabs_picker = require("leetcode.picker.tabs") | |
| 4 | + | |
| 5 | +local picker = require("snacks.picker") | |
| 6 | + | |
| 7 | +return function(tabs) | |
| 8 | + local items = tabs_picker.items(tabs) | |
| 9 | + local finder_items = {} | |
| 10 | + local completed = false | |
| 11 | + local items_reflect = {} | |
| 12 | + | |
| 13 | + for _, item in ipairs(items) do | |
| 14 | + items_reflect[item.value.question.q.frontend_id] = item.value | |
| 15 | + local text = tabs_picker.ordinal(item.value.question.q) | |
| 16 | + table.insert(finder_items, { | |
| 17 | + entry = item.entry, | |
| 18 | + item = item.value.question.q.frontend_id, | |
| 19 | + text = text, | |
| 20 | + }) | |
| 21 | + end | |
| 22 | + | |
| 23 | + picker.pick({ | |
| 24 | + items = finder_items, | |
| 25 | + format = function(item) | |
| 26 | + local ret = {} | |
| 27 | + vim.tbl_map(function(col) | |
| 28 | + if type(col) == "table" then | |
| 29 | + ret[#ret + 1] = col | |
| 30 | + else | |
| 31 | + ret[#ret + 1] = { col } | |
| 32 | + end | |
| 33 | + ret[#ret + 1] = { " " } | |
| 34 | + end, item.entry) | |
| 35 | + return ret | |
| 36 | + end, | |
| 37 | + title = t("Select a Question"), | |
| 38 | + layout = { | |
| 39 | + preview = false, | |
| 40 | + preset = "select", | |
| 41 | + layout = { | |
| 42 | + height = tabs_picker.height, | |
| 43 | + width = tabs_picker.width, | |
| 44 | + }, | |
| 45 | + }, | |
| 46 | + actions = { | |
| 47 | + confirm = function(p, item) | |
| 48 | + if completed then | |
| 49 | + return | |
| 50 | + end | |
| 51 | + completed = true | |
| 52 | + p:close() | |
| 53 | + vim.schedule(function() | |
| 54 | + tabs_picker.select(items_reflect[item.item]) | |
| 55 | + end) | |
| 56 | + end, | |
| 57 | + }, | |
| 58 | + on_close = function() | |
| 59 | + if completed then | |
| 60 | + return | |
| 61 | + end | |
| 62 | + completed = true | |
| 63 | + log.warn("No selection") | |
| 64 | + end, | |
| 65 | + }) | |
| 66 | +end |