From 1a965f4c1361d08d4d7a3ad3399aa33c8b964b2b Mon Sep 17 00:00:00 2001 From: Marc Jakobi Date: Thu, 17 Oct 2024 14:50:13 +0200 Subject: [PATCH] chore: fix caching in last refactor --- "\r" | 405 ++++++++++++++++++++++++++++++++++ lua/rustaceanvim/lsp/init.lua | 9 +- 2 files changed, 408 insertions(+), 6 deletions(-) create mode 100644 "\r" diff --git "a/\r" "b/\r" new file mode 100644 index 00000000..2147c385 --- /dev/null +++ "b/\r" @@ -0,0 +1,405 @@ +local M = {} +---@type rustaceanvim.Config +local config = require('rustaceanvim.config.internal') +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 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 + 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 vim.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(vim.fs.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 + +---HACK: Workaround for https://github.com/neovim/neovim/pull/28690 +--- to solve #423. +--- Checks if Neovim's file watcher is enabled, and if it isn't, +--- configures rust-analyzer to enable server-side file watching (if not configured otherwise). +--- +---@param server_cfg rustaceanvim.lsp.StartConfig LSP start settings +local function configure_file_watcher(server_cfg) + local is_client_file_watcher_enabled = + vim.tbl_get(server_cfg.capabilities, 'workspace', 'didChangeWatchedFiles', 'dynamicRegistration') + local file_watcher_setting = vim.tbl_get(server_cfg.settings, 'rust-analyzer', 'files', 'watcher') + if is_client_file_watcher_enabled and not file_watcher_setting then + server_cfg.settings = vim.tbl_deep_extend('force', server_cfg.settings, { + ['rust-analyzer'] = { + files = { + watcher = 'server', + }, + }, + }) + end +end + +---Handles retrieving rustc target architectures and running the passed in callback +---to perform certain actions using the retrieved targets. +---@param callback fun(targets: string[]) +local function with_rustc_target_architectures(callback) + if rustc_targets_cache then + return callback(rustc_targets_cache) + end + vim.system( + { 'rustc', '--print', 'target-list' }, + { text = true }, + ---@param result vim.SystemCompleted + function(result) + if result.code ~= 0 then + error('Failed to retrieve rustc targets: ' .. result.stderr) + end + rustc_targets_cache = vim.iter(result.stdout:gmatch('[^\r\n]+')):fold( + {}, + ---@param acc table + ---@param target string + function(acc, target) + acc[target] = true + return acc + end + ) + return callback(rustc_targets_cache) + end + ) +end + +---LSP restart internal implementations +---@param bufnr? number +---@param callback? fun(client: vim.lsp.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 +---@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 rustaceanvim.lsp.StartConfig + local lsp_start_config = vim.tbl_deep_extend('force', {}, client_config) + local root_dir = cargo.get_config_root_dir(client_config, 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) + configure_file_watcher(lsp_start_config) + + -- rust-analyzer treats settings in initializationOptions specially -- in particular, workspace_discoverConfig + -- so copy them to init_options (the vim name) + -- so they end up in initializationOptions (the LSP name) + -- ... and initialization_options (the rust name) in rust-analyzer's main.rs + lsp_start_config.init_options = vim.tbl_deep_extend( + 'force', + lsp_start_config.init_options or {}, + vim.tbl_get(lsp_start_config.settings, 'rust-analyzer') + ) + + -- 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 + if config.dap.autoload_configurations then + -- When switching projects, there might be new debuggables (#466) + require('rustaceanvim.commands.debuggables').add_dap_debuggables() + 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 vim.lsp.Client[] 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 vim.lsp.Client[] + for _, client in ipairs(clients) do + server_status.reset_client_state(client.id) + end + else + ---@cast clients vim.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 vim.lsp.Client[] 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 vim.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 + +---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) + ---@param client vim.lsp.Client + restart(bufnr, 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 + + with_rustc_target_architectures(function(rustc_targets) + if target == nil or rustc_targets[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) + end) +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) + M.restart(bufnr) +end + +---@enum RustAnalyzerCmd +local RustAnalyzerCmd = { + start = 'start', + 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() + elseif cmd == RustAnalyzerCmd.stop then + M.stop() + elseif cmd == RustAnalyzerCmd.restart then + 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, 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[] + 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 diff --git a/lua/rustaceanvim/lsp/init.lua b/lua/rustaceanvim/lsp/init.lua index bf78d3dc..2147c385 100644 --- a/lua/rustaceanvim/lsp/init.lua +++ b/lua/rustaceanvim/lsp/init.lua @@ -100,19 +100,17 @@ end ---to perform certain actions using the retrieved targets. ---@param callback fun(targets: string[]) local function with_rustc_target_architectures(callback) + if rustc_targets_cache then + return callback(rustc_targets_cache) + end vim.system( { 'rustc', '--print', 'target-list' }, { text = true }, ---@param result vim.SystemCompleted function(result) - if rustc_targets_cache then - return callback(rustc_targets_cache) - end - if result.code ~= 0 then error('Failed to retrieve rustc targets: ' .. result.stderr) end - rustc_targets_cache = vim.iter(result.stdout:gmatch('[^\r\n]+')):fold( {}, ---@param acc table @@ -122,7 +120,6 @@ local function with_rustc_target_architectures(callback) return acc end ) - return callback(rustc_targets_cache) end )