Skip to content

Commit

Permalink
feat(lsp): target architecture switching command for RustAnalyzer (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
borngraced authored Oct 17, 2024
1 parent 4e78e8d commit 95715b2
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 29 deletions.
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

0 comments on commit 95715b2

Please sign in to comment.