local vim = _G.vim
local M = {}
local function debug_print(...)
if vim.g.cargo_nvim_debug then
print(string.format("[cargo.nvim] %s", table.concat({ ... }, " ")))
end
end
local default_opts = {
debug = false,
float_window = true,
window_width = 0.8,
window_height = 0.8,
border = "rounded",
auto_close = true,
close_timeout = 30000,
show_line_numbers = true,
show_cursor_line = true,
wrap_output = true, show_progress = true,
run_timeout = 300, interactive_timeout = 30, input_field_height = 1,
force_interactive_run = true, max_inactivity_warnings = 3, detect_proconio = true,
force_smart_detection = true,
commands = {
bench = { nargs = "*", desc = "Run benchmarks" },
build = { nargs = "*", desc = "Compile package" },
clean = { nargs = "*", desc = "Clean target directory" },
doc = { nargs = "*", desc = "Build documentation" },
new = { nargs = 1, desc = "Create new package" },
run = { nargs = "*", desc = "Run package" },
test = { nargs = "*", desc = "Run tests" },
update = { nargs = "*", desc = "Update dependencies" },
check = { nargs = "*", desc = "Check package" },
init = { nargs = "*", desc = "Initialize package" },
add = { nargs = "+", desc = "Add dependency" },
remove = { nargs = "+", desc = "Remove dependency" },
fmt = { nargs = "*", desc = "Format code" },
clippy = { nargs = "*", desc = "Run clippy" },
fix = { nargs = "*", desc = "Auto-fix warnings" },
publish = { nargs = "*", desc = "Publish package" },
install = { nargs = "+", desc = "Install binary" },
uninstall = { nargs = "+", desc = "Uninstall binary" },
search = { nargs = "+", desc = "Search packages" },
tree = { nargs = "*", desc = "Show dep tree" },
vendor = { nargs = "*", desc = "Vendor dependencies" },
audit = { nargs = "*", desc = "Audit dependencies" },
outdated = { nargs = "*", desc = "Check outdated deps" },
autodd = { nargs = "*", desc = "Auto-manage dependencies" },
},
keymaps = {
close = "q",
scroll_up = "<C-u>",
scroll_down = "<C-d>",
scroll_top = "gg",
scroll_bottom = "G",
interrupt = "<C-c>",
send_input = "i",
toggle_wrap = "w",
copy_output = "y",
clear_output = "c",
},
timeouts = {
default = 300, run = 600, test = 600, bench = 900, build = 600, },
process_monitoring = {
enabled = true,
check_interval = 5, memory_limit = 1024, cpu_limit = 90, },
interactive = {
enabled = true,
prompt_patterns = {
"^%s*>%s*$", "^%s*%[y/N%]:%s*$", "^%s*Password:%s*$", "^%s*Input:%s*$", },
history_size = 50,
},
}
local function load_cargo_lib()
local plugin_dir = vim.fn.fnamemodify(vim.fn.resolve(debug.getinfo(1, "S").source:sub(2)), ":h:h:h")
debug_print("Plugin directory:", plugin_dir)
local lib_name = vim.fn.has("mac") == 1 and "libcargo_nvim.dylib"
or vim.fn.has("win32") == 1 and "cargo_nvim.dll"
or "libcargo_nvim.so"
local lib_path = plugin_dir .. "/target/release/" .. lib_name
debug_print("Looking for library at:", lib_path)
if vim.fn.filereadable(lib_path) == 0 then
local help_msg = string.format(
[[Cargo library not found at path: %s
To fix this issue:
1. Ensure you have the required dependencies installed:
- Ubuntu/Debian: sudo apt install libluajit-5.1-dev
- macOS: brew install luajit
- Arch Linux: sudo pacman -S luajit
2. Run the build command manually:
cd %s && cargo build --release
3. If using lazy.nvim, ensure your config includes:
build = "cargo build --release"
For more details, see: https://github.com/nwiizo/cargo.nvim#-requirements]],
lib_path,
vim.fn.fnamemodify(lib_path, ":h:h:h")
)
error(help_msg)
end
local loaded, err = package.loadlib(lib_path, "luaopen_cargo_nvim")
if not loaded then
error(string.format("Failed to load library %s: %s", lib_path, err or "unknown error"))
end
local cargo = loaded()
if not cargo then
error("Failed to initialize cargo module")
end
debug_print("Successfully loaded cargo library")
return cargo
end
local cargo_lib = nil
local function setup_highlights()
local highlights = {
CargoError = { fg = "#ff5555", bold = true },
CargoWarning = { fg = "#ffb86c", bold = true },
CargoSuccess = { fg = "#50fa7b", bold = true },
CargoInfo = { fg = "#8be9fd" },
CargoHeader = { fg = "#bd93f9", bold = true },
CargoCommand = { fg = "#6272a4", italic = true },
CargoProgress = { fg = "#50fa7b" },
}
for name, attrs in pairs(highlights) do
vim.api.nvim_set_hl(0, name, attrs)
end
local syntax_cmds = {
"syntax match CargoCommand /@command@.*/",
"syntax match CargoError /@error@.*/",
"syntax match CargoWarning /@warning@.*/",
"syntax match CargoSuccess /@success@.*/",
"syntax match CargoInfo /@info@.*/",
"syntax match CargoHeader /@header@.*/",
"syntax match CargoProgress /@progress@.*/",
"syntax match CargoHiddenTag /@\\(command\\|error\\|warning\\|success\\|info\\|header\\|progress\\)@/ conceal",
}
vim.api.nvim_create_autocmd("FileType", {
pattern = "cargo-output",
callback = function()
for _, cmd in ipairs(syntax_cmds) do
vim.cmd(cmd)
end
vim.opt_local.conceallevel = 2
vim.opt_local.concealcursor = "nvc"
end,
})
end
local function create_float_win(opts)
local width = math.floor(vim.o.columns * opts.window_width)
local height = math.floor(vim.o.lines * opts.window_height)
local bufnr = vim.api.nvim_create_buf(false, true)
local win_opts = {
relative = "editor",
width = width,
height = height,
col = math.floor((vim.o.columns - width) / 2),
row = math.floor((vim.o.lines - height) / 2),
style = "minimal",
border = opts.border,
title = opts.title,
title_pos = "center",
}
local winnr = vim.api.nvim_open_win(bufnr, true, win_opts)
vim.bo[bufnr].buftype = "nofile"
vim.bo[bufnr].swapfile = false
vim.bo[bufnr].modifiable = true
vim.bo[bufnr].filetype = "cargo-output"
if opts.show_line_numbers then
vim.wo[winnr].number = true
end
vim.wo[winnr].wrap = opts.wrap_output
vim.wo[winnr].cursorline = opts.show_cursor_line
local function set_keymap(mode, key, action)
vim.api.nvim_buf_set_keymap(bufnr, mode, key, action, {
nowait = true,
noremap = true,
silent = true,
})
end
set_keymap("n", opts.keymaps.close, ":q<CR>")
set_keymap("n", "<Esc>", ":q<CR>")
set_keymap("n", opts.keymaps.interrupt, '<cmd>lua require("cargo").interrupt()<CR>')
if opts.keymaps.send_input then
set_keymap("n", opts.keymaps.send_input, '<cmd>lua require("cargo").send_input()<CR>')
end
set_keymap("n", opts.keymaps.scroll_up, "<C-u>")
set_keymap("n", opts.keymaps.scroll_down, "<C-d>")
set_keymap("n", opts.keymaps.scroll_top, "gg")
set_keymap("n", opts.keymaps.scroll_bottom, "G")
set_keymap("n", opts.keymaps.toggle_wrap, "<cmd>lua vim.wo.wrap = not vim.wo.wrap<CR>")
set_keymap("n", opts.keymaps.copy_output, "<cmd>%y+<CR>")
set_keymap("n", opts.keymaps.clear_output, "<cmd>%delete_<CR>")
return bufnr, winnr
end
local function format_output_line(line)
local timestamp = os.date("%H:%M:%S")
if line:match("^%s*[Ee]rror") then
return string.format("[%s] @error@%s", timestamp, line)
elseif line:match("^%s*[Ww]arning") then
return string.format("[%s] @warning@%s", timestamp, line)
elseif line:match("^%s*Compiling") or line:match("^%s*Running") or line:match("^%s*Checking") then
return string.format("[%s] @info@%s", timestamp, line)
elseif line:match("^%s*Finished") then
return string.format("[%s] @success@%s", timestamp, line)
end
return string.format("[%s] %s", timestamp, line)
end
local function execute_command_native(cmd_name, args, opts)
vim.cmd("wa")
local bufnr, winnr = create_float_win({
title = string.format(" Cargo %s ", cmd_name:upper()),
window_width = opts.window_width,
window_height = opts.window_height,
border = opts.border,
show_line_numbers = opts.show_line_numbers,
wrap_output = opts.wrap_output,
show_cursor_line = opts.show_cursor_line,
keymaps = opts.keymaps,
})
local args_str = #args > 0 and (" " .. table.concat(args, " ")) or ""
local cmd_line = string.format("cargo %s%s", cmd_name, args_str)
vim.bo[bufnr].modifiable = true
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
"@command@" .. cmd_line,
string.rep("─", vim.api.nvim_win_get_width(winnr) - 2),
"",
})
vim.bo[bufnr].modifiable = false
local job_id = cargo_lib.start(cmd_name, args)
vim.b[bufnr].cargo_job_id = job_id
local timer = (vim.uv or vim.loop).new_timer()
local function stop_timer()
if timer and not timer:is_closing() then
timer:stop()
timer:close()
end
timer = nil
end
timer:start(
0,
opts.poll_interval or 50,
vim.schedule_wrap(function()
if not vim.api.nvim_buf_is_valid(bufnr) then
cargo_lib.interrupt(job_id)
stop_timer()
return
end
local lines, finished, success = cargo_lib.poll(job_id)
if lines and #lines > 0 then
local formatted = {}
for _, line in ipairs(lines) do
formatted[#formatted + 1] = format_output_line(line)
end
vim.bo[bufnr].modifiable = true
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, formatted)
vim.bo[bufnr].modifiable = false
if vim.api.nvim_win_is_valid(winnr) then
vim.api.nvim_win_set_cursor(winnr, { vim.api.nvim_buf_line_count(bufnr), 0 })
end
end
if finished then
stop_timer()
vim.b[bufnr].cargo_job_id = nil
vim.bo[bufnr].modifiable = true
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, {
"",
success and "@success@Command completed successfully." or "@error@Command finished with errors.",
"@info@Press q to close",
})
vim.bo[bufnr].modifiable = false
if opts.auto_close and success and vim.api.nvim_win_is_valid(winnr) then
vim.defer_fn(function()
if vim.api.nvim_win_is_valid(winnr) then
vim.api.nvim_win_close(winnr, true)
end
end, opts.close_timeout)
end
end
end)
)
return bufnr, winnr
end
function M.interrupt()
local bufnr = vim.api.nvim_get_current_buf()
local job_id = vim.b[bufnr].cargo_job_id
if job_id and cargo_lib and cargo_lib.interrupt then
vim.api.nvim_echo({ { "Interrupting cargo process...", "WarningMsg" } }, true, {})
cargo_lib.interrupt(job_id)
end
end
function M.send_input()
local bufnr = vim.api.nvim_get_current_buf()
local job_id = vim.b[bufnr].cargo_job_id
if not (job_id and cargo_lib and cargo_lib.send_input) then
return
end
vim.ui.input({ prompt = "stdin> " }, function(input)
if input ~= nil then
cargo_lib.send_input(job_id, input .. "\n")
end
end)
end
function M.setup(opts)
opts = vim.tbl_deep_extend("force", default_opts, opts or {})
if opts.debug then
vim.g.cargo_nvim_debug = true
debug_print("Debug mode enabled")
end
debug_print("Loading cargo library...")
cargo_lib = load_cargo_lib()
setup_highlights()
for cmd_name, cmd_opts in pairs(opts.commands) do
local command_name = "Cargo" .. cmd_name:sub(1, 1):upper() .. cmd_name:sub(2)
debug_print("Registering command:", command_name)
local subcommand = cmd_opts.cmd or cmd_name
local preset_args = cmd_opts.args or {}
vim.api.nvim_create_user_command(command_name, function(args)
local cmd_args = {}
for _, arg in ipairs(preset_args) do
table.insert(cmd_args, arg)
end
if args.args and args.args ~= "" then
for _, arg in ipairs(vim.split(args.args, "%s+")) do
if arg and arg ~= "" then
table.insert(cmd_args, arg)
end
end
end
execute_command_native(subcommand, cmd_args, opts)
end, {
nargs = cmd_opts.nargs or "*",
desc = cmd_opts.desc,
})
end
vim.api.nvim_create_user_command("CargoRunTerm", function(args)
local cmd_args = {}
if args.args and args.args ~= "" then
for _, arg in ipairs(vim.split(args.args, "%s+")) do
if arg and arg ~= "" then
table.insert(cmd_args, arg)
end
end
end
M.run_in_terminal(cmd_args)
end, {
nargs = "*",
desc = "Run cargo in interactive terminal mode (for proconio etc.)",
complete = function(ArgLead, _, _)
local completions = {
"--release",
"--bin",
"--example",
"--package",
"--target",
}
local matches = {}
for _, comp in ipairs(completions) do
if comp:find(ArgLead, 1, true) == 1 then
table.insert(matches, comp)
end
end
return matches
end,
})
debug_print("Plugin setup completed")
end
function M.run_in_terminal(args)
local width = math.floor(vim.o.columns * 0.8)
local height = math.floor(vim.o.lines * 0.8)
local bufnr = vim.api.nvim_create_buf(false, true)
local winnr = vim.api.nvim_open_win(bufnr, true, {
relative = "editor",
width = width,
height = height,
col = math.floor((vim.o.columns - width) / 2),
row = math.floor((vim.o.lines - height) / 2),
style = "minimal",
border = "rounded",
title = " Cargo RUN (Terminal) ",
title_pos = "center",
})
vim.wo[winnr].number = true
vim.wo[winnr].wrap = true
vim.wo[winnr].cursorline = true
local args_str = table.concat(args, " ")
local cmd = "cargo run " .. args_str
local _ = vim.fn.termopen(cmd, {
on_exit = function()
vim.schedule(function()
if vim.api.nvim_buf_is_valid(bufnr) then
vim.bo[bufnr].modifiable = true
vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, {
"",
"=== Process completed ===",
"Press q or <Esc> to close this window",
})
vim.bo[bufnr].modifiable = false
vim.api.nvim_buf_set_keymap(bufnr, "n", "q", ":q<CR>", {
noremap = true,
silent = true,
})
vim.api.nvim_buf_set_keymap(bufnr, "n", "<Esc>", ":q<CR>", {
noremap = true,
silent = true,
})
vim.cmd("stopinsert")
end
end)
end,
})
vim.api.nvim_buf_set_keymap(bufnr, "t", "<C-\\><C-n>", "", {
callback = function()
vim.cmd("stopinsert")
if vim.fn.confirm("Close terminal?", "&Yes\n&No", 2) == 1 then
vim.api.nvim_win_close(winnr, true)
else
vim.cmd("startinsert")
end
end,
noremap = true,
silent = true,
})
vim.api.nvim_buf_set_keymap(bufnr, "t", "<C-c>", "<C-c>", { noremap = true })
vim.api.nvim_buf_set_keymap(bufnr, "t", "<C-d>", "<C-d>", { noremap = true })
vim.cmd("startinsert")
return bufnr, winnr
end
return M