1

Regenerate nvim config

This commit is contained in:
2024-06-02 03:29:20 +02:00
parent 75eea0c030
commit ef2e28883d
5576 changed files with 604886 additions and 503 deletions

View File

@ -0,0 +1,54 @@
==============================================================================
mason-lspconfig troubleshooting *rustaceanvim.mason*
This plugin supports automatically detecting mason.nvim codelldb installations,
but not rust-analyzer.
The main reason for this choice is that it mason.nvim installations of rust-analyzer
will most likely have been built with a different toolchain than your project,
leading to inconsistencies and possibly subtle bugs.
If you want to use a mason.nvim installation anyway, you can do so by specifying
the `server.cmd` setting (see |rustaceanvim.config| and |RustaceanLspClientOpts|):
>lua
vim.g.rustaceanvim = {
server = {
cmd = function()
local mason_registry = require('mason-registry')
local ra_binary = mason_registry.is_installed('rust-analyzer')
-- This may need to be tweaked, depending on the operating system.
and mason_registry.get_package('rust-analyzer'):get_install_path() .. "/rust-analyzer"
or "rust-analyzer"
return { ra_binary } -- You can add args to the list, such as '--log-file'
end,
},
}
<
Note that mason-lspconfig.nvim, when configured to ensure rust-analyzer is installed,
assumes you are using the `nvim-lspconfig.rust_analyzer` client.
Some Neovim distributions will automatically call the client's `setup`
function, resulting in a conflict with this plugin.
General approach to prevent mason-lspconfig from setting up
`lspconfig.rust_analyzer`:
>lua
require('mason-lspconfig').setup_handlers {
['rust_analyzer'] = function() end,
}
<
Using LazyVim:
>lua
{
'neovim/nvim-lspconfig',
opts = {
setup = {
rust_analyzer = function()
return true
end,
},
},
}
<
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,409 @@
==============================================================================
Table of Contents *rustaceanvim.contents*
Introduction ··························································· |intro|
································································ |rustaceanvim|
plugin configuration ····································· |rustaceanvim.config|
LSP configuration utility ························· |rustaceanvim.config.server|
························································ |rustaceanvim.neotest|
==============================================================================
Introduction *intro*
This plugin automatically configures the `rust-analyzer` builtin LSP client
and integrates with other rust tools.
==============================================================================
*rustaceanvim*
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.
==============================================================================
plugin configuration *rustaceanvim.config*
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.
RustaceanOpts *RustaceanOpts*
Fields: ~
{tools?} (RustaceanToolsOpts) Plugin options
{server?} (RustaceanLspClientOpts) Language server client options
{dap?} (RustaceanDapOpts) Debug adapter options
RustaceanToolsOpts *RustaceanToolsOpts*
Fields: ~
{executor?} (RustaceanExecutor|executor_alias) The executor to use for runnables/debuggables
{test_executor?} (RustaceanExecutor|test_executor_alias) The executor to use for runnables that are tests / testables
{crate_test_executor?} (RustaceanExecutor|test_executor_alias) The executor to use for runnables that are crate test suites (--all-targets)
{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'.
{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.
{enable_clippy?} (boolean) Whether to enable clippy checks on save if a clippy installation is detected. Default: `true`
{on_initialized?} (fun(health:RustAnalyzerInitializedStatus)) Function that is invoked when the LSP server has finished initializing
{reload_workspace_from_cargo_toml?} (boolean) Automatically call `RustReloadWorkspace` when writing to a Cargo.toml file
{hover_actions?} (RustaceanHoverActionsOpts) Options for hover actions
{code_actions?} (RustaceanCodeActionOpts) Options for code actions
{float_win_config?} (FloatWinConfig) Options applied to floating windows. See |api-win_config|.
{create_graph?} (RustaceanCrateGraphConfig) Options for showing the crate graph based on graphviz and the dot
{open_url?} (fun(url:string):nil) If set, overrides how to open URLs
{rustc?} (RustcOpts) Options for `rustc`
RustaceanExecutor *RustaceanExecutor*
Fields: ~
{execute_command} (fun(cmd:string,args:string[],cwd:string|nil,opts?:RustaceanExecutorOpts))
RustaceanExecutorOpts *RustaceanExecutorOpts*
Fields: ~
{bufnr?} (integer) The buffer from which the executor was invoked.
FloatWinConfig *FloatWinConfig*
Fields: ~
{auto_focus?} (boolean)
{open_split?} ("horizontal"|"vertical")
See: ~
|vim.lsp.util.open_floating_preview.Opts|
|vim.api.nvim_open_win|
executor_alias *executor_alias*
Type: ~
"termopen"|"quickfix"|"toggleterm"|"vimux"|"neotest"
test_executor_alias *test_executor_alias*
Type: ~
executor_alias|"background"
RustaceanHoverActionsOpts *RustaceanHoverActionsOpts*
Fields: ~
{replace_builtin_hover?} (boolean) Whether to replace Neovim's built-in `vim.lsp.buf.hover` with hover actions. Default: `true`
RustaceanCodeActionOpts *RustaceanCodeActionOpts*
Fields: ~
{group_icon?} (string) Text appended to a group action
{ui_select_fallback?} (boolean) Whether to fall back to `vim.ui.select` if there are no grouped code actions. Default: `false`
lsp_server_health_status *lsp_server_health_status*
Type: ~
"ok"|"warning"|"error"
RustAnalyzerInitializedStatus *RustAnalyzerInitializedStatus*
Fields: ~
{health} (lsp_server_health_status)
RustaceanCrateGraphConfig *RustaceanCrateGraphConfig*
Fields: ~
{backend?} (string) Backend used for displaying the graph. See: https://graphviz.org/docs/outputs/ Defaults to `"x11"` if unset.
{output?} (string) Where to store the output. No output if unset. Relative path from `cwd`.
{enabled_graphviz_backends?} (string[]) Override the enabled graphviz backends list, used for input validation and autocompletion.
{pipe?} (string) Overide the pipe symbol in the shell command. Useful if using a shell that is not supported by this plugin.
RustcOpts *RustcOpts*
Fields: ~
{edition} (string) The edition to use. See https://rustc-dev-guide.rust-lang.org/guides/editions.html. Default '2021'.
RustaceanLspClientOpts *RustaceanLspClientOpts*
Fields: ~
{auto_attach?} (boolean|fun(bufnr:integer):boolean) Whether to automatically attach the LSP client. Defaults to `true` if the `rust-analyzer` executable is found.
{cmd?} (string[]|fun():string[]) Command and arguments for starting rust-analyzer
{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.
{standalone?} (boolean) Standalone file support (enabled by default). Disabling it may improve rust-analyzer's startup time.
{logfile?} (string) The path to the rust-analyzer log file.
{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
RustaceanDapOpts *RustaceanDapOpts*
Fields: ~
{adapter?} (DapExecutableConfig|DapServerConfig|disable|fun():DapExecutableConfig|DapServerConfig|disable) @field autoload_configurations boolean Whether to autoload nvim-dap configurations when rust-analyzer has attached? Default: `true`.
disable *disable*
Type: ~
false
DapCommand *DapCommand*
Type: ~
string
DapExecutableConfig *DapExecutableConfig*
Fields: ~
{type} (dap_adapter_type_executable) The type of debug adapter.
{command} (string) Default: `"lldb-vscode"`.
{args?} (string) Default: unset.
{name?} (string) Default: `"lldb"`.
DapServerConfig *DapServerConfig*
Fields: ~
{type} (dap_adapter_type_server) The type of debug adapter.
{host?} (string) The host to connect to.
{port} (string) The port to connect to.
{executable} (DapExecutable) The executable to run
{name?} (string)
DapExecutable *DapExecutable*
Fields: ~
{command} (string) The executable.
{args} (string[]) Its arguments.
dap_adapter_type_executable *dap_adapter_type_executable*
Type: ~
dap_adapter_type_server *dap_adapter_type_server*
Type: ~
DapClientConfig : Configuration *DapClientConfig*
Fields: ~
{type} (string) The dap adapter to use
{name} (string)
{request} (dap_config_request_launch|dap_config_request_attach|dap_config_request_custom) The type of dap session
{cwd?} (string) Current working directory
{program?} (string) Path to executable for most DAP clients
{args?} (string[]) Optional args to DAP client, not valid for all client types
{env?} (EnvironmentMap) Environmental variables
{initCommands?} (string[]) Initial commands to run, `lldb` clients only
{coreConfigs?} (table) Essential config values for `probe-rs` client, see https://probe.rs/docs/tools/debugger/
EnvironmentMap *EnvironmentMap*
Type: ~
table<string,string[]>
dap_config_request_launch *dap_config_request_launch*
Type: ~
dap_config_request_attach *dap_config_request_attach*
Type: ~
dap_config_request_custom *dap_config_request_custom*
Type: ~
*M.get_codelldb_adapter*
M.get_codelldb_adapter({codelldb_path}, {liblldb_path})
For the heroes who want to use it.
Parameters: ~
{codelldb_path} (string) Path to the codelldb executable
{liblldb_path} (string) Path to the liblldb dynamic library
Returns: ~
(DapServerConfig)
==============================================================================
LSP configuration utility *rustaceanvim.config.server*
LoadRASettingsOpts *LoadRASettingsOpts*
Fields: ~
{settings_file_pattern} (string|nil) File name or pattern to search for. Defaults to 'rust-analyzer.json'
{default_settings} (table|nil) Default settings to merge the loaded settings into
*server.load_rust_analyzer_settings*
server.load_rust_analyzer_settings({project_root}, {opts})
Load rust-analyzer settings from a JSON file,
falling back to the default settings if none is found or if it cannot be decoded.
Parameters: ~
{project_root} (string|nil) The project root
{opts} (LoadRASettingsOpts|nil)
Returns: ~
(table) server_settings
See: ~
|https://rust-analyzer.github.io/manual.html#configuration|
server.create_client_capabilities() *server.create_client_capabilities*
Returns: ~
(lsp.ClientCapabilities)
==============================================================================
*rustaceanvim.neotest*
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).
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,38 @@
DapClientConfig rustaceanvim.txt /*DapClientConfig*
DapCommand rustaceanvim.txt /*DapCommand*
DapExecutable rustaceanvim.txt /*DapExecutable*
DapExecutableConfig rustaceanvim.txt /*DapExecutableConfig*
DapServerConfig rustaceanvim.txt /*DapServerConfig*
EnvironmentMap rustaceanvim.txt /*EnvironmentMap*
FloatWinConfig rustaceanvim.txt /*FloatWinConfig*
LoadRASettingsOpts rustaceanvim.txt /*LoadRASettingsOpts*
M.get_codelldb_adapter rustaceanvim.txt /*M.get_codelldb_adapter*
RustAnalyzerInitializedStatus rustaceanvim.txt /*RustAnalyzerInitializedStatus*
RustaceanCodeActionOpts rustaceanvim.txt /*RustaceanCodeActionOpts*
RustaceanCrateGraphConfig rustaceanvim.txt /*RustaceanCrateGraphConfig*
RustaceanDapOpts rustaceanvim.txt /*RustaceanDapOpts*
RustaceanExecutor rustaceanvim.txt /*RustaceanExecutor*
RustaceanExecutorOpts rustaceanvim.txt /*RustaceanExecutorOpts*
RustaceanHoverActionsOpts rustaceanvim.txt /*RustaceanHoverActionsOpts*
RustaceanLspClientOpts rustaceanvim.txt /*RustaceanLspClientOpts*
RustaceanOpts rustaceanvim.txt /*RustaceanOpts*
RustaceanToolsOpts rustaceanvim.txt /*RustaceanToolsOpts*
RustcOpts rustaceanvim.txt /*RustcOpts*
dap_adapter_type_executable rustaceanvim.txt /*dap_adapter_type_executable*
dap_adapter_type_server rustaceanvim.txt /*dap_adapter_type_server*
dap_config_request_attach rustaceanvim.txt /*dap_config_request_attach*
dap_config_request_custom rustaceanvim.txt /*dap_config_request_custom*
dap_config_request_launch rustaceanvim.txt /*dap_config_request_launch*
disable rustaceanvim.txt /*disable*
executor_alias rustaceanvim.txt /*executor_alias*
intro rustaceanvim.txt /*intro*
lsp_server_health_status rustaceanvim.txt /*lsp_server_health_status*
rustaceanvim rustaceanvim.txt /*rustaceanvim*
rustaceanvim.config rustaceanvim.txt /*rustaceanvim.config*
rustaceanvim.config.server rustaceanvim.txt /*rustaceanvim.config.server*
rustaceanvim.contents rustaceanvim.txt /*rustaceanvim.contents*
rustaceanvim.mason mason.txt /*rustaceanvim.mason*
rustaceanvim.neotest rustaceanvim.txt /*rustaceanvim.neotest*
server.create_client_capabilities rustaceanvim.txt /*server.create_client_capabilities*
server.load_rust_analyzer_settings rustaceanvim.txt /*server.load_rust_analyzer_settings*
test_executor_alias rustaceanvim.txt /*test_executor_alias*

View File

@ -0,0 +1,64 @@
---@type RustaceanConfig
local config = require('rustaceanvim.config.internal')
if not vim.g.loaded_rustaceanvim then
require('rustaceanvim.config.check').check_for_lspconfig_conflict(vim.schedule_wrap(function(warn)
vim.notify_once(warn, vim.log.levels.WARN)
end))
vim.lsp.commands['rust-analyzer.runSingle'] = function(command)
local runnables = require('rustaceanvim.runnables')
local cached_commands = require('rustaceanvim.cached_commands')
---@type RARunnable[]
local ra_runnables = command.arguments
local runnable = ra_runnables[1]
local cargo_args = runnable.args.cargoArgs
if #cargo_args > 0 and vim.startswith(cargo_args[1], 'test') then
cached_commands.set_last_testable(1, ra_runnables)
end
cached_commands.set_last_runnable(1, ra_runnables)
runnables.run_command(1, ra_runnables)
end
vim.lsp.commands['rust-analyzer.gotoLocation'] = function(command, ctx)
local client = vim.lsp.get_client_by_id(ctx.client_id)
if client then
vim.lsp.util.jump_to_location(command.arguments[1], client.offset_encoding)
end
end
vim.lsp.commands['rust-analyzer.showReferences'] = function(_)
vim.lsp.buf.implementation()
end
vim.lsp.commands['rust-analyzer.debugSingle'] = function(command)
local overrides = require('rustaceanvim.overrides')
local args = command.arguments[1].args
overrides.sanitize_command_for_debugging(args.cargoArgs)
local cached_commands = require('rustaceanvim.cached_commands')
cached_commands.set_last_debuggable(args)
local rt_dap = require('rustaceanvim.dap')
rt_dap.start(args)
end
local commands = require('rustaceanvim.commands')
commands.create_rustc_command()
end
vim.g.loaded_rustaceanvim = true
local bufnr = vim.api.nvim_get_current_buf()
local auto_attach = config.server.auto_attach
if type(auto_attach) == 'function' then
auto_attach = auto_attach(bufnr)
end
if auto_attach then
-- Defer for a smoother experience on low-end devices
vim.api.nvim_create_autocmd('BufEnter', {
buffer = bufnr,
group = vim.api.nvim_create_augroup('RustaceanvimAttach', { clear = true }),
callback = function()
require('rustaceanvim.lsp').start()
end,
})
end

View File

@ -0,0 +1,26 @@
" Copied from built-in compiler/{rustc,cargo}.vim
setlocal errorformat=
\%f:%l:%c:\ %t%*[^:]:\ %m,
\%f:%l:%c:\ %*\\d:%*\\d\ %t%*[^:]:\ %m,
\%-G%f:%l\ %s,
\%-G%*[\ ]^,
\%-G%*[\ ]^%*[~],
\%-G%*[\ ]...
" New errorformat (after nightly 2016/08/10)
setlocal errorformat+=
\%-G,
\%-Gerror:\ aborting\ %.%#,
\%-Gerror:\ Could\ not\ compile\ %.%#,
\%Eerror:\ %m,
\%Eerror[E%n]:\ %m,
\%Wwarning:\ %m,
\%Inote:\ %m,
\%C\ %#-->\ %f:%l:%c
setlocal errorformat+=
\%-G%\\s%#Downloading%.%#,
\%-G%\\s%#Compiling%.%#,
\%-G%\\s%#Finished%.%#,
\%-G%\\s%#error:\ Could\ not\ compile\ %.%#,
\%-G%\\s%#To\ learn\ more\\,%.%#

View File

@ -0,0 +1,24 @@
local fname = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ':t')
if fname ~= 'Cargo.toml' then
return
end
local config = require('rustaceanvim.config.internal')
local ra = require('rustaceanvim.rust_analyzer')
if config.tools.reload_workspace_from_cargo_toml then
local group = vim.api.nvim_create_augroup('RustaceanCargoReloadWorkspace', { clear = false })
local bufnr = vim.api.nvim_get_current_buf()
vim.api.nvim_clear_autocmds {
buffer = bufnr,
group = group,
}
vim.api.nvim_create_autocmd('BufWritePost', {
buffer = vim.api.nvim_get_current_buf(),
group = group,
callback = function()
if #ra.get_active_rustaceanvim_clients(nil) > 0 then
vim.cmd.RustLsp { 'reloadWorkspace', mods = { silent = true } }
end
end,
})
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
/nix/store/mqbhz05llkddfb5wni0m48kw22ixxps4-lua-5.1.5

View File

@ -0,0 +1,78 @@
rock_manifest = {
doc = {
["mason.txt"] = "858adffa88b1649b91187f49f191e376",
["rustaceanvim.txt"] = "0bb03192be9a68d7b6c73e8414b01625"
},
ftplugin = {
["rust.lua"] = "40f5f67ffb8f441a636ff231b7878ff8",
["rust.vim"] = "3121ffa3b7f1aa9c3efbfe32e438dbeb",
["toml.lua"] = "a8807dcfb5f6687ac543f4f8da3a9554"
},
lua = {
rustaceanvim = {
["cached_commands.lua"] = "6e23c8231f44a3edc76be54766fca334",
["cargo.lua"] = "39415eb623c1376d5f2d5c717b8c0eb0",
commands = {
["code_action_group.lua"] = "55bf0c83a007e5d64924eafe2e2a96eb",
["crate_graph.lua"] = "30d01abc92dff63ace83a5d1697d2073",
["debuggables.lua"] = "6548c5c600f14c9dc3d10e87983f6fb4",
["diagnostic.lua"] = "55160b50ac49bcab96e15176fc5c7dde",
["expand_macro.lua"] = "0a5991437c93aa39ace91d31dfd5d041",
["external_docs.lua"] = "1d2fa5d88ea197ad9976c23b0ff70763",
["fly_check.lua"] = "3f7b89700bd8f98fd5ab0ec02cce1cdd",
["hover_range.lua"] = "68828cfd0c465bbe0915996795ff7b92",
["init.lua"] = "6d7415da1c6e12b7b7914ab4e0339fae",
["join_lines.lua"] = "f31a68f1ddac01b926d58ccd249bc03d",
["move_item.lua"] = "f05118ba148e66c1e92882141e68343f",
["open_cargo_toml.lua"] = "2368251db4ce52659693acc8eee15b74",
["parent_module.lua"] = "c274b7357e35825f75802dc96e609992",
["rebuild_proc_macros.lua"] = "6113336c3b82042f60c54446e6c5a887",
["rustc_unpretty.lua"] = "fafde14d3d482667cecce2981444cad2",
["ssr.lua"] = "1c936709630f09b2a81d22452d3e3fa2",
["syntax_tree.lua"] = "c28fd8b22608e3f7bd3bdbb2e10c1617",
["view_ir.lua"] = "12eadc00a2ca1005bcbeda4fa8fc80a0",
["workspace_refresh.lua"] = "083b2d70533d0f417b65d2b7866f531b",
["workspace_symbol.lua"] = "69bdc6d1255f7eb586ea8e350b840b85"
},
["compat.lua"] = "fedaef1aeaceaf086116ed62b423d0a3",
config = {
["check.lua"] = "8a68a6e78ea97ab59ea0be83bff7ffc7",
["init.lua"] = "f2ef97efc1f563c06932d51006e5b7cc",
["internal.lua"] = "319f08833432f1ec7b04d4432737a1f0",
["json.lua"] = "a8c7caf443ad9ce1193726aec053cc8d",
["server.lua"] = "d642a65f31cb4f12b812840c1171821f"
},
["dap.lua"] = "757ecf822f6c30ebf9e462f7009ce99a",
executors = {
["background.lua"] = "ae1c76a5c2f73e9c58456f580b7552b7",
["init.lua"] = "a443891fc3b974db6787502179d85c19",
["meta.lua"] = "99cae6c6b77725710ec2fc7d86bff665",
["neotest.lua"] = "64a17e71f15a1741f0e7f835472f26e3",
["quickfix.lua"] = "b1ea55a261b8623e73c2fd6aed278fd3",
["termopen.lua"] = "4a225919ab4e1ad67fc591a9947d86e9",
["toggleterm.lua"] = "221616f53cc50124694fcc1b0f1c92a2",
["vimux.lua"] = "24d3f692083ca86f0926e7aa4722c0fe"
},
["health.lua"] = "126952700269c9ab976f29f59604c5da",
["hover_actions.lua"] = "d0842822fa99c55b4cc5a65eae968ff2",
["init.lua"] = "a48de2e7e937208adc6781a5ea35ec88",
["lsp.lua"] = "f26e69489b384870c2774703599bf1bb",
neotest = {
["init.lua"] = "10bfd8f06b7ad2de8d706c2e26505ae7",
["trans.lua"] = "1f79320bca0c81d91673543dd7ad4984"
},
["os.lua"] = "4c8caca1afb6a8e7862300b4dcc29f09",
["overrides.lua"] = "177b171bfb8c16c19326d6402b94a535",
["runnables.lua"] = "d458cb318b2cd900e178930d3b14f224",
["rust_analyzer.lua"] = "aa3bce32849886001dc2cb38a4ff951e",
["server_status.lua"] = "25017ef9b15fadd713574c897e242970",
["shell.lua"] = "561e4be6d5ba43f93a00ad5e6169e0eb",
["test.lua"] = "e436ae539217729f46a1d556974e1100",
types = {
["internal.lua"] = "95d9ff5763390a3efae83cc27dbef452"
},
["ui.lua"] = "185cbb4ef9b15b4feb6699fcbd5d4435"
}
},
["rustaceanvim-4.22.8-1.rockspec"] = "1d483e8dffa0275323da043a1e8c9cf3"
}

View File

@ -0,0 +1,241 @@
commands = {}
dependencies = {
rustaceanvim = {
["4.22.8-1"] = {
{
constraints = {
{
op = ">=",
version = {
5, 1, string = "5.1"
}
}
},
name = "lua"
}
}
}
}
modules = {
["rustaceanvim.cached_commands"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.cargo"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.commands.code_action_group"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.commands.crate_graph"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.commands.debuggables"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.commands.diagnostic"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.commands.expand_macro"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.commands.external_docs"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.commands.fly_check"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.commands.hover_range"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.commands.init"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.commands.join_lines"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.commands.move_item"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.commands.open_cargo_toml"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.commands.parent_module"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.commands.rebuild_proc_macros"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.commands.rustc_unpretty"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.commands.ssr"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.commands.syntax_tree"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.commands.view_ir"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.commands.workspace_refresh"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.commands.workspace_symbol"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.compat"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.config.check"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.config.init"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.config.internal"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.config.json"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.config.server"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.dap"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.executors.background"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.executors.init"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.executors.meta"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.executors.neotest"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.executors.quickfix"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.executors.termopen"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.executors.toggleterm"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.executors.vimux"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.health"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.hover_actions"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.init"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.lsp"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.neotest.init"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.neotest.trans"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.os"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.overrides"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.runnables"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.rust_analyzer"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.server_status"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.shell"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.test"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.types.internal"] = {
"rustaceanvim/4.22.8-1"
},
["rustaceanvim.ui"] = {
"rustaceanvim/4.22.8-1"
}
}
repository = {
rustaceanvim = {
["4.22.8-1"] = {
{
arch = "installed",
commands = {},
dependencies = {},
modules = {
["rustaceanvim.cached_commands"] = "rustaceanvim/cached_commands.lua",
["rustaceanvim.cargo"] = "rustaceanvim/cargo.lua",
["rustaceanvim.commands.code_action_group"] = "rustaceanvim/commands/code_action_group.lua",
["rustaceanvim.commands.crate_graph"] = "rustaceanvim/commands/crate_graph.lua",
["rustaceanvim.commands.debuggables"] = "rustaceanvim/commands/debuggables.lua",
["rustaceanvim.commands.diagnostic"] = "rustaceanvim/commands/diagnostic.lua",
["rustaceanvim.commands.expand_macro"] = "rustaceanvim/commands/expand_macro.lua",
["rustaceanvim.commands.external_docs"] = "rustaceanvim/commands/external_docs.lua",
["rustaceanvim.commands.fly_check"] = "rustaceanvim/commands/fly_check.lua",
["rustaceanvim.commands.hover_range"] = "rustaceanvim/commands/hover_range.lua",
["rustaceanvim.commands.init"] = "rustaceanvim/commands/init.lua",
["rustaceanvim.commands.join_lines"] = "rustaceanvim/commands/join_lines.lua",
["rustaceanvim.commands.move_item"] = "rustaceanvim/commands/move_item.lua",
["rustaceanvim.commands.open_cargo_toml"] = "rustaceanvim/commands/open_cargo_toml.lua",
["rustaceanvim.commands.parent_module"] = "rustaceanvim/commands/parent_module.lua",
["rustaceanvim.commands.rebuild_proc_macros"] = "rustaceanvim/commands/rebuild_proc_macros.lua",
["rustaceanvim.commands.rustc_unpretty"] = "rustaceanvim/commands/rustc_unpretty.lua",
["rustaceanvim.commands.ssr"] = "rustaceanvim/commands/ssr.lua",
["rustaceanvim.commands.syntax_tree"] = "rustaceanvim/commands/syntax_tree.lua",
["rustaceanvim.commands.view_ir"] = "rustaceanvim/commands/view_ir.lua",
["rustaceanvim.commands.workspace_refresh"] = "rustaceanvim/commands/workspace_refresh.lua",
["rustaceanvim.commands.workspace_symbol"] = "rustaceanvim/commands/workspace_symbol.lua",
["rustaceanvim.compat"] = "rustaceanvim/compat.lua",
["rustaceanvim.config.check"] = "rustaceanvim/config/check.lua",
["rustaceanvim.config.init"] = "rustaceanvim/config/init.lua",
["rustaceanvim.config.internal"] = "rustaceanvim/config/internal.lua",
["rustaceanvim.config.json"] = "rustaceanvim/config/json.lua",
["rustaceanvim.config.server"] = "rustaceanvim/config/server.lua",
["rustaceanvim.dap"] = "rustaceanvim/dap.lua",
["rustaceanvim.executors.background"] = "rustaceanvim/executors/background.lua",
["rustaceanvim.executors.init"] = "rustaceanvim/executors/init.lua",
["rustaceanvim.executors.meta"] = "rustaceanvim/executors/meta.lua",
["rustaceanvim.executors.neotest"] = "rustaceanvim/executors/neotest.lua",
["rustaceanvim.executors.quickfix"] = "rustaceanvim/executors/quickfix.lua",
["rustaceanvim.executors.termopen"] = "rustaceanvim/executors/termopen.lua",
["rustaceanvim.executors.toggleterm"] = "rustaceanvim/executors/toggleterm.lua",
["rustaceanvim.executors.vimux"] = "rustaceanvim/executors/vimux.lua",
["rustaceanvim.health"] = "rustaceanvim/health.lua",
["rustaceanvim.hover_actions"] = "rustaceanvim/hover_actions.lua",
["rustaceanvim.init"] = "rustaceanvim/init.lua",
["rustaceanvim.lsp"] = "rustaceanvim/lsp.lua",
["rustaceanvim.neotest.init"] = "rustaceanvim/neotest/init.lua",
["rustaceanvim.neotest.trans"] = "rustaceanvim/neotest/trans.lua",
["rustaceanvim.os"] = "rustaceanvim/os.lua",
["rustaceanvim.overrides"] = "rustaceanvim/overrides.lua",
["rustaceanvim.runnables"] = "rustaceanvim/runnables.lua",
["rustaceanvim.rust_analyzer"] = "rustaceanvim/rust_analyzer.lua",
["rustaceanvim.server_status"] = "rustaceanvim/server_status.lua",
["rustaceanvim.shell"] = "rustaceanvim/shell.lua",
["rustaceanvim.test"] = "rustaceanvim/test.lua",
["rustaceanvim.types.internal"] = "rustaceanvim/types/internal.lua",
["rustaceanvim.ui"] = "rustaceanvim/ui.lua"
}
}
}
}
}

View File

@ -0,0 +1,54 @@
==============================================================================
mason-lspconfig troubleshooting *rustaceanvim.mason*
This plugin supports automatically detecting mason.nvim codelldb installations,
but not rust-analyzer.
The main reason for this choice is that it mason.nvim installations of rust-analyzer
will most likely have been built with a different toolchain than your project,
leading to inconsistencies and possibly subtle bugs.
If you want to use a mason.nvim installation anyway, you can do so by specifying
the `server.cmd` setting (see |rustaceanvim.config| and |RustaceanLspClientOpts|):
>lua
vim.g.rustaceanvim = {
server = {
cmd = function()
local mason_registry = require('mason-registry')
local ra_binary = mason_registry.is_installed('rust-analyzer')
-- This may need to be tweaked, depending on the operating system.
and mason_registry.get_package('rust-analyzer'):get_install_path() .. "/rust-analyzer"
or "rust-analyzer"
return { ra_binary } -- You can add args to the list, such as '--log-file'
end,
},
}
<
Note that mason-lspconfig.nvim, when configured to ensure rust-analyzer is installed,
assumes you are using the `nvim-lspconfig.rust_analyzer` client.
Some Neovim distributions will automatically call the client's `setup`
function, resulting in a conflict with this plugin.
General approach to prevent mason-lspconfig from setting up
`lspconfig.rust_analyzer`:
>lua
require('mason-lspconfig').setup_handlers {
['rust_analyzer'] = function() end,
}
<
Using LazyVim:
>lua
{
'neovim/nvim-lspconfig',
opts = {
setup = {
rust_analyzer = function()
return true
end,
},
},
}
<
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,409 @@
==============================================================================
Table of Contents *rustaceanvim.contents*
Introduction ··························································· |intro|
································································ |rustaceanvim|
plugin configuration ····································· |rustaceanvim.config|
LSP configuration utility ························· |rustaceanvim.config.server|
························································ |rustaceanvim.neotest|
==============================================================================
Introduction *intro*
This plugin automatically configures the `rust-analyzer` builtin LSP client
and integrates with other rust tools.
==============================================================================
*rustaceanvim*
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.
==============================================================================
plugin configuration *rustaceanvim.config*
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.
RustaceanOpts *RustaceanOpts*
Fields: ~
{tools?} (RustaceanToolsOpts) Plugin options
{server?} (RustaceanLspClientOpts) Language server client options
{dap?} (RustaceanDapOpts) Debug adapter options
RustaceanToolsOpts *RustaceanToolsOpts*
Fields: ~
{executor?} (RustaceanExecutor|executor_alias) The executor to use for runnables/debuggables
{test_executor?} (RustaceanExecutor|test_executor_alias) The executor to use for runnables that are tests / testables
{crate_test_executor?} (RustaceanExecutor|test_executor_alias) The executor to use for runnables that are crate test suites (--all-targets)
{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'.
{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.
{enable_clippy?} (boolean) Whether to enable clippy checks on save if a clippy installation is detected. Default: `true`
{on_initialized?} (fun(health:RustAnalyzerInitializedStatus)) Function that is invoked when the LSP server has finished initializing
{reload_workspace_from_cargo_toml?} (boolean) Automatically call `RustReloadWorkspace` when writing to a Cargo.toml file
{hover_actions?} (RustaceanHoverActionsOpts) Options for hover actions
{code_actions?} (RustaceanCodeActionOpts) Options for code actions
{float_win_config?} (FloatWinConfig) Options applied to floating windows. See |api-win_config|.
{create_graph?} (RustaceanCrateGraphConfig) Options for showing the crate graph based on graphviz and the dot
{open_url?} (fun(url:string):nil) If set, overrides how to open URLs
{rustc?} (RustcOpts) Options for `rustc`
RustaceanExecutor *RustaceanExecutor*
Fields: ~
{execute_command} (fun(cmd:string,args:string[],cwd:string|nil,opts?:RustaceanExecutorOpts))
RustaceanExecutorOpts *RustaceanExecutorOpts*
Fields: ~
{bufnr?} (integer) The buffer from which the executor was invoked.
FloatWinConfig *FloatWinConfig*
Fields: ~
{auto_focus?} (boolean)
{open_split?} ("horizontal"|"vertical")
See: ~
|vim.lsp.util.open_floating_preview.Opts|
|vim.api.nvim_open_win|
executor_alias *executor_alias*
Type: ~
"termopen"|"quickfix"|"toggleterm"|"vimux"|"neotest"
test_executor_alias *test_executor_alias*
Type: ~
executor_alias|"background"
RustaceanHoverActionsOpts *RustaceanHoverActionsOpts*
Fields: ~
{replace_builtin_hover?} (boolean) Whether to replace Neovim's built-in `vim.lsp.buf.hover` with hover actions. Default: `true`
RustaceanCodeActionOpts *RustaceanCodeActionOpts*
Fields: ~
{group_icon?} (string) Text appended to a group action
{ui_select_fallback?} (boolean) Whether to fall back to `vim.ui.select` if there are no grouped code actions. Default: `false`
lsp_server_health_status *lsp_server_health_status*
Type: ~
"ok"|"warning"|"error"
RustAnalyzerInitializedStatus *RustAnalyzerInitializedStatus*
Fields: ~
{health} (lsp_server_health_status)
RustaceanCrateGraphConfig *RustaceanCrateGraphConfig*
Fields: ~
{backend?} (string) Backend used for displaying the graph. See: https://graphviz.org/docs/outputs/ Defaults to `"x11"` if unset.
{output?} (string) Where to store the output. No output if unset. Relative path from `cwd`.
{enabled_graphviz_backends?} (string[]) Override the enabled graphviz backends list, used for input validation and autocompletion.
{pipe?} (string) Overide the pipe symbol in the shell command. Useful if using a shell that is not supported by this plugin.
RustcOpts *RustcOpts*
Fields: ~
{edition} (string) The edition to use. See https://rustc-dev-guide.rust-lang.org/guides/editions.html. Default '2021'.
RustaceanLspClientOpts *RustaceanLspClientOpts*
Fields: ~
{auto_attach?} (boolean|fun(bufnr:integer):boolean) Whether to automatically attach the LSP client. Defaults to `true` if the `rust-analyzer` executable is found.
{cmd?} (string[]|fun():string[]) Command and arguments for starting rust-analyzer
{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.
{standalone?} (boolean) Standalone file support (enabled by default). Disabling it may improve rust-analyzer's startup time.
{logfile?} (string) The path to the rust-analyzer log file.
{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
RustaceanDapOpts *RustaceanDapOpts*
Fields: ~
{adapter?} (DapExecutableConfig|DapServerConfig|disable|fun():DapExecutableConfig|DapServerConfig|disable) @field autoload_configurations boolean Whether to autoload nvim-dap configurations when rust-analyzer has attached? Default: `true`.
disable *disable*
Type: ~
false
DapCommand *DapCommand*
Type: ~
string
DapExecutableConfig *DapExecutableConfig*
Fields: ~
{type} (dap_adapter_type_executable) The type of debug adapter.
{command} (string) Default: `"lldb-vscode"`.
{args?} (string) Default: unset.
{name?} (string) Default: `"lldb"`.
DapServerConfig *DapServerConfig*
Fields: ~
{type} (dap_adapter_type_server) The type of debug adapter.
{host?} (string) The host to connect to.
{port} (string) The port to connect to.
{executable} (DapExecutable) The executable to run
{name?} (string)
DapExecutable *DapExecutable*
Fields: ~
{command} (string) The executable.
{args} (string[]) Its arguments.
dap_adapter_type_executable *dap_adapter_type_executable*
Type: ~
dap_adapter_type_server *dap_adapter_type_server*
Type: ~
DapClientConfig : Configuration *DapClientConfig*
Fields: ~
{type} (string) The dap adapter to use
{name} (string)
{request} (dap_config_request_launch|dap_config_request_attach|dap_config_request_custom) The type of dap session
{cwd?} (string) Current working directory
{program?} (string) Path to executable for most DAP clients
{args?} (string[]) Optional args to DAP client, not valid for all client types
{env?} (EnvironmentMap) Environmental variables
{initCommands?} (string[]) Initial commands to run, `lldb` clients only
{coreConfigs?} (table) Essential config values for `probe-rs` client, see https://probe.rs/docs/tools/debugger/
EnvironmentMap *EnvironmentMap*
Type: ~
table<string,string[]>
dap_config_request_launch *dap_config_request_launch*
Type: ~
dap_config_request_attach *dap_config_request_attach*
Type: ~
dap_config_request_custom *dap_config_request_custom*
Type: ~
*M.get_codelldb_adapter*
M.get_codelldb_adapter({codelldb_path}, {liblldb_path})
For the heroes who want to use it.
Parameters: ~
{codelldb_path} (string) Path to the codelldb executable
{liblldb_path} (string) Path to the liblldb dynamic library
Returns: ~
(DapServerConfig)
==============================================================================
LSP configuration utility *rustaceanvim.config.server*
LoadRASettingsOpts *LoadRASettingsOpts*
Fields: ~
{settings_file_pattern} (string|nil) File name or pattern to search for. Defaults to 'rust-analyzer.json'
{default_settings} (table|nil) Default settings to merge the loaded settings into
*server.load_rust_analyzer_settings*
server.load_rust_analyzer_settings({project_root}, {opts})
Load rust-analyzer settings from a JSON file,
falling back to the default settings if none is found or if it cannot be decoded.
Parameters: ~
{project_root} (string|nil) The project root
{opts} (LoadRASettingsOpts|nil)
Returns: ~
(table) server_settings
See: ~
|https://rust-analyzer.github.io/manual.html#configuration|
server.create_client_capabilities() *server.create_client_capabilities*
Returns: ~
(lsp.ClientCapabilities)
==============================================================================
*rustaceanvim.neotest*
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).
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,64 @@
---@type RustaceanConfig
local config = require('rustaceanvim.config.internal')
if not vim.g.loaded_rustaceanvim then
require('rustaceanvim.config.check').check_for_lspconfig_conflict(vim.schedule_wrap(function(warn)
vim.notify_once(warn, vim.log.levels.WARN)
end))
vim.lsp.commands['rust-analyzer.runSingle'] = function(command)
local runnables = require('rustaceanvim.runnables')
local cached_commands = require('rustaceanvim.cached_commands')
---@type RARunnable[]
local ra_runnables = command.arguments
local runnable = ra_runnables[1]
local cargo_args = runnable.args.cargoArgs
if #cargo_args > 0 and vim.startswith(cargo_args[1], 'test') then
cached_commands.set_last_testable(1, ra_runnables)
end
cached_commands.set_last_runnable(1, ra_runnables)
runnables.run_command(1, ra_runnables)
end
vim.lsp.commands['rust-analyzer.gotoLocation'] = function(command, ctx)
local client = vim.lsp.get_client_by_id(ctx.client_id)
if client then
vim.lsp.util.jump_to_location(command.arguments[1], client.offset_encoding)
end
end
vim.lsp.commands['rust-analyzer.showReferences'] = function(_)
vim.lsp.buf.implementation()
end
vim.lsp.commands['rust-analyzer.debugSingle'] = function(command)
local overrides = require('rustaceanvim.overrides')
local args = command.arguments[1].args
overrides.sanitize_command_for_debugging(args.cargoArgs)
local cached_commands = require('rustaceanvim.cached_commands')
cached_commands.set_last_debuggable(args)
local rt_dap = require('rustaceanvim.dap')
rt_dap.start(args)
end
local commands = require('rustaceanvim.commands')
commands.create_rustc_command()
end
vim.g.loaded_rustaceanvim = true
local bufnr = vim.api.nvim_get_current_buf()
local auto_attach = config.server.auto_attach
if type(auto_attach) == 'function' then
auto_attach = auto_attach(bufnr)
end
if auto_attach then
-- Defer for a smoother experience on low-end devices
vim.api.nvim_create_autocmd('BufEnter', {
buffer = bufnr,
group = vim.api.nvim_create_augroup('RustaceanvimAttach', { clear = true }),
callback = function()
require('rustaceanvim.lsp').start()
end,
})
end

View File

@ -0,0 +1,26 @@
" Copied from built-in compiler/{rustc,cargo}.vim
setlocal errorformat=
\%f:%l:%c:\ %t%*[^:]:\ %m,
\%f:%l:%c:\ %*\\d:%*\\d\ %t%*[^:]:\ %m,
\%-G%f:%l\ %s,
\%-G%*[\ ]^,
\%-G%*[\ ]^%*[~],
\%-G%*[\ ]...
" New errorformat (after nightly 2016/08/10)
setlocal errorformat+=
\%-G,
\%-Gerror:\ aborting\ %.%#,
\%-Gerror:\ Could\ not\ compile\ %.%#,
\%Eerror:\ %m,
\%Eerror[E%n]:\ %m,
\%Wwarning:\ %m,
\%Inote:\ %m,
\%C\ %#-->\ %f:%l:%c
setlocal errorformat+=
\%-G%\\s%#Downloading%.%#,
\%-G%\\s%#Compiling%.%#,
\%-G%\\s%#Finished%.%#,
\%-G%\\s%#error:\ Could\ not\ compile\ %.%#,
\%-G%\\s%#To\ learn\ more\\,%.%#

View File

@ -0,0 +1,24 @@
local fname = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ':t')
if fname ~= 'Cargo.toml' then
return
end
local config = require('rustaceanvim.config.internal')
local ra = require('rustaceanvim.rust_analyzer')
if config.tools.reload_workspace_from_cargo_toml then
local group = vim.api.nvim_create_augroup('RustaceanCargoReloadWorkspace', { clear = false })
local bufnr = vim.api.nvim_get_current_buf()
vim.api.nvim_clear_autocmds {
buffer = bufnr,
group = group,
}
vim.api.nvim_create_autocmd('BufWritePost', {
buffer = vim.api.nvim_get_current_buf(),
group = group,
callback = function()
if #ra.get_active_rustaceanvim_clients(nil) > 0 then
vim.cmd.RustLsp { 'reloadWorkspace', mods = { silent = true } }
end
end,
})
end

View File

@ -0,0 +1,78 @@
rock_manifest = {
doc = {
["mason.txt"] = "858adffa88b1649b91187f49f191e376",
["rustaceanvim.txt"] = "0bb03192be9a68d7b6c73e8414b01625"
},
ftplugin = {
["rust.lua"] = "40f5f67ffb8f441a636ff231b7878ff8",
["rust.vim"] = "3121ffa3b7f1aa9c3efbfe32e438dbeb",
["toml.lua"] = "a8807dcfb5f6687ac543f4f8da3a9554"
},
lua = {
rustaceanvim = {
["cached_commands.lua"] = "6e23c8231f44a3edc76be54766fca334",
["cargo.lua"] = "39415eb623c1376d5f2d5c717b8c0eb0",
commands = {
["code_action_group.lua"] = "55bf0c83a007e5d64924eafe2e2a96eb",
["crate_graph.lua"] = "30d01abc92dff63ace83a5d1697d2073",
["debuggables.lua"] = "6548c5c600f14c9dc3d10e87983f6fb4",
["diagnostic.lua"] = "55160b50ac49bcab96e15176fc5c7dde",
["expand_macro.lua"] = "0a5991437c93aa39ace91d31dfd5d041",
["external_docs.lua"] = "1d2fa5d88ea197ad9976c23b0ff70763",
["fly_check.lua"] = "3f7b89700bd8f98fd5ab0ec02cce1cdd",
["hover_range.lua"] = "68828cfd0c465bbe0915996795ff7b92",
["init.lua"] = "6d7415da1c6e12b7b7914ab4e0339fae",
["join_lines.lua"] = "f31a68f1ddac01b926d58ccd249bc03d",
["move_item.lua"] = "f05118ba148e66c1e92882141e68343f",
["open_cargo_toml.lua"] = "2368251db4ce52659693acc8eee15b74",
["parent_module.lua"] = "c274b7357e35825f75802dc96e609992",
["rebuild_proc_macros.lua"] = "6113336c3b82042f60c54446e6c5a887",
["rustc_unpretty.lua"] = "fafde14d3d482667cecce2981444cad2",
["ssr.lua"] = "1c936709630f09b2a81d22452d3e3fa2",
["syntax_tree.lua"] = "c28fd8b22608e3f7bd3bdbb2e10c1617",
["view_ir.lua"] = "12eadc00a2ca1005bcbeda4fa8fc80a0",
["workspace_refresh.lua"] = "083b2d70533d0f417b65d2b7866f531b",
["workspace_symbol.lua"] = "69bdc6d1255f7eb586ea8e350b840b85"
},
["compat.lua"] = "fedaef1aeaceaf086116ed62b423d0a3",
config = {
["check.lua"] = "8a68a6e78ea97ab59ea0be83bff7ffc7",
["init.lua"] = "f2ef97efc1f563c06932d51006e5b7cc",
["internal.lua"] = "319f08833432f1ec7b04d4432737a1f0",
["json.lua"] = "a8c7caf443ad9ce1193726aec053cc8d",
["server.lua"] = "d642a65f31cb4f12b812840c1171821f"
},
["dap.lua"] = "757ecf822f6c30ebf9e462f7009ce99a",
executors = {
["background.lua"] = "ae1c76a5c2f73e9c58456f580b7552b7",
["init.lua"] = "a443891fc3b974db6787502179d85c19",
["meta.lua"] = "99cae6c6b77725710ec2fc7d86bff665",
["neotest.lua"] = "64a17e71f15a1741f0e7f835472f26e3",
["quickfix.lua"] = "b1ea55a261b8623e73c2fd6aed278fd3",
["termopen.lua"] = "4a225919ab4e1ad67fc591a9947d86e9",
["toggleterm.lua"] = "221616f53cc50124694fcc1b0f1c92a2",
["vimux.lua"] = "24d3f692083ca86f0926e7aa4722c0fe"
},
["health.lua"] = "126952700269c9ab976f29f59604c5da",
["hover_actions.lua"] = "d0842822fa99c55b4cc5a65eae968ff2",
["init.lua"] = "a48de2e7e937208adc6781a5ea35ec88",
["lsp.lua"] = "f26e69489b384870c2774703599bf1bb",
neotest = {
["init.lua"] = "10bfd8f06b7ad2de8d706c2e26505ae7",
["trans.lua"] = "1f79320bca0c81d91673543dd7ad4984"
},
["os.lua"] = "4c8caca1afb6a8e7862300b4dcc29f09",
["overrides.lua"] = "177b171bfb8c16c19326d6402b94a535",
["runnables.lua"] = "d458cb318b2cd900e178930d3b14f224",
["rust_analyzer.lua"] = "aa3bce32849886001dc2cb38a4ff951e",
["server_status.lua"] = "25017ef9b15fadd713574c897e242970",
["shell.lua"] = "561e4be6d5ba43f93a00ad5e6169e0eb",
["test.lua"] = "e436ae539217729f46a1d556974e1100",
types = {
["internal.lua"] = "95d9ff5763390a3efae83cc27dbef452"
},
["ui.lua"] = "185cbb4ef9b15b4feb6699fcbd5d4435"
}
},
["rustaceanvim-4.22.8-1.rockspec"] = "1d483e8dffa0275323da043a1e8c9cf3"
}

View File

@ -0,0 +1,40 @@
local git_ref = '4.22.8'
local modrev = '4.22.8'
local specrev = '1'
local repo_url = 'https://github.com/mrcjkb/rustaceanvim'
rockspec_format = '3.0'
package = 'rustaceanvim'
version = modrev ..'-'.. specrev
description = {
summary = 'Supercharge your Rust experience in Neovim! A heavily modified fork of rust-tools.nvim',
detailed = [[
This plugin automatically configures the rust-analyzer builtin LSP client
and integrates with other Rust tools. See the README's #features section
for more info.]],
labels = { 'dap', 'debug-adapter-protocol', 'language-server-protocol', 'lsp', 'neovim', 'nvim', 'plugin', 'rust', 'rust-analyzer', 'rust-lang', 'rust-tools' } ,
homepage = 'https://github.com/mrcjkb/rustaceanvim',
license = 'GPL-2.0'
}
dependencies = { 'lua >= 5.1' }
test_dependencies = { }
source = {
url = repo_url .. '/archive/' .. git_ref .. '.zip',
dir = 'rustaceanvim-' .. '4.22.8',
}
if modrev == 'scm' or modrev == 'dev' then
source = {
url = repo_url:gsub('https', 'git')
}
end
build = {
type = 'builtin',
copy_directories = { 'doc', 'ftplugin' } ,
}

View File

@ -0,0 +1,40 @@
local git_ref = '4.22.8'
local modrev = '4.22.8'
local specrev = '1'
local repo_url = 'https://github.com/mrcjkb/rustaceanvim'
rockspec_format = '3.0'
package = 'rustaceanvim'
version = modrev ..'-'.. specrev
description = {
summary = 'Supercharge your Rust experience in Neovim! A heavily modified fork of rust-tools.nvim',
detailed = [[
This plugin automatically configures the rust-analyzer builtin LSP client
and integrates with other Rust tools. See the README's #features section
for more info.]],
labels = { 'dap', 'debug-adapter-protocol', 'language-server-protocol', 'lsp', 'neovim', 'nvim', 'plugin', 'rust', 'rust-analyzer', 'rust-lang', 'rust-tools' } ,
homepage = 'https://github.com/mrcjkb/rustaceanvim',
license = 'GPL-2.0'
}
dependencies = { 'lua >= 5.1' }
test_dependencies = { }
source = {
url = repo_url .. '/archive/' .. git_ref .. '.zip',
dir = 'rustaceanvim-' .. '4.22.8',
}
if modrev == 'scm' or modrev == 'dev' then
source = {
url = repo_url:gsub('https', 'git')
}
end
build = {
type = 'builtin',
copy_directories = { 'doc', 'ftplugin' } ,
}