diff --git a/lua/rustaceanvim/cargo.lua b/lua/rustaceanvim/cargo.lua index 487e128d..6207378c 100644 --- a/lua/rustaceanvim/cargo.lua +++ b/lua/rustaceanvim/cargo.lua @@ -29,92 +29,98 @@ end ---client root is found, returns the result of evaluating `config.root_dir`. ---@param config rustaceanvim.lsp.ClientConfig ---@param file_name string ----@return string | nil root_dir -function cargo.get_config_root_dir(config, file_name) +---@param callback fun(root_dir: string?) +function cargo.get_config_root_dir(config, file_name, callback) local reuse_active = get_mb_active_client_root(file_name) if reuse_active then - return reuse_active + return callback(reuse_active) end local config_root_dir = config.root_dir if type(config_root_dir) == 'function' then - return config_root_dir(file_name, cargo.get_root_dir) + config_root_dir(file_name, callback, function(file_name_) + cargo.get_root_dir(file_name_, callback) + end) else - return config_root_dir + callback(config_root_dir) end end ---@param path string The directory to search upward from ----@return string? cargo_crate_dir ----@return table? cargo_metadata -local function get_cargo_metadata(path) +---@param callback fun(cargo_crate_dir: string?, cargo_metadata: table?) +local function get_cargo_metadata(path, callback) ---@diagnostic disable-next-line: missing-fields local cargo_crate_dir = vim.fs.dirname(vim.fs.find({ 'Cargo.toml' }, { upward = true, path = path, })[1]) if vim.fn.executable('cargo') ~= 1 then - return cargo_crate_dir + return callback(cargo_crate_dir) end local cmd = { 'cargo', 'metadata', '--no-deps', '--format-version', '1' } if cargo_crate_dir ~= nil then cmd[#cmd + 1] = '--manifest-path' cmd[#cmd + 1] = vim.fs.joinpath(cargo_crate_dir, 'Cargo.toml') end - local sc = vim - .system(cmd, { - cwd = vim.uv.fs_stat(path) and path or cargo_crate_dir or vim.fn.getcwd(), - }) - :wait() - if sc.code ~= 0 then - return cargo_crate_dir - end - local ok, cargo_metadata_json = pcall(vim.fn.json_decode, sc.stdout) - if ok and cargo_metadata_json then - return cargo_crate_dir, cargo_metadata_json - end - return cargo_crate_dir + vim.uv.fs_stat(path, function(_, stat) + vim.system(cmd, { + cwd = stat and path or cargo_crate_dir or vim.fn.getcwd(), + }, function(sc) + if sc.code ~= 0 then + return callback(cargo_crate_dir) + end + local ok, cargo_metadata_json = pcall(vim.fn.json_decode, sc.stdout) + if ok and cargo_metadata_json then + return callback(cargo_crate_dir, cargo_metadata_json) + end + return callback(cargo_crate_dir) + end) + end) end ---@param buf_name? string ----@return string edition -function cargo.get_rustc_edition(buf_name) +---@param callback fun(edition: string) +function cargo.get_rustc_edition(buf_name, callback) local config = require('rustaceanvim.config.internal') ---@diagnostic disable-next-line: undefined-field if config.tools.rustc.edition then vim.deprecate('vim.g.rustaceanvim.config.tools.edition', 'default_edition', '6.0.0', 'rustaceanvim') ---@diagnostic disable-next-line: undefined-field - return config.tools.rustc.edition + callback(config.tools.rustc.edition) end buf_name = buf_name or vim.api.nvim_buf_get_name(0) local path = vim.fs.dirname(buf_name) - local _, cargo_metadata = get_cargo_metadata(path) - local default_edition = config.tools.rustc.default_edition - if not cargo_metadata then - return default_edition - end - local package = vim.iter(cargo_metadata.packages or {}):find(function(pkg) - return type(pkg.edition) == 'string' + get_cargo_metadata(path, function(_, cargo_metadata) + local default_edition = config.tools.rustc.default_edition + if not cargo_metadata then + return callback(default_edition) + end + local package = vim.iter(cargo_metadata.packages or {}):find(function(pkg) + return type(pkg.edition) == 'string' + end) + callback(package and package.edition or default_edition) end) - return package and package.edition or default_edition end ---The default implementation used for `vim.g.rustaceanvim.server.root_dir` ---@param file_name string ----@return string | nil root_dir -function cargo.get_root_dir(file_name) +---@param callback fun(root_dir: string | nil) +function cargo.get_root_dir(file_name, callback) local path = file_name:find('%.rs$') and vim.fs.dirname(file_name) or file_name if not path then return nil end - local cargo_crate_dir, cargo_metadata = get_cargo_metadata(path) - return cargo_metadata and cargo_metadata.workspace_root - or cargo_crate_dir - ---@diagnostic disable-next-line: missing-fields - or vim.fs.dirname(vim.fs.find({ 'rust-project.json' }, { - upward = true, - path = path, - })[1]) + get_cargo_metadata(path, function(cargo_crate_dir, cargo_metadata) + callback( + cargo_metadata and cargo_metadata.workspace_root + or cargo_crate_dir + ---@diagnostic disable-next-line: missing-fields + or vim.fs.dirname(vim.fs.find({ 'rust-project.json' }, { + upward = true, + path = path, + })[1]) + ) + end) end return cargo diff --git a/lua/rustaceanvim/commands/rustc_unpretty.lua b/lua/rustaceanvim/commands/rustc_unpretty.lua index 73b51c9e..90da2ea1 100644 --- a/lua/rustaceanvim/commands/rustc_unpretty.lua +++ b/lua/rustaceanvim/commands/rustc_unpretty.lua @@ -135,18 +135,20 @@ function M.rustc_unpretty(level) end text = table.concat(b, '\n') - vim.system({ - rustc, - '--crate-type', - 'lib', - '--edition', - cargo.get_rustc_edition(), - '-Z', - 'unstable-options', - '-Z', - 'unpretty=' .. level, - '-', - }, { stdin = text }, vim.schedule_wrap(handler)) + cargo.get_rustc_edition(nil, function(edition) + vim.system({ + rustc, + '--crate-type', + 'lib', + '--edition', + edition, + '-Z', + 'unstable-options', + '-Z', + 'unpretty=' .. level, + '-', + }, { stdin = text }, vim.schedule_wrap(handler)) + end) end return M diff --git a/lua/rustaceanvim/config/internal.lua b/lua/rustaceanvim/config/internal.lua index fdb96cdf..af6e4096 100644 --- a/lua/rustaceanvim/config/internal.lua +++ b/lua/rustaceanvim/config/internal.lua @@ -286,7 +286,7 @@ local RustaceanDefaultConfig = { return { 'rust-analyzer', '--log-file', RustaceanConfig.server.logfile } end, - ---@type string | fun(filename: string, default: fun(filename: string):string|nil):string|nil + ---@type string | fun(filename: string, callback: fun(root_dir: string|nil), default: fun(filename: string):string|nil) root_dir = cargo.get_root_dir, ra_multiplex = { diff --git a/lua/rustaceanvim/lsp/init.lua b/lua/rustaceanvim/lsp/init.lua index 7b6e8ace..75dea951 100644 --- a/lua/rustaceanvim/lsp/init.lua +++ b/lua/rustaceanvim/lsp/init.lua @@ -158,156 +158,162 @@ end --- 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) +---@param callback? fun(client_id: integer?) +M.start = function(bufnr, callback) bufnr = bufnr or vim.api.nvim_get_current_buf() + callback = callback or function() end 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 - vim.notify( - [[ + cargo.get_config_root_dir( + client_config, + bufname, + vim.schedule_wrap(function(root_dir) + if not root_dir then + vim.notify( + [[ rustaceanvim: No project root found. Starting rust-analyzer client in detached/standalone mode (with reduced functionality). ]], - vim.log.levels.INFO - ) - 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) - - -- 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 = {}, - }, - } - compat.client_notify(client, 'workspace/didChangeWorkspaceFolders', params) - if not client.workspace_folders then - client.workspace_folders = {} + vim.log.levels.INFO + ) + 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) + + -- 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 = {}, + }, + } + compat.client_notify(client, '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 callback() + end 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) + local rust_analyzer_cmd = types.evaluate(client_config.cmd) - local ra_multiplex = lsp_start_config.ra_multiplex - if ra_multiplex.enable then - local ok, running_ra_multiplex = pcall(function() - local result = vim.system({ 'pgrep', 'ra-multiplex' }):wait().code - return result == 0 - end) - if ok and running_ra_multiplex then - rust_analyzer_cmd = vim.lsp.rpc.connect(ra_multiplex.host, ra_multiplex.port) - local ra_settings = lsp_start_config.settings['rust-analyzer'] or {} - ra_settings.lspMux = ra_settings.lspMux - or { - version = '1', - method = 'connect', - server = 'rust-analyzer', - } - lsp_start_config.settings['rust-analyzer'] = ra_settings - end - end + local ra_multiplex = lsp_start_config.ra_multiplex + if ra_multiplex.enable then + local ok, running_ra_multiplex = pcall(function() + local result = vim.system({ 'pgrep', 'ra-multiplex' }):wait().code + return result == 0 + end) + if ok and running_ra_multiplex then + rust_analyzer_cmd = vim.lsp.rpc.connect(ra_multiplex.host, ra_multiplex.port) + local ra_settings = lsp_start_config.settings['rust-analyzer'] or {} + ra_settings.lspMux = ra_settings.lspMux + or { + version = '1', + method = 'connect', + server = 'rust-analyzer', + } + lsp_start_config.settings['rust-analyzer'] = ra_settings + end + end - -- special case: rust-analyzer has a `rust-analyzer.server.path` config option - -- that allows you to override the path via .vscode/settings.json - local server_path = vim.tbl_get(lsp_start_config.settings, 'rust-analyzer', 'server', 'path') - if type(server_path) == 'string' then - if type(rust_analyzer_cmd) == 'table' then - rust_analyzer_cmd[1] = server_path - else - rust_analyzer_cmd = { server_path } - end - -- - end - if type(rust_analyzer_cmd) == 'table' then - if #rust_analyzer_cmd == 0 then - vim.schedule(function() - vim.notify('rust-analyzer command is not set!', vim.log.levels.ERROR) - end) - return - end - if vim.fn.executable(rust_analyzer_cmd[1]) ~= 1 then - vim.schedule(function() - vim.notify(('%s is not executable'):format(rust_analyzer_cmd[1]), vim.log.levels.ERROR) - end) - return - end - end - ---@cast rust_analyzer_cmd string[] - lsp_start_config.cmd = rust_analyzer_cmd - lsp_start_config.name = 'rust-analyzer' - lsp_start_config.filetypes = { 'rust' } + -- special case: rust-analyzer has a `rust-analyzer.server.path` config option + -- that allows you to override the path via .vscode/settings.json + local server_path = vim.tbl_get(lsp_start_config.settings, 'rust-analyzer', 'server', 'path') + if type(server_path) == 'string' then + if type(rust_analyzer_cmd) == 'table' then + rust_analyzer_cmd[1] = server_path + else + rust_analyzer_cmd = { server_path } + end + -- + end + if type(rust_analyzer_cmd) == 'table' then + if #rust_analyzer_cmd == 0 then + vim.schedule(function() + vim.notify('rust-analyzer command is not set!', vim.log.levels.ERROR) + end) + return callback() + end + if vim.fn.executable(rust_analyzer_cmd[1]) ~= 1 then + vim.schedule(function() + vim.notify(('%s is not executable'):format(rust_analyzer_cmd[1]), vim.log.levels.ERROR) + end) + return callback() + end + 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 + 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 + 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 {}) + 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 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_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 + 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 - -- 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') + -- 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') + ) + + callback(vim.lsp.start(lsp_start_config)) + end) ) - - return vim.lsp.start(lsp_start_config) end ---Stop the LSP client.