Skip to content

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

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

Merged
merged 8 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
141 changes: 112 additions & 29 deletions lua/rustaceanvim/lsp/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ local server_status = require('rustaceanvim.server_status')
local cargo = require('rustaceanvim.cargo')
local os = require('rustaceanvim.os')

---Local rustc targets cache
local rustc_targets_cache = nil

local function override_apply_text_edits()
local old_func = vim.lsp.util.apply_text_edits
---@diagnostic disable-next-line
Expand Down Expand Up @@ -93,6 +96,71 @@ local function configure_file_watcher(server_cfg)
end
end

---Handles retrieving rustc target archs and running on_valid callback
---to perform certain actions using the retrieved targets.
---@param on_valid function(rustc_targets)
local function validate_rustc_target(on_valid)
local on_exit = function(result)
-- use cache if available.
if rustc_targets_cache then
return on_valid(rustc_targets_cache)
end

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 on_valid(rustc_targets_cache)
end

-- Call vim.system with on_exit callback to avoid blocking Neovim's event loop.
vim.system({ 'rustc', '--print', 'target-list' }, { text = true }, on_exit)
end

---LSP restart internal implementations
---@param bufnr? number
---@param callback? function(client) Optional callback to run for each client before restarting.
---@return number|nil client_id
local function restart(bufnr, callback)
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 callback then
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 +317,50 @@ 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 nil(the current buffer's target if not provided).
M.set_target_arch = function(bufnr, target)
local on_update_target = function(client)
-- Get current user's rust-analyzer target
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

---on_valid callback handles the main functionality in changing system's arch
---by checking if rustc targets contains user's target or if user's provided target is nil.
---Notifies user on unrecognized target arch request
local on_valid = function(rustc_tgs)
if target == nil or rustc_tgs[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

return validate_rustc_target(on_valid)
end

restart(bufnr, on_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 +369,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 +385,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
Loading