Regenerate nvim config
This commit is contained in:
@ -0,0 +1,84 @@
|
||||
local M = {}
|
||||
|
||||
---@alias RARunnablesChoice { choice: integer, runnables: RARunnable[] }
|
||||
|
||||
---@class CommandCache
|
||||
local cache = {
|
||||
---@type RARunnableArgs | nil
|
||||
last_debuggable = nil,
|
||||
---@type RARunnablesChoice
|
||||
last_runnable = nil,
|
||||
---@type RARunnablesChoice
|
||||
last_testable = nil,
|
||||
}
|
||||
|
||||
---@param choice integer
|
||||
---@param runnables RARunnable[]
|
||||
M.set_last_runnable = function(choice, runnables)
|
||||
cache.last_runnable = {
|
||||
choice = choice,
|
||||
runnables = runnables,
|
||||
}
|
||||
end
|
||||
|
||||
---@param choice integer
|
||||
---@param runnables RARunnable[]
|
||||
M.set_last_testable = function(choice, runnables)
|
||||
cache.last_testable = {
|
||||
choice = choice,
|
||||
runnables = runnables,
|
||||
}
|
||||
end
|
||||
|
||||
---@param args RARunnableArgs
|
||||
M.set_last_debuggable = function(args)
|
||||
cache.last_debuggable = args
|
||||
end
|
||||
|
||||
---@param executableArgsOverride? string[]
|
||||
M.execute_last_debuggable = function(executableArgsOverride)
|
||||
local args = cache.last_debuggable
|
||||
if args then
|
||||
if type(executableArgsOverride) == 'table' and #executableArgsOverride > 0 then
|
||||
args.executableArgs = executableArgsOverride
|
||||
end
|
||||
local rt_dap = require('rustaceanvim.dap')
|
||||
rt_dap.start(args)
|
||||
else
|
||||
local debuggables = require('rustaceanvim.commands.debuggables')
|
||||
debuggables.debuggables(executableArgsOverride)
|
||||
end
|
||||
end
|
||||
|
||||
---@param choice RARunnablesChoice
|
||||
---@param executableArgsOverride? string[]
|
||||
local function override_executable_args_if_set(choice, executableArgsOverride)
|
||||
if type(executableArgsOverride) == 'table' and #executableArgsOverride > 0 then
|
||||
choice.runnables[choice.choice].args.executableArgs = executableArgsOverride
|
||||
end
|
||||
end
|
||||
|
||||
M.execute_last_testable = function(executableArgsOverride)
|
||||
local action = cache.last_testable
|
||||
local runnables = require('rustaceanvim.runnables')
|
||||
if action then
|
||||
override_executable_args_if_set(action, executableArgsOverride)
|
||||
runnables.run_command(action.choice, action.runnables)
|
||||
else
|
||||
runnables.runnables(executableArgsOverride, { tests_only = true })
|
||||
end
|
||||
end
|
||||
|
||||
---@param executableArgsOverride? string[]
|
||||
M.execute_last_runnable = function(executableArgsOverride)
|
||||
local action = cache.last_runnable
|
||||
local runnables = require('rustaceanvim.runnables')
|
||||
if action then
|
||||
override_executable_args_if_set(action, executableArgsOverride)
|
||||
runnables.run_command(action.choice, action.runnables)
|
||||
else
|
||||
runnables.runnables(executableArgsOverride)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,80 @@
|
||||
local compat = require('rustaceanvim.compat')
|
||||
local rust_analyzer = require('rustaceanvim.rust_analyzer')
|
||||
local os = require('rustaceanvim.os')
|
||||
local joinpath = compat.joinpath
|
||||
|
||||
local cargo = {}
|
||||
|
||||
---Checks if there is an active client for file_name and returns its root directory if found.
|
||||
---@param file_name string
|
||||
---@return string | nil root_dir The root directory of the active client for file_name (if there is one)
|
||||
local function get_mb_active_client_root(file_name)
|
||||
---@diagnostic disable-next-line: missing-parameter
|
||||
local cargo_home = compat.uv.os_getenv('CARGO_HOME') or joinpath(vim.env.HOME, '.cargo')
|
||||
local registry = joinpath(cargo_home, 'registry', 'src')
|
||||
local checkouts = joinpath(cargo_home, 'git', 'checkouts')
|
||||
|
||||
---@diagnostic disable-next-line: missing-parameter
|
||||
local rustup_home = compat.uv.os_getenv('RUSTUP_HOME') or joinpath(vim.env.HOME, '.rustup')
|
||||
local toolchains = joinpath(rustup_home, 'toolchains')
|
||||
|
||||
for _, item in ipairs { toolchains, registry, checkouts } do
|
||||
item = os.normalize_path_on_windows(item)
|
||||
if file_name:sub(1, #item) == item then
|
||||
local clients = rust_analyzer.get_active_rustaceanvim_clients()
|
||||
return clients and #clients > 0 and clients[#clients].config.root_dir or nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param file_name string
|
||||
---@return string | nil root_dir
|
||||
function cargo.get_root_dir(file_name)
|
||||
local reuse_active = get_mb_active_client_root(file_name)
|
||||
if reuse_active then
|
||||
return reuse_active
|
||||
end
|
||||
local path = file_name:find('%.rs$') and vim.fs.dirname(file_name) or file_name
|
||||
if not path then
|
||||
return nil
|
||||
end
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
local cargo_crate_dir = vim.fs.dirname(vim.fs.find({ 'Cargo.toml' }, {
|
||||
upward = true,
|
||||
path = path,
|
||||
})[1])
|
||||
local cargo_workspace_dir = nil
|
||||
if vim.fn.executable('cargo') == 1 then
|
||||
local cmd = { 'cargo', 'metadata', '--no-deps', '--format-version', '1' }
|
||||
if cargo_crate_dir ~= nil then
|
||||
cmd[#cmd + 1] = '--manifest-path'
|
||||
cmd[#cmd + 1] = joinpath(cargo_crate_dir, 'Cargo.toml')
|
||||
end
|
||||
local cargo_metadata = ''
|
||||
local cm = vim.fn.jobstart(cmd, {
|
||||
on_stdout = function(_, d, _)
|
||||
cargo_metadata = table.concat(d, '\n')
|
||||
end,
|
||||
stdout_buffered = true,
|
||||
cwd = compat.uv.fs_stat(path) and path or cargo_crate_dir or vim.fn.getcwd(),
|
||||
})
|
||||
if cm > 0 then
|
||||
cm = vim.fn.jobwait({ cm })[1]
|
||||
else
|
||||
cm = -1
|
||||
end
|
||||
if cm == 0 then
|
||||
cargo_workspace_dir = vim.fn.json_decode(cargo_metadata)['workspace_root']
|
||||
---@cast cargo_workspace_dir string
|
||||
end
|
||||
end
|
||||
return cargo_workspace_dir
|
||||
or cargo_crate_dir
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
or vim.fs.dirname(vim.fs.find({ 'rust-project.json' }, {
|
||||
upward = true,
|
||||
path = path,
|
||||
})[1])
|
||||
end
|
||||
|
||||
return cargo
|
||||
@ -0,0 +1,388 @@
|
||||
local ui = require('rustaceanvim.ui')
|
||||
local config = require('rustaceanvim.config.internal')
|
||||
local M = {}
|
||||
|
||||
---@class RACodeAction
|
||||
---@field kind string
|
||||
---@field group? string
|
||||
---@field edit? table
|
||||
---@field command? { command: string } | string
|
||||
|
||||
---@class RACommand
|
||||
---@field title string
|
||||
---@field group? string
|
||||
---@field command string
|
||||
---@field arguments? any[]
|
||||
|
||||
---@param action RACodeAction | RACommand
|
||||
---@param client lsp.Client
|
||||
---@param ctx table
|
||||
function M.apply_action(action, client, ctx)
|
||||
if action.edit then
|
||||
vim.lsp.util.apply_workspace_edit(action.edit, client.offset_encoding)
|
||||
end
|
||||
if action.command then
|
||||
local command = type(action.command) == 'table' and action.command or action
|
||||
local fn = vim.lsp.commands[command.command]
|
||||
if fn then
|
||||
fn(command, ctx)
|
||||
else
|
||||
M.execute_command(command)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@alias action_tuple { [1]: number, [2]: RACodeAction|RACommand }
|
||||
|
||||
---@param action_tuple action_tuple | nil
|
||||
---@param ctx table
|
||||
function M.on_user_choice(action_tuple, ctx)
|
||||
if not action_tuple then
|
||||
return
|
||||
end
|
||||
local client = vim.lsp.get_client_by_id(action_tuple[1])
|
||||
local action = action_tuple[2]
|
||||
local code_action_provider = client and client.server_capabilities.codeActionProvider
|
||||
if not client then
|
||||
return
|
||||
end
|
||||
if not action.edit and type(code_action_provider) == 'table' and code_action_provider.resolveProvider then
|
||||
client.request('codeAction/resolve', action, function(err, resolved_action)
|
||||
---@cast resolved_action RACodeAction|RACommand
|
||||
if err then
|
||||
vim.notify(err.code .. ': ' .. err.message, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
M.apply_action(resolved_action, client, ctx)
|
||||
end, 0)
|
||||
else
|
||||
M.apply_action(action, client, ctx)
|
||||
end
|
||||
end
|
||||
|
||||
---@class CodeActionWindowGeometry
|
||||
---@field width integer
|
||||
|
||||
---@param action_tuples action_tuple[]
|
||||
---@param is_group boolean
|
||||
---@return CodeActionWindowGeometry
|
||||
local function compute_width(action_tuples, is_group)
|
||||
local width = 0
|
||||
|
||||
for _, value in pairs(action_tuples) do
|
||||
local action = value[2]
|
||||
local text = action.title
|
||||
|
||||
if is_group and action.group then
|
||||
text = action.group .. config.tools.code_actions.group_icon
|
||||
end
|
||||
local len = string.len(text)
|
||||
if len > width then
|
||||
width = len
|
||||
end
|
||||
end
|
||||
|
||||
return { width = width + 5 }
|
||||
end
|
||||
|
||||
local function on_primary_enter_press()
|
||||
if M.state.secondary.winnr then
|
||||
vim.api.nvim_set_current_win(M.state.secondary.winnr)
|
||||
return
|
||||
end
|
||||
|
||||
local line = vim.api.nvim_win_get_cursor(M.state.secondary.winnr or 0)[1]
|
||||
|
||||
for _, value in ipairs(M.state.actions.ungrouped) do
|
||||
if value[2].idx == line then
|
||||
M.on_user_choice(value, M.state.ctx)
|
||||
end
|
||||
end
|
||||
|
||||
M.cleanup()
|
||||
end
|
||||
|
||||
local function on_primary_quit()
|
||||
M.cleanup()
|
||||
end
|
||||
|
||||
---@class RACodeActionResult
|
||||
---@field result? RACodeAction[] | RACommand[]
|
||||
|
||||
---@param results { [number]: RACodeActionResult }
|
||||
---@param ctx table
|
||||
local function on_code_action_results(results, ctx)
|
||||
local cur_win = vim.api.nvim_get_current_win()
|
||||
M.state.ctx = ctx
|
||||
|
||||
---@type action_tuple[]
|
||||
local action_tuples = {}
|
||||
for client_id, result in pairs(results) do
|
||||
for _, action in pairs(result.result or {}) do
|
||||
table.insert(action_tuples, { client_id, action })
|
||||
end
|
||||
end
|
||||
if #action_tuples == 0 then
|
||||
vim.notify('No code actions available', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
M.state.primary.geometry = compute_width(action_tuples, true)
|
||||
---@alias grouped_actions_tbl { actions: action_tuple[], idx: integer | nil }
|
||||
---@class PartitionedActions
|
||||
M.state.actions = {
|
||||
grouped = {},
|
||||
ungrouped = {},
|
||||
}
|
||||
|
||||
for _, value in ipairs(action_tuples) do
|
||||
local action = value[2]
|
||||
-- Some clippy lints may have newlines in them
|
||||
action.title = string.gsub(action.title, '[\n\r]+', ' ')
|
||||
if action.group then
|
||||
if not M.state.actions.grouped[action.group] then
|
||||
M.state.actions.grouped[action.group] = { actions = {}, idx = nil }
|
||||
end
|
||||
table.insert(M.state.actions.grouped[action.group].actions, value)
|
||||
else
|
||||
table.insert(M.state.actions.ungrouped, value)
|
||||
end
|
||||
end
|
||||
|
||||
if #M.state.actions.grouped == 0 and config.tools.code_actions.ui_select_fallback then
|
||||
---@param item action_tuple
|
||||
local function format_item(item)
|
||||
local title = item[2].title:gsub('\r\n', '\\r\\n')
|
||||
return title:gsub('\n', '\\n')
|
||||
end
|
||||
local select_opts = {
|
||||
prompt = 'Code actions:',
|
||||
kind = 'codeaction',
|
||||
format_item = format_item,
|
||||
}
|
||||
vim.ui.select(M.state.actions.ungrouped, select_opts, M.on_user_choice)
|
||||
return
|
||||
end
|
||||
|
||||
M.state.primary.bufnr = vim.api.nvim_create_buf(false, true)
|
||||
local primary_winnr = vim.api.nvim_open_win(M.state.primary.bufnr, true, {
|
||||
relative = 'cursor',
|
||||
width = M.state.primary.geometry.width,
|
||||
height = vim.tbl_count(M.state.actions.grouped) + vim.tbl_count(M.state.actions.ungrouped),
|
||||
focusable = true,
|
||||
border = config.tools.float_win_config.border,
|
||||
row = 1,
|
||||
col = 0,
|
||||
})
|
||||
vim.wo[primary_winnr].signcolumn = 'no'
|
||||
M.state.primary.winnr = primary_winnr
|
||||
|
||||
local idx = 1
|
||||
for key, value in pairs(M.state.actions.grouped) do
|
||||
value.idx = idx
|
||||
vim.api.nvim_buf_set_lines(M.state.primary.bufnr, -1, -1, false, { key .. config.tools.code_actions.group_icon })
|
||||
idx = idx + 1
|
||||
end
|
||||
|
||||
for _, value in pairs(M.state.actions.ungrouped) do
|
||||
local action = value[2]
|
||||
value[2].idx = idx
|
||||
vim.api.nvim_buf_set_lines(M.state.primary.bufnr, -1, -1, false, { action.title })
|
||||
idx = idx + 1
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_set_lines(M.state.primary.bufnr, 0, 1, false, {})
|
||||
|
||||
vim.keymap.set('n', '<CR>', on_primary_enter_press, { buffer = M.state.primary.bufnr, noremap = true, silent = true })
|
||||
|
||||
vim.keymap.set('n', 'q', on_primary_quit, { buffer = M.state.primary.bufnr, noremap = true, silent = true })
|
||||
vim.keymap.set('n', '<Esc>', on_primary_quit, { buffer = M.state.primary.bufnr, noremap = true, silent = true })
|
||||
|
||||
M.codeactionify_window_buffer(M.state.primary.winnr, M.state.primary.bufnr)
|
||||
|
||||
vim.api.nvim_buf_attach(M.state.primary.bufnr, false, {
|
||||
on_detach = function(_, _)
|
||||
M.state.primary.clear()
|
||||
vim.schedule(function()
|
||||
M.cleanup()
|
||||
pcall(vim.api.nvim_set_current_win, cur_win)
|
||||
end)
|
||||
end,
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd('CursorMoved', {
|
||||
buffer = M.state.primary.bufnr,
|
||||
callback = M.on_cursor_move,
|
||||
})
|
||||
|
||||
vim.cmd.redraw()
|
||||
end
|
||||
|
||||
function M.codeactionify_window_buffer(winnr, bufnr)
|
||||
vim.bo[bufnr].modifiable = false
|
||||
vim.bo[bufnr].bufhidden = 'delete'
|
||||
vim.bo[bufnr].buftype = 'nofile'
|
||||
vim.bo[bufnr].ft = 'markdown'
|
||||
|
||||
vim.wo[winnr].nu = true
|
||||
vim.wo[winnr].rnu = false
|
||||
vim.wo[winnr].cul = true
|
||||
end
|
||||
|
||||
local function on_secondary_enter_press()
|
||||
local line = vim.api.nvim_win_get_cursor(M.state.secondary.winnr)[1]
|
||||
local active_group = nil
|
||||
|
||||
for _, value in pairs(M.state.actions.grouped) do
|
||||
if value.idx == M.state.active_group_index then
|
||||
active_group = value
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if active_group then
|
||||
for _, value in pairs(active_group.actions) do
|
||||
if value[2].idx == line then
|
||||
M.on_user_choice(value, M.state.ctx)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
M.cleanup()
|
||||
end
|
||||
|
||||
local function on_secondary_quit()
|
||||
local winnr = M.state.secondary.winnr
|
||||
-- we clear first because if we close the window first, the cursor moved
|
||||
-- autocmd of the first buffer gets called which then sees that
|
||||
-- M.state.secondary.winnr exists (when it shouldnt because it is closed)
|
||||
-- and errors out
|
||||
M.state.secondary.clear()
|
||||
|
||||
ui.close_win(winnr)
|
||||
end
|
||||
|
||||
function M.cleanup()
|
||||
if M.state.primary.winnr then
|
||||
ui.close_win(M.state.primary.winnr)
|
||||
M.state.primary.clear()
|
||||
end
|
||||
|
||||
if M.state.secondary.winnr then
|
||||
ui.close_win(M.state.secondary.winnr)
|
||||
M.state.secondary.clear()
|
||||
end
|
||||
|
||||
M.state.actions = {}
|
||||
M.state.active_group_index = nil
|
||||
M.state.ctx = {}
|
||||
end
|
||||
|
||||
function M.on_cursor_move()
|
||||
local line = vim.api.nvim_win_get_cursor(M.state.primary.winnr)[1]
|
||||
|
||||
for _, value in pairs(M.state.actions.grouped) do
|
||||
if value.idx == line then
|
||||
M.state.active_group_index = line
|
||||
|
||||
if M.state.secondary.winnr then
|
||||
ui.close_win(M.state.secondary.winnr)
|
||||
M.state.secondary.clear()
|
||||
end
|
||||
|
||||
M.state.secondary.geometry = compute_width(value.actions, false)
|
||||
|
||||
M.state.secondary.bufnr = vim.api.nvim_create_buf(false, true)
|
||||
local secondary_winnr = vim.api.nvim_open_win(M.state.secondary.bufnr, false, {
|
||||
relative = 'win',
|
||||
win = M.state.primary.winnr,
|
||||
width = M.state.secondary.geometry.width,
|
||||
height = #value.actions,
|
||||
focusable = true,
|
||||
border = config.tools.float_win_config.border,
|
||||
row = line - 2,
|
||||
col = M.state.primary.geometry.width + 1,
|
||||
})
|
||||
M.state.secondary.winnr = secondary_winnr
|
||||
vim.wo[secondary_winnr].signcolumn = 'no'
|
||||
|
||||
local idx = 1
|
||||
for _, inner_value in pairs(value.actions) do
|
||||
local action = inner_value[2]
|
||||
action.idx = idx
|
||||
vim.api.nvim_buf_set_lines(M.state.secondary.bufnr, -1, -1, false, { action.title })
|
||||
idx = idx + 1
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_set_lines(M.state.secondary.bufnr, 0, 1, false, {})
|
||||
|
||||
M.codeactionify_window_buffer(M.state.secondary.winnr, M.state.secondary.bufnr)
|
||||
|
||||
vim.keymap.set('n', '<CR>', on_secondary_enter_press, { buffer = M.state.secondary.bufnr })
|
||||
|
||||
vim.keymap.set('n', 'q', on_secondary_quit, { buffer = M.state.secondary.bufnr })
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
if M.state.secondary.winnr then
|
||||
ui.close_win(M.state.secondary.winnr)
|
||||
M.state.secondary.clear()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@class CodeActionWindowState
|
||||
---@field bufnr integer | nil
|
||||
---@field winnr integer | nil
|
||||
---@field geometry CodeActionWindowGeometry | nil
|
||||
---@field clear fun()
|
||||
|
||||
---@class CodeActionInternalState
|
||||
M.state = {
|
||||
ctx = {},
|
||||
---@type PartitionedActions
|
||||
actions = {
|
||||
---@type grouped_actions_tbl[]
|
||||
grouped = {},
|
||||
---@type action_tuple[]
|
||||
ungrouped = {},
|
||||
},
|
||||
---@type number | nil
|
||||
active_group_index = nil,
|
||||
---@type CodeActionWindowState
|
||||
primary = {
|
||||
bufnr = nil,
|
||||
winnr = nil,
|
||||
geometry = nil,
|
||||
clear = function()
|
||||
M.state.primary.geometry = nil
|
||||
M.state.primary.bufnr = nil
|
||||
M.state.primary.winnr = nil
|
||||
end,
|
||||
},
|
||||
---@type CodeActionWindowState
|
||||
secondary = {
|
||||
bufnr = nil,
|
||||
winnr = nil,
|
||||
geometry = nil,
|
||||
clear = function()
|
||||
M.state.secondary.geometry = nil
|
||||
M.state.secondary.bufnr = nil
|
||||
M.state.secondary.winnr = nil
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
M.code_action_group = function()
|
||||
local context = {}
|
||||
context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics()
|
||||
local params = vim.lsp.util.make_range_params()
|
||||
params.context = context
|
||||
|
||||
vim.lsp.buf_request_all(0, 'textDocument/codeAction', params, function(results)
|
||||
on_code_action_results(results, { bufnr = 0, method = 'textDocument/codeAction', params = params })
|
||||
end)
|
||||
end
|
||||
|
||||
return M.code_action_group
|
||||
@ -0,0 +1,77 @@
|
||||
local config = require('rustaceanvim.config.internal')
|
||||
local compat = require('rustaceanvim.compat')
|
||||
|
||||
local M = {}
|
||||
|
||||
---@return { full: boolean}
|
||||
local function get_opts()
|
||||
return { full = config.tools.crate_graph.full }
|
||||
end
|
||||
|
||||
--- Creation of the correct handler depending on the initial call of the command
|
||||
--- and give the option to override global settings
|
||||
---@param backend string | nil
|
||||
---@param output string | nil
|
||||
---@param pipe string | nil
|
||||
---@return fun(err: string, graph: string)
|
||||
local function handler_factory(backend, output, pipe)
|
||||
backend = backend or config.tools.crate_graph.backend
|
||||
output = output or config.tools.crate_graph.output
|
||||
pipe = pipe or config.tools.crate_graph.pipe
|
||||
|
||||
-- Graph is a representation of the crate graph following the graphviz format
|
||||
-- The handler processes and pipes the graph to the dot command that will
|
||||
-- visualize with the given backend
|
||||
return function(err, graph)
|
||||
if err ~= nil then
|
||||
vim.notify('Could not execute request to server ' .. (err or ''), vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- Validating backend
|
||||
if not backend then
|
||||
vim.notify('no crate graph backend specified.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
if not compat.list_contains(config.tools.crate_graph.enabled_graphviz_backends, backend) then
|
||||
vim.notify('crate graph backend not recognized as valid: ' .. vim.inspect(backend), vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
graph = string.gsub(graph, '\n', '')
|
||||
vim.notify('rustaceanvim: Processing crate graph. This may take a while...')
|
||||
|
||||
local cmd = 'dot -T' .. backend
|
||||
if pipe ~= nil then -- optionally pipe to `pipe`
|
||||
cmd = cmd .. ' | ' .. pipe
|
||||
end
|
||||
if output ~= nil then -- optionally redirect to `output`
|
||||
cmd = cmd .. ' > ' .. output
|
||||
end
|
||||
|
||||
-- Execute dot command to generate the output graph
|
||||
-- Needs to be handled with care to prevent security problems
|
||||
local handle, err_ = io.popen(cmd, 'w')
|
||||
if not handle then
|
||||
vim.notify('Could not create crate graph ' .. (err_ or ''), vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
handle:write(graph)
|
||||
|
||||
-- needs to be here otherwise dot may take a long time before it gets
|
||||
-- any input + cleaning up (not waiting for garbage collection)
|
||||
handle:flush()
|
||||
handle:close()
|
||||
end
|
||||
end
|
||||
|
||||
local rl = require('rustaceanvim.rust_analyzer')
|
||||
|
||||
---@param backend string | nil
|
||||
---@param output string | nil
|
||||
---@param pipe string | nil
|
||||
function M.view_crate_graph(backend, output, pipe)
|
||||
rl.buf_request(0, 'rust-analyzer/viewCrateGraph', get_opts(), handler_factory(backend, output, pipe))
|
||||
end
|
||||
|
||||
return M.view_crate_graph
|
||||
@ -0,0 +1,202 @@
|
||||
local M = {}
|
||||
|
||||
local compat = require('rustaceanvim.compat')
|
||||
local ra_runnables = require('rustaceanvim.runnables')
|
||||
local config = require('rustaceanvim.config.internal')
|
||||
|
||||
---@return { textDocument: lsp_text_document, position: nil }
|
||||
local function get_params()
|
||||
return {
|
||||
textDocument = vim.lsp.util.make_text_document_params(),
|
||||
position = nil, -- get em all
|
||||
}
|
||||
end
|
||||
|
||||
---@type { [string]: boolean? } Used to prevent this plugin from adding the same configuration twice
|
||||
local _dap_configuration_added = {}
|
||||
|
||||
---@param args RARunnableArgs
|
||||
---@return string
|
||||
local function build_label(args)
|
||||
local ret = ''
|
||||
for _, value in ipairs(args.cargoArgs) do
|
||||
ret = ret .. value .. ' '
|
||||
end
|
||||
|
||||
for _, value in ipairs(args.cargoExtraArgs) do
|
||||
ret = ret .. value .. ' '
|
||||
end
|
||||
|
||||
if not vim.tbl_isempty(args.executableArgs) then
|
||||
ret = ret .. '-- '
|
||||
for _, value in ipairs(args.executableArgs) do
|
||||
ret = ret .. value .. ' '
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
---@param result RARunnable[]
|
||||
---@return string[] option_strings
|
||||
local function get_options(result)
|
||||
---@type string[]
|
||||
local option_strings = {}
|
||||
|
||||
for _, debuggable in ipairs(result) do
|
||||
local label = build_label(debuggable.args)
|
||||
local str = label
|
||||
if config.tools.cargo_override then
|
||||
str = str:gsub('^cargo', config.tools.cargo_override)
|
||||
end
|
||||
table.insert(option_strings, str)
|
||||
end
|
||||
|
||||
return option_strings
|
||||
end
|
||||
|
||||
---@param args RARunnableArgs
|
||||
---@return boolean
|
||||
local function is_valid_test(args)
|
||||
local is_not_cargo_check = args.cargoArgs[1] ~= 'check'
|
||||
return is_not_cargo_check
|
||||
end
|
||||
|
||||
-- rust-analyzer doesn't actually support giving a list of debuggable targets,
|
||||
-- so work around that by manually removing non debuggable targets (only cargo
|
||||
-- check for now).
|
||||
-- This function also makes it so that the debuggable commands are more
|
||||
-- debugging friendly. For example, we move cargo run to cargo build, and cargo
|
||||
-- test to cargo test --no-run.
|
||||
---@param result RARunnable[]
|
||||
local function sanitize_results_for_debugging(result)
|
||||
---@type RARunnable[]
|
||||
local ret = vim.tbl_filter(function(value)
|
||||
---@cast value RARunnable
|
||||
return is_valid_test(value.args)
|
||||
end, result or {})
|
||||
|
||||
local overrides = require('rustaceanvim.overrides')
|
||||
for _, value in ipairs(ret) do
|
||||
overrides.sanitize_command_for_debugging(value.args.cargoArgs)
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
local function dap_run(args)
|
||||
local rt_dap = require('rustaceanvim.dap')
|
||||
local ok, dap = pcall(require, 'dap')
|
||||
if ok then
|
||||
rt_dap.start(args, true, dap.run)
|
||||
local cached_commands = require('rustaceanvim.cached_commands')
|
||||
cached_commands.set_last_debuggable(args)
|
||||
else
|
||||
vim.notify('nvim-dap is required for debugging', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
---@param debuggables RARunnable[]
|
||||
---@param executableArgsOverride? string[]
|
||||
local function ui_select_debuggable(debuggables, executableArgsOverride)
|
||||
debuggables = ra_runnables.apply_exec_args_override(executableArgsOverride, debuggables)
|
||||
local options = get_options(debuggables)
|
||||
if #options == 0 then
|
||||
return
|
||||
end
|
||||
vim.ui.select(options, { prompt = 'Debuggables', kind = 'rust-tools/debuggables' }, function(_, choice)
|
||||
if choice == nil then
|
||||
return
|
||||
end
|
||||
local args = debuggables[choice].args
|
||||
dap_run(args)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param debuggables RARunnable[]
|
||||
local function add_debuggables_to_nvim_dap(debuggables)
|
||||
local ok, dap = pcall(require, 'dap')
|
||||
if not ok then
|
||||
return
|
||||
end
|
||||
local rt_dap = require('rustaceanvim.dap')
|
||||
dap.configurations.rust = dap.configurations.rust or {}
|
||||
for _, debuggable in pairs(debuggables) do
|
||||
rt_dap.start(debuggable.args, false, function(configuration)
|
||||
local name = 'Cargo: ' .. build_label(debuggable.args)
|
||||
if not _dap_configuration_added[name] then
|
||||
configuration.name = name
|
||||
table.insert(dap.configurations.rust, configuration)
|
||||
_dap_configuration_added[name] = true
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---@param debuggables RARunnable[]
|
||||
---@param executableArgsOverride? string[]
|
||||
local function debug_at_cursor_position(debuggables, executableArgsOverride)
|
||||
if debuggables == nil then
|
||||
return
|
||||
end
|
||||
debuggables = ra_runnables.apply_exec_args_override(executableArgsOverride, debuggables)
|
||||
local choice = ra_runnables.get_runnable_at_cursor_position(debuggables)
|
||||
if not choice then
|
||||
vim.notify('No debuggable targets found for the current position.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local args = debuggables[choice].args
|
||||
dap_run(args)
|
||||
end
|
||||
|
||||
---@param callback fun(result:RARunnable[])
|
||||
local function mk_handler(callback)
|
||||
return function(_, result, _, _)
|
||||
---@cast result RARunnable[]
|
||||
if result == nil then
|
||||
return
|
||||
end
|
||||
result = sanitize_results_for_debugging(result)
|
||||
callback(result)
|
||||
end
|
||||
end
|
||||
|
||||
local rl = require('rustaceanvim.rust_analyzer')
|
||||
|
||||
---@param handler? lsp.Handler See |lsp-handler|
|
||||
local function runnables_request(handler)
|
||||
rl.buf_request(0, 'experimental/runnables', get_params(), handler)
|
||||
end
|
||||
|
||||
---Sends the request to rust-analyzer to get the debuggables and handles them
|
||||
---@param executableArgsOverride? string[]
|
||||
function M.debuggables(executableArgsOverride)
|
||||
runnables_request(mk_handler(function(debuggables)
|
||||
ui_select_debuggable(debuggables, executableArgsOverride)
|
||||
end))
|
||||
end
|
||||
|
||||
---Runs the debuggable under the cursor, if present
|
||||
---@param executableArgsOverride? string[]
|
||||
function M.debug(executableArgsOverride)
|
||||
runnables_request(mk_handler(function(debuggables)
|
||||
debug_at_cursor_position(debuggables, executableArgsOverride)
|
||||
end))
|
||||
end
|
||||
|
||||
--- Sends the request to rust-analyzer to get the debuggables and adds them to nvim-dap's
|
||||
--- configurations
|
||||
function M.add_dap_debuggables()
|
||||
-- Defer, because rust-analyzer may not be ready yet
|
||||
runnables_request(mk_handler(add_debuggables_to_nvim_dap))
|
||||
local timer = compat.uv.new_timer()
|
||||
timer:start(
|
||||
2000,
|
||||
0,
|
||||
vim.schedule_wrap(function()
|
||||
runnables_request(mk_handler(add_debuggables_to_nvim_dap))
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,252 @@
|
||||
local M = {}
|
||||
|
||||
local config = require('rustaceanvim.config.internal')
|
||||
local compat = require('rustaceanvim.compat')
|
||||
local ui = require('rustaceanvim.ui')
|
||||
|
||||
local rustc = 'rustc'
|
||||
|
||||
---@class DiagnosticWindowState
|
||||
local _window_state = {
|
||||
---@type integer | nil
|
||||
float_winnr = nil,
|
||||
---@type integer | nil
|
||||
latest_scratch_buf_id = nil,
|
||||
}
|
||||
|
||||
---@param bufnr integer
|
||||
---@param winnr integer
|
||||
---@param lines string[]
|
||||
local function set_open_split_keymap(bufnr, winnr, lines)
|
||||
local function open_split()
|
||||
-- check if a buffer with the latest id is already open, if it is then
|
||||
-- delete it and continue
|
||||
ui.delete_buf(_window_state.latest_scratch_buf_id)
|
||||
|
||||
-- create a new buffer
|
||||
_window_state.latest_scratch_buf_id = vim.api.nvim_create_buf(false, true) -- not listed and scratch
|
||||
|
||||
-- split the window to create a new buffer and set it to our window
|
||||
local vsplit = config.tools.float_win_config.open_split == 'vertical'
|
||||
ui.split(vsplit, _window_state.latest_scratch_buf_id)
|
||||
|
||||
-- set filetype to rust for syntax highlighting
|
||||
vim.bo[_window_state.latest_scratch_buf_id].filetype = 'rust'
|
||||
-- write the expansion content to the buffer
|
||||
vim.api.nvim_buf_set_lines(_window_state.latest_scratch_buf_id, 0, 0, false, lines)
|
||||
end
|
||||
vim.keymap.set('n', '<CR>', function()
|
||||
local line = vim.api.nvim_win_get_cursor(winnr)[1]
|
||||
if line > 1 then
|
||||
return
|
||||
end
|
||||
open_split()
|
||||
end, { buffer = bufnr, noremap = true, silent = true })
|
||||
end
|
||||
|
||||
---@return nil
|
||||
local function close_hover()
|
||||
local winnr = _window_state.float_winnr
|
||||
if winnr ~= nil and vim.api.nvim_win_is_valid(winnr) then
|
||||
vim.api.nvim_win_close(winnr, true)
|
||||
_window_state.float_winnr = nil
|
||||
end
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
local function set_close_keymaps(bufnr)
|
||||
vim.keymap.set('n', 'q', close_hover, { buffer = bufnr, noremap = true, silent = true })
|
||||
vim.keymap.set('n', '<Esc>', close_hover, { buffer = bufnr, noremap = true, silent = true })
|
||||
end
|
||||
|
||||
function M.explain_error()
|
||||
if vim.fn.executable(rustc) ~= 1 then
|
||||
vim.notify('rustc is needed to explain errors.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local diagnostics = vim.tbl_filter(function(diagnostic)
|
||||
return diagnostic.code ~= nil
|
||||
and diagnostic.source == 'rustc'
|
||||
and diagnostic.severity == vim.diagnostic.severity.ERROR
|
||||
end, vim.diagnostic.get(0, {}))
|
||||
if #diagnostics == 0 then
|
||||
vim.notify('No explainable errors found.', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
local win_id = vim.api.nvim_get_current_win()
|
||||
local opts = {
|
||||
cursor_position = vim.api.nvim_win_get_cursor(win_id),
|
||||
severity = vim.diagnostic.severity.ERROR,
|
||||
wrap = true,
|
||||
}
|
||||
local found = false
|
||||
local diagnostic
|
||||
local pos_map = {}
|
||||
---@type string
|
||||
local pos_id = '0'
|
||||
repeat
|
||||
diagnostic = vim.diagnostic.get_next(opts)
|
||||
pos_map[pos_id] = diagnostic
|
||||
if diagnostic == nil then
|
||||
break
|
||||
end
|
||||
found = diagnostic.code ~= nil and diagnostic.source == 'rustc'
|
||||
local pos = { diagnostic.lnum, diagnostic.col }
|
||||
-- check if there is an explainable error at the same location
|
||||
if not found then
|
||||
local cursor_diagnostics = vim.tbl_filter(function(diag)
|
||||
return pos[1] == diag.lnum and pos[2] == diag.col
|
||||
end, diagnostics)
|
||||
if #cursor_diagnostics ~= 0 then
|
||||
diagnostic = cursor_diagnostics[1]
|
||||
found = true
|
||||
break
|
||||
end
|
||||
end
|
||||
pos_id = vim.inspect(pos)
|
||||
-- diagnostics are (0,0)-indexed but cursors are (1,0)-indexed
|
||||
opts.cursor_position = { pos[1] + 1, pos[2] }
|
||||
local searched_all = pos_map[pos_id] ~= nil
|
||||
until diagnostic == nil or found or searched_all
|
||||
if not found then
|
||||
-- Fall back to first diagnostic
|
||||
diagnostic = diagnostics[1]
|
||||
local pos = { diagnostic.lnum, diagnostic.col }
|
||||
opts.cursor_position = pos
|
||||
return
|
||||
end
|
||||
|
||||
---@param sc vim.SystemCompleted
|
||||
local function handler(sc)
|
||||
if sc.code ~= 0 or not sc.stdout then
|
||||
vim.notify('Error calling rustc --explain' .. (sc.stderr and ': ' .. sc.stderr or ''), vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local output = sc.stdout:gsub('```', '```rust', 1)
|
||||
local markdown_lines = vim.lsp.util.convert_input_to_markdown_lines(output, {})
|
||||
local float_preview_lines = vim.deepcopy(markdown_lines)
|
||||
table.insert(float_preview_lines, 1, '---')
|
||||
table.insert(float_preview_lines, 1, '1. Open in split')
|
||||
vim.schedule(function()
|
||||
close_hover()
|
||||
local bufnr, winnr = vim.lsp.util.open_floating_preview(
|
||||
float_preview_lines,
|
||||
'markdown',
|
||||
vim.tbl_extend('keep', config.tools.float_win_config, {
|
||||
focus = false,
|
||||
focusable = true,
|
||||
focus_id = 'rustc-explain-error',
|
||||
close_events = { 'CursorMoved', 'BufHidden', 'InsertCharPre' },
|
||||
})
|
||||
)
|
||||
_window_state.float_winnr = winnr
|
||||
set_close_keymaps(bufnr)
|
||||
set_open_split_keymap(bufnr, winnr, markdown_lines)
|
||||
|
||||
if config.tools.float_win_config.auto_focus then
|
||||
vim.api.nvim_set_current_win(winnr)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Save position in the window's jumplist
|
||||
vim.cmd("normal! m'")
|
||||
vim.api.nvim_win_set_cursor(win_id, { diagnostic.lnum + 1, diagnostic.col })
|
||||
-- Open folds under the cursor
|
||||
vim.cmd('normal! zv')
|
||||
compat.system({ rustc, '--explain', tostring(diagnostic.code) }, nil, vim.schedule_wrap(handler))
|
||||
end
|
||||
|
||||
---@param diagnostic table
|
||||
---@return string | nil
|
||||
local function get_rendered_diagnostic(diagnostic)
|
||||
local result = vim.tbl_get(diagnostic, 'user_data', 'lsp', 'data', 'rendered')
|
||||
if type(result) == 'string' then
|
||||
---@diagnostic disable-next-line: cast-type-mismatch
|
||||
---@cast result string
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
function M.render_diagnostic()
|
||||
local diagnostics = vim.tbl_filter(function(diagnostic)
|
||||
return get_rendered_diagnostic(diagnostic) ~= nil
|
||||
end, vim.diagnostic.get(0, {}))
|
||||
if #diagnostics == 0 then
|
||||
vim.notify('No renderable diagnostics found.', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
local win_id = vim.api.nvim_get_current_win()
|
||||
local opts = {
|
||||
cursor_position = vim.api.nvim_win_get_cursor(win_id),
|
||||
wrap = true,
|
||||
}
|
||||
local rendered_diagnostic
|
||||
local diagnostic
|
||||
local pos_map = {}
|
||||
---@type string
|
||||
local pos_id = '0'
|
||||
repeat
|
||||
diagnostic = vim.diagnostic.get_next(opts)
|
||||
pos_map[pos_id] = diagnostic
|
||||
if diagnostic == nil then
|
||||
break
|
||||
end
|
||||
rendered_diagnostic = get_rendered_diagnostic(diagnostic)
|
||||
local pos = { diagnostic.lnum, diagnostic.col }
|
||||
-- check if there is a rendered diagnostic at the same location
|
||||
if rendered_diagnostic == nil then
|
||||
local cursor_diagnostics = vim.tbl_filter(function(diag)
|
||||
return pos[1] == diag.lnum and pos[2] == diag.col
|
||||
end, diagnostics)
|
||||
if #cursor_diagnostics ~= 0 then
|
||||
diagnostic = cursor_diagnostics[1]
|
||||
rendered_diagnostic = get_rendered_diagnostic(diagnostic)
|
||||
break
|
||||
end
|
||||
end
|
||||
pos_id = vim.inspect(pos)
|
||||
-- diagnostics are (0,0)-indexed but cursors are (1,0)-indexed
|
||||
opts.cursor_position = { pos[1] + 1, pos[2] }
|
||||
local searched_all = pos_map[pos_id] ~= nil
|
||||
until diagnostic == nil or rendered_diagnostic ~= nil or searched_all
|
||||
if not rendered_diagnostic then
|
||||
-- No diagnostics found. Fall back to first result from filter,
|
||||
diagnostic = diagnostics[1]
|
||||
rendered_diagnostic = get_rendered_diagnostic(diagnostic)
|
||||
---@cast rendered_diagnostic string
|
||||
end
|
||||
|
||||
-- Save position in the window's jumplist
|
||||
vim.cmd("normal! m'")
|
||||
vim.api.nvim_win_set_cursor(win_id, { diagnostic.lnum + 1, diagnostic.col })
|
||||
-- Open folds under the cursor
|
||||
vim.cmd('normal! zv')
|
||||
|
||||
local lines = vim.split(rendered_diagnostic, '\n')
|
||||
local float_preview_lines = vim.deepcopy(lines)
|
||||
table.insert(float_preview_lines, 1, '---')
|
||||
table.insert(float_preview_lines, 1, '1. Open in split')
|
||||
vim.schedule(function()
|
||||
close_hover()
|
||||
local bufnr, winnr = vim.lsp.util.open_floating_preview(
|
||||
float_preview_lines,
|
||||
'',
|
||||
vim.tbl_extend('keep', config.tools.float_win_config, {
|
||||
focus = false,
|
||||
focusable = true,
|
||||
focus_id = 'ra-render-diagnostic',
|
||||
close_events = { 'CursorMoved', 'BufHidden', 'InsertCharPre' },
|
||||
})
|
||||
)
|
||||
_window_state.float_winnr = winnr
|
||||
set_close_keymaps(bufnr)
|
||||
set_open_split_keymap(bufnr, winnr, lines)
|
||||
if config.tools.float_win_config.auto_focus then
|
||||
vim.api.nvim_set_current_win(winnr)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,80 @@
|
||||
local ui = require('rustaceanvim.ui')
|
||||
|
||||
local M = {}
|
||||
|
||||
---@return lsp_position_params
|
||||
local function get_params()
|
||||
return vim.lsp.util.make_position_params()
|
||||
end
|
||||
|
||||
---@type integer | nil
|
||||
local latest_buf_id = nil
|
||||
|
||||
---@class RAMacroExpansionResult
|
||||
---@field name string
|
||||
---@field expansion string
|
||||
|
||||
-- parse the lines from result to get a list of the desirable output
|
||||
-- Example:
|
||||
-- // Recursive expansion of the eprintln macro
|
||||
-- // ============================================
|
||||
|
||||
-- {
|
||||
-- $crate::io::_eprint(std::fmt::Arguments::new_v1(&[], &[std::fmt::ArgumentV1::new(&(err),std::fmt::Display::fmt),]));
|
||||
-- }
|
||||
---@param result RAMacroExpansionResult
|
||||
---@return string[]
|
||||
local function parse_lines(result)
|
||||
local ret = {}
|
||||
|
||||
local name = result.name
|
||||
local text = '// Recursive expansion of the ' .. name .. ' macro'
|
||||
table.insert(ret, '// ' .. string.rep('=', string.len(text) - 3))
|
||||
table.insert(ret, text)
|
||||
table.insert(ret, '// ' .. string.rep('=', string.len(text) - 3))
|
||||
table.insert(ret, '')
|
||||
|
||||
local expansion = result.expansion
|
||||
for string in string.gmatch(expansion, '([^\n]+)') do
|
||||
table.insert(ret, string)
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
---@param result? RAMacroExpansionResult
|
||||
local function handler(_, result)
|
||||
-- echo a message when result is nil (meaning no macro under cursor) and
|
||||
-- exit
|
||||
if result == nil then
|
||||
vim.notify('No macro under cursor!', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
-- check if a buffer with the latest id is already open, if it is then
|
||||
-- delete it and continue
|
||||
ui.delete_buf(latest_buf_id)
|
||||
|
||||
-- create a new buffer
|
||||
latest_buf_id = vim.api.nvim_create_buf(false, true) -- not listed and scratch
|
||||
|
||||
-- split the window to create a new buffer and set it to our window
|
||||
ui.split(true, latest_buf_id)
|
||||
|
||||
-- set filetype to rust for syntax highlighting
|
||||
vim.bo[latest_buf_id].filetype = 'rust'
|
||||
-- write the expansion content to the buffer
|
||||
vim.api.nvim_buf_set_lines(latest_buf_id, 0, 0, false, parse_lines(result))
|
||||
|
||||
-- make the new buffer smaller
|
||||
ui.resize(true, '-25')
|
||||
end
|
||||
|
||||
local rl = require('rustaceanvim.rust_analyzer')
|
||||
|
||||
--- Sends the request to rust-analyzer to expand the macro under the cursor
|
||||
function M.expand_macro()
|
||||
rl.buf_request(0, 'rust-analyzer/expandMacro', get_params(), handler)
|
||||
end
|
||||
|
||||
return M.expand_macro
|
||||
@ -0,0 +1,14 @@
|
||||
local M = {}
|
||||
|
||||
local rl = require('rustaceanvim.rust_analyzer')
|
||||
|
||||
function M.open_external_docs()
|
||||
rl.buf_request(0, 'experimental/externalDocs', vim.lsp.util.make_position_params(), function(_, url)
|
||||
if url then
|
||||
local config = require('rustaceanvim.config.internal')
|
||||
config.tools.open_url(url)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return M.open_external_docs
|
||||
@ -0,0 +1,13 @@
|
||||
local M = {}
|
||||
|
||||
local rl = require('rustaceanvim.rust_analyzer')
|
||||
|
||||
---@alias flyCheckCommand 'run' | 'clear' | 'cancel'
|
||||
|
||||
---@param cmd flyCheckCommand
|
||||
function M.fly_check(cmd)
|
||||
local params = cmd == 'run' and vim.lsp.util.make_text_document_params() or {}
|
||||
rl.notify('rust-analyzer/' .. cmd .. 'Flycheck', params)
|
||||
end
|
||||
|
||||
return M.fly_check
|
||||
@ -0,0 +1,59 @@
|
||||
local M = {}
|
||||
|
||||
-- Converts a tuple of range coordinates into LSP's position argument
|
||||
---@param row1 integer
|
||||
---@param col1 integer
|
||||
---@param row2 integer
|
||||
---@param col2 integer
|
||||
---@return lsp_range
|
||||
local function make_lsp_position(row1, col1, row2, col2)
|
||||
-- Note: vim's lines are 1-indexed, but LSP's are 0-indexed
|
||||
return {
|
||||
['start'] = {
|
||||
line = row1 - 1,
|
||||
character = col1,
|
||||
},
|
||||
['end'] = {
|
||||
line = row2 - 1,
|
||||
character = col2,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
---@return lsp_range | nil
|
||||
local function get_visual_selected_range()
|
||||
-- Taken from https://github.com/neovim/neovim/pull/13896#issuecomment-774680224
|
||||
local p1 = vim.fn.getpos('v')
|
||||
if not p1 then
|
||||
return nil
|
||||
end
|
||||
local row1 = p1[2]
|
||||
local col1 = p1[3]
|
||||
local p2 = vim.api.nvim_win_get_cursor(0)
|
||||
local row2 = p2[1]
|
||||
local col2 = p2[2]
|
||||
|
||||
if row1 < row2 then
|
||||
return make_lsp_position(row1, col1, row2, col2)
|
||||
elseif row2 < row1 then
|
||||
return make_lsp_position(row2, col2, row1, col1)
|
||||
end
|
||||
|
||||
return make_lsp_position(row1, math.min(col1, col2), row1, math.max(col1, col2))
|
||||
end
|
||||
|
||||
---@return lsp_range_params
|
||||
local function get_opts()
|
||||
local params = vim.lsp.util.make_range_params()
|
||||
params.position = get_visual_selected_range()
|
||||
params.range = nil
|
||||
return params
|
||||
end
|
||||
|
||||
local rl = require('rustaceanvim.rust_analyzer')
|
||||
|
||||
function M.hover_range()
|
||||
rl.buf_request(0, 'textDocument/hover', get_opts())
|
||||
end
|
||||
|
||||
return M.hover_range
|
||||
@ -0,0 +1,382 @@
|
||||
---@mod rustaceanvim.commands
|
||||
|
||||
local config = require('rustaceanvim.config.internal')
|
||||
|
||||
---@class RustaceanCommands
|
||||
local M = {}
|
||||
|
||||
local rust_lsp_cmd_name = 'RustLsp'
|
||||
local rustc_cmd_name = 'Rustc'
|
||||
|
||||
---@class command_tbl
|
||||
---@field impl fun(args: string[], opts: vim.api.keyset.user_command) The command implementation
|
||||
---@field complete? fun(subcmd_arg_lead: string): string[] Command completions callback, taking the lead of the subcommand's arguments
|
||||
---@field bang? boolean Whether this command supports a bang!
|
||||
|
||||
---@type command_tbl[]
|
||||
local rustlsp_command_tbl = {
|
||||
codeAction = {
|
||||
impl = function(_)
|
||||
require('rustaceanvim.commands.code_action_group')()
|
||||
end,
|
||||
},
|
||||
crateGraph = {
|
||||
impl = function(args)
|
||||
require('rustaceanvim.commands.crate_graph')(unpack(args))
|
||||
end,
|
||||
complete = function(subcmd_arg_lead)
|
||||
return vim.tbl_filter(function(backend)
|
||||
return backend:find(subcmd_arg_lead) ~= nil
|
||||
end, config.tools.crate_graph.enabled_graphviz_backends or {})
|
||||
end,
|
||||
},
|
||||
debuggables = {
|
||||
---@param args string[]
|
||||
---@param opts vim.api.keyset.user_command
|
||||
impl = function(args, opts)
|
||||
if opts.bang then
|
||||
require('rustaceanvim.cached_commands').execute_last_debuggable(args)
|
||||
else
|
||||
require('rustaceanvim.commands.debuggables').debuggables(args)
|
||||
end
|
||||
end,
|
||||
bang = true,
|
||||
},
|
||||
debug = {
|
||||
---@param args string[]
|
||||
---@param opts vim.api.keyset.user_command
|
||||
impl = function(args, opts)
|
||||
if opts.bang then
|
||||
require('rustaceanvim.cached_commands').execute_last_debuggable(args)
|
||||
else
|
||||
require('rustaceanvim.commands.debuggables').debug(args)
|
||||
end
|
||||
end,
|
||||
bang = true,
|
||||
},
|
||||
expandMacro = {
|
||||
impl = function(_)
|
||||
require('rustaceanvim.commands.expand_macro')()
|
||||
end,
|
||||
},
|
||||
explainError = {
|
||||
impl = function(_)
|
||||
require('rustaceanvim.commands.diagnostic').explain_error()
|
||||
end,
|
||||
},
|
||||
renderDiagnostic = {
|
||||
impl = function(_)
|
||||
require('rustaceanvim.commands.diagnostic').render_diagnostic()
|
||||
end,
|
||||
},
|
||||
rebuildProcMacros = {
|
||||
impl = function()
|
||||
require('rustaceanvim.commands.rebuild_proc_macros')()
|
||||
end,
|
||||
},
|
||||
externalDocs = {
|
||||
impl = function(_)
|
||||
require('rustaceanvim.commands.external_docs')()
|
||||
end,
|
||||
},
|
||||
hover = {
|
||||
impl = function(args)
|
||||
if #args == 0 then
|
||||
vim.notify("hover: called without 'actions' or 'range'", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local subcmd = args[1]
|
||||
if subcmd == 'actions' then
|
||||
require('rustaceanvim.hover_actions').hover_actions()
|
||||
elseif subcmd == 'range' then
|
||||
require('rustaceanvim.commands.hover_range')()
|
||||
else
|
||||
vim.notify('hover: unknown subcommand: ' .. subcmd .. " expected 'actions' or 'range'", vim.log.levels.ERROR)
|
||||
end
|
||||
end,
|
||||
complete = function()
|
||||
return { 'actions', 'range' }
|
||||
end,
|
||||
},
|
||||
runnables = {
|
||||
---@param opts vim.api.keyset.user_command
|
||||
impl = function(args, opts)
|
||||
if opts.bang then
|
||||
require('rustaceanvim.cached_commands').execute_last_runnable(args)
|
||||
else
|
||||
require('rustaceanvim.runnables').runnables(args)
|
||||
end
|
||||
end,
|
||||
bang = true,
|
||||
},
|
||||
run = {
|
||||
---@param opts vim.api.keyset.user_command
|
||||
impl = function(args, opts)
|
||||
if opts.bang then
|
||||
require('rustaceanvim.cached_commands').execute_last_runnable(args)
|
||||
else
|
||||
require('rustaceanvim.runnables').run(args)
|
||||
end
|
||||
end,
|
||||
bang = true,
|
||||
},
|
||||
testables = {
|
||||
---@param opts vim.api.keyset.user_command
|
||||
impl = function(args, opts)
|
||||
if opts.bang then
|
||||
require('rustaceanvim.cached_commands').execute_last_testable()
|
||||
else
|
||||
require('rustaceanvim.runnables').runnables(args, { tests_only = true })
|
||||
end
|
||||
end,
|
||||
bang = true,
|
||||
},
|
||||
joinLines = {
|
||||
impl = function(_, opts)
|
||||
---@cast opts vim.api.keyset.user_command
|
||||
local visual_mode = opts.range and opts.range ~= 0 or false
|
||||
require('rustaceanvim.commands.join_lines')(visual_mode)
|
||||
end,
|
||||
},
|
||||
moveItem = {
|
||||
impl = function(args)
|
||||
if #args == 0 then
|
||||
vim.notify("moveItem: called without 'up' or 'down'", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
if args[1] == 'down' then
|
||||
require('rustaceanvim.commands.move_item')()
|
||||
elseif args[1] == 'up' then
|
||||
require('rustaceanvim.commands.move_item')(true)
|
||||
else
|
||||
vim.notify(
|
||||
'moveItem: unexpected argument: ' .. vim.inspect(args) .. " expected 'up' or 'down'",
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
end
|
||||
end,
|
||||
complete = function()
|
||||
return { 'up', 'down' }
|
||||
end,
|
||||
},
|
||||
openCargo = {
|
||||
impl = function(_)
|
||||
require('rustaceanvim.commands.open_cargo_toml')()
|
||||
end,
|
||||
},
|
||||
openDocs = {
|
||||
impl = function(_)
|
||||
require('rustaceanvim.commands.external_docs')()
|
||||
end,
|
||||
},
|
||||
parentModule = {
|
||||
impl = function(_)
|
||||
require('rustaceanvim.commands.parent_module')()
|
||||
end,
|
||||
},
|
||||
ssr = {
|
||||
impl = function(args, opts)
|
||||
---@cast opts vim.api.keyset.user_command
|
||||
local visual_mode = opts.range and opts.range > 0 or false
|
||||
local query = args and #args > 0 and table.concat(args, ' ') or nil
|
||||
require('rustaceanvim.commands.ssr')(query, visual_mode)
|
||||
end,
|
||||
},
|
||||
reloadWorkspace = {
|
||||
impl = function()
|
||||
require('rustaceanvim.commands.workspace_refresh')()
|
||||
end,
|
||||
},
|
||||
workspaceSymbol = {
|
||||
---@param opts vim.api.keyset.user_command
|
||||
impl = function(args, opts)
|
||||
local c = require('rustaceanvim.commands.workspace_symbol')
|
||||
---@type WorkspaceSymbolSearchScope
|
||||
local searchScope = opts.bang and c.WorkspaceSymbolSearchScope.workspaceAndDependencies
|
||||
or c.WorkspaceSymbolSearchScope.workspace
|
||||
c.workspace_symbol(searchScope, args)
|
||||
end,
|
||||
complete = function(subcmd_arg_lead)
|
||||
local c = require('rustaceanvim.commands.workspace_symbol')
|
||||
return vim.tbl_filter(function(arg)
|
||||
return arg:find(subcmd_arg_lead) ~= nil
|
||||
end, vim.tbl_values(c.WorkspaceSymbolSearchKind))
|
||||
--
|
||||
end,
|
||||
bang = true,
|
||||
},
|
||||
syntaxTree = {
|
||||
impl = function()
|
||||
require('rustaceanvim.commands.syntax_tree')()
|
||||
end,
|
||||
},
|
||||
flyCheck = {
|
||||
impl = function(args)
|
||||
local cmd = args[1] or 'run'
|
||||
require('rustaceanvim.commands.fly_check')(cmd)
|
||||
end,
|
||||
complete = function(subcmd_arg_lead)
|
||||
return vim.tbl_filter(function(arg)
|
||||
return arg:find(subcmd_arg_lead) ~= nil
|
||||
end, { 'run', 'clear', 'cancel' })
|
||||
end,
|
||||
},
|
||||
view = {
|
||||
impl = function(args)
|
||||
if not args or #args == 0 then
|
||||
vim.notify("Expected argument: 'mir' or 'hir'", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local level
|
||||
local arg = args[1]:lower()
|
||||
if arg == 'mir' then
|
||||
level = 'Mir'
|
||||
elseif arg == 'hir' then
|
||||
level = 'Hir'
|
||||
else
|
||||
vim.notify('Unexpected argument: ' .. arg .. " Expected: 'mir' or 'hir'", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
require('rustaceanvim.commands.view_ir')(level)
|
||||
end,
|
||||
complete = function(subcmd_arg_lead)
|
||||
return vim.tbl_filter(function(arg)
|
||||
return arg:find(subcmd_arg_lead) ~= nil
|
||||
end, { 'mir', 'hir' })
|
||||
end,
|
||||
},
|
||||
logFile = {
|
||||
impl = function()
|
||||
vim.cmd.e(config.server.logfile)
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
---@type command_tbl[]
|
||||
local rustc_command_tbl = {
|
||||
unpretty = {
|
||||
impl = function(args)
|
||||
local err_msg = table.concat(require('rustaceanvim.commands.rustc_unpretty').available_unpretty, ' | ')
|
||||
if not args or #args == 0 then
|
||||
vim.notify('Expected argument list: ' .. err_msg, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local arg = args[1]:lower()
|
||||
local available = false
|
||||
for _, value in ipairs(require('rustaceanvim.commands.rustc_unpretty').available_unpretty) do
|
||||
if value == arg then
|
||||
available = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not available then
|
||||
vim.notify('Expected argument list: ' .. err_msg, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
require('rustaceanvim.commands.rustc_unpretty').rustc_unpretty(arg)
|
||||
end,
|
||||
complete = function(subcmd_arg_lead)
|
||||
return vim.tbl_filter(function(arg)
|
||||
return arg:find(subcmd_arg_lead) ~= nil
|
||||
end, require('rustaceanvim.commands.rustc_unpretty').available_unpretty)
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
---@param command_tbl command_tbl
|
||||
---@param opts table
|
||||
---@see vim.api.nvim_create_user_command
|
||||
local function run_command(command_tbl, cmd_name, opts)
|
||||
local fargs = opts.fargs
|
||||
local cmd = fargs[1]
|
||||
local args = #fargs > 1 and vim.list_slice(fargs, 2, #fargs) or {}
|
||||
local command = command_tbl[cmd]
|
||||
if type(command) ~= 'table' or type(command.impl) ~= 'function' then
|
||||
vim.notify(cmd_name .. ': Unknown subcommand: ' .. cmd, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
command.impl(args, opts)
|
||||
end
|
||||
|
||||
---@param opts table
|
||||
---@see vim.api.nvim_create_user_command
|
||||
local function rust_lsp(opts)
|
||||
run_command(rustlsp_command_tbl, rust_lsp_cmd_name, opts)
|
||||
end
|
||||
|
||||
---@param opts table
|
||||
---@see vim.api.nvim_create_user_command
|
||||
local function rustc(opts)
|
||||
run_command(rustc_command_tbl, rustc_cmd_name, opts)
|
||||
end
|
||||
|
||||
---@generic K, V
|
||||
---@param predicate fun(V):boolean
|
||||
---@param tbl table<K, V>
|
||||
---@return K[]
|
||||
local function tbl_keys_by_value_filter(predicate, tbl)
|
||||
local ret = {}
|
||||
for k, v in pairs(tbl) do
|
||||
if predicate(v) then
|
||||
ret[k] = v
|
||||
end
|
||||
end
|
||||
return vim.tbl_keys(ret)
|
||||
end
|
||||
|
||||
---Create the `:RustLsp` command
|
||||
function M.create_rust_lsp_command()
|
||||
vim.api.nvim_create_user_command(rust_lsp_cmd_name, rust_lsp, {
|
||||
nargs = '+',
|
||||
range = true,
|
||||
bang = true,
|
||||
desc = 'Interacts with the rust-analyzer LSP client',
|
||||
complete = function(arg_lead, cmdline, _)
|
||||
local commands = cmdline:match('^' .. rust_lsp_cmd_name .. '!') ~= nil
|
||||
-- bang!
|
||||
and tbl_keys_by_value_filter(function(command)
|
||||
return command.bang == true
|
||||
end, rustlsp_command_tbl)
|
||||
or vim.tbl_keys(rustlsp_command_tbl)
|
||||
local subcmd, subcmd_arg_lead = cmdline:match('^' .. rust_lsp_cmd_name .. '[!]*%s(%S+)%s(.*)$')
|
||||
if subcmd and subcmd_arg_lead and rustlsp_command_tbl[subcmd] and rustlsp_command_tbl[subcmd].complete then
|
||||
return rustlsp_command_tbl[subcmd].complete(subcmd_arg_lead)
|
||||
end
|
||||
if cmdline:match('^' .. rust_lsp_cmd_name .. '[!]*%s+%w*$') then
|
||||
return vim.tbl_filter(function(command)
|
||||
return command:find(arg_lead) ~= nil
|
||||
end, commands)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
--- Delete the `:RustLsp` command
|
||||
function M.delete_rust_lsp_command()
|
||||
if vim.cmd[rust_lsp_cmd_name] then
|
||||
pcall(vim.api.nvim_del_user_command, rust_lsp_cmd_name)
|
||||
end
|
||||
end
|
||||
|
||||
---Create the `:Rustc` command
|
||||
function M.create_rustc_command()
|
||||
vim.api.nvim_create_user_command(rustc_cmd_name, rustc, {
|
||||
nargs = '+',
|
||||
range = true,
|
||||
desc = 'Interacts with rustc',
|
||||
complete = function(arg_lead, cmdline, _)
|
||||
local commands = vim.tbl_keys(rustc_command_tbl)
|
||||
local subcmd, subcmd_arg_lead = cmdline:match('^' .. rustc_cmd_name .. '[!]*%s(%S+)%s(.*)$')
|
||||
if subcmd and subcmd_arg_lead and rustc_command_tbl[subcmd] and rustc_command_tbl[subcmd].complete then
|
||||
return rustc_command_tbl[subcmd].complete(subcmd_arg_lead)
|
||||
end
|
||||
if cmdline:match('^' .. rustc_cmd_name .. '[!]*%s+%w*$') then
|
||||
return vim.tbl_filter(function(command)
|
||||
return command:find(arg_lead) ~= nil
|
||||
end, commands)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,30 @@
|
||||
local M = {}
|
||||
|
||||
---@alias lsp_join_lines_params { textDocument: lsp_text_document, ranges: lsp_range[] }
|
||||
|
||||
---@param visual_mode boolean
|
||||
---@return lsp_join_lines_params
|
||||
local function get_params(visual_mode)
|
||||
local params = visual_mode and vim.lsp.util.make_given_range_params() or vim.lsp.util.make_range_params()
|
||||
local range = params.range
|
||||
|
||||
params.range = nil
|
||||
params.ranges = { range }
|
||||
|
||||
return params
|
||||
end
|
||||
|
||||
local function handler(_, result, ctx)
|
||||
vim.lsp.util.apply_text_edits(result, ctx.bufnr, vim.lsp.get_client_by_id(ctx.client_id).offset_encoding)
|
||||
end
|
||||
|
||||
local rl = require('rustaceanvim.rust_analyzer')
|
||||
|
||||
--- Sends the request to rust-analyzer to get the TextEdits to join the lines
|
||||
--- under the cursor and applies them
|
||||
---@param visual_mode boolean
|
||||
function M.join_lines(visual_mode)
|
||||
rl.buf_request(0, 'experimental/joinLines', get_params(visual_mode), handler)
|
||||
end
|
||||
|
||||
return M.join_lines
|
||||
@ -0,0 +1,32 @@
|
||||
local M = {}
|
||||
|
||||
---@alias lsp_move_items_params { textDocument: lsp_text_document, range: lsp_range, direction: 'Up' | 'Down' }
|
||||
|
||||
---@param up boolean
|
||||
---@return lsp_move_items_params
|
||||
local function get_params(up)
|
||||
local direction = up and 'Up' or 'Down'
|
||||
local params = vim.lsp.util.make_range_params()
|
||||
params.direction = direction
|
||||
|
||||
return params
|
||||
end
|
||||
|
||||
-- move it baby
|
||||
local function handler(_, result, ctx)
|
||||
if result == nil or #result == 0 then
|
||||
return
|
||||
end
|
||||
local overrides = require('rustaceanvim.overrides')
|
||||
overrides.snippet_text_edits_to_text_edits(result)
|
||||
vim.lsp.util.apply_text_edits(result, ctx.bufnr, vim.lsp.get_client_by_id(ctx.client_id).offset_encoding)
|
||||
end
|
||||
|
||||
local rl = require('rustaceanvim.rust_analyzer')
|
||||
|
||||
-- Sends the request to rust-analyzer to move the item and handle the response
|
||||
function M.move_item(up)
|
||||
rl.buf_request(0, 'experimental/moveItem', get_params(up or false), handler)
|
||||
end
|
||||
|
||||
return M.move_item
|
||||
@ -0,0 +1,27 @@
|
||||
local M = {}
|
||||
|
||||
local function get_params()
|
||||
return {
|
||||
textDocument = vim.lsp.util.make_text_document_params(0),
|
||||
}
|
||||
end
|
||||
|
||||
local function handler(_, result, ctx)
|
||||
if result == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local client = vim.lsp.get_client_by_id(ctx.client_id)
|
||||
if client then
|
||||
vim.lsp.util.jump_to_location(result, client.offset_encoding)
|
||||
end
|
||||
end
|
||||
|
||||
local rl = require('rustaceanvim.rust_analyzer')
|
||||
|
||||
--- Sends the request to rust-analyzer to get cargo.toml's location and open it
|
||||
function M.open_cargo_toml()
|
||||
rl.buf_request(0, 'experimental/openCargoToml', get_params(), handler)
|
||||
end
|
||||
|
||||
return M.open_cargo_toml
|
||||
@ -0,0 +1,33 @@
|
||||
local M = {}
|
||||
|
||||
local rl = require('rustaceanvim.rust_analyzer')
|
||||
local compat = require('rustaceanvim.compat')
|
||||
|
||||
local function get_params()
|
||||
return vim.lsp.util.make_position_params(0, nil)
|
||||
end
|
||||
|
||||
local function handler(_, result, ctx)
|
||||
if result == nil or vim.tbl_isempty(result) then
|
||||
vim.api.nvim_out_write("Can't find parent module\n")
|
||||
return
|
||||
end
|
||||
|
||||
local location = result
|
||||
|
||||
if compat.islist(result) then
|
||||
location = result[1]
|
||||
end
|
||||
|
||||
local client = vim.lsp.get_client_by_id(ctx.client_id)
|
||||
if client then
|
||||
vim.lsp.util.jump_to_location(location, client.offset_encoding)
|
||||
end
|
||||
end
|
||||
|
||||
--- Sends the request to rust-analyzer to get the parent modules location and open it
|
||||
function M.parent_module()
|
||||
rl.buf_request(0, 'experimental/parentModule', get_params(), handler)
|
||||
end
|
||||
|
||||
return M.parent_module
|
||||
@ -0,0 +1,18 @@
|
||||
local M = {}
|
||||
|
||||
---@param err string | nil
|
||||
local function handler(err, _, _)
|
||||
if err then
|
||||
vim.notify('Error rebuilding proc macros: ' .. err)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local rl = require('rustaceanvim.rust_analyzer')
|
||||
|
||||
--- Sends the request to rust-analyzer rebuild proc macros
|
||||
function M.rebuild_macros()
|
||||
rl.any_buf_request('rust-analyzer/rebuildProcMacros', nil, handler)
|
||||
end
|
||||
|
||||
return M.rebuild_macros
|
||||
@ -0,0 +1,146 @@
|
||||
local M = {}
|
||||
|
||||
local config = require('rustaceanvim.config.internal')
|
||||
local compat = require('rustaceanvim.compat')
|
||||
local ui = require('rustaceanvim.ui')
|
||||
local api = vim.api
|
||||
local ts = vim.treesitter
|
||||
|
||||
local rustc = 'rustc'
|
||||
|
||||
-- TODO: See if these can be queried from rustc?
|
||||
M.available_unpretty = {
|
||||
'normal',
|
||||
'identified',
|
||||
'expanded',
|
||||
'expanded,identified',
|
||||
'expanded,hygiene',
|
||||
'ast-tree',
|
||||
'ast-tree,expanded',
|
||||
'hir',
|
||||
'hir,identified',
|
||||
'hir,typed',
|
||||
'hir-tree',
|
||||
'thir-tree',
|
||||
'thir-flat',
|
||||
'mir',
|
||||
'stable-mir',
|
||||
'mir-cfg',
|
||||
}
|
||||
---@alias rustcir_level 'normal'| 'identified'| 'expanded'| 'expanded,identified'| 'expanded,hygiene'| 'ast-tree'| 'ast-tree,expanded'| 'hir'| 'hir,identified'| 'hir,typed'| 'hir-tree'| 'thir-tree'| 'thir-flat'| 'mir'| 'stable-mir'| 'mir-cfg'
|
||||
|
||||
---@type integer | nil
|
||||
local latest_buf_id = nil
|
||||
|
||||
---Get a compatible vim range (1 index based) from a TS node range.
|
||||
---
|
||||
---TS nodes start with 0 and the end col is ending exclusive.
|
||||
---They also treat a EOF/EOL char as a char ending in the first
|
||||
---col of the next row.
|
||||
---comment
|
||||
---@param range integer[]
|
||||
---@param buf integer|nil
|
||||
---@return integer, integer, integer, integer
|
||||
local function get_vim_range(range, buf)
|
||||
---@type integer, integer, integer, integer
|
||||
local srow, scol, erow, ecol = unpack(range)
|
||||
srow = srow + 1
|
||||
scol = scol + 1
|
||||
erow = erow + 1
|
||||
|
||||
if ecol == 0 then
|
||||
-- Use the value of the last col of the previous row instead.
|
||||
erow = erow - 1
|
||||
if not buf or buf == 0 then
|
||||
ecol = vim.fn.col { erow, '$' } - 1
|
||||
else
|
||||
ecol = #vim.api.nvim_buf_get_lines(buf, erow - 1, erow, false)[1]
|
||||
end
|
||||
ecol = math.max(ecol, 1)
|
||||
end
|
||||
|
||||
return srow, scol, erow, ecol
|
||||
end
|
||||
|
||||
---@param node TSNode
|
||||
local function get_rows(node)
|
||||
local start_row, _, end_row, _ = get_vim_range({ ts.get_node_range(node) }, 0)
|
||||
return vim.api.nvim_buf_get_lines(0, start_row - 1, end_row, true)
|
||||
end
|
||||
|
||||
---@param sc vim.SystemCompleted
|
||||
local function handler(sc)
|
||||
if sc.code ~= 0 then
|
||||
vim.notify('rustc unpretty failed' .. sc.stderr, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- check if a buffer with the latest id is already open, if it is then
|
||||
-- delete it and continue
|
||||
ui.delete_buf(latest_buf_id)
|
||||
|
||||
-- create a new buffer
|
||||
latest_buf_id = vim.api.nvim_create_buf(false, true) -- not listed and scratch
|
||||
|
||||
-- split the window to create a new buffer and set it to our window
|
||||
ui.split(true, latest_buf_id)
|
||||
|
||||
local lines = vim.split(sc.stdout, '\n')
|
||||
|
||||
-- set filetype to rust for syntax highlighting
|
||||
vim.bo[latest_buf_id].filetype = 'rust'
|
||||
-- write the expansion content to the buffer
|
||||
vim.api.nvim_buf_set_lines(latest_buf_id, 0, 0, false, lines)
|
||||
end
|
||||
|
||||
---@param level rustcir_level
|
||||
function M.rustc_unpretty(level)
|
||||
if #api.nvim_get_runtime_file('parser/rust.so', true) == 0 then
|
||||
vim.notify("a treesitter parser for Rust is required for 'rustc unpretty'", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
if vim.fn.executable(rustc) ~= 1 then
|
||||
vim.notify('rustc is needed to rustc unpretty.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local text
|
||||
|
||||
local cursor = api.nvim_win_get_cursor(0)
|
||||
local pos = { math.max(cursor[1] - 1, 0), cursor[2] }
|
||||
|
||||
local cline = api.nvim_get_current_line()
|
||||
if not string.find(cline, 'fn%s+') then
|
||||
local temp = vim.fn.searchpos('fn ', 'bcn', vim.fn.line('w0'))
|
||||
pos = { math.max(temp[1] - 1, 0), temp[2] }
|
||||
end
|
||||
|
||||
local node = ts.get_node { pos = pos }
|
||||
|
||||
if node == nil or node:type() ~= 'function_item' then
|
||||
vim.notify('no function found or function is incomplete', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local b = get_rows(node)
|
||||
if b == nil then
|
||||
vim.notify('get code text failed', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
text = table.concat(b, '\n')
|
||||
|
||||
compat.system({
|
||||
rustc,
|
||||
'--crate-type',
|
||||
'lib',
|
||||
'--edition',
|
||||
config.tools.rustc.edition,
|
||||
'-Z',
|
||||
'unstable-options',
|
||||
'-Z',
|
||||
'unpretty=' .. level,
|
||||
'-',
|
||||
}, { stdin = text }, vim.schedule_wrap(handler))
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,41 @@
|
||||
local M = {}
|
||||
|
||||
---@param query string
|
||||
---@param visual_mode boolean
|
||||
local function get_opts(query, visual_mode)
|
||||
local opts = vim.lsp.util.make_position_params()
|
||||
local range = (visual_mode and vim.lsp.util.make_given_range_params() or vim.lsp.util.make_range_params()).range
|
||||
opts.query = query
|
||||
opts.parseOnly = false
|
||||
opts.selections = { range }
|
||||
return opts
|
||||
end
|
||||
|
||||
local function handler(err, result, ctx)
|
||||
if err then
|
||||
error('Could not execute request to server: ' .. err.message)
|
||||
return
|
||||
end
|
||||
|
||||
local client = vim.lsp.get_client_by_id(ctx.client_id)
|
||||
if client then
|
||||
vim.lsp.util.apply_workspace_edit(result, client.offset_encoding)
|
||||
end
|
||||
end
|
||||
|
||||
local rl = require('rustaceanvim.rust_analyzer')
|
||||
|
||||
---@param query string | nil
|
||||
---@param visual_mode boolean
|
||||
function M.ssr(query, visual_mode)
|
||||
if not query then
|
||||
vim.ui.input({ prompt = 'Enter query: ' }, function(input)
|
||||
query = input
|
||||
end)
|
||||
end
|
||||
if query then
|
||||
rl.buf_request(0, 'experimental/ssr', get_opts(query, visual_mode), handler)
|
||||
end
|
||||
end
|
||||
|
||||
return M.ssr
|
||||
@ -0,0 +1,37 @@
|
||||
local ui = require('rustaceanvim.ui')
|
||||
|
||||
local M = {}
|
||||
|
||||
---@return lsp_range_params
|
||||
local function get_params()
|
||||
return vim.lsp.util.make_range_params()
|
||||
end
|
||||
|
||||
---@type integer | nil
|
||||
local latest_buf_id = nil
|
||||
|
||||
local function parse_lines(result)
|
||||
local ret = {}
|
||||
|
||||
for line in string.gmatch(result, '([^\n]+)') do
|
||||
table.insert(ret, line)
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
local function handler(_, result)
|
||||
ui.delete_buf(latest_buf_id)
|
||||
latest_buf_id = vim.api.nvim_create_buf(false, true)
|
||||
ui.split(true, latest_buf_id)
|
||||
vim.api.nvim_buf_set_name(latest_buf_id, 'syntax.rust')
|
||||
vim.api.nvim_buf_set_text(latest_buf_id, 0, 0, 0, 0, parse_lines(result))
|
||||
ui.resize(true, '-25')
|
||||
end
|
||||
|
||||
local rl = require('rustaceanvim.rust_analyzer')
|
||||
function M.syntax_tree()
|
||||
rl.buf_request(0, 'rust-analyzer/syntaxTree', get_params(), handler)
|
||||
end
|
||||
|
||||
return M.syntax_tree
|
||||
@ -0,0 +1,50 @@
|
||||
local M = {}
|
||||
|
||||
local rl = require('rustaceanvim.rust_analyzer')
|
||||
local ui = require('rustaceanvim.ui')
|
||||
|
||||
---@type integer | nil
|
||||
local latest_buf_id = nil
|
||||
|
||||
---@alias ir_level 'Hir' | 'Mir'
|
||||
|
||||
local function handler(level, err, result)
|
||||
local requestType = 'view' .. level
|
||||
if err then
|
||||
vim.notify(requestType .. ' failed' .. (result and ': ' .. result or vim.inspect(err)), vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
if result and result:match('Not inside a function body') then
|
||||
vim.notify(requestType .. ' failed: Not inside a function body', vim.log.levels.ERROR)
|
||||
return
|
||||
elseif type(result) ~= 'string' then
|
||||
vim.notify(requestType .. ' failed: ' .. vim.inspect(result), vim.log.levels.ERROR)
|
||||
end
|
||||
|
||||
-- check if a buffer with the latest id is already open, if it is then
|
||||
-- delete it and continue
|
||||
ui.delete_buf(latest_buf_id)
|
||||
|
||||
-- create a new buffer
|
||||
latest_buf_id = vim.api.nvim_create_buf(false, true) -- not listed and scratch
|
||||
|
||||
-- split the window to create a new buffer and set it to our window
|
||||
ui.split(true, latest_buf_id)
|
||||
|
||||
local lines = vim.split(result, '\n')
|
||||
|
||||
-- set filetype to rust for syntax highlighting
|
||||
vim.bo[latest_buf_id].filetype = 'rust'
|
||||
-- write the expansion content to the buffer
|
||||
vim.api.nvim_buf_set_lines(latest_buf_id, 0, 0, false, lines)
|
||||
end
|
||||
|
||||
---@param level ir_level
|
||||
function M.viewIR(level)
|
||||
local position_params = vim.lsp.util.make_position_params(0, nil)
|
||||
rl.buf_request(0, 'rust-analyzer/view' .. level, position_params, function(...)
|
||||
return handler(level, ...)
|
||||
end)
|
||||
end
|
||||
|
||||
return M.viewIR
|
||||
@ -0,0 +1,18 @@
|
||||
local M = {}
|
||||
|
||||
local function handler(err)
|
||||
if err then
|
||||
vim.notify(tostring(err), vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
vim.notify('Cargo workspace reloaded')
|
||||
end
|
||||
|
||||
local rl = require('rustaceanvim.rust_analyzer')
|
||||
|
||||
function M.reload_workspace()
|
||||
vim.notify('Reloading Cargo Workspace')
|
||||
rl.any_buf_request('rust-analyzer/reloadWorkspace', nil, handler)
|
||||
end
|
||||
|
||||
return M.reload_workspace
|
||||
@ -0,0 +1,63 @@
|
||||
local rl = require('rustaceanvim.rust_analyzer')
|
||||
|
||||
local M = {}
|
||||
|
||||
---@enum WorkspaceSymbolSearchScope
|
||||
M.WorkspaceSymbolSearchScope = {
|
||||
workspace = 'workspace',
|
||||
workspaceAndDependencies = 'workspaceAndDependencies',
|
||||
}
|
||||
|
||||
---@enum WorkspaceSymbolSearchKind
|
||||
M.WorkspaceSymbolSearchKind = {
|
||||
onlyTypes = 'onlyTypes',
|
||||
allSymbols = 'allSymbols',
|
||||
}
|
||||
|
||||
---@type WorkspaceSymbolSearchKind
|
||||
local default_search_kind = M.WorkspaceSymbolSearchKind.allSymbols
|
||||
|
||||
---@param searchScope WorkspaceSymbolSearchScope
|
||||
---@param searchKind WorkspaceSymbolSearchKind
|
||||
---@param query string
|
||||
local function get_params(searchScope, searchKind, query)
|
||||
return {
|
||||
query = query,
|
||||
searchScope = searchScope,
|
||||
searchKind = searchKind,
|
||||
}
|
||||
end
|
||||
|
||||
---@return string | nil
|
||||
local function query_from_input()
|
||||
return vim.F.npcall(vim.fn.input, 'Query: ')
|
||||
end
|
||||
|
||||
---@param searchScope WorkspaceSymbolSearchScope
|
||||
---@param args? unknown[]
|
||||
function M.workspace_symbol(searchScope, args)
|
||||
local searchKind = default_search_kind
|
||||
local query
|
||||
if not args or #args == 0 then
|
||||
query = query_from_input()
|
||||
if query == nil then
|
||||
return
|
||||
end
|
||||
args = {}
|
||||
end
|
||||
if #args > 0 and M.WorkspaceSymbolSearchKind[args[1]] then
|
||||
searchKind = args[1]
|
||||
table.remove(args, 1)
|
||||
end
|
||||
if #args == 0 then
|
||||
query = query_from_input()
|
||||
if not query then
|
||||
return
|
||||
end
|
||||
else
|
||||
query = args[1]
|
||||
end
|
||||
rl.any_buf_request('workspace/symbol', get_params(searchScope, searchKind, query))
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,116 @@
|
||||
---@diagnostic disable: deprecated, duplicate-doc-field, duplicate-doc-alias
|
||||
---@mod rustaceanvim.compat Functions for backward compatibility with older Neovim versions
|
||||
--- and with compatibility type annotations to make the type checker
|
||||
--- happy for both stable and nightly neovim versions.
|
||||
|
||||
local M = {}
|
||||
|
||||
M.joinpath = vim.fs.joinpath or function(...)
|
||||
return (table.concat({ ... }, '/'):gsub('//+', '/'))
|
||||
end
|
||||
|
||||
---@class vim.lsp.get_clients.Filter
|
||||
---@field id integer|nil Match clients by id
|
||||
---@field bufnr integer|nil match clients attached to the given buffer
|
||||
---@field name string|nil match clients by name
|
||||
---@field method string|nil match client by supported method name
|
||||
|
||||
---@alias vim.lsp.get_active_clients.filter vim.lsp.get_clients.Filter
|
||||
---@alias lsp.Client vim.lsp.Client
|
||||
---@alias lsp.ClientConfig vim.lsp.ClientConfig
|
||||
|
||||
M.get_clients = vim.lsp.get_clients or vim.lsp.get_active_clients
|
||||
|
||||
M.uv = vim.uv or vim.loop
|
||||
|
||||
--- @enum vim.diagnostic.Severity
|
||||
M.severity = {
|
||||
ERROR = 1,
|
||||
WARN = 2,
|
||||
INFO = 3,
|
||||
HINT = 4,
|
||||
[1] = 'ERROR',
|
||||
[2] = 'WARN',
|
||||
[3] = 'INFO',
|
||||
[4] = 'HINT',
|
||||
}
|
||||
|
||||
--- @class vim.Diagnostic
|
||||
--- @field bufnr? integer
|
||||
--- @field lnum integer 0-indexed
|
||||
--- @field end_lnum? integer 0-indexed
|
||||
--- @field col integer 0-indexed
|
||||
--- @field end_col? integer 0-indexed
|
||||
--- @field severity? vim.diagnostic.Severity
|
||||
--- @field message string
|
||||
--- @field source? string
|
||||
--- @field code? string
|
||||
--- @field _tags? { deprecated: boolean, unnecessary: boolean}
|
||||
--- @field user_data? any arbitrary data plugins can add
|
||||
--- @field namespace? integer
|
||||
|
||||
--- @class vim.api.keyset.user_command
|
||||
--- @field addr? any
|
||||
--- @field bang? boolean
|
||||
--- @field bar? boolean
|
||||
--- @field complete? any
|
||||
--- @field count? any
|
||||
--- @field desc? any
|
||||
--- @field force? boolean
|
||||
--- @field keepscript? boolean
|
||||
--- @field nargs? any
|
||||
--- @field preview? any
|
||||
--- @field range? any
|
||||
--- @field register? boolean
|
||||
|
||||
--- @class vim.SystemCompleted
|
||||
--- @field code integer
|
||||
--- @field signal integer
|
||||
--- @field stdout? string
|
||||
--- @field stderr? string
|
||||
|
||||
M.system = vim.system
|
||||
-- wrapper around vim.fn.system to give it a similar API to vim.system
|
||||
or function(cmd, opts, on_exit)
|
||||
---@cast cmd string[]
|
||||
---@cast opts vim.SystemOpts | nil
|
||||
---@cast on_exit fun(sc: vim.SystemCompleted) | nil
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
if opts and opts.cwd then
|
||||
local shell = require('rustaceanvim.shell')
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
cmd = shell.chain_commands { shell.make_cd_command(opts.cwd), table.concat(cmd, ' ') }
|
||||
---@cast cmd string
|
||||
end
|
||||
|
||||
local output = vim.fn.system(cmd)
|
||||
local ok = vim.v.shell_error
|
||||
---@type vim.SystemCompleted
|
||||
local systemObj = {
|
||||
signal = 0,
|
||||
stdout = ok and (output or '') or nil,
|
||||
stderr = not ok and (output or '') or nil,
|
||||
code = vim.v.shell_error,
|
||||
}
|
||||
if on_exit then
|
||||
on_exit(systemObj)
|
||||
end
|
||||
return systemObj
|
||||
end
|
||||
|
||||
M.list_contains = vim.list_contains
|
||||
or function(t, value)
|
||||
vim.validate { t = { t, 't' } }
|
||||
for _, v in ipairs(t) do
|
||||
if v == value then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---@alias lsp.Handler fun(err: lsp.ResponseError?, result: any, context: lsp.HandlerContext, config?: table): ...any
|
||||
|
||||
M.islist = vim.islist or vim.tbl_islist
|
||||
|
||||
return M
|
||||
@ -0,0 +1,158 @@
|
||||
---@mod rustaceanvim.config.check rustaceanvim configuration check
|
||||
|
||||
local types = require('rustaceanvim.types.internal')
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param path string
|
||||
---@param msg string|nil
|
||||
---@return string
|
||||
local function mk_error_msg(path, msg)
|
||||
return msg and path .. '.' .. msg or path
|
||||
end
|
||||
|
||||
---@param path string The config path
|
||||
---@param tbl table The table to validate
|
||||
---@see vim.validate
|
||||
---@return boolean is_valid
|
||||
---@return string|nil error_message
|
||||
local function validate(path, tbl)
|
||||
local prefix = 'Invalid config: '
|
||||
local ok, err = pcall(vim.validate, tbl)
|
||||
return ok or false, prefix .. mk_error_msg(path, err)
|
||||
end
|
||||
|
||||
---Validates the config.
|
||||
---@param cfg RustaceanConfig
|
||||
---@return boolean is_valid
|
||||
---@return string|nil error_message
|
||||
function M.validate(cfg)
|
||||
local ok, err
|
||||
ok, err = validate('rustaceanvim', {
|
||||
tools = { cfg.tools, 'table' },
|
||||
server = { cfg.server, 'table' },
|
||||
dap = { cfg.dap, 'table' },
|
||||
})
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
local tools = cfg.tools
|
||||
local crate_graph = tools.crate_graph
|
||||
ok, err = validate('tools.crate_graph', {
|
||||
backend = { crate_graph.backend, 'string', true },
|
||||
enabled_graphviz_backends = { crate_graph.enabled_graphviz_backends, 'table', true },
|
||||
full = { crate_graph.full, 'boolean' },
|
||||
output = { crate_graph.output, 'string', true },
|
||||
pipe = { crate_graph.pipe, 'string', true },
|
||||
})
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
local hover_actions = tools.hover_actions
|
||||
ok, err = validate('tools.hover_actions', {
|
||||
replace_builtin_hover = { hover_actions.replace_builtin_hover, 'boolean' },
|
||||
})
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
local float_win_config = tools.float_win_config
|
||||
ok, err = validate('tools.float_win_config', {
|
||||
auto_focus = { float_win_config.auto_focus, 'boolean' },
|
||||
open_split = { float_win_config.open_split, 'string' },
|
||||
})
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
local rustc = tools.rustc
|
||||
ok, err = validate('tools.rustc', {
|
||||
edition = { rustc.edition, 'string' },
|
||||
})
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
ok, err = validate('tools', {
|
||||
executor = { tools.executor, { 'table', 'string' } },
|
||||
test_executor = { tools.test_executor, { 'table', 'string' } },
|
||||
crate_test_executor = { tools.crate_test_executor, { 'table', 'string' } },
|
||||
cargo_override = { tools.cargo_override, 'string', true },
|
||||
enable_nextest = { tools.enable_nextest, 'boolean' },
|
||||
enable_clippy = { tools.enable_clippy, 'boolean' },
|
||||
on_initialized = { tools.on_initialized, 'function', true },
|
||||
reload_workspace_from_cargo_toml = { tools.reload_workspace_from_cargo_toml, 'boolean' },
|
||||
open_url = { tools.open_url, 'function' },
|
||||
})
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
local server = cfg.server
|
||||
ok, err = validate('server', {
|
||||
cmd = { server.cmd, { 'function', 'table' } },
|
||||
standalone = { server.standalone, 'boolean' },
|
||||
settings = { server.settings, { 'function', 'table' }, true },
|
||||
})
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
if type(server.settings) == 'table' then
|
||||
ok, err = validate('server.settings', {
|
||||
['rust-analyzer'] = { server.settings['rust-analyzer'], 'table', true },
|
||||
})
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
local dap = cfg.dap
|
||||
local adapter = types.evaluate(dap.adapter)
|
||||
if adapter == false then
|
||||
ok = true
|
||||
elseif adapter.type == 'executable' then
|
||||
---@cast adapter DapExecutableConfig
|
||||
ok, err = validate('dap.adapter', {
|
||||
command = { adapter.command, 'string' },
|
||||
name = { adapter.name, 'string', true },
|
||||
args = { adapter.args, 'table', true },
|
||||
})
|
||||
elseif adapter.type == 'server' then
|
||||
---@cast adapter DapServerConfig
|
||||
ok, err = validate('dap.adapter', {
|
||||
command = { adapter.executable, 'table' },
|
||||
name = { adapter.name, 'string', true },
|
||||
host = { adapter.host, 'string', true },
|
||||
port = { adapter.port, 'string' },
|
||||
})
|
||||
if ok then
|
||||
ok, err = validate('dap.adapter.executable', {
|
||||
command = { adapter.executable.command, 'string' },
|
||||
args = { adapter.executable.args, 'table', true },
|
||||
})
|
||||
end
|
||||
else
|
||||
ok = false
|
||||
err = 'dap.adapter: Expected DapExecutableConfig, DapServerConfig or false'
|
||||
end
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---@param callback fun(msg: string)
|
||||
function M.check_for_lspconfig_conflict(callback)
|
||||
for _, autocmd in ipairs(vim.api.nvim_get_autocmds { event = 'FileType', pattern = 'rust' }) do
|
||||
if
|
||||
autocmd.group_name
|
||||
and autocmd.group_name == 'lspconfig'
|
||||
and autocmd.desc
|
||||
and autocmd.desc:match('rust_analyzer')
|
||||
then
|
||||
callback([[
|
||||
nvim-lspconfig.rust_analyzer has been setup.
|
||||
This will likely lead to conflicts with the rustaceanvim LSP client.
|
||||
See ':h rustaceanvim.mason'
|
||||
]])
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,183 @@
|
||||
---@mod rustaceanvim.config plugin configuration
|
||||
---
|
||||
---@brief [[
|
||||
---
|
||||
---rustaceanvim is a filetype plugin, and does not need
|
||||
---a `setup` function to work.
|
||||
---
|
||||
---To configure rustaceanvim, set the variable `vim.g.rustaceanvim`,
|
||||
---which is a `RustaceanOpts` table, in your neovim configuration.
|
||||
---
|
||||
---Example:
|
||||
---
|
||||
--->lua
|
||||
------@type RustaceanOpts
|
||||
---vim.g.rustaceanvim = {
|
||||
--- ---@type RustaceanToolsOpts
|
||||
--- tools = {
|
||||
--- -- ...
|
||||
--- },
|
||||
--- ---@type RustaceanLspClientOpts
|
||||
--- server = {
|
||||
--- on_attach = function(client, bufnr)
|
||||
--- -- Set keybindings, etc. here.
|
||||
--- end,
|
||||
--- default_settings = {
|
||||
--- -- rust-analyzer language server configuration
|
||||
--- ['rust-analyzer'] = {
|
||||
--- },
|
||||
--- },
|
||||
--- -- ...
|
||||
--- },
|
||||
--- ---@type RustaceanDapOpts
|
||||
--- dap = {
|
||||
--- -- ...
|
||||
--- },
|
||||
--- }
|
||||
---<
|
||||
---
|
||||
---Notes:
|
||||
---
|
||||
--- - `vim.g.rustaceanvim` can also be a function that returns a `RustaceanOpts` table.
|
||||
--- - `server.settings`, by default, is a function that looks for a `rust-analyzer.json` file
|
||||
--- in the project root, to load settings from it. It falls back to an empty table.
|
||||
---
|
||||
---@brief ]]
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type RustaceanOpts | fun():RustaceanOpts | nil
|
||||
vim.g.rustaceanvim = vim.g.rustaceanvim
|
||||
|
||||
---@class RustaceanOpts
|
||||
---@field tools? RustaceanToolsOpts Plugin options
|
||||
---@field server? RustaceanLspClientOpts Language server client options
|
||||
---@field dap? RustaceanDapOpts Debug adapter options
|
||||
|
||||
---@class RustaceanToolsOpts
|
||||
---@field executor? RustaceanExecutor | executor_alias The executor to use for runnables/debuggables
|
||||
---@field test_executor? RustaceanExecutor | test_executor_alias The executor to use for runnables that are tests / testables
|
||||
---@field crate_test_executor? RustaceanExecutor | test_executor_alias The executor to use for runnables that are crate test suites (--all-targets)
|
||||
---@field cargo_override? string Set this to override the 'cargo' command for runnables, debuggables (etc., e.g. to 'cross'). If set, this takes precedence over 'enable_nextest'.
|
||||
---@field enable_nextest? boolean Whether to enable nextest. If enabled, `cargo test` commands will be transformed to `cargo nextest run` commands. Defaults to `true` if cargo-nextest is detected. Ignored if `cargo_override` is set.
|
||||
---@field enable_clippy? boolean Whether to enable clippy checks on save if a clippy installation is detected. Default: `true`
|
||||
---@field on_initialized? fun(health:RustAnalyzerInitializedStatus) Function that is invoked when the LSP server has finished initializing
|
||||
---@field reload_workspace_from_cargo_toml? boolean Automatically call `RustReloadWorkspace` when writing to a Cargo.toml file
|
||||
---@field hover_actions? RustaceanHoverActionsOpts Options for hover actions
|
||||
---@field code_actions? RustaceanCodeActionOpts Options for code actions
|
||||
---@field float_win_config? FloatWinConfig Options applied to floating windows. See |api-win_config|.
|
||||
---@field create_graph? RustaceanCrateGraphConfig Options for showing the crate graph based on graphviz and the dot
|
||||
---@field open_url? fun(url:string):nil If set, overrides how to open URLs
|
||||
---@field rustc? RustcOpts Options for `rustc`
|
||||
|
||||
---@class RustaceanExecutor
|
||||
---@field execute_command fun(cmd:string, args:string[], cwd:string|nil, opts?: RustaceanExecutorOpts)
|
||||
|
||||
---@class RustaceanExecutorOpts
|
||||
---@field bufnr? integer The buffer from which the executor was invoked.
|
||||
|
||||
---@class FloatWinConfig
|
||||
---@field auto_focus? boolean
|
||||
---@field open_split? 'horizontal' | 'vertical'
|
||||
---@see vim.lsp.util.open_floating_preview.Opts
|
||||
---@see vim.api.nvim_open_win
|
||||
|
||||
---@alias executor_alias 'termopen' | 'quickfix' | 'toggleterm' | 'vimux' | 'neotest'
|
||||
|
||||
---@alias test_executor_alias executor_alias | 'background'
|
||||
|
||||
---@class RustaceanHoverActionsOpts
|
||||
---@field replace_builtin_hover? boolean Whether to replace Neovim's built-in `vim.lsp.buf.hover` with hover actions. Default: `true`
|
||||
|
||||
---@class RustaceanCodeActionOpts
|
||||
---@field group_icon? string Text appended to a group action
|
||||
---@field ui_select_fallback? boolean Whether to fall back to `vim.ui.select` if there are no grouped code actions. Default: `false`
|
||||
|
||||
---@alias lsp_server_health_status 'ok' | 'warning' | 'error'
|
||||
|
||||
---@class RustAnalyzerInitializedStatus
|
||||
---@field health lsp_server_health_status
|
||||
|
||||
---@class RustaceanCrateGraphConfig
|
||||
---@field backend? string Backend used for displaying the graph. See: https://graphviz.org/docs/outputs/ Defaults to `"x11"` if unset.
|
||||
---@field output? string Where to store the output. No output if unset. Relative path from `cwd`.
|
||||
---@field enabled_graphviz_backends? string[] Override the enabled graphviz backends list, used for input validation and autocompletion.
|
||||
---@field pipe? string Overide the pipe symbol in the shell command. Useful if using a shell that is not supported by this plugin.
|
||||
|
||||
---@class RustcOpts
|
||||
---@field edition string The edition to use. See https://rustc-dev-guide.rust-lang.org/guides/editions.html. Default '2021'.
|
||||
|
||||
---@class RustaceanLspClientOpts
|
||||
---@field auto_attach? boolean | fun(bufnr: integer):boolean Whether to automatically attach the LSP client. Defaults to `true` if the `rust-analyzer` executable is found.
|
||||
---@field cmd? string[] | fun():string[] Command and arguments for starting rust-analyzer
|
||||
---@field settings? table | fun(project_root:string|nil, default_settings: table):table Setting passed to rust-analyzer. Defaults to a function that looks for a `rust-analyzer.json` file or returns an empty table. See https://rust-analyzer.github.io/manual.html#configuration.
|
||||
---@field standalone? boolean Standalone file support (enabled by default). Disabling it may improve rust-analyzer's startup time.
|
||||
---@field logfile? string The path to the rust-analyzer log file.
|
||||
---@field load_vscode_settings? boolean Whether to search (upward from the buffer) for rust-analyzer settings in .vscode/settings json. If found, loaded settings will override configured options. Default: false
|
||||
|
||||
---@class RustaceanDapOpts
|
||||
--- @field autoload_configurations boolean Whether to autoload nvim-dap configurations when rust-analyzer has attached? Default: `true`.
|
||||
---@field adapter? DapExecutableConfig | DapServerConfig | disable | fun():(DapExecutableConfig | DapServerConfig | disable) Defaults to creating the `rt_lldb` adapter, which is a `DapServerConfig` if `codelldb` is detected, and a `DapExecutableConfig` if `lldb` is detected. Set to `false` to disable.
|
||||
---@field configuration? DapClientConfig | disable | fun():(DapClientConfig | disable) Dap client configuration. Defaults to a function that looks for a `launch.json` file or returns a `DapExecutableConfig` that launches the `rt_lldb` adapter. Set to `false` to disable.
|
||||
---@field add_dynamic_library_paths? boolean | fun():boolean Accommodate dynamically-linked targets by passing library paths to lldb. Default: `true`.
|
||||
---@field auto_generate_source_map? fun():boolean | boolean Whether to auto-generate a source map for the standard library.
|
||||
---@field load_rust_types? fun():boolean | boolean Whether to get Rust types via initCommands (rustlib/etc/lldb_commands, lldb only). Default: `true`.
|
||||
|
||||
---@alias disable false
|
||||
|
||||
---@alias DapCommand string
|
||||
|
||||
---@class DapExecutableConfig
|
||||
---@field type dap_adapter_type_executable The type of debug adapter.
|
||||
---@field command string Default: `"lldb-vscode"`.
|
||||
---@field args? string Default: unset.
|
||||
---@field name? string Default: `"lldb"`.
|
||||
|
||||
---@class DapServerConfig
|
||||
---@field type dap_adapter_type_server The type of debug adapter.
|
||||
---@field host? string The host to connect to.
|
||||
---@field port string The port to connect to.
|
||||
---@field executable DapExecutable The executable to run
|
||||
---@field name? string
|
||||
|
||||
---@class DapExecutable
|
||||
---@field command string The executable.
|
||||
---@field args string[] Its arguments.
|
||||
|
||||
---@alias dap_adapter_type_executable "executable"
|
||||
---@alias dap_adapter_type_server "server"
|
||||
|
||||
---@class DapClientConfig: Configuration
|
||||
---@field type string The dap adapter to use
|
||||
---@field name string
|
||||
---@field request dap_config_request_launch | dap_config_request_attach | dap_config_request_custom The type of dap session
|
||||
---@field cwd? string Current working directory
|
||||
---@field program? string Path to executable for most DAP clients
|
||||
---@field args? string[] Optional args to DAP client, not valid for all client types
|
||||
---@field env? EnvironmentMap Environmental variables
|
||||
---@field initCommands? string[] Initial commands to run, `lldb` clients only
|
||||
---@field coreConfigs? table Essential config values for `probe-rs` client, see https://probe.rs/docs/tools/debugger/
|
||||
|
||||
---@alias EnvironmentMap table<string, string[]>
|
||||
|
||||
---@alias dap_config_request_launch "launch"
|
||||
---@alias dap_config_request_attach "attach"
|
||||
---@alias dap_config_request_custom "custom"
|
||||
|
||||
---For the heroes who want to use it.
|
||||
---@param codelldb_path string Path to the codelldb executable
|
||||
---@param liblldb_path string Path to the liblldb dynamic library
|
||||
---@return DapServerConfig
|
||||
function M.get_codelldb_adapter(codelldb_path, liblldb_path)
|
||||
return {
|
||||
type = 'server',
|
||||
port = '${port}',
|
||||
host = '127.0.0.1',
|
||||
executable = {
|
||||
command = codelldb_path,
|
||||
args = { '--liblldb', liblldb_path, '--port', '${port}' },
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,425 @@
|
||||
local types = require('rustaceanvim.types.internal')
|
||||
local compat = require('rustaceanvim.compat')
|
||||
local config = require('rustaceanvim.config')
|
||||
local executors = require('rustaceanvim.executors')
|
||||
local os = require('rustaceanvim.os')
|
||||
local server_config = require('rustaceanvim.config.server')
|
||||
|
||||
local RustaceanConfig
|
||||
|
||||
---@class RustAnalyzerInitializedStatusInternal : RustAnalyzerInitializedStatus
|
||||
---@field health lsp_server_health_status
|
||||
---@field quiescent boolean inactive?
|
||||
---
|
||||
---@param dap_adapter DapExecutableConfig | DapServerConfig | disable
|
||||
---@return boolean
|
||||
local function should_enable_dap_config_value(dap_adapter)
|
||||
local adapter = types.evaluate(dap_adapter)
|
||||
if adapter == false then
|
||||
return false
|
||||
end
|
||||
return vim.fn.executable('rustc') == 1
|
||||
end
|
||||
|
||||
---@param adapter DapServerConfig | DapExecutableConfig
|
||||
local function is_codelldb_adapter(adapter)
|
||||
return adapter.type == 'server'
|
||||
end
|
||||
|
||||
---@param adapter DapServerConfig | DapExecutableConfig
|
||||
local function is_lldb_adapter(adapter)
|
||||
return adapter.type == 'executable'
|
||||
end
|
||||
|
||||
---@param type string
|
||||
---@return DapClientConfig
|
||||
local function load_dap_configuration(type)
|
||||
-- default
|
||||
---@type DapClientConfig
|
||||
local dap_config = {
|
||||
name = 'Rust debug client',
|
||||
type = type,
|
||||
request = 'launch',
|
||||
stopOnEntry = false,
|
||||
}
|
||||
---@diagnostic disable-next-line: different-requires
|
||||
local dap = require('dap')
|
||||
-- Load configurations from a `launch.json`.
|
||||
-- It is necessary to check for changes in the `dap.configurations` table, as
|
||||
-- `load_launchjs` does not return anything, it loads directly into `dap.configurations`.
|
||||
local pre_launch = vim.deepcopy(dap.configurations) or {}
|
||||
require('dap.ext.vscode').load_launchjs(nil, { lldb = { 'rust' }, codelldb = { 'rust' } })
|
||||
for name, configuration_entries in pairs(dap.configurations) do
|
||||
if pre_launch[name] == nil or not vim.deep_equal(pre_launch[name], configuration_entries) then
|
||||
-- `configurations` are tables of `configuration` entries
|
||||
-- use the first `configuration` that matches
|
||||
for _, entry in pairs(configuration_entries) do
|
||||
---@cast entry DapClientConfig
|
||||
if entry.type == type then
|
||||
dap_config = entry
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return dap_config
|
||||
end
|
||||
|
||||
---@return RustaceanExecutor
|
||||
local function get_crate_test_executor()
|
||||
if vim.fn.has('nvim-0.10.0') == 1 then
|
||||
return executors.background
|
||||
else
|
||||
return executors.termopen
|
||||
end
|
||||
end
|
||||
|
||||
---@return RustaceanExecutor
|
||||
local function get_test_executor()
|
||||
if package.loaded['rustaceanvim.neotest'] ~= nil then
|
||||
-- neotest has been set up with rustaceanvim as an adapter
|
||||
return executors.neotest
|
||||
end
|
||||
return get_crate_test_executor()
|
||||
end
|
||||
|
||||
---@class RustaceanConfig
|
||||
local RustaceanDefaultConfig = {
|
||||
---@class RustaceanToolsConfig
|
||||
tools = {
|
||||
|
||||
--- how to execute terminal commands
|
||||
--- options right now: termopen / quickfix / toggleterm / vimux
|
||||
---@type RustaceanExecutor
|
||||
executor = executors.termopen,
|
||||
|
||||
---@type RustaceanExecutor
|
||||
test_executor = get_test_executor(),
|
||||
|
||||
---@type RustaceanExecutor
|
||||
crate_test_executor = get_crate_test_executor(),
|
||||
|
||||
---@type string | nil
|
||||
cargo_override = nil,
|
||||
|
||||
---@type boolean
|
||||
enable_nextest = true,
|
||||
|
||||
---@type boolean
|
||||
enable_clippy = true,
|
||||
|
||||
--- callback to execute once rust-analyzer is done initializing the workspace
|
||||
--- The callback receives one parameter indicating the `health` of the server: "ok" | "warning" | "error"
|
||||
---@type fun(health:RustAnalyzerInitializedStatus) | nil
|
||||
on_initialized = nil,
|
||||
|
||||
--- automatically call RustReloadWorkspace when writing to a Cargo.toml file.
|
||||
---@type boolean
|
||||
reload_workspace_from_cargo_toml = true,
|
||||
|
||||
--- options same as lsp hover
|
||||
---@see vim.lsp.util.open_floating_preview
|
||||
---@class RustaceanHoverActionsConfig
|
||||
hover_actions = {
|
||||
|
||||
--- whether to replace Neovim's built-in `vim.lsp.buf.hover`.
|
||||
---@type boolean
|
||||
replace_builtin_hover = true,
|
||||
},
|
||||
|
||||
code_actions = {
|
||||
--- text appended to a group action
|
||||
---@type string
|
||||
group_icon = ' ▶',
|
||||
|
||||
--- whether to fall back to `vim.ui.select` if there are no grouped code actions
|
||||
---@type boolean
|
||||
ui_select_fallback = false,
|
||||
},
|
||||
|
||||
--- options same as lsp hover
|
||||
---@see vim.lsp.util.open_floating_preview
|
||||
---@see vim.api.nvim_open_win
|
||||
---@type table Options applied to floating windows.
|
||||
float_win_config = {
|
||||
--- whether the window gets automatically focused
|
||||
--- default: false
|
||||
---@type boolean
|
||||
auto_focus = false,
|
||||
|
||||
--- whether splits opened from floating preview are vertical
|
||||
--- default: false
|
||||
---@type 'horizontal' | 'vertical'
|
||||
open_split = 'horizontal',
|
||||
},
|
||||
|
||||
--- settings for showing the crate graph based on graphviz and the dot
|
||||
--- command
|
||||
---@class RustaceanCrateGraphConfig
|
||||
crate_graph = {
|
||||
-- backend used for displaying the graph
|
||||
-- see: https://graphviz.org/docs/outputs/
|
||||
-- default: x11
|
||||
---@type string
|
||||
backend = 'x11',
|
||||
-- where to store the output, nil for no output stored (relative
|
||||
-- path from pwd)
|
||||
-- default: nil
|
||||
---@type string | nil
|
||||
output = nil,
|
||||
-- true for all crates.io and external crates, false only the local
|
||||
-- crates
|
||||
-- default: true
|
||||
---@type boolean
|
||||
full = true,
|
||||
|
||||
-- List of backends found on: https://graphviz.org/docs/outputs/
|
||||
-- Is used for input validation and autocompletion
|
||||
-- Last updated: 2021-08-26
|
||||
---@type string[]
|
||||
enabled_graphviz_backends = {
|
||||
'bmp',
|
||||
'cgimage',
|
||||
'canon',
|
||||
'dot',
|
||||
'gv',
|
||||
'xdot',
|
||||
'xdot1.2',
|
||||
'xdot1.4',
|
||||
'eps',
|
||||
'exr',
|
||||
'fig',
|
||||
'gd',
|
||||
'gd2',
|
||||
'gif',
|
||||
'gtk',
|
||||
'ico',
|
||||
'cmap',
|
||||
'ismap',
|
||||
'imap',
|
||||
'cmapx',
|
||||
'imap_np',
|
||||
'cmapx_np',
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'jpe',
|
||||
'jp2',
|
||||
'json',
|
||||
'json0',
|
||||
'dot_json',
|
||||
'xdot_json',
|
||||
'pdf',
|
||||
'pic',
|
||||
'pct',
|
||||
'pict',
|
||||
'plain',
|
||||
'plain-ext',
|
||||
'png',
|
||||
'pov',
|
||||
'ps',
|
||||
'ps2',
|
||||
'psd',
|
||||
'sgi',
|
||||
'svg',
|
||||
'svgz',
|
||||
'tga',
|
||||
'tiff',
|
||||
'tif',
|
||||
'tk',
|
||||
'vml',
|
||||
'vmlz',
|
||||
'wbmp',
|
||||
'webp',
|
||||
'xlib',
|
||||
'x11',
|
||||
},
|
||||
---@type string | nil
|
||||
pipe = nil,
|
||||
},
|
||||
|
||||
---@type fun(url:string):nil
|
||||
open_url = function(url)
|
||||
require('rustaceanvim.os').open_url(url)
|
||||
end,
|
||||
---settings for rustc
|
||||
---@class RustaceanRustcConfig
|
||||
rustc = {
|
||||
---@type string
|
||||
edition = '2021',
|
||||
},
|
||||
},
|
||||
|
||||
--- all the opts to send to the LSP client
|
||||
--- these override the defaults set by rust-tools.nvim
|
||||
---@diagnostic disable-next-line: undefined-doc-class
|
||||
---@class RustaceanLspClientConfig: vim.lsp.ClientConfig
|
||||
server = {
|
||||
---@type lsp.ClientCapabilities
|
||||
capabilities = server_config.create_client_capabilities(),
|
||||
---@type boolean | fun(bufnr: integer):boolean Whether to automatically attach the LSP client.
|
||||
---Defaults to `true` if the `rust-analyzer` executable is found.
|
||||
auto_attach = function(bufnr)
|
||||
if #vim.bo[bufnr].buftype > 0 then
|
||||
return false
|
||||
end
|
||||
local path = vim.api.nvim_buf_get_name(bufnr)
|
||||
if not os.is_valid_file_path(path) then
|
||||
return false
|
||||
end
|
||||
local cmd = types.evaluate(RustaceanConfig.server.cmd)
|
||||
---@cast cmd string[]
|
||||
local rs_bin = cmd[1]
|
||||
return vim.fn.executable(rs_bin) == 1
|
||||
end,
|
||||
---@type string[] | fun():string[]
|
||||
cmd = function()
|
||||
return { 'rust-analyzer', '--log-file', RustaceanConfig.server.logfile }
|
||||
end,
|
||||
--- standalone file support
|
||||
--- setting it to false may improve startup time
|
||||
---@type boolean
|
||||
standalone = true,
|
||||
|
||||
---@type string The path to the rust-analyzer log file.
|
||||
logfile = vim.fn.tempname() .. '-rust-analyzer.log',
|
||||
|
||||
---@type table | (fun(project_root:string|nil, default_settings: table|nil):table) -- The rust-analyzer settings or a function that creates them.
|
||||
settings = function(project_root, default_settings)
|
||||
return server_config.load_rust_analyzer_settings(project_root, { default_settings = default_settings })
|
||||
end,
|
||||
|
||||
--- @type table
|
||||
default_settings = {
|
||||
--- options to send to rust-analyzer
|
||||
--- See: https://rust-analyzer.github.io/manual.html#configuration
|
||||
--- @type table
|
||||
['rust-analyzer'] = {},
|
||||
},
|
||||
---@type boolean Whether to search (upward from the buffer) for rust-analyzer settings in .vscode/settings json.
|
||||
load_vscode_settings = false,
|
||||
},
|
||||
|
||||
--- debugging stuff
|
||||
--- @class RustaceanDapConfig
|
||||
dap = {
|
||||
--- @type boolean Whether to autoload nvim-dap configurations when rust-analyzer has attached?
|
||||
autoload_configurations = vim.fn.has('nvim-0.10.0') == 1, -- Compiling the debug build cannot be run asynchronously on Neovim < 0.10
|
||||
--- @type DapExecutableConfig | DapServerConfig | disable | fun():(DapExecutableConfig | DapServerConfig | disable)
|
||||
adapter = function()
|
||||
--- @type DapExecutableConfig | DapServerConfig | disable
|
||||
local result = false
|
||||
local has_mason, mason_registry = pcall(require, 'mason-registry')
|
||||
if has_mason and mason_registry.is_installed('codelldb') then
|
||||
local codelldb_package = mason_registry.get_package('codelldb')
|
||||
local mason_codelldb_path = compat.joinpath(codelldb_package:get_install_path(), 'extension')
|
||||
local codelldb_path = compat.joinpath(mason_codelldb_path, 'adapter', 'codelldb')
|
||||
local liblldb_path = compat.joinpath(mason_codelldb_path, 'lldb', 'lib', 'liblldb')
|
||||
local shell = require('rustaceanvim.shell')
|
||||
if shell.is_windows() then
|
||||
codelldb_path = codelldb_path .. '.exe'
|
||||
liblldb_path = compat.joinpath(mason_codelldb_path, 'lldb', 'bin', 'liblldb.dll')
|
||||
else
|
||||
liblldb_path = liblldb_path .. (shell.is_macos() and '.dylib' or '.so')
|
||||
end
|
||||
result = config.get_codelldb_adapter(codelldb_path, liblldb_path)
|
||||
elseif vim.fn.executable('codelldb') == 1 then
|
||||
---@cast result DapServerConfig
|
||||
result = {
|
||||
type = 'server',
|
||||
host = '127.0.0.1',
|
||||
port = '${port}',
|
||||
executable = {
|
||||
command = 'codelldb',
|
||||
args = { '--port', '${port}' },
|
||||
},
|
||||
}
|
||||
else
|
||||
local has_lldb_dap = vim.fn.executable('lldb-dap') == 1
|
||||
local has_lldb_vscode = vim.fn.executable('lldb-vscode') == 1
|
||||
if not has_lldb_dap and not has_lldb_vscode then
|
||||
return result
|
||||
end
|
||||
local command = has_lldb_dap and 'lldb-dap' or 'lldb-vscode'
|
||||
---@cast result DapExecutableConfig
|
||||
result = {
|
||||
type = 'executable',
|
||||
command = command,
|
||||
name = 'lldb',
|
||||
}
|
||||
end
|
||||
return result
|
||||
end,
|
||||
--- Accommodate dynamically-linked targets by passing library paths to lldb.
|
||||
---@type boolean | fun():boolean
|
||||
add_dynamic_library_paths = function()
|
||||
return should_enable_dap_config_value(RustaceanConfig.dap.adapter)
|
||||
end,
|
||||
--- Auto-generate a source map for the standard library.
|
||||
---@type boolean | fun():boolean
|
||||
auto_generate_source_map = function()
|
||||
return should_enable_dap_config_value(RustaceanConfig.dap.adapter)
|
||||
end,
|
||||
--- Get Rust types via initCommands (rustlib/etc/lldb_commands).
|
||||
---@type boolean | fun():boolean
|
||||
load_rust_types = function()
|
||||
if not should_enable_dap_config_value(RustaceanConfig.dap.adapter) then
|
||||
return false
|
||||
end
|
||||
local adapter = types.evaluate(RustaceanConfig.dap.adapter)
|
||||
---@cast adapter DapExecutableConfig | DapServerConfig | disable
|
||||
return adapter ~= false and is_lldb_adapter(adapter)
|
||||
end,
|
||||
--- @type DapClientConfig | disable | fun():(DapClientConfig | disable)
|
||||
configuration = function()
|
||||
local ok, _ = pcall(require, 'dap')
|
||||
if not ok then
|
||||
return false
|
||||
end
|
||||
local adapter = types.evaluate(RustaceanConfig.dap.adapter)
|
||||
---@cast adapter DapExecutableConfig | DapServerConfig | disable
|
||||
if adapter == false then
|
||||
return false
|
||||
end
|
||||
---@cast adapter DapExecutableConfig | DapServerConfig
|
||||
local type = is_codelldb_adapter(adapter) and 'codelldb' or 'lldb'
|
||||
return load_dap_configuration(type)
|
||||
end,
|
||||
},
|
||||
-- debug info
|
||||
was_g_rustaceanvim_sourced = vim.g.rustaceanvim ~= nil,
|
||||
}
|
||||
local rustaceanvim = vim.g.rustaceanvim or {}
|
||||
local opts = type(rustaceanvim) == 'function' and rustaceanvim() or rustaceanvim
|
||||
for _, executor in pairs { 'executor', 'test_executor', 'crate_test_executor' } do
|
||||
if opts.tools and opts.tools[executor] and type(opts.tools[executor]) == 'string' then
|
||||
opts.tools[executor] = assert(executors[opts.tools[executor]], 'Unknown RustaceanExecutor')
|
||||
end
|
||||
end
|
||||
|
||||
---@type RustaceanConfig
|
||||
RustaceanConfig = vim.tbl_deep_extend('force', {}, RustaceanDefaultConfig, opts)
|
||||
|
||||
-- Override user dap.adapter config in a backward compatible way
|
||||
if opts.dap and opts.dap.adapter then
|
||||
local user_adapter = opts.dap.adapter
|
||||
local default_adapter = types.evaluate(RustaceanConfig.dap.adapter)
|
||||
if
|
||||
type(user_adapter) == 'table'
|
||||
and type(default_adapter) == 'table'
|
||||
and user_adapter.type == default_adapter.type
|
||||
then
|
||||
---@diagnostic disable-next-line: inject-field
|
||||
RustaceanConfig.dap.adapter = vim.tbl_deep_extend('force', default_adapter, user_adapter)
|
||||
elseif user_adapter ~= nil then
|
||||
---@diagnostic disable-next-line: inject-field
|
||||
RustaceanConfig.dap.adapter = user_adapter
|
||||
end
|
||||
end
|
||||
|
||||
local check = require('rustaceanvim.config.check')
|
||||
local ok, err = check.validate(RustaceanConfig)
|
||||
if not ok then
|
||||
vim.notify('rustaceanvim: ' .. err, vim.log.levels.ERROR)
|
||||
end
|
||||
|
||||
return RustaceanConfig
|
||||
@ -0,0 +1,50 @@
|
||||
local M = {}
|
||||
|
||||
local function tbl_set(tbl, keys, value)
|
||||
local next = table.remove(keys, 1)
|
||||
if #keys > 0 then
|
||||
tbl[next] = tbl[next] or {}
|
||||
tbl_set(tbl[next], keys, value)
|
||||
else
|
||||
tbl[next] = value
|
||||
end
|
||||
end
|
||||
|
||||
---@param tbl table
|
||||
---@param json_key string e.g. "rust-analyzer.check.overrideCommand"
|
||||
---@param json_value unknown
|
||||
local function override_tbl_values(tbl, json_key, json_value)
|
||||
local keys = vim.split(json_key, '%.')
|
||||
tbl_set(tbl, keys, json_value)
|
||||
end
|
||||
|
||||
---@param json_content string
|
||||
---@return table
|
||||
function M.silent_decode(json_content)
|
||||
local ok, json_tbl = pcall(vim.json.decode, json_content)
|
||||
if not ok or type(json_tbl) ~= 'table' then
|
||||
return {}
|
||||
end
|
||||
return json_tbl
|
||||
end
|
||||
|
||||
---@param tbl table
|
||||
---@param json_tbl { [string]: unknown }
|
||||
---@param key_predicate? fun(string): boolean
|
||||
function M.override_with_json_keys(tbl, json_tbl, key_predicate)
|
||||
for json_key, value in pairs(json_tbl) do
|
||||
if not key_predicate or key_predicate(json_key) then
|
||||
override_tbl_values(tbl, json_key, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param tbl table
|
||||
---@param json_tbl { [string]: unknown }
|
||||
function M.override_with_rust_analyzer_json_keys(tbl, json_tbl)
|
||||
M.override_with_json_keys(tbl, json_tbl, function(key)
|
||||
return vim.startswith(key, 'rust-analyzer')
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,159 @@
|
||||
---@mod rustaceanvim.config.server LSP configuration utility
|
||||
|
||||
local server = {}
|
||||
|
||||
---@class LoadRASettingsOpts
|
||||
---@field settings_file_pattern string|nil File name or pattern to search for. Defaults to 'rust-analyzer.json'
|
||||
---@field default_settings table|nil Default settings to merge the loaded settings into
|
||||
|
||||
--- Load rust-analyzer settings from a JSON file,
|
||||
--- falling back to the default settings if none is found or if it cannot be decoded.
|
||||
---@param project_root string|nil The project root
|
||||
---@param opts LoadRASettingsOpts|nil
|
||||
---@return table server_settings
|
||||
---@see https://rust-analyzer.github.io/manual.html#configuration
|
||||
function server.load_rust_analyzer_settings(project_root, opts)
|
||||
local config = require('rustaceanvim.config.internal')
|
||||
local compat = require('rustaceanvim.compat')
|
||||
local os = require('rustaceanvim.os')
|
||||
|
||||
local default_opts = { settings_file_pattern = 'rust-analyzer.json' }
|
||||
opts = vim.tbl_deep_extend('force', {}, default_opts, opts or {})
|
||||
local default_settings = opts.default_settings or config.server.default_settings
|
||||
local use_clippy = config.tools.enable_clippy and vim.fn.executable('cargo-clippy') == 1
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
if
|
||||
default_settings['rust-analyzer'].check == nil
|
||||
and use_clippy
|
||||
and type(default_settings['rust-analyzer'].checkOnSave) ~= 'table'
|
||||
then
|
||||
---@diagnostic disable-next-line: inject-field
|
||||
default_settings['rust-analyzer'].check = {
|
||||
allFeatures = true,
|
||||
command = 'clippy',
|
||||
extraArgs = { '--no-deps' },
|
||||
}
|
||||
if type(default_settings['rust-analyzer'].checkOnSave) ~= 'boolean' then
|
||||
---@diagnostic disable-next-line: inject-field
|
||||
default_settings['rust-analyzer'].checkOnSave = true
|
||||
end
|
||||
end
|
||||
if not project_root then
|
||||
return default_settings
|
||||
end
|
||||
local results = vim.fn.glob(compat.joinpath(project_root, opts.settings_file_pattern), true, true)
|
||||
if #results == 0 then
|
||||
return default_settings
|
||||
end
|
||||
local config_json = results[1]
|
||||
local content = os.read_file(config_json)
|
||||
if not content then
|
||||
vim.notify('Could not read ' .. config_json, vim.log.levels.WARNING)
|
||||
return default_settings
|
||||
end
|
||||
local json = require('rustaceanvim.config.json')
|
||||
local rust_analyzer_settings = json.silent_decode(content)
|
||||
local ra_key = 'rust-analyzer'
|
||||
local has_ra_key = false
|
||||
for key, _ in pairs(rust_analyzer_settings) do
|
||||
if key:find(ra_key) ~= nil then
|
||||
has_ra_key = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if has_ra_key then
|
||||
-- Settings json with "rust-analyzer" key
|
||||
json.override_with_rust_analyzer_json_keys(default_settings, rust_analyzer_settings)
|
||||
else
|
||||
-- "rust-analyzer" settings are top level
|
||||
json.override_with_json_keys(default_settings, rust_analyzer_settings)
|
||||
end
|
||||
return default_settings
|
||||
end
|
||||
|
||||
---@return lsp.ClientCapabilities
|
||||
local function make_rustaceanvim_capabilities()
|
||||
local capabilities = vim.lsp.protocol.make_client_capabilities()
|
||||
|
||||
if vim.fn.has('nvim-0.10.0') == 1 then
|
||||
-- snippets
|
||||
-- This will also be added if cmp_nvim_lsp is detected.
|
||||
capabilities.textDocument.completion.completionItem.snippetSupport = true
|
||||
end
|
||||
|
||||
-- send actions with hover request
|
||||
capabilities.experimental = {
|
||||
hoverActions = true,
|
||||
hoverRange = true,
|
||||
serverStatusNotification = true,
|
||||
snippetTextEdit = true,
|
||||
codeActionGroup = true,
|
||||
ssr = true,
|
||||
}
|
||||
|
||||
-- enable auto-import
|
||||
capabilities.textDocument.completion.completionItem.resolveSupport = {
|
||||
properties = { 'documentation', 'detail', 'additionalTextEdits' },
|
||||
}
|
||||
|
||||
-- rust analyzer goodies
|
||||
local experimental_commands = {
|
||||
'rust-analyzer.runSingle',
|
||||
'rust-analyzer.showReferences',
|
||||
'rust-analyzer.gotoLocation',
|
||||
'editor.action.triggerParameterHints',
|
||||
}
|
||||
if package.loaded['dap'] ~= nil then
|
||||
table.insert(experimental_commands, 'rust-analyzer.debugSingle')
|
||||
end
|
||||
|
||||
capabilities.experimental.commands = {
|
||||
commands = experimental_commands,
|
||||
}
|
||||
|
||||
return capabilities
|
||||
end
|
||||
|
||||
---@param mod_name string
|
||||
---@param callback fun(mod: table): lsp.ClientCapabilities
|
||||
---@return lsp.ClientCapabilities
|
||||
local function mk_capabilities_if_available(mod_name, callback)
|
||||
local available, mod = pcall(require, mod_name)
|
||||
if available and type(mod) == 'table' then
|
||||
local ok, capabilities = pcall(callback, mod)
|
||||
if ok then
|
||||
return capabilities
|
||||
end
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
---@return lsp.ClientCapabilities
|
||||
function server.create_client_capabilities()
|
||||
local rs_capabilities = make_rustaceanvim_capabilities()
|
||||
local cmp_capabilities = mk_capabilities_if_available('cmp_nvim_lsp', function(cmp_nvim_lsp)
|
||||
return cmp_nvim_lsp.default_capabilities()
|
||||
end)
|
||||
local selection_range_capabilities = mk_capabilities_if_available('lsp-selection-range', function(lsp_selection_range)
|
||||
return lsp_selection_range.update_capabilities {}
|
||||
end)
|
||||
local folding_range_capabilities = mk_capabilities_if_available('ufo', function(_)
|
||||
return {
|
||||
textDocument = {
|
||||
foldingRange = {
|
||||
dynamicRegistration = false,
|
||||
lineFoldingOnly = true,
|
||||
},
|
||||
},
|
||||
}
|
||||
end)
|
||||
return vim.tbl_deep_extend(
|
||||
'keep',
|
||||
rs_capabilities,
|
||||
cmp_capabilities,
|
||||
selection_range_capabilities,
|
||||
folding_range_capabilities
|
||||
)
|
||||
end
|
||||
|
||||
return server
|
||||
@ -0,0 +1,390 @@
|
||||
local config = require('rustaceanvim.config.internal')
|
||||
local compat = require('rustaceanvim.compat')
|
||||
local shell = require('rustaceanvim.shell')
|
||||
local types = require('rustaceanvim.types.internal')
|
||||
|
||||
---@param err string
|
||||
local function scheduled_error(err)
|
||||
vim.schedule(function()
|
||||
vim.notify(err, vim.log.levels.ERROR)
|
||||
end)
|
||||
end
|
||||
|
||||
local ok, _ = pcall(require, 'dap')
|
||||
if not ok then
|
||||
return {
|
||||
---@param on_error fun(err:string)
|
||||
start = function(_, _, _, on_error)
|
||||
on_error = on_error or scheduled_error
|
||||
on_error('nvim-dap not found.')
|
||||
end,
|
||||
}
|
||||
end
|
||||
local dap = require('dap')
|
||||
|
||||
local M = {}
|
||||
|
||||
---@deprecated Use require('rustaceanvim.config').get_codelldb_adapter
|
||||
function M.get_codelldb_adapter(...)
|
||||
vim.deprecate(
|
||||
"require('rustaceanvim.dap').get_codelldb_adapter",
|
||||
"require('rustaceanvim.config').get_codelldb_adapter",
|
||||
'4.0.0',
|
||||
'rustaceanvim'
|
||||
)
|
||||
return require('rustaceanvim.config').get_codelldb_adapter(...)
|
||||
end
|
||||
|
||||
local function get_cargo_args_from_runnables_args(runnable_args)
|
||||
local cargo_args = runnable_args.cargoArgs
|
||||
|
||||
local message_json = '--message-format=json'
|
||||
if not compat.list_contains(cargo_args, message_json) then
|
||||
table.insert(cargo_args, message_json)
|
||||
end
|
||||
|
||||
for _, value in ipairs(runnable_args.cargoExtraArgs) do
|
||||
if not compat.list_contains(cargo_args, value) then
|
||||
table.insert(cargo_args, value)
|
||||
end
|
||||
end
|
||||
|
||||
return cargo_args
|
||||
end
|
||||
|
||||
---@param callback fun(commit_hash:string)
|
||||
local function get_rustc_commit_hash(callback)
|
||||
compat.system({ 'rustc', '--version', '--verbose' }, nil, function(sc)
|
||||
---@cast sc vim.SystemCompleted
|
||||
local result = sc.stdout
|
||||
if sc.code ~= 0 or result == nil then
|
||||
return
|
||||
end
|
||||
local commit_hash = result:match('commit%-hash:%s+([^\n]+)')
|
||||
if not commit_hash then
|
||||
return
|
||||
end
|
||||
callback(commit_hash)
|
||||
end)
|
||||
end
|
||||
|
||||
local function get_rustc_sysroot(callback)
|
||||
compat.system({ 'rustc', '--print', 'sysroot' }, nil, function(sc)
|
||||
---@cast sc vim.SystemCompleted
|
||||
local result = sc.stdout
|
||||
if sc.code ~= 0 or result == nil then
|
||||
return
|
||||
end
|
||||
callback((result:gsub('\n$', '')))
|
||||
end)
|
||||
end
|
||||
|
||||
---@alias DapSourceMap {[string]: string}
|
||||
|
||||
---@param tbl { [string]: string }
|
||||
---@return string[][]
|
||||
local function tbl_to_tuple_list(tbl)
|
||||
---@type string[][]
|
||||
local result = {}
|
||||
for k, v in pairs(tbl) do
|
||||
---@type string[]
|
||||
local tuple = { k, v }
|
||||
table.insert(result, tuple)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---codelldb expects a map,
|
||||
-- while lldb expects a list of tuples.
|
||||
---@param adapter DapExecutableConfig | DapServerConfig | boolean
|
||||
---@param tbl { [string]: string }
|
||||
---@return string[][] | { [string]: string }
|
||||
local function format_source_map(adapter, tbl)
|
||||
if adapter.type == 'server' then
|
||||
return tbl
|
||||
end
|
||||
return tbl_to_tuple_list(tbl)
|
||||
end
|
||||
|
||||
---@type {[string]: DapSourceMap}
|
||||
local source_maps = {}
|
||||
|
||||
---See https://github.com/vadimcn/codelldb/issues/204
|
||||
---@param workspace_root? string
|
||||
local function generate_source_map(workspace_root)
|
||||
if not workspace_root or source_maps[workspace_root] then
|
||||
return
|
||||
end
|
||||
get_rustc_commit_hash(function(commit_hash)
|
||||
get_rustc_sysroot(function(rustc_sysroot)
|
||||
local src_path
|
||||
for _, src_dir in pairs { 'src', 'rustc-src' } do
|
||||
src_path = compat.joinpath(rustc_sysroot, 'lib', 'rustlib', src_dir, 'rust')
|
||||
if compat.uv.fs_stat(src_path) then
|
||||
break
|
||||
end
|
||||
src_path = nil
|
||||
end
|
||||
if not src_path then
|
||||
return
|
||||
end
|
||||
---@type DapSourceMap
|
||||
source_maps[workspace_root] = {
|
||||
[compat.joinpath('/rustc', commit_hash)] = src_path,
|
||||
}
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
---@type {[string]: string[]}
|
||||
local init_commands = {}
|
||||
|
||||
---@param workspace_root? string
|
||||
local function get_lldb_commands(workspace_root)
|
||||
if not workspace_root or init_commands[workspace_root] then
|
||||
return
|
||||
end
|
||||
get_rustc_sysroot(function(rustc_sysroot)
|
||||
local script = compat.joinpath(rustc_sysroot, 'lib', 'rustlib', 'etc', 'lldb_lookup.py')
|
||||
if not compat.uv.fs_stat(script) then
|
||||
return
|
||||
end
|
||||
local script_import = 'command script import "' .. script .. '"'
|
||||
local commands_file = compat.joinpath(rustc_sysroot, 'lib', 'rustlib', 'etc', 'lldb_commands')
|
||||
local file = io.open(commands_file, 'r')
|
||||
local workspace_root_cmds = {}
|
||||
if file then
|
||||
for line in file:lines() do
|
||||
table.insert(workspace_root_cmds, line)
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
table.insert(workspace_root_cmds, 1, script_import)
|
||||
init_commands[workspace_root] = workspace_root_cmds
|
||||
end)
|
||||
end
|
||||
|
||||
---map for codelldb, list of strings for lldb-dap
|
||||
---@param adapter DapExecutableConfig | DapServerConfig
|
||||
---@param key string
|
||||
---@param segments string[]
|
||||
---@param sep string
|
||||
---@return {[string]: string} | string[]
|
||||
local function format_environment_variable(adapter, key, segments, sep)
|
||||
---@diagnostic disable-next-line: missing-parameter
|
||||
local existing = compat.uv.os_getenv(key)
|
||||
existing = existing and sep .. existing or ''
|
||||
local value = table.concat(segments, sep) .. existing
|
||||
return adapter.type == 'server' and { [key] = value } or { key .. '=' .. value }
|
||||
end
|
||||
|
||||
---@type {[string]: EnvironmentMap}
|
||||
local environments = {}
|
||||
|
||||
-- Most succinct description: https://github.com/bevyengine/bevy/issues/2589#issuecomment-1753413600
|
||||
---@param adapter DapExecutableConfig | DapServerConfig
|
||||
---@param workspace_root string | nil
|
||||
local function add_dynamic_library_paths(adapter, workspace_root)
|
||||
if not workspace_root or environments[workspace_root] then
|
||||
return
|
||||
end
|
||||
compat.system({ 'rustc', '--print', 'target-libdir' }, nil, function(sc)
|
||||
---@cast sc vim.SystemCompleted
|
||||
local result = sc.stdout
|
||||
if sc.code ~= 0 or result == nil then
|
||||
return
|
||||
end
|
||||
local rustc_target_path = (result:gsub('\n$', ''))
|
||||
local target_path = compat.joinpath(workspace_root, 'target', 'debug', 'deps')
|
||||
if shell.is_windows() then
|
||||
environments[workspace_root] = environments[workspace_root]
|
||||
or format_environment_variable(adapter, 'PATH', { rustc_target_path, target_path }, ';')
|
||||
elseif shell.is_macos() then
|
||||
---@diagnostic disable-next-line: missing-parameter
|
||||
environments[workspace_root] = environments[workspace_root]
|
||||
or format_environment_variable(adapter, 'DKLD_LIBRARY_PATH', { rustc_target_path, target_path }, ':')
|
||||
else
|
||||
---@diagnostic disable-next-line: missing-parameter
|
||||
environments[workspace_root] = environments[workspace_root]
|
||||
or format_environment_variable(adapter, 'LD_LIBRARY_PATH', { rustc_target_path, target_path }, ':')
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---@param action fun() Action to perform
|
||||
---@param desc? string Description of the action or nil to suppress warning
|
||||
local function pall_with_warn(action, desc)
|
||||
local success, err = pcall(action)
|
||||
if not success and desc then
|
||||
vim.schedule(function()
|
||||
vim.notify(desc .. ' failed: ' .. err, vim.log.levels.WARN)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---@param adapter DapExecutableConfig | DapServerConfig
|
||||
---@param args RARunnableArgs
|
||||
---@param verbose? boolean
|
||||
local function handle_configured_options(adapter, args, verbose)
|
||||
local is_generate_source_map_enabled = types.evaluate(config.dap.auto_generate_source_map)
|
||||
---@cast is_generate_source_map_enabled boolean
|
||||
if is_generate_source_map_enabled then
|
||||
pall_with_warn(function()
|
||||
generate_source_map(args.workspaceRoot)
|
||||
end, verbose and 'Generating source map' or nil)
|
||||
end
|
||||
|
||||
local is_load_rust_types_enabled = types.evaluate(config.dap.load_rust_types)
|
||||
---@cast is_load_rust_types_enabled boolean
|
||||
if is_load_rust_types_enabled then
|
||||
pall_with_warn(function()
|
||||
get_lldb_commands(args.workspaceRoot)
|
||||
end, verbose and 'Getting LLDB commands' or nil)
|
||||
end
|
||||
|
||||
local is_add_dynamic_library_paths_enabled = types.evaluate(config.dap.add_dynamic_library_paths)
|
||||
---@cast is_add_dynamic_library_paths_enabled boolean
|
||||
if is_add_dynamic_library_paths_enabled then
|
||||
pall_with_warn(function()
|
||||
add_dynamic_library_paths(adapter, args.workspaceRoot)
|
||||
end, verbose and 'Adding library paths' or nil)
|
||||
end
|
||||
end
|
||||
|
||||
---@param args RARunnableArgs
|
||||
---@param verbose? boolean
|
||||
---@param callback? fun(config: DapClientConfig)
|
||||
---@param on_error? fun(err: string)
|
||||
function M.start(args, verbose, callback, on_error)
|
||||
if verbose then
|
||||
on_error = on_error or scheduled_error
|
||||
else
|
||||
on_error = on_error or function() end
|
||||
end
|
||||
if type(callback) ~= 'function' then
|
||||
callback = dap.run
|
||||
end
|
||||
local adapter = types.evaluate(config.dap.adapter)
|
||||
--- @cast adapter DapExecutableConfig | DapServerConfig | disable
|
||||
if adapter == false then
|
||||
on_error('Debug adapter is disabled.')
|
||||
return
|
||||
end
|
||||
|
||||
handle_configured_options(adapter, args, verbose)
|
||||
|
||||
local cargo_args = get_cargo_args_from_runnables_args(args)
|
||||
local cmd = vim.list_extend({ config.tools.cargo_override or 'cargo' }, cargo_args)
|
||||
if verbose then
|
||||
vim.notify('Compiling a debug build for debugging. This might take some time...')
|
||||
end
|
||||
compat.system(cmd, { cwd = args.workspaceRoot }, function(sc)
|
||||
---@cast sc vim.SystemCompleted
|
||||
local output = sc.stdout
|
||||
if sc.code ~= 0 or output == nil then
|
||||
on_error(
|
||||
'An error occurred while compiling. Please fix all compilation issues and try again.'
|
||||
.. '\nCommand: '
|
||||
.. table.concat(cmd, ' ')
|
||||
.. (sc.stderr and '\nstderr: \n' .. sc.stderr or '')
|
||||
.. (output and '\nstdout: ' .. output or '')
|
||||
)
|
||||
return
|
||||
end
|
||||
vim.schedule(function()
|
||||
local executables = {}
|
||||
for value in output:gmatch('([^\n]*)\n?') do
|
||||
local is_json, artifact = pcall(vim.fn.json_decode, value)
|
||||
if not is_json then
|
||||
goto loop_end
|
||||
end
|
||||
|
||||
-- only process artifact if it's valid json object and it is a compiler artifact
|
||||
if type(artifact) ~= 'table' or artifact.reason ~= 'compiler-artifact' then
|
||||
goto loop_end
|
||||
end
|
||||
|
||||
local is_binary = compat.list_contains(artifact.target.crate_types, 'bin')
|
||||
local is_build_script = compat.list_contains(artifact.target.kind, 'custom-build')
|
||||
local is_test = ((artifact.profile.test == true) and (artifact.executable ~= nil))
|
||||
or compat.list_contains(artifact.target.kind, 'test')
|
||||
-- only add executable to the list if we want a binary debug and it is a binary
|
||||
-- or if we want a test debug and it is a test
|
||||
if
|
||||
(cargo_args[1] == 'build' and is_binary and not is_build_script)
|
||||
or (cargo_args[1] == 'test' and is_test)
|
||||
then
|
||||
table.insert(executables, artifact.executable)
|
||||
end
|
||||
|
||||
::loop_end::
|
||||
end
|
||||
-- only 1 executable is allowed for debugging - error out if zero or many were found
|
||||
if #executables <= 0 then
|
||||
on_error('No compilation artifacts found.')
|
||||
return
|
||||
end
|
||||
if #executables > 1 then
|
||||
on_error('Multiple compilation artifacts are not supported.')
|
||||
return
|
||||
end
|
||||
|
||||
-- If the adapter is not defined elsewhere, use the adapter
|
||||
-- defined in `config.dap.adapter`
|
||||
local is_codelldb = adapter.type == 'server'
|
||||
local adapter_key = is_codelldb and 'codelldb' or 'lldb'
|
||||
if dap.adapters[adapter_key] == nil then
|
||||
---@TODO: Add nvim-dap to lua-ls lint
|
||||
---@diagnostic disable-next-line: assign-type-mismatch
|
||||
dap.adapters[adapter_key] = adapter
|
||||
end
|
||||
|
||||
-- Use the first configuration, if it exists
|
||||
local _, dap_config = next(dap.configurations.rust or {})
|
||||
|
||||
local local_config = types.evaluate(config.dap.configuration)
|
||||
--- @cast local_config DapClientConfig | boolean
|
||||
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
local final_config = local_config ~= false and vim.deepcopy(local_config) or vim.deepcopy(dap_config)
|
||||
--- @cast final_config DapClientConfig
|
||||
|
||||
if dap.adapters[final_config.type] == nil then
|
||||
on_error('No adapter exists named "' .. final_config.type .. '". See ":h dap-adapter" for more information')
|
||||
return
|
||||
end
|
||||
|
||||
-- common entries
|
||||
-- `program` and `args` aren't supported in probe-rs but are safely ignored
|
||||
final_config.cwd = args.workspaceRoot
|
||||
final_config.program = executables[1]
|
||||
final_config.args = args.executableArgs or {}
|
||||
local environment = args.workspaceRoot and environments[args.workspaceRoot]
|
||||
final_config = next(environment or {}) ~= nil
|
||||
and vim.tbl_deep_extend('force', final_config, { env = environment })
|
||||
or final_config
|
||||
|
||||
if string.find(final_config.type, 'lldb') ~= nil then
|
||||
-- lldb specific entries
|
||||
final_config = args.workspaceRoot
|
||||
and next(init_commands or {}) ~= nil
|
||||
and vim.tbl_deep_extend('force', final_config, { initCommands = init_commands[args.workspaceRoot] })
|
||||
or final_config
|
||||
|
||||
local source_map = args.workspaceRoot and source_maps[args.workspaceRoot]
|
||||
final_config = source_map
|
||||
and next(source_map or {}) ~= nil
|
||||
and vim.tbl_deep_extend('force', final_config, { sourceMap = format_source_map(adapter, source_map) })
|
||||
or final_config
|
||||
elseif string.find(final_config.type, 'probe%-rs') ~= nil then
|
||||
-- probe-rs specific entries
|
||||
final_config.coreConfigs[1].programBinary = final_config.program
|
||||
end
|
||||
|
||||
-- start debugging
|
||||
callback(final_config)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,52 @@
|
||||
local diag_namespace = vim.api.nvim_create_namespace('rustaceanvim')
|
||||
|
||||
---@param output string
|
||||
---@return string | nil
|
||||
local function get_test_summary(output)
|
||||
return output:match('(test result:.*)')
|
||||
end
|
||||
|
||||
---@type RustaceanExecutor
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
local M = {}
|
||||
|
||||
---@class rustaceanvim.Diagnostic: vim.Diagnostic
|
||||
---@field test_id string
|
||||
|
||||
M.execute_command = function(command, args, cwd, opts)
|
||||
---@type RustaceanExecutorOpts
|
||||
opts = vim.tbl_deep_extend('force', { bufnr = 0 }, opts or {})
|
||||
if vim.fn.has('nvim-0.10.0') ~= 1 then
|
||||
vim.schedule(function()
|
||||
vim.notify_once("the 'background' executor is not recommended for Neovim < 0.10.", vim.log.levels.WARN)
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
vim.diagnostic.reset(diag_namespace, opts.bufnr)
|
||||
local is_single_test = args[1] == 'test'
|
||||
local notify_prefix = (is_single_test and 'test ' or 'tests ')
|
||||
local compat = require('rustaceanvim.compat')
|
||||
local cmd = vim.list_extend({ command }, args)
|
||||
local fname = vim.api.nvim_buf_get_name(opts.bufnr)
|
||||
compat.system(cmd, { cwd = cwd }, function(sc)
|
||||
---@cast sc vim.SystemCompleted
|
||||
if sc.code == 0 then
|
||||
local summary = get_test_summary(sc.stdout or '')
|
||||
vim.schedule(function()
|
||||
vim.notify(summary and summary or (notify_prefix .. 'passed!'), vim.log.levels.INFO)
|
||||
end)
|
||||
return
|
||||
end
|
||||
local output = (sc.stderr or '') .. '\n' .. (sc.stdout or '')
|
||||
local diagnostics = require('rustaceanvim.test').parse_diagnostics(fname, output)
|
||||
local summary = get_test_summary(sc.stdout or '')
|
||||
vim.schedule(function()
|
||||
vim.diagnostic.set(diag_namespace, opts.bufnr, diagnostics)
|
||||
vim.cmd.redraw()
|
||||
vim.notify(summary and summary or (notify_prefix .. 'failed!'), vim.log.levels.ERROR)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,20 @@
|
||||
---@mod rustaceanvim.executors
|
||||
|
||||
local termopen = require('rustaceanvim.executors.termopen')
|
||||
local quickfix = require('rustaceanvim.executors.quickfix')
|
||||
local toggleterm = require('rustaceanvim.executors.toggleterm')
|
||||
local vimux = require('rustaceanvim.executors.vimux')
|
||||
local background = require('rustaceanvim.executors.background')
|
||||
local neotest = require('rustaceanvim.executors.neotest')
|
||||
|
||||
---@type { [test_executor_alias]: RustaceanExecutor }
|
||||
local M = {}
|
||||
|
||||
M.termopen = termopen
|
||||
M.quickfix = quickfix
|
||||
M.toggleterm = toggleterm
|
||||
M.vimux = vimux
|
||||
M.background = background
|
||||
M.neotest = neotest
|
||||
|
||||
return M
|
||||
@ -0,0 +1,7 @@
|
||||
error('Cannot import a meta module')
|
||||
|
||||
---@class RustaceanTestExecutor: RustaceanExecutor
|
||||
---@field execute_command fun(cmd:string, args:string[], cwd:string|nil, opts?: RustaceanExecutorOpts)
|
||||
|
||||
---@class RustaceanTestExecutorOpts: RustaceanExecutorOpts
|
||||
---@field runnable? RARunnable
|
||||
@ -0,0 +1,20 @@
|
||||
local trans = require('rustaceanvim.neotest.trans')
|
||||
|
||||
---@type RustaceanTestExecutor
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
local M = {}
|
||||
|
||||
---@param opts RustaceanTestExecutorOpts
|
||||
M.execute_command = function(_, _, _, opts)
|
||||
---@type RustaceanTestExecutorOpts
|
||||
opts = vim.tbl_deep_extend('force', { bufnr = 0 }, opts or {})
|
||||
if type(opts.runnable) ~= 'table' then
|
||||
vim.notify('rustaceanvim neotest executor called without a runnable. This is a bug!', vim.log.levels.ERROR)
|
||||
end
|
||||
local file = vim.api.nvim_buf_get_name(opts.bufnr)
|
||||
local pos_id = trans.get_position_id(file, opts.runnable)
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
require('neotest').run.run(pos_id)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,46 @@
|
||||
local compat = require('rustaceanvim.compat')
|
||||
|
||||
local function clear_qf()
|
||||
vim.fn.setqflist({}, ' ', { title = 'cargo' })
|
||||
end
|
||||
|
||||
local function scroll_qf()
|
||||
if vim.bo.buftype ~= 'quickfix' then
|
||||
vim.api.nvim_command('cbottom')
|
||||
end
|
||||
end
|
||||
|
||||
local function append_qf(line)
|
||||
vim.fn.setqflist({}, 'a', { lines = { line } })
|
||||
scroll_qf()
|
||||
end
|
||||
|
||||
local function copen()
|
||||
vim.cmd('copen')
|
||||
end
|
||||
|
||||
---@type RustaceanExecutor
|
||||
local M = {
|
||||
execute_command = function(command, args, cwd, _)
|
||||
-- open quickfix
|
||||
copen()
|
||||
-- go back to the previous window
|
||||
vim.cmd.wincmd('p')
|
||||
-- clear the quickfix
|
||||
clear_qf()
|
||||
|
||||
-- start compiling
|
||||
local cmd = vim.list_extend({ command }, args)
|
||||
compat.system(
|
||||
cmd,
|
||||
cwd and { cwd = cwd } or {},
|
||||
vim.schedule_wrap(function(sc)
|
||||
---@cast sc vim.SystemCompleted
|
||||
local data = sc.stdout or sc.stderr
|
||||
append_qf(data)
|
||||
end)
|
||||
)
|
||||
end,
|
||||
}
|
||||
|
||||
return M
|
||||
@ -0,0 +1,44 @@
|
||||
---@type integer | nil
|
||||
local latest_buf_id = nil
|
||||
|
||||
---@type RustaceanExecutor
|
||||
local M = {
|
||||
execute_command = function(command, args, cwd, _)
|
||||
local shell = require('rustaceanvim.shell')
|
||||
local ui = require('rustaceanvim.ui')
|
||||
local commands = {}
|
||||
if cwd then
|
||||
table.insert(commands, shell.make_cd_command(cwd))
|
||||
end
|
||||
table.insert(commands, shell.make_command_from_args(command, args))
|
||||
local full_command = shell.chain_commands(commands)
|
||||
|
||||
-- check if a buffer with the latest id is already open, if it is then
|
||||
-- delete it and continue
|
||||
ui.delete_buf(latest_buf_id)
|
||||
|
||||
-- create the new buffer
|
||||
latest_buf_id = vim.api.nvim_create_buf(false, true)
|
||||
|
||||
-- split the window to create a new buffer and set it to our window
|
||||
ui.split(false, latest_buf_id)
|
||||
|
||||
-- make the new buffer smaller
|
||||
ui.resize(false, '-5')
|
||||
|
||||
-- close the buffer when escape is pressed :)
|
||||
vim.keymap.set('n', '<Esc>', '<CMD>q<CR>', { buffer = latest_buf_id, noremap = true })
|
||||
|
||||
-- run the command
|
||||
vim.fn.termopen(full_command)
|
||||
|
||||
-- when the buffer is closed, set the latest buf id to nil else there are
|
||||
-- some edge cases with the id being sit but a buffer not being open
|
||||
local function onDetach(_, _)
|
||||
latest_buf_id = nil
|
||||
end
|
||||
vim.api.nvim_buf_attach(latest_buf_id, false, { on_detach = onDetach })
|
||||
end,
|
||||
}
|
||||
|
||||
return M
|
||||
@ -0,0 +1,30 @@
|
||||
---@type RustaceanExecutor
|
||||
local M = {
|
||||
execute_command = function(command, args, cwd, _)
|
||||
local ok, term = pcall(require, 'toggleterm.terminal')
|
||||
if not ok then
|
||||
vim.schedule(function()
|
||||
vim.notify('toggleterm not found.', vim.log.levels.ERROR)
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
local shell = require('rustaceanvim.shell')
|
||||
term.Terminal
|
||||
:new({
|
||||
dir = cwd,
|
||||
cmd = shell.make_command_from_args(command, args),
|
||||
close_on_exit = false,
|
||||
on_open = function(t)
|
||||
-- enter normal mode
|
||||
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes([[<C-\><C-n>]], true, true, true), '', true)
|
||||
|
||||
-- set close keymap
|
||||
vim.keymap.set('n', 'q', '<CMD>close<CR>', { buffer = t.bufnr, noremap = true })
|
||||
end,
|
||||
})
|
||||
:toggle()
|
||||
end,
|
||||
}
|
||||
|
||||
return M
|
||||
@ -0,0 +1,16 @@
|
||||
local shell = require('rustaceanvim.shell')
|
||||
|
||||
---@type RustaceanExecutor
|
||||
local M = {
|
||||
execute_command = function(command, args, cwd, _)
|
||||
local commands = {}
|
||||
if cwd then
|
||||
table.insert(commands, shell.make_cd_command(cwd))
|
||||
end
|
||||
table.insert(commands, shell.make_command_from_args(command, args))
|
||||
local full_command = shell.chain_commands(commands)
|
||||
vim.fn.VimuxRunCommand(full_command)
|
||||
end,
|
||||
}
|
||||
|
||||
return M
|
||||
@ -0,0 +1,281 @@
|
||||
---@mod rustaceanvim.health Health checks
|
||||
|
||||
local health = {}
|
||||
|
||||
local h = vim.health or require('health')
|
||||
---@diagnostic disable-next-line: deprecated
|
||||
local start = h.start or h.report_start
|
||||
---@diagnostic disable-next-line: deprecated
|
||||
local ok = h.ok or h.report_ok
|
||||
---@diagnostic disable-next-line: deprecated
|
||||
local error = h.error or h.report_error
|
||||
---@diagnostic disable-next-line: deprecated
|
||||
local warn = h.warn or h.report_warn
|
||||
|
||||
---@class LuaDependency
|
||||
---@field module string The name of a module
|
||||
---@field optional fun():boolean Function that returns whether the dependency is optional
|
||||
---@field url string URL (markdown)
|
||||
---@field info string Additional information
|
||||
|
||||
---@type LuaDependency[]
|
||||
local lua_dependencies = {
|
||||
{
|
||||
module = 'dap',
|
||||
optional = function()
|
||||
return true
|
||||
end,
|
||||
url = '[mfussenegger/nvim-dap](https://github.com/mfussenegger/nvim-dap)',
|
||||
info = 'Needed for debugging features',
|
||||
},
|
||||
}
|
||||
|
||||
---@class ExternalDependency
|
||||
---@field name string Name of the dependency
|
||||
---@field get_binaries fun():string[] Function that returns the binaries to check for
|
||||
---@field is_installed? fun(bin: string):boolean Default: `vim.fn.executable(bin) == 1`
|
||||
---@field optional fun():boolean Function that returns whether the dependency is optional
|
||||
---@field url string URL (markdown)
|
||||
---@field info string Additional information
|
||||
---@field extra_checks_if_installed? fun(bin: string) Optional extra checks to perform if the dependency is installed
|
||||
---@field extra_checks_if_not_installed? fun() Optional extra checks to perform if the dependency is not installed
|
||||
|
||||
---@param dep LuaDependency
|
||||
local function check_lua_dependency(dep)
|
||||
if pcall(require, dep.module) then
|
||||
ok(dep.url .. ' installed.')
|
||||
return
|
||||
end
|
||||
if dep.optional() then
|
||||
warn(('%s not installed. %s %s'):format(dep.module, dep.info, dep.url))
|
||||
else
|
||||
error(('Lua dependency %s not found: %s'):format(dep.module, dep.url))
|
||||
end
|
||||
end
|
||||
|
||||
---@param dep ExternalDependency
|
||||
---@return boolean is_installed
|
||||
---@return string binary
|
||||
---@return string version
|
||||
local check_installed = function(dep)
|
||||
local binaries = dep.get_binaries()
|
||||
for _, binary in ipairs(binaries) do
|
||||
local is_executable = dep.is_installed or function(bin)
|
||||
return vim.fn.executable(bin) == 1
|
||||
end
|
||||
if is_executable(binary) then
|
||||
local handle = io.popen(binary .. ' --version')
|
||||
if handle then
|
||||
local binary_version, error_msg = handle:read('*a')
|
||||
handle:close()
|
||||
if error_msg then
|
||||
return false, binary, error_msg
|
||||
end
|
||||
return true, binary, binary_version
|
||||
end
|
||||
return false, binary, 'Unable to determine version.'
|
||||
end
|
||||
end
|
||||
return false, binaries[1], 'Could not find an executable binary.'
|
||||
end
|
||||
|
||||
---@param dep ExternalDependency
|
||||
local function check_external_dependency(dep)
|
||||
local is_installed, binary, version_or_err = check_installed(dep)
|
||||
if is_installed then
|
||||
---@cast binary string
|
||||
local mb_version_newline_idx = version_or_err and version_or_err:find('\n')
|
||||
local mb_version_len = version_or_err
|
||||
and (mb_version_newline_idx and mb_version_newline_idx - 1 or version_or_err:len())
|
||||
version_or_err = version_or_err and version_or_err:sub(0, mb_version_len) or '(unknown version)'
|
||||
ok(('%s: found %s'):format(dep.name, version_or_err))
|
||||
if dep.extra_checks_if_installed then
|
||||
dep.extra_checks_if_installed(binary)
|
||||
end
|
||||
return
|
||||
end
|
||||
if dep.optional() then
|
||||
warn(([[
|
||||
%s: not found.
|
||||
Install %s for extended capabilities.
|
||||
%s
|
||||
]]):format(dep.name, dep.url, dep.info))
|
||||
else
|
||||
error(([[
|
||||
%s: not found: %s
|
||||
rustaceanvim requires %s.
|
||||
%s
|
||||
]]):format(dep.name, version_or_err, dep.url, dep.info))
|
||||
end
|
||||
if dep.extra_checks_if_not_installed then
|
||||
dep.extra_checks_if_not_installed()
|
||||
end
|
||||
end
|
||||
|
||||
---@param config RustaceanConfig
|
||||
local function check_config(config)
|
||||
start('Checking config')
|
||||
if vim.g.rustaceanvim and not config.was_g_rustaceanvim_sourced then
|
||||
error('vim.g.rustaceanvim is set, but it was sourced after rustaceanvim was initialized.')
|
||||
end
|
||||
local valid, err = require('rustaceanvim.config.check').validate(config)
|
||||
if valid then
|
||||
ok('No errors found in config.')
|
||||
else
|
||||
error(err or '' .. vim.g.rustaceanvim and '' or ' This looks like a plugin bug!')
|
||||
end
|
||||
end
|
||||
|
||||
local function check_for_conflicts()
|
||||
start('Checking for conflicting plugins')
|
||||
require('rustaceanvim.config.check').check_for_lspconfig_conflict(error)
|
||||
if package.loaded['rustaceanvim.neotest'] ~= nil and package.loaded['neotest-rust'] ~= nil then
|
||||
error('rustaceanvim.neotest and neotest-rust are both loaded. This is likely a conflict.')
|
||||
return
|
||||
end
|
||||
ok('No conflicting plugins detected.')
|
||||
end
|
||||
|
||||
local function check_tree_sitter()
|
||||
start('Checking for tree-sitter parser')
|
||||
local has_tree_sitter_rust_parser = #vim.api.nvim_get_runtime_file('parser/rust.so', true) > 0
|
||||
if has_tree_sitter_rust_parser then
|
||||
ok('tree-sitter parser for Rust detected.')
|
||||
else
|
||||
warn("No tree-sitter parser for Rust detected. Required by 'Rustc unpretty' command.")
|
||||
end
|
||||
end
|
||||
|
||||
function health.check()
|
||||
local types = require('rustaceanvim.types.internal')
|
||||
local config = require('rustaceanvim.config.internal')
|
||||
|
||||
start('Checking for Lua dependencies')
|
||||
for _, dep in ipairs(lua_dependencies) do
|
||||
check_lua_dependency(dep)
|
||||
end
|
||||
|
||||
start('Checking external dependencies')
|
||||
|
||||
local adapter = types.evaluate(config.dap.adapter)
|
||||
---@cast adapter DapExecutableConfig | DapServerConfig | boolean
|
||||
|
||||
---@return string
|
||||
local function get_rust_analyzer_binary()
|
||||
local default = 'rust-analyzer'
|
||||
if not config then
|
||||
return default
|
||||
end
|
||||
local cmd = types.evaluate(config.server.cmd)
|
||||
if not cmd or #cmd == 0 then
|
||||
return default
|
||||
end
|
||||
return cmd[1]
|
||||
end
|
||||
|
||||
---@type ExternalDependency[]
|
||||
local external_dependencies = {
|
||||
{
|
||||
name = 'rust-analyzer',
|
||||
get_binaries = function()
|
||||
return { get_rust_analyzer_binary() }
|
||||
end,
|
||||
is_installed = function(bin)
|
||||
if type(vim.system) == 'function' then
|
||||
local success = pcall(function()
|
||||
vim.system { bin, '--version' }
|
||||
end)
|
||||
return success
|
||||
end
|
||||
return vim.fn.executable(bin) == 1
|
||||
end,
|
||||
optional = function()
|
||||
return false
|
||||
end,
|
||||
url = '[rust-analyzer](https://rust-analyzer.github.io/)',
|
||||
info = 'Required by the LSP client.',
|
||||
extra_checks_if_not_installed = function()
|
||||
local bin = get_rust_analyzer_binary()
|
||||
if vim.fn.executable(bin) == 1 then
|
||||
warn("rust-analyzer wrapper detected. Run 'rustup component add rust-analyzer' to install rust-analyzer.")
|
||||
end
|
||||
end,
|
||||
},
|
||||
{
|
||||
name = 'Cargo',
|
||||
get_binaries = function()
|
||||
return { 'cargo' }
|
||||
end,
|
||||
optional = function()
|
||||
return true
|
||||
end,
|
||||
url = '[Cargo](https://doc.rust-lang.org/cargo/)',
|
||||
info = [[
|
||||
The Rust package manager.
|
||||
Required by rust-analyzer for non-standalone files, and for debugging features.
|
||||
Not required in standalone files.
|
||||
]],
|
||||
},
|
||||
{
|
||||
name = 'rustc',
|
||||
get_binaries = function()
|
||||
return { 'rustc' }
|
||||
end,
|
||||
optional = function()
|
||||
return true
|
||||
end,
|
||||
url = '[rustc](https://doc.rust-lang.org/rustc/what-is-rustc.html)',
|
||||
info = [[
|
||||
The Rust compiler.
|
||||
Called by `:RustLsp explainError`.
|
||||
]],
|
||||
},
|
||||
}
|
||||
|
||||
if config.tools.cargo_override then
|
||||
table.insert(external_dependencies, {
|
||||
name = 'Cargo override: ' .. config.tools.cargo_override,
|
||||
get_binaries = function()
|
||||
return { config.tools.cargo_override }
|
||||
end,
|
||||
optional = function()
|
||||
return true
|
||||
end,
|
||||
url = '',
|
||||
info = [[
|
||||
Set in the config to override the 'cargo' command for debugging and testing.
|
||||
]],
|
||||
})
|
||||
end
|
||||
|
||||
if adapter ~= false then
|
||||
table.insert(external_dependencies, {
|
||||
name = adapter.name or 'debug adapter',
|
||||
get_binaries = function()
|
||||
if adapter.type == 'executable' then
|
||||
---@cast adapter DapExecutableConfig
|
||||
return { 'lldb', adapter.command }
|
||||
else
|
||||
---@cast adapter DapServerConfig
|
||||
return { 'codelldb', adapter.executable.command }
|
||||
end
|
||||
end,
|
||||
optional = function()
|
||||
return true
|
||||
end,
|
||||
url = '[lldb](https://lldb.llvm.org/)',
|
||||
info = [[
|
||||
A debug adapter (defaults to: LLDB).
|
||||
Required for debugging features.
|
||||
]],
|
||||
})
|
||||
end
|
||||
for _, dep in ipairs(external_dependencies) do
|
||||
check_external_dependency(dep)
|
||||
end
|
||||
check_config(config)
|
||||
check_for_conflicts()
|
||||
check_tree_sitter()
|
||||
end
|
||||
|
||||
return health
|
||||
@ -0,0 +1,146 @@
|
||||
local config = require('rustaceanvim.config.internal')
|
||||
local lsp_util = vim.lsp.util
|
||||
|
||||
local M = {}
|
||||
|
||||
local function get_params()
|
||||
return lsp_util.make_position_params(0, nil)
|
||||
end
|
||||
|
||||
---@class HoverActionsState
|
||||
local _state = {
|
||||
---@type integer
|
||||
winnr = nil,
|
||||
---@type unknown
|
||||
commands = nil,
|
||||
}
|
||||
|
||||
local function close_hover()
|
||||
local ui = require('rustaceanvim.ui')
|
||||
ui.close_win(_state.winnr)
|
||||
end
|
||||
|
||||
local function execute_rust_analyzer_command(action, ctx)
|
||||
local fn = vim.lsp.commands[action.command]
|
||||
if fn then
|
||||
fn(action, ctx)
|
||||
end
|
||||
end
|
||||
|
||||
-- run the command under the cursor, if the thing under the cursor is not the
|
||||
-- command then do nothing
|
||||
---@param ctx table
|
||||
local function run_command(ctx)
|
||||
local winnr = vim.api.nvim_get_current_win()
|
||||
local line = vim.api.nvim_win_get_cursor(winnr)[1]
|
||||
|
||||
if line > #_state.commands then
|
||||
return
|
||||
end
|
||||
|
||||
local action = _state.commands[line]
|
||||
|
||||
close_hover()
|
||||
execute_rust_analyzer_command(action, ctx)
|
||||
end
|
||||
|
||||
---@return string[]
|
||||
local function parse_commands()
|
||||
local prompt = {}
|
||||
|
||||
for i, value in ipairs(_state.commands) do
|
||||
if value.command == 'rust-analyzer.gotoLocation' then
|
||||
table.insert(prompt, string.format('%d. Go to %s (%s)', i, value.title, value.tooltip))
|
||||
elseif value.command == 'rust-analyzer.showReferences' then
|
||||
table.insert(prompt, string.format('%d. %s', i, 'Go to ' .. value.title))
|
||||
else
|
||||
table.insert(prompt, string.format('%d. %s', i, value.title))
|
||||
end
|
||||
end
|
||||
|
||||
return prompt
|
||||
end
|
||||
|
||||
function M.handler(_, result, ctx)
|
||||
if not (result and result.contents) then
|
||||
-- return { 'No information available' }
|
||||
return
|
||||
end
|
||||
|
||||
local markdown_lines = lsp_util.convert_input_to_markdown_lines(result.contents, {})
|
||||
if result.actions then
|
||||
_state.commands = result.actions[1].commands
|
||||
local prompt = parse_commands()
|
||||
local l = {}
|
||||
|
||||
for _, value in ipairs(prompt) do
|
||||
table.insert(l, value)
|
||||
end
|
||||
table.insert(l, '---')
|
||||
|
||||
markdown_lines = vim.list_extend(l, markdown_lines)
|
||||
end
|
||||
|
||||
if vim.tbl_isempty(markdown_lines) then
|
||||
-- return { 'No information available' }
|
||||
return
|
||||
end
|
||||
|
||||
-- NOTE: This is for backward compatibility
|
||||
local win_opt = vim.tbl_deep_extend('force', config.tools.float_win_config, config.tools.hover_actions)
|
||||
|
||||
local bufnr, winnr = lsp_util.open_floating_preview(
|
||||
markdown_lines,
|
||||
'markdown',
|
||||
vim.tbl_extend('keep', win_opt, {
|
||||
focusable = true,
|
||||
focus_id = 'rust-analyzer-hover-actions',
|
||||
close_events = { 'CursorMoved', 'BufHidden', 'InsertCharPre' },
|
||||
})
|
||||
)
|
||||
|
||||
if win_opt.auto_focus then
|
||||
vim.api.nvim_set_current_win(winnr)
|
||||
end
|
||||
|
||||
if _state.winnr ~= nil then
|
||||
return
|
||||
end
|
||||
|
||||
-- update the window number here so that we can map escape to close even
|
||||
-- when there are no actions, update the rest of the state later
|
||||
_state.winnr = winnr
|
||||
vim.keymap.set('n', 'q', close_hover, { buffer = bufnr, noremap = true, silent = true })
|
||||
vim.keymap.set('n', '<Esc>', close_hover, { buffer = bufnr, noremap = true, silent = true })
|
||||
|
||||
vim.api.nvim_buf_attach(bufnr, false, {
|
||||
on_detach = function()
|
||||
_state.winnr = nil
|
||||
end,
|
||||
})
|
||||
|
||||
--- stop here if there are no possible actions
|
||||
if result.actions == nil then
|
||||
return
|
||||
end
|
||||
|
||||
-- makes more sense in a dropdown-ish ui
|
||||
vim.wo[winnr].cursorline = true
|
||||
|
||||
-- explicitly disable signcolumn
|
||||
vim.wo[winnr].signcolumn = 'no'
|
||||
|
||||
-- run the command under the cursor
|
||||
vim.keymap.set('n', '<CR>', function()
|
||||
run_command(ctx)
|
||||
end, { buffer = bufnr, noremap = true, silent = true })
|
||||
end
|
||||
|
||||
local rl = require('rustaceanvim.rust_analyzer')
|
||||
|
||||
--- Sends the request to rust-analyzer to get hover actions and handle it
|
||||
function M.hover_actions()
|
||||
rl.buf_request(0, 'textDocument/hover', get_params(), M.handler)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,82 @@
|
||||
---@toc rustaceanvim.contents
|
||||
|
||||
---@mod intro Introduction
|
||||
---@brief [[
|
||||
---This plugin automatically configures the `rust-analyzer` builtin LSP client
|
||||
---and integrates with other rust tools.
|
||||
---@brief ]]
|
||||
---
|
||||
---@mod rustaceanvim
|
||||
---
|
||||
---@brief [[
|
||||
---
|
||||
---Commands:
|
||||
---
|
||||
--- ':RustAnalyzer start' - Start the LSP client.
|
||||
--- ':RustAnalyzer stop' - Stop the LSP client.
|
||||
--- ':RustAnalyzer restart' - Restart the LSP client.
|
||||
--- ':RustAnalyzer reloadSettings' - Reload settings for the LSP client.
|
||||
---
|
||||
---The ':RustLsp[!]' command is available after the LSP client has initialized.
|
||||
---It accepts the following subcommands:
|
||||
---
|
||||
--- 'runnables {args[]}?' - Run tests, executables, etc.
|
||||
--- ':RustLsp!' means run the last runnable (ignores any args).
|
||||
--- `args[]` allows you to override the executable's arguments.
|
||||
--- 'run {args[]}?' - Like 'runnables', but runs the target at the current cursor position.
|
||||
--- 'debuggables {args[]}?' - Debug tests, executables, etc. (requires |nvim-dap|).
|
||||
--- ':RustLsp!' means run the last debuggable (ignores any args).
|
||||
--- `args[]` allows you to override the executable's arguments.
|
||||
--- 'debug {args[]}?' - Like 'debuggables', but debugs the target at the current cursor position.
|
||||
--- 'testables {args[]}?' - Run tests
|
||||
--- ':RustLsp!' means run the last testable (ignores any args).
|
||||
--- `args[]` allows you to override the executable's arguments.
|
||||
--- 'expandMacro' - Expand macros recursively.
|
||||
--- 'moveItem {up|down}' - Move items up or down.
|
||||
--- 'hover {actions|range}' - Hover actions, or hover over visually selected range.
|
||||
--- 'explainError' - Display a hover window with explanations form the Rust error index.
|
||||
--- Like |vim.diagnostic.goto_next|, |explainError| will cycle diagnostics,
|
||||
--- starting at the cursor position, until it can find a diagnostic with
|
||||
--- an error code.
|
||||
--- 'renderDiagnostic' - Display a hover window with the rendered diagnostic,
|
||||
--- as displayed during `cargo build`.
|
||||
--- Like |vim.diagnostic.goto_next|, |renderDiagnostic| will cycle diagnostics,
|
||||
--- starting at the cursor position, until it can find a diagnostic with
|
||||
--- rendered data.
|
||||
--- 'openCargo' - Open the Cargo.toml file for the current package.
|
||||
--- 'openDocs' - Open docs.rs documentation for the symbol under the cursor.
|
||||
--- 'parentModule' - Open the current module's parent module.
|
||||
--- 'workspaceSymbol {onlyTypes?|allSymbols?} {query?}'
|
||||
--- Filtered workspace symbol search.
|
||||
--- When run with a bang (`:RustLsp! workspaceSymbol ...`),
|
||||
--- rust-analyzer will include dependencies in the search.
|
||||
--- You can also configure rust-analyzer so that |vim.lsp.buf.workspace_symbol|
|
||||
--- supports filtering (with a # suffix to the query) or searching dependencies.
|
||||
--- 'joinLines' - Join adjacent lines.
|
||||
--- 'ssr {query}' - Structural search and replace.
|
||||
--- Searches the entire buffer in normal mode.
|
||||
--- Searches the selected region in visual mode.
|
||||
--- 'crateGraph {backend}' - Create and view a crate graph with graphviz.
|
||||
--- 'syntaxTree' - View the syntax tree.
|
||||
--- 'view {mir|hir}' - View MIR or HIR.
|
||||
--- 'flyCheck' {run?|clear?|cancel?}
|
||||
--- - Run `cargo check` or another compatible command (f.x. `clippy`)
|
||||
--- in a background thread and provide LSP diagnostics based on
|
||||
--- the output of the command.
|
||||
--- Useful in large projects where running `cargo check` on each save
|
||||
--- can be costly.
|
||||
--- Defaults to `flyCheck run` if called without an argument.
|
||||
--- 'logFile' - Open the rust-analyzer log file.
|
||||
---
|
||||
---The ':Rustc' command can be used to interact with rustc.
|
||||
---It accepts the following subcommands:
|
||||
---
|
||||
--- 'unpretty {args[]}' - Opens a buffer with a textual representation of the MIR or others things,
|
||||
--- of the function closest to the cursor.
|
||||
--- Achieves an experience similar to Rust Playground.
|
||||
--- NOTE: This currently requires a tree-sitter parser for Rust,
|
||||
--- and a nightly compiler toolchain.
|
||||
---@brief ]]
|
||||
|
||||
local M = {}
|
||||
return M
|
||||
@ -0,0 +1,290 @@
|
||||
local M = {}
|
||||
---@type RustaceanConfig
|
||||
local config = require('rustaceanvim.config.internal')
|
||||
local compat = require('rustaceanvim.compat')
|
||||
local types = require('rustaceanvim.types.internal')
|
||||
local rust_analyzer = require('rustaceanvim.rust_analyzer')
|
||||
local server_status = require('rustaceanvim.server_status')
|
||||
local cargo = require('rustaceanvim.cargo')
|
||||
local os = require('rustaceanvim.os')
|
||||
|
||||
local function override_apply_text_edits()
|
||||
local old_func = vim.lsp.util.apply_text_edits
|
||||
---@diagnostic disable-next-line
|
||||
vim.lsp.util.apply_text_edits = function(edits, bufnr, offset_encoding)
|
||||
local overrides = require('rustaceanvim.overrides')
|
||||
overrides.snippet_text_edits_to_text_edits(edits)
|
||||
old_func(edits, bufnr, offset_encoding)
|
||||
end
|
||||
end
|
||||
|
||||
---@param client lsp.Client
|
||||
---@param root_dir string
|
||||
---@return boolean
|
||||
local function is_in_workspace(client, root_dir)
|
||||
if not client.workspace_folders then
|
||||
return false
|
||||
end
|
||||
|
||||
for _, dir in ipairs(client.workspace_folders) do
|
||||
if (root_dir .. '/'):sub(1, #dir.name + 1) == dir.name .. '/' then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---Searches upward for a .vscode/settings.json that contains rust-analyzer
|
||||
---settings and returns them.
|
||||
---@param bufname string
|
||||
---@return table server_settings or an empty table if no settings were found
|
||||
local function find_vscode_settings(bufname)
|
||||
local settings = {}
|
||||
local found_dirs = vim.fs.find({ '.vscode' }, { upward = true, path = vim.fs.dirname(bufname), type = 'directory' })
|
||||
if vim.tbl_isempty(found_dirs) then
|
||||
return settings
|
||||
end
|
||||
local vscode_dir = found_dirs[1]
|
||||
local results = vim.fn.glob(compat.joinpath(vscode_dir, 'settings.json'), true, true)
|
||||
if vim.tbl_isempty(results) then
|
||||
return settings
|
||||
end
|
||||
local content = os.read_file(results[1])
|
||||
return content and require('rustaceanvim.config.json').silent_decode(content) or {}
|
||||
end
|
||||
|
||||
---Generate the settings from config and vscode settings if found.
|
||||
---settings and returns them.
|
||||
---@param bufname string
|
||||
---@param root_dir string | nil
|
||||
---@param client_config table
|
||||
---@return table server_settings or an empty table if no settings were found
|
||||
local function get_start_settings(bufname, root_dir, client_config)
|
||||
local settings = client_config.settings
|
||||
local evaluated_settings = type(settings) == 'function' and settings(root_dir, client_config.default_settings)
|
||||
or settings
|
||||
|
||||
if config.server.load_vscode_settings then
|
||||
local json_settings = find_vscode_settings(bufname)
|
||||
require('rustaceanvim.config.json').override_with_rust_analyzer_json_keys(evaluated_settings, json_settings)
|
||||
end
|
||||
|
||||
return evaluated_settings
|
||||
end
|
||||
|
||||
---@class LspStartConfig: RustaceanLspClientConfig
|
||||
---@field root_dir string | nil
|
||||
---@field init_options? table
|
||||
---@field settings table
|
||||
---@field cmd string[]
|
||||
---@field name string
|
||||
---@field filetypes string[]
|
||||
---@field capabilities table
|
||||
---@field handlers lsp.Handler[]
|
||||
---@field on_init function
|
||||
---@field on_attach function
|
||||
---@field on_exit function
|
||||
|
||||
--- Start or attach the LSP client
|
||||
---@param bufnr? number The buffer number (optional), defaults to the current buffer
|
||||
---@return integer|nil client_id The LSP client ID
|
||||
M.start = function(bufnr)
|
||||
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
||||
local bufname = vim.api.nvim_buf_get_name(bufnr)
|
||||
local client_config = config.server
|
||||
---@type LspStartConfig
|
||||
local lsp_start_config = vim.tbl_deep_extend('force', {}, client_config)
|
||||
local root_dir = cargo.get_root_dir(bufname)
|
||||
if not root_dir then
|
||||
--- No project root found. Start in detached/standalone mode.
|
||||
root_dir = vim.fs.dirname(bufname)
|
||||
lsp_start_config.init_options = { detachedFiles = { bufname } }
|
||||
end
|
||||
root_dir = os.normalize_path_on_windows(root_dir)
|
||||
lsp_start_config.root_dir = root_dir
|
||||
|
||||
lsp_start_config.settings = get_start_settings(bufname, root_dir, client_config)
|
||||
|
||||
-- Check if a client is already running and add the workspace folder if necessary.
|
||||
for _, client in pairs(rust_analyzer.get_active_rustaceanvim_clients()) do
|
||||
if root_dir and not is_in_workspace(client, root_dir) then
|
||||
local workspace_folder = { uri = vim.uri_from_fname(root_dir), name = root_dir }
|
||||
local params = {
|
||||
event = {
|
||||
added = { workspace_folder },
|
||||
removed = {},
|
||||
},
|
||||
}
|
||||
client.rpc.notify('workspace/didChangeWorkspaceFolders', params)
|
||||
if not client.workspace_folders then
|
||||
client.workspace_folders = {}
|
||||
end
|
||||
table.insert(client.workspace_folders, workspace_folder)
|
||||
vim.lsp.buf_attach_client(bufnr, client.id)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local rust_analyzer_cmd = types.evaluate(client_config.cmd)
|
||||
if #rust_analyzer_cmd == 0 or vim.fn.executable(rust_analyzer_cmd[1]) ~= 1 then
|
||||
vim.notify('rust-analyzer binary not found.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
---@cast rust_analyzer_cmd string[]
|
||||
lsp_start_config.cmd = rust_analyzer_cmd
|
||||
lsp_start_config.name = 'rust-analyzer'
|
||||
lsp_start_config.filetypes = { 'rust' }
|
||||
|
||||
local custom_handlers = {}
|
||||
custom_handlers['experimental/serverStatus'] = server_status.handler
|
||||
|
||||
if config.tools.hover_actions.replace_builtin_hover then
|
||||
custom_handlers['textDocument/hover'] = require('rustaceanvim.hover_actions').handler
|
||||
end
|
||||
|
||||
lsp_start_config.handlers = vim.tbl_deep_extend('force', custom_handlers, lsp_start_config.handlers or {})
|
||||
|
||||
local commands = require('rustaceanvim.commands')
|
||||
local old_on_init = lsp_start_config.on_init
|
||||
lsp_start_config.on_init = function(...)
|
||||
override_apply_text_edits()
|
||||
commands.create_rust_lsp_command()
|
||||
if type(old_on_init) == 'function' then
|
||||
old_on_init(...)
|
||||
end
|
||||
end
|
||||
|
||||
local old_on_attach = lsp_start_config.on_attach
|
||||
lsp_start_config.on_attach = function(...)
|
||||
if type(old_on_attach) == 'function' then
|
||||
old_on_attach(...)
|
||||
end
|
||||
end
|
||||
|
||||
local old_on_exit = lsp_start_config.on_exit
|
||||
lsp_start_config.on_exit = function(...)
|
||||
override_apply_text_edits()
|
||||
-- on_exit runs in_fast_event
|
||||
vim.schedule(function()
|
||||
commands.delete_rust_lsp_command()
|
||||
end)
|
||||
if type(old_on_exit) == 'function' then
|
||||
old_on_exit(...)
|
||||
end
|
||||
end
|
||||
|
||||
return vim.lsp.start(lsp_start_config)
|
||||
end
|
||||
|
||||
---Stop the LSP client.
|
||||
---@param bufnr? number The buffer number, defaults to the current buffer
|
||||
---@return table[] clients A list of clients that will be stopped
|
||||
M.stop = function(bufnr)
|
||||
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
||||
local clients = rust_analyzer.get_active_rustaceanvim_clients(bufnr)
|
||||
vim.lsp.stop_client(clients)
|
||||
if type(clients) == 'table' then
|
||||
---@cast clients lsp.Client[]
|
||||
for _, client in ipairs(clients) do
|
||||
server_status.reset_client_state(client.id)
|
||||
end
|
||||
else
|
||||
---@cast clients lsp.Client
|
||||
server_status.reset_client_state(clients.id)
|
||||
end
|
||||
return clients
|
||||
end
|
||||
|
||||
---Reload settings for the LSP client.
|
||||
---@param bufnr? number The buffer number, defaults to the current buffer
|
||||
---@return table[] clients A list of clients that will be have their settings reloaded
|
||||
M.reload_settings = function(bufnr)
|
||||
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
||||
local clients = rust_analyzer.get_active_rustaceanvim_clients(bufnr)
|
||||
---@cast clients lsp.Client[]
|
||||
for _, client in ipairs(clients) do
|
||||
local settings = get_start_settings(vim.api.nvim_buf_get_name(bufnr), client.config.root_dir, config.server)
|
||||
---@diagnostic disable-next-line: inject-field
|
||||
client.settings = settings
|
||||
client.notify('workspace/didChangeConfiguration', {
|
||||
settings = client.settings,
|
||||
})
|
||||
end
|
||||
return clients
|
||||
end
|
||||
|
||||
---Restart the LSP client.
|
||||
---Fails silently if the buffer's filetype is not one of the filetypes specified in the config.
|
||||
---@param bufnr? number The buffer number (optional), defaults to the current buffer
|
||||
---@return number|nil client_id The LSP client ID after restart
|
||||
M.restart = function(bufnr)
|
||||
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
||||
local clients = M.stop(bufnr)
|
||||
local timer, _, _ = compat.uv.new_timer()
|
||||
if not timer then
|
||||
-- TODO: Log error when logging is implemented
|
||||
return
|
||||
end
|
||||
local attempts_to_live = 50
|
||||
local stopped_client_count = 0
|
||||
timer:start(200, 100, function()
|
||||
for _, client in ipairs(clients) do
|
||||
if client:is_stopped() then
|
||||
stopped_client_count = stopped_client_count + 1
|
||||
vim.schedule(function()
|
||||
M.start(bufnr)
|
||||
end)
|
||||
end
|
||||
end
|
||||
if stopped_client_count >= #clients then
|
||||
timer:stop()
|
||||
attempts_to_live = 0
|
||||
elseif attempts_to_live <= 0 then
|
||||
vim.notify('rustaceanvim.lsp: Could not restart all LSP clients.', vim.log.levels.ERROR)
|
||||
timer:stop()
|
||||
attempts_to_live = 0
|
||||
end
|
||||
attempts_to_live = attempts_to_live - 1
|
||||
end)
|
||||
end
|
||||
|
||||
---@enum RustAnalyzerCmd
|
||||
local RustAnalyzerCmd = {
|
||||
start = 'start',
|
||||
stop = 'stop',
|
||||
restart = 'restart',
|
||||
reload_settings = 'reloadSettings',
|
||||
}
|
||||
|
||||
local function rust_analyzer_cmd(opts)
|
||||
local fargs = opts.fargs
|
||||
local cmd = fargs[1]
|
||||
---@cast cmd RustAnalyzerCmd
|
||||
if cmd == RustAnalyzerCmd.start then
|
||||
M.start()
|
||||
elseif cmd == RustAnalyzerCmd.stop then
|
||||
M.stop()
|
||||
elseif cmd == RustAnalyzerCmd.restart then
|
||||
M.restart()
|
||||
elseif cmd == RustAnalyzerCmd.reload_settings then
|
||||
M.reload_settings()
|
||||
end
|
||||
end
|
||||
|
||||
vim.api.nvim_create_user_command('RustAnalyzer', rust_analyzer_cmd, {
|
||||
nargs = '+',
|
||||
desc = 'Starts or stops the rust-analyzer LSP client',
|
||||
complete = function(arg_lead, cmdline, _)
|
||||
local clients = rust_analyzer.get_active_rustaceanvim_clients()
|
||||
---@type RustAnalyzerCmd[]
|
||||
local commands = #clients == 0 and { 'start' } or { 'stop', 'restart', 'reloadSettings' }
|
||||
if cmdline:match('^RustAnalyzer%s+%w*$') then
|
||||
return vim.tbl_filter(function(command)
|
||||
return command:find(arg_lead) ~= nil
|
||||
end, commands)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
return M
|
||||
@ -0,0 +1,395 @@
|
||||
---@mod rustaceanvim.neotest
|
||||
---
|
||||
---@brief [[
|
||||
---
|
||||
---A |neotest| adapter for rust, powered by rustaceanvim.
|
||||
---
|
||||
---If you add this to neotest:
|
||||
---
|
||||
--->
|
||||
---require('neotest').setup {
|
||||
--- -- ...,
|
||||
--- adapters = {
|
||||
--- -- ...,
|
||||
--- require('rustaceanvim.neotest')
|
||||
--- },
|
||||
---}
|
||||
---<
|
||||
---
|
||||
---this plugin will configure itself to use |neotest|
|
||||
---as a test executor, and |neotest| will use rust-analyzer
|
||||
---for test discovery and command construction.
|
||||
---
|
||||
---Note: If you use this adapter, do not add the neotest-rust adapter
|
||||
---(another plugin).
|
||||
---
|
||||
---@brief ]]
|
||||
|
||||
---@diagnostic disable: duplicate-set-field
|
||||
|
||||
local lib = require('neotest.lib')
|
||||
local nio = require('nio')
|
||||
local trans = require('rustaceanvim.neotest.trans')
|
||||
local cargo = require('rustaceanvim.cargo')
|
||||
local overrides = require('rustaceanvim.overrides')
|
||||
local compat = require('rustaceanvim.compat')
|
||||
|
||||
---@package
|
||||
---@type neotest.Adapter
|
||||
local NeotestAdapter = { name = 'rustaceanvim' }
|
||||
|
||||
---@package
|
||||
---@param file_name string
|
||||
---@return string | nil
|
||||
NeotestAdapter.root = function(file_name)
|
||||
return cargo.get_root_dir(file_name)
|
||||
end
|
||||
|
||||
---@package
|
||||
---@param rel_path string Path to directory, relative to root
|
||||
---@return boolean
|
||||
NeotestAdapter.filter_dir = function(_, rel_path, _)
|
||||
return rel_path ~= 'target'
|
||||
end
|
||||
|
||||
---@package
|
||||
---@param file_path string
|
||||
---@return boolean
|
||||
NeotestAdapter.is_test_file = function(file_path)
|
||||
return vim.endswith(file_path, '.rs')
|
||||
end
|
||||
|
||||
---@package
|
||||
---@class rustaceanvim.neotest.Position: neotest.Position
|
||||
---@field runnable? RARunnable
|
||||
|
||||
----@param name string
|
||||
----@return integer
|
||||
local function find_buffer_by_name(name)
|
||||
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
|
||||
local buf_name = vim.api.nvim_buf_get_name(bufnr)
|
||||
if buf_name == name then
|
||||
return bufnr
|
||||
end
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
---@package
|
||||
---@class nio.rustaceanvim.Client: nio.lsp.Client
|
||||
---@field request nio.rustaceanvim.RequestClient Interface to all requests that can be sent by the client
|
||||
---@field config vim.lsp.ClientConfig
|
||||
|
||||
---@package
|
||||
---@class nio.rustaceanvim.RequestClient: nio.lsp.RequestClient
|
||||
---@field experimental_runnables fun(args: nio.lsp.types.ImplementationParams, bufnr: integer?, opts: nio.lsp.RequestOpts): nio.lsp.types.ResponseError|nil, RARunnable[]|nil
|
||||
|
||||
---@package
|
||||
---@param file_path string
|
||||
---@return neotest.Tree
|
||||
NeotestAdapter.discover_positions = function(file_path)
|
||||
---@type rustaceanvim.neotest.Position[]
|
||||
local positions = {}
|
||||
|
||||
local lsp_client = require('rustaceanvim.rust_analyzer').get_client_for_file(file_path, 'experimental/runnables')
|
||||
if not lsp_client then
|
||||
---@diagnostic disable-next-line: missing-parameter
|
||||
return lib.positions.parse_tree(positions)
|
||||
end
|
||||
local nio_client = nio.lsp.get_client_by_id(lsp_client.id)
|
||||
---@cast nio_client nio.rustaceanvim.Client
|
||||
local bufnr = find_buffer_by_name(file_path)
|
||||
local params = {
|
||||
textDocument = {
|
||||
uri = vim.uri_from_fname(file_path),
|
||||
},
|
||||
position = nil,
|
||||
}
|
||||
local err, runnables = nio_client.request.experimental_runnables(params, bufnr, {
|
||||
timeout = 100000,
|
||||
})
|
||||
|
||||
if err or type(runnables) ~= 'table' or #runnables == 0 then
|
||||
---@diagnostic disable-next-line: missing-parameter
|
||||
return lib.positions.parse_tree(positions)
|
||||
end
|
||||
|
||||
local max_end_row = 0
|
||||
for _, runnable in pairs(runnables) do
|
||||
local pos = trans.runnable_to_position(file_path, runnable)
|
||||
if pos then
|
||||
max_end_row = math.max(max_end_row, pos.range[3])
|
||||
if pos.type ~= 'dir' then
|
||||
table.insert(positions, pos)
|
||||
end
|
||||
end
|
||||
end
|
||||
---@diagnostic disable-next-line: cast-type-mismatch
|
||||
---@cast runnables RARunnable[]
|
||||
|
||||
---@type { [string]: neotest.Position }
|
||||
local tests_by_name = {}
|
||||
---@type rustaceanvim.neotest.Position[]
|
||||
local namespaces = {}
|
||||
for _, pos in pairs(positions) do
|
||||
if pos.type == 'test' then
|
||||
tests_by_name[pos.name] = pos
|
||||
elseif pos.type == 'namespace' then
|
||||
table.insert(namespaces, pos)
|
||||
end
|
||||
end
|
||||
|
||||
-- sort namespaces by name from longest to shortest
|
||||
table.sort(namespaces, function(a, b)
|
||||
return #a.name > #b.name
|
||||
end)
|
||||
|
||||
---@type { [string]: rustaceanvim.neotest.Position[] }
|
||||
local positions_by_namespace = {}
|
||||
-- group tests by their longest matching namespace
|
||||
for _, namespace in ipairs(namespaces) do
|
||||
if namespace.name ~= '' then
|
||||
---@type string[]
|
||||
local child_keys = vim.tbl_filter(function(name)
|
||||
return vim.startswith(name, namespace.name .. '::')
|
||||
end, vim.tbl_keys(tests_by_name))
|
||||
local children = { namespace }
|
||||
for _, key in ipairs(child_keys) do
|
||||
local child_pos = tests_by_name[key]
|
||||
tests_by_name[key] = nil
|
||||
--- strip the namespace and "::" from the name
|
||||
child_pos.name = child_pos.name:sub(#namespace.name + 3, #child_pos.name)
|
||||
table.insert(children, child_pos)
|
||||
end
|
||||
positions_by_namespace[namespace.name] = children
|
||||
end
|
||||
end
|
||||
|
||||
-- nest child namespaces in their parent namespace
|
||||
for i, namespace in ipairs(namespaces) do
|
||||
---@type rustaceanvim.neotest.Position?
|
||||
local parent = nil
|
||||
-- search remaning namespaces for the longest matching parent namespace
|
||||
for _, other_namespace in ipairs { unpack(namespaces, i + 1) } do
|
||||
if vim.startswith(namespace.name, other_namespace.name .. '::') then
|
||||
parent = other_namespace
|
||||
break
|
||||
end
|
||||
end
|
||||
if parent ~= nil then
|
||||
local namespace_name = namespace.name
|
||||
local children = positions_by_namespace[namespace_name]
|
||||
-- strip parent namespace + "::"
|
||||
children[1].name = children[1].name:sub(#parent.name + 3, #namespace_name)
|
||||
table.insert(positions_by_namespace[parent.name], children)
|
||||
positions_by_namespace[namespace_name] = nil
|
||||
end
|
||||
end
|
||||
|
||||
local sorted_positions = {}
|
||||
for _, namespace_positions in pairs(positions_by_namespace) do
|
||||
table.insert(sorted_positions, namespace_positions)
|
||||
end
|
||||
-- any remaning tests had no parent namespace
|
||||
vim.list_extend(sorted_positions, vim.tbl_values(tests_by_name))
|
||||
|
||||
-- sort positions by their start range
|
||||
local function sort_positions(to_sort)
|
||||
for _, item in ipairs(to_sort) do
|
||||
if compat.islist(item) then
|
||||
sort_positions(item)
|
||||
end
|
||||
end
|
||||
|
||||
-- pop header from the list before sorting since it's used to sort in its parent's context
|
||||
local header = table.remove(to_sort, 1)
|
||||
table.sort(to_sort, function(a, b)
|
||||
local a_item = compat.islist(a) and a[1] or a
|
||||
local b_item = compat.islist(b) and b[1] or b
|
||||
if a_item.range[1] == b_item.range[1] then
|
||||
return a_item.name < b_item.name
|
||||
else
|
||||
return a_item.range[1] < b_item.range[1]
|
||||
end
|
||||
end)
|
||||
table.insert(to_sort, 1, header)
|
||||
end
|
||||
sort_positions(sorted_positions)
|
||||
|
||||
local file_pos = {
|
||||
id = file_path,
|
||||
name = vim.fn.fnamemodify(file_path, ':t'),
|
||||
type = 'file',
|
||||
path = file_path,
|
||||
range = { 0, 0, max_end_row, 0 },
|
||||
-- use the shortest namespace for the file runnable
|
||||
runnable = #namespaces > 0 and namespaces[#namespaces].runnable or nil,
|
||||
}
|
||||
table.insert(sorted_positions, 1, file_pos)
|
||||
|
||||
return require('neotest.types.tree').from_list(sorted_positions, function(x)
|
||||
return x.name
|
||||
end)
|
||||
end
|
||||
|
||||
---@package
|
||||
---@class rustaceanvim.neotest.RunSpec: neotest.RunSpec
|
||||
---@field context rustaceanvim.neotest.RunContext
|
||||
|
||||
---@package
|
||||
---@class rustaceanvim.neotest.RunContext
|
||||
---@field file string
|
||||
---@field pos_id string
|
||||
---@field type neotest.PositionType
|
||||
---@field tree neotest.Tree
|
||||
|
||||
---@package
|
||||
---@param run_args neotest.RunArgs
|
||||
---@return neotest.RunSpec|nil
|
||||
---@private
|
||||
function NeotestAdapter.build_spec(run_args)
|
||||
local supported_types = { 'test', 'namespace', 'file', 'dir' }
|
||||
local tree = run_args and run_args.tree
|
||||
if not tree then
|
||||
return
|
||||
end
|
||||
local pos = tree:data()
|
||||
---@cast pos rustaceanvim.neotest.Position
|
||||
if not vim.tbl_contains(supported_types, pos.type) then
|
||||
return
|
||||
end
|
||||
local runnable = pos.runnable
|
||||
if not runnable then
|
||||
return
|
||||
end
|
||||
local context = {
|
||||
file = pos.path,
|
||||
pos_id = pos.id,
|
||||
type = pos.type,
|
||||
tree = tree,
|
||||
}
|
||||
local exe, args, cwd = require('rustaceanvim.runnables').get_command(runnable)
|
||||
if run_args.strategy == 'dap' then
|
||||
local dap = require('rustaceanvim.dap')
|
||||
overrides.sanitize_command_for_debugging(runnable.args.cargoArgs)
|
||||
local future = nio.control.future()
|
||||
dap.start(runnable.args, false, function(strategy)
|
||||
future.set(strategy)
|
||||
end, function(err)
|
||||
future.set_error(err)
|
||||
end)
|
||||
local ok, strategy = pcall(future.wait)
|
||||
if not ok then
|
||||
---@cast strategy string
|
||||
lib.notify(strategy, vim.log.levels.ERROR)
|
||||
end
|
||||
---@cast strategy DapClientConfig
|
||||
---@type rustaceanvim.neotest.RunSpec
|
||||
local run_spec = {
|
||||
cwd = cwd,
|
||||
context = context,
|
||||
strategy = strategy,
|
||||
}
|
||||
return run_spec
|
||||
else
|
||||
overrides.undo_debug_sanitize(runnable.args.cargoArgs)
|
||||
end
|
||||
---@type rustaceanvim.neotest.RunSpec
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
local run_spec = {
|
||||
command = vim.list_extend({ exe }, args),
|
||||
cwd = cwd,
|
||||
context = context,
|
||||
}
|
||||
return run_spec
|
||||
end
|
||||
|
||||
---@package
|
||||
---Get the file root from a test tree.
|
||||
---@param tree neotest.Tree The test tree.
|
||||
---@return neotest.Tree file_root The file root position.
|
||||
local function get_file_root(tree)
|
||||
for _, node in tree:iter_parents() do
|
||||
local data = node and node:data()
|
||||
if data and not vim.tbl_contains({ 'test', 'namespace' }, data.type) then
|
||||
return node
|
||||
end
|
||||
end
|
||||
return tree
|
||||
end
|
||||
|
||||
---@package
|
||||
---@param spec neotest.RunSpec
|
||||
---@param strategy_result neotest.StrategyResult
|
||||
---@return table<string, neotest.Result> results
|
||||
function NeotestAdapter.results(spec, strategy_result)
|
||||
---@type table<string, neotest.Result>
|
||||
local results = {}
|
||||
---@type rustaceanvim.neotest.RunContext
|
||||
local context = spec.context
|
||||
local ctx_pos_id = context.pos_id
|
||||
---@type string
|
||||
local output_content = lib.files.read(strategy_result.output)
|
||||
if strategy_result.code == 0 then
|
||||
results[ctx_pos_id] = {
|
||||
status = 'passed',
|
||||
output = strategy_result.output,
|
||||
}
|
||||
return results
|
||||
end
|
||||
---@type table<string,neotest.Error[]>
|
||||
local errors_by_test_id = {}
|
||||
output_content = output_content:gsub('\r\n', '\n')
|
||||
local diagnostics = require('rustaceanvim.test').parse_diagnostics(context.file, output_content)
|
||||
for _, diagnostic in pairs(diagnostics) do
|
||||
---@type neotest.Error
|
||||
local err = {
|
||||
line = diagnostic.lnum,
|
||||
message = diagnostic.message,
|
||||
}
|
||||
errors_by_test_id[diagnostic.test_id] = errors_by_test_id[diagnostic.test_id] or {}
|
||||
table.insert(errors_by_test_id[diagnostic.test_id], err)
|
||||
end
|
||||
if not vim.tbl_contains({ 'file', 'test', 'namespace' }, context.type) then
|
||||
return results
|
||||
end
|
||||
results[ctx_pos_id] = {
|
||||
status = 'failed',
|
||||
output = strategy_result.output,
|
||||
}
|
||||
local has_failures = not vim.tbl_isempty(diagnostics)
|
||||
for _, node in get_file_root(context.tree):iter_nodes() do
|
||||
local data = node:data()
|
||||
for test_id, errors in pairs(errors_by_test_id) do
|
||||
if vim.endswith(data.id, test_id) then
|
||||
results[data.id] = {
|
||||
status = 'failed',
|
||||
errors = errors,
|
||||
short = output_content,
|
||||
}
|
||||
elseif has_failures and data.type == 'test' then
|
||||
-- Initialise as skipped. Passed positions will be parsed and set later.
|
||||
results[data.id] = {
|
||||
status = 'skipped',
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
if has_failures then
|
||||
local pass_positions = output_content:gmatch('test%s(%S+)%s...%sok')
|
||||
for pos in pass_positions do
|
||||
results[trans.get_position_id(context.file, pos)] = {
|
||||
status = 'passed',
|
||||
}
|
||||
end
|
||||
end
|
||||
return results
|
||||
end
|
||||
|
||||
setmetatable(NeotestAdapter, {
|
||||
__call = function()
|
||||
return NeotestAdapter
|
||||
end,
|
||||
})
|
||||
|
||||
return NeotestAdapter
|
||||
@ -0,0 +1,68 @@
|
||||
local M = {}
|
||||
|
||||
---@param runnable RARunnable
|
||||
---@return string | nil
|
||||
local function get_test_path(runnable)
|
||||
local executableArgs = runnable.args and runnable.args.executableArgs or {}
|
||||
return #executableArgs > 0 and executableArgs[1] or nil
|
||||
end
|
||||
|
||||
---@overload fun(file_path: string, test_path: string | nil)
|
||||
---@overload fun(file_path: string, runnable: RARunnable)
|
||||
---@return string
|
||||
function M.get_position_id(file_path, runnable)
|
||||
local test_path = runnable
|
||||
if type(runnable) == 'table' then
|
||||
test_path = get_test_path(runnable)
|
||||
end
|
||||
---@cast test_path string | nil
|
||||
return test_path and table.concat(vim.list_extend({ file_path }, { test_path }), '::') or file_path
|
||||
end
|
||||
|
||||
---@param file_path string
|
||||
---@param runnable RARunnable
|
||||
---@return rustaceanvim.neotest.Position | nil
|
||||
function M.runnable_to_position(file_path, runnable)
|
||||
local cargoArgs = runnable.args and runnable.args.cargoArgs or {}
|
||||
if #cargoArgs > 0 and vim.startswith(cargoArgs[1], 'test') then
|
||||
---@type neotest.PositionType
|
||||
local type
|
||||
if vim.startswith(runnable.label, 'cargo test -p') then
|
||||
type = 'dir'
|
||||
elseif vim.startswith(runnable.label, 'test-mod') then
|
||||
type = 'namespace'
|
||||
elseif vim.startswith(runnable.label, 'test') or vim.startswith(runnable.label, 'doctest') then
|
||||
type = 'test'
|
||||
else
|
||||
return
|
||||
end
|
||||
local location = runnable.location
|
||||
local start_row, start_col, end_row, end_col = 0, 0, 0, 0
|
||||
if location then
|
||||
start_row = location.targetRange.start.line + 1
|
||||
start_col = location.targetRange.start.character
|
||||
end_row = location.targetRange['end'].line + 1
|
||||
end_col = location.targetRange['end'].character
|
||||
end
|
||||
local test_path = get_test_path(runnable)
|
||||
-- strip the file module prefix from the name
|
||||
if test_path then
|
||||
local mod_name = vim.fn.fnamemodify(file_path, ':t:r')
|
||||
if vim.startswith(test_path, mod_name .. '::') then
|
||||
test_path = test_path:sub(#mod_name + 3, #test_path)
|
||||
end
|
||||
end
|
||||
---@type rustaceanvim.neotest.Position
|
||||
local pos = {
|
||||
id = M.get_position_id(file_path, runnable),
|
||||
name = test_path or runnable.label,
|
||||
type = type,
|
||||
path = file_path,
|
||||
range = { start_row, start_col, end_row, end_col },
|
||||
runnable = runnable,
|
||||
}
|
||||
return pos
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,76 @@
|
||||
---@mod rustaceanvim.os Utilities for interacting with the operating system
|
||||
|
||||
local os = {}
|
||||
|
||||
local compat = require('rustaceanvim.compat')
|
||||
local shell = require('rustaceanvim.shell')
|
||||
|
||||
---@param url string
|
||||
function os.open_url(url)
|
||||
---@param obj table
|
||||
local function on_exit(obj)
|
||||
if obj.code ~= 0 then
|
||||
vim.schedule(function()
|
||||
vim.notify('Could not open URL: ' .. url, vim.log.levels.ERROR)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
if vim.fn.has('mac') == 1 then
|
||||
compat.system({ 'open', url }, nil, on_exit)
|
||||
return
|
||||
end
|
||||
if vim.fn.executable('sensible-browser') == 1 then
|
||||
compat.system({ 'sensible-browser', url }, nil, on_exit)
|
||||
return
|
||||
end
|
||||
if vim.fn.executable('xdg-open') == 1 then
|
||||
compat.system({ 'xdg-open', url }, nil, on_exit)
|
||||
return
|
||||
end
|
||||
local ok, err = pcall(vim.fn['netrw#BrowseX'], url, 0)
|
||||
if not ok then
|
||||
vim.notify('Could not open external docs. Neither xdg-open, nor netrw found: ' .. err, vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@return boolean
|
||||
local function starts_with_windows_drive_letter(path)
|
||||
return path:match('^%a:') ~= nil
|
||||
end
|
||||
|
||||
---Normalize path for Windows, which is case insensitive
|
||||
---@param path string
|
||||
---@return string normalized_path
|
||||
function os.normalize_path_on_windows(path)
|
||||
if shell.is_windows() and starts_with_windows_drive_letter(path) then
|
||||
return path:sub(1, 1):lower() .. path:sub(2):gsub('/+', '\\')
|
||||
end
|
||||
return path
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@return boolean
|
||||
function os.is_valid_file_path(path)
|
||||
local normalized_path = vim.fs.normalize(path, { expand_env = false })
|
||||
if shell.is_windows() then
|
||||
return starts_with_windows_drive_letter(normalized_path)
|
||||
end
|
||||
return vim.startswith(normalized_path, '/')
|
||||
end
|
||||
|
||||
---Read the content of a file
|
||||
---@param filename string
|
||||
---@return string|nil content
|
||||
function os.read_file(filename)
|
||||
local content
|
||||
local f = io.open(filename, 'r')
|
||||
if f then
|
||||
content = f:read('*a')
|
||||
f:close()
|
||||
end
|
||||
return content
|
||||
end
|
||||
|
||||
return os
|
||||
@ -0,0 +1,96 @@
|
||||
local M = {}
|
||||
|
||||
local compat = require('rustaceanvim.compat')
|
||||
|
||||
---@param input string unparsed snippet
|
||||
---@return string parsed snippet
|
||||
local function parse_snippet_fallback(input)
|
||||
local output = input
|
||||
-- $0 -> Nothing
|
||||
:gsub('%$%d', '')
|
||||
-- ${0:_} -> _
|
||||
:gsub('%${%d:(.-)}', '%1')
|
||||
:gsub([[\}]], '}')
|
||||
return output
|
||||
end
|
||||
|
||||
---@param input string unparsed snippet
|
||||
---@return string parsed snippet
|
||||
local function parse_snippet(input)
|
||||
local ok, parsed = pcall(function()
|
||||
return vim.lsp._snippet_grammar.parse(input)
|
||||
end)
|
||||
return ok and tostring(parsed) or parse_snippet_fallback(input)
|
||||
end
|
||||
|
||||
---@param spe? table
|
||||
function M.snippet_text_edits_to_text_edits(spe)
|
||||
if type(spe) ~= 'table' then
|
||||
return
|
||||
end
|
||||
for _, value in ipairs(spe) do
|
||||
if value.newText and value.insertTextFormat then
|
||||
value.newText = parse_snippet(value.newText)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Transforms the args to cargo-nextest args if it is detected.
|
||||
---Mutates command!
|
||||
---@param args string[]
|
||||
function M.try_nextest_transform(args)
|
||||
if vim.fn.executable('cargo-nextest') ~= 1 then
|
||||
return args
|
||||
end
|
||||
if args[1] == 'test' then
|
||||
args[1] = 'run'
|
||||
table.insert(args, 1, 'nextest')
|
||||
end
|
||||
if args[#args] == '--nocapture' then
|
||||
table.insert(args, 3, '--nocapture')
|
||||
table.remove(args, #args)
|
||||
end
|
||||
local nextest_unsupported_flags = {
|
||||
'--exact',
|
||||
'--show-output',
|
||||
}
|
||||
local indexes_to_remove_reverse_order = {}
|
||||
for i, arg in ipairs(args) do
|
||||
if compat.list_contains(nextest_unsupported_flags, arg) then
|
||||
table.insert(indexes_to_remove_reverse_order, 1, i)
|
||||
end
|
||||
end
|
||||
for _, i in pairs(indexes_to_remove_reverse_order) do
|
||||
table.remove(args, i)
|
||||
end
|
||||
return args
|
||||
end
|
||||
|
||||
-- sanitize_command_for_debugging substitutes the command arguments so it can be used to run a
|
||||
-- debugger.
|
||||
--
|
||||
-- @param command should be a table like: { "run", "--package", "<program>", "--bin", "<program>" }
|
||||
-- For some reason the endpoint textDocument/hover from rust-analyzer returns
|
||||
-- cargoArgs = { "run", "--package", "<program>", "--bin", "<program>" } for Debug entry.
|
||||
-- It doesn't make any sense to run a program before debugging. Even more the debugging won't run if
|
||||
-- the program waits some input. Take a look at rust-analyzer/editors/code/src/toolchain.ts.
|
||||
---@param command string[]
|
||||
function M.sanitize_command_for_debugging(command)
|
||||
if command[1] == 'run' then
|
||||
command[1] = 'build'
|
||||
elseif command[1] == 'test' and not compat.list_contains(command, '--no-run') then
|
||||
table.insert(command, 2, '--no-run')
|
||||
end
|
||||
end
|
||||
|
||||
---Undo sanitize_command_for_debugging.
|
||||
---@param command string[]
|
||||
function M.undo_debug_sanitize(command)
|
||||
if command[1] == 'build' then
|
||||
command[1] = 'run'
|
||||
elseif command[1] == 'test' and command[2] == '--no-run' then
|
||||
table.remove(command, 2)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,245 @@
|
||||
local config = require('rustaceanvim.config.internal')
|
||||
local overrides = require('rustaceanvim.overrides')
|
||||
|
||||
local M = {}
|
||||
|
||||
---@return { textDocument: lsp_text_document, position: nil }
|
||||
local function get_params()
|
||||
return {
|
||||
textDocument = vim.lsp.util.make_text_document_params(0),
|
||||
position = nil, -- get em all
|
||||
}
|
||||
end
|
||||
|
||||
---@class RARunnable
|
||||
---@field args RARunnableArgs
|
||||
---@field label string
|
||||
---@field location? RARunnableLocation
|
||||
|
||||
---@class RARunnableLocation
|
||||
---@field targetRange lsp.Range
|
||||
---@field targetSelectionRange lsp.Range
|
||||
|
||||
---@class RARunnableArgs
|
||||
---@field workspaceRoot string
|
||||
---@field cargoArgs string[]
|
||||
---@field cargoExtraArgs string[]
|
||||
---@field executableArgs string[]
|
||||
|
||||
---@param option string
|
||||
---@return string
|
||||
local function prettify_test_option(option)
|
||||
for _, prefix in pairs { 'test-mod ', 'test ', 'cargo test -p ' } do
|
||||
if vim.startswith(option, prefix) then
|
||||
return option:sub(prefix:len() + 1, option:len()):gsub('%-%-all%-targets', '(all targets)') or option
|
||||
end
|
||||
end
|
||||
return option:gsub('%-%-all%-targets', '(all targets)') or option
|
||||
end
|
||||
|
||||
---@param result RARunnable[]
|
||||
---@param executableArgsOverride? string[]
|
||||
---@param opts RunnablesOpts
|
||||
---@return string[]
|
||||
local function get_options(result, executableArgsOverride, opts)
|
||||
local option_strings = {}
|
||||
|
||||
for _, runnable in ipairs(result) do
|
||||
local str = runnable.label
|
||||
.. (
|
||||
executableArgsOverride and #executableArgsOverride > 0 and ' -- ' .. table.concat(executableArgsOverride, ' ')
|
||||
or ''
|
||||
)
|
||||
if opts.tests_only then
|
||||
str = prettify_test_option(str)
|
||||
end
|
||||
if config.tools.cargo_override then
|
||||
str = str:gsub('^cargo', config.tools.cargo_override)
|
||||
end
|
||||
table.insert(option_strings, str)
|
||||
end
|
||||
|
||||
return option_strings
|
||||
end
|
||||
|
||||
---@alias CargoCmd 'cargo'
|
||||
|
||||
---@param runnable RARunnable
|
||||
---@return string executable
|
||||
---@return string[] args
|
||||
---@return string | nil dir
|
||||
function M.get_command(runnable)
|
||||
local args = runnable.args
|
||||
|
||||
local dir = args.workspaceRoot
|
||||
|
||||
local ret = vim.list_extend({}, args.cargoArgs or {})
|
||||
ret = vim.list_extend(ret, args.cargoExtraArgs or {})
|
||||
table.insert(ret, '--')
|
||||
ret = vim.list_extend(ret, args.executableArgs or {})
|
||||
if
|
||||
config.tools.enable_nextest
|
||||
and not config.tools.cargo_override
|
||||
and not vim.startswith(runnable.label, 'doctest')
|
||||
then
|
||||
ret = overrides.try_nextest_transform(ret)
|
||||
end
|
||||
|
||||
return config.tools.cargo_override or 'cargo', ret, dir
|
||||
end
|
||||
|
||||
---@param choice integer
|
||||
---@param runnables RARunnable[]
|
||||
---@return CargoCmd command build command
|
||||
---@return string[] args
|
||||
---@return string|nil dir
|
||||
local function getCommand(choice, runnables)
|
||||
return M.get_command(runnables[choice])
|
||||
end
|
||||
|
||||
---@param choice integer
|
||||
---@param runnables RARunnable[]
|
||||
function M.run_command(choice, runnables)
|
||||
-- do nothing if choice is too high or too low
|
||||
if not choice or choice < 1 or choice > #runnables then
|
||||
return
|
||||
end
|
||||
|
||||
local opts = config.tools
|
||||
|
||||
local command, args, cwd = getCommand(choice, runnables)
|
||||
if not cwd then
|
||||
return
|
||||
end
|
||||
|
||||
if #args > 0 and (vim.startswith(args[1], 'test') or vim.startswith(args[1], 'nextest')) then
|
||||
local test_executor = vim.tbl_contains(args, '--all-targets') and opts.crate_test_executor or opts.test_executor
|
||||
test_executor.execute_command(command, args, cwd, {
|
||||
bufnr = vim.api.nvim_get_current_buf(),
|
||||
runnable = runnables[choice],
|
||||
})
|
||||
else
|
||||
opts.executor.execute_command(command, args, cwd)
|
||||
end
|
||||
end
|
||||
|
||||
---@param runnable RARunnable
|
||||
---@return boolean
|
||||
local function is_testable(runnable)
|
||||
---@cast runnable RARunnable
|
||||
local cargoArgs = runnable.args and runnable.args.cargoArgs or {}
|
||||
return #cargoArgs > 0 and vim.startswith(cargoArgs[1], 'test')
|
||||
end
|
||||
|
||||
---@param executableArgsOverride? string[]
|
||||
---@param runnables RARunnable[]
|
||||
---@return RARunnable[]
|
||||
function M.apply_exec_args_override(executableArgsOverride, runnables)
|
||||
if type(executableArgsOverride) == 'table' and #executableArgsOverride > 0 then
|
||||
local unique_runnables = {}
|
||||
for _, runnable in pairs(runnables) do
|
||||
runnable.args.executableArgs = executableArgsOverride
|
||||
unique_runnables[vim.inspect(runnable)] = runnable
|
||||
end
|
||||
runnables = vim.tbl_values(unique_runnables)
|
||||
end
|
||||
return runnables
|
||||
end
|
||||
|
||||
---@param executableArgsOverride? string[]
|
||||
---@param opts RunnablesOpts
|
||||
---@return fun(_, result: RARunnable[])
|
||||
local function mk_handler(executableArgsOverride, opts)
|
||||
---@param runnables RARunnable[]
|
||||
return function(_, runnables)
|
||||
if runnables == nil then
|
||||
return
|
||||
end
|
||||
runnables = M.apply_exec_args_override(executableArgsOverride, runnables)
|
||||
if opts.tests_only then
|
||||
runnables = vim.tbl_filter(is_testable, runnables)
|
||||
end
|
||||
|
||||
-- get the choice from the user
|
||||
local options = get_options(runnables, executableArgsOverride, opts)
|
||||
vim.ui.select(options, { prompt = 'Runnables', kind = 'rust-tools/runnables' }, function(_, choice)
|
||||
---@cast choice integer
|
||||
M.run_command(choice, runnables)
|
||||
|
||||
local cached_commands = require('rustaceanvim.cached_commands')
|
||||
if opts.tests_only then
|
||||
cached_commands.set_last_testable(choice, runnables)
|
||||
else
|
||||
cached_commands.set_last_runnable(choice, runnables)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---@param position lsp.Position
|
||||
---@param targetRange lsp.Range
|
||||
local function is_within_range(position, targetRange)
|
||||
return targetRange.start.line <= position.line and targetRange['end'].line >= position.line
|
||||
end
|
||||
|
||||
---@param runnables RARunnable
|
||||
---@return integer | nil choice
|
||||
function M.get_runnable_at_cursor_position(runnables)
|
||||
---@type lsp.Position
|
||||
local position = vim.lsp.util.make_position_params().position
|
||||
---@type integer|nil, integer|nil
|
||||
local choice, fallback
|
||||
for idx, runnable in ipairs(runnables) do
|
||||
if runnable.location then
|
||||
local range = runnable.location.targetRange
|
||||
if is_within_range(position, range) then
|
||||
if vim.startswith(runnable.label, 'test-mod') then
|
||||
fallback = idx
|
||||
else
|
||||
choice = idx
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return choice or fallback
|
||||
end
|
||||
|
||||
local function mk_cursor_position_handler(executableArgsOverride)
|
||||
---@param runnables RARunnable[]
|
||||
return function(_, runnables)
|
||||
if runnables == nil then
|
||||
return
|
||||
end
|
||||
runnables = M.apply_exec_args_override(executableArgsOverride, runnables)
|
||||
local choice = M.get_runnable_at_cursor_position(runnables)
|
||||
if not choice then
|
||||
vim.notify('No runnable targets found for the current position.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
M.run_command(choice, runnables)
|
||||
local cached_commands = require('rustaceanvim.cached_commands')
|
||||
if is_testable(runnables[choice]) then
|
||||
cached_commands.set_last_testable(choice, runnables)
|
||||
end
|
||||
cached_commands.set_last_runnable(choice, runnables)
|
||||
end
|
||||
end
|
||||
|
||||
---@class RunnablesOpts
|
||||
---@field tests_only? boolean
|
||||
|
||||
---Sends the request to rust-analyzer to get the runnables and handles them
|
||||
---@param executableArgsOverride? string[]
|
||||
---@param opts? RunnablesOpts
|
||||
function M.runnables(executableArgsOverride, opts)
|
||||
---@type RunnablesOpts
|
||||
opts = vim.tbl_deep_extend('force', { tests_only = false }, opts or {})
|
||||
vim.lsp.buf_request(0, 'experimental/runnables', get_params(), mk_handler(executableArgsOverride, opts))
|
||||
end
|
||||
|
||||
function M.run(executableArgsOverride)
|
||||
vim.lsp.buf_request(0, 'experimental/runnables', get_params(), mk_cursor_position_handler(executableArgsOverride))
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,82 @@
|
||||
---@mod rustaceanvim.rust_analyzer Functions for interacting with rust-analyzer
|
||||
|
||||
local compat = require('rustaceanvim.compat')
|
||||
local os = require('rustaceanvim.os')
|
||||
|
||||
---@class RustAnalyzerClientAdapter
|
||||
local M = {}
|
||||
|
||||
---@param bufnr number | nil 0 for the current buffer, `nil` for no buffer filter
|
||||
---@param filter? vim.lsp.get_clients.Filter
|
||||
---@return lsp.Client[]
|
||||
M.get_active_rustaceanvim_clients = function(bufnr, filter)
|
||||
---@type vim.lsp.get_clients.Filter
|
||||
filter = vim.tbl_deep_extend('force', filter or {}, {
|
||||
name = 'rust-analyzer',
|
||||
})
|
||||
if bufnr then
|
||||
filter.bufnr = bufnr
|
||||
end
|
||||
return compat.get_clients(filter)
|
||||
end
|
||||
|
||||
---@param method string LSP method name
|
||||
---@param params table|nil Parameters to send to the server
|
||||
---@param handler? lsp.Handler See |lsp-handler|
|
||||
--- If nil, follows resolution strategy defined in |lsp-handler-configuration|
|
||||
M.any_buf_request = function(method, params, handler)
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local client_found = M.buf_request(bufnr, method, params, handler)
|
||||
if client_found then
|
||||
return
|
||||
end
|
||||
-- No buffer found. Try any client.
|
||||
for _, client in ipairs(M.get_active_rustaceanvim_clients(nil, { method = method })) do
|
||||
client.request(method, params, handler, 0)
|
||||
end
|
||||
end
|
||||
|
||||
---@param bufnr integer Buffer handle, or 0 for current.
|
||||
---@param method string LSP method name
|
||||
---@param params table|nil Parameters to send to the server
|
||||
---@param handler? lsp.Handler See |lsp-handler|
|
||||
--- If nil, follows resolution strategy defined in |lsp-handler-configuration|
|
||||
---@return boolean client_found
|
||||
M.buf_request = function(bufnr, method, params, handler)
|
||||
if bufnr == nil or bufnr == 0 then
|
||||
bufnr = vim.api.nvim_get_current_buf()
|
||||
end
|
||||
local client_found = false
|
||||
for _, client in ipairs(M.get_active_rustaceanvim_clients(bufnr, { method = method })) do
|
||||
client.request(method, params, handler, bufnr)
|
||||
client_found = true
|
||||
end
|
||||
return client_found
|
||||
end
|
||||
|
||||
---@param file_path string Search for clients with a root_dir matching this file path
|
||||
---@param method string LSP method name
|
||||
---@return lsp.Client|nil
|
||||
M.get_client_for_file = function(file_path, method)
|
||||
for _, client in ipairs(M.get_active_rustaceanvim_clients(nil, { method = method })) do
|
||||
local root_dir = client.config.root_dir
|
||||
if root_dir and vim.startswith(os.normalize_path_on_windows(file_path), root_dir) then
|
||||
return client
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param method string LSP method name
|
||||
---@param params table|nil Parameters to send to the server
|
||||
M.notify = function(method, params)
|
||||
local client_found = false
|
||||
for _, client in ipairs(M.get_active_rustaceanvim_clients(0, { method = method })) do
|
||||
client.notify(method, params)
|
||||
client_found = true
|
||||
end
|
||||
if not client_found then
|
||||
vim.notify('No rust-analyzer client found for method: ' .. method, vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,42 @@
|
||||
local config = require('rustaceanvim.config.internal')
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type { [integer]: boolean }
|
||||
local _ran_once = {}
|
||||
|
||||
---@param result RustAnalyzerInitializedStatusInternal
|
||||
function M.handler(_, result, ctx, _)
|
||||
-- quiescent means the full set of results is ready.
|
||||
if not result.quiescent or _ran_once[ctx.client_id] then
|
||||
return
|
||||
end
|
||||
-- rust-analyzer may provide incomplete/empty inlay hints by the time Neovim
|
||||
-- calls the `on_attach` callback.
|
||||
-- [https://github.com/neovim/neovim/issues/26511]
|
||||
-- This workaround forces Neovim to redraw inlay hints if they are enabled,
|
||||
-- as soon as rust-analyzer has fully initialized.
|
||||
if type(vim.lsp.inlay_hint) == 'table' then
|
||||
for _, bufnr in ipairs(vim.lsp.get_buffers_by_client_id(ctx.client_id)) do
|
||||
if vim.lsp.inlay_hint.is_enabled { bufnr = bufnr } then
|
||||
vim.lsp.inlay_hint.enable(false, { bufnr = bufnr })
|
||||
vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Load user on_initialized
|
||||
if config.tools.on_initialized then
|
||||
config.tools.on_initialized(result)
|
||||
end
|
||||
if config.dap.autoload_configurations then
|
||||
require('rustaceanvim.commands.debuggables').add_dap_debuggables()
|
||||
end
|
||||
_ran_once[ctx.client_id] = true
|
||||
end
|
||||
|
||||
---@param client_id integer
|
||||
function M.reset_client_state(client_id)
|
||||
_ran_once[client_id] = false
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,65 @@
|
||||
local M = {}
|
||||
|
||||
local compat = require('rustaceanvim.compat')
|
||||
|
||||
---@return boolean
|
||||
function M.is_windows()
|
||||
local sysname = compat.uv.os_uname().sysname
|
||||
return sysname == 'Windows' or sysname == 'Windows_NT'
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function M.is_macos()
|
||||
return compat.uv.os_uname().sysname == 'Darwin'
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
local function is_nushell()
|
||||
---@diagnostic disable-next-line: missing-parameter
|
||||
local shell = compat.uv.os_getenv('SHELL')
|
||||
local nu = 'nu'
|
||||
-- Check if $SHELL ends in "nu"
|
||||
return shell ~= nil and shell:sub(-string.len(nu)) == nu
|
||||
end
|
||||
|
||||
---Get a new command which is a chain of all the old commands
|
||||
---Note that a space is not added at the end of the returned command string
|
||||
---@param commands string[]
|
||||
---@return string
|
||||
function M.chain_commands(commands)
|
||||
local separator = M.is_windows() and ' | ' or is_nushell() and ';' or ' && '
|
||||
local ret = ''
|
||||
|
||||
for i, value in ipairs(commands) do
|
||||
local is_last = i == #commands
|
||||
ret = ret .. value
|
||||
|
||||
if not is_last then
|
||||
ret = ret .. separator
|
||||
end
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
---Create a `cd` command for the path
|
||||
---@param path string
|
||||
---@return string
|
||||
function M.make_cd_command(path)
|
||||
return ('cd "%s"'):format(path)
|
||||
end
|
||||
|
||||
---@param command string
|
||||
---@param args string[]
|
||||
---@return string command
|
||||
function M.make_command_from_args(command, args)
|
||||
local ret = command .. ' '
|
||||
|
||||
for _, value in ipairs(args) do
|
||||
ret = ret .. value .. ' '
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,55 @@
|
||||
local M = {}
|
||||
|
||||
---@param file_name string
|
||||
---@param output string
|
||||
---@return rustaceanvim.Diagnostic[]
|
||||
---@diagnostic disable-next-line: inject-field
|
||||
M.parse_diagnostics = function(file_name, output)
|
||||
output = output:gsub('\r\n', '\n')
|
||||
local lines = vim.split(output, '\n')
|
||||
---@type rustaceanvim.Diagnostic[]
|
||||
local diagnostics = {}
|
||||
for i, line in ipairs(lines) do
|
||||
local message = ''
|
||||
local test_id, file, lnum, col = line:match("thread '([^']+)' panicked at ([^:]+):(%d+):(%d+):")
|
||||
if lnum and col and message and vim.endswith(file_name, file) then
|
||||
local next_i = i + 1
|
||||
while #lines >= next_i and lines[next_i] ~= '' do
|
||||
message = message .. lines[next_i] .. '\n'
|
||||
next_i = next_i + 1
|
||||
end
|
||||
---@type rustaceanvim.Diagnostic
|
||||
local diagnostic = {
|
||||
test_id = test_id,
|
||||
lnum = tonumber(lnum) - 1,
|
||||
col = tonumber(col) or 0,
|
||||
message = message,
|
||||
source = 'rustaceanvim',
|
||||
severity = vim.diagnostic.severity.ERROR,
|
||||
}
|
||||
table.insert(diagnostics, diagnostic)
|
||||
end
|
||||
end
|
||||
if #diagnostics == 0 then
|
||||
--- Fall back to old format
|
||||
for test_id, message, file, lnum, col in
|
||||
output:gmatch("thread '([^']+)' panicked at '([^']+)', ([^:]+):(%d+):(%d+)")
|
||||
do
|
||||
if vim.endswith(file_name, file) then
|
||||
---@type rustaceanvim.Diagnostic
|
||||
local diagnostic = {
|
||||
test_id = test_id,
|
||||
lnum = tonumber(lnum) - 1,
|
||||
col = tonumber(col) or 0,
|
||||
message = message,
|
||||
source = 'rustaceanvim',
|
||||
severity = vim.diagnostic.severity.ERROR,
|
||||
}
|
||||
table.insert(diagnostics, diagnostic)
|
||||
end
|
||||
end
|
||||
end
|
||||
return diagnostics
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,21 @@
|
||||
---@alias lsp_position { character: integer, line: integer }
|
||||
---@alias lsp_range { start: lsp_position, end: lsp_position }
|
||||
---@alias lsp_text_document { uri: string }
|
||||
---@alias lsp_range_params { textDocument: lsp_text_document, range: lsp_range }
|
||||
---@alias lsp_position_params { textDocument: lsp_text_document, position: lsp_position }
|
||||
|
||||
local M = {}
|
||||
|
||||
---Evaluate a value that may be a function
|
||||
---or an evaluated value
|
||||
---@generic T
|
||||
---@param value (fun():T)|T
|
||||
---@return T
|
||||
M.evaluate = function(value)
|
||||
if type(value) == 'function' then
|
||||
return value()
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,36 @@
|
||||
local M = {}
|
||||
|
||||
---@param bufnr integer | nil
|
||||
function M.delete_buf(bufnr)
|
||||
if bufnr ~= nil and vim.api.nvim_buf_is_valid(bufnr) then
|
||||
vim.api.nvim_buf_delete(bufnr, { force = true })
|
||||
end
|
||||
end
|
||||
|
||||
---@param winnr integer | nil
|
||||
function M.close_win(winnr)
|
||||
if winnr ~= nil and vim.api.nvim_win_is_valid(winnr) then
|
||||
vim.api.nvim_win_close(winnr, true)
|
||||
end
|
||||
end
|
||||
|
||||
---@param vertical boolean
|
||||
---@param bufnr integer
|
||||
function M.split(vertical, bufnr)
|
||||
local cmd = vertical and 'vsplit' or 'split'
|
||||
|
||||
vim.cmd(cmd)
|
||||
local win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(win, bufnr)
|
||||
end
|
||||
|
||||
---@param vertical boolean
|
||||
---@param amount string
|
||||
function M.resize(vertical, amount)
|
||||
local cmd = vertical and 'vertical resize ' or 'resize'
|
||||
cmd = cmd .. amount
|
||||
|
||||
vim.cmd(cmd)
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user