This commit is contained in:
Harivansh Rathi 2026-03-12 19:06:07 -04:00
parent 02c996d21a
commit 28622332a3
83 changed files with 6969 additions and 57 deletions

View file

@ -0,0 +1,23 @@
local M = {}
function M.on_attach(_, bufnr)
local function buf(mode, lhs, rhs)
bmap(mode, lhs, rhs, { buffer = bufnr })
end
buf('n', 'gd', vim.lsp.buf.definition)
buf('n', 'gD', vim.lsp.buf.declaration)
buf('n', '<C-]>', vim.lsp.buf.definition)
buf('n', 'gi', vim.lsp.buf.implementation)
buf('n', 'gr', vim.lsp.buf.references)
buf('n', 'K', vim.lsp.buf.hover)
buf('n', '<leader>rn', vim.lsp.buf.rename)
buf({ 'n', 'v' }, '<leader>ca', vim.lsp.buf.code_action)
buf('n', '<leader>f', function() vim.lsp.buf.format({ async = true }) end)
end
function M.capabilities()
return vim.lsp.protocol.make_client_capabilities()
end
return M

View file

@ -0,0 +1,238 @@
local M = {}
local constants = require('cp.constants')
local logger = require('cp.log')
local utils = require('cp.utils')
local function syshandle(result)
if result.code ~= 0 then
local msg = 'Scraper failed: ' .. (result.stderr or 'Unknown error')
return { success = false, error = msg }
end
local ok, data = pcall(vim.json.decode, result.stdout)
if not ok then
local msg = 'Failed to parse scraper output: ' .. tostring(data)
logger.log(msg, vim.log.levels.ERROR)
return { success = false, error = msg }
end
return { success = true, data = data }
end
local function spawn_env_list(env_map)
local out = {}
for key, value in pairs(env_map) do
out[#out + 1] = tostring(key) .. '=' .. tostring(value)
end
table.sort(out)
return out
end
---@param platform string
---@param subcommand string
---@param args string[]
---@param opts { sync?: boolean, ndjson?: boolean, on_event?: fun(ev: table), on_exit?: fun(result: table) }
local function run_scraper(platform, subcommand, args, opts)
if not utils.setup_python_env() then
local msg = 'no Python environment available (install uv or nix)'
logger.log(msg, vim.log.levels.ERROR)
if opts and opts.on_exit then
opts.on_exit({ success = false, error = msg })
end
return { success = false, error = msg }
end
local plugin_path = utils.get_plugin_path()
local cmd = utils.get_python_cmd(platform, plugin_path)
vim.list_extend(cmd, { subcommand })
vim.list_extend(cmd, args)
logger.log('scraper cmd: ' .. table.concat(cmd, ' '))
local env = vim.fn.environ()
env.VIRTUAL_ENV = ''
env.PYTHONPATH = ''
env.CONDA_PREFIX = ''
if opts and opts.ndjson then
local uv = vim.loop
local stdout = uv.new_pipe(false)
local stderr = uv.new_pipe(false)
local buf = ''
local handle
handle = uv.spawn(cmd[1], {
args = vim.list_slice(cmd, 2),
stdio = { nil, stdout, stderr },
env = spawn_env_list(env),
cwd = plugin_path,
}, function(code, signal)
if buf ~= '' and opts.on_event then
local ok_tail, ev_tail = pcall(vim.json.decode, buf)
if ok_tail then
opts.on_event(ev_tail)
end
buf = ''
end
if opts.on_exit then
opts.on_exit({ success = (code == 0), code = code, signal = signal })
end
if not stdout:is_closing() then
stdout:close()
end
if not stderr:is_closing() then
stderr:close()
end
if handle and not handle:is_closing() then
handle:close()
end
end)
if not handle then
logger.log('Failed to start scraper process', vim.log.levels.ERROR)
return { success = false, error = 'spawn failed' }
end
uv.read_start(stdout, function(_, data)
if data == nil then
if buf ~= '' and opts.on_event then
local ok_tail, ev_tail = pcall(vim.json.decode, buf)
if ok_tail then
opts.on_event(ev_tail)
end
buf = ''
end
return
end
buf = buf .. data
while true do
local s, e = buf:find('\n', 1, true)
if not s then
break
end
local line = buf:sub(1, s - 1)
buf = buf:sub(e + 1)
local ok, ev = pcall(vim.json.decode, line)
if ok and opts.on_event then
opts.on_event(ev)
end
end
end)
uv.read_start(stderr, function(_, _) end)
return
end
local sysopts = { text = true, timeout = 30000, env = env, cwd = plugin_path }
if opts and opts.sync then
local result = vim.system(cmd, sysopts):wait()
return syshandle(result)
else
vim.system(cmd, sysopts, function(result)
if opts and opts.on_exit then
return opts.on_exit(syshandle(result))
end
end)
end
end
function M.scrape_contest_metadata(platform, contest_id, callback)
run_scraper(platform, 'metadata', { contest_id }, {
on_exit = function(result)
if not result or not result.success then
logger.log(
("Failed to scrape metadata for %s contest '%s'."):format(
constants.PLATFORM_DISPLAY_NAMES[platform],
contest_id
),
vim.log.levels.ERROR
)
return
end
local data = result.data or {}
if not data.problems or #data.problems == 0 then
logger.log(
("No problems returned for %s contest '%s'."):format(
constants.PLATFORM_DISPLAY_NAMES[platform],
contest_id
),
vim.log.levels.ERROR
)
return
end
if type(callback) == 'function' then
callback(data)
end
end,
})
end
function M.scrape_contest_list(platform)
local result = run_scraper(platform, 'contests', {}, { sync = true })
if not result or not result.success or not (result.data and result.data.contests) then
logger.log(
('Could not scrape contests list for platform %s: %s'):format(
platform,
(result and result.error) or 'unknown'
),
vim.log.levels.ERROR
)
return {}
end
return result.data.contests
end
---@param platform string
---@param contest_id string
---@param callback fun(data: table)|nil
function M.scrape_all_tests(platform, contest_id, callback)
run_scraper(platform, 'tests', { contest_id }, {
ndjson = true,
on_event = function(ev)
if ev.done then
return
end
if ev.error and ev.problem_id then
logger.log(
("Failed to load tests for problem '%s' in contest '%s': %s"):format(
ev.problem_id,
contest_id,
ev.error
),
vim.log.levels.WARN
)
return
end
if not ev.problem_id or not ev.tests then
return
end
vim.schedule(function()
require('cp.utils').ensure_dirs()
local config = require('cp.config')
local base_name = config.default_filename(contest_id, ev.problem_id)
for i, t in ipairs(ev.tests) do
local input_file = 'io/' .. base_name .. '.' .. i .. '.cpin'
local expected_file = 'io/' .. base_name .. '.' .. i .. '.cpout'
local input_content = t.input:gsub('\r', '')
local expected_content = t.expected:gsub('\r', '')
vim.fn.writefile(vim.split(input_content, '\n'), input_file)
vim.fn.writefile(vim.split(expected_content, '\n'), expected_file)
end
if type(callback) == 'function' then
callback({
combined = ev.combined,
tests = ev.tests,
timeout_ms = ev.timeout_ms or 0,
memory_mb = ev.memory_mb or 0,
interactive = ev.interactive or false,
multi_test = ev.multi_test or false,
problem_id = ev.problem_id,
})
end
end)
end,
})
end
return M

View file

@ -0,0 +1,14 @@
return {
settings = {
Lua = {
diagnostics = { globals = { 'vim' } },
runtime = { version = 'LuaJIT' },
workspace = {
checkThirdParty = false,
library = { vim.env.VIMRUNTIME },
},
telemetry = { enable = false },
hint = { enable = true },
},
},
}

View file

@ -0,0 +1,12 @@
return {
settings = {
python = {
analysis = {
typeCheckingMode = 'basic',
autoSearchPaths = true,
useLibraryCodeForTypes = true,
diagnosticMode = 'workspace',
},
},
},
}

View file

@ -0,0 +1,19 @@
return {
settings = {
['rust-analyzer'] = {
checkOnSave = { command = 'clippy' },
cargo = { allFeatures = true },
procMacro = { enable = true },
diagnostics = { enable = true },
inlayHints = {
bindingModeHints = { enable = true },
chainingHints = { enable = true },
closingBraceHints = { enable = true },
closureReturnTypeHints = { enable = 'always' },
lifetimeElisionHints = { enable = 'always' },
parameterHints = { enable = true },
typeHints = { enable = true },
},
},
},
}

View file

@ -0,0 +1,26 @@
return {
settings = {
typescript = {
inlayHints = {
includeInlayParameterNameHints = 'all',
includeInlayParameterNameHintsWhenArgumentMatchesName = false,
includeInlayFunctionParameterTypeHints = true,
includeInlayVariableTypeHints = true,
includeInlayPropertyDeclarationTypeHints = true,
includeInlayFunctionLikeReturnTypeHints = true,
includeInlayEnumMemberValueHints = true,
},
},
javascript = {
inlayHints = {
includeInlayParameterNameHints = 'all',
includeInlayParameterNameHintsWhenArgumentMatchesName = false,
includeInlayFunctionParameterTypeHints = true,
includeInlayVariableTypeHints = true,
includeInlayPropertyDeclarationTypeHints = true,
includeInlayFunctionLikeReturnTypeHints = true,
includeInlayEnumMemberValueHints = true,
},
},
},
}

View file

@ -0,0 +1,186 @@
return {
{
'windwp/nvim-autopairs',
config = true,
},
{
'folke/flash.nvim',
opts = {
modes = { search = { enabled = true } },
},
config = function(_, opts)
require('flash').setup(opts)
map({ 'n', 'x', 'o' }, 's', function() require('flash').jump() end)
map({ 'n', 'x', 'o' }, 'S', function() require('flash').treesitter() end)
map('o', 'r', function() require('flash').remote() end)
map({ 'o', 'x' }, 'R', function() require('flash').treesitter_search() end)
map('c', '<c-s>', function() require('flash').toggle() end)
end,
},
{
'kylechui/nvim-surround',
config = true,
},
{
'kevinhwang91/nvim-ufo',
dependencies = { 'kevinhwang91/promise-async' },
opts = {
provider_selector = function()
return { 'treesitter', 'indent' }
end,
},
config = function(_, opts)
require('ufo').setup(opts)
map('n', 'zR', require('ufo').openAllFolds)
map('n', 'zM', require('ufo').closeAllFolds)
end,
},
{
enabled = false,
'barrettruth/cp.nvim',
dependencies = { 'ibhagwan/fzf-lua' },
init = function()
-- Keep uv cache in-project so cp.nvim scraping works in restricted environments.
if vim.env.UV_CACHE_DIR == nil or vim.env.UV_CACHE_DIR == '' then
local uv_cache_dir = vim.fn.getcwd() .. '/.uv-cache'
vim.fn.mkdir(uv_cache_dir, 'p')
vim.env.UV_CACHE_DIR = uv_cache_dir
end
vim.g.cp = {
languages = {
python = {
extension = 'py',
commands = {
run = { 'python3', '{source}' },
debug = { 'python3', '{source}' },
},
},
},
platforms = {
codeforces = {
enabled_languages = { 'python' },
default_language = 'python',
},
atcoder = {
enabled_languages = { 'python' },
default_language = 'python',
},
cses = {
enabled_languages = { 'python' },
default_language = 'python',
},
},
open_url = true,
ui = {
picker = 'fzf-lua',
},
}
end,
config = function()
local function open_url(url)
local ok_ui_open, opened = pcall(vim.ui.open, url)
if ok_ui_open and opened ~= false then
return
end
local opener = nil
if vim.fn.has('macunix') == 1 then
opener = 'open'
elseif vim.fn.has('unix') == 1 then
opener = 'xdg-open'
end
if opener then
vim.fn.jobstart({ opener, url }, { detach = true })
end
end
local function open_current_cp_problem_url()
local ok_state, state = pcall(require, 'cp.state')
local ok_cache, cache = pcall(require, 'cp.cache')
if not (ok_state and ok_cache) then
return
end
local platform = state.get_platform()
local contest_id = state.get_contest_id()
local problem_id = state.get_problem_id()
if not (platform and contest_id and problem_id) then
return
end
cache.load()
local contest = cache.get_contest_data(platform, contest_id)
if contest and contest.url then
open_url(contest.url:format(problem_id))
end
end
-- cp.nvim only opens URLs when first entering a contest; extend this locally for next/prev.
local ok_setup, setup = pcall(require, 'cp.setup')
local ok_config, cp_config = pcall(require, 'cp.config')
if ok_setup and ok_config and not setup._url_open_patch_applied then
local original_navigate_problem = setup.navigate_problem
setup.navigate_problem = function(direction, language)
local ok_state, state = pcall(require, 'cp.state')
local old_problem_id = ok_state and state.get_problem_id() or nil
original_navigate_problem(direction, language)
local cfg = cp_config.get_config()
local new_problem_id = ok_state and state.get_problem_id() or nil
local moved = old_problem_id ~= nil and new_problem_id ~= nil and old_problem_id ~= new_problem_id
if cfg and cfg.open_url and moved then
vim.schedule(open_current_cp_problem_url)
end
end
setup._url_open_patch_applied = true
end
map('n', '<leader>cr', '<cmd>CP run<cr>', { desc = 'CP run' })
map('n', '<leader>cp', '<cmd>CP panel<cr>', { desc = 'CP panel' })
map('n', '<leader>ce', '<cmd>CP edit<cr>', { desc = 'CP edit tests' })
map('n', '<leader>cn', '<cmd>CP next<cr>', { desc = 'CP next problem' })
map('n', '<leader>cN', '<cmd>CP prev<cr>', { desc = 'CP previous problem' })
map('n', '<leader>cc', '<cmd>CP pick<cr>', { desc = 'CP contest picker' })
map('n', '<leader>ci', '<cmd>CP interact<cr>', { desc = 'CP interact' })
map('n', '<leader>co', open_current_cp_problem_url, { desc = 'CP open problem url' })
end,
},
{
'supermaven-inc/supermaven-nvim',
opts = {
keymaps = {
accept_suggestion = '<Tab>',
clear_suggestion = '<C-]>',
accept_word = '<C-j>',
},
ignore_filetypes = { gitcommit = true },
color = {
suggestion_color = vim.api.nvim_get_hl(0, { name = 'Comment' }).fg,
cterm = 244,
},
},
},
{
'barrettruth/pending.nvim',
init = function()
map('n', '<leader>p', '<cmd>Pending<cr><cmd>only<cr>')
end,
},
{
'barrettruth/preview.nvim',
init = function()
vim.g.preview = {
typst = true,
latex = true,
github = {
output = function(ctx)
return '/tmp/' .. vim.fn.fnamemodify(ctx.file, ':t:r') .. '.html'
end,
},
mermaid = true,
}
end,
},
}

View file

@ -0,0 +1,74 @@
---@param kind 'issue'|'pr'
---@param state 'all'|'open'|'closed'
local function gh_picker(kind, state)
if vim.fn.executable('gh') ~= 1 then
vim.notify('gh CLI not found', vim.log.levels.WARN)
return
end
local next_state = ({ all = 'open', open = 'closed', closed = 'all' })[state]
local label = kind == 'pr' and 'PRs' or 'Issues'
require('fzf-lua').fzf_exec(('gh %s list --limit 100 --state %s'):format(kind, state), {
prompt = ('%s (%s)> '):format(label, state),
header = ':: <c-o> to toggle all/open/closed',
actions = {
['default'] = function(selected)
local num = selected[1]:match('^#?(%d+)')
if num then
vim.system({ 'gh', kind, 'view', num, '--web' })
end
end,
['ctrl-o'] = function()
gh_picker(kind, next_state)
end,
},
})
end
return {
'ibhagwan/fzf-lua',
dependencies = { 'nvim-tree/nvim-web-devicons' },
config = function(_, opts)
require('fzf-lua').setup(opts)
map('n', '<C-f>', function()
local fzf = require('fzf-lua')
local git_dir = vim.fn.system('git rev-parse --git-dir 2>/dev/null'):gsub('\n', '')
if vim.v.shell_error == 0 and git_dir ~= '' then
fzf.git_files()
else
fzf.files()
end
end)
map('n', '<leader>ff', '<cmd>FzfLua files<cr>')
map('n', '<leader>fg', '<cmd>FzfLua live_grep<cr>')
map('n', '<leader>fb', '<cmd>FzfLua buffers<cr>')
map('n', '<leader>fh', '<cmd>FzfLua help_tags<cr>')
map('n', '<leader>fr', '<cmd>FzfLua resume<cr>')
map('n', '<leader>fo', '<cmd>FzfLua oldfiles<cr>')
map('n', '<leader>fc', '<cmd>FzfLua commands<cr>')
map('n', '<leader>fk', '<cmd>FzfLua keymaps<cr>')
map('n', '<leader>f/', '<cmd>FzfLua search_history<cr>')
map('n', '<leader>f:', '<cmd>FzfLua command_history<cr>')
map('n', '<leader>fe', '<cmd>FzfLua files cwd=~/.config<cr>')
map('n', 'gq', '<cmd>FzfLua quickfix<cr>')
map('n', 'gl', '<cmd>FzfLua loclist<cr>')
map('n', '<leader>GB', '<cmd>FzfLua git_branches<cr>')
map('n', '<leader>Gc', '<cmd>FzfLua git_commits<cr>')
map('n', '<leader>Gs', '<cmd>FzfLua git_status<cr>')
map('n', '<leader>Gp', function() gh_picker('pr', 'open') end)
map('n', '<leader>Gi', function() gh_picker('issue', 'open') end)
end,
opts = {
'default-title',
winopts = {
border = 'single',
preview = {
layout = 'vertical',
vertical = 'down:50%',
},
},
fzf_opts = {
['--layout'] = 'reverse',
},
},
}

View file

@ -0,0 +1,91 @@
local function file_loc()
local root = vim.trim(vim.fn.system('git rev-parse --show-toplevel'))
if vim.v.shell_error ~= 0 or root == '' then
return nil
end
local path = vim.api.nvim_buf_get_name(0)
if path == '' or path:sub(1, #root + 1) ~= root .. '/' then
return nil
end
return ('%s:%d'):format(path:sub(#root + 2), vim.fn.line('.'))
end
local function gh_browse()
if vim.fn.executable('gh') ~= 1 then
vim.notify('gh CLI not found', vim.log.levels.WARN)
return
end
local loc = file_loc()
if loc then
vim.system({ 'gh', 'browse', loc })
else
vim.system({ 'gh', 'browse' })
end
end
return {
{
'tpope/vim-fugitive',
config = function()
map('n', '<leader>gg', '<cmd>Git<cr><cmd>only<cr>')
map('n', '<leader>gc', '<cmd>Git commit<cr>')
map('n', '<leader>gp', '<cmd>Git push<cr>')
map('n', '<leader>gl', '<cmd>Git pull<cr>')
map('n', '<leader>gb', '<cmd>Git blame<cr>')
map('n', '<leader>gd', '<cmd>Gvdiffsplit<cr>')
map('n', '<leader>gr', '<cmd>Gread<cr>')
map('n', '<leader>gw', '<cmd>Gwrite<cr>')
map({ 'n', 'v' }, '<leader>go', gh_browse)
end,
},
{
'lewis6991/gitsigns.nvim',
opts = {
signs = {
add = { text = '██' },
change = { text = '██' },
delete = { text = '▄▄' },
topdelete = { text = '▀▀' },
changedelete = { text = '██' },
},
signs_staged = {
add = { text = '▓▓' },
change = { text = '▓▓' },
delete = { text = '▄▄' },
topdelete = { text = '▀▀' },
changedelete = { text = '▓▓' },
},
signs_staged_enable = true,
},
config = function(_, opts)
require('gitsigns').setup(opts)
map('n', ']g', '<cmd>Gitsigns next_hunk<cr>')
map('n', '[g', '<cmd>Gitsigns prev_hunk<cr>')
map('n', '<leader>ghs', '<cmd>Gitsigns stage_hunk<cr>')
map('n', '<leader>ghr', '<cmd>Gitsigns reset_hunk<cr>')
map('n', '<leader>ghp', '<cmd>Gitsigns preview_hunk<cr>')
map('n', '<leader>gB', '<cmd>Gitsigns toggle_current_line_blame<cr>')
end,
},
{
'barrettruth/diffs.nvim',
enabled = true,
init = function()
vim.g.diffs = {
integrations = {
fugitive = {
enabled = true,
horizontal = false,
vertical = false,
},
},
hide_prefix = true,
highlights = {
gutter = true,
blend_alpha = 0.4,
intra = { enabled = true },
},
}
end,
},
}

View file

@ -0,0 +1,4 @@
return {
'neovim/nvim-lspconfig',
lazy = false,
}

View file

@ -0,0 +1,33 @@
return {
'barrettruth/oil.nvim',
dependencies = {
'nvim-tree/nvim-web-devicons',
{
'malewicz1337/oil-git.nvim',
opts = {
show_ignored_files = false,
show_ignored_directories = false,
debounce_ms = 300,
},
},
},
opts = {
default_file_explorer = true,
columns = { 'icon' },
view_options = { show_hidden = true },
keymaps = {
['g?'] = 'actions.show_help',
['<CR>'] = 'actions.select',
['<C-v>'] = 'actions.select_vsplit',
['<C-x>'] = 'actions.select_split',
['<C-p>'] = 'actions.preview',
['<C-c>'] = 'actions.close',
['-'] = 'actions.parent',
['g.'] = 'actions.toggle_hidden',
},
},
init = function()
map('n', '-', '<cmd>Oil<cr>')
map('n', '<leader>e', '<cmd>Oil<cr>')
end,
}

View file

@ -0,0 +1,61 @@
return {
{
'nvim-treesitter/nvim-treesitter',
build = ':TSUpdate',
dependencies = { 'nvim-treesitter/nvim-treesitter-textobjects' },
config = function()
require('nvim-treesitter.configs').setup({
auto_install = true,
highlight = { enable = true },
indent = { enable = true },
textobjects = {
select = {
enable = true,
lookahead = true,
keymaps = {
['af'] = '@function.outer',
['if'] = '@function.inner',
['ac'] = '@class.outer',
['ic'] = '@class.inner',
['aa'] = '@parameter.outer',
['ia'] = '@parameter.inner',
['ai'] = '@conditional.outer',
['ii'] = '@conditional.inner',
['al'] = '@loop.outer',
['il'] = '@loop.inner',
['ab'] = '@block.outer',
['ib'] = '@block.inner',
},
},
move = {
enable = true,
set_jumps = true,
goto_next_start = {
[']f'] = '@function.outer',
[']c'] = '@class.outer',
[']a'] = '@parameter.inner',
},
goto_next_end = {
[']F'] = '@function.outer',
[']C'] = '@class.outer',
},
goto_previous_start = {
['[f'] = '@function.outer',
['[c'] = '@class.outer',
['[a'] = '@parameter.inner',
},
goto_previous_end = {
['[F'] = '@function.outer',
['[C'] = '@class.outer',
},
},
swap = {
enable = true,
swap_next = { ['<leader>sn'] = '@parameter.inner' },
swap_previous = { ['<leader>sp'] = '@parameter.inner' },
},
},
})
end,
},
}

View file

@ -0,0 +1,42 @@
return {
{
'ellisonleao/gruvbox.nvim',
lazy = false,
priority = 1000,
config = function()
require('gruvbox').setup({
contrast = 'hard',
transparent_mode = true,
italic = { comments = true },
overrides = {
MatchParen = { bold = true, underline = true, bg = '' },
},
})
vim.cmd.colorscheme('gruvbox')
end,
},
{
'nvim-lualine/lualine.nvim',
dependencies = { 'nvim-tree/nvim-web-devicons' },
opts = {
options = {
theme = 'gruvbox',
icons_enabled = false,
component_separators = '',
section_separators = { left = '', right = '' },
},
sections = {
lualine_a = { 'mode' },
lualine_b = { 'FugitiveHead', 'diff' },
lualine_c = { { 'filename', path = 0 } },
lualine_x = { 'diagnostics' },
lualine_y = { 'filetype' },
lualine_z = { 'progress' },
},
},
},
{
'barrettruth/nonicons.nvim',
dependencies = { 'nvim-tree/nvim-web-devicons' },
},
}