Problem: setup_python_env() is called from check_required_runtime() during config.setup(), which runs on the very first :CP command. The uv sync and nix build calls use vim.system():wait(), blocking the Neovim event loop. During the block the UI is frozen and vim.schedule-based log messages never render, so the user sees an unresponsive editor with no feedback. Solution: remove setup_python_env() from check_required_runtime() so config init is instant. Call it lazily from run_scraper() instead, only when a scraper subprocess is actually needed. Use vim.notify + vim.cmd.redraw() before blocking calls so the notification renders immediately via a forced screen repaint, rather than being queued behind vim.schedule.
362 lines
8.9 KiB
Lua
362 lines
8.9 KiB
Lua
local M = {}
|
|
|
|
local logger = require('cp.log')
|
|
|
|
local _nix_python = nil
|
|
local _nix_discovered = false
|
|
|
|
local uname = vim.loop.os_uname()
|
|
|
|
local _time_cached = false
|
|
local _time_path = nil
|
|
local _time_reason = nil
|
|
local _timeout_cached = false
|
|
local _timeout_path = nil
|
|
local _timeout_reason = nil
|
|
|
|
local function is_windows()
|
|
return uname.sysname == 'Windows_NT'
|
|
end
|
|
|
|
local function check_time_is_gnu_time(bin)
|
|
local ok = vim.fn.executable(bin) == 1
|
|
if not ok then
|
|
return false
|
|
end
|
|
local r = vim.system({ bin, '--version' }, { text = true }):wait()
|
|
if r and r.code == 0 and r.stdout and r.stdout:lower():find('gnu time', 1, true) then
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function find_gnu_time()
|
|
if _time_cached then
|
|
return _time_path, _time_reason
|
|
end
|
|
|
|
if is_windows() then
|
|
_time_cached = true
|
|
_time_path = nil
|
|
_time_reason = 'unsupported on Windows'
|
|
return _time_path, _time_reason
|
|
end
|
|
|
|
local candidates
|
|
if uname and uname.sysname == 'Darwin' then
|
|
candidates = { 'gtime', '/opt/homebrew/bin/gtime', '/usr/local/bin/gtime' }
|
|
else
|
|
candidates = { '/usr/bin/time', 'time' }
|
|
end
|
|
|
|
for _, bin in ipairs(candidates) do
|
|
if check_time_is_gnu_time(bin) then
|
|
_time_cached = true
|
|
_time_path = bin
|
|
_time_reason = nil
|
|
return _time_path, _time_reason
|
|
end
|
|
end
|
|
|
|
_time_cached = true
|
|
_time_path = nil
|
|
if uname and uname.sysname == 'Darwin' then
|
|
_time_reason = 'GNU time not found (install via: brew install coreutils)'
|
|
else
|
|
_time_reason = 'GNU time not found'
|
|
end
|
|
return _time_path, _time_reason
|
|
end
|
|
|
|
---@return string|nil path to GNU time binary
|
|
function M.time_path()
|
|
local path = find_gnu_time()
|
|
return path
|
|
end
|
|
|
|
---@return {ok:boolean, path:string|nil, reason:string|nil}
|
|
function M.time_capability()
|
|
local path, reason = find_gnu_time()
|
|
return { ok = path ~= nil, path = path, reason = reason }
|
|
end
|
|
|
|
---@return string
|
|
function M.get_plugin_path()
|
|
local plugin_path = debug.getinfo(1, 'S').source:sub(2)
|
|
return vim.fn.fnamemodify(plugin_path, ':h:h:h')
|
|
end
|
|
|
|
---@return boolean
|
|
function M.is_nix_build()
|
|
return _nix_python ~= nil
|
|
end
|
|
|
|
---@return string|nil
|
|
function M.get_nix_python()
|
|
return _nix_python
|
|
end
|
|
|
|
---@return boolean
|
|
function M.is_nix_discovered()
|
|
return _nix_discovered
|
|
end
|
|
|
|
---@param module string
|
|
---@param plugin_path string
|
|
---@return string[]
|
|
function M.get_python_cmd(module, plugin_path)
|
|
if _nix_python then
|
|
return { _nix_python, '-m', 'scrapers.' .. module }
|
|
end
|
|
return { 'uv', 'run', '--directory', plugin_path, '-m', 'scrapers.' .. module }
|
|
end
|
|
|
|
local python_env_setup = false
|
|
|
|
---@return boolean
|
|
local function discover_nix_python()
|
|
local cache_dir = vim.fn.stdpath('cache') .. '/cp-nvim'
|
|
local cache_file = cache_dir .. '/nix-python'
|
|
|
|
local f = io.open(cache_file, 'r')
|
|
if f then
|
|
local cached = f:read('*l')
|
|
f:close()
|
|
if cached and vim.fn.executable(cached) == 1 then
|
|
_nix_python = cached
|
|
return true
|
|
end
|
|
end
|
|
|
|
local plugin_path = M.get_plugin_path()
|
|
vim.notify('[cp.nvim] Building Python environment with nix...', vim.log.levels.INFO)
|
|
vim.cmd.redraw()
|
|
local result = vim
|
|
.system(
|
|
{ 'nix', 'build', plugin_path .. '#pythonEnv', '--no-link', '--print-out-paths' },
|
|
{ text = true }
|
|
)
|
|
:wait()
|
|
|
|
if result.code ~= 0 then
|
|
logger.log('nix build #pythonEnv failed: ' .. (result.stderr or ''), vim.log.levels.WARN)
|
|
return false
|
|
end
|
|
|
|
local store_path = result.stdout:gsub('%s+$', '')
|
|
local python_path = store_path .. '/bin/python3'
|
|
|
|
if vim.fn.executable(python_path) ~= 1 then
|
|
logger.log('nix python not executable at ' .. python_path, vim.log.levels.WARN)
|
|
return false
|
|
end
|
|
|
|
vim.fn.mkdir(cache_dir, 'p')
|
|
f = io.open(cache_file, 'w')
|
|
if f then
|
|
f:write(python_path)
|
|
f:close()
|
|
end
|
|
|
|
_nix_python = python_path
|
|
_nix_discovered = true
|
|
return true
|
|
end
|
|
|
|
---@return boolean success
|
|
function M.setup_python_env()
|
|
if python_env_setup then
|
|
return true
|
|
end
|
|
|
|
if _nix_python then
|
|
logger.log('Python env: nix (python=' .. _nix_python .. ')')
|
|
python_env_setup = true
|
|
return true
|
|
end
|
|
|
|
if vim.fn.executable('uv') == 1 then
|
|
local plugin_path = M.get_plugin_path()
|
|
logger.log('Python env: uv sync (dir=' .. plugin_path .. ')')
|
|
vim.notify('[cp.nvim] Setting up Python environment...', vim.log.levels.INFO)
|
|
vim.cmd.redraw()
|
|
|
|
local env = vim.fn.environ()
|
|
env.VIRTUAL_ENV = ''
|
|
env.PYTHONPATH = ''
|
|
env.CONDA_PREFIX = ''
|
|
local result = vim
|
|
.system({ 'uv', 'sync' }, { cwd = plugin_path, text = true, env = env })
|
|
:wait()
|
|
if result.code ~= 0 then
|
|
logger.log('Failed to setup Python environment: ' .. result.stderr, vim.log.levels.ERROR)
|
|
return false
|
|
end
|
|
if result.stderr and result.stderr ~= '' then
|
|
logger.log('uv sync stderr: ' .. result.stderr:gsub('%s+$', ''))
|
|
end
|
|
|
|
python_env_setup = true
|
|
return true
|
|
end
|
|
|
|
if vim.fn.executable('nix') == 1 then
|
|
logger.log('Python env: nix discovery')
|
|
if discover_nix_python() then
|
|
python_env_setup = true
|
|
return true
|
|
end
|
|
end
|
|
|
|
logger.log(
|
|
'No Python environment available. Install uv (https://docs.astral.sh/uv/) or use nix.',
|
|
vim.log.levels.WARN
|
|
)
|
|
return false
|
|
end
|
|
|
|
--- Configure the buffer with good defaults
|
|
---@param filetype? string
|
|
function M.create_buffer_with_options(filetype)
|
|
local buf = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_set_option_value('bufhidden', 'hide', { buf = buf })
|
|
vim.api.nvim_set_option_value('readonly', true, { buf = buf })
|
|
vim.api.nvim_set_option_value('modifiable', false, { buf = buf })
|
|
|
|
if filetype then
|
|
vim.api.nvim_set_option_value('filetype', filetype, { buf = buf })
|
|
end
|
|
return buf
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@param lines string[]
|
|
---@param highlights? Highlight[]
|
|
---@param namespace? integer
|
|
function M.update_buffer_content(bufnr, lines, highlights, namespace)
|
|
local was_readonly = vim.api.nvim_get_option_value('readonly', { buf = bufnr })
|
|
|
|
vim.api.nvim_set_option_value('readonly', false, { buf = bufnr })
|
|
vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr })
|
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
|
vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr })
|
|
vim.api.nvim_set_option_value('readonly', was_readonly, { buf = bufnr })
|
|
|
|
if highlights and namespace then
|
|
local highlight = require('cp.ui.highlight')
|
|
highlight.apply_highlights(bufnr, highlights, namespace)
|
|
end
|
|
end
|
|
|
|
function M.check_required_runtime()
|
|
if is_windows() then
|
|
return false, 'Windows is not supported'
|
|
end
|
|
|
|
if vim.fn.has('nvim-0.10.0') ~= 1 then
|
|
return false, 'Neovim 0.10.0+ required'
|
|
end
|
|
|
|
local time = M.time_capability()
|
|
if not time.ok then
|
|
return false, time.reason
|
|
end
|
|
|
|
local timeout = M.timeout_capability()
|
|
if not timeout.ok then
|
|
return false, timeout.reason
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local function check_timeout_is_gnu_timeout(bin)
|
|
if vim.fn.executable(bin) ~= 1 then
|
|
return false
|
|
end
|
|
local r = vim.system({ bin, '--version' }, { text = true }):wait()
|
|
if r and r.code == 0 and r.stdout then
|
|
local s = r.stdout:lower()
|
|
if s:find('gnu coreutils', 1, true) or s:find('timeout %(gnu coreutils%)', 1, true) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function find_gnu_timeout()
|
|
if _timeout_cached then
|
|
return _timeout_path, _timeout_reason
|
|
end
|
|
|
|
if is_windows() then
|
|
_timeout_cached = true
|
|
_timeout_path = nil
|
|
_timeout_reason = 'unsupported on Windows'
|
|
return _timeout_path, _timeout_reason
|
|
end
|
|
|
|
local candidates
|
|
if uname and uname.sysname == 'Darwin' then
|
|
candidates = { 'gtimeout', '/opt/homebrew/bin/gtimeout', '/usr/local/bin/gtimeout' }
|
|
else
|
|
candidates = { '/usr/bin/timeout', 'timeout' }
|
|
end
|
|
|
|
for _, bin in ipairs(candidates) do
|
|
if check_timeout_is_gnu_timeout(bin) then
|
|
_timeout_cached = true
|
|
_timeout_path = bin
|
|
_timeout_reason = nil
|
|
return _timeout_path, _timeout_reason
|
|
end
|
|
end
|
|
|
|
_timeout_cached = true
|
|
_timeout_path = nil
|
|
if uname and uname.sysname == 'Darwin' then
|
|
_timeout_reason = 'GNU timeout not found (install via: brew install coreutils)'
|
|
else
|
|
_timeout_reason = 'GNU timeout not found'
|
|
end
|
|
return _timeout_path, _timeout_reason
|
|
end
|
|
|
|
function M.timeout_path()
|
|
local path = find_gnu_timeout()
|
|
return path
|
|
end
|
|
|
|
function M.timeout_capability()
|
|
local path, reason = find_gnu_timeout()
|
|
return { ok = path ~= nil, path = path, reason = reason }
|
|
end
|
|
|
|
function M.cwd_executables()
|
|
local uv = vim.uv or vim.loop
|
|
local req = uv.fs_scandir('.')
|
|
if not req then
|
|
return {}
|
|
end
|
|
local out = {}
|
|
while true do
|
|
local name, t = uv.fs_scandir_next(req)
|
|
if not name then
|
|
break
|
|
end
|
|
if t == 'file' or t == 'link' then
|
|
local path = './' .. name
|
|
if vim.fn.executable(path) == 1 then
|
|
out[#out + 1] = name
|
|
end
|
|
end
|
|
end
|
|
table.sort(out)
|
|
return out
|
|
end
|
|
|
|
function M.ensure_dirs()
|
|
vim.system({ 'mkdir', '-p', 'build', 'io' }):wait()
|
|
end
|
|
|
|
return M
|