Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lsp): target architecture switching command for RustAnalyzer #541

Merged
merged 8 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/rustaceanvim.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ Commands:
':RustAnalyzer stop' - Stop the LSP client.
':RustAnalyzer restart' - Restart the LSP client.
':RustAnalyzer reloadSettings' - Reload settings for the LSP client.
':RustAnalyzer target <target_arch>' - Set the target architecture for the LSP client.

The ':RustAnalyzer target' command can take a valid rustc target, such as 'wasm32-unknown-unknown', or it can be left empty to set the LSP client to use the default target architecture for the operating system.

The ':RustLsp[!]' command is available after the LSP client has initialized.
It accepts the following subcommands:
Expand Down
5 changes: 5 additions & 0 deletions lua/rustaceanvim/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
--- ':RustAnalyzer stop' - Stop the LSP client.
--- ':RustAnalyzer restart' - Restart the LSP client.
--- ':RustAnalyzer reloadSettings' - Reload settings for the LSP client.
--- ':RustAnalyzer target <target_arch>' - Set the target architecture for the LSP client.

--- The ':RustAnalyzer target' command can take a valid rustc target,
--- such as 'wasm32-unknown-unknown', or it can be left empty to set the LSP client
--- to use the default target architecture for the operating system.
---
---The ':RustLsp[!]' command is available after the LSP client has initialized.
---It accepts the following subcommands:
Expand Down
106 changes: 77 additions & 29 deletions lua/rustaceanvim/lsp/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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 targets = require('rustaceanvim.lsp.targets')

local function override_apply_text_edits()
local old_func = vim.lsp.util.apply_text_edits
Expand Down Expand Up @@ -93,6 +94,45 @@ local function configure_file_watcher(server_cfg)
end
end

---LSP restart ininer implementations
borngraced marked this conversation as resolved.
Show resolved Hide resolved
---@param bufnr? number
---@param set_target_callback? function(client) Optional callback to run for each client before restarting.
---@return number|nil client_id
local function restart(bufnr, set_target_callback)
borngraced marked this conversation as resolved.
Show resolved Hide resolved
bufnr = bufnr or vim.api.nvim_get_current_buf()
local clients = M.stop(bufnr)
local timer, _, _ = vim.uv.new_timer()
if not timer then
vim.notify('Failed to init timer for LSP client restart.', vim.log.levels.ERROR)
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()
-- Execute the callback, if provided, for additional actions before restarting
if set_target_callback then
set_target_callback(client)
end
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

---@class rustaceanvim.lsp.StartConfig: rustaceanvim.lsp.ClientConfig
---@field root_dir string | nil
---@field init_options? table
Expand Down Expand Up @@ -249,39 +289,43 @@ M.reload_settings = function(bufnr)
return clients
end

---Updates the target architecture setting for the LSP client associated with the given buffer.
---@param bufnr? number The buffer number, defaults to the current buffer
---@param target? string The target architecture. Defaults to the current buffer's target if not provided.
M.set_target_arch = function(bufnr, target)
local function update_target(client)
-- Get the current target from the client's settings
local current_target = vim.tbl_get(client, 'config', 'settings', 'rust-analyzer', 'cargo', 'target')

if not target then
if not current_target then
vim.notify('Using default OS target architecture.', vim.log.levels.INFO)
else
vim.notify('Target architecture is already set to the default OS target.', vim.log.levels.INFO)
end
return
end

if targets.target_is_valid_rustc_target(target) then
client.settings['rust-analyzer'].cargo.target = target
client.notify('workspace/didChangeConfiguration', { settings = client.config.settings })
vim.notify('Target architecture updated successfully to: ' .. target, vim.log.levels.INFO)
return
else
vim.notify('Invalid target architecture provided: ' .. tostring(target), vim.log.levels.ERROR)
return
end
end

restart(bufnr, update_target)
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, _, _ = vim.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)
M.restart(bufnr)
end

---@enum RustAnalyzerCmd
Expand All @@ -290,11 +334,13 @@ local RustAnalyzerCmd = {
stop = 'stop',
restart = 'restart',
reload_settings = 'reloadSettings',
target = 'target',
}

local function rust_analyzer_cmd(opts)
local fargs = opts.fargs
local cmd = fargs[1]
local arch = fargs[2]
---@cast cmd RustAnalyzerCmd
if cmd == RustAnalyzerCmd.start then
M.start()
Expand All @@ -304,12 +350,14 @@ local function rust_analyzer_cmd(opts)
M.restart()
elseif cmd == RustAnalyzerCmd.reload_settings then
M.reload_settings()
elseif cmd == RustAnalyzerCmd.target then
M.set_target_arch(nil, arch)
end
end

vim.api.nvim_create_user_command('RustAnalyzer', rust_analyzer_cmd, {
nargs = '+',
desc = 'Starts or stops the rust-analyzer LSP client',
desc = 'Starts, stops the rust-analyzer LSP client or changes the target',
complete = function(arg_lead, cmdline, _)
local clients = rust_analyzer.get_active_rustaceanvim_clients()
---@type RustAnalyzerCmd[]
Expand Down
43 changes: 43 additions & 0 deletions lua/rustaceanvim/lsp/targets.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
local T = {}

local rustc_targets_cache = nil

--- Get rustc targets, use cache if available
---@return table<string, boolean>
local function get_rustc_targets()
if rustc_targets_cache then
return rustc_targets_cache
end

local result = vim.system({ 'rustc', '--print', 'target-list' }):wait()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: vim.system():wait() blocks Neovim's event loop.
It's generally better to use vim.system and pass in a callback, which is a function(result) where result is a vim.SystemCompleted.

This means that this function has to be changed, e.g. to with_rustc_targets(callback: fun(targets)).
It's a bit of a refactor, but I think it's doable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

if result.code ~= 0 then
error('Failed to retrieve rustc targets: ' .. result.stderr)
end

rustc_targets_cache = {}
for line in result.stdout:gmatch('[^\r\n]+') do
rustc_targets_cache[line] = true
end

return rustc_targets_cache
end

--- Validates if the provided target is a valid Rust compiler (rustc) target.
--- If no target is provided, it defaults to the system's architecture.
---@param target? string
---@return boolean
function T.target_is_valid_rustc_target(target)
borngraced marked this conversation as resolved.
Show resolved Hide resolved
if target == nil then
return true
end

local success, targets = pcall(get_rustc_targets)
if not success then
vim.notify('Error retrieving rustc targets: ' .. tostring(targets), vim.log.levels.ERROR)
return false
end

return targets[target] or false
end

return T
Loading