feat(picker): picker support
This commit is contained in:
parent
ea9883895f
commit
a33e66680b
11 changed files with 829 additions and 2 deletions
41
doc/cp.txt
41
doc/cp.txt
|
|
@ -72,6 +72,12 @@ COMMANDS *cp-commands*
|
||||||
Use --debug flag to compile with debug flags.
|
Use --debug flag to compile with debug flags.
|
||||||
Requires contest setup first.
|
Requires contest setup first.
|
||||||
|
|
||||||
|
:CP pick Launch configured picker for interactive
|
||||||
|
platform/contest/problem selection. Requires
|
||||||
|
picker = 'telescope' or picker = 'fzf-lua'
|
||||||
|
in configuration and corresponding plugin
|
||||||
|
to be installed.
|
||||||
|
|
||||||
Navigation Commands ~
|
Navigation Commands ~
|
||||||
:CP next Navigate to next problem in current contest.
|
:CP next Navigate to next problem in current contest.
|
||||||
Stops at last problem (no wrapping).
|
Stops at last problem (no wrapping).
|
||||||
|
|
@ -117,6 +123,32 @@ Template Variables ~
|
||||||
g++ abc324a.cpp -o build/abc324a.run -std=c++17
|
g++ abc324a.cpp -o build/abc324a.run -std=c++17
|
||||||
<
|
<
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
PICKER INTEGRATION *cp-picker*
|
||||||
|
|
||||||
|
When picker integration is enabled in configuration, cp.nvim provides interactive
|
||||||
|
platform, contest, and problem selection using telescope.nvim or fzf-lua.
|
||||||
|
|
||||||
|
:CP pick *:CP-pick*
|
||||||
|
Launch configured picker for interactive problem selection.
|
||||||
|
Flow: Platform → Contest → Problem → Setup
|
||||||
|
|
||||||
|
Requires picker = 'telescope' or picker = 'fzf-lua' in configuration.
|
||||||
|
Requires corresponding plugin (telescope.nvim or fzf-lua) to be installed.
|
||||||
|
Picker availability is checked at runtime when command is executed.
|
||||||
|
|
||||||
|
Picker Flow ~
|
||||||
|
1. Platform Selection: Choose from AtCoder, Codeforces, CSES
|
||||||
|
2. Contest Selection: Choose from available contests for selected platform
|
||||||
|
3. Problem Selection: Choose from problems in selected contest
|
||||||
|
4. Problem Setup: Automatically runs equivalent of :CP platform contest problem
|
||||||
|
|
||||||
|
Notes ~
|
||||||
|
• Contest lists are fetched dynamically using scrapers
|
||||||
|
• Large contest lists may take time to load
|
||||||
|
• Runtime picker validation - shows clear error if picker plugin not available
|
||||||
|
• Picker configuration can be changed without plugin restart
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
CONFIGURATION *cp-config*
|
CONFIGURATION *cp-config*
|
||||||
|
|
||||||
|
|
@ -131,10 +163,11 @@ Here's an example configuration with lazy.nvim: >lua
|
||||||
default = {
|
default = {
|
||||||
cpp = {
|
cpp = {
|
||||||
compile = { 'g++', '{source}', '-o', '{binary}',
|
compile = { 'g++', '{source}', '-o', '{binary}',
|
||||||
'-std=c++17' },
|
'-std=c++17', '-fdiagnostic-colors=always' },
|
||||||
test = { '{binary}' },
|
test = { '{binary}' },
|
||||||
debug = { 'g++', '{source}', '-o', '{binary}',
|
debug = { 'g++', '{source}', '-o', '{binary}',
|
||||||
'-std=c++17', '-g',
|
'-std=c++17', '-g',
|
||||||
|
'-fdiagnostic-colors=always'
|
||||||
'-fsanitize=address,undefined' },
|
'-fsanitize=address,undefined' },
|
||||||
},
|
},
|
||||||
python = {
|
python = {
|
||||||
|
|
@ -165,6 +198,7 @@ Here's an example configuration with lazy.nvim: >lua
|
||||||
'--word-diff-regex=.', '--no-prefix' },
|
'--word-diff-regex=.', '--no-prefix' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
picker = 'telescope', -- 'telescope', 'fzf-lua', or nil (disabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
<
|
<
|
||||||
|
|
@ -180,6 +214,11 @@ Here's an example configuration with lazy.nvim: >lua
|
||||||
Default: all scrapers enabled
|
Default: all scrapers enabled
|
||||||
{run_panel} (|RunPanelConfig|) Test panel behavior configuration.
|
{run_panel} (|RunPanelConfig|) Test panel behavior configuration.
|
||||||
{diff} (|DiffConfig|) Diff backend configuration.
|
{diff} (|DiffConfig|) Diff backend configuration.
|
||||||
|
{picker} (string, optional) Picker integration: "telescope",
|
||||||
|
"fzf-lua", or nil to disable. When enabled, provides
|
||||||
|
:Telescope cp or :FzfLua cp commands for interactive
|
||||||
|
platform/contest/problem selection. Requires the
|
||||||
|
corresponding picker plugin to be installed.
|
||||||
{filename} (function, optional) Custom filename generation.
|
{filename} (function, optional) Custom filename generation.
|
||||||
function(contest, contest_id, problem_id, config, language)
|
function(contest, contest_id, problem_id, config, language)
|
||||||
Should return full filename with extension.
|
Should return full filename with extension.
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@
|
||||||
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
|
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
|
||||||
---@field run_panel RunPanelConfig
|
---@field run_panel RunPanelConfig
|
||||||
---@field diff DiffConfig
|
---@field diff DiffConfig
|
||||||
|
---@field picker "telescope"|"fzf-lua"|nil
|
||||||
|
|
||||||
---@class cp.UserConfig
|
---@class cp.UserConfig
|
||||||
---@field contests? table<string, PartialContestConfig>
|
---@field contests? table<string, PartialContestConfig>
|
||||||
|
|
@ -62,6 +63,7 @@
|
||||||
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
|
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
|
||||||
---@field run_panel? RunPanelConfig
|
---@field run_panel? RunPanelConfig
|
||||||
---@field diff? DiffConfig
|
---@field diff? DiffConfig
|
||||||
|
---@field picker? "telescope"|"fzf-lua"|nil
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
local constants = require('cp.constants')
|
local constants = require('cp.constants')
|
||||||
|
|
@ -110,6 +112,7 @@ M.defaults = {
|
||||||
args = { 'diff', '--no-index', '--word-diff=plain', '--word-diff-regex=.', '--no-prefix' },
|
args = { 'diff', '--no-index', '--word-diff=plain', '--word-diff-regex=.', '--no-prefix' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
picker = nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
---@param user_config cp.UserConfig|nil
|
---@param user_config cp.UserConfig|nil
|
||||||
|
|
@ -129,6 +132,7 @@ function M.setup(user_config)
|
||||||
filename = { user_config.filename, { 'function', 'nil' }, true },
|
filename = { user_config.filename, { 'function', 'nil' }, true },
|
||||||
run_panel = { user_config.run_panel, { 'table', 'nil' }, true },
|
run_panel = { user_config.run_panel, { 'table', 'nil' }, true },
|
||||||
diff = { user_config.diff, { 'table', 'nil' }, true },
|
diff = { user_config.diff, { 'table', 'nil' }, true },
|
||||||
|
picker = { user_config.picker, { 'string', 'nil' }, true },
|
||||||
})
|
})
|
||||||
|
|
||||||
if user_config.contests then
|
if user_config.contests then
|
||||||
|
|
@ -164,6 +168,12 @@ function M.setup(user_config)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if user_config.picker then
|
||||||
|
if not vim.tbl_contains({ 'telescope', 'fzf-lua' }, user_config.picker) then
|
||||||
|
error(("Invalid picker '%s'. Must be 'telescope' or 'fzf-lua'"):format(user_config.picker))
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local config = vim.tbl_deep_extend('force', M.defaults, user_config or {})
|
local config = vim.tbl_deep_extend('force', M.defaults, user_config or {})
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
M.PLATFORMS = { 'atcoder', 'codeforces', 'cses' }
|
M.PLATFORMS = { 'atcoder', 'codeforces', 'cses' }
|
||||||
M.ACTIONS = { 'run', 'next', 'prev' }
|
M.ACTIONS = { 'run', 'next', 'prev', 'pick' }
|
||||||
|
|
||||||
|
M.PLATFORM_DISPLAY_NAMES = {
|
||||||
|
atcoder = 'AtCoder',
|
||||||
|
codeforces = 'CodeForces',
|
||||||
|
cses = 'CSES',
|
||||||
|
}
|
||||||
|
|
||||||
M.CPP = 'cpp'
|
M.CPP = 'cpp'
|
||||||
M.PYTHON = 'python'
|
M.PYTHON = 'python'
|
||||||
|
|
|
||||||
|
|
@ -698,6 +698,48 @@ local function navigate_problem(delta, language)
|
||||||
setup_problem(state.contest_id, new_problem.id, language)
|
setup_problem(state.contest_id, new_problem.id, language)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function handle_pick_action()
|
||||||
|
if not config.picker then
|
||||||
|
logger.log(
|
||||||
|
'No picker configured. Set picker = "telescope" or picker = "fzf-lua" in config',
|
||||||
|
vim.log.levels.ERROR
|
||||||
|
)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if config.picker == 'telescope' then
|
||||||
|
local ok, telescope = pcall(require, 'telescope')
|
||||||
|
if not ok then
|
||||||
|
logger.log(
|
||||||
|
'Telescope not available. Install telescope.nvim or change picker config',
|
||||||
|
vim.log.levels.ERROR
|
||||||
|
)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local ok_cp, telescope_cp = pcall(require, 'cp.pickers.telescope')
|
||||||
|
if not ok_cp then
|
||||||
|
logger.log('Failed to load telescope integration', vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
telescope_cp.platform_picker()
|
||||||
|
elseif config.picker == 'fzf-lua' then
|
||||||
|
local ok, _ = pcall(require, 'fzf-lua')
|
||||||
|
if not ok then
|
||||||
|
logger.log(
|
||||||
|
'fzf-lua not available. Install fzf-lua or change picker config',
|
||||||
|
vim.log.levels.ERROR
|
||||||
|
)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local ok_cp, fzf_cp = pcall(require, 'cp.pickers.fzf_lua')
|
||||||
|
if not ok_cp then
|
||||||
|
logger.log('Failed to load fzf-lua integration', vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
fzf_cp.platform_picker()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function restore_from_current_file()
|
local function restore_from_current_file()
|
||||||
local current_file = vim.fn.expand('%:p')
|
local current_file = vim.fn.expand('%:p')
|
||||||
if current_file == '' then
|
if current_file == '' then
|
||||||
|
|
@ -837,6 +879,8 @@ function M.handle_command(opts)
|
||||||
navigate_problem(1, cmd.language)
|
navigate_problem(1, cmd.language)
|
||||||
elseif cmd.action == 'prev' then
|
elseif cmd.action == 'prev' then
|
||||||
navigate_problem(-1, cmd.language)
|
navigate_problem(-1, cmd.language)
|
||||||
|
elseif cmd.action == 'pick' then
|
||||||
|
handle_pick_action()
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
|
||||||
123
lua/cp/pickers/fzf_lua.lua
Normal file
123
lua/cp/pickers/fzf_lua.lua
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
local picker_utils = require('cp.pickers')
|
||||||
|
|
||||||
|
local function problem_picker(platform, contest_id)
|
||||||
|
local constants = require('cp.constants')
|
||||||
|
local platform_display_name = constants.PLATFORM_DISPLAY_NAMES[platform] or platform
|
||||||
|
local fzf = require('fzf-lua')
|
||||||
|
local problems = picker_utils.get_problems_for_contest(platform, contest_id)
|
||||||
|
|
||||||
|
if #problems == 0 then
|
||||||
|
vim.notify(
|
||||||
|
('No problems found for contest: %s %s'):format(platform_display_name, contest_id),
|
||||||
|
vim.log.levels.WARN
|
||||||
|
)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local entries = vim.tbl_map(function(problem)
|
||||||
|
return problem.display_name
|
||||||
|
end, problems)
|
||||||
|
|
||||||
|
return fzf.fzf_exec(entries, {
|
||||||
|
prompt = ('Select Problem (%s %s)> '):format(platform_display_name, contest_id),
|
||||||
|
actions = {
|
||||||
|
['default'] = function(selected)
|
||||||
|
if not selected or #selected == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local selected_name = selected[1]
|
||||||
|
local problem = nil
|
||||||
|
for _, p in ipairs(problems) do
|
||||||
|
if p.display_name == selected_name then
|
||||||
|
problem = p
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if problem then
|
||||||
|
picker_utils.setup_problem(platform, contest_id, problem.id)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function contest_picker(platform)
|
||||||
|
local constants = require('cp.constants')
|
||||||
|
local platform_display_name = constants.PLATFORM_DISPLAY_NAMES[platform] or platform
|
||||||
|
local fzf = require('fzf-lua')
|
||||||
|
local contests = picker_utils.get_contests_for_platform(platform)
|
||||||
|
|
||||||
|
if #contests == 0 then
|
||||||
|
vim.notify(
|
||||||
|
('No contests found for platform: %s'):format(platform_display_name),
|
||||||
|
vim.log.levels.WARN
|
||||||
|
)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local entries = vim.tbl_map(function(contest)
|
||||||
|
return contest.display_name
|
||||||
|
end, contests)
|
||||||
|
|
||||||
|
return fzf.fzf_exec(entries, {
|
||||||
|
prompt = ('Select Contest (%s)> '):format(platform_display_name),
|
||||||
|
actions = {
|
||||||
|
['default'] = function(selected)
|
||||||
|
if not selected or #selected == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local selected_name = selected[1]
|
||||||
|
local contest = nil
|
||||||
|
for _, c in ipairs(contests) do
|
||||||
|
if c.display_name == selected_name then
|
||||||
|
contest = c
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if contest then
|
||||||
|
problem_picker(platform, contest.id)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function platform_picker()
|
||||||
|
local fzf = require('fzf-lua')
|
||||||
|
local platforms = picker_utils.get_platforms()
|
||||||
|
local entries = vim.tbl_map(function(platform)
|
||||||
|
return platform.display_name
|
||||||
|
end, platforms)
|
||||||
|
|
||||||
|
return fzf.fzf_exec(entries, {
|
||||||
|
prompt = 'Select Platform> ',
|
||||||
|
actions = {
|
||||||
|
['default'] = function(selected)
|
||||||
|
if not selected or #selected == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local selected_name = selected[1]
|
||||||
|
local platform = nil
|
||||||
|
for _, p in ipairs(platforms) do
|
||||||
|
if p.display_name == selected_name then
|
||||||
|
platform = p
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if platform then
|
||||||
|
contest_picker(platform.id)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
platform_picker = platform_picker,
|
||||||
|
}
|
||||||
145
lua/cp/pickers/init.lua
Normal file
145
lua/cp/pickers/init.lua
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local cache = require('cp.cache')
|
||||||
|
local logger = require('cp.log')
|
||||||
|
local scrape = require('cp.scrape')
|
||||||
|
|
||||||
|
---@class cp.PlatformItem
|
||||||
|
---@field id string Platform identifier (e.g. "codeforces", "atcoder", "cses")
|
||||||
|
---@field display_name string Human-readable platform name (e.g. "Codeforces", "AtCoder", "CSES")
|
||||||
|
|
||||||
|
---@class cp.ContestItem
|
||||||
|
---@field id string Contest identifier (e.g. "1951", "abc324", "sorting")
|
||||||
|
---@field name string Full contest name (e.g. "Educational Codeforces Round 168")
|
||||||
|
---@field display_name string Formatted display name for picker
|
||||||
|
|
||||||
|
---@class cp.ProblemItem
|
||||||
|
---@field id string Problem identifier (e.g. "a", "b", "c")
|
||||||
|
---@field name string Problem name (e.g. "Two Permutations", "Painting Walls")
|
||||||
|
---@field display_name string Formatted display name for picker
|
||||||
|
|
||||||
|
---Get list of available competitive programming platforms
|
||||||
|
---@return cp.PlatformItem[]
|
||||||
|
local function get_platforms()
|
||||||
|
local constants = require('cp.constants')
|
||||||
|
return vim.tbl_map(function(platform)
|
||||||
|
return {
|
||||||
|
id = platform,
|
||||||
|
display_name = constants.PLATFORM_DISPLAY_NAMES[platform] or platform,
|
||||||
|
}
|
||||||
|
end, constants.PLATFORMS)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get list of contests for a specific platform
|
||||||
|
---@param platform string Platform identifier (e.g. "codeforces", "atcoder")
|
||||||
|
---@return cp.ContestItem[]
|
||||||
|
local function get_contests_for_platform(platform)
|
||||||
|
local contests = {}
|
||||||
|
|
||||||
|
local function get_plugin_path()
|
||||||
|
local plugin_path = debug.getinfo(1, 'S').source:sub(2)
|
||||||
|
return vim.fn.fnamemodify(plugin_path, ':h:h:h')
|
||||||
|
end
|
||||||
|
|
||||||
|
local plugin_path = get_plugin_path()
|
||||||
|
local cmd = {
|
||||||
|
'uv',
|
||||||
|
'run',
|
||||||
|
'--directory',
|
||||||
|
plugin_path,
|
||||||
|
'-m',
|
||||||
|
'scrapers.' .. platform,
|
||||||
|
'contests',
|
||||||
|
}
|
||||||
|
local result = vim
|
||||||
|
.system(cmd, {
|
||||||
|
cwd = plugin_path,
|
||||||
|
text = true,
|
||||||
|
timeout = 30000,
|
||||||
|
})
|
||||||
|
:wait()
|
||||||
|
|
||||||
|
if result.code ~= 0 then
|
||||||
|
logger.log(
|
||||||
|
('Failed to get contests for %s: %s'):format(platform, result.stderr or 'unknown error'),
|
||||||
|
vim.log.levels.ERROR
|
||||||
|
)
|
||||||
|
return contests
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, data = pcall(vim.json.decode, result.stdout)
|
||||||
|
if not ok or not data.success then
|
||||||
|
logger.log(('Failed to parse contest data for %s'):format(platform), vim.log.levels.ERROR)
|
||||||
|
return contests
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, contest in ipairs(data.contests or {}) do
|
||||||
|
table.insert(contests, {
|
||||||
|
id = contest.id,
|
||||||
|
name = contest.name,
|
||||||
|
display_name = contest.display_name,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return contests
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get list of problems for a specific contest
|
||||||
|
---@param platform string Platform identifier
|
||||||
|
---@param contest_id string Contest identifier
|
||||||
|
---@return cp.ProblemItem[]
|
||||||
|
local function get_problems_for_contest(platform, contest_id)
|
||||||
|
local problems = {}
|
||||||
|
|
||||||
|
cache.load()
|
||||||
|
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||||
|
if contest_data and contest_data.problems then
|
||||||
|
for _, problem in ipairs(contest_data.problems) do
|
||||||
|
table.insert(problems, {
|
||||||
|
id = problem.id,
|
||||||
|
name = problem.name,
|
||||||
|
display_name = problem.name,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
return problems
|
||||||
|
end
|
||||||
|
|
||||||
|
local metadata_result = scrape.scrape_contest_metadata(platform, contest_id)
|
||||||
|
if not metadata_result.success then
|
||||||
|
logger.log(
|
||||||
|
('Failed to get problems for %s %s: %s'):format(
|
||||||
|
platform,
|
||||||
|
contest_id,
|
||||||
|
metadata_result.error or 'unknown error'
|
||||||
|
),
|
||||||
|
vim.log.levels.ERROR
|
||||||
|
)
|
||||||
|
return problems
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, problem in ipairs(metadata_result.problems or {}) do
|
||||||
|
table.insert(problems, {
|
||||||
|
id = problem.id,
|
||||||
|
name = problem.name,
|
||||||
|
display_name = problem.name,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return problems
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set up a specific problem by calling the main CP handler
|
||||||
|
---@param platform string Platform identifier
|
||||||
|
---@param contest_id string Contest identifier
|
||||||
|
---@param problem_id string Problem identifier
|
||||||
|
local function setup_problem(platform, contest_id, problem_id)
|
||||||
|
local cp = require('cp')
|
||||||
|
cp.handle_command({ fargs = { platform, contest_id, problem_id } })
|
||||||
|
end
|
||||||
|
|
||||||
|
M.get_platforms = get_platforms
|
||||||
|
M.get_contests_for_platform = get_contests_for_platform
|
||||||
|
M.get_problems_for_contest = get_problems_for_contest
|
||||||
|
M.setup_problem = setup_problem
|
||||||
|
|
||||||
|
return M
|
||||||
130
lua/cp/pickers/telescope.lua
Normal file
130
lua/cp/pickers/telescope.lua
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
local finders = require('telescope.finders')
|
||||||
|
local pickers = require('telescope.pickers')
|
||||||
|
local telescope = require('telescope')
|
||||||
|
local conf = require('telescope.config').values
|
||||||
|
local action_state = require('telescope.actions.state')
|
||||||
|
local actions = require('telescope.actions')
|
||||||
|
|
||||||
|
local picker_utils = require('cp.pickers')
|
||||||
|
|
||||||
|
local function platform_picker(opts)
|
||||||
|
opts = opts or {}
|
||||||
|
|
||||||
|
local platforms = picker_utils.get_platforms()
|
||||||
|
|
||||||
|
pickers
|
||||||
|
.new(opts, {
|
||||||
|
prompt_title = 'Select Platform',
|
||||||
|
finder = finders.new_table({
|
||||||
|
results = platforms,
|
||||||
|
entry_maker = function(entry)
|
||||||
|
return {
|
||||||
|
value = entry,
|
||||||
|
display = entry.display_name,
|
||||||
|
ordinal = entry.display_name,
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
sorter = conf.generic_sorter(opts),
|
||||||
|
attach_mappings = function(prompt_bufnr, map)
|
||||||
|
actions.select_default:replace(function()
|
||||||
|
local selection = action_state.get_selected_entry()
|
||||||
|
actions.close(prompt_bufnr)
|
||||||
|
|
||||||
|
if selection then
|
||||||
|
contest_picker(opts, selection.value.id)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
:find()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function contest_picker(opts, platform)
|
||||||
|
local constants = require('cp.constants')
|
||||||
|
local platform_display_name = constants.PLATFORM_DISPLAY_NAMES[platform] or platform
|
||||||
|
local contests = picker_utils.get_contests_for_platform(platform)
|
||||||
|
|
||||||
|
if #contests == 0 then
|
||||||
|
vim.notify(
|
||||||
|
('No contests found for platform: %s'):format(platform_display_name),
|
||||||
|
vim.log.levels.WARN
|
||||||
|
)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
pickers
|
||||||
|
.new(opts, {
|
||||||
|
prompt_title = ('Select Contest (%s)'):format(platform_display_name),
|
||||||
|
finder = finders.new_table({
|
||||||
|
results = contests,
|
||||||
|
entry_maker = function(entry)
|
||||||
|
return {
|
||||||
|
value = entry,
|
||||||
|
display = entry.display_name,
|
||||||
|
ordinal = entry.display_name,
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
sorter = conf.generic_sorter(opts),
|
||||||
|
attach_mappings = function(prompt_bufnr, map)
|
||||||
|
actions.select_default:replace(function()
|
||||||
|
local selection = action_state.get_selected_entry()
|
||||||
|
actions.close(prompt_bufnr)
|
||||||
|
|
||||||
|
if selection then
|
||||||
|
problem_picker(opts, platform, selection.value.id)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
:find()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function problem_picker(opts, platform, contest_id)
|
||||||
|
local constants = require('cp.constants')
|
||||||
|
local platform_display_name = constants.PLATFORM_DISPLAY_NAMES[platform] or platform
|
||||||
|
local problems = picker_utils.get_problems_for_contest(platform, contest_id)
|
||||||
|
|
||||||
|
if #problems == 0 then
|
||||||
|
vim.notify(
|
||||||
|
('No problems found for contest: %s %s'):format(platform_display_name, contest_id),
|
||||||
|
vim.log.levels.WARN
|
||||||
|
)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
pickers
|
||||||
|
.new(opts, {
|
||||||
|
prompt_title = ('Select Problem (%s %s)'):format(platform_display_name, contest_id),
|
||||||
|
finder = finders.new_table({
|
||||||
|
results = problems,
|
||||||
|
entry_maker = function(entry)
|
||||||
|
return {
|
||||||
|
value = entry,
|
||||||
|
display = entry.display_name,
|
||||||
|
ordinal = entry.display_name,
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
sorter = conf.generic_sorter(opts),
|
||||||
|
attach_mappings = function(prompt_bufnr, map)
|
||||||
|
actions.select_default:replace(function()
|
||||||
|
local selection = action_state.get_selected_entry()
|
||||||
|
actions.close(prompt_bufnr)
|
||||||
|
|
||||||
|
if selection then
|
||||||
|
picker_utils.setup_problem(platform, contest_id, selection.value.id)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
:find()
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
platform_picker = platform_picker,
|
||||||
|
}
|
||||||
|
|
@ -231,6 +231,39 @@ describe('cp.config', function()
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
describe('picker validation', function()
|
||||||
|
it('validates picker is valid value', function()
|
||||||
|
local invalid_config = {
|
||||||
|
picker = 'invalid_picker',
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.has_error(function()
|
||||||
|
config.setup(invalid_config)
|
||||||
|
end, "Invalid picker 'invalid_picker'. Must be 'telescope' or 'fzf-lua'")
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('allows nil picker', function()
|
||||||
|
assert.has_no.errors(function()
|
||||||
|
local result = config.setup({ picker = nil })
|
||||||
|
assert.is_nil(result.picker)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('allows telescope picker without checking availability', function()
|
||||||
|
assert.has_no.errors(function()
|
||||||
|
local result = config.setup({ picker = 'telescope' })
|
||||||
|
assert.equals('telescope', result.picker)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('allows fzf-lua picker without checking availability', function()
|
||||||
|
assert.has_no.errors(function()
|
||||||
|
local result = config.setup({ picker = 'fzf-lua' })
|
||||||
|
assert.equals('fzf-lua', result.picker)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe('default_filename', function()
|
describe('default_filename', function()
|
||||||
|
|
|
||||||
31
spec/fzf_lua_spec.lua
Normal file
31
spec/fzf_lua_spec.lua
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
describe('cp.fzf_lua', function()
|
||||||
|
local spec_helper = require('spec.spec_helper')
|
||||||
|
|
||||||
|
before_each(function()
|
||||||
|
spec_helper.setup()
|
||||||
|
|
||||||
|
package.preload['fzf-lua'] = function()
|
||||||
|
return {
|
||||||
|
fzf_exec = function(entries, opts) end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
after_each(function()
|
||||||
|
spec_helper.teardown()
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('module loading', function()
|
||||||
|
it('loads fzf-lua integration without error', function()
|
||||||
|
assert.has_no.errors(function()
|
||||||
|
require('cp.pickers.fzf_lua')
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('returns module with platform_picker function', function()
|
||||||
|
local fzf_lua_cp = require('cp.pickers.fzf_lua')
|
||||||
|
assert.is_table(fzf_lua_cp)
|
||||||
|
assert.is_function(fzf_lua_cp.platform_picker)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
188
spec/picker_spec.lua
Normal file
188
spec/picker_spec.lua
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
describe('cp.picker', function()
|
||||||
|
local picker
|
||||||
|
local spec_helper = require('spec.spec_helper')
|
||||||
|
|
||||||
|
before_each(function()
|
||||||
|
spec_helper.setup()
|
||||||
|
picker = require('cp.pickers')
|
||||||
|
end)
|
||||||
|
|
||||||
|
after_each(function()
|
||||||
|
spec_helper.teardown()
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('get_platforms', function()
|
||||||
|
it('returns platform list with display names', function()
|
||||||
|
local platforms = picker.get_platforms()
|
||||||
|
|
||||||
|
assert.is_table(platforms)
|
||||||
|
assert.is_true(#platforms > 0)
|
||||||
|
|
||||||
|
for _, platform in ipairs(platforms) do
|
||||||
|
assert.is_string(platform.id)
|
||||||
|
assert.is_string(platform.display_name)
|
||||||
|
assert.is_true(platform.display_name:match('^%u'))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('includes expected platforms with correct display names', function()
|
||||||
|
local platforms = picker.get_platforms()
|
||||||
|
local platform_map = {}
|
||||||
|
for _, p in ipairs(platforms) do
|
||||||
|
platform_map[p.id] = p.display_name
|
||||||
|
end
|
||||||
|
|
||||||
|
assert.equals('CodeForces', platform_map['codeforces'])
|
||||||
|
assert.equals('AtCoder', platform_map['atcoder'])
|
||||||
|
assert.equals('CSES', platform_map['cses'])
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('get_contests_for_platform', function()
|
||||||
|
it('returns empty list when scraper fails', function()
|
||||||
|
vim.system = function(cmd, opts)
|
||||||
|
return {
|
||||||
|
wait = function()
|
||||||
|
return { code = 1, stderr = 'test error' }
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local contests = picker.get_contests_for_platform('test_platform')
|
||||||
|
assert.is_table(contests)
|
||||||
|
assert.equals(0, #contests)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('returns empty list when JSON is invalid', function()
|
||||||
|
vim.system = function(cmd, opts)
|
||||||
|
return {
|
||||||
|
wait = function()
|
||||||
|
return { code = 0, stdout = 'invalid json' }
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local contests = picker.get_contests_for_platform('test_platform')
|
||||||
|
assert.is_table(contests)
|
||||||
|
assert.equals(0, #contests)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('returns contest list when scraper succeeds', function()
|
||||||
|
vim.system = function(cmd, opts)
|
||||||
|
return {
|
||||||
|
wait = function()
|
||||||
|
return {
|
||||||
|
code = 0,
|
||||||
|
stdout = vim.json.encode({
|
||||||
|
success = true,
|
||||||
|
contests = {
|
||||||
|
{
|
||||||
|
id = 'abc123',
|
||||||
|
name = 'AtCoder Beginner Contest 123',
|
||||||
|
display_name = 'Beginner Contest 123 (ABC)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id = '1951',
|
||||||
|
name = 'Educational Round 168',
|
||||||
|
display_name = 'Educational Round 168',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local contests = picker.get_contests_for_platform('test_platform')
|
||||||
|
assert.is_table(contests)
|
||||||
|
assert.equals(2, #contests)
|
||||||
|
assert.equals('abc123', contests[1].id)
|
||||||
|
assert.equals('AtCoder Beginner Contest 123', contests[1].name)
|
||||||
|
assert.equals('Beginner Contest 123 (ABC)', contests[1].display_name)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('get_problems_for_contest', function()
|
||||||
|
it('returns problems from cache when available', function()
|
||||||
|
local cache = require('cp.cache')
|
||||||
|
cache.load = function() end
|
||||||
|
cache.get_contest_data = function(platform, contest_id)
|
||||||
|
return {
|
||||||
|
problems = {
|
||||||
|
{ id = 'a', name = 'Problem A' },
|
||||||
|
{ id = 'b', name = 'Problem B' },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local problems = picker.get_problems_for_contest('test_platform', 'test_contest')
|
||||||
|
assert.is_table(problems)
|
||||||
|
assert.equals(2, #problems)
|
||||||
|
assert.equals('a', problems[1].id)
|
||||||
|
assert.equals('Problem A', problems[1].name)
|
||||||
|
assert.equals('a - Problem A', problems[1].display_name)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('falls back to scraping when cache miss', function()
|
||||||
|
local cache = require('cp.cache')
|
||||||
|
local scrape = require('cp.scrape')
|
||||||
|
|
||||||
|
cache.load = function() end
|
||||||
|
cache.get_contest_data = function()
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
scrape.scrape_contest_metadata = function(platform, contest_id)
|
||||||
|
return {
|
||||||
|
success = true,
|
||||||
|
problems = {
|
||||||
|
{ id = 'x', name = 'Problem X' },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local problems = picker.get_problems_for_contest('test_platform', 'test_contest')
|
||||||
|
assert.is_table(problems)
|
||||||
|
assert.equals(1, #problems)
|
||||||
|
assert.equals('x', problems[1].id)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('returns empty list when scraping fails', function()
|
||||||
|
local cache = require('cp.cache')
|
||||||
|
local scrape = require('cp.scrape')
|
||||||
|
|
||||||
|
cache.load = function() end
|
||||||
|
cache.get_contest_data = function()
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
scrape.scrape_contest_metadata = function(platform, contest_id)
|
||||||
|
return {
|
||||||
|
success = false,
|
||||||
|
error = 'test error',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local problems = picker.get_problems_for_contest('test_platform', 'test_contest')
|
||||||
|
assert.is_table(problems)
|
||||||
|
assert.equals(0, #problems)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('setup_problem', function()
|
||||||
|
it('calls cp.handle_command with correct arguments', function()
|
||||||
|
local cp = require('cp')
|
||||||
|
local called_with = nil
|
||||||
|
|
||||||
|
cp.handle_command = function(opts)
|
||||||
|
called_with = opts
|
||||||
|
end
|
||||||
|
|
||||||
|
picker.setup_problem('codeforces', '1951', 'a')
|
||||||
|
|
||||||
|
assert.is_table(called_with)
|
||||||
|
assert.is_table(called_with.fargs)
|
||||||
|
assert.equals('codeforces', called_with.fargs[1])
|
||||||
|
assert.equals('1951', called_with.fargs[2])
|
||||||
|
assert.equals('a', called_with.fargs[3])
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
78
spec/telescope_spec.lua
Normal file
78
spec/telescope_spec.lua
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
describe('cp.telescope', function()
|
||||||
|
local spec_helper = require('spec.spec_helper')
|
||||||
|
|
||||||
|
before_each(function()
|
||||||
|
spec_helper.setup()
|
||||||
|
|
||||||
|
package.preload['telescope'] = function()
|
||||||
|
return {
|
||||||
|
register_extension = function(ext_config)
|
||||||
|
return ext_config
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
package.preload['telescope.pickers'] = function()
|
||||||
|
return {
|
||||||
|
new = function(opts, picker_opts)
|
||||||
|
return {
|
||||||
|
find = function() end,
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
package.preload['telescope.finders'] = function()
|
||||||
|
return {
|
||||||
|
new_table = function(opts)
|
||||||
|
return opts
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
package.preload['telescope.config'] = function()
|
||||||
|
return {
|
||||||
|
values = {
|
||||||
|
generic_sorter = function()
|
||||||
|
return {}
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
package.preload['telescope.actions'] = function()
|
||||||
|
return {
|
||||||
|
select_default = {
|
||||||
|
replace = function() end,
|
||||||
|
},
|
||||||
|
close = function() end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
package.preload['telescope.actions.state'] = function()
|
||||||
|
return {
|
||||||
|
get_selected_entry = function()
|
||||||
|
return nil
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
after_each(function()
|
||||||
|
spec_helper.teardown()
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('module loading', function()
|
||||||
|
it('registers telescope extension without error', function()
|
||||||
|
assert.has_no.errors(function()
|
||||||
|
require('cp.pickers.telescope')
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('returns module with platform_picker function', function()
|
||||||
|
local telescope_cp = require('cp.pickers.telescope')
|
||||||
|
assert.is_table(telescope_cp)
|
||||||
|
assert.is_function(telescope_cp.platform_picker)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue