Skip to content

Commit 3015cf3

Browse files
Marc Jakobimrcjkb
authored andcommitted
feat: neotest adapter
1 parent c3fcb96 commit 3015cf3

File tree

15 files changed

+536
-124
lines changed

15 files changed

+536
-124
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ All notable changes to this project will be documented in this file.
66
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
77
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
88

9+
## [4.4.0] - 2024-01-31
10+
11+
### Added
12+
13+
- You can now register a `rustaceanvim.neotest` adapter with [neotest](https://github.com/nvim-neotest/neotest).
14+
It will query rust-analyzer for test positions and test commands in any
15+
buffer to which the LSP client is attached.
16+
If you do so, `tools.test_executor` will default to a new `'neotest'`
17+
executor, which will use neotest to run `testables` or `runnables` that are tests.
18+
919
## [4.3.0] - 2024-01-31
1020

1121
### Changed

lua/rustaceanvim/cargo.lua

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
local compat = require('rustaceanvim.compat')
2+
local rust_analyzer = require('rustaceanvim.rust_analyzer')
3+
local joinpath = compat.joinpath
4+
5+
local cargo = {}
6+
7+
---Checks if there is an active client for file_name and returns its root directory if found.
8+
---@param file_name string
9+
---@return string | nil root_dir The root directory of the active client for file_name (if there is one)
10+
local function get_mb_active_client_root(file_name)
11+
---@diagnostic disable-next-line: missing-parameter
12+
local cargo_home = compat.uv.os_getenv('CARGO_HOME') or joinpath(vim.env.HOME, '.cargo')
13+
local registry = joinpath(cargo_home, 'registry', 'src')
14+
15+
---@diagnostic disable-next-line: missing-parameter
16+
local rustup_home = compat.uv.os_getenv('RUSTUP_HOME') or joinpath(vim.env.HOME, '.rustup')
17+
local toolchains = joinpath(rustup_home, 'toolchains')
18+
19+
for _, item in ipairs { toolchains, registry } do
20+
if file_name:sub(1, #item) == item then
21+
local clients = rust_analyzer.get_active_rustaceanvim_clients()
22+
return clients and #clients > 0 and clients[#clients].config.root_dir or nil
23+
end
24+
end
25+
end
26+
27+
---@param file_name string
28+
---@return string | nil root_dir
29+
function cargo.get_root_dir(file_name)
30+
local reuse_active = get_mb_active_client_root(file_name)
31+
if reuse_active then
32+
return reuse_active
33+
end
34+
local cargo_crate_dir = vim.fs.dirname(vim.fs.find({ 'Cargo.toml' }, {
35+
upward = true,
36+
path = vim.fs.dirname(file_name),
37+
})[1])
38+
local cargo_workspace_dir = nil
39+
if vim.fn.executable('cargo') == 1 then
40+
local cmd = { 'cargo', 'metadata', '--no-deps', '--format-version', '1' }
41+
if cargo_crate_dir ~= nil then
42+
cmd[#cmd + 1] = '--manifest-path'
43+
cmd[#cmd + 1] = joinpath(cargo_crate_dir, 'Cargo.toml')
44+
end
45+
local cargo_metadata = ''
46+
local cm = vim.fn.jobstart(cmd, {
47+
on_stdout = function(_, d, _)
48+
cargo_metadata = table.concat(d, '\n')
49+
end,
50+
stdout_buffered = true,
51+
})
52+
if cm > 0 then
53+
cm = vim.fn.jobwait({ cm })[1]
54+
else
55+
cm = -1
56+
end
57+
if cm == 0 then
58+
cargo_workspace_dir = vim.fn.json_decode(cargo_metadata)['workspace_root']
59+
---@cast cargo_workspace_dir string
60+
end
61+
end
62+
return cargo_workspace_dir
63+
or cargo_crate_dir
64+
or vim.fs.dirname(vim.fs.find({ 'rust-project.json' }, {
65+
upward = true,
66+
path = vim.fs.dirname(file_name),
67+
})[1])
68+
end
69+
70+
return cargo

lua/rustaceanvim/compat.lua

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,32 @@ M.get_clients = vim.lsp.get_clients or vim.lsp.get_active_clients
1111

1212
M.uv = vim.uv or vim.loop
1313

14+
--- @enum vim.diagnostic.Severity
15+
M.severity = {
16+
ERROR = 1,
17+
WARN = 2,
18+
INFO = 3,
19+
HINT = 4,
20+
[1] = 'ERROR',
21+
[2] = 'WARN',
22+
[3] = 'INFO',
23+
[4] = 'HINT',
24+
}
25+
26+
--- @class vim.Diagnostic
27+
--- @field bufnr? integer
28+
--- @field lnum integer 0-indexed
29+
--- @field end_lnum? integer 0-indexed
30+
--- @field col integer 0-indexed
31+
--- @field end_col? integer 0-indexed
32+
--- @field severity? vim.diagnostic.Severity
33+
--- @field message string
34+
--- @field source? string
35+
--- @field code? string
36+
--- @field _tags? { deprecated: boolean, unnecessary: boolean}
37+
--- @field user_data? any arbitrary data plugins can add
38+
--- @field namespace? integer
39+
1440
--- @class vim.SystemCompleted
1541
--- @field code integer
1642
--- @field signal integer

lua/rustaceanvim/config/init.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ vim.g.rustaceanvim = vim.g.rustaceanvim
7272
---@class RustaceanExecutorOpts
7373
---@field bufnr? integer The buffer from which the executor was invoked.
7474

75-
---@alias executor_alias 'termopen' | 'quickfix' | 'toggleterm' | 'vimux'
75+
---@alias executor_alias 'termopen' | 'quickfix' | 'toggleterm' | 'vimux' | 'neotest'
7676

7777
---@alias test_executor_alias executor_alias | 'background'
7878

lua/rustaceanvim/config/internal.lua

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ local function is_lldb_adapter(adapter)
2929
return adapter.type == 'executable'
3030
end
3131

32+
---@return RustaceanExecutor
33+
local function get_test_executor()
34+
if package.loaded['rustaceanvim.neotest'] ~= nil then
35+
-- neotest has been set up with rustaceanvim as an adapter
36+
return executors.neotest
37+
elseif vim.fn.has('nvim-0.10.0') == 1 then
38+
return executors.background
39+
else
40+
return executors.termopen
41+
end
42+
end
43+
3244
---@class RustaceanConfig
3345
local RustaceanDefaultConfig = {
3446
---@class RustaceanToolsConfig
@@ -40,7 +52,7 @@ local RustaceanDefaultConfig = {
4052
executor = executors.termopen,
4153

4254
---@type RustaceanExecutor
43-
test_executor = vim.fn.has('nvim-0.10.0') == 1 and executors.background or executors.termopen,
55+
test_executor = get_test_executor(),
4456

4557
--- callback to execute once rust-analyzer is done initializing the workspace
4658
--- The callback receives one parameter indicating the `health` of the server: "ok" | "warning" | "error"

lua/rustaceanvim/executors/background.lua

Lines changed: 3 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -10,52 +10,8 @@ end
1010
---@diagnostic disable-next-line: missing-fields
1111
local M = {}
1212

13-
---@package
14-
---@param file_name string
15-
---@param output string
16-
---@return vim.Diagnostic[]
17-
---@diagnostic disable-next-line: inject-field
18-
M.parse_diagnostics = function(file_name, output)
19-
output = output:gsub('\r\n', '\n')
20-
local lines = vim.split(output, '\n')
21-
---@type vim.Diagnostic[]
22-
local diagnostics = {}
23-
for i, line in ipairs(lines) do
24-
local message = ''
25-
local file, lnum, col = line:match("thread '[^']+' panicked at ([^:]+):(%d+):(%d+):")
26-
if lnum and col and message and vim.endswith(file_name, file) then
27-
local next_i = i + 1
28-
while #lines >= next_i and lines[next_i] ~= '' do
29-
message = message .. lines[next_i] .. '\n'
30-
next_i = next_i + 1
31-
end
32-
local diagnostic = {
33-
lnum = tonumber(lnum) - 1,
34-
col = tonumber(col) or 0,
35-
message = message,
36-
source = 'rustaceanvim',
37-
severity = vim.diagnostic.severity.ERROR,
38-
}
39-
table.insert(diagnostics, diagnostic)
40-
end
41-
end
42-
if #diagnostics == 0 then
43-
--- Fall back to old format
44-
for message, file, lnum, col in output:gmatch("thread '[^']+' panicked at '([^']+)', ([^:]+):(%d+):(%d+)") do
45-
if vim.endswith(file_name, file) then
46-
local diagnostic = {
47-
lnum = tonumber(lnum) - 1,
48-
col = tonumber(col) or 0,
49-
message = message,
50-
source = 'rustaceanvim',
51-
severity = vim.diagnostic.severity.ERROR,
52-
}
53-
table.insert(diagnostics, diagnostic)
54-
end
55-
end
56-
end
57-
return diagnostics
58-
end
13+
---@class rustaceanvim.Diagnostic: vim.Diagnostic
14+
---@field test_id string
5915

6016
M.execute_command = function(command, args, cwd, opts)
6117
---@type RustaceanExecutorOpts
@@ -83,7 +39,7 @@ M.execute_command = function(command, args, cwd, opts)
8339
return
8440
end
8541
local output = (sc.stderr or '') .. '\n' .. (sc.stdout or '')
86-
local diagnostics = M.parse_diagnostics(fname, output)
42+
local diagnostics = require('rustaceanvim.test').parse_diagnostics(fname, output)
8743
local summary = get_test_summary(sc.stdout or '')
8844
vim.schedule(function()
8945
vim.diagnostic.set(diag_namespace, opts.bufnr, diagnostics)

lua/rustaceanvim/executors/init.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ local quickfix = require('rustaceanvim.executors.quickfix')
55
local toggleterm = require('rustaceanvim.executors.toggleterm')
66
local vimux = require('rustaceanvim.executors.vimux')
77
local background = require('rustaceanvim.executors.background')
8+
local neotest = require('rustaceanvim.executors.neotest')
89

910
---@type { [test_executor_alias]: RustaceanExecutor }
1011
local M = {}
@@ -14,5 +15,6 @@ M.quickfix = quickfix
1415
M.toggleterm = toggleterm
1516
M.vimux = vimux
1617
M.background = background
18+
M.neotest = neotest
1719

1820
return M
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
error('Cannot import a meta module')
2+
3+
---@class RustaceanTestExecutor: RustaceanExecutor
4+
---@field execute_command fun(cmd:string, args:string[], cwd:string|nil, opts?: RustaceanExecutorOpts)
5+
6+
---@class RustaceanTestExecutorOpts: RustaceanExecutorOpts
7+
---@field runnable? RARunnable
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
local trans = require('rustaceanvim.neotest.trans')
2+
3+
---@type RustaceanTestExecutor
4+
---@diagnostic disable-next-line: missing-fields
5+
local M = {}
6+
7+
---@param opts RustaceanTestExecutorOpts
8+
M.execute_command = function(_, _, _, opts)
9+
---@type RustaceanTestExecutorOpts
10+
opts = vim.tbl_deep_extend('force', { bufnr = 0 }, opts or {})
11+
if type(opts.runnable) ~= 'table' then
12+
vim.notify('rustaceanvim neotest executor called without a runnable. This is a bug!', vim.log.levels.ERROR)
13+
end
14+
local file = vim.api.nvim_buf_get_name(opts.bufnr)
15+
local pos_id = trans.get_position_id(file, opts.runnable)
16+
---@diagnostic disable-next-line: undefined-field
17+
require('neotest').run.run(pos_id)
18+
end
19+
20+
return M

lua/rustaceanvim/lsp.lua

Lines changed: 2 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ local compat = require('rustaceanvim.compat')
55
local types = require('rustaceanvim.types.internal')
66
local rust_analyzer = require('rustaceanvim.rust_analyzer')
77
local server_status = require('rustaceanvim.server_status')
8-
local joinpath = compat.joinpath
8+
local cargo = require('rustaceanvim.cargo')
99

1010
local function override_apply_text_edits()
1111
local old_func = vim.lsp.util.apply_text_edits
@@ -17,69 +17,6 @@ local function override_apply_text_edits()
1717
end
1818
end
1919

20-
---Checks if there is an active client for file_name and returns its root directory if found.
21-
---@param file_name string
22-
---@return string | nil root_dir The root directory of the active client for file_name (if there is one)
23-
local function get_mb_active_client_root(file_name)
24-
---@diagnostic disable-next-line: missing-parameter
25-
local cargo_home = compat.uv.os_getenv('CARGO_HOME') or joinpath(vim.env.HOME, '.cargo')
26-
local registry = joinpath(cargo_home, 'registry', 'src')
27-
28-
---@diagnostic disable-next-line: missing-parameter
29-
local rustup_home = compat.uv.os_getenv('RUSTUP_HOME') or joinpath(vim.env.HOME, '.rustup')
30-
local toolchains = joinpath(rustup_home, 'toolchains')
31-
32-
for _, item in ipairs { toolchains, registry } do
33-
if file_name:sub(1, #item) == item then
34-
local clients = rust_analyzer.get_active_rustaceanvim_clients()
35-
return clients and #clients > 0 and clients[#clients].config.root_dir or nil
36-
end
37-
end
38-
end
39-
40-
---@param file_name string
41-
---@return string | nil root_dir
42-
local function get_root_dir(file_name)
43-
local reuse_active = get_mb_active_client_root(file_name)
44-
if reuse_active then
45-
return reuse_active
46-
end
47-
local cargo_crate_dir = vim.fs.dirname(vim.fs.find({ 'Cargo.toml' }, {
48-
upward = true,
49-
path = vim.fs.dirname(file_name),
50-
})[1])
51-
local cargo_workspace_dir = nil
52-
if vim.fn.executable('cargo') == 1 then
53-
local cmd = { 'cargo', 'metadata', '--no-deps', '--format-version', '1' }
54-
if cargo_crate_dir ~= nil then
55-
cmd[#cmd + 1] = '--manifest-path'
56-
cmd[#cmd + 1] = joinpath(cargo_crate_dir, 'Cargo.toml')
57-
end
58-
local cargo_metadata = ''
59-
local cm = vim.fn.jobstart(cmd, {
60-
on_stdout = function(_, d, _)
61-
cargo_metadata = table.concat(d, '\n')
62-
end,
63-
stdout_buffered = true,
64-
})
65-
if cm > 0 then
66-
cm = vim.fn.jobwait({ cm })[1]
67-
else
68-
cm = -1
69-
end
70-
if cm == 0 then
71-
cargo_workspace_dir = vim.fn.json_decode(cargo_metadata)['workspace_root']
72-
---@cast cargo_workspace_dir string
73-
end
74-
end
75-
return cargo_workspace_dir
76-
or cargo_crate_dir
77-
or vim.fs.dirname(vim.fs.find({ 'rust-project.json' }, {
78-
upward = true,
79-
path = vim.fs.dirname(file_name),
80-
})[1])
81-
end
82-
8320
---@param client lsp.Client
8421
---@param root_dir string
8522
---@return boolean
@@ -131,7 +68,7 @@ M.start = function(bufnr)
13168
local client_config = config.server
13269
---@type LspStartConfig
13370
local lsp_start_config = vim.tbl_deep_extend('force', {}, client_config)
134-
local root_dir = get_root_dir(vim.api.nvim_buf_get_name(bufnr))
71+
local root_dir = cargo.get_root_dir(vim.api.nvim_buf_get_name(bufnr))
13572
root_dir = root_dir and normalize_path(root_dir)
13673
lsp_start_config.root_dir = root_dir
13774
if not root_dir then

0 commit comments

Comments
 (0)